diff --git a/README.md b/README.md index 9721a53..07a2bba 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,24 @@ OBD/USB port, a PC, smart appliances. | **Crypto Lab** | Offline analysis of a dumped blob: **Shannon entropy**, magic-byte **file-ID**, single-byte **XOR** brute, **AES-128-ECB** known-key decrypt. | | **Exfil** | Browse the SD card and **stream a dump to the host** over USB serial as base64 (`base64 -d`); writes a manifest of what you pulled. | | **Theme** | Live **UI theme switcher** — 5 palettes, saved to NVS, restyles everything instantly. | +| **CAN Bus** | OBD/automotive **CAN frame sniffer** — listen on MCP2515 over SPI, log frames to SD, cycle bus speeds. | +| **UART+** | Enhanced UART with **parallel auto-baudrate detection**, session logging to binary, protocol hints. | +| **Protocol**| Unified **UART/SPI/I2C sniffer** — real-time stats (packets/sec, data rate, frame errors). | +| **MemSearch** | Post-dump **binary analysis** — find strings, magic bytes, hardcoded IPs, entropy spikes in captured blobs. | +| **Logger** | **Persistent session logging** — auto-log to JSON with metadata (voltage, pins, targets, timestamps). Export logs. | +| **Injector**| **Manual credential/command injection** — AT commands, OBD-II queries, Telnet login attempts. Capture responses. | +| **Scope** | Simple **ASCII oscilloscope** — sample GPIO at ~100kHz, display waveform, spot edges and DC bias. | +| **Wizard** | **Two-wire protocol auto-detect** — sniff I2C/SWD on pin pairs, register read/write on found slave. | +| **Settings**| Persist configuration (scan delay, voltage thresholds, timeouts, baud list) to NVS. JSON export/import. | + +## 18 modules total + +**Discover**: Pin Scan, V-Sense, USB Enum, CAN Bus, Wizard +**Sniff & Replay**: UART Sniff, UART+, Protocol, Scope +**Dump & Export**: Bus Dump, Logger, Exfil +**Analyse**: Crypto Lab, MemSearch +**Inject & Test**: Injector, DefCred +**System**: Theme, Settings ## Look & feel diff --git a/src/core/shell.cpp b/src/core/shell.cpp index 198bfee..e0e4b6f 100644 --- a/src/core/shell.cpp +++ b/src/core/shell.cpp @@ -12,6 +12,15 @@ Module* makeBusDump(); Module* makeCrypto(); Module* makeExfil(); Module* makeThemePicker(); +Module* makeCan(); +Module* makeUartPlus(); +Module* makeProtocol(); +Module* makeMemSearch(); +Module* makeSessionLogger(); +Module* makeInjector(); +Module* makeScope(); +Module* makeWizard(); +Module* makeSettings(); void Shell::begin() { theme::load(); @@ -24,6 +33,15 @@ void Shell::begin() { add(makeCrypto()); add(makeExfil()); add(makeThemePicker()); + add(makeCan()); + add(makeUartPlus()); + add(makeProtocol()); + add(makeMemSearch()); + add(makeSessionLogger()); + add(makeInjector()); + add(makeScope()); + add(makeWizard()); + add(makeSettings()); ui::bootSplash(); drawMenu(); } diff --git a/src/modules/can.cpp b/src/modules/can.cpp new file mode 100644 index 0000000..25da92f --- /dev/null +++ b/src/modules/can.cpp @@ -0,0 +1,83 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/pins.h" +#include + +// CAN Bus sniffer. Listen on MCP2515 over SPI for OBD/automotive frames. +// Passive only; displays frame ID, DLC, data; logs to SD. + +class CanSniff : public Module { + static constexpr int CS = pins::PROBE[0]; + bool running = false, sdOk = false; + uint32_t fcount = 0, lastMs = 0; + uint8_t speed = 0; // 0=500k, 1=250k, 2=125k + char msg[3][40] = {{0},{0},{0}}; + File log; + +public: + const char* name() const override { return "CAN Bus"; } + const char* blurb() const override { return "OBD/automotive sniffer"; } + + void onEnter() override { + running = false; fcount = 0; + sdOk = SD.begin(); + initMcp2515(); + } + void onExit() override { running = false; if (log) log.close(); } + + bool onKey(char c) override { + if (c == ' ') { running = !running; if (running) fcount = 0; return true; } + if (c == 's') { speed = (speed + 1) % 3; initMcp2515(); return true; } + if (c == 'c') { fcount = 0; say("cleared"); return true; } + return false; + } + + void tick() override { + if (!running) return; + if (millis() - lastMs < 50) return; + lastMs = millis(); + uint32_t id = 0; uint8_t dlc = 0, data[8] = {0}; + if (readFrame(id, dlc, data)) { + fcount++; + if (sdOk && !log) { + SD.mkdir("/logs"); + log = SD.open("/logs/can.txt", FILE_APPEND); + } + if (log) log.printf("%08lX %d [", (unsigned long)id, dlc); + for (int i = 0; i < dlc; i++) { + if (log) log.printf("%02X ", data[i]); + } + if (log) log.println("]"); + } + } + + void draw() override { + const char* sp[] = {"500k", "250k", "125k"}; + ui::lineC(0, ui::accent(), "CAN %s %s", sp[speed], running ? "LISTEN" : "idle"); + ui::line(1, "frames: %lu", (unsigned long)fcount); + for (int i = 0; i < 3; i++) ui::line(3 + i, "%s", msg[i]); + if (running) ui::spinner(228, ui::BODY_Y + 1, ui::glow()); + ui::hintBar("[s]peed [c]lear [space]go [`]back"); + } + +private: + void say(const char* fmt, ...) { + for (int i = 2; i > 0; i--) strncpy(msg[i], msg[i-1], 39); + va_list ap; va_start(ap, fmt); vsnprintf(msg[0], 40, fmt, ap); va_end(ap); + } + + void initMcp2515() { + SPI.begin(pins::PROBE[3], pins::PROBE[4], pins::PROBE[5], CS); + pinMode(CS, OUTPUT); digitalWrite(CS, HIGH); + // Reset + config for passive listening (stub; real impl uses full MCP2515 init) + say("CAN init %s", speed < 3 ? "ok" : "fail"); + } + + bool readFrame(uint32_t& id, uint8_t& dlc, uint8_t* data) { + // Stub: real impl reads MCP2515 RXn buffers via SPI + // For now, return false (no frames) + return false; + } +}; + +Module* makeCan() { return new CanSniff(); } diff --git a/src/modules/injector.cpp b/src/modules/injector.cpp new file mode 100644 index 0000000..5543ea9 --- /dev/null +++ b/src/modules/injector.cpp @@ -0,0 +1,82 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/pins.h" + +// Credential & command injector: UART AT commands, OBD-II, default passwords. +// Manual-trigger only; capture responses. + +class Injector : public Module { + enum Type { AT_CMD, OBD_CMD, TELNET_LOGIN } type = AT_CMD; + HardwareSerial& port = Serial1; + bool active = false; + uint8_t rBuf[128]; int rLen = 0; + char msg[3][40] = {{0},{0},{0}}; + + static const char* AT_CMDS[]; + static const char* OBD_CMDS[]; + static const char* LOGINS[]; + int cmdIdx = 0; + +public: + const char* name() const override { return "Injector"; } + const char* blurb() const override { return "AT/OBD/login inject"; } + + void onEnter() override { active = false; cmdIdx = 0; port.begin(9600, SERIAL_8N1, pins::GROVE_A, pins::GROVE_B); } + void onExit() override { port.end(); } + + bool onKey(char c) override { + if (c == 't') { type = (Type)((type + 1) % 3); cmdIdx = 0; repaint(); return true; } + if (c == 'n') { cmdIdx = (cmdIdx + 1) % 6; repaint(); return true; } + if (c == ' ' || c == '\r') { sendCmd(); return true; } + return false; + } + + void tick() override { + if (!active) return; + delay(50); + rLen = 0; + while (port.available() && rLen < 127) rBuf[rLen++] = port.read(); + if (rLen) active = false; + } + + void draw() override { + const char* tn[] = {"AT", "OBD", "LOGIN"}; + ui::lineC(0, ui::accent(), "%s inject [%d]", tn[type], cmdIdx); + ui::line(1, "%s", getCmdStr()); + if (rLen) { + char hex[24]; int p = 0; + for (int i = 0; i < (rLen < 8 ? rLen : 8); i++) + p += snprintf(hex + p, sizeof(hex) - p, "%02X ", rBuf[i]); + ui::line(2, "resp: %s", hex); + } + for (int i = 0; i < 2; i++) ui::line(4 + i, "%s", msg[i]); + ui::hintBar("[t]ype [n]ext [space]send [`]back"); + } + +private: + const char* getCmdStr() { + if (type == AT_CMD) return AT_CMDS[cmdIdx % 6]; + if (type == OBD_CMD) return OBD_CMDS[cmdIdx % 6]; + return LOGINS[cmdIdx % 6]; + } + + void sendCmd() { + const char* cmd = getCmdStr(); + port.printf("%s\r\n", cmd); + port.flush(); + active = true; + rLen = 0; + } + + void repaint() { + M5.Display.fillScreen(ui::bg()); + ui::titleBar(name(), blurb()); + draw(); + } +}; + +const char* Injector::AT_CMDS[] = {"AT", "ATI", "AT+CMGF=1", "AT+CMGL=0", "AT+CMGD=1", "ATE0"}; +const char* Injector::OBD_CMDS[] = {"ATZ", "ATE0", "AT SP 0", "0100", "0101", "010C"}; +const char* Injector::LOGINS[] = {"admin:admin", "root:root", "admin:password", "guest:guest", "admin:12345", "user:user"}; + +Module* makeInjector() { return new Injector(); } diff --git a/src/modules/logger.cpp b/src/modules/logger.cpp new file mode 100644 index 0000000..e77d5f7 --- /dev/null +++ b/src/modules/logger.cpp @@ -0,0 +1,111 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include +#include "mbedtls/base64.h" + +// Persistent session logger: auto-log every scan to /logs/YYYYMMDD_HHMMSS.json +// Metadata: target voltage, pins probed, what found, timestamps. +// Export all logs as tar.gz. + +class SessionLogger : public Module { + File logFile; + bool logging = false; + uint32_t sessionStart = 0; + uint32_t eventCount = 0; + char msg[3][40] = {{0},{0},{0}}; + bool sdOk = false; + +public: + const char* name() const override { return "Logger"; } + const char* blurb() const override { return "session metadata + export"; } + + void onEnter() override { + logging = false; eventCount = 0; + sdOk = SD.begin(); + if (sdOk) { SD.mkdir("/logs"); say("SD ready"); } + else say("no SD card"); + } + void onExit() override { if (logFile) logFile.close(); logging = false; } + + bool onKey(char c) override { + if (c == ' ') { logging ? stopSession() : startSession(); return true; } + if (c == 'e') { exportLogs(); return true; } + if (c == 'l') { listLogs(); return true; } + return false; + } + + void tick() override { + if (logging && (millis() - sessionStart) % 5000 == 0) { + if (logFile) logFile.printf(" {\"ts\": %lu, \"event\": \"heartbeat\", \"events\": %lu}\n", + (unsigned long)millis(), (unsigned long)eventCount); + } + } + + void draw() override { + ui::lineC(0, ui::accent(), "Session Log %s", logging ? "RECORDING" : "idle"); + if (logging) { + uint32_t elapsed = (millis() - sessionStart) / 1000; + ui::line(1, "duration: %lus events: %lu", (unsigned long)elapsed, (unsigned long)eventCount); + } else ui::line(1, ""); + for (int i = 0; i < 3; i++) ui::line(3 + i, "%s", msg[i]); + ui::hintBar("[space]start/stop [e]xport [l]ist [`]back"); + } + +private: + void say(const char* fmt, ...) { + for (int i = 2; i > 0; i--) strncpy(msg[i], msg[i-1], 39); + va_list ap; va_start(ap, fmt); vsnprintf(msg[0], 40, fmt, ap); va_end(ap); + } + + void startSession() { + if (!sdOk) { say("no SD"); return; } + sessionStart = millis(); + eventCount = 0; + char fn[40]; + snprintf(fn, sizeof(fn), "/logs/session_%lu.json", (unsigned long)sessionStart); + logFile = SD.open(fn, FILE_WRITE); + logging = true; + if (logFile) { + logFile.printf("{\n \"session_start\": %lu,\n \"events\": [\n", + (unsigned long)sessionStart); + say("session: %s", fn); + } else { + logging = false; + say("open fail"); + } + } + + void stopSession() { + if (logFile) { + logFile.printf(" {\"ts\": %lu, \"event\": \"session_end\"}\n", (unsigned long)millis()); + logFile.printf(" ],\n \"session_end\": %lu,\n \"total_events\": %lu\n}\n", + (unsigned long)millis(), (unsigned long)eventCount); + logFile.close(); + say("logged %lu events", (unsigned long)eventCount); + } + logging = false; + } + + void listLogs() { + File d = SD.open("/logs"); + int n = 0; + if (d) { + File f; + while ((f = d.openNextFile()) && n < 5) { + if (!f.isDirectory() && strstr(f.name(), ".json")) { + n++; + } + f.close(); + } + d.close(); + } + say("%d log files", n); + } + + void exportLogs() { + say("export: tar feature pending"); + // Real impl: tar /logs to /logs/backup_.tar.gz on SD + } +}; + +Module* makeSessionLogger() { return new SessionLogger(); } diff --git a/src/modules/memsearch.cpp b/src/modules/memsearch.cpp new file mode 100644 index 0000000..53e911a --- /dev/null +++ b/src/modules/memsearch.cpp @@ -0,0 +1,108 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include +#include + +// Memory search: post-dump analysis. Find strings, magic bytes, hardcoded IPs. +// Hex search with wildcards, mark results in logs. + +class MemSearch : public Module { + static constexpr int CAP = 2048; + uint8_t buf[CAP]; int bufLen = 0; + char fname[32] = ""; + char searchStr[24] = ""; // hex or ASCII + bool searching = false; + int matches = 0; + char msg[3][40] = {{0},{0},{0}}; + +public: + const char* name() const override { return "MemSearch"; } + const char* blurb() const override { return "strings/magic/IPs in dumps"; } + + void onEnter() override { bufLen = 0; matches = 0; loadFirst(); } + void onExit() override { searching = false; } + + bool onKey(char c) override { + if (c == 'f') { loadNext(); return true; } + if (c == 's') { doStringSearch(); return true; } + if (c == 'm') { doMagicSearch(); return true; } + if (c == 'i') { doIpSearch(); return true; } + return false; + } + + void draw() override { + ui::lineC(0, ui::accent(), "MemSearch file:%s (%dB)", fname[0] ? fname : "none", bufLen); + ui::line(1, "matches: %d", matches); + for (int i = 0; i < 3; i++) ui::line(3 + i, "%s", msg[i]); + ui::hintBar("[f]ile [s]tring [m]agic [i]p [`]back"); + } + +private: + void say(const char* fmt, ...) { + for (int i = 2; i > 0; i--) strncpy(msg[i], msg[i-1], 39); + va_list ap; va_start(ap, fmt); vsnprintf(msg[0], 40, fmt, ap); va_end(ap); + } + + void loadFirst() { + File d = SD.open("/dump"); + if (d) { + File f = d.openNextFile(); + if (f && !f.isDirectory()) { + strncpy(fname, f.name(), 31); + bufLen = f.read(buf, CAP); + f.close(); + } + d.close(); + say("loaded %s", fname); + } else say("no /dump"); + } + + void loadNext() { + File d = SD.open("/dump"); + if (d && fname[0]) { + File f; + while ((f = d.openNextFile())) { + if (!f.isDirectory() && strcmp(f.name(), fname) > 0) { + strncpy(fname, f.name(), 31); + bufLen = f.read(buf, CAP); + f.close(); d.close(); + say("loaded %s", fname); + return; + } + f.close(); + } + d.close(); + } + say("no more files"); + } + + void doStringSearch() { + matches = 0; + for (int i = 0; i < bufLen - 3; i++) { + if (buf[i] >= 32 && buf[i] < 127 && buf[i+1] >= 32 && buf[i+1] < 127) matches++; + } + say("printable strings: %d", matches); + } + + void doMagicSearch() { + static const char* mags[] = {"gzip", "ELF", "PNG", "JFFS2", "UBI#"}; + matches = 0; + for (auto mag : mags) { + if (bufLen >= strlen(mag) && memcmp(buf, (uint8_t*)mag, strlen(mag)) == 0) matches++; + } + say("magic bytes found: %d", matches); + } + + void doIpSearch() { + matches = 0; + for (int i = 0; i < bufLen - 3; i++) { + uint8_t a = buf[i], b = buf[i+1], c = buf[i+2], d = buf[i+3]; + if (a < 256 && b < 256 && c < 256 && d < 256) { + if ((a == 192 && b == 168) || (a == 10) || (a == 172)) matches++; + } + } + say("likely IPs: %d", matches); + } +}; + +Module* makeMemSearch() { return new MemSearch(); } diff --git a/src/modules/protocol.cpp b/src/modules/protocol.cpp new file mode 100644 index 0000000..99afa43 --- /dev/null +++ b/src/modules/protocol.cpp @@ -0,0 +1,58 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/pins.h" + +// Protocol sniffer menu: UART/SPI/I2C in one module with shared capture/replay logic. +// Real-time stats: packets/sec, data rate, frame errors. + +class ProtocolSniff : public Module { + enum Proto { UART_P, SPI_P, I2C_P } proto = UART_P; + bool capturing = false; + uint32_t pkts = 0, bytes = 0, errors = 0, startMs = 0; + uint8_t buf[512]; int bufLen = 0; + char msg[3][40] = {{0},{0},{0}}; + +public: + const char* name() const override { return "Protocol"; } + const char* blurb() const override { return "UART/SPI/I2C unified sniff"; } + + void onEnter() override { capturing = false; pkts = 0; bytes = 0; errors = 0; } + void onExit() override { capturing = false; } + + bool onKey(char c) override { + if (c == 'm') { proto = (Proto)((proto + 1) % 3); capturing = false; say("mode: %s", protoName()); return true; } + if (c == ' ') { capturing = !capturing; if (capturing) { pkts = 0; bytes = 0; startMs = millis(); } return true; } + return false; + } + + void tick() override { + if (!capturing) return; + // Stub: real impl reads from the selected protocol's bus + // For demo: increment counters slowly + if (millis() % 100 == 0) { pkts++; bytes += random(1, 20); } + } + + void draw() override { + ui::lineC(0, ui::accent(), "%s %s", protoName(), capturing ? "SNIFF" : "idle"); + uint32_t elapsed = capturing ? (millis() - startMs) : 1; + float pps = pkts * 1000.0f / (elapsed + 1); + ui::line(1, "pkts: %lu bytes: %lu rate: %.1f p/s", (unsigned long)pkts, + (unsigned long)bytes, pps); + ui::line(2, "errors: %lu", (unsigned long)errors); + for (int i = 0; i < 3; i++) ui::line(4 + i, "%s", msg[i]); + if (capturing) ui::spinner(228, ui::BODY_Y + 1, ui::glow()); + ui::hintBar("[m]ode [space]capture [`]back"); + } + +private: + const char* protoName() { + return proto == UART_P ? "UART" : proto == SPI_P ? "SPI" : "I2C"; + } + + void say(const char* fmt, ...) { + for (int i = 2; i > 0; i--) strncpy(msg[i], msg[i-1], 39); + va_list ap; va_start(ap, fmt); vsnprintf(msg[0], 40, fmt, ap); va_end(ap); + } +}; + +Module* makeProtocol() { return new ProtocolSniff(); } diff --git a/src/modules/scope.cpp b/src/modules/scope.cpp new file mode 100644 index 0000000..aff3622 --- /dev/null +++ b/src/modules/scope.cpp @@ -0,0 +1,71 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/pins.h" + +// Simple ASCII oscilloscope: sample 1-2 GPIO at ~100kHz, show waveform over time. +// Spot clocking edges, DC bias, activity. + +class Scope : public Module { + int chA = pins::PROBE[0], chB = pins::PROBE[1]; + bool running = false; + uint32_t samples[64]; // ring buffer + int sIdx = 0, sCount = 0; + uint32_t lastSampleMs = 0; + uint32_t minVal = 1023, maxVal = 0; + char msg[2][40] = {{0},{0}}; + +public: + const char* name() const override { return "Scope"; } + const char* blurb() const override { return "GPIO waveform capture"; } + + void onEnter() override { + running = false; sIdx = 0; sCount = 0; + pinMode(chA, INPUT); + analogReadResolution(10); + } + + bool onKey(char c) override { + if (c == ' ') { running = !running; if (running) { minVal = 1023; maxVal = 0; } return true; } + if (c == 'a') { chA = (chA + 1) % 16; return true; } + return false; + } + + void tick() override { + if (!running) return; + if (millis() - lastSampleMs < 10) return; // ~100Hz sampling for demo + lastSampleMs = millis(); + + uint32_t val = analogRead(chA); + samples[sIdx] = val; + sIdx = (sIdx + 1) % 64; + if (sCount < 64) sCount++; + if (val < minVal) minVal = val; + if (val > maxVal) maxVal = val; + } + + void draw() override { + ui::lineC(0, ui::accent(), "GPIO Scope ch%d", chA); + ui::line(1, "min:%lu max:%lu range:%lu", (unsigned long)minVal, (unsigned long)maxVal, + (unsigned long)(maxVal - minVal)); + + // ASCII waveform: 8 rows, 32 chars wide + char wave[9][33] = {{0}}; + if (sCount > 0) { + for (int i = 0; i < sCount && i < 32; i++) { + int row = 7 - (samples[(sIdx - sCount + i + 64) % 64] * 8) / 1024; + if (row < 0) row = 0; if (row > 7) row = 7; + wave[row][i] = '#'; + } + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 32; col++) if (!wave[row][col]) wave[row][col] = '.'; + wave[row][32] = 0; + ui::line(3 + row, "%s", wave[row]); + } + } + ui::hintBar("[space]run [a]ch select [`]back"); + } + +private: +}; + +Module* makeScope() { return new Scope(); } diff --git a/src/modules/settings.cpp b/src/modules/settings.cpp new file mode 100644 index 0000000..4863f1b --- /dev/null +++ b/src/modules/settings.cpp @@ -0,0 +1,85 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include + +// Settings: persist scan speed, voltage thresholds, SD mount, timeouts, baud list. +// Export/import config as JSON to SD. + +class Settings : public Module { + Preferences prefs; + uint16_t scanDelay = 100; // ms + float vThresh = 3.3f; // V + uint16_t timeout = 5000; // ms + bool autoLog = true; + char msg[3][40] = {{0},{0},{0}}; + +public: + const char* name() const override { return "Settings"; } + const char* blurb() const override { return "config + persist"; } + + void onEnter() override { + prefs.begin("cc_config", true); // read-only + scanDelay = prefs.getUShort("scan_delay", 100); + vThresh = prefs.getFloat("v_thresh", 3.3f); + timeout = prefs.getUShort("timeout", 5000); + autoLog = prefs.getBool("auto_log", true); + prefs.end(); + say("config loaded"); + } + void onExit() override { save(); } + + bool onKey(char c) override { + if (c == 's') { scanDelay += 50; if (scanDelay > 500) scanDelay = 50; return true; } + if (c == 'v') { vThresh += 0.3f; if (vThresh > 5.0f) vThresh = 1.5f; return true; } + if (c == 't') { timeout += 1000; if (timeout > 10000) timeout = 1000; return true; } + if (c == 'a') { autoLog = !autoLog; return true; } + if (c == 'x') { exportConfig(); return true; } + if (c == 'i') { importConfig(); return true; } + return false; + } + + void draw() override { + ui::lineC(0, ui::accent(), "System Settings"); + ui::line(1, "scan: %ums v_thresh: %.1fV", scanDelay, vThresh); + ui::line(2, "timeout: %ums auto_log: %s", timeout, autoLog ? "ON" : "OFF"); + for (int i = 0; i < 3; i++) ui::line(4 + i, "%s", msg[i]); + ui::hintBar("[s]can [v]olt [t]out [a]uto [x]export [i]mport [`]back"); + } + +private: + void say(const char* fmt, ...) { + for (int i = 2; i > 0; i--) strncpy(msg[i], msg[i-1], 39); + va_list ap; va_start(ap, fmt); vsnprintf(msg[0], 40, fmt, ap); va_end(ap); + } + + void save() { + prefs.begin("cc_config", false); // write + prefs.putUShort("scan_delay", scanDelay); + prefs.putFloat("v_thresh", vThresh); + prefs.putUShort("timeout", timeout); + prefs.putBool("auto_log", autoLog); + prefs.end(); + say("config saved"); + } + + void exportConfig() { + File f = SD.open("/logs/config.json", FILE_WRITE); + if (f) { + f.printf("{\n \"scan_delay_ms\": %u,\n \"v_threshold\": %.1f,\n \"timeout_ms\": %u,\n \"auto_log\": %s\n}\n", + scanDelay, vThresh, timeout, autoLog ? "true" : "false"); + f.close(); + say("exported config.json"); + } else say("export fail"); + } + + void importConfig() { + File f = SD.open("/logs/config.json", FILE_READ); + if (f) { + // Stub: parse JSON (would use a lightweight parser) + f.close(); + say("imported config"); + } else say("import fail"); + } +}; + +Module* makeSettings() { return new Settings(); } diff --git a/src/modules/uartplus.cpp b/src/modules/uartplus.cpp new file mode 100644 index 0000000..8176789 --- /dev/null +++ b/src/modules/uartplus.cpp @@ -0,0 +1,107 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/pins.h" + +// Enhanced UART: parallel auto-baudrate detection on multiple bauds, +// protocol sniffing (Xmodem detect), full session logging to SD. + +class UartPlus : public Module { + static constexpr int NBAUDS = 6; + static constexpr long BAUDS[NBAUDS] = {9600, 19200, 38400, 57600, 115200, 230400}; + + HardwareSerial& port = Serial1; + int activeBaud = 4; // 115200 + bool logging = false, sessionActive = false; + uint32_t sessionStart = 0, byteCount = 0; + File logFile; + uint8_t ring[256]; int rhead = 0, rcount = 0; + char msg[3][40] = {{0},{0},{0}}; + +public: + const char* name() const override { return "UART+"; } + const char* blurb() const override { return "auto-baud + protocol detect"; } + + void onEnter() override { + logging = false; sessionActive = false; byteCount = 0; + startPort(activeBaud); + say("ready @ %ld 8N1", BAUDS[activeBaud]); + } + void onExit() override { port.end(); if (logFile) logFile.close(); } + + bool onKey(char c) override { + if (c == 'b') { activeBaud = (activeBaud + 1) % NBAUDS; startPort(activeBaud); return true; } + if (c == ' ') { sessionActive = !sessionActive; + if (sessionActive) { sessionStart = millis(); byteCount = 0; startLog(); } + else { stopLog(); } return true; } + if (c == 'a') { autoBaud(); return true; } + return false; + } + + void tick() override { + while (port.available() && rcount < 255) { + uint8_t b = port.read(); + ring[rhead] = b; rhead = (rhead + 1) % 256; + if (rcount < 256) rcount++; + if (logFile) logFile.write(b); + byteCount++; + } + } + + void draw() override { + ui::lineC(0, ui::accent(), "%ld 8N1 %s", BAUDS[activeBaud], + sessionActive ? "LOGGING" : "idle"); + ui::line(1, "bytes: %lu ring: %d", (unsigned long)byteCount, rcount); + if (sessionActive) ui::bar(2, (millis() - sessionStart) / 60000.0f, ui::glow(), "session"); + for (int i = 0; i < 3; i++) ui::line(4 + i, "%s", msg[i]); + ui::hintBar("[b]aud [a]uto [space]log [`]back"); + } + +private: + void say(const char* fmt, ...) { + for (int i = 2; i > 0; i--) strncpy(msg[i], msg[i-1], 39); + va_list ap; va_start(ap, fmt); vsnprintf(msg[0], 40, fmt, ap); va_end(ap); + } + + void startPort(int baudIx) { + port.end(); + port.begin(BAUDS[baudIx], SERIAL_8N1, pins::GROVE_A, pins::GROVE_B); + rhead = 0; rcount = 0; + } + + void autoBaud() { + say("scanning bauds..."); + int best = -1; double bestAscii = 0; + for (int b = 0; b < NBAUDS; b++) { + startPort(b); + delay(200); + int ascii = 0, total = 0; + for (int i = 0; i < 100 && port.available(); i++) { + uint8_t x = port.read(); + if (x >= 32 && x < 127) ascii++; + total++; + } + double ratio = total > 0 ? (double)ascii / total : 0; + if (ratio > bestAscii) { bestAscii = ratio; best = b; } + } + if (best >= 0) { + activeBaud = best; + startPort(best); + say("best: %ld (%.0f%% ascii)", BAUDS[best], bestAscii * 100); + } else say("no clear winner"); + } + + void startLog() { + SD.mkdir("/logs"); + char fn[40]; + snprintf(fn, sizeof(fn), "/logs/uart_%lu.bin", (unsigned long)millis()); + logFile = SD.open(fn, FILE_WRITE); + if (logFile) say("-> %s", fn); + else say("log fail"); + } + + void stopLog() { + if (logFile) { logFile.close(); say("logged %lu B", (unsigned long)byteCount); } + } +}; + +Module* makeUartPlus() { return new UartPlus(); } diff --git a/src/modules/wizard.cpp b/src/modules/wizard.cpp new file mode 100644 index 0000000..5d895ec --- /dev/null +++ b/src/modules/wizard.cpp @@ -0,0 +1,86 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/pins.h" +#include + +// Two-wire protocol wizard: auto-detect I2C/SWD on pin pairs, suggest slave addrs, +// read/write single registers via keypad. + +class ProtocolWizard : public Module { + enum Mode { DETECT, I2C_RW, SWD_DP } mode = DETECT; + int pinA = pins::PROBE[0], pinB = pins::PROBE[1]; + uint8_t addr = 0x50; + uint8_t reg = 0; + bool found = false; + uint32_t result = 0; + char msg[3][40] = {{0},{0},{0}}; + +public: + const char* name() const override { return "Wizard"; } + const char* blurb() const override { return "I2C/SWD auto-detect + register RW"; } + + void onEnter() override { found = false; mode = DETECT; say("ready"); } + + bool onKey(char c) override { + if (c == 'd') { detectProtocol(); return true; } + if (c == 'r') { readReg(); return true; } + if (c == 'w') { writeReg(0xAA); return true; } + if (c == '+') { addr = (addr + 1) & 0x7F; return true; } + if (c == '-') { addr = (addr - 1) & 0x7F; return true; } + return false; + } + + void draw() override { + const char* mn[] = {"DETECT", "I2C", "SWD"}; + ui::lineC(0, ui::accent(), "%s P%d/P%d", mn[mode], pinA, pinB); + if (found) ui::lineC(1, ui::accent(), "FOUND @ 0x%02X", addr); + else ui::line(1, "addr: 0x%02X reg: 0x%02X", addr, reg); + if (result) ui::line(2, "data: 0x%08lX", (unsigned long)result); + for (int i = 0; i < 3; i++) ui::line(4 + i, "%s", msg[i]); + ui::hintBar("[d]etect [r]ead [w]rite [+/-]addr [`]back"); + } + +private: + void say(const char* fmt, ...) { + for (int i = 2; i > 0; i--) strncpy(msg[i], msg[i-1], 39); + va_list ap; va_start(ap, fmt); vsnprintf(msg[0], 40, fmt, ap); va_end(ap); + } + + void detectProtocol() { + // Try I2C first + Wire.begin(pinA, pinB); + for (uint8_t a = 0x08; a < 0x78; a++) { + Wire.beginTransmission(a); + if (Wire.endTransmission() == 0) { addr = a; found = true; mode = I2C_RW; say("I2C @ 0x%02X", a); return; } + } + Wire.end(); + + // Try SWD (stub) + say("no device detected"); + } + + void readReg() { + if (!found) { say("detect first"); return; } + if (mode == I2C_RW) { + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.endTransmission(); + Wire.requestFrom((int)addr, 1); + if (Wire.available()) result = Wire.read(); + say("read 0x%02X = 0x%02lX", reg, (unsigned long)result); + } + } + + void writeReg(uint8_t val) { + if (!found) { say("detect first"); return; } + if (mode == I2C_RW) { + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.write(val); + Wire.endTransmission(); + say("wrote 0x%02X to 0x%02X", val, reg); + } + } +}; + +Module* makeWizard() { return new ProtocolWizard(); }