Backup RFP Infinity controller state before Resolume changes
Some checks failed
WLED CI / wled_build (push) Has been cancelled
Deploy Nightly / wled_build (push) Has been cancelled
Deploy Nightly / Deploy nightly (push) Has been cancelled

This commit is contained in:
jan
2026-05-14 12:31:13 +02:00
parent ebc4498d89
commit 4bc4e1257e
33 changed files with 3482 additions and 695 deletions

View File

@@ -91,6 +91,268 @@ bool canUseSerial(void) { // WLEDMM returns true if Serial can be used for deb
return true;
} // WLEDMM end
#if defined(WLED_ENABLE_INFINITY_CONTROLLER) && defined(WLED_INFINITY_MASTER)
namespace {
String rfpJsonValue(const String& json, const char* key) {
StaticJsonDocument<384> command;
DeserializationError error = deserializeJson(command, json);
if (error) return String();
return command[key] | "";
}
uint32_t rfpJsonU32(const String& json, const char* key) {
StaticJsonDocument<384> command;
DeserializationError error = deserializeJson(command, json);
if (error) return 0;
return command[key] | 0;
}
uint32_t rfpParseChunkLength(const String& line, String& encoded) {
if (!line.startsWith("RFPCHUNK1 ")) return 0;
const int separator = line.indexOf(' ', 10);
if (separator < 0) return 0;
const int32_t length = line.substring(10, separator).toInt();
encoded = line.substring(separator + 1);
return length > 0 ? static_cast<uint32_t>(length) : 0;
}
void rfpSerialPrintError(const __FlashStringHelper* message) {
Serial.print(F("RFPERR1 {\"error\":\""));
Serial.print(message);
Serial.println(F("\"}"));
}
void rfpSerialPrintErrorDetail(const __FlashStringHelper* message, uint32_t a, uint32_t b) {
Serial.print(F("RFPERR1 {\"error\":\""));
Serial.print(message);
Serial.print(F("\",\"a\":"));
Serial.print(a);
Serial.print(F(",\"b\":"));
Serial.print(b);
Serial.println(F("}"));
}
int8_t rfpBase64Value(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
int rfpDecodeBase64Chunk(const String& encoded, uint8_t* output, size_t outputSize) {
uint32_t accumulator = 0;
uint8_t bits = 0;
size_t written = 0;
for (uint16_t i = 0; i < encoded.length(); i++) {
const char c = encoded[i];
if (c == '=') break;
const int8_t value = rfpBase64Value(c);
if (value < 0) return -1;
accumulator = (accumulator << 6) | static_cast<uint8_t>(value);
bits += 6;
if (bits >= 8) {
bits -= 8;
if (written >= outputSize) return -1;
output[written++] = static_cast<uint8_t>((accumulator >> bits) & 0xFF);
}
}
return static_cast<int>(written);
}
bool rfpReadHttpBody(WiFiClient& client, String& body, uint32_t timeoutMs) {
const uint32_t start = millis();
bool inBody = false;
String headerTail;
body.reserve(512);
while ((millis() - start) < timeoutMs) {
while (client.available()) {
const char c = static_cast<char>(client.read());
if (!inBody) {
headerTail += c;
if (headerTail.length() > 4) headerTail.remove(0, headerTail.length() - 4);
if (headerTail == "\r\n\r\n") inBody = true;
} else if (body.length() < 2048) {
body += c;
}
}
if (!client.connected() && !client.available()) return inBody;
delay(1);
}
return inBody;
}
void rfpHandleInfoCommand(const String& json) {
const String target = rfpJsonValue(json, "target");
if (target.length() == 0) {
rfpSerialPrintError(F("missing target"));
return;
}
WiFiClient client;
if (!client.connect(target.c_str(), 80)) {
rfpSerialPrintError(F("node connect failed"));
return;
}
client.print(F("GET /json/info HTTP/1.1\r\nHost: "));
client.print(target);
client.print(F("\r\nConnection: close\r\n\r\n"));
String body;
if (!rfpReadHttpBody(client, body, 5000) || body.length() == 0) {
rfpSerialPrintError(F("node info failed"));
client.stop();
return;
}
client.stop();
body.replace("\r", "");
body.replace("\n", "");
Serial.print(F("RFPINFO1 "));
Serial.println(body);
}
void rfpHandleOtaCommand(const String& json) {
const String target = rfpJsonValue(json, "target");
const uint32_t firmwareSize = rfpJsonU32(json, "size");
const uint32_t ackBytes = rfpJsonU32(json, "ackBytes");
if (target.length() == 0 || firmwareSize == 0) {
rfpSerialPrintError(F("missing target or size"));
return;
}
constexpr char boundary[] = "----RFPInfinityOtaBoundary";
const String head =
String("--") + boundary + "\r\n"
"Content-Disposition: form-data; name=\"update\"; filename=\"firmware.bin\"\r\n"
"Content-Type: application/octet-stream\r\n\r\n";
const String tail =
String("\r\n--") + boundary + "\r\n"
"Content-Disposition: form-data; name=\"skipValidation\"\r\n\r\n"
"1\r\n"
"--" + boundary + "--\r\n";
const uint32_t contentLength = head.length() + firmwareSize + tail.length();
WiFiClient client;
if (!client.connect(target.c_str(), 80)) {
rfpSerialPrintError(F("node connect failed"));
return;
}
client.print(F("POST /update?skipValidation=1 HTTP/1.1\r\nHost: "));
client.print(target);
client.print(F("\r\nConnection: close\r\nContent-Type: multipart/form-data; boundary="));
client.print(boundary);
client.print(F("\r\nContent-Length: "));
client.print(contentLength);
client.print(F("\r\n\r\n"));
client.print(head);
Serial.print(F("RFPREADY1 {\"target\":\""));
Serial.print(target);
Serial.print(F("\",\"size\":"));
Serial.print(firmwareSize);
Serial.print(F(",\"ackBytes\":"));
Serial.print(ackBytes);
Serial.print(F(",\"proto\":4"));
Serial.println(F("}"));
Serial.flush();
Serial.setTimeout(20000);
String dataStart = Serial.readStringUntil('\n');
dataStart.trim();
if (dataStart != "RFPDATA1") {
client.stop();
rfpSerialPrintError(F("serial data start timeout"));
return;
}
uint8_t buffer[768];
uint32_t remaining = firmwareSize;
uint32_t nextAck = ackBytes;
uint32_t lastDataMs = millis();
while (remaining > 0) {
String chunkHeader = Serial.readStringUntil('\n');
chunkHeader.trim();
String encoded;
const uint32_t chunkLength = rfpParseChunkLength(chunkHeader, encoded);
if (chunkLength == 0 || chunkLength > remaining) {
client.stop();
rfpSerialPrintError(F("serial chunk header invalid"));
return;
}
if (chunkLength > sizeof(buffer) || encoded.length() == 0) {
client.stop();
rfpSerialPrintError(F("serial chunk too large"));
return;
}
const int decoded = rfpDecodeBase64Chunk(encoded, buffer, sizeof(buffer));
if (decoded != static_cast<int>(chunkLength)) {
client.stop();
rfpSerialPrintErrorDetail(F("serial chunk decode failed"), encoded.length(), decoded < 0 ? 0 : decoded);
return;
}
lastDataMs = millis();
if (client.write(buffer, decoded) != static_cast<size_t>(decoded)) {
client.stop();
rfpSerialPrintError(F("node write failed"));
return;
}
remaining -= decoded;
yield();
const uint32_t received = firmwareSize - remaining;
if (ackBytes > 0 && (received >= nextAck || remaining == 0)) {
Serial.print(F("RFPACK1 {\"bytes\":"));
Serial.print(received);
Serial.println(F("}"));
nextAck += ackBytes;
}
}
client.print(tail);
String body;
rfpReadHttpBody(client, body, 30000);
client.stop();
body.replace("\r", " ");
body.replace("\n", " ");
if (body.length() > 360) body = body.substring(0, 360);
Serial.print(F("RFPDONE1 {\"target\":\""));
Serial.print(target);
Serial.print(F("\",\"bytes\":"));
Serial.print(firmwareSize);
Serial.print(F(",\"response\":\""));
for (uint16_t i = 0; i < body.length(); i++) {
const char c = body[i];
Serial.print(c == '"' || c == '\\' ? '_' : c);
}
Serial.println(F("\"}"));
}
bool rfpHandleSerialCommandLine() {
Serial.setTimeout(2000);
String line = Serial.readStringUntil('\n');
line.trim();
if (line.startsWith("RFPINFO1 ")) {
rfpHandleInfoCommand(line.substring(9));
return true;
}
if (line.startsWith("RFPOTA1 ")) {
rfpHandleOtaCommand(line.substring(8));
return true;
}
rfpSerialPrintError(F("unknown rfp command"));
return true;
}
} // namespace
#endif
void handleSerial()
{
#if !ARDUINO_USB_CDC_ON_BOOT
@@ -155,6 +417,11 @@ void handleSerial()
#endif
} else if (next == 'X') {
forceReconnect = true; // WLEDMM - force reconnect via Serial
} else if (next == 'R') {
#if defined(WLED_ENABLE_INFINITY_CONTROLLER) && defined(WLED_INFINITY_MASTER)
rfpHandleSerialCommandLine();
return;
#endif
} else if (next == 0xB0) {updateBaudRate( 115200);
} else if (next == 0xB1) {updateBaudRate( 230400);
} else if (next == 0xB2) {updateBaudRate( 460800);