Card-Crack: Cardputer ESP32-S3 home-lab recon toolkit
Manual-trigger firmware toolkit for pentesting owned devices: - Pin Scan: UART/JTAG/SWD detection (baud est + IDCODE reads) - V-Sense: target voltage probe w/ logic-family guess - UART Sniff: passive capture + manual-only frame replay - USB Enum: ESP32-S3 host, read-only descriptor fingerprinting - DefCred: single-host factory-default login check, rate-limited Modular shell (core/ + modules/), PlatformIO build, hardware/safety docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AAhMHMRAQLQ9hSbBECKNfn
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.pio/
|
||||||
|
.vscode/
|
||||||
|
*.bin
|
||||||
|
*.elf
|
||||||
|
*.map
|
||||||
|
build/
|
||||||
50
README.md
Normal file
50
README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Card-Crack
|
||||||
|
|
||||||
|
A manual-trigger hardware/network **recon toolkit** for the
|
||||||
|
[M5Stack Cardputer](https://docs.m5stack.com/en/core/Cardputer) (ESP32-S3),
|
||||||
|
built for **home-lab pentesting of devices you own** — your router, a car's
|
||||||
|
OBD/USB port, a PC, smart appliances.
|
||||||
|
|
||||||
|
> ⚠️ **Authorized use only.** Every tool here is scoped for your own bench:
|
||||||
|
> single targets you enter by hand, curated default-credential lists (not
|
||||||
|
> brute-force), rate-limited network checks, and read-only descriptor/ID
|
||||||
|
> reads. Nothing scans, transmits, or replays on its own — you press the
|
||||||
|
> action key. Don't point it at anything you don't own or run.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Module | What it does |
|
||||||
|
|-------------|---------------------------------------------------------------------|
|
||||||
|
| **Pin Scan**| JTAGulator-style detection of **UART / JTAG / SWD** pins on the probe header. UART baud estimation; JTAG & SWD IDCODE reads. |
|
||||||
|
| **V-Sense** | Live target-voltage probe through a divider; guesses the logic family so you know what you're touching before driving a pin. |
|
||||||
|
| **UART Sniff** | Passive UART capture (hex+ascii, selectable baud); freeze a frame and **replay it on a keypress** (manual TX only). |
|
||||||
|
| **USB Enum**| ESP32-S3 as USB host — reads **descriptors** (VID/PID, class, strings, config) to fingerprint an unknown device. Read-only. |
|
||||||
|
| **DefCred** | Checks a single host (default: gateway) against a short list of **factory-default logins** over HTTP Basic Auth. Stops on first hit. |
|
||||||
|
|
||||||
|
## Build & flash
|
||||||
|
|
||||||
|
Requires [PlatformIO](https://platformio.org/).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# set your lab Wi-Fi for the DefCred module (or edit defcred.cpp)
|
||||||
|
pio run -e cardputer \
|
||||||
|
-a "--build-property build.flags=-DCC_WIFI_SSID='\"MyAP\"' -DCC_WIFI_PASS='\"secret\"'"
|
||||||
|
pio run -e cardputer -t upload
|
||||||
|
pio device monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- Menu: `;` up · `.` down · `Enter` open
|
||||||
|
- In a module: <code>`</code> (backtick) back · per-module hints on the bottom bar
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/core/ shell, module interface, UI helpers, pin map
|
||||||
|
src/modules/ one file per recon tool
|
||||||
|
docs/ HARDWARE.md wiring + safety
|
||||||
|
```
|
||||||
|
|
||||||
|
See **[docs/HARDWARE.md](docs/HARDWARE.md)** before wiring — the S3 is a 3V3
|
||||||
|
part and 5V on a bare GPIO will destroy it.
|
||||||
41
docs/HARDWARE.md
Normal file
41
docs/HARDWARE.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Hardware & wiring — read before probing
|
||||||
|
|
||||||
|
The Cardputer's ESP32-S3 is a **3.3 V** part. Several targets you'll poke are
|
||||||
|
not:
|
||||||
|
|
||||||
|
- Car OBD/USB, PC USB: **5 V** VBUS.
|
||||||
|
- Some debug headers idle at 1.8 V.
|
||||||
|
|
||||||
|
**Always** go through protection. Never land a probe on a live target until
|
||||||
|
V-Sense has told you the voltage.
|
||||||
|
|
||||||
|
## Probe header
|
||||||
|
|
||||||
|
`src/core/pins.h` defines `pins::PROBE[]`. By default:
|
||||||
|
|
||||||
|
- `G1`, `G2` — the side Grove connector (safe, use these first).
|
||||||
|
- `G8..G13` — StampS3 pads via a breakout ribbon (optional).
|
||||||
|
|
||||||
|
## Minimum protection rig
|
||||||
|
|
||||||
|
| Signal | Between probe and target |
|
||||||
|
|---------------|-------------------------------------------------------|
|
||||||
|
| Any GPIO in | 1 kΩ series + 3.3 V clamp (BAT54S to 3V3/GND) |
|
||||||
|
| V-Sense (G10) | resistor divider R1:R2 = 2:1 → 0–9.9 V range (ratio 3)|
|
||||||
|
| Level shift | bidirectional shifter (e.g. TXS0108) for 5 V buses |
|
||||||
|
|
||||||
|
Adjust `pins::VSENSE_RATIO` if you change the divider.
|
||||||
|
|
||||||
|
## USB host
|
||||||
|
|
||||||
|
To use **USB Enum** the S3 must supply VBUS to the target as a host. Use an
|
||||||
|
OTG adapter on the S3 USB port, keep the CDC console on the built-in USB, and
|
||||||
|
never hot-plug a 5 V target onto a GPIO — it goes on the USB D+/D-/VBUS lines
|
||||||
|
only.
|
||||||
|
|
||||||
|
## Safety checklist
|
||||||
|
|
||||||
|
1. Target powered from its **own** supply, common ground with the Cardputer.
|
||||||
|
2. V-Sense the line first. If it reads ≥ 5 V, shift or stop.
|
||||||
|
3. Start passive (Pin Scan / UART Sniff). Only replay/TX when you mean to.
|
||||||
|
4. It's your device. Keep it that way.
|
||||||
23
platformio.ini
Normal file
23
platformio.ini
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
; Card-Crack — Cardputer (ESP32-S3) pentest recon toolkit
|
||||||
|
; Home-lab / authorized use only.
|
||||||
|
[platformio]
|
||||||
|
default_envs = cardputer
|
||||||
|
src_dir = src
|
||||||
|
|
||||||
|
[env:cardputer]
|
||||||
|
platform = espressif32@6.9.0
|
||||||
|
board = m5stack-stamps3
|
||||||
|
framework = arduino
|
||||||
|
board_build.flash_mode = qio
|
||||||
|
board_build.f_flash = 80000000L
|
||||||
|
board_build.partitions = default_16MB.csv
|
||||||
|
monitor_speed = 115200
|
||||||
|
upload_speed = 1500000
|
||||||
|
build_flags =
|
||||||
|
-D CORE_DEBUG_LEVEL=1
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-D CARDCRACK_VERSION=\"0.1.0\"
|
||||||
|
lib_deps =
|
||||||
|
m5stack/M5Cardputer@^1.0.3
|
||||||
|
m5stack/M5GFX@^0.2.3
|
||||||
39
src/core/module.h
Normal file
39
src/core/module.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// Card-Crack module interface.
|
||||||
|
//
|
||||||
|
// Every recon tool is a Module. The shell owns the screen and the
|
||||||
|
// keyboard; a module only gets called on the events below and draws
|
||||||
|
// into the content area the shell hands it. All modules are
|
||||||
|
// MANUAL-TRIGGER: nothing scans, transmits, or replays until the user
|
||||||
|
// presses the action key. This is deliberate — passive-by-default keeps
|
||||||
|
// you from poking a live bus you didn't mean to.
|
||||||
|
|
||||||
|
class Module {
|
||||||
|
public:
|
||||||
|
virtual ~Module() {}
|
||||||
|
|
||||||
|
// Short name shown in the main menu.
|
||||||
|
virtual const char* name() const = 0;
|
||||||
|
|
||||||
|
// One-line description shown under the title bar.
|
||||||
|
virtual const char* blurb() const = 0;
|
||||||
|
|
||||||
|
// Called once when the module becomes active. Set up peripherals here.
|
||||||
|
virtual void onEnter() {}
|
||||||
|
|
||||||
|
// Called when leaving. Release GPIOs / buses back to hi-Z.
|
||||||
|
virtual void onExit() {}
|
||||||
|
|
||||||
|
// Called every shell tick (~30 Hz). Do incremental work, never block
|
||||||
|
// longer than a frame; long scans must be chunked and resumable.
|
||||||
|
virtual void tick() {}
|
||||||
|
|
||||||
|
// A key was pressed. Return true if the module consumed it.
|
||||||
|
// ` (backtick) is reserved by the shell for "back to menu".
|
||||||
|
virtual bool onKey(char c) { return false; }
|
||||||
|
|
||||||
|
// Draw the module body. The shell has already drawn the chrome.
|
||||||
|
virtual void draw() {}
|
||||||
|
};
|
||||||
28
src/core/pins.h
Normal file
28
src/core/pins.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
// Card-Crack — Cardputer probe/target pin map.
|
||||||
|
//
|
||||||
|
// The Cardputer exposes a small set of free GPIOs on the bottom
|
||||||
|
// header (Grove-compatible G1/G2) plus the internal StampS3 pads.
|
||||||
|
// These are the lines the recon modules drive/read. Keep the count
|
||||||
|
// small: JTAGulator-style scanning is O(pins!) in the worst case.
|
||||||
|
//
|
||||||
|
// NOTE: only wire probes to the target through a level shifter /
|
||||||
|
// series resistors. The ESP32-S3 is a 3V3 part; 5V (car USB, PC USB
|
||||||
|
// VBUS) on a bare GPIO will kill it. See docs/HARDWARE.md.
|
||||||
|
|
||||||
|
namespace pins {
|
||||||
|
// Grove port (safe, broken out on the side connector)
|
||||||
|
constexpr int GROVE_A = 1; // G1
|
||||||
|
constexpr int GROVE_B = 2; // G2
|
||||||
|
|
||||||
|
// Extra probe channels (StampS3 exposed pads via ribbon/breakout).
|
||||||
|
// Order matters: index == logical channel number shown in the UI.
|
||||||
|
constexpr int PROBE[] = {1, 2, 8, 9, 10, 11, 12, 13};
|
||||||
|
constexpr int PROBE_COUNT = sizeof(PROBE) / sizeof(PROBE[0]);
|
||||||
|
|
||||||
|
// ADC channel used by the voltage-sense module (through a divider).
|
||||||
|
constexpr int VSENSE_ADC = 10;
|
||||||
|
|
||||||
|
// Divider ratio: Vin = Vadc * ((R1+R2)/R2). Default 1:3 (0-9.9V).
|
||||||
|
constexpr float VSENSE_RATIO = 3.0f;
|
||||||
|
}
|
||||||
71
src/core/shell.cpp
Normal file
71
src/core/shell.cpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#include "shell.h"
|
||||||
|
#include "ui.h"
|
||||||
|
#include <M5Cardputer.h>
|
||||||
|
|
||||||
|
// Module factories (defined in each module .cpp).
|
||||||
|
Module* makePinScan();
|
||||||
|
Module* makeVSense();
|
||||||
|
Module* makeUartSniff();
|
||||||
|
Module* makeUsbEnum();
|
||||||
|
Module* makeDefCred();
|
||||||
|
|
||||||
|
void Shell::begin() {
|
||||||
|
add(makePinScan());
|
||||||
|
add(makeVSense());
|
||||||
|
add(makeUartSniff());
|
||||||
|
add(makeUsbEnum());
|
||||||
|
add(makeDefCred());
|
||||||
|
M5.Display.fillScreen(ui::bg());
|
||||||
|
drawMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shell::drawMenu() {
|
||||||
|
M5.Display.fillScreen(ui::bg());
|
||||||
|
ui::titleBar("Card-Crack", CARDCRACK_VERSION);
|
||||||
|
for (int i = 0; i < nmods; i++) {
|
||||||
|
bool cur = i == sel;
|
||||||
|
ui::lineC(i, cur ? ui::accent() : ui::fg(), "%c %-10s %s",
|
||||||
|
cur ? '>' : ' ', mods[i]->name(), mods[i]->blurb());
|
||||||
|
}
|
||||||
|
ui::hintBar("arrows move [enter]open home-lab only");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shell::enter(int i) {
|
||||||
|
active = i;
|
||||||
|
M5.Display.fillScreen(ui::bg());
|
||||||
|
ui::titleBar(mods[i]->name(), mods[i]->blurb());
|
||||||
|
mods[i]->onEnter();
|
||||||
|
mods[i]->draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shell::back() {
|
||||||
|
if (active >= 0) mods[active]->onExit();
|
||||||
|
active = -1;
|
||||||
|
drawMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shell::loop() {
|
||||||
|
M5Cardputer.update();
|
||||||
|
|
||||||
|
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
|
||||||
|
auto st = M5Cardputer.Keyboard.keysState();
|
||||||
|
for (char c : st.word) {
|
||||||
|
if (c == '`') { if (active >= 0) back(); continue; }
|
||||||
|
if (active < 0) {
|
||||||
|
if (c == ';') { sel = (sel - 1 + nmods) % nmods; drawMenu(); } // up
|
||||||
|
else if (c == '.') { sel = (sel + 1) % nmods; drawMenu(); } // down
|
||||||
|
else if (c == '\r' || c == ' ') enter(sel);
|
||||||
|
} else {
|
||||||
|
mods[active]->onKey(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (st.enter && active < 0) enter(sel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~30 Hz tick for the active module.
|
||||||
|
if (active >= 0 && millis() - lastTick > 33) {
|
||||||
|
lastTick = millis();
|
||||||
|
mods[active]->tick();
|
||||||
|
mods[active]->draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/core/shell.h
Normal file
20
src/core/shell.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "module.h"
|
||||||
|
|
||||||
|
// The shell: owns the module list, the main menu, and event routing.
|
||||||
|
// Backtick (`) always returns to the menu from inside a module.
|
||||||
|
class Shell {
|
||||||
|
public:
|
||||||
|
void begin();
|
||||||
|
void loop();
|
||||||
|
private:
|
||||||
|
static constexpr int MAX = 12;
|
||||||
|
Module* mods[MAX]; int nmods = 0;
|
||||||
|
int sel = 0; // menu cursor
|
||||||
|
int active = -1; // -1 == in menu
|
||||||
|
uint32_t lastTick = 0;
|
||||||
|
void add(Module* m) { if (nmods < MAX) mods[nmods++] = m; }
|
||||||
|
void drawMenu();
|
||||||
|
void enter(int i);
|
||||||
|
void back();
|
||||||
|
};
|
||||||
53
src/core/ui.cpp
Normal file
53
src/core/ui.cpp
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#include "ui.h"
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
void titleBar(const char* title, const char* blurb) {
|
||||||
|
auto& d = M5.Display;
|
||||||
|
d.fillRect(0, 0, W, BODY_Y, d.color565(24, 28, 34));
|
||||||
|
d.setTextColor(accent(), d.color565(24, 28, 34));
|
||||||
|
d.setTextSize(1);
|
||||||
|
d.setCursor(3, 4);
|
||||||
|
d.print(title);
|
||||||
|
if (blurb && *blurb) {
|
||||||
|
d.setTextColor(fg(), d.color565(24, 28, 34));
|
||||||
|
// Right-align-ish: just continue after a separator.
|
||||||
|
d.print(" ");
|
||||||
|
d.print(blurb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hintBar(const char* hint) {
|
||||||
|
auto& d = M5.Display;
|
||||||
|
int y = H - 12;
|
||||||
|
d.fillRect(0, y, W, 12, d.color565(24, 28, 34));
|
||||||
|
d.setTextColor(d.color565(150, 156, 164), d.color565(24, 28, 34));
|
||||||
|
d.setTextSize(1);
|
||||||
|
d.setCursor(3, y + 2);
|
||||||
|
d.print(hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearBody() {
|
||||||
|
M5.Display.fillRect(0, BODY_Y, W, BODY_H, bg());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void vline(int row, uint16_t color, const char* fmt, va_list ap) {
|
||||||
|
char buf[64];
|
||||||
|
vsnprintf(buf, sizeof(buf), fmt, ap);
|
||||||
|
auto& d = M5.Display;
|
||||||
|
int y = BODY_Y + row * 12;
|
||||||
|
d.fillRect(0, y, W, 12, bg());
|
||||||
|
d.setTextColor(color, bg());
|
||||||
|
d.setTextSize(1);
|
||||||
|
d.setCursor(3, y + 1);
|
||||||
|
d.print(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void line(int row, const char* fmt, ...) {
|
||||||
|
va_list ap; va_start(ap, fmt); vline(row, fg(), fmt, ap); va_end(ap);
|
||||||
|
}
|
||||||
|
void lineC(int row, uint16_t color, const char* fmt, ...) {
|
||||||
|
va_list ap; va_start(ap, fmt); vline(row, color, fmt, ap); va_end(ap);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/core/ui.h
Normal file
25
src/core/ui.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <M5Cardputer.h>
|
||||||
|
|
||||||
|
// Tiny drawing helpers shared by all modules. The Cardputer screen is
|
||||||
|
// 240x135. We reserve a 16px title bar and a 12px hint bar, leaving a
|
||||||
|
// 240x107 body starting at y=16.
|
||||||
|
|
||||||
|
namespace ui {
|
||||||
|
constexpr int W = 240, H = 135;
|
||||||
|
constexpr int BODY_Y = 16, BODY_H = 107;
|
||||||
|
|
||||||
|
// Palette (RGB565 via M5GFX helpers).
|
||||||
|
inline uint16_t bg() { return M5.Display.color565(12, 14, 18); }
|
||||||
|
inline uint16_t fg() { return M5.Display.color565(210, 214, 220); }
|
||||||
|
inline uint16_t accent(){ return M5.Display.color565(80, 200, 120); }
|
||||||
|
inline uint16_t warn() { return M5.Display.color565(230, 170, 60); }
|
||||||
|
inline uint16_t bad() { return M5.Display.color565(220, 80, 80); }
|
||||||
|
|
||||||
|
void titleBar(const char* title, const char* blurb);
|
||||||
|
void hintBar(const char* hint);
|
||||||
|
void clearBody();
|
||||||
|
// Print a line at body row `row` (0-based, 12px pitch).
|
||||||
|
void line(int row, const char* fmt, ...);
|
||||||
|
void lineC(int row, uint16_t color, const char* fmt, ...);
|
||||||
|
}
|
||||||
21
src/main.cpp
Normal file
21
src/main.cpp
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#include <M5Cardputer.h>
|
||||||
|
#include "core/shell.h"
|
||||||
|
|
||||||
|
// Card-Crack — Cardputer (ESP32-S3) recon toolkit for home-lab hardware
|
||||||
|
// pentesting. Manual-trigger tools only; see README and docs/HARDWARE.md
|
||||||
|
// before wiring anything to a live target.
|
||||||
|
|
||||||
|
static Shell shell;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
auto cfg = M5.config();
|
||||||
|
M5Cardputer.begin(cfg, true);
|
||||||
|
M5.Display.setRotation(1);
|
||||||
|
M5.Display.setTextFont(&fonts::Font0);
|
||||||
|
shell.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
shell.loop();
|
||||||
|
delay(2);
|
||||||
|
}
|
||||||
114
src/modules/defcred.cpp
Normal file
114
src/modules/defcred.cpp
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#include "../core/module.h"
|
||||||
|
#include "../core/ui.h"
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include "mbedtls/base64.h"
|
||||||
|
|
||||||
|
// Default-credential checker for devices on YOUR network. Point it at a
|
||||||
|
// host (default = the gateway) and it walks a short list of well-known
|
||||||
|
// vendor default logins, trying HTTP Basic Auth against the admin path.
|
||||||
|
// It reports which pair the device ACCEPTS so you can go change it.
|
||||||
|
//
|
||||||
|
// Scope guards, on purpose:
|
||||||
|
// * one host at a time, entered by you — no subnet sweeping;
|
||||||
|
// * a tiny curated default list — this is a "did you forget to change
|
||||||
|
// the factory password" check, not a brute-force/dictionary attack;
|
||||||
|
// * a delay between attempts so it can't hammer a device;
|
||||||
|
// * stops on the first success.
|
||||||
|
//
|
||||||
|
// Join your own AP first (creds compiled below, or reflash to change).
|
||||||
|
|
||||||
|
#ifndef CC_WIFI_SSID
|
||||||
|
#define CC_WIFI_SSID "set-me"
|
||||||
|
#endif
|
||||||
|
#ifndef CC_WIFI_PASS
|
||||||
|
#define CC_WIFI_PASS "set-me"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class DefCred : public Module {
|
||||||
|
struct Cred { const char* u; const char* p; };
|
||||||
|
// Common factory defaults across consumer routers/appliances.
|
||||||
|
static constexpr Cred LIST[] = {
|
||||||
|
{"admin", "admin"}, {"admin", "password"}, {"admin", ""},
|
||||||
|
{"admin", "1234"}, {"root", "root"}, {"root", "admin"},
|
||||||
|
{"user", "user"}, {"admin", "12345678"}, {"support", "support"},
|
||||||
|
};
|
||||||
|
static constexpr int N = sizeof(LIST) / sizeof(LIST[0]);
|
||||||
|
|
||||||
|
String host = "192.168.1.1";
|
||||||
|
String path = "/";
|
||||||
|
char status[3][40] = {{0},{0},{0}};
|
||||||
|
bool running = false;
|
||||||
|
int ix = 0;
|
||||||
|
uint32_t lastTry = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "DefCred"; }
|
||||||
|
const char* blurb() const override { return "default login check"; }
|
||||||
|
|
||||||
|
void onEnter() override {
|
||||||
|
running = false; ix = 0;
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.begin(CC_WIFI_SSID, CC_WIFI_PASS);
|
||||||
|
say("joining %s...", CC_WIFI_SSID);
|
||||||
|
}
|
||||||
|
if (WiFi.status() == WL_CONNECTED) host = WiFi.gatewayIP().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onKey(char c) override {
|
||||||
|
if (c == ' ' || c == '\r') {
|
||||||
|
if (WiFi.status() != WL_CONNECTED) { say("wifi not up yet"); return true; }
|
||||||
|
running = !running; if (running) { ix = 0; say("target %s", host.c_str()); }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick() override {
|
||||||
|
if (WiFi.status() == WL_CONNECTED && host == "192.168.1.1")
|
||||||
|
host = WiFi.gatewayIP().toString();
|
||||||
|
if (!running) return;
|
||||||
|
if (millis() - lastTry < 600) return; // rate-limit
|
||||||
|
lastTry = millis();
|
||||||
|
if (ix >= N) { running = false; say("-- no default creds worked --"); return; }
|
||||||
|
tryOne(LIST[ix]);
|
||||||
|
ix++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() override {
|
||||||
|
bool up = WiFi.status() == WL_CONNECTED;
|
||||||
|
ui::lineC(0, up ? ui::accent() : ui::warn(), "wifi:%s host:%s",
|
||||||
|
up ? "up" : "down", host.c_str());
|
||||||
|
ui::line(1, "trying %d/%d %s", ix, N, running ? "RUN" : "idle");
|
||||||
|
for (int i = 0; i < 3; i++) ui::line(3 + i, "%s", status[i]);
|
||||||
|
ui::hintBar("[space]start/stop [`]back");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void say(const char* fmt, ...) {
|
||||||
|
for (int i = 2; i > 0; i--) strncpy(status[i], status[i-1], 39);
|
||||||
|
va_list ap; va_start(ap, fmt);
|
||||||
|
vsnprintf(status[0], 40, fmt, ap); va_end(ap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tryOne(const Cred& c) {
|
||||||
|
HTTPClient http;
|
||||||
|
String url = "http://" + host + path;
|
||||||
|
if (!http.begin(url)) { say("begin fail"); return; }
|
||||||
|
String token = String(c.u) + ":" + c.p;
|
||||||
|
unsigned char enc[128]; size_t olen = 0;
|
||||||
|
mbedtls_base64_encode(enc, sizeof(enc), &olen,
|
||||||
|
(const unsigned char*)token.c_str(), token.length());
|
||||||
|
http.addHeader("Authorization", "Basic " + String((char*)enc).substring(0, olen));
|
||||||
|
http.setConnectTimeout(1500);
|
||||||
|
int code = http.GET();
|
||||||
|
if (code == 200) { say("OK! %s:%s <-- CHANGE IT", c.u, c.p[0] ? c.p : "(blank)"); running = false; }
|
||||||
|
else if (code == 401) say("401 %s:%s", c.u, c.p[0] ? c.p : "-");
|
||||||
|
else say("%d %s:%s", code, c.u, c.p[0] ? c.p : "-");
|
||||||
|
http.end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
constexpr DefCred::Cred DefCred::LIST[];
|
||||||
|
|
||||||
|
Module* makeDefCred() { return new DefCred(); }
|
||||||
192
src/modules/pinscan.cpp
Normal file
192
src/modules/pinscan.cpp
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
#include "../core/module.h"
|
||||||
|
#include "../core/ui.h"
|
||||||
|
#include "../core/pins.h"
|
||||||
|
|
||||||
|
// Pin-detection scanner. Three passive/semi-active discovery modes on
|
||||||
|
// the PROBE channels:
|
||||||
|
//
|
||||||
|
// UART : hold each candidate as input, watch for line activity, then
|
||||||
|
// bit-time the shortest low pulse to estimate baud. Optional
|
||||||
|
// TX-poke ("\r\n") on a paired pin to elicit a boot banner.
|
||||||
|
// JTAG : IDCODE scan — drive TCK/TMS on candidate pairs, shift TDO,
|
||||||
|
// look for a valid 32-bit IDCODE (bit0==1, != all-ones).
|
||||||
|
// SWD : line-reset + read IDCODE via SWD-DP (ARM ADIv5). Two wires
|
||||||
|
// (SWCLK/SWDIO), so the permutation space is small.
|
||||||
|
//
|
||||||
|
// All modes are manual-trigger and chunked across ticks so the UI stays
|
||||||
|
// live and you can abort. This is discovery only: it identifies pins and
|
||||||
|
// reads public ID registers. It does not halt cores or dump memory.
|
||||||
|
|
||||||
|
class PinScan : public Module {
|
||||||
|
enum Mode { UART, JTAG, SWD } mode = UART;
|
||||||
|
bool running = false;
|
||||||
|
int step = 0; // permutation cursor
|
||||||
|
int found = 0;
|
||||||
|
char last[3][40] = {{0},{0},{0}};
|
||||||
|
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "Pin Scan"; }
|
||||||
|
const char* blurb() const override { return "UART/JTAG/SWD detect"; }
|
||||||
|
|
||||||
|
void onEnter() override {
|
||||||
|
for (int i = 0; i < pins::PROBE_COUNT; i++)
|
||||||
|
pinMode(pins::PROBE[i], INPUT); // hi-Z until we act
|
||||||
|
running = false; step = 0; found = 0;
|
||||||
|
}
|
||||||
|
void onExit() override {
|
||||||
|
for (int i = 0; i < pins::PROBE_COUNT; i++)
|
||||||
|
pinMode(pins::PROBE[i], INPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onKey(char c) override {
|
||||||
|
if (c == 'm') { mode = (Mode)((mode + 1) % 3); running = false; step = 0; found = 0; return true; }
|
||||||
|
if (c == ' ' || c == '\r') { running = !running; if (running) { step = 0; found = 0; } return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick() override {
|
||||||
|
if (!running) return;
|
||||||
|
// Do one permutation per tick (bounded work -> responsive UI).
|
||||||
|
switch (mode) {
|
||||||
|
case UART: scanUartStep(); break;
|
||||||
|
case JTAG: scanJtagStep(); break;
|
||||||
|
case SWD: scanSwdStep(); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() override {
|
||||||
|
const char* mn = mode == UART ? "UART" : mode == JTAG ? "JTAG" : "SWD";
|
||||||
|
ui::lineC(0, ui::accent(), "mode:%-4s %s", mn, running ? "SCANNING" : "idle");
|
||||||
|
ui::line(1, "chans:%d perm:%d hits:%d", pins::PROBE_COUNT, step, found);
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
ui::line(3 + i, "%s", last[i]);
|
||||||
|
ui::hintBar("[m]ode [space]start/stop [`]back");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void record(const char* s) {
|
||||||
|
for (int i = 2; i > 0; i--) strncpy(last[i], last[i-1], 39);
|
||||||
|
strncpy(last[0], s, 39); last[0][39] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UART ---------------------------------------------------------
|
||||||
|
// Sample a candidate RX pin, measure the narrowest pulse, map to baud.
|
||||||
|
void scanUartStep() {
|
||||||
|
int ch = step % pins::PROBE_COUNT;
|
||||||
|
int pin = pins::PROBE[ch];
|
||||||
|
pinMode(pin, INPUT_PULLUP);
|
||||||
|
uint32_t minLow = measureMinPulse(pin, 3000 /*us window*/);
|
||||||
|
if (minLow) {
|
||||||
|
long baud = pulseToBaud(minLow);
|
||||||
|
char b[40]; snprintf(b, sizeof(b), "UART? G%d ~%ld 8N1", pin, baud);
|
||||||
|
record(b); found++;
|
||||||
|
}
|
||||||
|
if (++step >= pins::PROBE_COUNT) { running = false; record("-- uart scan done --"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t measureMinPulse(int pin, uint32_t windowUs) {
|
||||||
|
uint32_t t0 = micros(), minLow = 0;
|
||||||
|
int last = digitalRead(pin);
|
||||||
|
uint32_t edge = micros();
|
||||||
|
while (micros() - t0 < windowUs) {
|
||||||
|
int now = digitalRead(pin);
|
||||||
|
if (now != last) {
|
||||||
|
uint32_t w = micros() - edge;
|
||||||
|
if (last == LOW && (minLow == 0 || w < minLow)) minLow = w;
|
||||||
|
edge = micros(); last = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minLow;
|
||||||
|
}
|
||||||
|
|
||||||
|
long pulseToBaud(uint32_t us) {
|
||||||
|
static const long std[] = {9600, 19200, 38400, 57600, 115200, 230400, 460800};
|
||||||
|
long est = 1000000L / (long)us; // one bit-time
|
||||||
|
long best = std[0]; long bd = 1L<<30;
|
||||||
|
for (long s : std) { long d = labs(s - est); if (d < bd) { bd = d; best = s; } }
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- JTAG (IDCODE) ------------------------------------------------
|
||||||
|
// Try ordered (TCK,TMS,TDO) triples; TDI is optional for IDCODE.
|
||||||
|
void scanJtagStep() {
|
||||||
|
int n = pins::PROBE_COUNT;
|
||||||
|
int tck = step / (n * n) % n;
|
||||||
|
int tms = step / n % n;
|
||||||
|
int tdo = step % n;
|
||||||
|
if (tck != tms && tms != tdo && tck != tdo) {
|
||||||
|
uint32_t id = readJtagIdcode(pins::PROBE[tck], pins::PROBE[tms], pins::PROBE[tdo]);
|
||||||
|
if (id && id != 0xFFFFFFFF && (id & 1)) {
|
||||||
|
char b[40]; snprintf(b, sizeof(b), "JTAG id=%08lX T%d/%d/%d",
|
||||||
|
(unsigned long)id, pins::PROBE[tck], pins::PROBE[tms], pins::PROBE[tdo]);
|
||||||
|
record(b); found++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (++step >= n*n*n) { running = false; record("-- jtag scan done --"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t readJtagIdcode(int tck, int tms, int tdo) {
|
||||||
|
pinMode(tck, OUTPUT); pinMode(tms, OUTPUT); pinMode(tdo, INPUT_PULLUP);
|
||||||
|
auto clk = [&](int tmsv){ digitalWrite(tms, tmsv); digitalWrite(tck, LOW);
|
||||||
|
delayMicroseconds(2); digitalWrite(tck, HIGH); delayMicroseconds(2); };
|
||||||
|
for (int i = 0; i < 5; i++) clk(1); // -> Test-Logic-Reset (loads IDCODE)
|
||||||
|
clk(0); clk(0); // -> Run-Test/Idle -> Select-DR
|
||||||
|
clk(1); clk(0); clk(0); // -> Shift-DR
|
||||||
|
uint32_t id = 0;
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
digitalWrite(tck, LOW); delayMicroseconds(2);
|
||||||
|
if (digitalRead(tdo)) id |= (1UL << i);
|
||||||
|
digitalWrite(tck, HIGH); delayMicroseconds(2);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SWD (ARM ADIv5 DP IDCODE) ------------------------------------
|
||||||
|
void scanSwdStep() {
|
||||||
|
int n = pins::PROBE_COUNT;
|
||||||
|
int clk = step / n % n, io = step % n;
|
||||||
|
if (clk != io) {
|
||||||
|
uint32_t id = readSwdIdcode(pins::PROBE[clk], pins::PROBE[io]);
|
||||||
|
if (id && id != 0xFFFFFFFF) {
|
||||||
|
char b[40]; snprintf(b, sizeof(b), "SWD id=%08lX CLK%d IO%d",
|
||||||
|
(unsigned long)id, pins::PROBE[clk], pins::PROBE[io]);
|
||||||
|
record(b); found++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (++step >= n*n) { running = false; record("-- swd scan done --"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t readSwdIdcode(int swclk, int swdio) {
|
||||||
|
pinMode(swclk, OUTPUT); pinMode(swdio, OUTPUT);
|
||||||
|
auto wbit = [&](int b){ digitalWrite(swdio, b); digitalWrite(swclk, LOW);
|
||||||
|
delayMicroseconds(2); digitalWrite(swclk, HIGH); delayMicroseconds(2); };
|
||||||
|
// Line reset: >=50 clocks with SWDIO high, then JTAG->SWD magic 0xE79E.
|
||||||
|
for (int i = 0; i < 56; i++) wbit(1);
|
||||||
|
uint16_t magic = 0xE79E;
|
||||||
|
for (int i = 0; i < 16; i++) wbit((magic >> i) & 1);
|
||||||
|
for (int i = 0; i < 56; i++) wbit(1);
|
||||||
|
for (int i = 0; i < 4; i++) wbit(0);
|
||||||
|
// Request: read DP reg 0 (IDCODE). Start=1 APnDP=0 RnW=1 A[2:3]=00, parity, stop=0, park=1.
|
||||||
|
uint8_t req = 0xA5;
|
||||||
|
for (int i = 0; i < 8; i++) wbit((req >> i) & 1);
|
||||||
|
// Turnaround, read 3-bit ACK.
|
||||||
|
pinMode(swdio, INPUT_PULLUP);
|
||||||
|
digitalWrite(swclk, LOW); delayMicroseconds(2); digitalWrite(swclk, HIGH); delayMicroseconds(2);
|
||||||
|
uint8_t ack = 0;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
digitalWrite(swclk, LOW); delayMicroseconds(2);
|
||||||
|
if (digitalRead(swdio)) ack |= (1 << i);
|
||||||
|
digitalWrite(swclk, HIGH); delayMicroseconds(2);
|
||||||
|
}
|
||||||
|
if (ack != 0x1) return 0; // 0b001 = OK
|
||||||
|
uint32_t id = 0;
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
digitalWrite(swclk, LOW); delayMicroseconds(2);
|
||||||
|
if (digitalRead(swdio)) id |= (1UL << i);
|
||||||
|
digitalWrite(swclk, HIGH); delayMicroseconds(2);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Module* makePinScan() { return new PinScan(); }
|
||||||
96
src/modules/uartsniff.cpp
Normal file
96
src/modules/uartsniff.cpp
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#include "../core/module.h"
|
||||||
|
#include "../core/ui.h"
|
||||||
|
#include "../core/pins.h"
|
||||||
|
|
||||||
|
// UART protocol sniff + replay. Passive by default:
|
||||||
|
//
|
||||||
|
// SNIFF : attach HardwareSerial to the Grove pins, dump a rolling
|
||||||
|
// hex/ascii view, and keep a small ring buffer of the last
|
||||||
|
// N bytes. Cycle common bauds with [b] until it reads clean.
|
||||||
|
// CAPTURE : freeze the current ring buffer as the "replay frame".
|
||||||
|
// REPLAY : re-transmit the captured frame ON A KEYPRESS ONLY. This is
|
||||||
|
// the one active operation in the module and it never fires
|
||||||
|
// on its own — you press [t] each time.
|
||||||
|
//
|
||||||
|
// Replay is for your own bench: confirming a command you already saw the
|
||||||
|
// device send/accept (e.g. a UART-controlled relay, an OBD ELM327 AT
|
||||||
|
// string). It is not a fuzzer and does not brute-force.
|
||||||
|
|
||||||
|
class UartSniff : public Module {
|
||||||
|
static constexpr int RING = 256;
|
||||||
|
uint8_t ring[RING]; int head = 0, count = 0;
|
||||||
|
uint8_t frame[RING]; int frameLen = 0;
|
||||||
|
const long BAUDS[6] = {9600, 19200, 38400, 57600, 115200, 230400};
|
||||||
|
int baudIx = 4;
|
||||||
|
bool live = false;
|
||||||
|
HardwareSerial& port = Serial1;
|
||||||
|
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "UART Sniff"; }
|
||||||
|
const char* blurb() const override { return "sniff / capture / replay"; }
|
||||||
|
|
||||||
|
void onEnter() override { count = head = 0; frameLen = 0; startPort(); }
|
||||||
|
void onExit() override { port.end(); live = false; }
|
||||||
|
|
||||||
|
bool onKey(char c) override {
|
||||||
|
if (c == 'b') { baudIx = (baudIx + 1) % 6; startPort(); return true; }
|
||||||
|
if (c == ' ') { live = !live; return true; }
|
||||||
|
if (c == 'c') { capture(); return true; }
|
||||||
|
if (c == 't') { replayOnce(); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick() override {
|
||||||
|
if (!live) return;
|
||||||
|
while (port.available() && count < RING * 2) { // bound per tick
|
||||||
|
uint8_t b = port.read();
|
||||||
|
ring[head] = b; head = (head + 1) % RING;
|
||||||
|
if (count < RING) count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() override {
|
||||||
|
ui::lineC(0, ui::accent(), "%ld 8N1 %s buf:%d frame:%d",
|
||||||
|
BAUDS[baudIx], live ? "LIVE" : "paused", count, frameLen);
|
||||||
|
// Last 3 rows = 8 bytes each of the tail of the ring, hex+ascii.
|
||||||
|
for (int r = 0; r < 3; r++) drawRow(2 + r, r);
|
||||||
|
ui::hintBar("[b]aud [space]live [c]apture [t]x-replay");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void startPort() {
|
||||||
|
port.end();
|
||||||
|
port.begin(BAUDS[baudIx], SERIAL_8N1, pins::GROVE_A /*RX*/, pins::GROVE_B /*TX*/);
|
||||||
|
live = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawRow(int uiRow, int chunk) {
|
||||||
|
char line[40]; int p = 0;
|
||||||
|
int start = (head - count + (count - (chunk + 1) * 8) + RING * 4) % RING;
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
uint8_t b = ring[(start + i) % RING];
|
||||||
|
p += snprintf(line + p, sizeof(line) - p, "%02X ", b);
|
||||||
|
}
|
||||||
|
p += snprintf(line + p, sizeof(line) - p, " ");
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
uint8_t b = ring[(start + i) % RING];
|
||||||
|
line[p++] = (b >= 32 && b < 127) ? b : '.';
|
||||||
|
}
|
||||||
|
line[p] = 0;
|
||||||
|
ui::line(uiRow, "%s", line);
|
||||||
|
}
|
||||||
|
|
||||||
|
void capture() {
|
||||||
|
frameLen = count < RING ? count : RING;
|
||||||
|
int start = (head - frameLen + RING) % RING;
|
||||||
|
for (int i = 0; i < frameLen; i++) frame[i] = ring[(start + i) % RING];
|
||||||
|
}
|
||||||
|
|
||||||
|
void replayOnce() { // manual-only TX
|
||||||
|
if (frameLen == 0) return;
|
||||||
|
port.write(frame, frameLen);
|
||||||
|
port.flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Module* makeUartSniff() { return new UartSniff(); }
|
||||||
126
src/modules/usbenum.cpp
Normal file
126
src/modules/usbenum.cpp
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#include "../core/module.h"
|
||||||
|
#include "../core/ui.h"
|
||||||
|
|
||||||
|
// USB descriptor enumeration. The ESP32-S3 has a USB-OTG core, so it can
|
||||||
|
// act as a host and read the standard descriptors a device advertises:
|
||||||
|
// VID/PID, device class, manufacturer/product strings, and the interface
|
||||||
|
// classes of each configuration. That's exactly what you want for
|
||||||
|
// fingerprinting an unknown dongle, a car's USB port, or a peripheral
|
||||||
|
// before you decide what it actually is.
|
||||||
|
//
|
||||||
|
// This is read-only enumeration. We open the device, pull descriptors,
|
||||||
|
// and close. We do NOT claim interfaces, send class requests, or drive
|
||||||
|
// endpoints — no HID injection, no mass-storage access.
|
||||||
|
//
|
||||||
|
// Requires the board's USB pins wired to the target as HOST (VBUS out).
|
||||||
|
// Build with ARDUINO_USB_MODE so the console stays on the CDC, and use a
|
||||||
|
// separate OTG connection for the target. See docs/HARDWARE.md.
|
||||||
|
|
||||||
|
#if __has_include("usb/usb_host.h")
|
||||||
|
#include "usb/usb_host.h"
|
||||||
|
#define HAVE_USB_HOST 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class UsbEnum : public Module {
|
||||||
|
char lines[6][40];
|
||||||
|
int nlines = 0;
|
||||||
|
bool installed = false;
|
||||||
|
#ifdef HAVE_USB_HOST
|
||||||
|
usb_host_client_handle_t client = nullptr;
|
||||||
|
usb_device_handle_t dev = nullptr;
|
||||||
|
uint8_t devAddr = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "USB Enum"; }
|
||||||
|
const char* blurb() const override { return "descriptor readout"; }
|
||||||
|
|
||||||
|
void onEnter() override {
|
||||||
|
nlines = 0;
|
||||||
|
say("host idle. plug target, press [space]");
|
||||||
|
#ifndef HAVE_USB_HOST
|
||||||
|
say("build w/ ESP-IDF usb_host to enable");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
void onExit() override { teardown(); }
|
||||||
|
|
||||||
|
bool onKey(char c) override {
|
||||||
|
if (c == ' ') { enumerate(); return true; }
|
||||||
|
if (c == 'x') { teardown(); nlines = 0; say("host released"); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() override {
|
||||||
|
ui::lineC(0, ui::accent(), "USB host %s", installed ? "up" : "down");
|
||||||
|
for (int i = 0; i < nlines; i++) ui::line(1 + i, "%s", lines[i]);
|
||||||
|
ui::hintBar("[space]enumerate [x]release [`]back");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void say(const char* fmt, ...) {
|
||||||
|
if (nlines >= 6) { for (int i = 1; i < 6; i++) strncpy(lines[i-1], lines[i], 39); nlines = 5; }
|
||||||
|
va_list ap; va_start(ap, fmt);
|
||||||
|
vsnprintf(lines[nlines], 40, fmt, ap); va_end(ap);
|
||||||
|
nlines++;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_USB_HOST
|
||||||
|
bool ensureHost() {
|
||||||
|
if (installed) return true;
|
||||||
|
usb_host_config_t hc = {}; hc.intr_flags = ESP_INTR_FLAG_LEVEL1;
|
||||||
|
if (usb_host_install(&hc) != ESP_OK) { say("usb_host_install fail"); return false; }
|
||||||
|
usb_host_client_config_t cc = {};
|
||||||
|
cc.max_num_event_msg = 5;
|
||||||
|
if (usb_host_client_register(&cc, &client) != ESP_OK) { say("client reg fail"); return false; }
|
||||||
|
installed = true; return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enumerate() {
|
||||||
|
if (!ensureHost()) return;
|
||||||
|
// Pump the host lib so a freshly-plugged device gets an address.
|
||||||
|
for (int i = 0; i < 200; i++) {
|
||||||
|
usb_host_lib_handle_events(pdMS_TO_TICKS(5), nullptr);
|
||||||
|
usb_host_client_handle_events(client, pdMS_TO_TICKS(5));
|
||||||
|
}
|
||||||
|
int num = 0; uint8_t list[8];
|
||||||
|
if (usb_host_device_addr_list_fill(sizeof(list), list, &num) != ESP_OK || num == 0) {
|
||||||
|
say("no device detected"); return;
|
||||||
|
}
|
||||||
|
devAddr = list[0];
|
||||||
|
if (usb_host_device_open(client, devAddr, &dev) != ESP_OK) { say("open @%d fail", devAddr); return; }
|
||||||
|
|
||||||
|
const usb_device_desc_t* dd = nullptr;
|
||||||
|
if (usb_host_get_device_descriptor(dev, &dd) == ESP_OK && dd) {
|
||||||
|
say("VID:PID %04X:%04X", dd->idVendor, dd->idProduct);
|
||||||
|
say("class %02X sub %02X proto %02X", dd->bDeviceClass, dd->bDeviceSubClass, dd->bDeviceProtocol);
|
||||||
|
say("bcdUSB %04X cfgs %d", dd->bcdUSB, dd->bNumConfigurations);
|
||||||
|
readString(dd->iManufacturer, "mfr");
|
||||||
|
readString(dd->iProduct, "prod");
|
||||||
|
} else say("get dev desc fail");
|
||||||
|
|
||||||
|
const usb_config_desc_t* cd = nullptr;
|
||||||
|
if (usb_host_get_active_config_descriptor(dev, &cd) == ESP_OK && cd)
|
||||||
|
say("cfg len %d ifaces %d", cd->wTotalLength, cd->bNumInterfaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
void readString(uint8_t idx, const char* label) {
|
||||||
|
if (!idx) return;
|
||||||
|
usb_transfer_t* t = nullptr;
|
||||||
|
// Minimal control-in for a string descriptor; best-effort.
|
||||||
|
char out[24] = {0};
|
||||||
|
if (usb_host_get_string_descriptor(dev, idx, 0x0409, (uint8_t*)out, sizeof(out)) == ESP_OK)
|
||||||
|
say("%s: %s", label, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void teardown() {
|
||||||
|
if (dev) { usb_host_device_close(client, dev); dev = nullptr; }
|
||||||
|
if (client) { usb_host_client_deregister(client); client = nullptr; }
|
||||||
|
if (installed) { usb_host_uninstall(); installed = false; }
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
void enumerate() { say("USB host stack not compiled in"); }
|
||||||
|
void teardown() {}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
Module* makeUsbEnum() { return new UsbEnum(); }
|
||||||
62
src/modules/vsense.cpp
Normal file
62
src/modules/vsense.cpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#include "../core/module.h"
|
||||||
|
#include "../core/ui.h"
|
||||||
|
#include "../core/pins.h"
|
||||||
|
|
||||||
|
// Voltage sensing. Reads the ADC behind an external divider and reports
|
||||||
|
// a live voltage plus a guess at the logic family, so you know what
|
||||||
|
// you're probing BEFORE you drive a pin into it. Min/max are held so a
|
||||||
|
// brief transient (e.g. a bus idling high) is captured.
|
||||||
|
//
|
||||||
|
// The ESP32-S3 ADC is non-linear near the rails; we use the Arduino
|
||||||
|
// analogReadMilliVolts() calibration path and apply the divider ratio.
|
||||||
|
|
||||||
|
class VSense : public Module {
|
||||||
|
float v = 0, vmin = 99, vmax = 0;
|
||||||
|
uint32_t lastMs = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "V-Sense"; }
|
||||||
|
const char* blurb() const override { return "target voltage probe"; }
|
||||||
|
|
||||||
|
void onEnter() override {
|
||||||
|
analogReadResolution(12);
|
||||||
|
pinMode(pins::VSENSE_ADC, INPUT);
|
||||||
|
vmin = 99; vmax = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onKey(char c) override {
|
||||||
|
if (c == 'r') { vmin = 99; vmax = 0; return true; } // reset hold
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick() override {
|
||||||
|
if (millis() - lastMs < 100) return;
|
||||||
|
lastMs = millis();
|
||||||
|
uint32_t mv = analogReadMilliVolts(pins::VSENSE_ADC);
|
||||||
|
v = (mv / 1000.0f) * pins::VSENSE_RATIO;
|
||||||
|
if (v < vmin) vmin = v;
|
||||||
|
if (v > vmax) vmax = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() override {
|
||||||
|
ui::lineC(0, ui::accent(), "Vin = %5.2f V", v);
|
||||||
|
ui::line(1, "min %4.2f max %4.2f (ratio %.1f)", vmin, vmax, pins::VSENSE_RATIO);
|
||||||
|
ui::lineC(3, family(v).color, "logic: %s", family(v).name);
|
||||||
|
ui::line(5, "wire target -> divider -> G%d", pins::VSENSE_ADC);
|
||||||
|
ui::hintBar("[r]eset hold [`]back");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Fam { const char* name; uint16_t color; };
|
||||||
|
Fam family(float x) {
|
||||||
|
if (x < 0.3f) return {"floating / GND", ui::fg()};
|
||||||
|
if (x < 1.5f) return {"1.2V core rail", ui::warn()};
|
||||||
|
if (x < 2.1f) return {"1.8V logic", ui::warn()};
|
||||||
|
if (x < 3.0f) return {"2.5V logic", ui::warn()};
|
||||||
|
if (x < 3.9f) return {"3.3V logic OK", ui::accent()};
|
||||||
|
if (x < 6.0f) return {"5V ! shift req", ui::bad()};
|
||||||
|
return {"HIGH >6V DANGER", ui::bad()};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Module* makeVSense() { return new VSense(); }
|
||||||
Reference in New Issue
Block a user