Compare commits

..

31 Commits

Author SHA1 Message Date
philippe44
be7fb4b669 Merge branch 'master-v4.3' of https://github.com/sle118/squeezelite-esp32 into master-v4.3 2023-12-31 18:50:29 -08:00
philippe44
3554af1460 add vorbis/ogg live metadata (and fix some ogg issues) - release 2023-12-31 18:50:25 -08:00
Sébastien
02d422c1f4 update workflow dependency 2023-12-28 09:11:46 -05:00
github-actions
343b728323 Update prebuilt objects [skip actions] 2023-12-28 13:56:07 +00:00
philippe44
9679c5c104 fix flush mode 2023-12-27 19:46:16 -08:00
philippe44
5c90086bbd sync with upstream/cspot 2023-11-21 12:52:55 -08:00
philippe44
807a0e547a airplay improvements 2023-11-19 22:57:21 -08:00
philippe44
1d54331224 Merge branch 'master-v4.3' of https://github.com/sle118/squeezelite-esp32 into master-v4.3 2023-11-16 22:52:23 -08:00
philippe44
e85a3cddf1 GDS optimization 2023-11-16 22:52:18 -08:00
github-actions
d4d97d5a60 Update prebuilt objects [skip actions] 2023-11-17 05:45:59 +00:00
philippe44
2afbee7cb1 add SH1122 - release 2023-11-16 21:43:56 -08:00
philippe44
bbf9a3af70 Merge branch 'master-v4.3' of https://github.com/sle118/squeezelite-esp32 into master-v4.3 2023-11-13 22:30:51 -08:00
philippe44
a9ca4c4450 attempt to fix cspot track issues - release 2023-11-13 22:30:45 -08:00
github-actions
55bce084eb Update prebuilt objects [skip actions] 2023-11-09 21:49:08 +00:00
philippe44
d31697e7ef force pad_select for gpio use in dac_controlset - release 2023-11-09 13:47:08 -08:00
philippe44
e830b4db73 Merge branch 'master-v4.3' of https://github.com/sle118/squeezelite-esp32 into master-v4.3 2023-11-09 13:42:11 -08:00
philippe44
8fe21327b8 Update README.md 2023-11-08 16:54:54 -08:00
philippe44
3f487366ee Update README.md 2023-11-08 16:51:47 -08:00
philippe44
b875585aec Merge branch 'master-v4.3' of https://github.com/sle118/squeezelite-esp32 into master-v4.3 2023-11-08 16:37:53 -08:00
philippe44
b46fccfc83 execute dac_controlset even w/o i2c (for gpio) - release 2023-11-08 16:37:49 -08:00
philippe44
655c17fb29 Update README.md 2023-11-08 11:36:29 -08:00
philippe44
b8bda0435a Update README.md 2023-11-08 11:34:17 -08:00
github-actions
dc5cb31efb Update prebuilt objects [skip actions] 2023-11-08 06:18:54 +00:00
philippe44
44ccbb49a6 ledvu update 2023-11-07 22:15:54 -08:00
philippe44
6589387cd3 Merge pull request #351 from wizmo2/ledvu-update-clean
Ledvu update (clean)
2023-11-07 22:08:42 -08:00
github-actions
0d3f6a8870 Update prebuilt objects [skip actions] 2023-11-06 01:24:27 +00:00
Wizmo2
dad8efff8b Merge branch 'ledvu-update-clean' of https://github.com/wizmo2/squeezelite-esp32 into ledvu-update-clean 2023-11-05 14:30:23 -05:00
Wizmo2
e8c6169e09 formating changes 2023-11-05 14:27:45 -05:00
Wizmo2
fc8d15f58d formating changes 2023-11-04 23:07:25 -04:00
Wizmo2
f9d7e15d4b add suspend service 2023-11-04 22:56:37 -04:00
Wizmo2
667b90fafc ledvu-update-clean 2023-10-30 19:24:23 -04:00
75 changed files with 909 additions and 381 deletions

View File

@@ -88,7 +88,7 @@ jobs:
git commit -m "Update prebuilt objects [skip actions]"
git push https://${{secrets.github_token}}@github.com/sle118/squeezelite-esp32.git
- name: Locally store commonly built objects
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: prebuilt_objects
path: |
@@ -130,7 +130,7 @@ jobs:
git status
build_tools.py environment --build ${{ needs.bootstrap.outputs.build_number }} --env_file "$GITHUB_ENV" --node "${{matrix.node}}" --depth ${{matrix.depth}} --major 2 --docker sle118/squeezelite-esp32-idfv435
- uses: actions/download-artifact@master
- uses: actions/download-artifact@v4
name: Restore common objects
with:
name: prebuilt_objects

View File

@@ -1,3 +1,24 @@
2023-11-19
- more robust (?) airplay RTP frame recovery
- initialize of scratch string in monitor (trying to figure out random reboot)
2023-11-16
- add SH1122 support
- optimize GDS DrawPixel function
2023-11-09
- force gpio_pad_select_gpio in dac_controlset in case somebody uses UART gpio's (or other pre-programmed)
2023-11-08
- execute dac_controlset even whne there is no i2s (for gpio)
2023-11-07
- led-vu gain + misc fixes
- bump plugin version to 0.600
2023-11-03
- don't reboot when external decoder is connected even with a LMS server
2023-10-28
- fix recovery size (remove bootstrap)
- improve NVS initialization structure

View File

@@ -189,13 +189,19 @@ if "model" is not set or is not recognized, then default "I2S" is used. The opti
So far, TAS57xx, TAS5713, AC101, WM8978 and ES8388 are recognized models where the proper init sequence/volume/power controls are sent. For other codecs that might require an I2C commands, please use the parameter "dac_controlset" that allows definition of simple commands to be sent over i2c for init, power, speaker and headset on and off using a JSON syntax:
```json
{ <command>: [ {"reg":<register>,"val":<value>,"mode":<nothing>|"or"|"and"}, ... {{"reg":<register>,"val":<value>,"mode":<nothing>|"or"|"and"} ],
<command>: [ {"reg":<register>,"val":<value>,"mode":<nothing>|"or"|"and"}, ... {{"reg":<register>,"val":<value>,"mode":<nothing>|"or"|"and"} ],
{ <command>: [ <item1>, <item2>, ... <item3> ],
<command>: [ <item1>, <item2>, ... <item3> ],
... }
```
Where `<command>` is one of init, poweron, poweroff, speakeron, speakeroff, headseton, headsetoff
Where `<command>` is one of init, poweron, poweroff, speakeron, speakeroff, headseton, headsetoff (it **must** be an array even for a single item). Item is any of the following elements
```
{"reg":<register>,"val":<value>,"mode":<nothing>|"or"|"and"}
{"gpio":<gpio>,"level":0|1}
{"delay":<ms>}
```
This is standard JSON notation, so if you are not familiar with it, Google is your best friend. Be aware that the '...' means you can have as many entries as you want, it's not part of the syntax. Every section is optional, but it does not make sense to set i2c in the 'dac_config' parameter and not setting anything here.
This is standard JSON notation, so if you are not familiar with it, Google is your best friend. Be aware that the '...' means you can have as many entries as you want, it's not part of the syntax. Every section is optional, but it does not make sense to set i2c in the 'dac_config' parameter and not setting anything here. The parameter 'mode' allows to *or* the register with the value or to *and* it. Don't set 'mode' if you simply want to write. The 'val parameter can be an array [v1, v2,...] to write a serie of bytes in a single i2c burst (in that case 'mode' is ignored). **Note that all values must be decimal**. You can use a validator like [this](https://jsonlint.com) to verify your syntax
The `reg` key allow to write registers on i2c bus. The parameter `mode` allows to *or* the register with the value or to *and* it. Don't set `mode` if you simply want to write. The `val` parameter can be an array [v1, v2,...] to write a serie of bytes in a single i2c burst (in that case 'mode' is ignored). **Note that all values must be decimal**. You can use a validator like [this](https://jsonlint.com) to verify your syntax. The `gpio` key is simply to set a gpio as part of DAC action and `delay` allows a pause between elements.
The 'power' command is used when powering on/off the DAC after the idle period (see -C option of squeezelite) and the 'speaker/headset' commands are sent when switching between speakers and headsets (see headset jack detection).
@@ -321,18 +327,18 @@ See [set_GPIO](#set-gpio) for how to set the green and red LEDs (including addre
NB: For named configuration, GPIO affected to green and red LED cannot be changed but brightness option applies
### LED Strip
One LED strip with up to 255 addressable LEDs can be configured to offer enhanced visualizations. The LED strip can also be controlled remotely though the LMS server (using the CLI interface). Currently only WS2812B LEDs are supported. Set the LED Strip configuration (or NVS led_vu_config) to `WS2812,length=<n>,gpio=<gpio>, where <n> is the number of leds in the strip (1..255), and <gpio> is the data pin.`
The latest LMS plugin update is required to set the visualizer mode and brightness, in the ESP32 settings page for the player. The plugin also adds the following CLI command options
One LED strip with up to 255 addressable LEDs can be configured to offer enhanced visualizations. The VU Meter visualizer includes a battery status indicator (see Battery). Currently only WS2812B LEDs are supported. Set the LED Strip hardware configuration, or the NVS led_vu_config syntax is
```
<playerid> led_visual [<mode>] [brightness(1-255)]
Toggles or selects the visulaizer mode.
The visualizer brighness can be controled using the optional <brighness> tag.
<playerid> dmx <R,G,B|R,G,B,R,G,B ... R,G,B> [<offset>]
Sets the LED at position "offset" to any RGB color where "R"(red),"G"(green), and "B"(blue) are values from 0(off) to 255(max brightness).
Add additional RGB values to the delimited string to set multiple LEDs.
type=[WS2812],length=<n>,gpio=<dataPin>[,scale=<gain>]
```
where `<n>` is the number of LEDs in the strip (1..255). A `<scale>` gain value (percentage) can be added to enhance effect responses.
The latest LMS plugin update is required to set the visualizer mode and brightness in the ESP32 Settings page for the player, or a controllable display (see Extra/SqueezeESP32 menus). The plugin adds additional LMS CLI commands.
| Command | Notes |
| -------------------------------------------------- | ----------- |
| \<playerid\> led_visual \[\<mode\>\] \[\<brightness\>\] | Toggles or selects the visualizer "mode".<br />The visualizer brightness(0..255) can be controlled using the "brightness" tag. |
| \<playerid\> dmx \<R,G,B,R,G,B, ... R,G,B\> \[\<offset\>\] | Sets the LED color starting at position "offset"<br /> with "R"(red),"G"(green),and "B"(blue) color sequences.<br />Add additional RGB values to the delimited string to set multiple LEDs.<br /> |
### Rotary Encoder
One rotary encoder is supported, quadrature shift with press. Such encoders usually have 2 pins for encoders (A and B), and common C that must be set to ground and an optional SW pin for press. A, B and SW must be pulled up, so automatic pull-up is provided by ESP32, but you can add your own resistors. A bit of filtering on A and B (~470nF) helps for debouncing which is not made by software.

176
components/display/SH1122.c Normal file
View File

@@ -0,0 +1,176 @@
/**
* Copyright (c) 2017-2018 Tara Keeling
* 2020 Philippe G.
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <esp_heap_caps.h>
#include <esp_log.h>
#include "gds.h"
#include "gds_private.h"
#define SHADOW_BUFFER
#define PAGE_BLOCK 1024
#define min(a,b) (((a) < (b)) ? (a) : (b))
static char TAG[] = "SH1122";
struct PrivateSpace {
uint8_t *iRAM, *Shadowbuffer;
uint8_t PageSize;
};
// Functions are not declared to minimize # of lines
static void SetColumnAddress( struct GDS_Device* Device, uint8_t Start, uint8_t End ) {
Device->WriteCommand( Device, 0x10 | (Start >> 4) );
Device->WriteCommand( Device, 0x00 | (Start & 0x0f) );
}
static void SetRowAddress( struct GDS_Device* Device, uint8_t Start, uint8_t End ) {
Device->WriteCommand( Device, 0xB0 );
Device->WriteCommand( Device, Start );
}
static void Update( struct GDS_Device* Device ) {
struct PrivateSpace *Private = (struct PrivateSpace*) Device->Private;
// RAM is by columns of 4 pixels ...
SetColumnAddress( Device, 0, Device->Width / 4 - 1);
#ifdef SHADOW_BUFFER
uint16_t *optr = (uint16_t*) Private->Shadowbuffer, *iptr = (uint16_t*) Device->Framebuffer;
bool dirty = false;
for (int r = 0, page = 0; r < Device->Height; r++) {
// look for change and update shadow (cheap optimization = width always / by 2)
for (int c = Device->Width / 2 / 2; --c >= 0;) {
if (*optr != *iptr) {
dirty = true;
*optr = *iptr;
}
iptr++; optr++;
}
// one line done, check for page boundary
if (++page == Private->PageSize) {
if (dirty) {
SetRowAddress( Device, r - page + 1, r );
if (Private->iRAM) {
memcpy(Private->iRAM, Private->Shadowbuffer + (r - page + 1) * Device->Width / 2, page * Device->Width / 2 );
Device->WriteData( Device, Private->iRAM, Device->Width * page / 2 );
} else {
Device->WriteData( Device, Private->Shadowbuffer + (r - page + 1) * Device->Width / 2, page * Device->Width / 2);
}
dirty = false;
}
page = 0;
}
}
#else
SetRowAddress( Device, 0, Device->Height - 1 );
for (int r = 0; r < Device->Height; r += Private->PageSize) {
if (Private->iRAM) {
memcpy(Private->iRAM, Device->Framebuffer + r * Device->Width / 2, Private->PageSize * Device->Width / 2 );
Device->WriteData( Device, Private->iRAM, Private->PageSize * Device->Width / 2 );
} else {
Device->WriteData( Device, Device->Framebuffer + r * Device->Width / 2, Private->PageSize * Device->Width / 2 );
}
}
#endif
}
static void SetLayout( struct GDS_Device* Device, struct GDS_Layout *Layout ) {
if (Layout->HFlip) {
Device->WriteCommand( Device, 0x40 + 0x20 );
Device->WriteCommand( Device, 0xA1 );
} else {
Device->WriteCommand( Device, 0x40 + 0x00 );
Device->WriteCommand( Device, 0xA0 );
}
Device->WriteCommand( Device, Layout->VFlip ? 0xC8 : 0xC0 );
Device->WriteCommand( Device, Layout->Invert ? 0xA7 : 0xA6 );
}
static void DisplayOn( struct GDS_Device* Device ) { Device->WriteCommand( Device, 0xAF ); }
static void DisplayOff( struct GDS_Device* Device ) { Device->WriteCommand( Device, 0xAE ); }
static void SetContrast( struct GDS_Device* Device, uint8_t Contrast ) {
Device->WriteCommand( Device, 0x81 );
Device->WriteCommand( Device, Contrast );
}
static bool Init( struct GDS_Device* Device ) {
struct PrivateSpace *Private = (struct PrivateSpace*) Device->Private;
// find a page size that is not too small is an integer of height
Private->PageSize = min(8, PAGE_BLOCK / (Device->Width / 2));
while (Private->PageSize && Device->Height != (Device->Height / Private->PageSize) * Private->PageSize) Private->PageSize--;
#ifdef SHADOW_BUFFER
Private->Shadowbuffer = malloc( Device->FramebufferSize );
memset(Private->Shadowbuffer, 0xFF, Device->FramebufferSize);
#endif
// only use iRAM for SPI
if (Device->IF == GDS_IF_SPI) {
Private->iRAM = heap_caps_malloc( Private->PageSize * Device->Width / 2, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA );
}
ESP_LOGI(TAG, "SH1122 page %u, iRAM %p", Private->PageSize, Private->iRAM);
// need to be off and disable display RAM
Device->DisplayOff( Device );
Device->WriteCommand( Device, 0xA5 );
// Display Offset
Device->WriteCommand( Device, 0xD3 );
Device->WriteCommand( Device, 0 );
// set flip modes
struct GDS_Layout Layout = { };
Device->SetLayout( Device, &Layout );
// set Clocks => check value
Device->WriteCommand( Device, 0xD5 );
Device->WriteCommand( Device, ( 0x04 << 4 ) | 0x00 );
// MUX Ratio => fixed
Device->WriteCommand( Device, 0xA8 );
Device->WriteCommand( Device, Device->Height - 1);
// no Display Inversion
Device->WriteCommand( Device, 0xA6 );
// gone with the wind
Device->WriteCommand( Device, 0xA4 );
Device->DisplayOn( Device );
Device->Update( Device );
return true;
}
static const struct GDS_Device SH1122 = {
.DisplayOn = DisplayOn, .DisplayOff = DisplayOff, .SetContrast = SetContrast,
.SetLayout = SetLayout,
.Update = Update, .Init = Init,
.Mode = GDS_GRAYSCALE, .Depth = 4,
.HighNibble = true,
};
struct GDS_Device* SH1122_Detect(char *Driver, struct GDS_Device* Device) {
if (!strcasestr(Driver, "SH1122")) return NULL;
if (!Device) Device = calloc(1, sizeof(struct GDS_Device));
*Device = SH1122;
return Device;
}

View File

@@ -71,8 +71,8 @@ static void Update( struct GDS_Device* Device ) {
if (dirty) {
uint16_t *optr = (uint16_t*) Private->iRAM, *iptr = (uint16_t*) (Private->Shadowbuffer + (r - page + 1) * Device->Width / 2);
SetRowAddress( Device, r - page + 1, r );
// need byte swapping
for (int i = page * Device->Width / 2 / 2; --i >= 0; iptr++) *optr++ = (*iptr >> 8) | (*iptr << 8);
//memcpy(Private->iRAM, Private->Shadowbuffer + (r - page + 1) * Device->Width / 2, page * Device->Width / 2 );
Device->WriteCommand( Device, 0x5c );
Device->WriteData( Device, Private->iRAM, Device->Width * page / 2 );
dirty = false;
@@ -84,14 +84,10 @@ static void Update( struct GDS_Device* Device ) {
for (int r = 0; r < Device->Height; r += Private->PageSize) {
SetRowAddress( Device, r, r + Private->PageSize - 1 );
Device->WriteCommand( Device, 0x5c );
if (Private->iRAM) {
uint16_t *optr = (uint16_t*) Private->iRAM, *iptr = (uint16_t*) (Device->Framebuffer + r * Device->Width / 2);
for (int i = Private->PageSize * Device->Width / 2 / 2; --i >= 0; iptr++) *optr++ = (*iptr >> 8) | (*iptr << 8);
//memcpy(Private->iRAM, Device->Framebuffer + r * Device->Width / 2, Private->PageSize * Device->Width / 2 );
Device->WriteData( Device, Private->iRAM, Private->PageSize * Device->Width / 2 );
} else {
Device->WriteData( Device, Device->Framebuffer + r * Device->Width / 2, Private->PageSize * Device->Width / 2 );
}
// need byte swapping
uint16_t *optr = (uint16_t*) Private->iRAM, *iptr = (uint16_t*) (Device->Framebuffer + r * Device->Width / 2);
for (int i = Private->PageSize * Device->Width / 2 / 2; --i >= 0; iptr++) *optr++ = (*iptr >> 8) | (*iptr << 8);
Device->WriteData( Device, Private->iRAM, Private->PageSize * Device->Width / 2 );
}
#endif
}

View File

@@ -109,7 +109,7 @@ void GDS_ClearWindow( struct GDS_Device* Device, int x1, int y1, int x2, int y2,
int c = x1;
// for a row that is not on a boundary, no optimization possible
while (r & 0x07 && r <= y2) {
for (c = x1; c <= x2; c++) DrawPixelFast( Device, c, r, Color );
for (c = x1; c <= x2; c++) Device->DrawPixelFast( Device, c, r, Color );
r++;
}
// go fast if we have more than 8 lines to write
@@ -117,7 +117,7 @@ void GDS_ClearWindow( struct GDS_Device* Device, int x1, int y1, int x2, int y2,
memset(optr + Width * r + x1, _Color, x2 - x1 + 1);
r += 8;
} else while (r <= y2) {
for (c = x1; c <= x2; c++) DrawPixelFast( Device, c, r, Color );
for (c = x1; c <= x2; c++) Device->DrawPixelFast( Device, c, r, Color );
r++;
}
}
@@ -133,10 +133,10 @@ void GDS_ClearWindow( struct GDS_Device* Device, int x1, int y1, int x2, int y2,
// try to do byte processing as much as possible
for (int r = y1; r <= y2; r++) {
int c = x1;
if (c & 0x01) DrawPixelFast( Device, c++, r, Color);
if (c & 0x01) Device->DrawPixelFast( Device, c++, r, Color);
int chunk = (x2 - c + 1) >> 1;
memset(optr + ((r * Width + c) >> 1), _Color, chunk);
if (c + chunk <= x2) DrawPixelFast( Device, x2, r, Color);
if (c + chunk <= x2) Device->DrawPixelFast( Device, x2, r, Color);
}
}
} else if (Device->Depth == 8) {
@@ -148,7 +148,7 @@ void GDS_ClearWindow( struct GDS_Device* Device, int x1, int y1, int x2, int y2,
} else {
for (int y = y1; y <= y2; y++) {
for (int x = x1; x <= x2; x++) {
DrawPixelFast( Device, x, y, Color);
Device->DrawPixelFast( Device, x, y, Color);
}
}
}
@@ -171,10 +171,76 @@ bool GDS_Reset( struct GDS_Device* Device ) {
return true;
}
static void IRAM_ATTR DrawPixel1Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
uint32_t YBit = ( Y & 0x07 );
uint8_t* FBOffset;
/*
* We only need to modify the Y coordinate since the pitch
* of the screen is the same as the width.
* Dividing Y by 8 gives us which row the pixel is in but not
* the bit position.
*/
Y>>= 3;
FBOffset = Device->Framebuffer + ( ( Y * Device->Width ) + X );
if ( Color == GDS_COLOR_XOR ) {
*FBOffset ^= BIT( YBit );
} else {
*FBOffset = ( Color == GDS_COLOR_BLACK ) ? *FBOffset & ~BIT( YBit ) : *FBOffset | BIT( YBit );
}
}
static void IRAM_ATTR DrawPixel4Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
uint8_t* FBOffset = Device->Framebuffer + ( (Y * Device->Width >> 1) + (X >> 1));
*FBOffset = X & 0x01 ? (*FBOffset & 0x0f) | ((Color & 0x0f) << 4) : ((*FBOffset & 0xf0) | (Color & 0x0f));
}
static void IRAM_ATTR DrawPixel4FastHigh( struct GDS_Device* Device, int X, int Y, int Color ) {
uint8_t* FBOffset = Device->Framebuffer + ( (Y * Device->Width >> 1) + (X >> 1));
*FBOffset = X & 0x01 ? ((*FBOffset & 0xf0) | (Color & 0x0f)) : (*FBOffset & 0x0f) | ((Color & 0x0f) << 4);
}
static void IRAM_ATTR DrawPixel8Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
Device->Framebuffer[Y * Device->Width + X] = Color;
}
// assumes that Color is 16 bits R..RG..GB..B from MSB to LSB and FB wants 1st serialized byte to start with R
static void IRAM_ATTR DrawPixel16Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
uint16_t* FBOffset = (uint16_t*) Device->Framebuffer + Y * Device->Width + X;
*FBOffset = __builtin_bswap16(Color);
}
// assumes that Color is 18 bits RGB from MSB to LSB RRRRRRGGGGGGBBBBBB, so byte[0] is B
// FB is 3-bytes packets and starts with R for serialization so 0,1,2 ... = xxRRRRRR xxGGGGGG xxBBBBBB
static void IRAM_ATTR DrawPixel18Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
uint8_t* FBOffset = Device->Framebuffer + (Y * Device->Width + X) * 3;
*FBOffset++ = Color >> 12; *FBOffset++ = (Color >> 6) & 0x3f; *FBOffset = Color & 0x3f;
}
// assumes that Color is 24 bits RGB from MSB to LSB RRRRRRRRGGGGGGGGBBBBBBBB, so byte[0] is B
// FB is 3-bytes packets and starts with R for serialization so 0,1,2 ... = RRRRRRRR GGGGGGGG BBBBBBBB
static void IRAM_ATTR DrawPixel24Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
uint8_t* FBOffset = Device->Framebuffer + (Y * Device->Width + X) * 3;
*FBOffset++ = Color >> 16; *FBOffset++ = Color >> 8; *FBOffset = Color;
}
bool GDS_Init( struct GDS_Device* Device ) {
if (Device->Depth > 8) Device->FramebufferSize = Device->Width * Device->Height * ((8 + Device->Depth - 1) / 8);
else Device->FramebufferSize = (Device->Width * Device->Height) / (8 / Device->Depth);
// set the proper DrawPixel function if not already set by driver
if (!Device->DrawPixelFast) {
if (Device->Depth == 1) Device->DrawPixelFast = DrawPixel1Fast;
else if (Device->Depth == 4 && Device->HighNibble) Device->DrawPixelFast = DrawPixel4FastHigh;
else if (Device->Depth == 4) Device->DrawPixelFast = DrawPixel4Fast;
else if (Device->Depth == 8) Device->DrawPixelFast = DrawPixel8Fast;
else if (Device->Depth == 16) Device->DrawPixelFast = DrawPixel16Fast;
else if (Device->Depth == 24 && Device->Mode == GDS_RGB666) Device->DrawPixelFast = DrawPixel18Fast;
else if (Device->Depth == 24 && Device->Mode == GDS_RGB888) Device->DrawPixelFast = DrawPixel24Fast;
}
// allocate FB unless explicitely asked not to
if (!(Device->Alloc & GDS_ALLOC_NONE)) {

View File

@@ -45,7 +45,7 @@ __attribute__( ( always_inline ) ) static inline void SwapInt( int* a, int* b )
}
void IRAM_ATTR GDS_DrawPixelFast( struct GDS_Device* Device, int X, int Y, int Color ) {
DrawPixelFast( Device, X, Y, Color );
Device->DrawPixelFast( Device, X, Y, Color );
}
void IRAM_ATTR GDS_DrawPixel( struct GDS_Device* Device, int X, int Y, int Color ) {
@@ -63,7 +63,7 @@ void GDS_DrawHLine( struct GDS_Device* Device, int x, int y, int Width, int Colo
if (y < 0) y = 0;
else if (y >= Device->Height) y = Device->Height - 1;
for ( ; x < XEnd; x++ ) DrawPixelFast( Device, x, y, Color );
for ( ; x < XEnd; x++ ) Device->DrawPixelFast( Device, x, y, Color );
}
void GDS_DrawVLine( struct GDS_Device* Device, int x, int y, int Height, int Color ) {
@@ -97,7 +97,7 @@ static inline void DrawWideLine( struct GDS_Device* Device, int x0, int y0, int
for ( ; x < x1; x++ ) {
if ( IsPixelVisible( Device, x, y ) == true ) {
DrawPixelFast( Device, x, y, Color );
Device->DrawPixelFast( Device, x, y, Color );
}
if ( Error > 0 ) {
@@ -126,7 +126,7 @@ static inline void DrawTallLine( struct GDS_Device* Device, int x0, int y0, int
for ( ; y < y1; y++ ) {
if ( IsPixelVisible( Device, x, y ) == true ) {
DrawPixelFast( Device, x, y, Color );
Device->DrawPixelFast( Device, x, y, Color );
}
if ( Error > 0 ) {
@@ -213,37 +213,65 @@ void GDS_DrawBitmapCBR(struct GDS_Device* Device, uint8_t *Data, int Width, int
iptr += Height;
}
}
} else if (Device->Depth == 4) {
} else if (Device->Depth == 4) {
uint8_t *optr = Device->Framebuffer;
int LineLen = Device->Width >> 1;
Height >>= 3;
Color &= 0x0f;
for (int i = Width * Height, r = 0, c = 0; --i >= 0;) {
uint8_t Byte = BitReverseTable256[*Data++];
// we need to linearize code to let compiler better optimize
if (c & 0x01) {
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen;
} else {
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen;
}
// end of a column, move to next one
if (++r == Height) { c++; r = 0; optr = Device->Framebuffer + (c >> 1); }
if (Device->HighNibble) {
for (int i = Width * Height, r = 0, c = 0; --i >= 0;) {
uint8_t Byte = BitReverseTable256[*Data++];
// we need to linearize code to let compiler better optimize
if (c & 0x01) {
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen;
} else {
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen;
}
// end of a column, move to next one
if (++r == Height) { c++; r = 0; optr = Device->Framebuffer + (c >> 1); }
}
} else {
for (int i = Width * Height, r = 0, c = 0; --i >= 0;) {
uint8_t Byte = BitReverseTable256[*Data++];
// we need to linearize code to let compiler better optimize
if (c & 0x01) {
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen;
} else {
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1;
*optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen;
}
// end of a column, move to next one
if (++r == Height) { c++; r = 0; optr = Device->Framebuffer + (c >> 1); }
}
}
} else if (Device->Depth == 8) {
uint8_t *optr = Device->Framebuffer;
@@ -320,14 +348,14 @@ void GDS_DrawBitmapCBR(struct GDS_Device* Device, uint8_t *Data, int Width, int
// don't know bitdepth, use brute-force solution
for (int i = Width * Height, r = 0, c = 0; --i >= 0;) {
uint8_t Byte = *Data++;
DrawPixelFast( Device, c, (r << 3) + 7, (Byte & 0x01) * Color ); Byte >>= 1;
DrawPixelFast( Device, c, (r << 3) + 6, (Byte & 0x01) * Color ); Byte >>= 1;
DrawPixelFast( Device, c, (r << 3) + 5, (Byte & 0x01) * Color ); Byte >>= 1;
DrawPixelFast( Device, c, (r << 3) + 4, (Byte & 0x01) * Color ); Byte >>= 1;
DrawPixelFast( Device, c, (r << 3) + 3, (Byte & 0x01) * Color ); Byte >>= 1;
DrawPixelFast( Device, c, (r << 3) + 2, (Byte & 0x01) * Color ); Byte >>= 1;
DrawPixelFast( Device, c, (r << 3) + 1, (Byte & 0x01) * Color ); Byte >>= 1;
DrawPixelFast( Device, c, (r << 3) + 0, (Byte & 0x01) * Color );
Device->DrawPixelFast( Device, c, (r << 3) + 7, (Byte & 0x01) * Color ); Byte >>= 1;
Device->DrawPixelFast( Device, c, (r << 3) + 6, (Byte & 0x01) * Color ); Byte >>= 1;
Device->DrawPixelFast( Device, c, (r << 3) + 5, (Byte & 0x01) * Color ); Byte >>= 1;
Device->DrawPixelFast( Device, c, (r << 3) + 4, (Byte & 0x01) * Color ); Byte >>= 1;
Device->DrawPixelFast( Device, c, (r << 3) + 3, (Byte & 0x01) * Color ); Byte >>= 1;
Device->DrawPixelFast( Device, c, (r << 3) + 2, (Byte & 0x01) * Color ); Byte >>= 1;
Device->DrawPixelFast( Device, c, (r << 3) + 1, (Byte & 0x01) * Color ); Byte >>= 1;
Device->DrawPixelFast( Device, c, (r << 3) + 0, (Byte & 0x01) * Color );
if (++r == Height) { c++; r = 0; }
}
/* for better understanding, here is the mundane version

View File

@@ -98,6 +98,7 @@ struct GDS_Device {
uint16_t Width, TextWidth;
uint16_t Height;
uint8_t Depth, Mode;
bool HighNibble;
uint8_t Alloc;
uint8_t* Framebuffer;
@@ -155,69 +156,9 @@ static inline bool IsPixelVisible( struct GDS_Device* Device, int x, int y ) {
return Result;
}
static inline void DrawPixel1Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
uint32_t YBit = ( Y & 0x07 );
uint8_t* FBOffset;
/*
* We only need to modify the Y coordinate since the pitch
* of the screen is the same as the width.
* Dividing Y by 8 gives us which row the pixel is in but not
* the bit position.
*/
Y>>= 3;
FBOffset = Device->Framebuffer + ( ( Y * Device->Width ) + X );
if ( Color == GDS_COLOR_XOR ) {
*FBOffset ^= BIT( YBit );
} else {
*FBOffset = ( Color == GDS_COLOR_BLACK ) ? *FBOffset & ~BIT( YBit ) : *FBOffset | BIT( YBit );
}
}
static inline void DrawPixel4Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
uint8_t* FBOffset = Device->Framebuffer + ( (Y * Device->Width >> 1) + (X >> 1));
*FBOffset = X & 0x01 ? (*FBOffset & 0x0f) | ((Color & 0x0f) << 4) : ((*FBOffset & 0xf0) | (Color & 0x0f));
}
static inline void DrawPixel8Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
Device->Framebuffer[Y * Device->Width + X] = Color;
}
// assumes that Color is 16 bits R..RG..GB..B from MSB to LSB and FB wants 1st serialized byte to start with R
static inline void DrawPixel16Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
uint16_t* FBOffset = (uint16_t*) Device->Framebuffer + Y * Device->Width + X;
*FBOffset = __builtin_bswap16(Color);
}
// assumes that Color is 18 bits RGB from MSB to LSB RRRRRRGGGGGGBBBBBB, so byte[0] is B
// FB is 3-bytes packets and starts with R for serialization so 0,1,2 ... = xxRRRRRR xxGGGGGG xxBBBBBB
static inline void DrawPixel18Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
uint8_t* FBOffset = Device->Framebuffer + (Y * Device->Width + X) * 3;
*FBOffset++ = Color >> 12; *FBOffset++ = (Color >> 6) & 0x3f; *FBOffset = Color & 0x3f;
}
// assumes that Color is 24 bits RGB from MSB to LSB RRRRRRRRGGGGGGGGBBBBBBBB, so byte[0] is B
// FB is 3-bytes packets and starts with R for serialization so 0,1,2 ... = RRRRRRRR GGGGGGGG BBBBBBBB
static inline void DrawPixel24Fast( struct GDS_Device* Device, int X, int Y, int Color ) {
uint8_t* FBOffset = Device->Framebuffer + (Y * Device->Width + X) * 3;
*FBOffset++ = Color >> 16; *FBOffset++ = Color >> 8; *FBOffset = Color;
}
static inline void IRAM_ATTR DrawPixelFast( struct GDS_Device* Device, int X, int Y, int Color ) {
if (Device->DrawPixelFast) Device->DrawPixelFast( Device, X, Y, Color );
else if (Device->Depth == 4) DrawPixel4Fast( Device, X, Y, Color );
else if (Device->Depth == 1) DrawPixel1Fast( Device, X, Y, Color );
else if (Device->Depth == 16) DrawPixel16Fast( Device, X, Y, Color );
else if (Device->Depth == 24 && Device->Mode == GDS_RGB666) DrawPixel18Fast( Device, X, Y, Color );
else if (Device->Depth == 24 && Device->Mode == GDS_RGB888) DrawPixel24Fast( Device, X, Y, Color );
else if (Device->Depth == 8) DrawPixel8Fast( Device, X, Y, Color );
}
static inline void IRAM_ATTR DrawPixel( struct GDS_Device* Device, int x, int y, int Color ) {
if ( IsPixelVisible( Device, x, y ) == true ) {
DrawPixelFast( Device, x, y, Color );
Device->DrawPixelFast( Device, x, y, Color );
}
}

View File

@@ -108,7 +108,7 @@ bool GDS_TextLine(struct GDS_Device* Device, int N, int Pos, int Attr, char *Tex
int Y_min = max(0, Device->Lines[N].Y), Y_max = max(0, Device->Lines[N].Y + Device->Lines[N].Font->Height);
for (int c = (Attr & GDS_TEXT_CLEAR_EOL) ? X : 0; c < Device->TextWidth; c++)
for (int y = Y_min; y < Y_max; y++)
DrawPixelFast( Device, c, y, GDS_COLOR_BLACK );
Device->DrawPixelFast( Device, c, y, GDS_COLOR_BLACK );
}
GDS_FontDrawString( Device, X, Device->Lines[N].Y, Text, GDS_COLOR_WHITE );

View File

@@ -63,6 +63,7 @@ static EXT_RAM_ATTR struct {
} displayer;
static const char *known_drivers[] = {"SH1106",
"SH1122",
"SSD1306",
"SSD1322",
"SSD1326",
@@ -79,8 +80,8 @@ static void displayer_task(void *args);
static void display_sleep(void);
struct GDS_Device *display;
extern GDS_DetectFunc SSD1306_Detect, SSD132x_Detect, SH1106_Detect, SSD1675_Detect, SSD1322_Detect, SSD1351_Detect, ST77xx_Detect, ILI9341_Detect;
GDS_DetectFunc *drivers[] = { SH1106_Detect, SSD1306_Detect, SSD132x_Detect, SSD1675_Detect, SSD1322_Detect, SSD1351_Detect, ST77xx_Detect, ILI9341_Detect, NULL };
extern GDS_DetectFunc SSD1306_Detect, SSD132x_Detect, SH1106_Detect, SH1122_Detect, SSD1675_Detect, SSD1322_Detect, SSD1351_Detect, ST77xx_Detect, ILI9341_Detect;
GDS_DetectFunc *drivers[] = { SH1106_Detect, SH1122_Detect, SSD1306_Detect, SSD132x_Detect, SSD1675_Detect, SSD1322_Detect, SSD1351_Detect, ST77xx_Detect, ILI9341_Detect, NULL };
/****************************************************************************************
*

View File

@@ -293,11 +293,8 @@ bool led_strip_init(struct led_strip_t *led_strip)
static EXT_RAM_ATTR StackType_t xStack[LED_STRIP_TASK_SIZE] __attribute__ ((aligned (4)));
if ((led_strip == NULL) ||
(led_strip->rmt_channel >= RMT_CHANNEL_MAX) ||
(led_strip->gpio > GPIO_NUM_33) ||
(led_strip->led_strip_working == NULL) ||
(led_strip->led_strip_showing == NULL) ||
(led_strip->led_strip_length == 0) ||
(led_strip->access_semaphore == NULL)) {
return false;
}

View File

@@ -24,6 +24,7 @@
#include "monitor.h"
#include "led_strip.h"
#include "platform_config.h"
#include "services.h"
#include "led_vu.h"
static const char *TAG = "led_vu";
@@ -55,6 +56,7 @@ static EXT_RAM_ATTR struct {
int vu_start_l;
int vu_start_r;
int vu_status;
int vu_scale;
} strip;
static int led_addr(int pos ) {
@@ -70,31 +72,31 @@ static void battery_svc(float value, int cells) {
if (battery_handler_chain) battery_handler_chain(value, cells);
}
/****************************************************************************************
* Suspend.
*
*/
static void led_vu_sleep(void) {
led_vu_clear(led_display);
}
/****************************************************************************************
* Initialize the led vu strip if configured.
*
*/
void led_vu_init()
{
char* p;
char* config = config_alloc_get_str("led_vu_config", NULL, "N/A");
// Initialize led VU strip
char* drivername = strcasestr(config, "WS2812");
if ((p = strcasestr(config, "length")) != NULL) {
strip.length = atoi(strchr(p, '=') + 1);
} // else 0
if ((p = strcasestr(config, "gpio")) != NULL) {
strip.gpio = atoi(strchr(p, '=') + 1);
} else {
strip.gpio = LED_VU_DEFAULT_GPIO;
}
PARSE_PARAM(config, "length",'=', strip.length);
PARSE_PARAM(config, "gpio",'=', strip.gpio);
// check for valid configuration
if (!drivername || !strip.gpio) {
if (!strip.gpio) {
ESP_LOGI(TAG, "led_vu configuration invalid");
goto done;
}
strip.vu_scale = 100;
PARSE_PARAM(config, "scale",'=',strip.vu_scale);
battery_handler_chain = battery_handler_svc;
battery_handler_svc = battery_svc;
@@ -114,7 +116,7 @@ void led_vu_init()
strip.vu_start_r = strip.vu_length + 1;
strip.vu_status = strip.vu_length;
}
ESP_LOGI(TAG, "vu meter using length:%d left:%d right:%d status:%d", strip.vu_length, strip.vu_start_l, strip.vu_start_r, strip.vu_status);
ESP_LOGI(TAG, "vu meter using length:%d left:%d right:%d status:%d scale:%d", strip.vu_length, strip.vu_start_l, strip.vu_start_r, strip.vu_status, strip.vu_scale);
// create driver configuration
led_strip_config.rgb_led_type = RGB_LED_TYPE_WS2812;
@@ -138,6 +140,8 @@ void led_vu_init()
// reserver max memory for remote management systems
rmt_set_mem_block_num(led_strip_config.rmt_channel, 7);
services_sleep_setsuspend(led_vu_sleep);
led_vu_clear(led_display);
done:
@@ -157,6 +161,14 @@ uint16_t led_vu_string_length() {
return (uint16_t)strip.length;
}
/****************************************************************************************
* Returns a user defined scale (percent)
*/
uint16_t led_vu_scale() {
if (!led_display) return 0;
return (uint16_t)strip.vu_scale;
}
/****************************************************************************************
* Turns all LEDs off (Black)
*/

View File

@@ -21,6 +21,7 @@
extern struct led_strip_t* led_display;
uint16_t led_vu_string_length();
uint16_t led_vu_scale();
void led_vu_progress_bar(int pct, int bright);
void led_vu_display(int vu_l, int vu_r, int bright, bool comet);
void led_vu_spin_dial(int gain, int rate, int speed, bool comet);

View File

@@ -115,6 +115,7 @@ static struct {
struct arg_str *type;
struct arg_int *length;
struct arg_int *gpio;
struct arg_int * scale;
struct arg_lit *clear;
struct arg_end *end;
} ledvu_args;
@@ -657,7 +658,7 @@ static int do_cspot_config(int argc, char **argv) {
#endif
static int do_ledvu_cmd(int argc, char **argv) {
ledvu_struct_t ledvu = {.type = "WS2812", .gpio = -1, .length = 0};
ledvu_struct_t ledvu = {.type = "WS2812", .gpio = -1, .length = 0, .scale = 100};
esp_err_t err = ESP_OK;
int nerrors = arg_parse(argc, argv, (void **)&ledvu_args);
if (ledvu_args.clear->count) {
@@ -685,6 +686,8 @@ static int do_ledvu_cmd(int argc, char **argv) {
} else {
ledvu.length = ledvu_args.length->count > 0 ? ledvu_args.length->ival[0] : 0;
}
ledvu.scale = ledvu_args.scale->count>0?ledvu_args.scale->ival[0]:ledvu.scale;
if (!nerrors) {
fprintf(f, "Storing ledvu parameters.\n");
@@ -914,6 +917,7 @@ cJSON *ledvu_cb() {
} else {
cJSON_AddStringToObject(values, "type", "WS2812");
}
cJSON_AddNumberToObject(values,"scale",ledvu->scale);
return values;
}
@@ -1314,6 +1318,7 @@ void register_ledvu_config(void) {
ledvu_args.type = arg_str1(NULL, "type", "<none>|WS2812", "Led type (supports one rgb strip to display built in effects and allow remote control through 'dmx' messaging)");
ledvu_args.length = arg_int1(NULL, "length", "<1..255>", "Strip length (1-255 supported)");
ledvu_args.gpio = arg_int1(NULL, "gpio", "gpio", "Data pin");
ledvu_args.scale = arg_int0(NULL,"scale","<n>","Gain scale (precent)");
ledvu_args.clear = arg_lit0(NULL, "clear", "Clear configuration");
ledvu_args.end = arg_end(4);

View File

@@ -461,7 +461,7 @@ static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool fi
return;
}
}
if (seqno == (u16_t) (ctx->ab_write+1)) {
// expected packet
abuf = ctx->audio_buffer + BUFIDX(seqno);
@@ -572,17 +572,25 @@ static void buffer_push_packet(rtp_t *ctx) {
}
LOG_SDEBUG("playtime %u %d [W:%hu R:%hu] %d", playtime, playtime - now, ctx->ab_write, ctx->ab_read, curframe->ready);
// try to request resend missing packet in order, explore up to 32 frames
for (int step = max((ctx->ab_write - ctx->ab_read + 1) / 32, 1),
i = 0, first = 0;
seq_order(ctx->ab_read + i, ctx->ab_write); i += step) {
abuf_t* frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i);
// each missing packet will be requested up to (latency_frames / 16) times
for (int i = 0; seq_order(ctx->ab_read + i, ctx->ab_write); i += 16) {
abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i);
if (!frame->ready && now - frame->last_resend > RESEND_TO) {
// stop if one fails
if (!rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i)) break;
frame->last_resend = now;
}
}
// stop when we reach a ready frame or a recent pending resend
if (first && (frame->ready || now - frame->last_resend <= RESEND_TO)) {
if (!rtp_request_resend(ctx, first, ctx->ab_read + i - 1)) break;
first = 0;
i += step - 1;
} else if (!frame->ready && now - frame->last_resend > RESEND_TO) {
if (!first) first = ctx->ab_read + i;
frame->last_resend = now;
}
}
}
/*---------------------------------------------------------------------------*/

View File

@@ -329,7 +329,7 @@ esp_err_t config_ledvu_set(ledvu_struct_t * config){
esp_err_t err=ESP_OK;
char * config_buffer=malloc_init_external(buffer_size);
if(config_buffer) {
snprintf(config_buffer,buffer_size,"%s,length=%i,gpio=%i",config->type, config->length, config->gpio);
snprintf(config_buffer,buffer_size,"%s,length=%i,gpio=%i,scale=%i",config->type, config->length, config->gpio, config->scale);
log_send_messaging(MESSAGING_INFO,"Updating ledvu configuration to %s",config_buffer);
err = config_set_value(NVS_TYPE_STR, "led_vu_config", config_buffer);
if(err!=ESP_OK){
@@ -761,14 +761,13 @@ const rotary_struct_t * config_rotary_get() {
*/
const ledvu_struct_t * config_ledvu_get() {
static ledvu_struct_t ledvu={ .type = "WS2812", .gpio = -1, .length = 0};
static ledvu_struct_t ledvu={ .type = "WS2812", .gpio = -1, .length = 0, .scale= 100 };
char *config = config_alloc_get_default(NVS_TYPE_STR, "led_vu_config", NULL, 0);
if (config && *config) {
char *p;
// ToDo: Add code for future support of alternate led types
if ((p = strcasestr(config, "gpio")) != NULL) ledvu.gpio = atoi(strchr(p, '=') + 1);
if ((p = strcasestr(config, "length")) != NULL) ledvu.length = atoi(strchr(p, '=') + 1);
PARSE_PARAM_STR(config, "type", '=', ledvu.type, 15);
PARSE_PARAM(config, "gpio", '=', ledvu.gpio);
PARSE_PARAM(config, "length", '=', ledvu.length);
PARSE_PARAM(config, "scale", '=', ledvu.scale);
free(config);
}
return &ledvu;

View File

@@ -89,6 +89,7 @@ typedef struct {
char type[16];
int length;
int gpio;
int scale;
} ledvu_struct_t;
typedef struct {

View File

@@ -62,7 +62,7 @@ static void task_stats( cJSON* top ) {
current.n = uxTaskGetSystemState( current.tasks, current.n, &current.total );
cJSON_AddNumberToObject(top,"ntasks",current.n);
char scratch[SCRATCH_SIZE] = { };
char scratch[SCRATCH_SIZE] = {0};
#ifdef CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
#pragma message("Compiled with runtime stats")
@@ -143,11 +143,13 @@ static void monitor_trace(uint32_t now) {
heap_caps_get_minimum_free_size(MALLOC_CAP_DMA));
task_stats(top);
char * top_a= cJSON_PrintUnformatted(top);
if(top_a){
messaging_post_message(MESSAGING_INFO, MESSAGING_CLASS_STATS,top_a);
FREE_AND_NULL(top_a);
}
cJSON_Delete(top);
}

View File

@@ -361,7 +361,7 @@ void services_init(void) {
}
#endif
// set potential power GPIO on chip first in case expanders are power using these
// set potential power GPIO on chip first in case expanders are powered using these
parse_set_GPIO(set_chip_power_gpio);
// shared I2C bus

View File

@@ -227,7 +227,7 @@ int main(int argc, char *argv[]) {
} else if (!strcasecmp(arg, "-i")) {
identity = *++argv;
} else {
// nothing let's try to be smart and handle legacy crappy
// nothing let's try to be smart and handle legacy crap
if (!identity) identity = *argv;
else if (!type) (void) !asprintf(&type, "%s.local", *argv);
else if (!port) port = atoi(*argv);
@@ -235,6 +235,7 @@ int main(int argc, char *argv[]) {
txt = (const char**) malloc((argc + 1) * sizeof(char**));
memcpy(txt, argv, argc * sizeof(char**));
txt[argc] = NULL;
break;
}
argc--;
}
@@ -250,13 +251,14 @@ int main(int argc, char *argv[]) {
mdnsd_set_hostname(svr, hostname, host);
svc = mdnsd_register_svc(svr, identity, type, port, NULL, txt);
mdns_service_destroy(svc);
// mdns_service_destroy(svc);
#ifdef _WIN32
Sleep(INFINITE);
#else
pause();
#endif
mdns_service_remove(svr, svc);
mdnsd_stop(svr);
} else {
printf("Can't start server");
@@ -264,7 +266,7 @@ int main(int argc, char *argv[]) {
}
free(type);
free(txt);
if (txt) free(txt);
#ifdef _WIN32
winsock_close();

View File

@@ -144,11 +144,10 @@ static int create_recv_sock(uint32_t host) {
}
#if !defined(_WIN32)
on = sizeof(on);
socklen_t len;
if (!getsockopt(sd, SOL_SOCKET, SO_REUSEPORT,(char*) &on, &len)) {
socklen_t len = sizeof(on);
if (!getsockopt(sd, SOL_SOCKET, SO_REUSEPORT, &on, &len)) {
on = 1;
if ((r = setsockopt(sd, SOL_SOCKET, SO_REUSEPORT,(char*) &on, sizeof(on))) < 0) {
if ((r = setsockopt(sd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on))) < 0) {
log_message(LOG_ERR, "recv setsockopt(SO_REUSEPORT): %m\n", r);
}
}

View File

@@ -5,6 +5,7 @@
#include <unistd.h>
#include <cstring>
#include <vector>
#include <mutex>
#if __has_include("avahi-client/client.h")
#include <avahi-client/client.h>

View File

@@ -201,21 +201,23 @@ void SpircHandler::handleFrame(std::vector<uint8_t>& data) {
break;
}
case MessageType_kMessageTypeReplace: {
CSPOT_LOG(debug, "Got replace frame");
CSPOT_LOG(debug, "Got replace frame %d",
playbackState->remoteTracks.size());
playbackState->syncWithRemote();
// 1st track is the current one, but update the position
bool cleared = trackQueue->updateTracks(
playbackState->remoteFrame.state.position_ms +
ctx->timeProvider->getSyncedTimestamp() -
playbackState->innerFrame.state.position_measured_at);
ctx->timeProvider->getSyncedTimestamp() -
playbackState->innerFrame.state.position_measured_at,
false);
this->notify();
// need to re-load all if streaming track is completed
if (cleared) {
sendEvent(EventType::FLUSH);
trackPlayer->resetState();
sendEvent(EventType::FLUSH);
trackPlayer->resetState();
}
break;
}

View File

@@ -591,15 +591,14 @@ bool TrackQueue::updateTracks(uint32_t requestedPosition, bool initial) {
std::scoped_lock lock(tracksMutex);
bool cleared = true;
// Copy requested track list
currentTracks = playbackState->remoteTracks;
currentTracksIndex = playbackState->innerFrame.state.playing_track_index;
if (initial) {
// Clear preloaded tracks
preloadedTracks.clear();
// Copy requested track list
currentTracks = playbackState->remoteTracks;
currentTracksIndex = playbackState->innerFrame.state.playing_track_index;
if (currentTracksIndex < currentTracks.size()) {
// Push a song on the preloaded queue
queueNextTrack(0, requestedPosition);
@@ -609,28 +608,24 @@ bool TrackQueue::updateTracks(uint32_t requestedPosition, bool initial) {
notifyPending = true;
playableSemaphore->give();
} else {
// Copy requested track list
currentTracks = playbackState->remoteTracks;
} else if (preloadedTracks[0]->loading) {
// try to not re-load track if we are still loading it
if (preloadedTracks[0]->loading) {
// remove everything except first track
preloadedTracks.erase(preloadedTracks.begin() + 1, preloadedTracks.end());
// Push a song on the preloaded queue
CSPOT_LOG(info, "Keeping current track");
queueNextTrack(1);
// remove everything except first track
preloadedTracks.erase(preloadedTracks.begin() + 1, preloadedTracks.end());
cleared = false;
} else {
// Clear preloaded tracks
preloadedTracks.clear();
// Push a song on the preloaded queue
CSPOT_LOG(info, "Keeping current track %d", currentTracksIndex);
queueNextTrack(1);
// Push a song on the preloaded queue
CSPOT_LOG(info, "Re-loading current track");
queueNextTrack(0, requestedPosition);
}
cleared = false;
} else {
// Clear preloaded tracks
preloadedTracks.clear();
// Push a song on the preloaded queue
CSPOT_LOG(info, "Re-loading current track");
queueNextTrack(0, requestedPosition);
}
return cleared;

View File

@@ -216,7 +216,7 @@ static EXT_RAM_ATTR struct {
static EXT_RAM_ATTR struct {
int mode;
int n, style, max;
int n, style, max, gain;
u16_t config;
struct bar_s bars[MAX_BARS] ;
} led_visu;
@@ -1089,10 +1089,10 @@ static void displayer_update(void) {
if (led_display && led_visu.mode) {
// run built in visualizer effects
if (led_visu.mode == VISU_VUMETER) {
vu_scale(led_visu.bars, led_visu.max, meters.levels);
vu_scale(led_visu.bars, led_visu.gain, meters.levels);
led_vu_display(led_visu.bars[0].current, led_visu.bars[1].current, led_visu.max, led_visu.style);
} else if (led_visu.mode == VISU_SPECTRUM) {
spectrum_scale(led_visu.n, led_visu.bars, led_visu.max, meters.samples);
spectrum_scale(led_visu.n, led_visu.bars, led_visu.gain, meters.samples);
uint8_t* p = (uint8_t*) led_data;
for (int i = 0; i < led_visu.n; i++) {
*p = led_visu.bars[i].current;
@@ -1100,7 +1100,7 @@ static void displayer_update(void) {
}
led_vu_spectrum(led_data, led_visu.max, led_visu.n, led_visu.style);
} else if (led_visu.mode == VISU_WAVEFORM) {
spectrum_scale(led_visu.n, led_visu.bars, led_visu.max, meters.samples);
spectrum_scale(led_visu.n, led_visu.bars, led_visu.gain, meters.samples);
led_vu_spin_dial(
led_visu.bars[led_visu.n-2].current,
led_visu.bars[(led_visu.n/2)+1].current * 50 / led_visu.max,
@@ -1277,8 +1277,8 @@ static void ledv_handler( u8_t *data, int len) {
led_visu.mode = pkt->which;
led_visu.style = pkt->style;
led_visu.max = pkt->bright;
led_visu.gain = led_visu.max * led_vu_scale() / 100;
led_vu_clear();
if (led_visu.mode) {
if (led_visu.mode == VISU_SPECTRUM) {
led_visu.n = (led_visu.config < MAX_BARS) ? led_visu.config : MAX_BARS;
@@ -1293,8 +1293,10 @@ static void ledv_handler( u8_t *data, int len) {
// reset bars maximum
for (int i = led_visu.n; --i >= 0;) led_visu.bars[i].max = 0;
LOG_INFO("LED Visualizer mode %u with bars:%u max:%u style:%d", led_visu.mode, led_visu.n, led_visu.max, led_visu.style);
LOG_INFO("LED Visualizer mode %u with bars:%u max:%u style:%d gain:%u", led_visu.mode, led_visu.n, led_visu.max, led_visu.style, led_visu.gain);
} else {
led_vu_clear();
LOG_INFO("Stopping led visualizer");
}

View File

@@ -61,7 +61,6 @@ static bool init(char *config, int i2c_port_num, i2s_config_t *i2s_config, bool
char *p;
i2c_addr = adac_init(config, i2c_port_num);
if (!i2c_addr) return true;
ESP_LOGI(TAG, "DAC on I2C @%d", i2c_addr);
@@ -137,6 +136,7 @@ bool i2c_json_execute(char *set) {
if ((action = cJSON_GetObjectItemCaseSensitive(item, "gpio")) != NULL) {
cJSON *level = cJSON_GetObjectItemCaseSensitive(item, "level");
ESP_LOGI(TAG, "set GPIO %d at %d", action->valueint, level->valueint);
if (action->valueint < GPIO_NUM_MAX) gpio_pad_select_gpio(action->valueint);
gpio_set_direction_x(action->valueint, GPIO_MODE_OUTPUT);
gpio_set_level_x(action->valueint, level->valueint);
continue;

View File

@@ -44,7 +44,7 @@
#define MAX_OPUS_FRAMES 5760
struct opus {
enum {OGG_SYNC, OGG_ID_HEADER, OGG_COMMENT_HEADER} status;
enum { OGG_ID_HEADER, OGG_COMMENT_HEADER } status;
ogg_stream_state state;
ogg_packet packet;
ogg_sync_state sync;
@@ -131,95 +131,108 @@ static opus_uint32 parse_uint32(const unsigned char* _data) {
(opus_uint32)_data[2] << 16 | (opus_uint32)_data[3] << 24;
}
static int get_opus_packet(void) {
static int get_audio_packet(void) {
int status, packet = -1;
LOCK_S;
size_t bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
while (!(status = OG(&go, stream_packetout, &u->state, &u->packet)) && bytes) {
// if sync_pageout (or sync_pageseek) is not called here, sync builds ups
while (!(status = OG(&go, sync_pageout, &u->sync, &u->page)) && bytes) {
// if sync_pageout (or sync_pageseek) is not called here, sync buffers build up
while (!(status = OG(&go, sync_pageout, &u->sync, &u->page)) && bytes) {
size_t consumed = min(bytes, 4096);
char* buffer = OG(&gu, sync_buffer, &u->sync, consumed);
char* buffer = OG(&go, sync_buffer, &u->sync, consumed);
memcpy(buffer, streambuf->readp, consumed);
OG(&gu, sync_wrote, &u->sync, consumed);
OG(&go, sync_wrote, &u->sync, consumed);
_buf_inc_readp(streambuf, consumed);
bytes -= consumed;
}
}
// if we have a new page, put it in
if (status) OG(&go, stream_pagein, &u->state, &u->page);
}
// only return a negative value when true end of streaming is reached
if (status > 0) packet = status;
else if (stream.state > DISCONNECT || _buf_used(streambuf)) packet = 0;
// if we have a new page, put it in and reset serialno at BoS
if (status) {
OG(&go, stream_pagein, &u->state, &u->page);
if (OG(&go, page_bos, &u->page)) OG(&go, stream_reset_serialno, &u->state, OG(&go, page_serialno, &u->page));
}
}
/* discard header packets. With no packet, we return a negative value
* when there is really nothing more to proceed */
if (status > 0 && memcmp(u->packet.packet, "OpusHead", 8) && memcmp(u->packet.packet, "OpusTags", 8)) packet = status;
else if (stream.state > DISCONNECT || _buf_used(streambuf)) packet = 0;
UNLOCK_S;
return packet;
}
static int read_opus_header(void) {
int status = 0;
bool fetch = true;
int done = 0;
bool fetch = true;
LOCK_S;
size_t bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
while (bytes && !status) {
// first fetch a page if we need one
if (fetch) {
while (bytes && !done) {
int status;
// get aligned to a page and ready to bring it in
do {
size_t consumed = min(bytes, 4096);
char* buffer = OG(&gu, sync_buffer, &u->sync, consumed);
char* buffer = OG(&go, sync_buffer, &u->sync, consumed);
memcpy(buffer, streambuf->readp, consumed);
OG(&gu, sync_wrote, &u->sync, consumed);
OG(&go, sync_wrote, &u->sync, consumed);
_buf_inc_readp(streambuf, consumed);
bytes -= consumed;
if (!OG(&gu, sync_pageseek, &u->sync, &u->page)) continue;
}
status = fetch ? OG(&go, sync_pageout, &u->sync, &u->page) :
OG(&go, sync_pageseek, &u->sync, &u->page);
} while (bytes && status <= 0);
switch (u->status) {
case OGG_SYNC:
u->status = OGG_ID_HEADER;
OG(&gu, stream_reset_serialno, &u->state, OG(&gu, page_serialno, &u->page));
fetch = false;
break;
case OGG_ID_HEADER:
status = OG(&gu, stream_pagein, &u->state, &u->page);
if (OG(&gu, stream_packetout, &u->state, &u->packet)) {
if (u->packet.bytes < 19 || memcmp(u->packet.packet, "OpusHead", 8)) {
LOG_ERROR("wrong opus header packet (size:%u)", u->packet.bytes);
status = -100;
break;
}
u->status = OGG_COMMENT_HEADER;
// nothing has been found and we have no more bytes, come back later
if (status <= 0) break;
// always set stream serialno if we have a new one
if (OG(&go, page_bos, &u->page)) OG(&go, stream_reset_serialno, &u->state, OG(&go, page_serialno, &u->page));
// bring new page in if we want it (otherwise we're just skipping)
if (fetch) OG(&go, stream_pagein, &u->state, &u->page);
// no need for a switch...case
if (u->status == OGG_ID_HEADER) {
// we need the id packet, get more pages if we don't
if (OG(&go, stream_packetout, &u->state, &u->packet) <= 0) continue;
// make sure this is a valid packet
if (u->packet.bytes < 19 || memcmp(u->packet.packet, "OpusHead", 8)) {
LOG_ERROR("wrong header packet (size:%u)", u->packet.bytes);
done = -100;
} else {
u->status = OGG_COMMENT_HEADER;
u->channels = u->packet.packet[9];
u->pre_skip = parse_uint16(u->packet.packet + 10);
u->rate = parse_uint32(u->packet.packet + 12);
u->gain = parse_int16(u->packet.packet + 16);
u->decoder = OP(&gu, decoder_create, 48000, u->channels, &status);
fetch = false;
if (!u->decoder || status != OPUS_OK) {
LOG_ERROR("can't create decoder %d (channels:%u)", status, u->channels);
}
else {
LOG_INFO("codec up and running");
}
}
fetch = true;
break;
case OGG_COMMENT_HEADER:
// skip packets to consume VorbisComment. With opus, header packets align on pages
status = OG(&gu, page_packets, &u->page);
break;
default:
break;
} else if (u->status == OGG_COMMENT_HEADER) {
// don't consume VorbisComment which could be a huge packet, just skip it
if (!OG(&go, page_packets, &u->page)) continue;
done = 1;
}
}
UNLOCK_S;
return status;
return done;
}
static decode_state opus_decompress(void) {
@@ -271,7 +284,7 @@ static decode_state opus_decompress(void) {
memcpy(write_buf, u->overbuf, u->overframes * BYTES_PER_FRAME);
n = u->overframes;
u->overframes = 0;
} else if ((packet = get_opus_packet()) > 0) {
} else if ((packet = get_audio_packet()) > 0) {
if (frames < MAX_OPUS_FRAMES) {
// don't have enough contiguous space, use the overflow buffer
n = OP(&gu, decode, u->decoder, u->packet.packet, u->packet.bytes, (opus_int16*) u->overbuf, MAX_OPUS_FRAMES, 0);
@@ -286,7 +299,7 @@ static decode_state opus_decompress(void) {
* outputbuf and streambuf for maybe a long time while we process it all, so don't do that */
n = OP(&gu, decode, u->decoder, u->packet.packet, u->packet.bytes, (opus_int16*) write_buf, frames, 0);
}
} else if (!packet && !OG(&go, page_eos, &u->page)) {
} else if (!packet) {
UNLOCK_O_direct;
return DECODE_RUNNING;
}
@@ -342,7 +355,7 @@ static decode_state opus_decompress(void) {
} else {
LOG_INFO("opus decode error: %d", n);
LOG_INFO("decode error: %d", n);
UNLOCK_O_direct;
return DECODE_COMPLETE;
}
@@ -357,7 +370,7 @@ static void opus_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
if (!u->overbuf) u->overbuf = malloc(MAX_OPUS_FRAMES * BYTES_PER_FRAME);
u->status = OGG_SYNC;
u->status = OGG_ID_HEADER;
u->overframes = 0;
OG(&go, stream_clear, &u->state);

View File

@@ -443,7 +443,7 @@ void output_close_common(void) {
}
void output_flush(void) {
LOG_INFO("flush output buffer");
LOG_INFO("flush output buffer (full)");
buf_flush(outputbuf);
LOCK;
output.fade = FADE_INACTIVE;
@@ -457,3 +457,15 @@ void output_flush(void) {
output.frames_played = 0;
UNLOCK;
}
bool output_flush_streaming(void) {
LOG_INFO("flush output buffer (streaming)");
LOCK;
bool flushed = output.track_start != NULL;
if (output.track_start) {
outputbuf->writep = output.track_start;
output.track_start = NULL;
}
UNLOCK;
return flushed;
}

View File

@@ -305,8 +305,18 @@ static void process_strm(u8_t *pkt, int len) {
sendSTAT("STMt", strm->replay_gain); // STMt replay_gain is no longer used to track latency, but support it
break;
case 'f':
{
decode_flush(false);
bool flushed = false;
if (!output.external) flushed |= output_flush_streaming();
// we can have fully finished the current streaming, that's still a flush
if (stream_disconnect() || flushed) sendSTAT("STMf", 0);
buf_flush(streambuf);
output.stop_time = gettime_ms();
break;
}
case 'q':
decode_flush(strm->command == 'q');
decode_flush(true);
if (!output.external) output_flush();
status.frames_played = 0;
if (stream_disconnect() && strm->command == 'f') sendSTAT("STMf", 0);
@@ -383,7 +393,7 @@ static void process_strm(u8_t *pkt, int len) {
stream_file(header, header_len, strm->threshold * 1024);
autostart -= 2;
} else {
stream_sock(ip, port, header, header_len, strm->threshold * 1024, autostart >= 2);
stream_sock(ip, port, strm->format, header, header_len, strm->threshold * 1024, autostart >= 2);
}
sendSTAT("STMc", 0);
sentSTMu = sentSTMo = sentSTMl = false;

View File

@@ -580,12 +580,26 @@ struct streamstate {
u32_t meta_next;
u32_t meta_left;
bool meta_send;
struct {
enum { STREAM_OGG_OFF, STREAM_OGG_SYNC, STREAM_OGG_HEADER, STREAM_OGG_SEGMENTS, STREAM_OGG_PAGE } state;
u32_t want, miss, match;
u8_t* data;
#pragma pack(push, 1)
struct {
char pattern[4];
u8_t version, type;
u64_t granule;
u32_t serial, page, checksum;
u8_t count;
} header;
} ogg;
#pragma pack(pop)
};
void stream_init(log_level level, unsigned stream_buf_size);
void stream_close(void);
void stream_file(const char *header, size_t header_len, unsigned threshold);
void stream_sock(u32_t ip, u16_t port, const char *header, size_t header_len, unsigned threshold, bool cont_wait);
void stream_sock(u32_t ip, u16_t port, char codec, const char *header, size_t header_len, unsigned threshold, bool cont_wait);
bool stream_disconnect(void);
// decode.c
@@ -727,6 +741,7 @@ struct outputstate {
void output_init_common(log_level level, const char *device, unsigned output_buf_size, unsigned rates[], unsigned idle);
void output_close_common(void);
void output_flush(void);
bool output_flush_streaming(void);
// _* called with mutex locked
frames_t _output_frames(frames_t avail);
void _checkfade(bool);

View File

@@ -148,6 +148,8 @@ static bool running = true;
static void _disconnect(stream_state state, disconnect_code disconnect) {
stream.state = state;
stream.disconnect = disconnect;
if (stream.ogg.state == STREAM_OGG_HEADER && stream.ogg.data) free(stream.ogg.data);
stream.ogg.data = NULL;
#if USE_SSL
if (ssl) {
SSL_shutdown(ssl);
@@ -160,6 +162,122 @@ static void _disconnect(stream_state state, disconnect_code disconnect) {
wake_controller();
}
static u32_t memfind(const u8_t* haystack, u32_t n, const char* needle, u32_t len, u32_t *offset) {
int i;
for (i = 0; i < n && *offset != len; i++) *offset = (haystack[i] == needle[*offset]) ? *offset + 1 : 0;
return i;
}
static void stream_ogg(size_t n) {
if (stream.ogg.state == STREAM_OGG_OFF) return;
u8_t* p = streambuf->writep;
while (n) {
size_t consumed = min(stream.ogg.miss, n);
// copy as many bytes as possible and come back later if we do'nt have enough
if (stream.ogg.data) {
memcpy(stream.ogg.data + stream.ogg.want - stream.ogg.miss, p, consumed);
stream.ogg.miss -= consumed;
if (stream.ogg.miss) return;
}
// we have what we want, let's parse
switch (stream.ogg.state) {
case STREAM_OGG_SYNC: {
stream.ogg.miss -= consumed;
if (consumed) break;
// we have to memorize position in case any of last 3 bytes match...
int pos = memfind(p, n, "OggS", 4, &stream.ogg.match);
if (stream.ogg.match == 4) {
consumed = pos - stream.ogg.match;
stream.ogg.state = STREAM_OGG_HEADER;
stream.ogg.miss = stream.ogg.want = sizeof(stream.ogg.header);
stream.ogg.data = (u8_t*) &stream.ogg.header;
stream.ogg.match = 0;
} else {
LOG_INFO("OggS not at expected position");
return;
}
break;
}
case STREAM_OGG_HEADER:
if (!memcmp(stream.ogg.header.pattern, "OggS", 4)) {
stream.ogg.miss = stream.ogg.want = stream.ogg.header.count;
stream.ogg.data = malloc(stream.ogg.miss);
stream.ogg.state = STREAM_OGG_SEGMENTS;
} else {
stream.ogg.state = STREAM_OGG_SYNC;
stream.ogg.data = NULL;
}
break;
case STREAM_OGG_SEGMENTS:
// calculate size of page using lacing values
for (int i = 0; i < stream.ogg.want; i++) stream.ogg.miss += stream.ogg.data[i];
stream.ogg.want = stream.ogg.miss;
if (stream.ogg.header.granule == 0) {
// granule 0 means a new stream, so let's look into it
stream.ogg.state = STREAM_OGG_PAGE;
stream.ogg.data = realloc(stream.ogg.data, stream.ogg.want);
} else {
// otherwise, jump over data
stream.ogg.state = STREAM_OGG_SYNC;
free(stream.ogg.data);
stream.ogg.data = NULL;
}
break;
case STREAM_OGG_PAGE: {
u32_t offset = 0;
// try to find one of valid Ogg pattern (vorbis, opus)
for (char** tag = (char*[]) { "\x3vorbis", "OpusTags", NULL }; *tag; tag++, offset = 0) {
u32_t pos = memfind(stream.ogg.data, stream.ogg.want, *tag, strlen(*tag), &offset);
if (offset != strlen(*tag)) continue;
// u32:len,char[]:vendorId, u32:N, N x (u32:len,char[]:comment)
char* p = (char*) stream.ogg.data + pos;
p += *p + 4;
u32_t count = *p;
p += 4;
// LMS metadata format for Ogg is "Ogg", N x (u16:len,char[]:comment)
memcpy(stream.header, "Ogg", 3);
stream.header_len = 3;
for (u32_t len; count--; p += len) {
len = *p;
p += 4;
// only report what we use and don't overflow (network byte order)
if (!strncasecmp(p, "TITLE=", 6) || !strncasecmp(p, "ARTIST=", 7) || !strncasecmp(p, "ALBUM=", 6)) {
if (stream.header_len + len > MAX_HEADER) break;
stream.header[stream.header_len++] = len >> 8;
stream.header[stream.header_len++] = len;
memcpy(stream.header + stream.header_len, p, len);
stream.header_len += len;
}
}
stream.meta_send = true;
wake_controller();
LOG_INFO("Ogg meta len: %u", stream.header_len);
break;
}
free(stream.ogg.data);
stream.ogg.state = STREAM_OGG_SYNC;
break;
}
default:
break;
}
p += consumed;
n -= consumed;
}
}
static void *stream_thread() {
while (running) {
@@ -343,6 +461,7 @@ static void *stream_thread() {
}
if (n > 0) {
stream_ogg(n);
_buf_inc_writep(streambuf, n);
stream.bytes += n;
if (stream.meta_interval) {
@@ -485,7 +604,7 @@ void stream_file(const char *header, size_t header_len, unsigned threshold) {
UNLOCK;
}
void stream_sock(u32_t ip, u16_t port, const char *header, size_t header_len, unsigned threshold, bool cont_wait) {
void stream_sock(u32_t ip, u16_t port, char codec, const char *header, size_t header_len, unsigned threshold, bool cont_wait) {
struct sockaddr_in addr;
#if EMBEDDED
@@ -584,6 +703,9 @@ void stream_sock(u32_t ip, u16_t port, const char *header, size_t header_len, un
stream.sent_headers = false;
stream.bytes = 0;
stream.threshold = threshold;
stream.ogg.miss = stream.ogg.match = 0;
stream.ogg.state = (codec == 'o' || codec == 'p') ? STREAM_OGG_SYNC : STREAM_OGG_OFF;
UNLOCK;
}
@@ -604,6 +726,8 @@ bool stream_disconnect(void) {
disc = true;
}
stream.state = STOPPED;
if (stream.ogg.state == STREAM_OGG_HEADER && stream.ogg.data) free(stream.ogg.data);
stream.ogg.data = NULL;
UNLOCK;
return disc;
}

View File

@@ -50,7 +50,7 @@ static inline int32_t clip15(int32_t x) {
struct vorbis {
bool opened;
enum { OGG_SYNC, OGG_ID_HEADER, OGG_COMMENT_HEADER, OGG_SETUP_HEADER } status;
enum { OGG_ID_HEADER, OGG_COMMENT_HEADER, OGG_SETUP_HEADER } status;
struct {
ogg_stream_state state;
ogg_packet packet;
@@ -138,48 +138,16 @@ extern struct processstate process;
#define OG(h, fn, ...) (h)->ogg_ ## fn(__VA_ARGS__)
#endif
static int get_ogg_packet(void) {
static int get_audio_packet(void) {
int status, packet = -1;
LOCK_S;
size_t bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
while (!(status = OG(&go, stream_packetout, &v->state, &v->packet)) && bytes) {
// if sync_pageout (or sync_pageseek) is not called first, sync buffers build ups
while (!(status = OG(&go, sync_pageout, &v->sync, &v->page)) && bytes) {
size_t consumed = min(bytes, 4096);
char* buffer = OG(&gv, sync_buffer, &v->sync, consumed);
memcpy(buffer, streambuf->readp, consumed);
OG(&gv, sync_wrote, &v->sync, consumed);
_buf_inc_readp(streambuf, consumed);
bytes -= consumed;
}
// if we have a new page, put it in
if (status) OG(&go, stream_pagein, &v->state, &v->page);
}
// only return a negative value when true end of streaming is reached
if (status > 0) packet = status;
else if (stream.state > DISCONNECT || _buf_used(streambuf)) packet = 0;
UNLOCK_S;
return packet;
}
static int read_vorbis_header(void) {
int status = 0;
bool fetch = true;
LOCK_S;
size_t bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
while (bytes && !status) {
// first fetch a page if we need one
if (fetch) {
// if sync_pageout (or sync_pageseek) is not called here, sync buffers build up
while (!(status = OG(&go, sync_pageout, &v->sync, &v->page)) && bytes) {
size_t consumed = min(bytes, 4096);
char* buffer = OG(&go, sync_buffer, &v->sync, consumed);
memcpy(buffer, streambuf->readp, consumed);
@@ -187,81 +155,122 @@ static int read_vorbis_header(void) {
_buf_inc_readp(streambuf, consumed);
bytes -= consumed;
if (!OG(&go, sync_pageseek, &v->sync, &v->page)) continue;
}
switch (v->status) {
case OGG_SYNC:
v->status = OGG_ID_HEADER;
OG(&go, stream_reset_serialno, &v->state, OG(&go, page_serialno, &v->page));
fetch = false;
break;
case OGG_ID_HEADER:
status = OG(&go, stream_pagein, &v->state, &v->page);
if (!OG(&go, stream_packetout, &v->state, &v->packet)) break;
// if we have a new page, put it in and reset serialno at BoS
if (status) {
OG(&go, stream_pagein, &v->state, &v->page);
if (OG(&go, page_bos, &v->page)) OG(&go, stream_reset_serialno, &v->state, OG(&go, page_serialno, &v->page));
}
}
/* odd packets are not audio and should be discarded. With no packet, we
* return a negative value when there is really nothing more to proceed */
if (status > 0 && (v->packet.packet[0] & 0x01) == 0) packet = status;
else if (stream.state > DISCONNECT || _buf_used(streambuf)) packet = 0;
UNLOCK_S;
return packet;
}
static int read_vorbis_header(void) {
int done = 0;
bool fetch = true;
LOCK_S;
size_t bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
while (bytes && !done) {
int status;
// get aligned to a page and ready to bring it in
do {
size_t consumed = min(bytes, 4096);
char* buffer = OG(&go, sync_buffer, &v->sync, consumed);
memcpy(buffer, streambuf->readp, consumed);
OG(&go, sync_wrote, &v->sync, consumed);
_buf_inc_readp(streambuf, consumed);
bytes -= consumed;
status = fetch ? OG(&go, sync_pageout, &v->sync, &v->page) :
OG(&go, sync_pageseek, &v->sync, &v->page);
} while (bytes && status <= 0);
// nothing has been found and we have no more bytes, come back later
if (status <= 0) break;
// always set stream serialno if we have a new one
if (OG(&go, page_bos, &v->page)) OG(&go, stream_reset_serialno, &v->state, OG(&go, page_serialno, &v->page));
// bring new page in if we want it (otherwise we're just skipping)
if (fetch) OG(&go, stream_pagein, &v->state, &v->page);
// not a switch...case b/c we might have multiple packets in a page in vorbis
if (v->status == OGG_ID_HEADER) {
// we need the id packet, get more pages if we don't
if (!OG(&go, stream_packetout, &v->state, &v->packet)) continue;
OV(&gv, info_init, &v->info);
status = OV(&gv, synthesis_headerin, &v->info, &v->comment, &v->packet);
if (status) {
LOG_ERROR("vorbis id header packet error %d", status);
status = -1;
LOG_ERROR("id header packet error %d", status);
done = -1;
} else {
v->channels = v->info.channels;
v->rate = v->info.rate;
v->status = OGG_COMMENT_HEADER;
// only fetch if no other packet already in (they should not)
fetch = OG(&go, page_packets, &v->page) <= 1;
if (!fetch) LOG_INFO("id packet should terminate page");
fetch = false;
LOG_INFO("id acquired");
// we should only have one packet, so get next pages
if (OG(&go, page_packets, &v->page) == 1) continue;
}
break;
case OGG_SETUP_HEADER:
// header packets don't align with pages on Vorbis (contrary to Opus)
if (fetch) OG(&go, stream_pagein, &v->state, &v->page);
}
if (v->status == OGG_COMMENT_HEADER) {
// don't consume VorbisComment which could be a huge packet, just skip it
int packets = OG(&go, page_packets, &v->page);
if (!packets) continue;
// we have a "fake" comment packet that is just has the last page...
v->status = OGG_SETUP_HEADER;
OG(&go, stream_pagein, &v->state, &v->page);
OG(&go, stream_packetout, &v->state, &v->packet);
OV(&gv, comment_init, &v->comment);
v->comment.vendor = "N/A";
fetch = true;
LOG_INFO("comment skipped succesfully");
// because of lack of page alignment, we might have the setup page already fully in
if (packets == 1) continue;
}
if (v->status == OGG_SETUP_HEADER) {
// we need the setup packet, get more pages if we don't
if (OG(&go, stream_packetout, &v->state, &v->packet) <= 0) continue;
// finally build a codec if we have the packet
status = OG(&go, stream_packetout, &v->state, &v->packet);
if (status && ((status = OV(&gv, synthesis_headerin, &v->info, &v->comment, &v->packet)) ||
(status = OV(&gv, synthesis_init, &v->decoder, &v->info)))) {
LOG_ERROR("vorbis setup header packet error %d", status);
if (OV(&gv, synthesis_headerin, &v->info, &v->comment, &v->packet) ||
OV(&gv, synthesis_init, &v->decoder, &v->info)) {
LOG_ERROR("setup header packet error %d", status);
// no need to free comment, it's fake
OV(&gv, info_clear, &v->info);
status = -1;
done = -1;
} else {
OV(&gv, block_init, &v->decoder, &v->block);
v->opened = true;
LOG_INFO("codec up and running (rate: %d, channels:%d)", v->rate, v->channels);
status = 1;
LOG_INFO("codec up and running");
done = 1;
}
//@FIXME: can we have audio on that page as well?
break;
case OGG_COMMENT_HEADER: {
// don't consume VorbisComment, just skip it
int packets = OG(&go, page_packets, &v->page);
if (packets) {
v->status = OGG_SETUP_HEADER;
OG(&go, stream_pagein, &v->state, &v->page);
OG(&go, stream_packetout, &v->state, &v->packet);
OV(&gv, comment_init, &v->comment);
v->comment.vendor = "N/A";
// because of lack of page alignment, we might have the setup page already fully in
if (packets > 1) fetch = false;
LOG_INFO("comment skipped succesfully");
}
break;
}
default:
break;
}
}
UNLOCK_S;
return status;
return done;
}
inline int pcm_out(vorbis_dsp_state* decoder, void*** pcm) {
@@ -317,12 +326,12 @@ static decode_state vorbis_decode(void) {
if (v->overflow) {
n = pcm_out(&v->decoder, &pcm);
v->overflow = n - min(n, frames);
} else if ((packet = get_ogg_packet()) > 0) {
} else if ((packet = get_audio_packet()) > 0) {
n = OV(&gv, synthesis, &v->block, &v->packet);
if (n == 0) n = OV(&gv, synthesis_blockin, &v->decoder, &v->block);
if (n == 0) n = pcm_out(&v->decoder, &pcm);
v->overflow = n - min(n, frames);
} else if (!packet && !OG(&go, page_eos, &v->page)) {
} else if (!packet) {
UNLOCK_O_direct;
return DECODE_RUNNING;
}
@@ -410,7 +419,7 @@ static void vorbis_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
}
v->opened = false;
v->status = OGG_SYNC;
v->status = OGG_ID_HEADER;
v->overflow = 0;
OG(&go, stream_clear, &v->state);

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -69,6 +69,12 @@ declare function getStatus(): {};
declare function getStatus(): {};
declare function getStatus(): {};
declare function getStatus(): {};
declare function getStatus(): {};
declare function getStatus(): {};
declare function getStatus(): {};
declare function getRadioButton(entry: any): string;
declare function getRadioButton(entry: any): string;
declare function getRadioButton(entry: any): string;
declare function getRadioButton(entry: any): string;
declare function getRadioButton(entry: any): string;
declare function getRadioButton(entry: any): string;
@@ -211,6 +217,9 @@ declare function pushStatus(): void;
declare function pushStatus(): void;
declare function pushStatus(): void;
declare function pushStatus(): void;
declare function pushStatus(): void;
declare function pushStatus(): void;
declare function pushStatus(): void;
declare let sd: {};
declare let rf: boolean;
declare function refreshStatus(): void;

View File

@@ -1,5 +1,5 @@
target_add_binary_data( __idf_wifi-manager webapp/dist/css/index.4bbe29a78a667faa2b6f.css.gz BINARY)
target_add_binary_data( __idf_wifi-manager webapp/dist/favicon-32x32.png BINARY)
target_add_binary_data( __idf_wifi-manager webapp/dist/index.html.gz BINARY)
target_add_binary_data( __idf_wifi-manager webapp/dist/js/index.cead70.bundle.js.gz BINARY)
target_add_binary_data( __idf_wifi-manager webapp/dist/js/node_vendors.cead70.bundle.js.gz BINARY)
target_add_binary_data( __idf_wifi-manager webapp/dist/js/index.4cb8fa.bundle.js.gz BINARY)
target_add_binary_data( __idf_wifi-manager webapp/dist/js/node_vendors.4cb8fa.bundle.js.gz BINARY)

View File

@@ -6,29 +6,29 @@ extern const uint8_t _favicon_32x32_png_start[] asm("_binary_favicon_32x32_png_s
extern const uint8_t _favicon_32x32_png_end[] asm("_binary_favicon_32x32_png_end");
extern const uint8_t _index_html_gz_start[] asm("_binary_index_html_gz_start");
extern const uint8_t _index_html_gz_end[] asm("_binary_index_html_gz_end");
extern const uint8_t _index_cead70_bundle_js_gz_start[] asm("_binary_index_cead70_bundle_js_gz_start");
extern const uint8_t _index_cead70_bundle_js_gz_end[] asm("_binary_index_cead70_bundle_js_gz_end");
extern const uint8_t _node_vendors_cead70_bundle_js_gz_start[] asm("_binary_node_vendors_cead70_bundle_js_gz_start");
extern const uint8_t _node_vendors_cead70_bundle_js_gz_end[] asm("_binary_node_vendors_cead70_bundle_js_gz_end");
extern const uint8_t _index_4cb8fa_bundle_js_gz_start[] asm("_binary_index_4cb8fa_bundle_js_gz_start");
extern const uint8_t _index_4cb8fa_bundle_js_gz_end[] asm("_binary_index_4cb8fa_bundle_js_gz_end");
extern const uint8_t _node_vendors_4cb8fa_bundle_js_gz_start[] asm("_binary_node_vendors_4cb8fa_bundle_js_gz_start");
extern const uint8_t _node_vendors_4cb8fa_bundle_js_gz_end[] asm("_binary_node_vendors_4cb8fa_bundle_js_gz_end");
const char * resource_lookups[] = {
"/css/index.4bbe29a78a667faa2b6f.css.gz",
"/favicon-32x32.png",
"/index.html.gz",
"/js/index.cead70.bundle.js.gz",
"/js/node_vendors.cead70.bundle.js.gz",
"/js/index.4cb8fa.bundle.js.gz",
"/js/node_vendors.4cb8fa.bundle.js.gz",
""
};
const uint8_t * resource_map_start[] = {
_index_4bbe29a78a667faa2b6f_css_gz_start,
_favicon_32x32_png_start,
_index_html_gz_start,
_index_cead70_bundle_js_gz_start,
_node_vendors_cead70_bundle_js_gz_start
_index_4cb8fa_bundle_js_gz_start,
_node_vendors_4cb8fa_bundle_js_gz_start
};
const uint8_t * resource_map_end[] = {
_index_4bbe29a78a667faa2b6f_css_gz_end,
_favicon_32x32_png_end,
_index_html_gz_end,
_index_cead70_bundle_js_gz_end,
_node_vendors_cead70_bundle_js_gz_end
_index_4cb8fa_bundle_js_gz_end,
_node_vendors_4cb8fa_bundle_js_gz_end
};

View File

@@ -1,6 +1,6 @@
/***********************************
webpack_headers
dist/css/index.4bbe29a78a667faa2b6f.css.gz,dist/favicon-32x32.png,dist/index.html.gz,dist/js/index.cead70.bundle.js.gz,dist/js/node_vendors.cead70.bundle.js.gz
dist/css/index.4bbe29a78a667faa2b6f.css.gz,dist/favicon-32x32.png,dist/index.html.gz,dist/js/index.4cb8fa.bundle.js.gz,dist/js/node_vendors.4cb8fa.bundle.js.gz
***********************************/
#pragma once
#include <inttypes.h>

Binary file not shown.

View File

@@ -64,6 +64,9 @@ sub initPlugin {
Slim::Control::Request::subscribe( sub { onNotification(@_) }, [ ['newmetadata'] ] );
Slim::Control::Request::subscribe( sub { onNotification(@_) }, [ ['playlist'], ['open', 'newsong'] ]);
Slim::Control::Request::subscribe( \&onStopClear, [ ['playlist'], ['stop', 'clear'] ]);
# Add menu item to extras
Slim::Buttons::Home::addSubMenu('PLUGINS', 'PLUGIN_SQUEEZEESP32', { 'useMode' => 'squeezeesp32_mode', });
}
sub onStopClear {

View File

@@ -174,6 +174,20 @@ sub setLEDVisu {
}
updateLED($client);
# display name
my $modes = ledVisualizerModes;
my $desc = $modes->[$visu]{'desc'};
my $name = '';
for (my $j = 0; $j < scalar @$desc; $j++) {
$name .= ' ' if ($j > 0);
$name .= $client->string(@{$desc}[$j]) || @{$desc}[$j];
}
$client->showBriefly( {
'line1' => $client->string('PLUGIN_SQUEEZEESP32_LED_VISUALIZER'),
'line2' => $name,
});
}
sub onNotification {
@@ -186,4 +200,62 @@ sub onNotification {
}
}
sub setMainMode {
my $client = shift;
my $method = shift;
if ($method eq 'pop') {
Slim::Buttons::Common::popMode($client);
$client->update();
return;
}
Slim::Buttons::Common::pushModeLeft($client, 'INPUT.Choice', {
'listRef' => [
{
name => string('PLUGIN_SQUEEZEESP32_LED_VISUALIZER'),
onPlay => sub { Slim::Control::Request::executeRequest($client, ['led_visual']); },
},
{
name => string('PLUGIN_SQUEEZEESP32_LED_BRIGHTNESS'),
onPlay => sub { Slim::Buttons::Common::pushModeLeft($client, 'squeezeesp32_ledvu_bright'); },
},
],
'header' => string('PLUGIN_SQUEEZEESP32'),
'headerAddCount' => 1,
'overlayRef' => sub { return (undef, shift->symbols('rightarrow')) },
});
}
sub setLedvuBrightMode {
my $client = shift;
my $method = shift;
if ($method eq 'pop') {
Slim::Buttons::Common::popMode($client);
$client->update();
return;
}
my $bright = $prefs->client($client)->get('led_brightness');
Slim::Control::Request::executeRequest($client, ['led_visual',1,$bright]);
Slim::Buttons::Common::pushMode($client, 'INPUT.Bar', {
'header' => 'PLUGIN_SQUEEZEESP32_LED_BRIGHTNESS',
'stringHeader' => 1,
'headerValue' => 'unscaled',
'min' => 1,
'max' => 255,
'increment' => 1,
'onChange' => sub {
my ($client, $value) = @_;
$bright = $bright + $value;
if ($bright > 0 && $bright <= 255) {
$prefs->client($client)->set('led_brightness', $bright);
updateLED($client);
}
},
'valueRef' => $bright,
});
}
1;

View File

@@ -10,6 +10,6 @@
<name>PLUGIN_SQUEEZEESP32</name>
<description>PLUGIN_SQUEEZEESP32_DESC</description>
<module>Plugins::SqueezeESP32::Plugin</module>
<version>0.501</version>
<version>0.600</version>
<creator>Philippe</creator>
</extensions>

View File

@@ -1,10 +1,10 @@
<?xml version='1.0' standalone='yes'?>
<extensions>
<plugins>
<plugin version="0.501" name="SqueezeESP32" minTarget="7.9" maxTarget="*">
<plugin version="0.600" name="SqueezeESP32" minTarget="7.9" maxTarget="*">
<link>https://github.com/sle118/squeezelite-esp32</link>
<creator>Philippe</creator>
<sha>842ee0f7b8ccaf2e6df4a741565068f9e4c7a27e</sha>
<sha>335b585e22eddbddacce00f34001d8bf2ee0f54f</sha>
<email>philippe_44@outlook.com</email>
<desc lang="EN">SqueezeESP32 additional player id (100/101)</desc>
<url>http://raw.githubusercontent.com/sle118/squeezelite-esp32/master-v4.3/plugin/SqueezeESP32.zip</url>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
server_certs/r2m01.cer.37 Normal file

Binary file not shown.

BIN
server_certs/r2m01.cer.38 Normal file

Binary file not shown.

BIN
server_certs/r2m01.cer.39 Normal file

Binary file not shown.

BIN
server_certs/r2m01.cer.40 Normal file

Binary file not shown.

BIN
server_certs/r2m01.cer.41 Normal file

Binary file not shown.