/* * VALIS Node Firmware — ESP32-C3 * Variable Arrived Location In Superposition * * Proof of concept: Meilong scatter protocol over serial. * Phase 1: Pi sends message via serial, ESP32 scrambles into 3 XOR shares, * outputs each share on separate "channels" (simulated via tagged serial output). * Pi collects shares and reassembles. * * Phase 2 (with coupling hardware): inject shares onto power line via DAC/ADC. * * "It's a walkie-talkie with math." */ #include #include #define LED_PIN 8 #define MAX_MSG_LEN 256 // === MEILONG PROTOCOL === void scramble(const uint8_t* data, size_t len, uint8_t* s1, uint8_t* s2, uint8_t* s3) { // Generate two random shares, XOR to create third for (size_t i = 0; i < len; i++) { s1[i] = (uint8_t)random(256); s2[i] = (uint8_t)random(256); s3[i] = data[i] ^ s1[i] ^ s2[i]; } } void reassemble(const uint8_t* s1, const uint8_t* s2, const uint8_t* s3, size_t len, uint8_t* out) { for (size_t i = 0; i < len; i++) { out[i] = s1[i] ^ s2[i] ^ s3[i]; } } // === BASE64 (minimal for share transport) === static const char b64chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; String toBase64(const uint8_t* data, size_t len) { String out = ""; for (size_t i = 0; i < len; i += 3) { uint32_t n = ((uint32_t)data[i]) << 16; if (i + 1 < len) n |= ((uint32_t)data[i + 1]) << 8; if (i + 2 < len) n |= data[i + 2]; out += b64chars[(n >> 18) & 0x3F]; out += b64chars[(n >> 12) & 0x3F]; out += (i + 1 < len) ? b64chars[(n >> 6) & 0x3F] : '='; out += (i + 2 < len) ? b64chars[n & 0x3F] : '='; } return out; } // === LED FEEDBACK === void blinkLED(int times, int delayMs) { for (int i = 0; i < times; i++) { digitalWrite(LED_PIN, HIGH); delay(delayMs); digitalWrite(LED_PIN, LOW); delay(delayMs); } } void setLED(bool on) { digitalWrite(LED_PIN, on ? HIGH : LOW); } // === NODE STATE === enum NodeMode { MODE_IDLE, MODE_SCATTER, MODE_COLLECT, MODE_RELAY }; NodeMode currentMode = MODE_IDLE; String nodeId = ""; unsigned long lastHeartbeat = 0; // === SERIAL PROTOCOL === // Commands from Pi: // {"cmd":"scatter","msg":"hello world"} — scramble and output 3 shares // {"cmd":"collect","s1":"...","s2":"...","s3":"..."} — reassemble from 3 shares // {"cmd":"ping"} — heartbeat check // {"cmd":"id"} — report node ID // // Responses to Pi: // {"type":"share","index":0,"data":"base64...","hop":"uuid"} // {"type":"share","index":1,"data":"base64...","hop":"uuid"} // {"type":"share","index":2,"data":"base64...","hop":"uuid"} // {"type":"assembled","msg":"hello world","hop":"uuid"} // {"type":"pong","id":"node-xxxx","uptime":12345} String serialBuffer = ""; void processCommand(const String& json) { JsonDocument doc; DeserializationError err = deserializeJson(doc, json); if (err) { Serial.println("{\"type\":\"error\",\"msg\":\"bad json\"}"); return; } const char* cmd = doc["cmd"]; if (!cmd) return; // === PING === if (strcmp(cmd, "ping") == 0) { JsonDocument resp; resp["type"] = "pong"; resp["id"] = nodeId; resp["uptime"] = millis() / 1000; resp["mode"] = currentMode; serializeJson(resp, Serial); Serial.println(); blinkLED(1, 50); return; } // === ID === if (strcmp(cmd, "id") == 0) { JsonDocument resp; resp["type"] = "identity"; resp["id"] = nodeId; resp["chip"] = "ESP32-C3"; resp["protocol"] = "VALIS/Meilong"; resp["version"] = "0.1"; serializeJson(resp, Serial); Serial.println(); return; } // === SCATTER (Meilong scramble) === if (strcmp(cmd, "scatter") == 0) { const char* msg = doc["msg"]; if (!msg) { Serial.println("{\"type\":\"error\",\"msg\":\"no message\"}"); return; } setLED(true); currentMode = MODE_SCATTER; size_t len = strlen(msg); if (len > MAX_MSG_LEN) len = MAX_MSG_LEN; uint8_t s1[MAX_MSG_LEN], s2[MAX_MSG_LEN], s3[MAX_MSG_LEN]; scramble((const uint8_t*)msg, len, s1, s2, s3); // Generate hop ID String hopId = String(random(100000, 999999)); // Output each share with a staggered delay (simulating different routes) uint8_t* shares[] = {s1, s2, s3}; const char* routes[] = {"alpha", "beta", "gamma"}; for (int i = 0; i < 3; i++) { delay(100 + random(400)); // staggered — different "routes" take different time JsonDocument shareDoc; shareDoc["type"] = "share"; shareDoc["index"] = i; shareDoc["route"] = routes[i]; shareDoc["data"] = toBase64(shares[i], len); shareDoc["hop"] = hopId; shareDoc["len"] = len; serializeJson(shareDoc, Serial); Serial.println(); blinkLED(1, 30); } // Confirm scatter complete JsonDocument done; done["type"] = "scattered"; done["hop"] = hopId; done["shares"] = 3; done["msg_len"] = len; done["note"] = "the dragon sleeps between the nodes"; serializeJson(done, Serial); Serial.println(); currentMode = MODE_IDLE; setLED(false); return; } // === HEARTBEAT === Serial.println("{\"type\":\"error\",\"msg\":\"unknown command\"}"); } // === SETUP === void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); // Generate node ID from chip MAC uint64_t mac = ESP.getEfuseMac(); nodeId = "valis-" + String((uint32_t)(mac >> 24), HEX); // Seed random from analog noise randomSeed(analogRead(0) ^ micros()); delay(1000); // Boot announcement JsonDocument boot; boot["type"] = "boot"; boot["id"] = nodeId; boot["protocol"] = "VALIS/Meilong"; boot["version"] = "0.1"; boot["chip"] = "ESP32-C3"; boot["msg"] = "node online — the dragon stirs"; serializeJson(boot, Serial); Serial.println(); blinkLED(3, 100); } // === LOOP === void loop() { // Read serial input while (Serial.available()) { char c = Serial.read(); if (c == '\n' || c == '\r') { if (serialBuffer.length() > 0) { processCommand(serialBuffer); serialBuffer = ""; } } else { serialBuffer += c; if (serialBuffer.length() > 1024) { serialBuffer = ""; // overflow protection } } } // Periodic heartbeat (every 30s) if (millis() - lastHeartbeat > 30000) { lastHeartbeat = millis(); JsonDocument hb; hb["type"] = "heartbeat"; hb["id"] = nodeId; hb["uptime"] = millis() / 1000; hb["mode"] = currentMode; serializeJson(hb, Serial); Serial.println(); } }