Backup RFP Infinity controller state before Resolume changes
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user