diff --git a/assets/firmware/HyperSerial/HyperSerialESP32_Neopixel/HyperSerialESP32_Neopixel.ino b/assets/firmware/HyperSerial/HyperSerialESP32_Neopixel/HyperSerialESP32_Neopixel.ino new file mode 100644 index 00000000..89a039bb --- /dev/null +++ b/assets/firmware/HyperSerial/HyperSerialESP32_Neopixel/HyperSerialESP32_Neopixel.ino @@ -0,0 +1,655 @@ +#include +//////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////// CONFIG SECTION STARTS ///////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#define THIS_IS_RGBW // RGBW SK6812, otherwise comment it +#define COLD_WHITE // for RGBW (THIS_IS_RGBW enabled) select COLD version, comment it if NEUTRAL + +const bool skipFirstLed = false; // if set the first led in the strip will be set to black (for level shifters using sacrifice LED) +const int serialSpeed = 2000000; // serial port speed +#define DATA_PIN 2 // PIN: data output for LED strip + +const bool reportStats = false; // Send back processing statistics +const int reportStatInterval_s = 10; // Send back processing every interval in seconds + +/* Statistics breakdown: + FPS: Updates to the LEDs per second + F-FPS: Frames identified per second + S: Shown (Done) updates to the LEDs per given interval + F: Frames identified per interval (garbled grames cannot be counted) + G: Good frames identified per interval + B: Total bad frames of all types identified per interval + BF: Bad frames identified per interval + BS: Skipped incomplete frames + BC: Frames failing CRC check per interval + BFL Frames failing Fletcher content validation per interval +*/ + +//Developer configs +#define ENABLE_STRIP +#define ENABLE_CHECK_FLETCHER + +const int SERIAL_SIZE_RX = 4096; + +#ifndef ENABLE_STRIP +const int serial2Speed = 460800; +const bool reportInput = false; +#endif +//////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////// CONFIG SECTION ENDS ///////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +const String version = "8.0"; + +#ifdef THIS_IS_RGBW +float whiteLimit = 1.0f; +#ifdef COLD_WHITE +uint8_t rCorrection = 0xA0; // adjust red -> white in 0-0xFF range +uint8_t gCorrection = 0xA0; // adjust green -> white in 0-0xFF range +uint8_t bCorrection = 0xA0; // adjust blue -> white in 0-0xFF range +#else +uint8_t rCorrection = 0xB0; // adjust red -> white in 0-0xFF range +uint8_t gCorrection = 0xB0; // adjust green -> white in 0-0xFF range +uint8_t bCorrection = 0x70; // adjust blue -> white in 0-0xFF range +#endif +#endif + +int ledCount = 0; // This is dynamic, don't change it +int pixelCount = 0; // This is dynamic, don't change it + +#ifdef THIS_IS_RGBW +#define LED_TYPE NeoGrbwFeature + #if defined(ARDUINO_LOLIN_S2_MINI) + #define LED_METHOD NeoEsp32I2s0Sk6812Method + #else + #define LED_METHOD NeoEsp32I2s1Sk6812Method + #endif +#else +#define LED_TYPE NeoGrbFeature + #if defined(ARDUINO_LOLIN_S2_MINI) + #define LED_METHOD NeoEsp32I2s0Ws2812xMethod + #else + #define LED_METHOD NeoEsp32I2s1Ws2812xMethod + #endif +#endif + +#define LED_DRIVER NeoPixelBus + +uint8_t* ledBuffer; +int ledBufferSize; + +#ifdef ENABLE_STRIP +LED_DRIVER* strip = NULL; +#endif + +enum class AwaProtocol +{ + HEADER_A, + HEADER_w, + HEADER_a, + HEADER_HI, + HEADER_LO, + HEADER_CRC, + CHANNELCALIB_GAIN, + CHANNELCALIB_RED, + CHANNELCALIB_GREEN, + CHANNELCALIB_BLUE, + PIXEL, + FLETCHER1, + FLETCHER2, + FLETCHER_EXT +}; + +AwaProtocol state = AwaProtocol::HEADER_A; + +const int headerSize = 6; +const int trailerSize = 3; +const int calibInfoSize = 4; +int bytesRead = 0; + +bool isVersion2 = false; +bool isChannelCalib = false; +uint8_t CRC = 0; +int count = 0; +int currentPixel = 0; +uint16_t fletcher1 = 0; +uint16_t fletcher2 = 0; +uint16_t fletcherExt = 0; + +#ifdef THIS_IS_RGBW +RgbwColor inputColor; +uint8_t wChannel[256]; +uint8_t rChannel[256]; +uint8_t gChannel[256]; +uint8_t bChannel[256]; +#else +RgbColor inputColor; +#endif + +bool ledsComplete = false; + +// statistics +const int reportStatInterval_ms = reportStatInterval_s * 1000; +unsigned long curTime; +unsigned long stat_start = 0; +uint16_t stat_shown = 0; +uint16_t stat_frames = 0; +uint16_t stat_good = 0; +uint16_t stat_bad = 0; + +uint16_t stat_bad_frame = 0; +uint16_t stat_bad_skip = 0; +uint16_t stat_bad_crc = 0; +uint16_t stat_bad_fletcher = 0; + +uint16_t stat_final_shown = 0; +uint16_t stat_final_frames = 0; +uint16_t stat_final_good = 0; +uint16_t stat_final_bad = 0; + +uint16_t stat_final_bad_frame = 0; +uint16_t stat_final_bad_skip = 0; +uint16_t stat_final_bad_crc = 0; +uint16_t stat_final_bad_fletcher = 0; + +//Debugging +String inputString; +String inputErrorString; +String debugString; + +void printStringHex(String string) +{ +#ifndef ENABLE_STRIP + Serial2.println(string.length()); + for (int i = 0; i < string.length(); ++i) + { + if (i % 36 == 0) + { + Serial2.println(); + Serial2.print("["); + Serial2.print(i); + Serial2.print("] "); + } + + if (string[i] < 16) + Serial2.print("0"); + Serial2.print(string[i], HEX); + Serial2.print(":"); + } +#endif +} + +inline void showMe() +{ +#ifdef ENABLE_STRIP + if (strip != NULL && strip->CanShow()) + { + stat_shown++; + strip->Show(); + } +#endif +} + +// statistics +inline void showStats() +{ + if (reportStats) + { + if (stat_frames > 0) + { + stat_final_shown = stat_shown; + stat_final_frames = stat_frames; + stat_final_good = stat_good; + stat_final_bad = stat_bad; + + stat_final_bad_frame = stat_bad_frame; + stat_final_bad_skip = stat_bad_skip; + stat_final_bad_crc = stat_bad_crc; + stat_final_bad_fletcher = stat_bad_fletcher; + } + + stat_start = curTime; + stat_shown = 0; + stat_frames = 0; + stat_good = 0; + stat_bad = 0; + + stat_bad_frame = 0; + stat_bad_skip = 0; + stat_bad_crc = 0; + stat_bad_fletcher = 0; + + String summary = String("FPS: ") + (stat_final_shown / reportStatInterval_s) + + " F-FPS: " + (stat_final_frames / reportStatInterval_s) + + " S: " + stat_final_shown + + " F: " + stat_final_frames + + " G: " + stat_final_good + + " B: " + stat_final_bad + + " (BF: " + stat_final_bad_frame + + " BS: " + stat_final_bad_skip + + " BC: " + stat_final_bad_crc + + " BFL: " + stat_final_bad_fletcher + + ")"; +#ifdef ENABLE_STRIP + Serial.println(summary); +#else + Serial2.println(summary); +#endif + } +} + +void InitLeds(uint16_t ledCount, int pixelCount, bool channelCalibration = false) +{ + if (ledBuffer != NULL) + delete ledBuffer; + + ledBufferSize = pixelCount + (channelCalibration ? calibInfoSize : 0); + ledBuffer = new uint8_t[ledBufferSize]; + +#ifdef ENABLE_STRIP + if (strip != NULL) + delete strip; + + strip = new LED_DRIVER(ledCount, DATA_PIN); + strip->Begin(); +#endif +} + +inline void processSerialData() +{ + while (Serial.available()) { + + char input = Serial.read(); + ++bytesRead; + +#ifndef ENABLE_STRIP + if (reportInput) + inputString += input; +#endif + + switch (state) + { + case AwaProtocol::HEADER_A: + if (input == 'A') + { + state = AwaProtocol::HEADER_w; + } + break; + + case AwaProtocol::HEADER_w: + if (input == 'w') + state = AwaProtocol::HEADER_a; + else + { + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::HEADER_a: + if (input == 'a') + { + isVersion2 = false; + state = AwaProtocol::HEADER_HI; + } + else if (input == 'A') + { + state = AwaProtocol::HEADER_HI; + isVersion2 = true; + } + else + { + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::HEADER_HI: + + stat_frames++; + + count = input << 8; + + CRC = input; + fletcher1 = 0; + fletcher2 = 0; + fletcherExt = 0; + state = AwaProtocol::HEADER_LO; + break; + + case AwaProtocol::HEADER_LO: + count += input + 1; + + if (ledCount != count || isChannelCalib != isVersion2) + { + ledCount = count; + isChannelCalib = isVersion2; + pixelCount = ledCount * 3; + + if (isChannelCalib) + prepareCalibration(); + + InitLeds(ledCount, pixelCount, isChannelCalib); + } + + CRC = CRC ^ input ^ 0x55; + + state = AwaProtocol::HEADER_CRC; + + break; + + case AwaProtocol::HEADER_CRC: + + // Check, if incomplete package information was skipped, set bytesread to headersize and skip wrong input + if (bytesRead != headerSize) + { + stat_bad_skip++; + bytesRead = headerSize; + } + + currentPixel = 0; + if (CRC == input) + { + state = AwaProtocol::PIXEL; + } + else + { + // CRC failure + stat_bad++; + stat_bad_crc++; + + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::PIXEL: + ledBuffer[currentPixel++] = input; + if (currentPixel == pixelCount) + { + if (isChannelCalib) + state = AwaProtocol::CHANNELCALIB_GAIN; + else + state = AwaProtocol::FLETCHER1; + } + break; + + case AwaProtocol::CHANNELCALIB_GAIN: + ledBuffer[currentPixel++] = input; + state = AwaProtocol::CHANNELCALIB_RED; + break; + + case AwaProtocol::CHANNELCALIB_RED: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::CHANNELCALIB_GREEN; + break; + + case AwaProtocol::CHANNELCALIB_GREEN: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::CHANNELCALIB_BLUE; + break; + + case AwaProtocol::CHANNELCALIB_BLUE: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::FLETCHER1; + break; + + case AwaProtocol::FLETCHER1: + fletcher1 = input; + + state = AwaProtocol::FLETCHER2; + break; + + case AwaProtocol::FLETCHER2: + fletcher2 = input; + + state = AwaProtocol::FLETCHER_EXT; + break; + + case AwaProtocol::FLETCHER_EXT: + fletcherExt = input; + ledsComplete = true; + + state = AwaProtocol::HEADER_A; + break; + } + } +} + +void setup() +{ + // Init serial port + int bufSize = Serial.setRxBufferSize(SERIAL_SIZE_RX); + Serial.begin(serialSpeed); + Serial.setTimeout(50); + +#ifndef ENABLE_STRIP + Serial2.begin(serial2Speed); + + Serial2.println(); + Serial2.println("Welcome!"); + Serial2.println("Hyperion Awa driver " + version); + Serial2.println("!!! Debug Output !!!"); +#endif + + // Display config + Serial.println(); + Serial.println("Welcome!"); + Serial.println("Hyperion Awa driver " + version); + Serial.print("(Build: "); + Serial.print(__DATE__); + Serial.print(" "); + Serial.print(__TIME__); + Serial.println(")"); + + // first LED info + if (skipFirstLed) + Serial.println("First LED: disabled"); + else + Serial.println("First LED: enabled"); + + // RGBW claibration info +#ifdef THIS_IS_RGBW +#ifdef COLD_WHITE + Serial.println("Default color mode: RGBW cold"); +#else + Serial.println("Default color mode: RGBW neutral"); +#endif + prepareCalibration(); +#else + Serial.println("Color mode: RGB"); +#endif + + InitLeds(ledCount, pixelCount); +} + +void prepareCalibration() +{ +#ifdef THIS_IS_RGBW + // prepare LUT calibration table, cold white is much better than "neutral" white + for (uint32_t i = 0; i < 256; i++) + { + // color calibration + float red = rCorrection * i; // adjust red + float green = gCorrection * i; // adjust green + float blue = bCorrection * i; // adjust blue + + wChannel[i] = (uint8_t)round(min(whiteLimit * i, 255.0f)); + rChannel[i] = (uint8_t)round(min(red / 0xFF, 255.0f)); + gChannel[i] = (uint8_t)round(min(green / 0xFF, 255.0f)); + bChannel[i] = (uint8_t)round(min(blue / 0xFF, 255.0f)); + } + + Serial.write("RGBW calibration. White limit(%): "); + Serial.print(whiteLimit * 100.0f); + Serial.write(" %, red: "); + Serial.print(rCorrection); + Serial.write(" , green: "); + Serial.print(gCorrection); + Serial.write(" , blue: "); + Serial.print(bCorrection); + Serial.println(); +#endif +} + +void loop() +{ + curTime = millis(); + +#ifdef __AVR__ + // nothing , USART Interrupt is implemented + ESPserialEvent(); +#else + // ESP8266 polling + ESPserialEvent(); +#endif + + if (ledsComplete) + { +#ifndef ENABLE_STRIP + if (reportInput) + { + Serial2.println(); + Serial2.print(" L: "); + printStringHex(inputString); + Serial2.println("<\input>"); + inputString = ""; + + Serial2.print("bytesRead: "); + Serial2.print(bytesRead); + Serial2.print(" , currentPixel: "); + Serial2.print(currentPixel); + Serial2.print(" ,pixelCount: "); + Serial2.print(pixelCount); + Serial2.println(); + } +#endif + + int frameSize = headerSize + ledBufferSize + trailerSize; + + if (bytesRead > frameSize) + { + //Add number of frames ignored on top of frame + int frames = bytesRead / frameSize; + stat_frames += frames; + + //Count frame plus frames ignored as bad frames + int badFrames = frames + 1; + stat_bad += badFrames; + stat_bad_frame += badFrames; + } + else + { + +#ifdef ENABLE_CHECK_FLETCHER + //Test if content is valid + uint16_t item = 0; + uint16_t fletch1 = 0; + uint16_t fletch2 = 0; + uint16_t fletchExt = 0; + + while (item < ledBufferSize) + { + fletch1 = (fletch1 + (uint16_t)ledBuffer[item]) % 255; + fletch2 = (fletch2 + fletch1) % 255; + fletcherExt = (fletcherExt + ((uint16_t)ledBuffer[item] ^ (item))) % 255; + ++item; + } + if ((fletch1 == fletcher1) && (fletch2 == fletcher2) && (ledBuffer[item-1] == (fletcherExt != 0x41) ? fletcherExt : 0xaa)) + { +#endif + stat_good++; + + uint16_t startLed = 0; + if (skipFirstLed) + { +#ifdef ENABLE_STRIP + #ifdef THIS_IS_RGBW + strip->SetPixelColor(startLed, RgbwColor(0, 0, 0, 0)); + #else + strip->SetPixelColor(startLed, RgbColor(0, 0, 0)); + #endif +#endif + startLed = 1; + } + + for (uint16_t led = startLed; led < ledCount; ++led) + { + inputColor.R = ledBuffer[led * 3]; + inputColor.G = ledBuffer[led * 3 + 1]; + inputColor.B = ledBuffer[led * 3 + 2]; + + #ifdef THIS_IS_RGBW + inputColor.W = min(rChannel[inputColor.R], + min(gChannel[inputColor.G], + bChannel[inputColor.B])); + inputColor.R -= rChannel[inputColor.W]; + inputColor.G -= gChannel[inputColor.W]; + inputColor.B -= bChannel[inputColor.W]; + inputColor.W = wChannel[inputColor.W]; + #endif +#ifdef ENABLE_STRIP + strip->SetPixelColor(led, inputColor); +#endif + } + + showMe(); + yield(); + + #ifdef THIS_IS_RGBW + if (isChannelCalib) + { + uint8_t incoming_gain = ledBuffer[pixelCount]; + uint8_t incoming_red = ledBuffer[pixelCount + 1]; + uint8_t incoming_green = ledBuffer[pixelCount + 2]; + uint8_t incoming_blue = ledBuffer[pixelCount + 3]; + + float final_limit = (incoming_gain != 255) ? incoming_gain / 255.0f : 1.0f; + if (rCorrection != incoming_red || gCorrection != incoming_green || bCorrection != incoming_blue || whiteLimit != final_limit) + { + rCorrection = incoming_red; + gCorrection = incoming_green; + bCorrection = incoming_blue; + whiteLimit = final_limit; + prepareCalibration(); + } + } + #endif + +#ifdef ENABLE_CHECK_FLETCHER + } + else + { + stat_bad++; + stat_bad_fletcher++; + } +#endif + } + + bytesRead = 0; + state = AwaProtocol::HEADER_A; + + ledsComplete = false; + } + + if ((curTime - stat_start > reportStatInterval_ms)) + { + if (stat_frames > 0) + { + showStats(); + } + } +} + +#ifdef __AVR__ +void serialEvent() +{ + processSerialData(); +} +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) +void ESPserialEvent() +{ + processSerialData(); +} +#endif diff --git a/assets/firmware/HyperSerial/HyperSerialESP8266_Neopixel/HyperSerialESP8266_Neopixel.ino b/assets/firmware/HyperSerial/HyperSerialESP8266_Neopixel/HyperSerialESP8266_Neopixel.ino new file mode 100644 index 00000000..33aa5988 --- /dev/null +++ b/assets/firmware/HyperSerial/HyperSerialESP8266_Neopixel/HyperSerialESP8266_Neopixel.ino @@ -0,0 +1,646 @@ +#include +//////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////// CONFIG SECTION STARTS ///////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#define THIS_IS_RGBW // RGBW SK6812, otherwise comment it +#define COLD_WHITE // for RGBW (THIS_IS_RGBW enabled) select COLD version, comment it if NEUTRAL + +const bool skipFirstLed = false; // if set the first led in the strip will be set to black (for level shifters using sacrifice LED) +const int serialSpeed = 2000000; // serial port speed + +const bool reportStats = false; // Send back processing statistics +const int reportStatInterval_s = 10; // Send back processing every interval in seconds + +/* Statistics breakdown: + FPS: Updates to the LEDs per second + F-FPS: Frames identified per second + S: Shown (Done) updates to the LEDs per given interval + F: Frames identified per interval (garbled grames cannot be counted) + G: Good frames identified per interval + B: Total bad frames of all types identified per interval + BF: Bad frames identified per interval + BS: Skipped incomplete frames + BC: Frames failing CRC check per interval + BFL Frames failing Fletcher content validation per interval +*/ + +//Developer configs +#define ENABLE_STRIP +#define ENABLE_CHECK_FLETCHER + +const int SERIAL_SIZE_RX = 4096; + +#ifndef ENABLE_STRIP +const int serial2Speed = 460800; +const bool reportInput = false; +#endif +//////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////// CONFIG SECTION ENDS ///////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +const String version = "8.0"; + +#ifdef THIS_IS_RGBW +float whiteLimit = 1.0f; +#ifdef COLD_WHITE +uint8_t rCorrection = 0xA0; // adjust red -> white in 0-0xFF range +uint8_t gCorrection = 0xA0; // adjust green -> white in 0-0xFF range +uint8_t bCorrection = 0xA0; // adjust blue -> white in 0-0xFF range +#else +uint8_t rCorrection = 0xB0; // adjust red -> white in 0-0xFF range +uint8_t gCorrection = 0xB0; // adjust green -> white in 0-0xFF range +uint8_t bCorrection = 0x70; // adjust blue -> white in 0-0xFF range +#endif +#endif + +int ledCount = 0; // This is dynamic, don't change it +int pixelCount = 0; // This is dynamic, don't change it + +#ifdef THIS_IS_RGBW +#define LED_TYPE NeoGrbwFeature + #define LED_METHOD NeoEsp8266Uart1Sk6812Method +#else +#define LED_TYPE NeoGrbFeature + #define LED_METHOD NeoEsp8266Uart1Ws2812xMethod +#endif + +#define LED_DRIVER NeoPixelBus + +uint8_t* ledBuffer; +int ledBufferSize; + +#ifdef ENABLE_STRIP +LED_DRIVER* strip = NULL; +#endif + +enum class AwaProtocol +{ + HEADER_A, + HEADER_w, + HEADER_a, + HEADER_HI, + HEADER_LO, + HEADER_CRC, + CHANNELCALIB_GAIN, + CHANNELCALIB_RED, + CHANNELCALIB_GREEN, + CHANNELCALIB_BLUE, + PIXEL, + FLETCHER1, + FLETCHER2, + FLETCHER_EXT +}; + +AwaProtocol state = AwaProtocol::HEADER_A; + +const int headerSize = 6; +const int trailerSize = 3; +const int calibInfoSize = 4; +int bytesRead = 0; + +bool isVersion2 = false; +bool isChannelCalib = false; +uint8_t CRC = 0; +int count = 0; +int currentPixel = 0; +uint16_t fletcher1 = 0; +uint16_t fletcher2 = 0; +uint16_t fletcherExt = 0; + +#ifdef THIS_IS_RGBW +RgbwColor inputColor; +uint8_t wChannel[256]; +uint8_t rChannel[256]; +uint8_t gChannel[256]; +uint8_t bChannel[256]; +#else +RgbColor inputColor; +#endif + +bool ledsComplete = false; + +// statistics +const int reportStatInterval_ms = reportStatInterval_s * 1000; +unsigned long curTime; +unsigned long stat_start = 0; +uint16_t stat_shown = 0; +uint16_t stat_frames = 0; +uint16_t stat_good = 0; +uint16_t stat_bad = 0; + +uint16_t stat_bad_frame = 0; +uint16_t stat_bad_skip = 0; +uint16_t stat_bad_crc = 0; +uint16_t stat_bad_fletcher = 0; + +uint16_t stat_final_shown = 0; +uint16_t stat_final_frames = 0; +uint16_t stat_final_good = 0; +uint16_t stat_final_bad = 0; + +uint16_t stat_final_bad_frame = 0; +uint16_t stat_final_bad_skip = 0; +uint16_t stat_final_bad_crc = 0; +uint16_t stat_final_bad_fletcher = 0; + +//Debugging +String inputString; +String inputErrorString; +String debugString; + +void printStringHex(String string) +{ +#ifndef ENABLE_STRIP + Serial2.println(string.length()); + for (int i = 0; i < string.length(); ++i) + { + if (i % 36 == 0) + { + Serial2.println(); + Serial2.print("["); + Serial2.print(i); + Serial2.print("] "); + } + + if (string[i] < 16) + Serial2.print("0"); + Serial2.print(string[i], HEX); + Serial2.print(":"); + } +#endif +} + +inline void showMe() +{ +#ifdef ENABLE_STRIP + if (strip != NULL && strip->CanShow()) + { + stat_shown++; + strip->Show(); + } +#endif +} + +// statistics +inline void showStats() +{ + if (reportStats) + { + if (stat_frames > 0) + { + stat_final_shown = stat_shown; + stat_final_frames = stat_frames; + stat_final_good = stat_good; + stat_final_bad = stat_bad; + + stat_final_bad_frame = stat_bad_frame; + stat_final_bad_skip = stat_bad_skip; + stat_final_bad_crc = stat_bad_crc; + stat_final_bad_fletcher = stat_bad_fletcher; + } + + stat_start = curTime; + stat_shown = 0; + stat_frames = 0; + stat_good = 0; + stat_bad = 0; + + stat_bad_frame = 0; + stat_bad_skip = 0; + stat_bad_crc = 0; + stat_bad_fletcher = 0; + + String summary = String("FPS: ") + (stat_final_shown / reportStatInterval_s) + + " F-FPS: " + (stat_final_frames / reportStatInterval_s) + + " S: " + stat_final_shown + + " F: " + stat_final_frames + + " G: " + stat_final_good + + " B: " + stat_final_bad + + " (BF: " + stat_final_bad_frame + + " BS: " + stat_final_bad_skip + + " BC: " + stat_final_bad_crc + + " BFL: " + stat_final_bad_fletcher + + ")"; +#ifdef ENABLE_STRIP + Serial.println(summary); +#else + Serial2.println(summary); +#endif + } +} + +void InitLeds(uint16_t ledCount, int pixelCount, bool channelCalibration = false) +{ + if (ledBuffer != NULL) + delete ledBuffer; + + ledBufferSize = pixelCount + (channelCalibration ? calibInfoSize : 0); + ledBuffer = new uint8_t[ledBufferSize]; + +#ifdef ENABLE_STRIP + if (strip != NULL) + delete strip; + + strip = new LED_DRIVER(ledCount); + strip->Begin(); +#endif +} + +inline void processSerialData() +{ + while (Serial.available()) { + + char input = Serial.read(); + ++bytesRead; + +#ifndef ENABLE_STRIP + if (reportInput) + inputString += input; +#endif + + switch (state) + { + case AwaProtocol::HEADER_A: + if (input == 'A') + { + state = AwaProtocol::HEADER_w; + } + break; + + case AwaProtocol::HEADER_w: + if (input == 'w') + state = AwaProtocol::HEADER_a; + else + { + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::HEADER_a: + if (input == 'a') + { + isVersion2 = false; + state = AwaProtocol::HEADER_HI; + } + else if (input == 'A') + { + state = AwaProtocol::HEADER_HI; + isVersion2 = true; + } + else + { + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::HEADER_HI: + + stat_frames++; + + count = input << 8; + + CRC = input; + fletcher1 = 0; + fletcher2 = 0; + fletcherExt = 0; + state = AwaProtocol::HEADER_LO; + break; + + case AwaProtocol::HEADER_LO: + count += input + 1; + + if (ledCount != count || isChannelCalib != isVersion2) + { + ledCount = count; + isChannelCalib = isVersion2; + pixelCount = ledCount * 3; + + if (isChannelCalib) + prepareCalibration(); + + InitLeds(ledCount, pixelCount, isChannelCalib); + } + + CRC = CRC ^ input ^ 0x55; + + state = AwaProtocol::HEADER_CRC; + + break; + + case AwaProtocol::HEADER_CRC: + + // Check, if incomplete package information was skipped, set bytesread to headersize and skip wrong input + if (bytesRead != headerSize) + { + stat_bad_skip++; + bytesRead = headerSize; + } + + currentPixel = 0; + if (CRC == input) + { + state = AwaProtocol::PIXEL; + } + else + { + // CRC failure + stat_bad++; + stat_bad_crc++; + + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::PIXEL: + ledBuffer[currentPixel++] = input; + if (currentPixel == pixelCount) + { + if (isChannelCalib) + state = AwaProtocol::CHANNELCALIB_GAIN; + else + state = AwaProtocol::FLETCHER1; + } + break; + + case AwaProtocol::CHANNELCALIB_GAIN: + ledBuffer[currentPixel++] = input; + state = AwaProtocol::CHANNELCALIB_RED; + break; + + case AwaProtocol::CHANNELCALIB_RED: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::CHANNELCALIB_GREEN; + break; + + case AwaProtocol::CHANNELCALIB_GREEN: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::CHANNELCALIB_BLUE; + break; + + case AwaProtocol::CHANNELCALIB_BLUE: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::FLETCHER1; + break; + + case AwaProtocol::FLETCHER1: + fletcher1 = input; + + state = AwaProtocol::FLETCHER2; + break; + + case AwaProtocol::FLETCHER2: + fletcher2 = input; + + state = AwaProtocol::FLETCHER_EXT; + break; + + case AwaProtocol::FLETCHER_EXT: + fletcherExt = input; + ledsComplete = true; + + state = AwaProtocol::HEADER_A; + break; + } + } +} + +void setup() +{ + // Init serial port + int bufSize = Serial.setRxBufferSize(SERIAL_SIZE_RX); + Serial.begin(serialSpeed); + Serial.setTimeout(50); + +#ifndef ENABLE_STRIP + Serial2.begin(serial2Speed); + + Serial2.println(); + Serial2.println("Welcome!"); + Serial2.println("Hyperion Awa driver " + version); + Serial2.println("!!! Debug Output !!!"); +#endif + + // Display config + Serial.println(); + Serial.println("Welcome!"); + Serial.println("Hyperion Awa driver " + version); + Serial.print("(Build: "); + Serial.print(__DATE__); + Serial.print(" "); + Serial.print(__TIME__); + Serial.println(")"); + + // first LED info + if (skipFirstLed) + Serial.println("First LED: disabled"); + else + Serial.println("First LED: enabled"); + + // RGBW claibration info +#ifdef THIS_IS_RGBW +#ifdef COLD_WHITE + Serial.println("Default color mode: RGBW cold"); +#else + Serial.println("Default color mode: RGBW neutral"); +#endif + prepareCalibration(); +#else + Serial.println("Color mode: RGB"); +#endif + + InitLeds(ledCount, pixelCount); +} + +void prepareCalibration() +{ +#ifdef THIS_IS_RGBW + // prepare LUT calibration table, cold white is much better than "neutral" white + for (uint32_t i = 0; i < 256; i++) + { + // color calibration + float red = rCorrection * i; // adjust red + float green = gCorrection * i; // adjust green + float blue = bCorrection * i; // adjust blue + + wChannel[i] = (uint8_t)round(min(whiteLimit * i, 255.0f)); + rChannel[i] = (uint8_t)round(min(red / 0xFF, 255.0f)); + gChannel[i] = (uint8_t)round(min(green / 0xFF, 255.0f)); + bChannel[i] = (uint8_t)round(min(blue / 0xFF, 255.0f)); + } + + Serial.write("RGBW calibration. White limit(%): "); + Serial.print(whiteLimit * 100.0f); + Serial.write(" %, red: "); + Serial.print(rCorrection); + Serial.write(" , green: "); + Serial.print(gCorrection); + Serial.write(" , blue: "); + Serial.print(bCorrection); + Serial.println(); +#endif +} + +void loop() +{ + curTime = millis(); + +#ifdef __AVR__ + // nothing , USART Interrupt is implemented + ESPserialEvent(); +#else + // ESP8266 polling + ESPserialEvent(); +#endif + + if (ledsComplete) + { +#ifndef ENABLE_STRIP + if (reportInput) + { + Serial2.println(); + Serial2.print(" L: "); + printStringHex(inputString); + Serial2.println("<\input>"); + inputString = ""; + + Serial2.print("bytesRead: "); + Serial2.print(bytesRead); + Serial2.print(" , currentPixel: "); + Serial2.print(currentPixel); + Serial2.print(" ,pixelCount: "); + Serial2.print(pixelCount); + Serial2.println(); + } +#endif + + int frameSize = headerSize + ledBufferSize + trailerSize; + + if (bytesRead > frameSize) + { + //Add number of frames ignored on top of frame + int frames = bytesRead / frameSize; + stat_frames += frames; + + //Count frame plus frames ignored as bad frames + int badFrames = frames + 1; + stat_bad += badFrames; + stat_bad_frame += badFrames; + } + else + { + +#ifdef ENABLE_CHECK_FLETCHER + //Test if content is valid + uint16_t item = 0; + uint16_t fletch1 = 0; + uint16_t fletch2 = 0; + uint16_t fletchExt = 0; + + while (item < ledBufferSize) + { + fletch1 = (fletch1 + (uint16_t)ledBuffer[item]) % 255; + fletch2 = (fletch2 + fletch1) % 255; + fletcherExt = (fletcherExt + ((uint16_t)ledBuffer[item] ^ (item))) % 255; + ++item; + } + if ((fletch1 == fletcher1) && (fletch2 == fletcher2) && (ledBuffer[item-1] == (fletcherExt != 0x41) ? fletcherExt : 0xaa)) + { +#endif + stat_good++; + + uint16_t startLed = 0; + if (skipFirstLed) + { +#ifdef ENABLE_STRIP + #ifdef THIS_IS_RGBW + strip->SetPixelColor(startLed, RgbwColor(0, 0, 0, 0)); + #else + strip->SetPixelColor(startLed, RgbColor(0, 0, 0)); + #endif +#endif + startLed = 1; + } + + for (uint16_t led = startLed; led < ledCount; ++led) + { + inputColor.R = ledBuffer[led * 3]; + inputColor.G = ledBuffer[led * 3 + 1]; + inputColor.B = ledBuffer[led * 3 + 2]; + + #ifdef THIS_IS_RGBW + inputColor.W = min(rChannel[inputColor.R], + min(gChannel[inputColor.G], + bChannel[inputColor.B])); + inputColor.R -= rChannel[inputColor.W]; + inputColor.G -= gChannel[inputColor.W]; + inputColor.B -= bChannel[inputColor.W]; + inputColor.W = wChannel[inputColor.W]; + #endif +#ifdef ENABLE_STRIP + strip->SetPixelColor(led, inputColor); +#endif + } + + showMe(); + yield(); + + #ifdef THIS_IS_RGBW + if (isChannelCalib) + { + uint8_t incoming_gain = ledBuffer[pixelCount]; + uint8_t incoming_red = ledBuffer[pixelCount + 1]; + uint8_t incoming_green = ledBuffer[pixelCount + 2]; + uint8_t incoming_blue = ledBuffer[pixelCount + 3]; + + float final_limit = (incoming_gain != 255) ? incoming_gain / 255.0f : 1.0f; + if (rCorrection != incoming_red || gCorrection != incoming_green || bCorrection != incoming_blue || whiteLimit != final_limit) + { + rCorrection = incoming_red; + gCorrection = incoming_green; + bCorrection = incoming_blue; + whiteLimit = final_limit; + prepareCalibration(); + } + } + #endif + +#ifdef ENABLE_CHECK_FLETCHER + } + else + { + stat_bad++; + stat_bad_fletcher++; + } +#endif + } + + bytesRead = 0; + state = AwaProtocol::HEADER_A; + + ledsComplete = false; + } + + if ((curTime - stat_start > reportStatInterval_ms)) + { + if (stat_frames > 0) + { + showStats(); + } + } +} + +#ifdef __AVR__ +void serialEvent() +{ + processSerialData(); +} +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) +void ESPserialEvent() +{ + processSerialData(); +} +#endif diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 6421d6de..5cff7fd3 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -2379,23 +2379,29 @@ function updateElementsWled(ledType, key) { } else { //If failed to get properties var hardwareLedCount; + var segmentConfig = false; if (configuredDeviceType == ledType && configuredHost == host) { // Populate elements from existing configuration + if (window.serverConfig.device.segments) { + segmentConfig = true; + } + hardwareLedCount = window.serverConfig.device.hardwareLedCount; + } else { + // Populate elements with default values + hardwareLedCount = 1; + } + + if (segmentConfig) { var configuredstreamSegmentId = window.serverConfig.device.segments.streamSegmentId.toString(); enumSegSelectVals = [configuredstreamSegmentId]; enumSegSelectTitleVals = ["Segment " + configuredstreamSegmentId]; enumSegSelectDefaultVal = configuredstreamSegmentId; - - hardwareLedCount = window.serverConfig.device.hardwareLedCount; } else { - // Populate elements with default values defaultSegmentId = "-1"; enumSegSelectVals.push(defaultSegmentId); enumSegSelectTitleVals.push($.i18n('edt_dev_spec_segments_disabled_title')); enumSegSelectDefaultVal = defaultSegmentId; - - hardwareLedCount = 1; } conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); } diff --git a/assets/webconfig/js/content_remote.js b/assets/webconfig/js/content_remote.js index d0c1d9c2..e689c885 100644 --- a/assets/webconfig/js/content_remote.js +++ b/assets/webconfig/js/content_remote.js @@ -97,7 +97,7 @@ $(document).ready(function () { } function updateInputSelect() { - $('.sstbody').html(""); + $('.sstbody').empty(); var prios = window.serverInfo.priorities; var clearAll = false; diff --git a/include/hyperion/LinearColorSmoothing.h b/include/hyperion/LinearColorSmoothing.h index 5636c278..d22c5da5 100644 --- a/include/hyperion/LinearColorSmoothing.h +++ b/include/hyperion/LinearColorSmoothing.h @@ -289,6 +289,9 @@ private: int _currentConfigId; bool _enabled; + //The system enable state, to restore smoothing state after effect with smoothing ran + bool _enabledSystemCfg; + /// The type of smoothing to perform SmoothingType _smoothingType; diff --git a/libsrc/effectengine/EffectEngine.cpp b/libsrc/effectengine/EffectEngine.cpp index f192df99..2c5ab521 100644 --- a/libsrc/effectengine/EffectEngine.cpp +++ b/libsrc/effectengine/EffectEngine.cpp @@ -121,11 +121,17 @@ void EffectEngine::handleUpdatedEffectList() // add smoothing configurations to Hyperion if (def.args["smoothing-custom-settings"].toBool()) { + int settlingTime_ms = def.args["smoothing-time_ms"].toInt(); + double ledUpdateFrequency_hz = def.args["smoothing-updateFrequency"].toDouble(); + unsigned updateDelay {0}; + + Debug(_log, "Effect \"%s\": Add custom smoothing settings [%d]. Type: Linear, Settling time: %dms, Interval: %.fHz ", QSTRING_CSTR(def.name), specificId, settlingTime_ms, ledUpdateFrequency_hz); + def.smoothCfg = _hyperion->updateSmoothingConfig( - ++specificId, - def.args["smoothing-time_ms"].toInt(), - def.args["smoothing-updateFrequency"].toDouble(), - 0 ); + ++specificId, + settlingTime_ms, + ledUpdateFrequency_hz, + updateDelay ); } else { @@ -155,11 +161,18 @@ int EffectEngine::runEffect(const QString &effectName, const QJsonObject &args, //In case smoothing information is provided dynamically use temp smoothing config item (2) if (smoothCfg == SmoothingConfigID::SYSTEM && args["smoothing-custom-settings"].toBool()) { + int settlingTime_ms = args["smoothing-time_ms"].toInt(); + double ledUpdateFrequency_hz = args["smoothing-updateFrequency"].toDouble(); + unsigned updateDelay {0}; + + Debug(_log, "Effect \"%s\": Apply dynamic smoothing settings, if smoothing. Type: Linear, Settling time: %dms, Interval: %.fHz ", QSTRING_CSTR(effectName), settlingTime_ms, ledUpdateFrequency_hz); + smoothCfg = _hyperion->updateSmoothingConfig( - SmoothingConfigID::EFFECT_DYNAMIC, - args["smoothing-time_ms"].toInt(), - args["smoothing-updateFrequency"].toDouble(), - 0 ); + SmoothingConfigID::EFFECT_DYNAMIC, + settlingTime_ms, + ledUpdateFrequency_hz, + updateDelay + ); } if (pythonScript.isEmpty()) diff --git a/libsrc/hyperion/LinearColorSmoothing.cpp b/libsrc/hyperion/LinearColorSmoothing.cpp index 4cf6d4b9..c8d4d657 100644 --- a/libsrc/hyperion/LinearColorSmoothing.cpp +++ b/libsrc/hyperion/LinearColorSmoothing.cpp @@ -70,6 +70,7 @@ LinearColorSmoothing::LinearColorSmoothing(const QJsonDocument &config, Hyperion , _pause(false) , _currentConfigId(SmoothingConfigID::SYSTEM) , _enabled(false) + , _enabledSystemCfg(false) , _smoothingType(SmoothingType::Linear) , tempValues(std::vector(0, 0L)) { @@ -114,11 +115,12 @@ void LinearColorSmoothing::handleSettingsUpdate(settings::type type, const QJson QJsonObject obj = config.object(); setEnable(obj["enable"].toBool(_enabled)); + _enabledSystemCfg = _enabled; - SmoothingCfg cfg(false, - static_cast(obj[SETTINGS_KEY_SETTLING_TIME].toInt(DEFAULT_SETTLINGTIME)), - static_cast(MS_PER_MICRO / obj[SETTINGS_KEY_UPDATE_FREQUENCY].toDouble(DEFAULT_UPDATEFREQUENCY)) - ); + int64_t settlingTime_ms = static_cast(obj[SETTINGS_KEY_SETTLING_TIME].toInt(DEFAULT_SETTLINGTIME)); + int _updateInterval_ms =static_cast(MS_PER_MICRO / obj[SETTINGS_KEY_UPDATE_FREQUENCY].toDouble(DEFAULT_UPDATEFREQUENCY)); + + SmoothingCfg cfg(false, settlingTime_ms, _updateInterval_ms); const QString typeString = obj[SETTINGS_KEY_SMOOTHING_TYPE].toString(); @@ -162,7 +164,10 @@ int LinearColorSmoothing::write(const std::vector &ledValues) _previousValues = ledValues; _previousInterpolationTime = micros(); - _timer->start(_updateInterval); + if (!_pause) + { + _timer->start(_updateInterval); + } } return 0; @@ -510,6 +515,8 @@ void LinearColorSmoothing::clearRememberedFrames() void LinearColorSmoothing::queueColors(const std::vector &ledColors) { + assert (ledColors.size() > 0); + if (_outputDelay == 0) { // No output delay => immediate write @@ -558,13 +565,16 @@ void LinearColorSmoothing::componentStateChange(hyperion::Components component, void LinearColorSmoothing::setEnable(bool enable) { - _enabled = enable; - if (!_enabled) + if ( _enabled != enable) { - clearQueuedColors(); + _enabled = enable; + if (!_enabled) + { + clearQueuedColors(); + } + // update comp register + _hyperion->setNewComponentState(hyperion::COMP_SMOOTHING, enable); } - // update comp register - _hyperion->setNewComponentState(hyperion::COMP_SMOOTHING, enable); } void LinearColorSmoothing::setPause(bool pause) @@ -603,7 +613,7 @@ unsigned LinearColorSmoothing::updateConfig(int cfgID, int settlingTime_ms, doub updateDelay }; _cfgList[updatedCfgID] = cfg; - DebugIf(verbose && _enabled, _log,"%s", QSTRING_CSTR(getConfig(updatedCfgID))); + Debug(_log,"%s", QSTRING_CSTR(getConfig(updatedCfgID))); } else { @@ -660,6 +670,19 @@ bool LinearColorSmoothing::selectConfig(int cfgID, bool force) _interpolationCounter = 0; _interpolationStatCounter = 0; + //Enable smoothing for effects with smoothing + if (cfgID >= SmoothingConfigID::EFFECT_DYNAMIC) + { + Debug(_log,"Run Effect with Smoothing enabled"); + _enabledSystemCfg = _enabled; + setEnable(true); + } + else + { + // Restore enabled state after running an effect with smoothing + setEnable(_enabledSystemCfg); + } + if (_cfgList[cfgID]._updateInterval != _updateInterval) { @@ -667,7 +690,10 @@ bool LinearColorSmoothing::selectConfig(int cfgID, bool force) _updateInterval = _cfgList[cfgID]._updateInterval; if (this->enabled()) { - _timer->start(_updateInterval); + if (!_pause && !_targetValues.empty()) + { + _timer->start(_updateInterval); + } } } _currentConfigId = cfgID; @@ -689,30 +715,36 @@ QString LinearColorSmoothing::getConfig(int cfgID) { SmoothingCfg cfg = _cfgList[cfgID]; - configText = QString ("[%1] - type: %2, pause: %3, settlingTime: %4ms, interval: %5ms (%6Hz), delay: %7 frames") - .arg(cfgID) - .arg(SmoothingCfg::EnumToString(cfg._type),(cfg._pause) ? "true" : "false") - .arg(cfg._settlingTime) - .arg(cfg._updateInterval) - .arg(int(MS_PER_MICRO/cfg._updateInterval)) - .arg(cfg._outputDelay); + configText = QString ("[%1] - Type: %2, Pause: %3") + .arg(cfgID) + .arg(SmoothingCfg::EnumToString(cfg._type),(cfg._pause) ? "true" : "false") ; switch (cfg._type) { - case SmoothingType::Linear: - break; - case SmoothingType::Decay: { const double thalf = (1.0-std::pow(1.0/2, 1.0/_decay))*_settlingTime; - configText += QString (", interpolationRate: %1Hz, dithering: %2, decay: %3 -> halftime: %4ms") - .arg(cfg._interpolationRate,0,'f',2) - .arg((cfg._dithering) ? "true" : "false") - .arg(cfg._decay,0,'f',2) - .arg(thalf,0,'f',2); + configText += QString (", Interpolation rate: %1Hz, Dithering: %2, decay: %3 -> Halftime: %4ms") + .arg(cfg._interpolationRate,0,'f',2) + .arg((cfg._dithering) ? "true" : "false") + .arg(cfg._decay,0,'f',2) + .arg(thalf,0,'f',2); + [[fallthrough]]; + } + + case SmoothingType::Linear: + { + configText += QString (", Settling time: %1ms, Interval: %2ms (%3Hz)") + .arg(cfg._settlingTime) + .arg(cfg._updateInterval) + .arg(int(MS_PER_MICRO/cfg._updateInterval)); break; } } + + configText += QString (", delay: %1 frames") + .arg(cfg._outputDelay); } + return configText; } @@ -736,7 +768,6 @@ LinearColorSmoothing::SmoothingCfg::SmoothingCfg(bool pause, int64_t settlingTim { } - QString LinearColorSmoothing::SmoothingCfg::EnumToString(SmoothingType type) { if (type == SmoothingType::Linear) { diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.cpp b/libsrc/leddevice/dev_net/LedDeviceWled.cpp index 7b4fed2a..3aa4662f 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceWled.cpp @@ -523,7 +523,10 @@ bool LedDeviceWled::restoreState() _originalStateProperties[STATE_LIVE] = false; _originalStateProperties[STATE_TRANSITIONTIME_CURRENTCALL] = 0; - _originalStateProperties[STATE_ON] = _isStayOnAfterStreaming; + if (_isStayOnAfterStreaming) + { + _originalStateProperties[STATE_ON] = true; + } httpResponse response = _restApi->put(_originalStateProperties); if ( response.error() ) diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.cpp b/libsrc/leddevice/dev_net/ProviderRestApi.cpp index 0d318e56..d2732f8e 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.cpp +++ b/libsrc/leddevice/dev_net/ProviderRestApi.cpp @@ -20,25 +20,34 @@ enum HttpStatusCode { NoContent = 204, BadRequest = 400, UnAuthorized = 401, + Forbidden = 403, NotFound = 404 }; -constexpr std::chrono::milliseconds DEFAULT_REST_TIMEOUT{ 400 }; - } //End of constants -ProviderRestApi::ProviderRestApi(const QString& host, int port, const QString& basePath) - :_log(Logger::getInstance("LEDDEVICE")) - , _networkManager(nullptr) +ProviderRestApi::ProviderRestApi(const QString& scheme, const QString& host, int port, const QString& basePath) + : _log(Logger::getInstance("LEDDEVICE")) + , _networkManager(nullptr) + , _requestTimeout(DEFAULT_REST_TIMEOUT) { _networkManager = new QNetworkAccessManager(); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)) + _networkManager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); +#endif - _apiUrl.setScheme("http"); + _apiUrl.setScheme(scheme); _apiUrl.setHost(host); _apiUrl.setPort(port); _basePath = basePath; } +ProviderRestApi::ProviderRestApi(const QString& scheme, const QString& host, int port) + : ProviderRestApi(scheme, host, port, "") {} + +ProviderRestApi::ProviderRestApi(const QString& host, int port, const QString& basePath) +: ProviderRestApi("http", host, port, basePath) {} + ProviderRestApi::ProviderRestApi(const QString& host, int port) : ProviderRestApi(host, port, "") {} @@ -62,6 +71,12 @@ void ProviderRestApi::setBasePath(const QString& basePath) appendPath(_basePath, basePath); } +void ProviderRestApi::setPath(const QStringList& pathElements) +{ + _path.clear(); + appendPath(_path, pathElements.join(ONE_SLASH)); +} + void ProviderRestApi::setPath(const QString& path) { _path.clear(); @@ -73,6 +88,11 @@ void ProviderRestApi::appendPath(const QString& path) appendPath(_path, path); } +void ProviderRestApi::appendPath(const QStringList& pathElements) +{ + appendPath(_path, pathElements.join(ONE_SLASH)); +} + void ProviderRestApi::appendPath ( QString& path, const QString &appendPath) { if (!appendPath.isEmpty() && appendPath != ONE_SLASH) @@ -132,40 +152,7 @@ httpResponse ProviderRestApi::get() httpResponse ProviderRestApi::get(const QUrl& url) { - // Perform request - QNetworkRequest request(_networkRequestHeaders); - request.setUrl(url); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - _networkManager->setTransferTimeout(DEFAULT_REST_TIMEOUT.count()); -#endif - - QNetworkReply* reply = _networkManager->get(request); - - // Connect requestFinished signal to quit slot of the loop. - QEventLoop loop; - QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - ReplyTimeout::set(reply, DEFAULT_REST_TIMEOUT.count()); -#endif - - // Go into the loop until the request is finished. - loop.exec(); - - httpResponse response; - if (reply->operation() == QNetworkAccessManager::GetOperation) - { - if(reply->error() != QNetworkReply::NoError) - { - Debug(_log, "GET: [%s]", QSTRING_CSTR( url.toString() )); - } - response = getResponse(reply ); - } - // Free space. - reply->deleteLater(); - // Return response - return response; + return executeOperation(QNetworkAccessManager::GetOperation, url); } httpResponse ProviderRestApi::put(const QJsonObject &body) @@ -180,40 +167,7 @@ httpResponse ProviderRestApi::put(const QString &body) httpResponse ProviderRestApi::put(const QUrl &url, const QByteArray &body) { - // Perform request - QNetworkRequest request(_networkRequestHeaders); - request.setUrl(url); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - _networkManager->setTransferTimeout(DEFAULT_REST_TIMEOUT.count()); -#endif - - QNetworkReply* reply = _networkManager->put(request, body); - // Connect requestFinished signal to quit slot of the loop. - QEventLoop loop; - QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - ReplyTimeout::set(reply, DEFAULT_REST_TIMEOUT.count()); -#endif - - // Go into the loop until the request is finished. - loop.exec(); - - httpResponse response; - if (reply->operation() == QNetworkAccessManager::PutOperation) - { - if(reply->error() != QNetworkReply::NoError) - { - Debug(_log, "PUT: [%s] [%s]", QSTRING_CSTR( url.toString() ),body.constData() ); - } - response = getResponse(reply); - } - // Free space. - reply->deleteLater(); - - // Return response - return response; + return executeOperation(QNetworkAccessManager::PutOperation, url, body); } httpResponse ProviderRestApi::post(const QJsonObject& body) @@ -228,76 +182,69 @@ httpResponse ProviderRestApi::post(const QString& body) httpResponse ProviderRestApi::post(const QUrl& url, const QByteArray& body) { - // Perform request - QNetworkRequest request(_networkRequestHeaders); - request.setUrl(url); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - _networkManager->setTransferTimeout(DEFAULT_REST_TIMEOUT.count()); -#endif - - QNetworkReply* reply = _networkManager->post(request, body); - // Connect requestFinished signal to quit slot of the loop. - QEventLoop loop; - QEventLoop::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit); - -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - ReplyTimeout::set(reply, DEFAULT_REST_TIMEOUT.count()); -#endif - - // Go into the loop until the request is finished. - loop.exec(); - - httpResponse response; - if (reply->operation() == QNetworkAccessManager::PostOperation) - { - if(reply->error() != QNetworkReply::NoError) - { - Debug(_log, "POST: [%s] [%s]", QSTRING_CSTR( url.toString() ),body.constData() ); - } - response = getResponse(reply); - } - // Free space. - reply->deleteLater(); - - // Return response - return response; + return executeOperation(QNetworkAccessManager::PostOperation, url, body); } httpResponse ProviderRestApi::deleteResource(const QUrl& url) +{ + return executeOperation(QNetworkAccessManager::DeleteOperation, url); +} + +httpResponse ProviderRestApi::executeOperation(QNetworkAccessManager::Operation operation, const QUrl& url, const QByteArray& body) { // Perform request QNetworkRequest request(_networkRequestHeaders); request.setUrl(url); + request.setOriginatingObject(this); #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - _networkManager->setTransferTimeout(DEFAULT_REST_TIMEOUT.count()); + _networkManager->setTransferTimeout(_requestTimeout.count()); #endif - QNetworkReply* reply = _networkManager->deleteResource(request); + QDateTime start = QDateTime::currentDateTime(); + QString opCode; + QNetworkReply* reply; + switch (operation) { + case QNetworkAccessManager::GetOperation: + opCode = "GET"; + reply = _networkManager->get(request); + break; + case QNetworkAccessManager::PutOperation: + opCode = "PUT"; + reply = _networkManager->put(request, body); + break; + case QNetworkAccessManager::PostOperation: + opCode = "POST"; + reply = _networkManager->post(request, body); + break; + case QNetworkAccessManager::DeleteOperation: + opCode = "DELETE"; + reply = _networkManager->deleteResource(request); + break; + default: + Error(_log, "Unsupported operation"); + return httpResponse(); + } + // Connect requestFinished signal to quit slot of the loop. QEventLoop loop; QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - // Go into the loop until the request is finished. - loop.exec(); #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - ReplyTimeout::set(reply, DEFAULT_REST_TIMEOUT.count()); + ReplyTimeout* timeout = ReplyTimeout::set(reply, _requestTimeout.count()); #endif - httpResponse response; - if (reply->operation() == QNetworkAccessManager::DeleteOperation) - { - if(reply->error() != QNetworkReply::NoError) - { - Debug(_log, "DELETE: [%s]", QSTRING_CSTR(url.toString())); - } - response = getResponse(reply); - } + // Go into the loop until the request is finished. + loop.exec(); + QDateTime end = QDateTime::currentDateTime(); + + httpResponse response = (reply->operation() == operation) ? getResponse(reply) : httpResponse(); + + Debug(_log, "%s took %lldms, HTTP %d: [%s] [%s]", QSTRING_CSTR(opCode), start.msecsTo(end), response.getHttpStatusCode(), QSTRING_CSTR(url.toString()), body.constData()); + // Free space. reply->deleteLater(); - // Return response return response; } @@ -311,34 +258,31 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply) if (reply->error() == QNetworkReply::NoError) { - if ( httpStatusCode != HttpStatusCode::NoContent ){ - QByteArray replyData = reply->readAll(); + QByteArray replyData = reply->readAll(); - if (!replyData.isEmpty()) + if (!replyData.isEmpty()) + { + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(replyData, &error); + + if (error.error != QJsonParseError::NoError) { - QJsonParseError error; - QJsonDocument jsonDoc = QJsonDocument::fromJson(replyData, &error); - - if (error.error != QJsonParseError::NoError) - { - //Received not valid JSON response - response.setError(true); - response.setErrorReason(error.errorString()); - } - else - { - response.setBody(jsonDoc); - } + //Received not valid JSON response + response.setError(true); + response.setErrorReason(error.errorString()); } else - { // Create valid body which is empty - response.setBody(QJsonDocument()); + { + response.setBody(jsonDoc); } } + else + { // Create valid body which is empty + response.setBody(QJsonDocument()); + } } else { - Debug(_log, "Reply.httpStatusCode [%d]", httpStatusCode ); QString errorReason; if (httpStatusCode > 0) { QString httpReason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); @@ -350,25 +294,32 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply) case HttpStatusCode::UnAuthorized: advise = "Check Authentication Token (API Key)"; break; + case HttpStatusCode::Forbidden: + advise = "No permission to access the given resource"; + break; case HttpStatusCode::NotFound: advise = "Check Resource given"; break; default: + advise = httpReason; break; } errorReason = QString ("[%3 %4] - %5").arg(httpStatusCode).arg(httpReason, advise); } else { - errorReason = reply->errorString(); + if (reply->error() == QNetworkReply::OperationCanceledError) { - response.setError(true); - response.setErrorReason(errorReason); + errorReason = "Network request timeout error"; + } + else + { + errorReason = reply->errorString(); } - } - // Create valid body which is empty - response.setBody(QJsonDocument()); + } + response.setError(true); + response.setErrorReason(errorReason); } return response; } @@ -388,3 +339,8 @@ void ProviderRestApi::setHeader(QNetworkRequest::KnownHeaders header, const QVar } } } + +void ProviderRestApi::setHeader(const QByteArray &headerName, const QByteArray &headerValue) +{ + _networkRequestHeaders.setRawHeader(headerName, headerValue); +} diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.h b/libsrc/leddevice/dev_net/ProviderRestApi.h index a87c5f2c..8d8e0a83 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.h +++ b/libsrc/leddevice/dev_net/ProviderRestApi.h @@ -13,15 +13,22 @@ #include #include +#include + +constexpr std::chrono::milliseconds DEFAULT_REST_TIMEOUT{ 1000 }; + //Set QNetworkReply timeout without external timer //https://stackoverflow.com/questions/37444539/how-to-set-qnetworkreply-timeout-without-external-timer -class ReplyTimeout : public QObject { +class ReplyTimeout : public QObject +{ Q_OBJECT + public: enum HandleMethod { Abort, Close }; + ReplyTimeout(QNetworkReply* reply, const int timeout, HandleMethod method = Abort) : - QObject(reply), m_method(method) + QObject(reply), m_method(method), m_timedout(false) { Q_ASSERT(reply); if (reply && reply->isRunning()) { @@ -29,20 +36,30 @@ public: connect(reply, &QNetworkReply::finished, this, &QObject::deleteLater); } } - static void set(QNetworkReply* reply, const int timeout, HandleMethod method = Abort) + + bool isTimedout() const { - new ReplyTimeout(reply, timeout, method); + return m_timedout; } + static ReplyTimeout * set(QNetworkReply* reply, const int timeout, HandleMethod method = Abort) + { + return new ReplyTimeout(reply, timeout, method); + } + +signals: + void timedout(); + protected: - QBasicTimer m_timer; - HandleMethod m_method; + void timerEvent(QTimerEvent * ev) override { if (!m_timer.isActive() || ev->timerId() != m_timer.timerId()) return; auto reply = static_cast(parent()); if (reply->isRunning()) { + m_timedout = true; + emit timedout(); if (m_method == Close) reply->close(); else if (m_method == Abort) @@ -50,6 +67,10 @@ protected: m_timer.stop(); } } + + QBasicTimer m_timer; + HandleMethod m_method; + bool m_timedout; }; /// @@ -104,11 +125,12 @@ private: /// ///@endcode /// -class ProviderRestApi +class ProviderRestApi : public QObject { + Q_OBJECT + public: - /// /// @brief Constructor of the REST-API wrapper /// ProviderRestApi(); @@ -121,6 +143,15 @@ public: /// explicit ProviderRestApi(const QString& host, int port); + /// + /// @brief Constructor of the REST-API wrapper + /// + /// @param[in] scheme + /// @param[in] host + /// @param[in] port + /// + explicit ProviderRestApi(const QString& scheme, const QString& host, int port); + /// /// @brief Constructor of the REST-API wrapper /// @@ -130,10 +161,20 @@ public: /// explicit ProviderRestApi(const QString& host, int port, const QString& basePath); + /// + /// @brief Constructor of the REST-API wrapper + /// + /// @param[in] scheme + /// @param[in] host + /// @param[in] port + /// @param[in] API base-path + /// + explicit ProviderRestApi(const QString& scheme, const QString& host, int port, const QString& basePath); + /// /// @brief Destructor of the REST-API wrapper /// - virtual ~ProviderRestApi(); + virtual ~ProviderRestApi() override; /// /// @brief Set an API's host @@ -177,6 +218,12 @@ public: /// void setPath(const QString& path); + /// @brief Set an API's path to address resources + /// + /// @param[in] pathElements to form a path, e.g. (lights,1,state) results in "/lights/1/state/" + /// + void setPath(const QStringList& pathElements); + /// /// @brief Append an API's path element to path set before /// @@ -184,6 +231,13 @@ public: /// void appendPath(const QString& appendPath); + /// + /// @brief Append API's path elements to path set before + /// + /// @param[in] pathElements + /// + void appendPath(const QStringList& pathElements); + /// /// @brief Set an API's fragment /// @@ -283,14 +337,28 @@ public: /// @param[in] The type of the header field. /// @param[in] The value of the header field. /// If the header field exists, the value will be combined as comma separated string. - void setHeader(QNetworkRequest::KnownHeaders header, const QVariant& value); + /// + /// Set a header field. + /// + /// @param[in] The type of the header field. + /// @param[in] The value of the header field. + /// If the header field exists, the value will override the previous setting. + void setHeader(const QByteArray &headerName, const QByteArray &headerValue); + /// /// Remove all header fields. /// void removeAllHeaders() { _networkRequestHeaders = QNetworkRequest(); } + /// + /// Sets the timeout time frame after a request is aborted + /// Zero means no timer is set. + /// + /// @param[in] timeout in milliseconds. + void setTransferTimeout(std::chrono::milliseconds timeout = DEFAULT_REST_TIMEOUT) { _requestTimeout = timeout; } + /// /// @brief Set the common logger for LED-devices. /// @@ -308,10 +376,14 @@ private: /// static void appendPath (QString &path, const QString &appendPath) ; + + httpResponse executeOperation(QNetworkAccessManager::Operation op, const QUrl& url, const QByteArray& body = {}); + Logger* _log; // QNetworkAccessManager object for sending REST-requests. QNetworkAccessManager* _networkManager; + std::chrono::milliseconds _requestTimeout; QUrl _apiUrl; diff --git a/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp b/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp index d3b9dcce..8c45e233 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp +++ b/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp @@ -94,7 +94,7 @@ void LedDeviceAdalight::prepareHeader() break; case Adalight::AWA: - _bufferLength += 7; + _bufferLength += 8; [[fallthrough]]; case Adalight::ADA: [[fallthrough]]; @@ -162,14 +162,20 @@ int LedDeviceAdalight::write(const std::vector & ledValues) { whiteChannelExtension(writer); - uint16_t fletcher1 = 0, fletcher2 = 0; + uint16_t fletcher1 = 0; + uint16_t fletcher2 = 0; + uint16_t fletcherExt = 0; + uint8_t position = 0; + while (hasher < writer) { + fletcherExt = (fletcherExt + (*(hasher) ^ (position++))) % 255; fletcher1 = (fletcher1 + *(hasher++)) % 255; fletcher2 = (fletcher2 + fletcher1) % 255; } *(writer++) = static_cast(fletcher1); *(writer++) = static_cast(fletcher2); + *(writer++) = static_cast((fletcherExt != 0x41) ? fletcherExt : 0xaa); } _bufferLength = writer - _ledBuffer.data(); }