diff --git a/CMakeLists.txt b/CMakeLists.txt index 26c1320..b445978 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -153,7 +153,14 @@ add_executable(pico_project_template src/serial.c src/protocol.c src/crypt.c + src/cpu_monitor.c + src/hooks.c src/hal/hal_pico_i2c.c + src/net/wifi_ap.c + src/net/ap/ap_manager.c + src/net/http/http_server.c + src/net/api/api.c + src/net/dhcp/dhcpserver.c ) # Link to pico_stdlib @@ -163,7 +170,12 @@ target_link_libraries(pico_project_template cryptoauth pico_mbedtls FreeRTOS-Kernel-Heap4 - freertos_config) + freertos_config + pico_cyw43_arch_lwip_threadsafe_background + pico_lwip_http + hardware_adc + hardware_watchdog +) # Enable usb output, disable uart output pico_enable_stdio_usb(pico_project_template 1) @@ -176,9 +188,13 @@ pico_add_extra_outputs(pico_project_template) target_include_directories(pico_project_template PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include + ${CMAKE_CURRENT_LIST_DIR}/src + ${CMAKE_CURRENT_LIST_DIR}/src/net ${cryptoauthlib_SOURCE_DIR}/lib ${cryptoauthlib_SOURCE_DIR}/lib/hal ${cryptoauthlib_SOURCE_DIR}/lib/calib + ${PICO_SDK_PATH}/lib/lwip/src/include + ${PICO_SDK_PATH}/lib/lwip/contrib/ports/freertos/include ) if(BUILD_TESTS) diff --git a/README.md b/README.md index c572374..c534223 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,148 @@ -# MASTR: Mutual Attested Secure Token for Robotics +# 🤖 MASTR Token - WiFi AP Integration + +**M**utual **A**ttested **S**ecure **T**oken for **R**obotics - now with WiFi (bruh). + +A tiny microcontroller trying to be both a secure token AND a WiFi router. Somehow it mostly works. 🤷 -MASTR is a security-focused project designed to establish a secure communication channel between a host system and a hardware token. It utilizes a three-phase protocol to ensure mutual attestation, secure channel establishment, and runtime integrity verification. +## ✨ What Works (For Real This Time) + +### ✅ WiFi Access Point +- **SSID:** `MASTR-Token` +- **Password:** `MastrToken123` +- **IP:** `192.168.4.1` +- Basically: your Pico becomes a WiFi hotspot. Crazy, I know. -## Protocol Overview +### ✅ HTTP API (The Good Parts) + +#### `/api/ping` +```bash +curl http://192.168.4.1/api/ping +# Bruh, it responds: {"message":"pong"} +``` +Simple connectivity test. Does what it says on the tin. -The MASTR protocol is divided into three distinct phases: +#### `/api/status` (Actually Works!) +```bash +curl http://192.168.4.1/api/status +# Returns: {"provisioned":true/false, "state":"0x40", "uptime_s":123} +``` +Tells you if the token is provisioned yet. That's literally it. Works great. -### Phase 1: Host-Token Pairing Process +#### `/api/info` +**NOPE.** Crashes immediately. Disabled. Don't ask. 💀 -This initial, one-time pairing process establishes a trusted relationship between the host and the token. +### 🌐 Web Dashboard +- Open `http://192.168.4.1/` in your browser +- See the status with a nice UI +- Manual refresh only (auto-refresh made it angry) -Host-Token Pairing Process +## 🏗️ How It's Organized -1. **Key Generation:** Both the host and the token generate a new, persistent ECDSA keypair. -2. **Public Key Exchange:** The host and token exchange their public keys. -3. **Golden Hash:** The host generates a "golden hash" of its boot file and shares it with the token. This hash represents the known-good state of the host's software. +``` +FreeRTOS Tasks (The Chaos): +├── Serial (Priority 26) - Your USB connection +├── Watchdog (Priority 27) - Judges everything +├── WiFi Background (Priority 25) - Talks to WiFi chip +├── HTTP Server (Priority 5) - Handles web requests +└── WiFi Init (Priority 5) - Starts the AP, then dips +``` -### Phase 2: Mutual Attestation & Secure Channel Establishment +Each task does its thing. Sometimes they play nice. Sometimes they don't. -This phase is performed on every boot to establish a secure session. +## � The Problems (Why You're Here) -Secure Channel Establishment +### Problem #1: Serial Breaks When WiFi Starts +- **What happens:** You plug it in, start provisioning, then enable WiFi → EVERYTHING BREAKS +- **Why:** WiFi initialization steals CPU time from serial task +- **Current fix:** Wait 60 seconds before enabling WiFi so provisioning can finish +- **Better fix:** TODO (we're working on it) -1. **Ephemeral Key Generation:** The host and token each generate an ephemeral ECDH keypair. -2. **Signed Key Exchange:** They exchange their ephemeral public keys, signing them with their persistent private keys from the pairing phase. -3. **Signature Verification:** Each party verifies the signature on the received ephemeral public key using the other's stored persistent public key. -4. **Secure Secret Derivation:** A shared secret is derived using the ECDH algorithm. -5. **Session Key Generation:** A KDF (Key Derivation Function) is used to generate an AES-128 session key from the shared secret. -6. **Channel Verification:** The channel is verified with an encrypted ping-pong exchange. +**TL;DR:** Do provisioning FIRST, THEN WiFi is okay. -### Phase 3: Integrity Verification & Runtime Guard +### Problem #1.5: Garbage Output During Provisioning (The Real Issue) +- **What you see:** Corrupted binary data mixed with partial debug messages +- **Example:** +``` +�YVOS����fCOO�f=p$ +*Sent T2H_ECDH_SHARE (host-initiated ECDH) +J��y�����)ۘ%�EU +``` +- **Why it happens:** Serial ISR receiving data while main protocol handler is processing +- **Root cause:** No proper frame synchronization - data gets interleaved +- **This means:** Protocol state machine is running during USB ISR, causing data corruption +- **Impact:** ECDH exchange fails, provisioning breaks completely -This phase ensures the host is running the correct software before allowing it to boot. +**TL;DR:** Serial + WiFi task switching breaks the protocol. Need to disable WiFi polling DURING provisioning. -Integrity Attestation +### Problem #2: macOS Hates Unplugging It +- **What happens:** Unplug/replug a few times → macOS loses the device completely +- **Why:** macOS USB driver gets confused (not our problem but we suffer) +- **How to fix it:** Restart your Mac (bruh) +- **Pro tip:** Just leave it plugged in. Works fine if you don't touch it. -1. **Integrity Challenge:** The token sends a random nonce to the host. -2. **Hash Calculation:** The host calculates a hash of its current boot file. -3. **Signed Response:** The host signs the hash and the nonce with its persistent private key and sends the signature and hash to the token. -4. **Verification:** The token verifies the signature and compares the received hash with the stored "golden hash". -5. **Boot Signal:** If the verification is successful, the token sends a `T2H_BOOT_OK` signal to the host; otherwise, it sends `T2H_INTEGRITY_FAIL_HALT`. +**TL;DR:** Regular Pico 2 doesn't have this. WiFi chip adds drama. -### Runtime Heartbeat +### Problem #3: API Info Crashes +- **What it tried to do:** Read temperature sensor +- **What actually happened:** Pico went to another dimension 🌀 +- **Status:** Removed from API +- **Can we bring it back?** Maybe, if we rewrite it -After a successful boot, the host sends periodic heartbeat messages to the token to maintain the session. +**TL;DR:** Just don't ask for it. -### Shutdown Policy +## 🔨 Build It -The system will shut down under the following conditions: +```bash +cd build +cmake .. +make -j4 +picotool load pico_project_template.uf2 -u -f +``` -* A protocol phase is not completed within 30 seconds. -* Either the host or token sends a "no-go" signal. -* The `T2H_BOOT_OK` signal is not received within 2 minutes of starting the attestation process. -* The heartbeat timeout occurs more than 3 times. +Standard procedure. Nothing fancy. -## Building and Running the Project +## 🧪 Test It -### Prerequisites +### Test via USB Serial +```bash +screen /dev/tty.usbmodem* 115200 +# See debug output, watch provisioning happen +``` -* Raspberry Pi Pico SDK -* CMake (version 3.13 or later) -* ARM GCC Compiler +### Test via WiFi +Connect to `MASTR-Token` WiFi, then: -### Build Instructions +```bash +# Quick check - is it alive? +curl http://192.168.4.1/api/ping -1. **Create a build directory:** +# Check provisioning status +curl http://192.168.4.1/api/status - ```bash - mkdir build - cd build - ``` +# Open web interface +open http://192.168.4.1 +``` -2. **Configure for your board:** +## � Stats - * **For Raspberry Pi Pico (RP2040):** +- **Firmware Size:** 463 KB (still fits in 4 MB) +- **RAM Used:** 138 KB (we got room) +- **Bugs:** Still has some but we shipped it anyway - ```bash - cmake .. -DPICO_BOARD=pico - ``` +## 🎯 What's Next - * **For Raspberry Pi Pico W (RP2040 with WiFi):** +1. **FIX: Disable WiFi polling during provisioning** + - Add `provisioning_active` flag to protocol_state + - Set to `true` at boot, `false` when reaching state 0x40 + - Modify `wifi_background_task()` to skip `cyw43_arch_poll()` if provisioning_active + - This prevents task switching during ECDH key exchange - ```bash - cmake .. -DPICO_BOARD=picow - ``` +2. **Test full flow:** Provision → then WiFi polling starts → both work together - * **For a generic RP2350 board:** +3. **Maybe fix `/api/info`** → Temperature reading without crashing - ```bash - cmake .. -DPICO_PLATFORM=rp2350 - ``` +4. **macOS USB drama** → Not much we can do, macOS issue -3. **Build the project:** +--- - ```bash - make - ``` - -### Running - -1. Connect your Pico board to your computer while holding the `BOOTSEL` button. -2. Drag and drop the `mastr.uf2` file from the `build` directory onto the `RPI-RP2` mass storage device. - -## Testing - -The project uses the Unity test framework. The tests are located in the `test` directory. - -### Running the Tests - -1. Navigate to the build directory: `cd build` -2. Build the test runner: `make test_runner` -3. Run the tests: `ctest` +Made on a Pico 2 W. Works most of the time. That's a win in my book. ✌️ \ No newline at end of file diff --git a/docs/ap_http_architecture.md b/docs/ap_http_architecture.md new file mode 100644 index 0000000..2bb3e59 --- /dev/null +++ b/docs/ap_http_architecture.md @@ -0,0 +1,220 @@ +# Access Point & HTTP API Architecture + +This document explains how the Wi‑Fi Access Point (AP) and embedded HTTP API stack are initialized, how requests flow through the system, and the design decisions taken to improve stability (including the recent changes you requested). + +--- +## High-Level Overview + +Component | Responsibility +--------- | -------------- +`wifi_ap.c` | Holds persistent AP configuration, starts/stops/reconfigures AP, background tasks. +`ap_manager.c` | Lower-level AP credential (re)configuration helper (e.g. `reconfigure_access_point`). +`http_server.c` | Minimal single-connection HTTP server built on lwIP `tcp_*` APIs. +`api.c` | Registers REST-style endpoints and implements each handler. +`cpu_monitor.c` | Runtime CPU percentage calculation (used by `/api/cpu`). + +Boot sequence (simplified): +1. `main.c` calls `wifi_ap_init()` (lightweight marker, not full CYW43 init). +2. Scheduler starts; `wifi_ap_init_task` runs and calls `wifi_ap_start()` to bring up AP (initially OPEN). +3. `ap_manager` initializes AP stack and calls `http_server_init()`. +4. `api_register_routes()` registers all endpoint handlers. +5. `wifi_background_task` wakes periodically to allow lwIP + Wi‑Fi driver progress. +6. Client connects to AP and issues HTTP requests (curl or browser). Handlers respond and connection closes. + +--- +## AP Lifecycle & Configuration + +### Persistent Password Storage +- `wifi_pass_storage[65]` holds the active passphrase (or empty string for OPEN). This avoids dangling pointers when the password changes. +- `wifi_config.password` always points to this storage. + +### Start +`wifi_ap_start(const wifi_ap_config_t *config)` deep-copies the password, sets IP (192.168.4.1), and calls `start_access_point()`. + +### Rotate Password +`wifi_ap_rotate_password(new_pass)` updates the persistent buffer then calls `reconfigure_access_point()` (no full teardown). If reconfigure fails: +- Falls back to OPEN (empty password) for recovery. + +### Stop +`wifi_ap_stop()` invokes `stop_access_point()` and marks `is_running=false`. + +### Claim Flow +- `/api/claim` (in `api.c`) generates a random passphrase (currently 16 characters from [A–Za–z0–9]). +- Sets `g_claimed=true` and starts a one-shot timer (grace period ~750 ms) before applying new credentials via `wifi_ap_rotate_password()`. +- Clients receive JSON with new password and reconnect before AP reconfigures. + +--- +## Tasks & Priorities (FreeRTOS) + +Task | Priority (approx) | Purpose +---- | ------------------ | ------- +Serial / Protocol | High (MAX-6) | Handles secure protocol, crypto events. +Watchdog | High (MAX-5) | Monitors session timeout, triggers re-attestation. +WiFi Background (`wifi_background_task`) | High (MAX-7) | Allows lwIP + CYW43 driver housekeeping every 50 ms. +HTTP Server Task (`http_server_task`) | Low (~5) | Currently passive (monitoring); core HTTP I/O is interrupt/callback-driven. +AP Init Task (`wifi_ap_init_task`) | Low (~5) | One-shot start of AP after scheduler boot. + +Notes: +- The HTTP server does not require a dedicated worker for each connection; lwIP invokes recv callbacks in the context of its stack processing. +- The background Wi‑Fi task is critical—without its periodic delay loop, driver events and DHCP timeouts can stall, causing intermittent API failures. + +--- +## HTTP Server Design (`http_server.c`) + +### Rationale +The stock lwIP raw TCP interface is used for a tiny, predictable footprint. Earlier instability was traced to closing the TCP connection immediately after queuing writes, occasionally producing client-side errors (curl: Recv failure / connection reset). The updated server defers closure until the data is ACKed. + +### Core Structures +``` +struct route_entry { const char *path; http_handler_fn handler; }; +static route_entry routes[MAX_ROUTES]; +static http_state_t g_state; // Single connection state +``` +- Single active connection slot to minimize RAM: one `request[1024]` buffer. +- Additional connections are politely closed (not aborted) while busy. + +### Flow +1. Accept: `http_accept` -> if free, mark `g_state.in_use=true`, install callbacks. +2. Receive: `http_recv` accumulates data until `\r\n\r\n` (end of headers). +3. Dispatch: `handle_request` matches path against `routes[]` and invokes handler. +4. Respond: Handler calls `http_send_json()` -> `send_response()` writes header/body. +5. Close: `tcp_sent` callback triggers `http_close()` once ACKed. + +### Stability Improvements +Change | Benefit +------ | ------- +Deferred close (wait for ACK) | Avoids race where client sees truncated or reset connection. +Polite busy handling (close vs abort) | Prevents RST storms under rapid curls. +Removed multi-connection experimental code | Simpler state, fewer edge cases. +Minimal per-request parsing | Keeps latency and RAM usage low. + +### Adding Endpoints +``` +void api_register_routes(void) { + http_register("/api/ping", ping_handler); + http_register("/api/health", health_handler); + http_register("/api/status", status_handler); + // ... etc ... +} +``` +1. Implement `static void new_handler(struct tcp_pcb *pcb, const char *request)` in `api.c`. +2. Register with `http_register("/api/your_path", new_handler);`. +3. Use `http_send_json(pcb, status_code, json_string);` to respond. + +--- +## API Layer (`api.c`) + +Endpoint | Purpose | Key Operations +-------- | ------- | -------------- +`/api/health` | Connectivity probe | Returns `{"ok":true}` quickly. +`/api/ping` | Simple RTT test | Returns `{"message":"pong"}`. +`/api/status` | Device + protocol state | Uptime, provisioning, claim flag. +`/api/network` | AP + DHCP lease info | Enumerates connected MAC/IP pairs. +`/api/ram` | Heap usage snapshot | Total/used/free, percent. +`/api/cpu` | CPU utilization | Uses `cpu_monitor` runtime stats. +`/api/temp` | Internal MCU temp | Averaged ADC samples -> °C. +`/api/claim` | Provisioning password set | Random PSK generation + deferred rotation. + +Handler pattern: +1. (Optional) gather metrics/state. +2. Format JSON with `snprintf` into a stack buffer. +3. Call `http_send_json()`. + +### Claim Timer Mechanism +- Creates a one-shot FreeRTOS timer (`xTimerCreate`) after responding. +- Timer callback spawns `ap_restart_task` (worker task) to perform password rotation after a grace delay (~750 ms). This avoids resetting the AP before the HTTP response leaves the stack. + +--- +## CPU & Temperature Metrics + +Metric | Source | Notes +------ | ------ | ----- +CPU% | `cpu_get_percent()` | Runtime stats only; idle task accounted via FreeRTOS. +Temp | Pico ADC (channel 4) | First sample discarded; 8-sample average; formula 27°C at 0.706V. +RAM | FreeRTOS heap API | `xPortGetFreeHeapSize()` vs `configTOTAL_HEAP_SIZE`. + +--- +## Error Handling & Fallbacks +Scenario | Behavior +-------- | -------- +Failed AP start | Logs error, returns false; higher layer may retry. +Failed password rotate | Falls back to OPEN mode to keep access path alive. +Route not found | Returns 404 JSON consistently. +TCP write failure | Aborts connection to prevent partial/inconsistent response. +Busy server (already serving) | Closes new connection gracefully (no RST). + +--- +## Known Limitations / Future Enhancements +Item | Description | Possible Improvement +---- | ----------- | ------------------- +Single connection slot | Sequential request handling only | Add small pool (2–4) if concurrency needed. +No rate limiting | High-frequency curls may monopolize slot | Lightweight token bucket per handler. +Static JSON formatting | Manual `snprintf` | Consider tiny JSON builder for safety/escaping. +No persistence of claim flag | Lost on reboot | Store claimed state in flash/OTP. +Open AP initial state | Convenience for provisioning | Optionally advertise via captive portal & then lock down. + +--- +## Quick Reference Cheat Sheet +Action | How +------ | ---- +Add endpoint | Implement handler in `api.c`, register in `api_register_routes()`. +Get AP config | `wifi_ap_get_config()->ssid / password`. +Rotate password | `wifi_ap_rotate_password(new_psk)`. +Check claimed state | `g_claimed` (internal to `api.c`). +Probe health | curl `/api/health`. +Restart AP manually | `wifi_ap_rotate_password(current_password)` (no change) or add stop/start wrappers. + +--- +## Minimal Curl Examples +``` +# Connectivity +curl -v http://192.168.4.1/api/health + +# Status +curl -s http://192.168.4.1/api/status | jq + +# Claim flow +curl -s http://192.168.4.1/api/claim | jq # then reconnect with returned password after grace period + +# Metrics +curl -s http://192.168.4.1/api/cpu +curl -s http://192.168.4.1/api/temp +``` + +--- +## Stability Summary of Recent Changes +Change | Problem Addressed +------ | ----------------- +Deferred close via `tcp_sent` | Eliminated intermittent client-side RST during short responses. +Polite busy handling | Prevented abrupt resets under rapid successive curls. +Health endpoint | Provided ultra-light probe to separate transport vs handler latency issues. +Password rotation timer | Ensured `/api/claim` response is delivered before AP reconfiguration. +Runtime-only CPU monitoring | Removed earlier idle tick heuristic that produced 0% or erratic values. + +--- +## Troubleshooting Tips +Symptom | Check | Action +------- | ----- | ------ +Curl sporadic failures | Is `/api/health` stable? | If yes, inspect specific heavy endpoints (ADC, DHCP). Rate-limit them. +Claim response received but reconnection fails | Was grace timer executed? | Verify timer creation and worker task ran; increase grace period. +CPU always 0% | Is `cpu_monitor` initialized? | Ensure runtime stats scaling function compiled and scheduler running. +No clients listed | DHCP leases empty? | Confirm device associated; check Wi‑Fi channel/interference. + +--- +## Glossary +Term | Meaning +---- | ------- +AP | Access Point broadcasting SSID for provisioning. +Grace Period | Delay after claim before applying new password. +lwIP | Lightweight IP stack used for TCP/UDP. +CYW43 | Wi‑Fi chip/driver (Raspberry Pi Pico W). +Idle Task | FreeRTOS lowest-priority task measuring unused CPU time. + +--- +## Revision History +Date | Change | Author +---- | ------ | ------ +2025-11-09 | Initial architecture doc capturing AP + HTTP design and stability fixes | Generated assistant + +--- +End of document. diff --git a/docs/provisioning_flow.md b/docs/provisioning_flow.md new file mode 100644 index 0000000..4dfb62e --- /dev/null +++ b/docs/provisioning_flow.md @@ -0,0 +1,258 @@ +# One-time Host–Token Pairing (Provisioning) Plan + +This document explains the goal Lucas is aiming for and outlines the concrete API and UI flow to move the first‑run provisioning from debug prints to the HTTP API + web UI. + +--- +## Goal in plain terms +- On first boot, the Token (device) and a Host (PC/server) must pair exactly once. +- Today, this pairing uses debug console messages to exchange the Token public key and a "golden hash". The ask is to make this happen via API endpoints and a simple UI wizard so the user can copy/paste the necessary data. +- After pairing, the device should remember the Host and the golden hash, and normal operation begins; subsequent boots should not require pairing again unless reset. + +--- +## How it works today (CURRENT STATE) + +Mechanism today is console/serial driven: +1. Device boots and prints debug messages (via `print_dbg`) exposing: public key, intermediate protocol states, and the "golden hash". +2. A host Python script (or manual copy in a terminal) reads those debug lines, parses out required values. +3. Host responds (over serial protocol frames) with its public key and expected golden hash. +4. Device stores these in RAM (not persisted) and transitions the internal `protocol_state.current_state` to runtime (0x40). + +Limitations: +- Manual copy/paste, easy to mis-transcribe. +- No structured validation feedback (just debug prints). +- Values are ephemeral; reboot requires repeating unless separately saved. +- Harder to script remotely (must attach to USB serial). +- No UI representation; provisioning progress opaque unless watching terminal. + +Summary: Pairing is implicit, using the existing framed serial protocol and debug lines, not an explicit transactional API. + +--- +## Target architecture (AFTER CHANGES) + +Replace ad‑hoc serial exchange with a REST + UI wizard: +1. Device starts OPEN AP (no password) and advertises provisioning endpoints. +2. Browser hits `/api/provision/state` to see if `provisioned=false`. +3. User opens "Provisioning" page: UI calls `/api/provision/token_info` and displays token public key, optional attestation blob, and (optionally) device-generated golden hash. +4. User (or host software) supplies Host Public Key + Golden Hash via POST `/api/provision/host_submit`. +5. UI shows submitted values for confirmation; user presses "Confirm" -> POST `/api/provision/confirm`. +6. Device persists host key + golden hash + provisioned flag in flash; subsequent `/api/provision/state` returns `provisioned=true` and endpoints are locked. +7. Runtime heartbeat uses `/api/heartbeat` or existing `/api/status`. + +Benefits: +- Predictable JSON contracts; easy to automate. +- One-time explicit confirmation step. +- Copy/paste friendly UI (no terminal required). +- Persistence ensures reboot resilience. +- Clear error codes (400, 409) instead of generic debug lines. + +--- +## Delta: What must change + +Area | Current | Required Change +---- | ------- | --------------- +Public key exposure | Printed in debug | Serve via `/api/provision/token_info` JSON +Golden hash exchange | Printed / serial frame | Submit via `/api/provision/host_submit` body +Provisioning state | Implicit in protocol_state | Explicit `/api/provision/state` (provisioned + step) +Confirmation | None (implicit) | `/api/provision/confirm` finalizes & persists +Persistence | RAM only | Flash/EEPROM sector with CRC (host key + hash + flag) +UI | Debug terminal | Web wizard (three steps) in `index.html` +Security gating | N/A | Disable provisioning endpoints after success; require factory reset to re-enable +Attestation | Mixed debug prints | Optional structured field (`attestation.report`, `nonce`) +Errors | Freeform prints | HTTP status + JSON `{ "error": "..." }` + +--- +## Minimal state machine (new) + +State | Description | Transitions +----- | ----------- | ---------- +`start` | Device unprovisioned, waiting for user | -> `token_info` on first GET +`token_info` | Token info served | -> `await_host` after host submits data +`await_host` | Host data stored, waiting confirmation | -> `done` on confirm +`done` | Provisioned complete | (locked) unless factory reset + +Implementation hint: Store `current_provision_step` in RAM; persist only the final `provisioned` flag and artifacts to flash. + +--- +## Persistence strategy (flash layout example) + +| Offset | Length | Field | +| ------ | ------ | ----- | +| 0x00 | 4 | Magic (0x504B5450 'PKTP') | +| 0x04 | 2 | Version | +| 0x06 | 2 | Host key length | +| 0x08 | N | Host public key PEM (N bytes) | +| 0x08+N | 2 | Golden hash length | +| ... | M | Golden hash bytes (hex or raw) | +| ... | 1 | Provisioned flag (0x01) | +| ... | 4 | CRC32 over all preceding fields | + +Failure handling: +- On CRC mismatch -> treat as unprovisioned. +- On flash write failure -> return 500 from `/api/provision/confirm`. + +--- +## Validation rules (to implement) +- Host public key PEM: begins with `-----BEGIN PUBLIC KEY-----`, length within bounds (e.g. 512–2048 bytes). +- Golden hash: hex (even length) or base64; server normalizes to hex internally. +- Reject duplicate provisioning (respond 409 if `provisioned=true`). +- Attestation (if used): nonce length fixed (e.g. 16 bytes), report base64 size check. + +--- +## Migration plan (step-by-step) +1. Add in-memory scaffolding for new endpoints (no persistence yet) returning mock values. +2. Integrate real token public key extraction (from crypto subsystem) into `/api/provision/token_info`. +3. Implement host submission parsing & validation. +4. Implement flash persistence layer (read/write + CRC) and finalize confirm step. +5. Extend `index.html` with provisioning wizard section; hide existing metrics until provisioned. +6. Gate existing sensitive endpoints (if any) while `!provisioned` (optional). +7. Add factory reset mechanism (e.g., long button press or `/api/provision/reset` with physical presence check). +8. Remove or minimize debug prints of sensitive material (replace with informational logs only). + +--- +## Risks & Mitigations +Risk | Mitigation +---- | ---------- +Power loss during flash write | Write to temp sector then atomic flag set; verify CRC on boot. +Host submits malformed PEM | Strict parser + length limits + 400 response. +Replay / MITM on open AP | Physical proximity assumption + optional attestation challenge. +Accidental re-provision | Require factory reset or signed admin token. + +--- +## Token vs Host Responsibilities (recap) +Token: Serve provisioning endpoints, validate inputs, persist artifacts, enforce golden hash. +Host: Fetch token info, verify/record, POST host key + golden hash, confirm. + +--- +## Actors and artifacts +- Token (this device) + - Token Public Key (from secure element ATECC608A or internal key store) + - Attestation proof/evidence (optional but recommended) + - Golden Hash (reference hash of firmware/config or key material agreed upon with Host) +- Host (your PC/app) + - Host Public Key + - Expected Golden Hash + +Persistent on Token after pairing: +- host_public_key +- golden_hash +- provisioned flag (true) + +--- +## Trust model (high-level) +- First‑run pairing happens over the Token’s local AP; the user is physically present. +- The Token shows its identity (public key + optional attestation) so the Host can verify. +- The Host provides its public key and the golden hash the Token must enforce at runtime. +- Token stores both and marks itself provisioned. + +--- +## Proposed API surface (one-time provisioning) +All endpoints are unauthenticated only when not yet provisioned; once provisioned, they are either disabled or require auth. + +1) GET `/api/provision/state` +- Purpose: Tell the UI if pairing is needed and what step we’re at. +- Response: +``` +{ + "provisioned": false, + "step": "start" | "token_info" | "await_host" | "done" +} +``` + +2) GET `/api/provision/token_info` +- Purpose: Provide copy/pasteable Token identity to the user/Host. +- Response: +``` +{ + "token_pubkey_pem": "-----BEGIN PUBLIC KEY-----...", + "attestation": { "nonce": "...", "report": "base64..." }, + "golden_hash": "" +} +``` +- Notes: `golden_hash` can be the device’s computed reference hash OR left blank if the Host is the source of truth. + +3) POST `/api/provision/host_submit` +- Purpose: Host submits its identity and the expected golden hash. +- Body: +``` +{ + "host_pubkey_pem": "-----BEGIN PUBLIC KEY-----...", + "golden_hash": "" +} +``` +- Response: `{ "status": "ok" }` (or errors if invalid format/length) + +4) POST `/api/provision/confirm` +- Purpose: Finalize pairing. The Token verifies inputs, persists them, and marks provisioned. +- Body: `{ "confirm": true }` +- Response: `{ "status": "ok", "provisioned": true }` + +5) GET `/api/heartbeat` (optional) +- Purpose: UI runtime heartbeat after pairing. Returns a minimal JSON to show live status. +- Response: `{ "ok": true, "uptime_s": , "state": "0x40" }` + +--- +## UI wizard sketch (one page, three panels) +1) "Token identity" (auto-filled via GET `/api/provision/token_info`) + - Show Token Public Key (PEM textarea) + - Show Attestation blob/nonce (optional) + - Show/Copy the Token’s Golden Hash (if device-generated) + - Button: Copy to clipboard + +2) "Host details" + - Textarea: Paste Host Public Key (PEM) + - Input: Golden Hash (hex/base64) – if Host-sourced + - Button: Submit (POST `/api/provision/host_submit`) + +3) "Confirm" + - Button: Confirm (POST `/api/provision/confirm`) + - On success, show: "Provisioned ✓" and disable the wizard + +Quick safety notes: +- Disable these routes (or require auth) after `provisioned == true`. +- If you support a factory reset, re-enable the routes when reset occurs. + +--- +## Persistence & storage +- Store `host_pubkey` and `golden_hash` in non-volatile memory (flash/EEPROM or secure element slots, if available). +- Store a `provisioned` flag. +- On boot, read these; set `protocol_state.current_state = 0x40` only when provisioned and verified. + +--- +## Heartbeat on the UI +- Heartbeat messages don’t need to be the raw serial logs. The UI can simply poll `/api/heartbeat` (or `/api/status`) every few seconds and display a green dot, uptime, and current state. +- This gives users confidence during and after provisioning without exposing serial debug transports. + +--- +## Error modes & validations +- Invalid PEM formatting → 400 with message +- Golden hash wrong length/encoding → 400 +- Already provisioned → 409 (conflict) +- Persistence I/O error → 500 with reason + +--- +## Security considerations +- Lock provisioning endpoints after success, or require a temporary claim token/password. +- Bind Host public key tightly to the device once set; require explicit reset to change it. +- Consider including an attestation challenge/response to defend against token cloning. + +--- +## Minimal contracts (inputs/outputs) +Inputs: +- Host public key (PEM) +- Golden hash (hex/base64) +Outputs: +- Token public key (PEM) +- Optional attestation report +- Provisioned state boolean +Success criteria: +- Device stores host key + golden hash and reports `provisioned=true`. +- Subsequent boots treat device as paired and hide provisioning UI. + +--- +## Next steps to implement +1) Add new endpoints in `api.c` (state, token_info, host_submit, confirm, heartbeat). +2) Add simple persistence layer (flash sector) with CRC. +3) Build UI wizard in `index.html` to call the new endpoints. +4) Gate endpoints by `!provisioned` and add a factory reset path. + +(We can implement these in small PRs. Let me know and I’ll start with the endpoints + dummy persistence, then wire the UI.) diff --git a/include/FreeRTOSConfig.h b/include/FreeRTOSConfig.h index ee1cf3b..ea4742e 100644 --- a/include/FreeRTOSConfig.h +++ b/include/FreeRTOSConfig.h @@ -43,7 +43,7 @@ /* Scheduler Related */ #define configUSE_PREEMPTION 1 #define configUSE_TICKLESS_IDLE 0 -#define configUSE_IDLE_HOOK 0 +#define configUSE_IDLE_HOOK 1 #define configUSE_TICK_HOOK 0 #define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) #define configMAX_PRIORITIES 32 @@ -76,16 +76,30 @@ #define configAPPLICATION_ALLOCATED_HEAP 0 /* Hook function related definitions. */ -#define configCHECK_FOR_STACK_OVERFLOW 0 -#define configUSE_MALLOC_FAILED_HOOK 0 +#define configCHECK_FOR_STACK_OVERFLOW 2 +#define configUSE_MALLOC_FAILED_HOOK 1 #define configUSE_DAEMON_TASK_STARTUP_HOOK 0 #define configUSE_PASSIVE_IDLE_HOOK 0 /* Run time and task stats gathering related definitions. */ -#define configGENERATE_RUN_TIME_STATS 0 +#define configGENERATE_RUN_TIME_STATS 1 #define configUSE_TRACE_FACILITY 1 #define configUSE_STATS_FORMATTING_FUNCTIONS 0 +/* Runtime counter for stats - use hardware microsecond timer for accuracy */ +/* Prefer Pico SDK time header, fall back to extern if not available to parser */ +#ifndef __has_include +#define __has_include(x) 0 +#endif +#include +#ifdef __cplusplus +extern "C" uint32_t freertos_runtime_counter_get(void); +#else +extern uint32_t freertos_runtime_counter_get(void); +#endif +#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ((void)0) +#define portGET_RUN_TIME_COUNTER_VALUE() (freertos_runtime_counter_get()) + /* Co-routine related definitions. */ #define configUSE_CO_ROUTINES 0 #define configMAX_CO_ROUTINE_PRIORITIES 1 @@ -154,6 +168,7 @@ to exclude the API function. */ #define INCLUDE_xTaskGetIdleTaskHandle 1 #define INCLUDE_eTaskGetState 1 #define INCLUDE_xTimerPendFunctionCall 1 + #define INCLUDE_xTaskAbortDelay 1 #define INCLUDE_xTaskGetHandle 1 #define INCLUDE_xTaskResumeFromISR 1 diff --git a/include/cpu_monitor.h b/include/cpu_monitor.h new file mode 100644 index 0000000..46e1da4 --- /dev/null +++ b/include/cpu_monitor.h @@ -0,0 +1,16 @@ +#pragma once +#include +#ifdef __cplusplus +extern "C" { +#endif + +// Tick-based idle counter used by the simple CPU endpoint path +extern volatile uint32_t g_idleTicks; + +// Returns CPU utilization percent over the interval since the last call. +// Uses FreeRTOS run-time stats when available; otherwise may return 0. +uint32_t cpu_get_percent(void); + +#ifdef __cplusplus +} +#endif diff --git a/include/crypt.h b/include/crypt.h index 2f1d128..bd5f41f 100644 --- a/include/crypt.h +++ b/include/crypt.h @@ -30,6 +30,13 @@ */ bool crypt_init(void); +/** + * @brief Check if token is provisioned (has valid host pubkey) + * + * Provisioning is complete when the host's permanent public key has been written to Slot 8. + * + */ + /** * @brief Encrypt a frame payload using AES-128-GCM * @@ -164,6 +171,31 @@ bool ecdh_sign_with_permanent_key(const uint8_t* message, size_t message_len, */ bool ecdh_read_host_pubkey(uint8_t* host_pubkey_out); +/** + * @brief Store host's permanent public key into ATECC608A Slot 8 + * + * Writes the 64-byte P-256 public key (X||Y) into slot 8 using two + * 32-byte block writes (block 0 and block 1). This centralizes the + * storage logic so both the serial protocol path and HTTP API path + * can call the same function. + * + * Layout note (slot 8): + * - Block 0..1: host public key (64 bytes) + * - Block 2: golden hash (32 bytes) + * + * @param host_pubkey Pointer to 64-byte buffer containing X||Y + * @return true on success, false otherwise + */ +bool crypto_set_host_pubkey(const uint8_t* host_pubkey); + +// Token permanent public key prefetch/cache API +// Spawn background task (idempotent) to prefetch token permanent public key +void crypt_spawn_pubkey_prefetch(void); +// Retrieve cached hex (128 chars + NUL). Returns true if ready; sets *ready_out if provided. +bool crypt_get_cached_token_pubkey_hex(const char **hex_out, bool *ready_out); +// Returns true if prefetch permanently failed +bool crypt_token_pubkey_failed(void); + /** * @brief Verify signature using host's permanent public key * @@ -254,4 +286,82 @@ bool crypto_get_golden_hash(uint8_t* p_result); */ bool crypto_set_golden_hash(uint8_t* p_hash); +/** + * Convert hex string to bytes + * @param hex_str Null-terminated hex string (e.g., "deadbeef") + * @param out_bytes Output buffer for bytes + * @param max_bytes Maximum number of bytes to write to out_bytes + * @return Number of bytes converted, or -1 on error + */ +int crypto_hex_to_bytes(const char* hex_str, uint8_t* out_bytes, size_t max_bytes); + +/** + * Set host public key from hex string + * @param hex_pubkey 128-character hex string representing 64 bytes + * @return true if successful, false otherwise + */ +bool crypto_set_host_pubkey_hex(const char* hex_pubkey); + +// ============================================================================ +// Non-blocking host pubkey management API +// ============================================================================ + +/** + * Spawns the background task for host pubkey operations. + * Call once during system initialization. + */ +void crypt_spawn_host_pubkey_task(void); + +/** + * Gets the cached host public key in hex format (non-blocking). + * + * @param hex_out Pointer to receive hex string (128 chars + null terminator) + * @param ready_out Pointer to receive ready status (true if pubkey is cached) + * @param failed_out Pointer to receive failure status (true if read failed) + * @return true if pubkey is ready and cached, false otherwise + */ +bool crypt_get_cached_host_pubkey_hex(const char **hex_out, bool *ready_out, bool *failed_out); + +/** + * Requests a host public key write operation (non-blocking). + * The actual write happens in background task. + * + * @param hex_pubkey 128-character hex string to write + * @param write_ready_out Pointer to receive write completion status (optional) + * @param write_failed_out Pointer to receive write failure status (optional) + * @return true if write request was accepted, false if invalid or already pending + */ +bool crypt_request_host_pubkey_write(const char *hex_pubkey, bool *write_ready_out, bool *write_failed_out); + +/** + * Gets the status of the last host pubkey write operation (non-blocking). + * + * @param write_ready_out Pointer to receive write completion status + * @param write_failed_out Pointer to receive write failure status + * @return true if write completed successfully, false otherwise + */ +bool crypt_get_host_pubkey_write_status(bool *write_ready_out, bool *write_failed_out); + +/** + * Spawn the golden hash background task (one-time initialization) + * Safe to call multiple times - only creates task on first call + */ +void crypt_spawn_golden_hash_task(void); + +/** + * Queue a golden hash write operation (non-blocking) + * @param golden_hash 32-byte golden hash to write + * @return true if operation queued, false if busy + */ +bool crypt_spawn_golden_hash_task_with_data(const uint8_t* golden_hash); + +/** + * Get golden hash write operation status (non-blocking) + * @param write_ready_out Pointer to receive write completion status + * @param write_failed_out Pointer to receive write failure status + * @param golden_hash_out Pointer to receive verified golden hash (32 bytes, if ready) + * @return true if write completed successfully, false otherwise + */ +bool crypt_get_golden_hash_write_status(bool *write_ready_out, bool *write_failed_out, uint8_t *golden_hash_out); + #endif // CRYPT_H diff --git a/index.html b/index.html new file mode 100644 index 0000000..a7cf89c --- /dev/null +++ b/index.html @@ -0,0 +1,1414 @@ + + + + + + MASTR Dashboard + + + + +
+

🔐 MASTR Dashboard

+

System Monitoring & Device Status

+
+ +
+ +
+
Memory
+
+
+
--
+
RAM Usage
+
+
+
+ +
+
CPU Usage
+
+
+
--
+
CPU Percent
+
+
+
+ +
+
Uptime
+
+
+
--
+
Seconds
+
+
+
+ +
+
Temperature
+
+
+
--
+
Celsius
+
+
+
+ + +
+
System Status
+
+
+
+ + Checking... +
+
+ + Wi-Fi Status +
+
+ + Network Status +
+
+
+ +
+
+
+ + +
+
Network Information
+
+
+ SSID: -- +
+
+ Security: -- +
+
+ AP IP: -- +
+
+ Connected Clients: 0 +
+
+
+ + +
+
Wi-Fi Configuration
+
+
+ Configured: -- +
+ + +
+
+ + +
+
Device Information
+
+
+ State: -- +
+
+ IP Address: -- +
+
+ RAM Usage: -- KB / -- KB +
+
+
+ + +
+
Connected Clients
+
+
+ No connected clients +
+
+
+ + +
+
Actions
+
+ + +
+
+
+ +
+ + + + + + + diff --git a/src/cpu_monitor.c b/src/cpu_monitor.c new file mode 100644 index 0000000..a5d5640 --- /dev/null +++ b/src/cpu_monitor.c @@ -0,0 +1,118 @@ +/** + * CPU Utilization Monitoring + * + * Tracks idle task execution time to calculate real CPU utilization. + * Uses FreeRTOS idle hook to increment idle tick counter. + */ + +#include +#include +#include "FreeRTOS.h" +#include "task.h" +#include "cpu_monitor.h" +#include +#include +#include +#include "pico/time.h" +#include "serial.h" +// Forward declaration for cpu_get_percent (optional header could be added later) + + +// Tick-based idle accounting reinstated for fallback stability. +volatile uint32_t g_idleTicks = 0; +void vApplicationIdleHook(void) { + static uint32_t lastTick = 0; + uint32_t now = xTaskGetTickCount(); + if (now != lastTick) { + g_idleTicks++; + lastTick = now; + } +} + +// Return CPU usage percent (integer 0-100) using FreeRTOS run-time stats. +// Accurate path: sum per-task run time counters and derive busy = total - idle. +// Uses a minimum delta window to avoid noisy tiny samples. +uint32_t cpu_get_percent(void) +{ + #define CPU_MON_MAX_TASKS 48 + static TaskStatus_t stats_buf[CPU_MON_MAX_TASKS]; + static uint32_t last_total = 0; + static uint32_t last_idle = 0; + static uint32_t acc_total = 0; + static uint32_t acc_idle = 0; + static uint32_t last_percent = 0; + + // If scheduler not running yet, just return last cached percent. + if (xTaskGetSchedulerState() != taskSCHEDULER_RUNNING) { + return last_percent; + } + + UBaseType_t numTasks = uxTaskGetNumberOfTasks(); + if (numTasks < 2) { // Need at least Idle + one other + return last_percent; + } + + UBaseType_t cap = (numTasks > CPU_MON_MAX_TASKS) ? CPU_MON_MAX_TASKS : numTasks; + uint32_t ignoredTotalTime = 0; + UBaseType_t got = uxTaskGetSystemState(stats_buf, cap, &ignoredTotalTime); + if (got == 0) { + return last_percent; + } + + uint32_t sumAll = 0; + uint32_t sumIdle = 0; + for (UBaseType_t i = 0; i < got; i++) { + sumAll += stats_buf[i].ulRunTimeCounter; + if (stats_buf[i].uxCurrentPriority == 0 && stats_buf[i].pcTaskName) { + const char *nm = stats_buf[i].pcTaskName; + if ((nm[0]=='I' || nm[0]=='i') && (strncmp(nm, "IDLE", 4)==0 || strncmp(nm,"Idle",4)==0 || strncmp(nm,"idle",4)==0)) { + sumIdle += stats_buf[i].ulRunTimeCounter; + } + } + } + + if (last_total == 0) { // establish baseline + last_total = sumAll; + last_idle = sumIdle; + return last_percent; + } + + uint32_t dTotal = sumAll - last_total; + uint32_t dIdle = sumIdle - last_idle; + last_total = sumAll; + last_idle = sumIdle; + + if (dTotal == 0 || dIdle > dTotal) { + return last_percent; + } + + // Accumulate to reach ~100ms window at 10kHz runtime counter (threshold = 1000 ticks) + acc_total += dTotal; + acc_idle += dIdle; + const uint32_t MIN_RT_DELTA = 1000; // ~100ms + if (acc_total < MIN_RT_DELTA) { + return last_percent; // wait for sufficient window + } + + dTotal = acc_total; + dIdle = acc_idle; + acc_total = 0; + acc_idle = 0; + + if (dIdle > dTotal) { + return last_percent; + } + + uint32_t busy = dTotal - dIdle; + uint32_t percent = (busy * 100U + (dTotal / 2U)) / dTotal; // rounded + last_percent = percent; + return percent; +} + +// Provide the runtime counter for FreeRTOS stats without including Pico headers in FreeRTOSConfig.h +uint32_t freertos_runtime_counter_get(void) +{ + // Scale microsecond hardware timer to ~10kHz resolution (100 microseconds per tick) + // Improves stability of short-window measurements and extends wrap period. + return (uint32_t)(time_us_64() / 100ULL); +} diff --git a/src/crypt.c b/src/crypt.c index 969052d..9cbc78f 100644 --- a/src/crypt.c +++ b/src/crypt.c @@ -1,13 +1,14 @@ #include "crypt.h" #include "protocol.h" #include "serial.h" -#include +#include #include - +#include #ifndef UNIT_TEST +#include "FreeRTOS.h" +#include "task.h" #include "pico/stdlib.h" #include "pico/rand.h" -// Use hardware SHA256 on RP2350, mbedtls software SHA256 on RP2040 #ifdef LIB_PICO_SHA256 #include "pico/sha256.h" #endif @@ -26,6 +27,59 @@ bool crypt_init(void) { #endif } +// ============================================================================ +// Token permanent public key prefetch/cache +// ============================================================================ +static char g_token_pubkey_hex[129]; +static volatile bool g_token_pubkey_ready = false; +static volatile bool g_token_pubkey_failed = false; + +// Prefetch task only for non-unit-test builds +#ifndef UNIT_TEST +static void token_pubkey_prefetch_task(void *arg) { + (void)arg; + for (int attempt = 0; attempt < 3 && !g_token_pubkey_ready; attempt++) { + uint8_t raw[64]; + ATCA_STATUS status = atcab_get_pubkey(SLOT_PERMANENT_PRIVKEY, raw); + if (status == ATCA_SUCCESS) { + static const char HEX[] = "0123456789abcdef"; + for (int i = 0; i < 64; i++) { + uint8_t b = raw[i]; + g_token_pubkey_hex[i*2] = HEX[b >> 4]; + g_token_pubkey_hex[i*2 + 1] = HEX[b & 0x0F]; + } + g_token_pubkey_hex[128] = '\0'; + g_token_pubkey_ready = true; + break; + } + vTaskDelay(pdMS_TO_TICKS(50)); + } + if (!g_token_pubkey_ready) { + g_token_pubkey_failed = true; + } + vTaskDelete(NULL); +} + +void crypt_spawn_pubkey_prefetch(void) { + if (!g_token_pubkey_ready && !g_token_pubkey_failed) { + xTaskCreate(token_pubkey_prefetch_task, "pk_prefetch", 768, NULL, tskIDLE_PRIORITY + 1, NULL); + } +} +#else +void crypt_spawn_pubkey_prefetch(void) { + // Mark failed in unit test mode; hardware not available + g_token_pubkey_failed = true; +} +#endif + +bool crypt_get_cached_token_pubkey_hex(const char **hex_out, bool *ready_out) { + if (hex_out) *hex_out = g_token_pubkey_hex; + if (ready_out) *ready_out = g_token_pubkey_ready; + return g_token_pubkey_ready; +} + +bool crypt_token_pubkey_failed(void) { return g_token_pubkey_failed; } + /** * Generates initialization vector for AES-GCM using hardware RNG. * Uses Pico's hardware random number generator for cryptographically secure IVs. @@ -385,6 +439,68 @@ bool ecdh_read_host_pubkey(uint8_t* host_pubkey_out) { #endif } +/** + * Stores 64-byte host permanent public key (X||Y) into ATECC608A Slot 8. + * Performs two 32-byte block writes (block 0 and 1). Does not touch block 2 + * where the golden hash resides. + */ +bool crypto_set_host_pubkey(const uint8_t* host_pubkey) { +#ifndef UNIT_TEST + if (!host_pubkey) return false; + // Write two blocks of 32 bytes + for (int block = 0; block < 2; block++) { + ATCA_STATUS status = atcab_write_zone( + ATCA_ZONE_DATA, + SLOT_HOST_PUBKEY, // slot 8 + block, // block 0 or 1 + 0, // offset + host_pubkey + (block * 32), + 32 + ); + if (status != ATCA_SUCCESS) { + return false; + } + } + return true; +#else + (void)host_pubkey; return false; +#endif +} + +int crypto_hex_to_bytes(const char* hex_str, uint8_t* out_bytes, size_t max_bytes) { + if (!hex_str || !out_bytes) return -1; + + size_t hex_len = strlen(hex_str); + if (hex_len % 2 != 0) return -1; // Must be even number of hex chars + + size_t byte_count = hex_len / 2; + if (byte_count > max_bytes) return -1; // Not enough space in output buffer + + for (size_t i = 0; i < byte_count; i++) { + char hex_pair[3] = {hex_str[i*2], hex_str[i*2+1], '\0'}; + unsigned int byte; + if (sscanf(hex_pair, "%02x", &byte) != 1) { + return -1; // Invalid hex character + } + out_bytes[i] = (uint8_t)byte; + } + + return (int)byte_count; +} + +bool crypto_set_host_pubkey_hex(const char* hex_pubkey) { + if (!hex_pubkey) return false; + + // Expect exactly 128 hex characters (64 bytes) + if (strlen(hex_pubkey) != 128) return false; + + uint8_t host_pubkey[64]; + int bytes_converted = crypto_hex_to_bytes(hex_pubkey, host_pubkey, sizeof(host_pubkey)); + if (bytes_converted != 64) return false; + + return crypto_set_host_pubkey(host_pubkey); +} + /** * Verifies ECDSA signature using ATECC608A hardware verification. * Uses host's permanent public key for verification. @@ -556,4 +672,212 @@ bool crypto_set_golden_hash(uint8_t* p_hash){ return false; } return true; -} \ No newline at end of file +} + +/** + * Checks if the token is provisioned. + * Provisioned means the host's public key exists in the ATECC608A (Slot 8). + * + * @return true if host public key is stored and valid, false otherwise. + */ +bool crypto_is_token_provisioned(void) { + uint8_t host_pubkey[64]; + return ecdh_read_host_pubkey(host_pubkey); +} + +// ============================================================================ +// Host pubkey non-blocking management system +// ============================================================================ +static char g_host_pubkey_hex[129]; +static volatile bool g_host_pubkey_read_ready = false; +static volatile bool g_host_pubkey_read_failed = false; +static volatile bool g_host_pubkey_write_pending = false; +static volatile bool g_host_pubkey_write_ready = false; +static volatile bool g_host_pubkey_write_failed = false; +static char g_pending_host_pubkey_hex[129]; + +// Golden hash operation state (non-blocking) +static uint8_t g_golden_hash_result[32]; +static volatile bool g_golden_hash_write_pending = false; +static volatile bool g_golden_hash_write_ready = false; +static volatile bool g_golden_hash_write_failed = false; +static uint8_t g_pending_golden_hash[32]; + +#ifndef UNIT_TEST +// Background task for host pubkey operations +static void host_pubkey_task(void *arg) { + (void)arg; + + // First, try to read existing host pubkey + uint8_t host_pubkey[64]; + bool read_success = ecdh_read_host_pubkey(host_pubkey); + + if (read_success) { + // Convert to hex + static const char HEX[] = "0123456789abcdef"; + for (int i = 0; i < 64; i++) { + uint8_t b = host_pubkey[i]; + g_host_pubkey_hex[i*2] = HEX[b >> 4]; + g_host_pubkey_hex[i*2 + 1] = HEX[b & 0x0F]; + } + g_host_pubkey_hex[128] = '\0'; + g_host_pubkey_read_ready = true; + } else { + g_host_pubkey_read_failed = true; + } + + // Main loop for write operations + while (1) { + if (g_host_pubkey_write_pending) { + g_host_pubkey_write_pending = false; + g_host_pubkey_write_ready = false; + g_host_pubkey_write_failed = false; + + // Convert hex to bytes + uint8_t new_host_pubkey[64]; + int bytes_converted = crypto_hex_to_bytes(g_pending_host_pubkey_hex, new_host_pubkey, sizeof(new_host_pubkey)); + + if (bytes_converted == 64) { + // Attempt to write (blocking operation safe in background task) + bool write_success = crypto_set_host_pubkey(new_host_pubkey); + if (write_success) { + // Update cache with new value + strcpy(g_host_pubkey_hex, g_pending_host_pubkey_hex); + g_host_pubkey_read_ready = true; + g_host_pubkey_read_failed = false; + g_host_pubkey_write_ready = true; + } else { + g_host_pubkey_write_failed = true; + } + } else { + g_host_pubkey_write_failed = true; + } + } + + vTaskDelay(pdMS_TO_TICKS(10)); + } +} + +void crypt_spawn_host_pubkey_task(void) { + static bool task_spawned = false; + if (!task_spawned) { + xTaskCreate(host_pubkey_task, "hpk_task", 1024, NULL, tskIDLE_PRIORITY + 1, NULL); + task_spawned = true; + } +} +#else +void crypt_spawn_host_pubkey_task(void) { + // No-op in unit test mode +} +#endif + +// Non-blocking host pubkey API functions +bool crypt_get_cached_host_pubkey_hex(const char **hex_out, bool *ready_out, bool *failed_out) { + if (hex_out) *hex_out = g_host_pubkey_hex; + if (ready_out) *ready_out = g_host_pubkey_read_ready; + if (failed_out) *failed_out = g_host_pubkey_read_failed; + return g_host_pubkey_read_ready; +} + +bool crypt_request_host_pubkey_write(const char *hex_pubkey, bool *write_ready_out, bool *write_failed_out) { + if (!hex_pubkey || strlen(hex_pubkey) != 128) { + return false; + } + + if (g_host_pubkey_write_pending) { + // Write already in progress + if (write_ready_out) *write_ready_out = false; + if (write_failed_out) *write_failed_out = false; + return false; + } + + // Copy hex string and start write operation + strcpy(g_pending_host_pubkey_hex, hex_pubkey); + g_host_pubkey_write_pending = true; + g_host_pubkey_write_ready = false; + g_host_pubkey_write_failed = false; + + if (write_ready_out) *write_ready_out = g_host_pubkey_write_ready; + if (write_failed_out) *write_failed_out = g_host_pubkey_write_failed; + return true; +} + +bool crypt_get_host_pubkey_write_status(bool *write_ready_out, bool *write_failed_out) { + if (write_ready_out) *write_ready_out = g_host_pubkey_write_ready; + if (write_failed_out) *write_failed_out = g_host_pubkey_write_failed; + return g_host_pubkey_write_ready; +} + +#ifndef UNIT_TEST +// Background task for golden hash operations (non-blocking) +static void golden_hash_task(void *arg) { + (void)arg; + + while (1) { + if (g_golden_hash_write_pending) { + g_golden_hash_write_pending = false; + g_golden_hash_write_ready = false; + g_golden_hash_write_failed = false; + + // Set golden hash (blocking operation safe in background task) + bool write_success = crypto_set_golden_hash(g_pending_golden_hash); + if (write_success) { + // Verify by reading back + uint8_t verify_hash[32]; + bool read_success = crypto_get_golden_hash(verify_hash); + if (read_success && memcmp(g_pending_golden_hash, verify_hash, 32) == 0) { + // Success - store result + memcpy(g_golden_hash_result, verify_hash, 32); + g_golden_hash_write_ready = true; + } else { + g_golden_hash_write_failed = true; + } + } else { + g_golden_hash_write_failed = true; + } + } + + vTaskDelay(pdMS_TO_TICKS(10)); + } +} + +void crypt_spawn_golden_hash_task(void) { + static bool task_spawned = false; + if (!task_spawned) { + xTaskCreate(golden_hash_task, "gh_task", 1024, NULL, tskIDLE_PRIORITY + 1, NULL); + task_spawned = true; + } +} +#else +void crypt_spawn_golden_hash_task(void) { + // No-op in unit test mode +} +#endif + +// Non-blocking golden hash API functions +bool crypt_spawn_golden_hash_task_with_data(const uint8_t* golden_hash) { + if (!golden_hash) { + return false; + } + + if (g_golden_hash_write_pending) { + return false; // Already busy + } + + // Copy golden hash data and start write operation + memcpy(g_pending_golden_hash, golden_hash, 32); + g_golden_hash_write_pending = true; + g_golden_hash_write_ready = false; + g_golden_hash_write_failed = false; + + return true; +} + +bool crypt_get_golden_hash_write_status(bool *write_ready_out, bool *write_failed_out, uint8_t *golden_hash_out) { + if (write_ready_out) *write_ready_out = g_golden_hash_write_ready; + if (write_failed_out) *write_failed_out = g_golden_hash_write_failed; + if (golden_hash_out && g_golden_hash_write_ready) { + memcpy(golden_hash_out, g_golden_hash_result, 32); + } + return g_golden_hash_write_ready; +} diff --git a/src/hooks.c b/src/hooks.c new file mode 100644 index 0000000..2964e07 --- /dev/null +++ b/src/hooks.c @@ -0,0 +1,17 @@ +#include "FreeRTOS.h" +#include "task.h" + +void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { + // Enhanced handler: try to log the error before halting + (void)xTask; + (void)pcTaskName; + // Note: Cannot safely call print_dbg here as stack may be corrupted + // System must halt to prevent further corruption + for(;;) {} +} + +void vApplicationMallocFailedHook(void) { + // Memory allocation failed - system stability compromised + // Halt system to prevent unpredictable behavior + for(;;) {} +} diff --git a/src/main.c b/src/main.c index a67d82c..253cf94 100644 --- a/src/main.c +++ b/src/main.c @@ -2,6 +2,7 @@ #include #include "pico/stdlib.h" #include "pico/binary_info.h" +#include "pico/time.h" #include "hardware/i2c.h" #include "FreeRTOS.h" #include "task.h" @@ -10,11 +11,14 @@ #include "constants.h" #include "protocol.h" #include "crypt.h" +#include "net/wifi_ap.h" +#include "net/http/http_server.h" +#include "cpu_monitor.h" // Add binary info bi_decl(bi_program_name("MASTR")); bi_decl(bi_program_description("Mutual Attested Secure Token for Robotics")); -bi_decl(bi_program_version_string("0.0.2")); +bi_decl(bi_program_version_string("0.0.3")); void print_board_info() { // All debug output goes through DEBUG_MSG protocol @@ -49,22 +53,103 @@ void serial_task(void *params) { } } -// Watchdog task - monitors session timeout and triggers re-attestation +// Enhanced watchdog task - monitors session timeout, system health, and AP stability void watchdog_task(void *params) { (void)params; // Unused parameter - print_dbg("Watchdog task started\n"); + print_dbg("Enhanced watchdog task started\n"); + + // System health monitoring variables + static uint32_t last_heap_check = 0; + static uint32_t heap_warning_count = 0; + static uint32_t task_count_baseline = 0; + static bool baseline_set = false; while (true) { vTaskDelay(pdMS_TO_TICKS(1000)); // Check every 1 second protocol_state.last_watchdog_check = time_us_64(); + uint32_t current_time_ms = to_ms_since_boot(get_absolute_time()); + + // === SYSTEM HEALTH MONITORING === + + // 1. Memory health check (every 5 seconds) + if (current_time_ms - last_heap_check > 5000) { + size_t free_heap = xPortGetFreeHeapSize(); + size_t min_free_ever = xPortGetMinimumEverFreeHeapSize(); + + // Warning if free heap drops below 4KB or minimum ever below 2KB + if (free_heap < 4096 || min_free_ever < 2048) { + heap_warning_count++; + print_dbg("WATCHDOG: Low memory warning - Free: %u bytes, Min ever: %u bytes (count: %u)\n", + (unsigned)free_heap, (unsigned)min_free_ever, heap_warning_count); + + // If persistent memory issues, force garbage collection + if (heap_warning_count > 3) { + print_dbg("WATCHDOG: Forcing task cleanup due to persistent memory pressure\n"); + // Could add task cleanup logic here if needed + } + } else if (heap_warning_count > 0) { + heap_warning_count = 0; // Reset counter when memory recovers + } + + last_heap_check = current_time_ms; + } + + // 2. Task count monitoring (detect task leaks) + UBaseType_t current_task_count = uxTaskGetNumberOfTasks(); + if (!baseline_set) { + task_count_baseline = current_task_count; + baseline_set = true; + print_dbg("WATCHDOG: Task baseline set to %u tasks\n", task_count_baseline); + } else if (current_task_count > task_count_baseline + 3) { + print_dbg("WATCHDOG: Task count increased significantly - Current: %u, Baseline: %u\n", + current_task_count, task_count_baseline); + } + + // 3. WiFi AP Health Monitoring (every 10 seconds) + static uint32_t last_wifi_check = 0; + static uint32_t wifi_failure_count = 0; + + if (current_time_ms - last_wifi_check > 10000) { + // Check if AP is still operational + bool ap_active = wifi_ap_is_active(); + if (!ap_active) { + wifi_failure_count++; + print_dbg("WATCHDOG: WiFi AP failure detected (count: %u)\n", wifi_failure_count); + + // Try to restart AP after 3 consecutive failures + if (wifi_failure_count >= 3) { + print_dbg("WATCHDOG: Attempting WiFi AP recovery...\n"); + wifi_ap_restart(); + wifi_failure_count = 0; // Reset counter after restart attempt + } + } else { + wifi_failure_count = 0; // Reset counter when AP is healthy + } + + last_wifi_check = current_time_ms; + } + + // 4. HTTP Server Health Check (every 15 seconds) + static uint32_t last_http_check = 0; + + if (current_time_ms - last_http_check > 15000) { + // Check for HTTP server responsiveness + uint32_t active_connections = http_get_active_connections(); + if (active_connections > 10) { + print_dbg("WATCHDOG: High HTTP connection count: %u (potential DoS)\n", active_connections); + } + + last_http_check = current_time_ms; + } - // Skip watchdog if in permanent halt state + // Skip session monitoring if in permanent halt state if (protocol_state.in_halt_state) { continue; } + // === SESSION TIMEOUT MONITORING === // Only monitor timeout when in runtime state (0x40) if (protocol_state.current_state == 0x40) { if (!protocol_is_session_valid()) { @@ -75,6 +160,8 @@ void watchdog_task(void *params) { } } +// Idle monitor removed (tick-based idle accounting deprecated) + int main() { stdio_init_all(); @@ -103,36 +190,51 @@ int main() { } // Create the serial processing task - // Priority hierarchy relative to (configMAX_PRIORITIES = 32): - // 31 (MAX-1): Timer task (FreeRTOS system) - // ~20-25: Critical protocol/crypto tasks - // ~10-15: Web server - // ~5: Background tasks + // OPTIMIZED Priority hierarchy for stability (configMAX_PRIORITIES = 32): + // 31 (MAX-1): FreeRTOS Timer Service Task + // 28: Watchdog (critical system monitoring) + // 26: Serial Protocol (attestation critical) + // 24: WiFi Background (driver stability) + // 20: Crypto/Host pubkey tasks + // 15: HTTP API tasks + // 10: WiFi Init (deferred startup) + // 5: Background maintenance // 0: Idle task // - // Note: Serial is interrupt-driven, so it wakes immediately when data arrives. - // High priority ensures protocol processing isn't blocked by web server. + // Note: Serial is interrupt-driven, high priority prevents web server blocking. + // Watchdog gets highest priority to ensure system monitoring is never blocked. TaskHandle_t serial_task_handle; - xTaskCreate( + BaseType_t serial_result = xTaskCreate( serial_task, "Serial", // Task name - DEFAULT_STACK_SIZE, // Stack size (words, not bytes) + DEFAULT_STACK_SIZE + 512, // Larger stack for stability (words, not bytes) NULL, // Parameters - configMAX_PRIORITIES - 6, // Priority + 26, // Priority 26 (high, but below watchdog) &serial_task_handle // Task handle ); - // Create the watchdog task (high priority for session monitoring) + if (serial_result != pdPASS || serial_task_handle == NULL) { + print_dbg("FATAL: Failed to create Serial task - system cannot continue\n"); + // System cannot function without serial task + while (1) { tight_loop_contents(); } + } + + // Create the enhanced watchdog task (HIGHEST priority for system monitoring) TaskHandle_t watchdog_task_handle; - xTaskCreate( + BaseType_t watchdog_result = xTaskCreate( watchdog_task, "Watchdog", // Task name - DEFAULT_STACK_SIZE, // Stack size + DEFAULT_STACK_SIZE + 256, // Extra stack for monitoring logic NULL, // Parameters - configMAX_PRIORITIES - 5, // High priority (just below serial) + 28, // Highest user priority (below timer service) &watchdog_task_handle // Task handle ); + if (watchdog_result != pdPASS || watchdog_task_handle == NULL) { + print_dbg("FATAL: Failed to create Watchdog task - system stability compromised\n"); + // System can continue but without monitoring - proceed with caution + } + // Initialize serial subsystem with task handle for notifications serial_init(serial_task_handle); @@ -151,6 +253,45 @@ int main() { // TODO check if the token has been provisioned, if not, do special magic to // start the web server in a special admin mode. + // WiFi initialization with LONG delay + // IMPORTANT: WiFi initialization is causing serial provisioning issues + // We start WiFi but NOT the background polling task + if (!wifi_ap_init()) { + print_dbg("WARNING: WiFi subsystem preparation failed\n"); + } else { + // Enable WiFi background task (required for stable lwIP & driver event processing) + // Higher priority than before for better WiFi stability + BaseType_t wifi_bg_result = xTaskCreate( + wifi_background_task, + "WiFi-BG", + DEFAULT_STACK_SIZE + 512, // Larger stack for lwIP stability + NULL, + 24, // Priority 24 (high, for driver stability) + NULL + ); + + if (wifi_bg_result != pdPASS) { + print_dbg("ERROR: Failed to create WiFi background task - AP may be unstable\n"); + } + + // Create WiFi AP initialization task (lower priority, deferred startup) + // Waits for system to stabilize before starting AP + BaseType_t wifi_init_result = xTaskCreate( + wifi_ap_init_task, + "WiFi-Init", + DEFAULT_STACK_SIZE + 256, // Extra stack for initialization + NULL, + 10, // Priority 10 (medium-low, deferred) + NULL + ); + + if (wifi_init_result != pdPASS) { + print_dbg("ERROR: Failed to create WiFi init task - AP will not start\n"); + } + } + + // Idle monitor task removed + // Start the FreeRTOS scheduler vTaskStartScheduler(); diff --git a/src/net/ap/ap_manager.c b/src/net/ap/ap_manager.c new file mode 100644 index 0000000..c4f416c --- /dev/null +++ b/src/net/ap/ap_manager.c @@ -0,0 +1,103 @@ +#include "ap_manager.h" + +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include +#include "lwip/ip4_addr.h" +#include "dhcp/dhcpserver.h" +#include "http/http_server.h" +#include "api/api.h" +#include "FreeRTOS.h" +#include "task.h" +#include "serial.h" + +static dhcp_server_t g_dhcp; + +// Convert const ip4_addr_t to an ip_addr_t (lwIP type) +static void ip4_to_ipaddr(const ip4_addr_t *in, ip_addr_t *out) { + IP4_ADDR(ip_2_ip4(out), ip4_addr1(in), ip4_addr2(in), ip4_addr3(in), ip4_addr4(in)); +} + +int start_access_point(const char *ssid, const char *pass) { + if (cyw43_arch_init()) { + print_dbg("ERROR: cyw43_arch_init failed\n"); + return -1; + } + + // Ensure passphrase length is valid for WPA2 (>= 8). If not, fall back + // to an open AP so the client can at least associate for debugging. + const char *ap_pass = pass; + int auth_mode = CYW43_AUTH_WPA2_AES_PSK; + if (pass == NULL || strlen(pass) < 8) { + auth_mode = CYW43_AUTH_OPEN; + ap_pass = ""; // driver will treat this as open + print_dbg("WARNING: WiFi password too short, using open AP\n"); + } + + cyw43_arch_enable_ap_mode(ssid, ap_pass, auth_mode); + const ip4_addr_t *ap_ip4 = netif_ip4_addr(&cyw43_state.netif[CYW43_ITF_AP]); + const ip4_addr_t *ap_nm4 = netif_ip4_netmask(&cyw43_state.netif[CYW43_ITF_AP]); + + // Use non-blocking approach: wait briefly but yield to other tasks + // This prevents blocking serial provisioning + int retries = 20; // up to ~2 seconds, but each retry yields + while (retries-- > 0) { + ap_ip4 = netif_ip4_addr(&cyw43_state.netif[CYW43_ITF_AP]); + ap_nm4 = netif_ip4_netmask(&cyw43_state.netif[CYW43_ITF_AP]); + if (ap_ip4 && ip4_addr_get_u32(ap_ip4) != 0) break; + // Use vTaskDelay instead of sleep_ms to allow task switching + vTaskDelay(pdMS_TO_TICKS(100)); + } + ip_addr_t ip, nm; + ip4_to_ipaddr(ap_ip4, &ip); + ip4_to_ipaddr(ap_nm4, &nm); + + dhcp_server_init(&g_dhcp, &ip, &nm); + + // Start HTTP API server + http_server_init(); + + // Register API routes + api_register_routes(); + + print_dbg("WiFi AP started: SSID=%s (192.168.4.1)\n", ssid); + + return 0; +} + +void stop_access_point(void) { + dhcp_server_deinit(&g_dhcp); + cyw43_arch_deinit(); + print_dbg("WiFi AP stopped\n"); +} + +int reconfigure_access_point(const char *ssid, const char *pass) { + // Determine auth mode and password + const char *ap_pass = pass; + int auth_mode = CYW43_AUTH_WPA2_AES_PSK; + if (pass == NULL || strlen(pass) < 8) { + auth_mode = CYW43_AUTH_OPEN; + ap_pass = ""; + print_dbg("Reconfiguring AP to OPEN (password too short)\n"); + } else { + print_dbg("Reconfiguring AP to WPA2-PSK\n"); + } + + // Disable AP mode then re-enable with new credentials (keep driver/lwIP alive) + cyw43_arch_disable_ap_mode(); + cyw43_arch_enable_ap_mode(ssid, ap_pass, auth_mode); + + // Keep existing DHCP server; IP/netmask are unchanged + print_dbg("AP reconfigured: SSID=%s, auth=%s\n", ssid, + (auth_mode == CYW43_AUTH_OPEN) ? "OPEN" : "WPA2-PSK"); + return 0; +} + + + +/** + * Get DHCP server instance for querying connected clients + */ +const dhcp_server_t* get_dhcp_server(void) { + return &g_dhcp; +} diff --git a/src/net/ap/ap_manager.h b/src/net/ap/ap_manager.h new file mode 100644 index 0000000..ebc29c0 --- /dev/null +++ b/src/net/ap/ap_manager.h @@ -0,0 +1,17 @@ +#ifndef AP_MANAGER_H +#define AP_MANAGER_H + +int start_access_point(const char *ssid, const char *pass); +void stop_access_point(void); + +// Reconfigure AP without fully deinitializing the driver: disable AP mode, +// then enable again with new SSID/password. Returns 0 on success. +int reconfigure_access_point(const char *ssid, const char *pass); + +// Forward declare DHCP server type +typedef struct _dhcp_server_t dhcp_server_t; + +// Get DHCP server for querying connected clients +const dhcp_server_t* get_dhcp_server(void); + +#endif // AP_MANAGER_H diff --git a/src/net/api/api.c b/src/net/api/api.c new file mode 100644 index 0000000..813557d --- /dev/null +++ b/src/net/api/api.c @@ -0,0 +1,591 @@ +#include "api/api.h" +#include "http/http_server.h" +#include "pico/time.h" +#include "pico/cyw43_arch.h" +#include "hardware/adc.h" +#include "lwip/ip4_addr.h" +#include +#include +#include +#include +#include "serial.h" +#include "protocol.h" +#include "ap/ap_manager.h" +#include "dhcp/dhcpserver.h" +#include "crypt.h" +#include "net/wifi_ap.h" +#include "cryptoauthlib.h" +#include "cpu_monitor.h" +#include "wifi_ap.h" + +// Forward-declared timer for deferred AP password rotation after claim +#include "FreeRTOS.h" +#include "task.h" +#include "timers.h" +#include "semphr.h" + +// Throttled API logging: set to 1 to enable verbose API logs to serial +#ifndef API_DEBUG +#define API_DEBUG 0 +#endif +#if API_DEBUG +#define API_DBG(...) print_dbg(__VA_ARGS__) +#else +#define API_DBG(...) do { } while (0) +#endif + +// State: has the device been claimed already? +static bool g_claimed = false; +// Cache the last generated password so we can optionally re-display or debug +static char g_last_psk[33] = ""; // 32 chars + NUL + +// Token pubkey caching/prefetch handled by crypt.c now + +// One-shot timer callback to restart AP with new password +// Worker task to perform AP restart in a normal task context (not timer task) +static void ap_restart_task(void *arg) { + (void)arg; + // Small delay to ensure response left the device stack + vTaskDelay(pdMS_TO_TICKS(50)); + if (g_last_psk[0] != '\0') { + wifi_ap_rotate_password(g_last_psk); + } + vTaskDelete(NULL); +} + +static void ap_rotate_timer_cb(TimerHandle_t xTimer) { + (void)xTimer; + // Create detached worker to do the heavy lifting (deinit/init may block) + xTaskCreate(ap_restart_task, "ap_rst", 1024, NULL, tskIDLE_PRIORITY + 2, NULL); +} + +// Generate a random WPA2 passphrase (length between 16 and 24) using get_rand_32() +static void generate_random_psk(char *out, size_t out_len) { + static const char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + size_t charset_len = sizeof(charset) - 1; + if (out_len == 0) return; + uint32_t seed = get_rand_32(); + size_t target = 16; // fixed length for now + if (target > out_len - 1) target = out_len - 1; + for (size_t i = 0; i < target; i++) { + // Mix hardware random each iteration for unpredictability + uint32_t r = get_rand_32() ^ (seed + i * 0x9E3779B1u); + out[i] = charset[r % charset_len]; + } + out[target] = '\0'; +} + +// Claim handler: if not claimed, generate password, respond, then schedule AP restart. +static void claim_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; + API_DBG("[API] claim_handler called\n"); + if (g_claimed) { + http_send_json(pcb, 409, "{\"error\":\"already_claimed\"}"); + return; + } + generate_random_psk(g_last_psk, sizeof(g_last_psk)); + g_claimed = true; + + // Create one-shot timer to rotate AP after grace period so response is delivered first. + const uint32_t GRACE_MS = 750; // client sees password then AP restarts + TimerHandle_t t = xTimerCreate("ap_rot", pdMS_TO_TICKS(GRACE_MS), pdFALSE, NULL, ap_rotate_timer_cb); + if (t) { + xTimerStart(t, 0); + } else { + // Fallback: rotate immediately + ap_rotate_timer_cb(NULL); + } + + char body[160]; + int n = snprintf(body, sizeof(body), + "{\"status\":\"ok\",\"ssid\":\"%s\",\"new_password\":\"%s\",\"reconnect_in_ms\":%u}", + wifi_ap_get_config()->ssid, g_last_psk, GRACE_MS); + (void)n; + http_send_json(pcb, 200, body); +} + +// CPU utilization tracking handled inside cpu_monitor (runtime-only) + +static void ping_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; + http_send_json(pcb, 200, "{\"message\":\"pong\"}"); +} + +// Lightweight health endpoint for quick connectivity checks +static void health_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; + http_send_json(pcb, 200, "{\"ok\":true}"); +} + +/* +Status handler - returns system status information, including attecc status and uptime +*/ +static void status_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; + API_DBG("[API] status_handler called\n"); + + uint32_t ms = to_ms_since_boot(get_absolute_time()); + uint32_t s = ms / 1000; + API_DBG("[API] got uptime: %u\n", (unsigned)s); + + API_DBG("[API] checking provisioning state\n"); + bool provisioned = (protocol_state.current_state == 0x40); + API_DBG("[API] provisioned: %d\n", provisioned); + + char body[256]; + API_DBG("[API] building JSON response\n"); + int n = snprintf(body, sizeof(body), + "{\"provisioned\":%s, \"state\":\"0x%02X\", \"uptime_s\":%u, \"wifi_configured\":%s}", + provisioned ? "true" : "false", + protocol_state.current_state, + (unsigned)s, + g_claimed ? "true" : "false"); + API_DBG("[API] snprintf returned: %d, body: %s\n", n, body); + (void)n; + + API_DBG("[API] sending response\n"); + http_send_json(pcb, 200, body); + API_DBG("[API] response sent\n"); +} + +/** + * Network info handler - returns AP SSID, client IPs, and MAC addresses + * + * Requirements: + * R-4.6.1: Display SSID and WPA2-PSK status + * R-4.6.2: Display connected client IP addresses + * R-4.6.3: Display MAC address + IP for each client + * R-4.6.4: Refresh every 5 seconds (handled by client-side) + */ +static void network_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; + API_DBG("[API] network_handler called\n"); + + // Get AP IP + const ip4_addr_t *ap_ip = netif_ip4_addr(&cyw43_state.netif[CYW43_ITF_AP]); + const char *ap_ip_str = ap_ip ? ip4addr_ntoa(ap_ip) : "192.168.4.1"; + + // Get DHCP server to query connected clients + const dhcp_server_t *dhcp = get_dhcp_server(); + + // Build JSON response with network info and connected clients + // Maximum size: headers + 8 clients * (MAC 17 bytes + IP 15 bytes + JSON overhead) + char body[768]; + int pos = 0; + + // Start JSON header + pos += snprintf(body + pos, sizeof(body) - pos, + "{\"ssid\":\"MASTR-Token\",\"security\":\"WPA2-PSK\",\"ap_ip\":\"%s\",\"clients\":[", + ap_ip_str); + + // Add connected clients from DHCP leases + #define DHCPS_MAX_IP 8 + #define DHCPS_BASE_IP 16 + + int client_count = 0; + for (int i = 0; i < DHCPS_MAX_IP; i++) { + // Check if lease is active (expiry is non-zero and not expired) + if (dhcp->lease[i].expiry != 0) { + // Only add comma if not first client + if (client_count > 0) { + pos += snprintf(body + pos, sizeof(body) - pos, ","); + } + + // Format MAC address from lease + const uint8_t *mac = dhcp->lease[i].mac; + uint8_t ip_octet = DHCPS_BASE_IP + i; + + pos += snprintf(body + pos, sizeof(body) - pos, + "{\"mac\":\"%02X:%02X:%02X:%02X:%02X:%02X\",\"ip\":\"192.168.4.%u\"}", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], + ip_octet); + + client_count++; + API_DBG("[API] client %d: %02X:%02X:%02X:%02X:%02X:%02X -> 192.168.4.%u\n", + client_count, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], ip_octet); + } + } + + // Close JSON + pos += snprintf(body + pos, sizeof(body) - pos, "]}"); + + API_DBG("[API] network info: SSID=MASTR-Token, AP_IP=%s, clients=%d\n", ap_ip_str, client_count); + http_send_json(pcb, 200, body); + API_DBG("[API] network response sent\n"); +} + +/** + * RAM info handler - returns RAM usage + * + * Requirements: + * R-4.5.2: Display RAM usage (total, used, free) + */ +static void ram_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; + API_DBG("[API] ram_handler called\n"); + + // Get heap info + size_t free_heap = xPortGetFreeHeapSize(); + size_t total_heap = configTOTAL_HEAP_SIZE; + size_t used_heap = total_heap - free_heap; + + API_DBG("[API] heap - total: %u, used: %u, free: %u\n", + (unsigned)total_heap, (unsigned)used_heap, (unsigned)free_heap); + + // Build JSON response + char body[256]; + int n = snprintf(body, sizeof(body), + "{\"ram_total_kb\":%u,\"ram_used_kb\":%u,\"ram_free_kb\":%u,\"ram_used_percent\":%u}", + (unsigned)(total_heap / 1024), + (unsigned)(used_heap / 1024), + (unsigned)(free_heap / 1024), + (unsigned)((used_heap * 100) / total_heap)); + (void)n; + + API_DBG("[API] ram response: %s\n", body); + http_send_json(pcb, 200, body); + API_DBG("[API] ram response sent\n"); +} + +/** + * Temperature handler - returns internal MCU temperature (°C) + * + * Safe, low-overhead read using Pico SDK ADC driver. We enable the + * internal sensor, discard the first sample, average a few readings, + * then convert to degrees Celsius using the standard formula. + */ +static float read_internal_temperature_c(void) { + // One-time ADC init (idempotent) + static bool adc_ready = false; + if (!adc_ready) { + adc_init(); + adc_set_temp_sensor_enabled(true); + adc_ready = true; + } + + // Select internal temperature sensor channel (ADC input 4) + adc_select_input(4); + + // Throw away the first reading after switching channels/sensor enable + (void)adc_read(); + + // Average several samples for stability + const int SAMPLES = 8; + uint32_t acc = 0; + for (int i = 0; i < SAMPLES; i++) { + acc += adc_read(); + } + float raw = acc / (float)SAMPLES; + + // Convert raw 12-bit ADC reading to voltage (assumes 3.3V reference) + const float VREF = 3.3f; + float v = raw * VREF / 4095.0f; + + // Pico formula: 27°C at 0.706V, slope 1.721 mV/°C + float temp_c = 27.0f - (v - 0.706f) / 0.001721f; + return temp_c; +} + +static void temperature_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; + API_DBG("[API] temperature_handler called\n"); + + float t = read_internal_temperature_c(); + // Clamp to a sane range in case of transient anomalies + if (t < -40.0f) t = -40.0f; + if (t > 125.0f) t = 125.0f; + + char body[128]; + int n = snprintf(body, sizeof(body), "{\"temp_c\":%.1f}", (double)t); + (void)n; + API_DBG("[API] temperature: %.1f C\n", (double)t); + http_send_json(pcb, 200, body); +} + +/** + * CPU utilization handler - returns CPU usage percentage + * + * Requirements: + * R-4.5.1: Display CPU utilization + * + * Formula: CPU% = (TotalDelta - IdleDelta) / TotalDelta * 100 + * + * Uses FreeRTOS idle hook to accurately measure idle vs busy time. + * g_idleTicks is incremented by vApplicationIdleHook() each time idle task runs. + */ +static void cpu_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; + API_DBG("[API] cpu_handler called\n"); + uint32_t cpu_percent = cpu_get_percent(); + if (cpu_percent > 100U) cpu_percent = 100U; + char body[128]; + int n = snprintf(body, sizeof(body), "{\"cpu_percent\":%u}", cpu_percent); + (void)n; + http_send_json(pcb, 200, body); +} + +/** + * Token info handler - returns token's public key for provisioning + * GET /api/provision/token_info + * Returns: {"token_pubkey":""} + */ +static void token_info_handler(struct tcp_pcb *pcb, const char *request){ + (void)request; + bool ready = false; + const char *hex = NULL; + ready = crypt_get_cached_token_pubkey_hex(&hex, &ready); + API_DBG("[API] token_info_handler called (ready=%d failed=%d)\n", ready, crypt_token_pubkey_failed()); + if (!ready) { + if (crypt_token_pubkey_failed()) { + http_send_json(pcb, 500, "{\"error\":\"pubkey_prefetch_failed\"}"); + } else { + http_send_json(pcb, 503, "{\"status\":\"initializing\",\"retry_ms\":100}"); + } + return; + } + char body[180]; + int n = snprintf(body, sizeof(body), "{\"token_pubkey\":\"%s\",\"cached\":true}", hex); + (void)n; + http_send_json(pcb, 200, body); +} + +/** + * Set host public key handler (non-blocking) + * POST /api/provision/host_pubkey + * Expects: 64-byte hex string in request body (128 hex chars) + * Returns: {"status":"accepted"} or {"error":"..."} + */ +static void set_host_pubkey_handler(struct tcp_pcb *pcb, const char *request) { + print_dbg("API: set_host_pubkey_handler called (non-blocking)\n"); + + // Find the request body (after double CRLF) + const char *body_start = strstr(request, "\r\n\r\n"); + if (!body_start) { + print_dbg("API: missing request body\n"); + http_send_json(pcb, 400, "{\"error\":\"missing_body\"}"); + return; + } + body_start += 4; // Skip past "\r\n\r\n" + + // Trim whitespace and newlines from the end + const char *body_end = body_start + strlen(body_start); + while (body_end > body_start && (body_end[-1] == '\r' || body_end[-1] == '\n' || body_end[-1] == ' ')) { + body_end--; + } + + // Create null-terminated string for the hex data + size_t hex_len = body_end - body_start; + print_dbg("API: received hex data length: %zu\n", hex_len); + + if (hex_len != 128) { + print_dbg("API: invalid hex length, expected 128, got %zu\n", hex_len); + char error_msg[100]; + snprintf(error_msg, sizeof(error_msg), "{\"error\":\"invalid_length\",\"expected\":128,\"got\":%zu}", hex_len); + http_send_json(pcb, 400, error_msg); + return; + } + + // Validate hex format before proceeding + for (size_t i = 0; i < 128; i++) { + char c = body_start[i]; + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + print_dbg("API: invalid hex character at position %zu: '%c'\n", i, c); + http_send_json(pcb, 400, "{\"error\":\"invalid_hex_format\"}"); + return; + } + } + + print_dbg("API: hex validation passed, creating null-terminated string\n"); + + // Create null-terminated string for the crypto function + char hex_str[129]; + memcpy(hex_str, body_start, 128); + hex_str[128] = '\0'; + + print_dbg("API: requesting non-blocking host pubkey write\n"); + + // Use the new non-blocking crypto function + bool write_ready, write_failed; + bool accepted = crypt_request_host_pubkey_write(hex_str, &write_ready, &write_failed); + + if (!accepted) { + print_dbg("API: Host pubkey write request rejected (already pending)\n"); + http_send_json(pcb, 409, "{\"error\":\"write_pending\",\"retry_ms\":100}"); + return; + } + + print_dbg("API: Host pubkey write request accepted\n"); + http_send_json(pcb, 202, "{\"status\":\"accepted\",\"message\":\"write_queued\"}"); +} + +/** + * Get host public key handler (non-blocking) + * GET /api/provision/host_pubkey/get + * Returns: {"host_pubkey":""} or {"status":"reading"} or {"error":"..."} + */ +static void get_host_pubkey_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; + print_dbg("API: get_host_pubkey_handler called (non-blocking)\n"); + + const char *hex_pubkey = NULL; + bool ready = false; + bool failed = false; + + crypt_get_cached_host_pubkey_hex(&hex_pubkey, &ready, &failed); + + if (failed) { + print_dbg("API: Host pubkey read failed\n"); + http_send_json(pcb, 500, "{\"error\":\"read_failed\"}"); + return; + } + + if (ready && hex_pubkey && hex_pubkey[0] != '\0') { + // Return cached result + char body[180]; + int n = snprintf(body, sizeof(body), "{\"host_pubkey\":\"%s\",\"cached\":true}", hex_pubkey); + (void)n; + print_dbg("API: Returning cached host pubkey\n"); + http_send_json(pcb, 200, body); + return; + } + + // Still reading + print_dbg("API: Host pubkey not ready yet\n"); + http_send_json(pcb, 503, "{\"status\":\"reading\",\"retry_ms\":100}"); +} + +/** + * Get host public key write status handler (non-blocking) + * GET /api/provision/host_pubkey/status + * Returns: {"status":"ready|pending|failed"} + */ +static void host_pubkey_status_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; + print_dbg("API: host_pubkey_status_handler called\n"); + + bool write_ready = false; + bool write_failed = false; + + crypt_get_host_pubkey_write_status(&write_ready, &write_failed); + + if (write_failed) { + http_send_json(pcb, 200, "{\"status\":\"failed\"}"); + } else if (write_ready) { + http_send_json(pcb, 200, "{\"status\":\"ready\"}"); + } else { + http_send_json(pcb, 200, "{\"status\":\"pending\"}"); + } +} + +/** + * Set golden hash handler - POST /api/provision/golden_hash + * Non-blocking version: validates input then triggers async crypto operation + */ +static void set_golden_hash_handler(struct tcp_pcb *pcb, const char *request) { + // Extract hex data from request body + const char *body_start = strstr(request, "\r\n\r\n"); + if (!body_start) { + http_send_json(pcb, 400, "{\"error\":\"missing_body\"}"); + return; + } + body_start += 4; + + // Trim whitespace + const char *body_end = body_start + strlen(body_start); + while (body_end > body_start && (body_end[-1] == '\r' || body_end[-1] == '\n' || body_end[-1] == ' ')) { + body_end--; + } + + size_t hex_len = body_end - body_start; + if (hex_len != 64) { + http_send_json(pcb, 400, "{\"error\":\"invalid_length\"}"); + return; + } + + // Validate hex format + for (size_t i = 0; i < 64; i++) { + char c = body_start[i]; + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + http_send_json(pcb, 400, "{\"error\":\"invalid_hex\"}"); + return; + } + } + + // Convert hex to bytes + uint8_t golden_hash[32]; + for (int i = 0; i < 32; i++) { + char hex_pair[3] = {body_start[i*2], body_start[i*2 + 1], '\0'}; + golden_hash[i] = (uint8_t)strtol(hex_pair, NULL, 16); + } + + // Trigger async golden hash operation (non-blocking) + bool queued = crypt_spawn_golden_hash_task_with_data(golden_hash); + if (!queued) { + http_send_json(pcb, 503, "{\"error\":\"task_busy\"}"); + return; + } + + // Return immediate response - client should poll status + http_send_json(pcb, 202, "{\"status\":\"accepted\",\"message\":\"golden_hash_operation_queued\"}"); +} + +/** + * Get golden hash status handler - GET /api/provision/golden_hash/status + * Returns the result of the most recent golden hash operation + */ +static void golden_hash_status_handler(struct tcp_pcb *pcb, const char *request) { + (void)request; // Unused parameter + + bool write_ready = false; + bool write_failed = false; + uint8_t golden_hash_result[32]; + + crypt_get_golden_hash_write_status(&write_ready, &write_failed, golden_hash_result); + + if (write_ready) { + // Convert result hash to hex + char hex_response[65]; + for (int i = 0; i < 32; i++) { + sprintf(&hex_response[i*2], "%02x", golden_hash_result[i]); + } + + char response[150]; + snprintf(response, sizeof(response), "{\"status\":\"success\",\"golden_hash\":\"%s\"}", hex_response); + http_send_json(pcb, 200, response); + } else if (write_failed) { + http_send_json(pcb, 200, "{\"status\":\"error\",\"error\":\"crypto_operation_failed\"}"); + } else { + // Still processing or idle + http_send_json(pcb, 200, "{\"status\":\"processing\"}"); + } +} + +void api_register_routes(void) { + http_register("/api/ping", ping_handler); + http_register("/api/health", health_handler); + http_register("/api/status", status_handler); + http_register("/api/network", network_handler); + http_register("/api/ram", ram_handler); + http_register("/api/temp", temperature_handler); + http_register("/api/cpu", cpu_handler); + http_register("/api/claim", claim_handler); + // Provisioning token public key endpoint (single canonical path) + http_register("/api/provision/token_info", token_info_handler); + // Provisioning host public key endpoints (non-blocking versions) + http_register("/api/provision/host_pubkey", set_host_pubkey_handler); // POST to set + http_register("/api/provision/host_pubkey/get", get_host_pubkey_handler); // GET to read + http_register("/api/provision/host_pubkey/status", host_pubkey_status_handler); // GET write status + // Provisioning golden hash endpoints (non-blocking versions) + http_register("/api/provision/golden_hash", set_golden_hash_handler); // POST to set + http_register("/api/provision/golden_hash/status", golden_hash_status_handler); // GET status + // Ask crypt layer to spawn prefetch task (low priority) + crypt_spawn_pubkey_prefetch(); + + // Start background task for host pubkey operations (non-blocking) + crypt_spawn_host_pubkey_task(); + + // Start background task for golden hash operations (non-blocking) + crypt_spawn_golden_hash_task(); + + print_dbg("API routes registered\n"); +} diff --git a/src/net/api/api.h b/src/net/api/api.h new file mode 100644 index 0000000..0d89ad7 --- /dev/null +++ b/src/net/api/api.h @@ -0,0 +1,7 @@ +#ifndef API_H +#define API_H + +// Register API routes with the HTTP server. Call after http_server_init(). +void api_register_routes(void); + +#endif // API_H diff --git a/src/net/dhcp/dhcpserver.c b/src/net/dhcp/dhcpserver.c new file mode 100644 index 0000000..dcf6666 --- /dev/null +++ b/src/net/dhcp/dhcpserver.c @@ -0,0 +1,326 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2018-2019 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// For DHCP specs see: +// https://www.ietf.org/rfc/rfc2131.txt +// https://tools.ietf.org/html/rfc2132 -- DHCP Options and BOOTP Vendor Extensions + +#include +#include +#include + +#include "cyw43_config.h" +#include "dhcp/dhcpserver.h" +#include "lwip/udp.h" + +#define DHCPDISCOVER (1) +#define DHCPOFFER (2) +#define DHCPREQUEST (3) +#define DHCPDECLINE (4) +#define DHCPACK (5) +#define DHCPNACK (6) +#define DHCPRELEASE (7) +#define DHCPINFORM (8) + +#define DHCP_OPT_PAD (0) +#define DHCP_OPT_SUBNET_MASK (1) +#define DHCP_OPT_ROUTER (3) +#define DHCP_OPT_DNS (6) +#define DHCP_OPT_HOST_NAME (12) +#define DHCP_OPT_REQUESTED_IP (50) +#define DHCP_OPT_IP_LEASE_TIME (51) +#define DHCP_OPT_MSG_TYPE (53) +#define DHCP_OPT_SERVER_ID (54) +#define DHCP_OPT_PARAM_REQUEST_LIST (55) +#define DHCP_OPT_MAX_MSG_SIZE (57) +#define DHCP_OPT_VENDOR_CLASS_ID (60) +#define DHCP_OPT_CLIENT_ID (61) +#define DHCP_OPT_END (255) + +#define PORT_DHCP_SERVER (67) +#define PORT_DHCP_CLIENT (68) + +#define DEFAULT_LEASE_TIME_S (24 * 60 * 60) // in seconds + +#define MAC_LEN (6) +#define MAKE_IP4(a, b, c, d) ((a) << 24 | (b) << 16 | (c) << 8 | (d)) + +typedef struct { + uint8_t op; // message opcode + uint8_t htype; // hardware address type + uint8_t hlen; // hardware address length + uint8_t hops; + uint32_t xid; // transaction id, chosen by client + uint16_t secs; // client seconds elapsed + uint16_t flags; + uint8_t ciaddr[4]; // client IP address + uint8_t yiaddr[4]; // your IP address + uint8_t siaddr[4]; // next server IP address + uint8_t giaddr[4]; // relay agent IP address + uint8_t chaddr[16]; // client hardware address + uint8_t sname[64]; // server host name + uint8_t file[128]; // boot file name + uint8_t options[312]; // optional parameters, variable, starts with magic +} dhcp_msg_t; + +// Convert lwIP ip_addr_t to 4 bytes in dotted order (a.b.c.d) into buf[0..3]. +// Use the ip4_addrX() helpers to avoid endianness issues (ip4_addr_get_u32 +// can be in host byte order on little-endian platforms). Using the helpers +// guarantees we extract the octets in the correct network (dotted) order. +static void ipaddr_to_bytes(const ip_addr_t *a, uint8_t *buf) { + const ip4_addr_t *ip4 = ip_2_ip4((ip_addr_t *)a); + buf[0] = ip4_addr1(ip4); + buf[1] = ip4_addr2(ip4); + buf[2] = ip4_addr3(ip4); + buf[3] = ip4_addr4(ip4); +} + +static int dhcp_socket_new_dgram(struct udp_pcb **udp, void *cb_data, udp_recv_fn cb_udp_recv) { + // family is AF_INET + // type is SOCK_DGRAM + + *udp = udp_new(); + if (*udp == NULL) { + return -ENOMEM; + } + + // Register callback + udp_recv(*udp, cb_udp_recv, (void *)cb_data); + + return 0; // success +} + +static void dhcp_socket_free(struct udp_pcb **udp) { + if (*udp != NULL) { + udp_remove(*udp); + *udp = NULL; + } +} + +static int dhcp_socket_bind(struct udp_pcb **udp, uint16_t port) { + // TODO convert lwIP errors to errno + return udp_bind(*udp, IP_ANY_TYPE, port); +} + +static int dhcp_socket_sendto(struct udp_pcb **udp, struct netif *nif, const void *buf, size_t len, uint32_t ip, uint16_t port) { + if (len > 0xffff) { + len = 0xffff; + } + + struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); + if (p == NULL) { + return -ENOMEM; + } + + memcpy(p->payload, buf, len); + + ip_addr_t dest; + IP4_ADDR(ip_2_ip4(&dest), ip >> 24 & 0xff, ip >> 16 & 0xff, ip >> 8 & 0xff, ip & 0xff); + err_t err; + if (nif != NULL) { + err = udp_sendto_if(*udp, p, &dest, port, nif); + } else { + err = udp_sendto(*udp, p, &dest, port); + } + + pbuf_free(p); + + if (err != ERR_OK) { + return err; + } + + return len; +} + +static uint8_t *opt_find(uint8_t *opt, uint8_t cmd) { + for (int i = 0; i < 308 && opt[i] != DHCP_OPT_END;) { + if (opt[i] == cmd) { + return &opt[i]; + } + i += 2 + opt[i + 1]; + } + return NULL; +} + +static void opt_write_n(uint8_t **opt, uint8_t cmd, size_t n, const void *data) { + uint8_t *o = *opt; + *o++ = cmd; + *o++ = n; + memcpy(o, data, n); + *opt = o + n; +} + +static void opt_write_u8(uint8_t **opt, uint8_t cmd, uint8_t val) { + uint8_t *o = *opt; + *o++ = cmd; + *o++ = 1; + *o++ = val; + *opt = o; +} + +static void opt_write_u32(uint8_t **opt, uint8_t cmd, uint32_t val) { + uint8_t *o = *opt; + *o++ = cmd; + *o++ = 4; + *o++ = val >> 24; + *o++ = val >> 16; + *o++ = val >> 8; + *o++ = val; + *opt = o; +} + +static void dhcp_server_process(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *src_addr, u16_t src_port) { + dhcp_server_t *d = arg; + (void)upcb; + (void)src_addr; + (void)src_port; + + // This is around 548 bytes + dhcp_msg_t dhcp_msg; + + #define DHCP_MIN_SIZE (240 + 3) + if (p->tot_len < DHCP_MIN_SIZE) { + goto ignore_request; + } + + size_t len = pbuf_copy_partial(p, &dhcp_msg, sizeof(dhcp_msg), 0); + if (len < DHCP_MIN_SIZE) { + goto ignore_request; + } + + dhcp_msg.op = DHCPOFFER; + // Copy server IP into yiaddr as four network-order bytes + ipaddr_to_bytes(&d->ip, dhcp_msg.yiaddr); + + uint8_t *opt = (uint8_t *)&dhcp_msg.options; + opt += 4; // assume magic cookie: 99, 130, 83, 99 + + uint8_t *msgtype = opt_find(opt, DHCP_OPT_MSG_TYPE); + if (msgtype == NULL) { + // A DHCP package without MSG_TYPE? + goto ignore_request; + } + + switch (msgtype[2]) { + case DHCPDISCOVER: { + int yi = DHCPS_MAX_IP; + for (int i = 0; i < DHCPS_MAX_IP; ++i) { + if (memcmp(d->lease[i].mac, dhcp_msg.chaddr, MAC_LEN) == 0) { + // MAC match, use this IP address + yi = i; + break; + } + if (yi == DHCPS_MAX_IP) { + // Look for a free IP address + if (memcmp(d->lease[i].mac, "\x00\x00\x00\x00\x00\x00", MAC_LEN) == 0) { + // IP available + yi = i; + } + uint32_t expiry = d->lease[i].expiry << 16 | 0xffff; + if ((int32_t)(expiry - cyw43_hal_ticks_ms()) < 0) { + // IP expired, reuse it + memset(d->lease[i].mac, 0, MAC_LEN); + yi = i; + } + } + } + if (yi == DHCPS_MAX_IP) { + // No more IP addresses left + goto ignore_request; + } + dhcp_msg.yiaddr[3] = DHCPS_BASE_IP + yi; + opt_write_u8(&opt, DHCP_OPT_MSG_TYPE, DHCPOFFER); + break; + } + + case DHCPREQUEST: { + uint8_t *o = opt_find(opt, DHCP_OPT_REQUESTED_IP); + if (o == NULL) { + // Should be NACK + goto ignore_request; + } + uint8_t server_id_bytes[4]; + ipaddr_to_bytes(&d->ip, server_id_bytes); + if (memcmp(o + 2, server_id_bytes, 3) != 0) { + // Should be NACK + goto ignore_request; + } + uint8_t yi = o[5] - DHCPS_BASE_IP; + if (yi >= DHCPS_MAX_IP) { + // Should be NACK + goto ignore_request; + } + if (memcmp(d->lease[yi].mac, dhcp_msg.chaddr, MAC_LEN) == 0) { + // MAC match, ok to use this IP address + } else if (memcmp(d->lease[yi].mac, "\x00\x00\x00\x00\x00\x00", MAC_LEN) == 0) { + // IP unused, ok to use this IP address + memcpy(d->lease[yi].mac, dhcp_msg.chaddr, MAC_LEN); + } else { + // IP already in use + // Should be NACK + goto ignore_request; + } + d->lease[yi].expiry = (cyw43_hal_ticks_ms() + DEFAULT_LEASE_TIME_S * 1000) >> 16; + dhcp_msg.yiaddr[3] = DHCPS_BASE_IP + yi; + opt_write_u8(&opt, DHCP_OPT_MSG_TYPE, DHCPACK); + break; + } + + default: + goto ignore_request; + } + + uint8_t tmp_ip[4]; + ipaddr_to_bytes(&d->ip, tmp_ip); + opt_write_n(&opt, DHCP_OPT_SERVER_ID, 4, tmp_ip); + ipaddr_to_bytes(&d->nm, tmp_ip); + opt_write_n(&opt, DHCP_OPT_SUBNET_MASK, 4, tmp_ip); + ipaddr_to_bytes(&d->ip, tmp_ip); + opt_write_n(&opt, DHCP_OPT_ROUTER, 4, tmp_ip); // aka gateway; can have multiple addresses + ipaddr_to_bytes(&d->ip, tmp_ip); + opt_write_n(&opt, DHCP_OPT_DNS, 4, tmp_ip); // this server is the dns + opt_write_u32(&opt, DHCP_OPT_IP_LEASE_TIME, DEFAULT_LEASE_TIME_S); + *opt++ = DHCP_OPT_END; + struct netif *nif = ip_current_input_netif(); + dhcp_socket_sendto(&d->udp, nif, &dhcp_msg, opt - (uint8_t *)&dhcp_msg, 0xffffffff, PORT_DHCP_CLIENT); + +ignore_request: + pbuf_free(p); +} + +void dhcp_server_init(dhcp_server_t *d, ip_addr_t *ip, ip_addr_t *nm) { + ip_addr_copy(d->ip, *ip); + ip_addr_copy(d->nm, *nm); + memset(d->lease, 0, sizeof(d->lease)); + if (dhcp_socket_new_dgram(&d->udp, d, dhcp_server_process) != 0) { + return; + } + dhcp_socket_bind(&d->udp, PORT_DHCP_SERVER); +} + +void dhcp_server_deinit(dhcp_server_t *d) { + dhcp_socket_free(&d->udp); +} diff --git a/src/net/dhcp/dhcpserver.h b/src/net/dhcp/dhcpserver.h new file mode 100644 index 0000000..2349d2e --- /dev/null +++ b/src/net/dhcp/dhcpserver.h @@ -0,0 +1,49 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2018-2019 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MICROPY_INCLUDED_LIB_NETUTILS_DHCPSERVER_H +#define MICROPY_INCLUDED_LIB_NETUTILS_DHCPSERVER_H + +#include "lwip/ip_addr.h" + +#define DHCPS_BASE_IP (16) +#define DHCPS_MAX_IP (8) + +typedef struct _dhcp_server_lease_t { + uint8_t mac[6]; + uint16_t expiry; +} dhcp_server_lease_t; + +typedef struct _dhcp_server_t { + ip_addr_t ip; + ip_addr_t nm; + dhcp_server_lease_t lease[DHCPS_MAX_IP]; + struct udp_pcb *udp; +} dhcp_server_t; + +void dhcp_server_init(dhcp_server_t *d, ip_addr_t *ip, ip_addr_t *nm); +void dhcp_server_deinit(dhcp_server_t *d); + +#endif // MICROPY_INCLUDED_LIB_NETUTILS_DHCPSERVER_H diff --git a/src/net/http/http_server.c b/src/net/http/http_server.c new file mode 100644 index 0000000..de02ce7 --- /dev/null +++ b/src/net/http/http_server.c @@ -0,0 +1,177 @@ +// Rebuilt HTTP server file: cleaned duplicates, minimal single-connection server +// with deferred close via tcp_sent to reduce intermittent curl failures. + +#include "http_server.h" +#include "lwip/pbuf.h" +#include "lwip/tcp.h" +#include +#include +#include +#include "serial.h" + +// Forward declarations for connection tracking +static void http_connection_opened(void); +static void http_connection_closed(void); + +#define MAX_ROUTES 16 // Increased to accommodate new provisioning endpoints +struct route_entry { const char *path; http_handler_fn handler; }; +static struct route_entry routes[MAX_ROUTES]; + +int http_register(const char *path, http_handler_fn handler) { + for (int i = 0; i < MAX_ROUTES; ++i) { + if (routes[i].path == NULL) { + routes[i].path = path; + routes[i].handler = handler; +#ifdef DEBUG + print_dbg("[HTTP] Registered route: %s\n", path); +#endif + return 0; + } + } +#ifdef DEBUG + print_dbg("[HTTP] Route table full, failed to register: %s\n", path); +#endif + return -1; +} + +typedef struct { + char request[1024]; + int request_len; + bool in_use; + bool close_when_sent; +} http_state_t; + +static http_state_t g_state; + +static void reset_state(void) { + g_state.request_len = 0; + g_state.request[0] = '\0'; + g_state.in_use = false; + g_state.close_when_sent = false; + + // Track connection closing + http_connection_closed(); +} + +static void send_response(struct tcp_pcb *pcb, const char *status, const char *content_type, const char *body) { + char header[256]; + int header_len = snprintf(header, sizeof(header), + "HTTP/1.1 %s\r\n" + "Content-Type: %s\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n" + "Content-Length: %d\r\n\r\n", + status, content_type, (int)strlen(body)); + + if (tcp_write(pcb, header, header_len, TCP_WRITE_FLAG_COPY) != ERR_OK) { + tcp_abort(pcb); return; } + if (tcp_write(pcb, body, strlen(body), TCP_WRITE_FLAG_COPY) != ERR_OK) { + tcp_abort(pcb); return; } + tcp_output(pcb); + g_state.close_when_sent = true; +} + +void http_send_json(struct tcp_pcb *pcb, int status_code, const char *json_body) { + char status[32]; + switch (status_code) { + case 200: strcpy(status, "200 OK"); break; + case 404: strcpy(status, "404 Not Found"); break; + case 500: strcpy(status, "500 Internal Server Error"); break; + default: snprintf(status, sizeof(status), "%d", status_code); break; + } + send_response(pcb, status, "application/json", json_body); +} + +static void handle_request(struct tcp_pcb *pcb, char *request) { + char method[8], path[64]; + sscanf(request, "%7s %63s", method, path); + for (int i = 0; i < MAX_ROUTES; ++i) { + if (routes[i].path && strcmp(path, routes[i].path) == 0) { + routes[i].handler(pcb, request); + return; + } + } + send_response(pcb, "404 Not Found", "application/json", "{\"error\":\"not found\"}"); +} + +static err_t http_close(struct tcp_pcb *pcb) { + reset_state(); + return tcp_close(pcb); +} + +static void http_err(void *arg, err_t err) { + (void)arg; (void)err; reset_state(); } + +static err_t http_sent(void *arg, struct tcp_pcb *pcb, u16_t len) { + (void)arg; (void)len; + if (g_state.close_when_sent) return http_close(pcb); + return ERR_OK; +} + +static err_t http_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { + (void)arg; (void)err; + if (!p) return http_close(pcb); + if (p->tot_len > 0) { + int copy_len = sizeof(g_state.request) - g_state.request_len - 1; + if (copy_len < 0) copy_len = 0; + if (p->tot_len < copy_len) copy_len = p->tot_len; + if (copy_len > 0) { + pbuf_copy_partial(p, g_state.request + g_state.request_len, copy_len, 0); + g_state.request_len += copy_len; + g_state.request[g_state.request_len] = '\0'; + } + tcp_recved(pcb, p->tot_len); + if (strstr(g_state.request, "\r\n\r\n")) { + handle_request(pcb, g_state.request); + } + } + pbuf_free(p); + return ERR_OK; +} + +static err_t http_accept(void *arg, struct tcp_pcb *client_pcb, err_t err) { + (void)arg; (void)err; + if (g_state.in_use) { + tcp_close(client_pcb); + return ERR_OK; + } + + // Track new connection + http_connection_opened(); + + g_state.in_use = true; g_state.request_len = 0; g_state.close_when_sent = false; g_state.request[0] = '\0'; + tcp_arg(client_pcb, &g_state); + tcp_recv(client_pcb, http_recv); + tcp_err(client_pcb, http_err); + tcp_sent(client_pcb, http_sent); + return ERR_OK; +} + +void http_server_init(void) { + struct tcp_pcb *pcb = tcp_new_ip_type(IPADDR_TYPE_ANY); + if (!pcb) { print_dbg("HTTP: tcp_new failed\n"); return; } + if (tcp_bind(pcb, NULL, 80) != ERR_OK) { print_dbg("HTTP: bind failed\n"); tcp_abort(pcb); return; } + pcb = tcp_listen(pcb); + tcp_accept(pcb, http_accept); + print_dbg("HTTP server initialized on port 80\n"); +} + +// ============================================================================ +// HTTP Server Monitoring Functions +// ============================================================================ + +static uint32_t g_active_connections = 0; + +static void http_connection_opened(void) { + g_active_connections++; +} + +static void http_connection_closed(void) { + if (g_active_connections > 0) { + g_active_connections--; + } +} + +uint32_t http_get_active_connections(void) { + return g_active_connections; +} diff --git a/src/net/http/http_server.h b/src/net/http/http_server.h new file mode 100644 index 0000000..2d30013 --- /dev/null +++ b/src/net/http/http_server.h @@ -0,0 +1,21 @@ +#ifndef HTTP_SERVER_H +#define HTTP_SERVER_H + +#include + +struct tcp_pcb; +typedef void (*http_handler_fn)(struct tcp_pcb *pcb, const char *request); + +void http_server_init(void); + +// Register a handler for an exact path (e.g. "/api/ping"). +// Returns 0 on success, -1 on failure (no space). +int http_register(const char *path, http_handler_fn handler); + +// Helper to send a JSON response with a numeric status code (200, 404, ...) +void http_send_json(struct tcp_pcb *pcb, int status_code, const char *json_body); + +// HTTP connection monitoring functions +uint32_t http_get_active_connections(void); + +#endif // HTTP_SERVER_H diff --git a/src/net/lwipopts.h b/src/net/lwipopts.h new file mode 100644 index 0000000..30f1602 --- /dev/null +++ b/src/net/lwipopts.h @@ -0,0 +1,93 @@ +#ifndef _LWIPOPTS_EXAMPLE_COMMONH_H +#define _LWIPOPTS_EXAMPLE_COMMONH_H + + +// Common settings used in most of the pico_w examples +// (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html for details) + +// allow override in some examples +#ifndef NO_SYS +#define NO_SYS 1 +#endif +// allow override in some examples +#ifndef LWIP_SOCKET +#define LWIP_SOCKET 0 +#endif +#if PICO_CYW43_ARCH_POLL +#define MEM_LIBC_MALLOC 1 +#else +// MEM_LIBC_MALLOC is incompatible with non polling versions +#define MEM_LIBC_MALLOC 0 +#endif +#define MEM_ALIGNMENT 4 +#ifndef MEM_SIZE +#define MEM_SIZE 4000 +#endif +#define MEMP_NUM_TCP_SEG 32 +#define MEMP_NUM_ARP_QUEUE 10 +#define PBUF_POOL_SIZE 24 +#define LWIP_ARP 1 +#define LWIP_ETHERNET 1 +#define LWIP_ICMP 1 +#define LWIP_RAW 1 +#define TCP_WND (8 * TCP_MSS) +#define TCP_MSS 1460 +#define TCP_SND_BUF (8 * TCP_MSS) +#define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS)) +#define LWIP_NETIF_STATUS_CALLBACK 1 +#define LWIP_NETIF_LINK_CALLBACK 1 +#define LWIP_NETIF_HOSTNAME 1 +#define LWIP_NETCONN 0 +#define MEM_STATS 0 +#define SYS_STATS 0 +#define MEMP_STATS 0 +#define LINK_STATS 0 +// #define ETH_PAD_SIZE 2 +#define LWIP_CHKSUM_ALGORITHM 3 +#define LWIP_DHCP 1 +#define LWIP_IPV4 1 +#define LWIP_TCP 1 +#define LWIP_UDP 1 +#define LWIP_DNS 1 +#define LWIP_TCP_KEEPALIVE 1 +#define LWIP_NETIF_TX_SINGLE_PBUF 1 +#define DHCP_DOES_ARP_CHECK 0 +#define LWIP_DHCP_DOES_ACD_CHECK 0 +#define LWIP_PROVIDE_ERRNO 0 + +#ifndef NDEBUG +#define LWIP_DEBUG 1 +#define LWIP_STATS 1 +#define LWIP_STATS_DISPLAY 1 +#endif + +#define ETHARP_DEBUG LWIP_DBG_OFF +#define NETIF_DEBUG LWIP_DBG_OFF +#define PBUF_DEBUG LWIP_DBG_OFF +#define API_LIB_DEBUG LWIP_DBG_OFF +#define API_MSG_DEBUG LWIP_DBG_OFF +#define SOCKETS_DEBUG LWIP_DBG_OFF +#define ICMP_DEBUG LWIP_DBG_OFF +#define INET_DEBUG LWIP_DBG_OFF +#define IP_DEBUG LWIP_DBG_OFF +#define IP_REASS_DEBUG LWIP_DBG_OFF +#define RAW_DEBUG LWIP_DBG_OFF +#define MEM_DEBUG LWIP_DBG_OFF +#define MEMP_DEBUG LWIP_DBG_OFF +#define SYS_DEBUG LWIP_DBG_OFF +#define TCP_DEBUG LWIP_DBG_OFF +#define TCP_INPUT_DEBUG LWIP_DBG_OFF +#define TCP_OUTPUT_DEBUG LWIP_DBG_OFF +#define TCP_RTO_DEBUG LWIP_DBG_OFF +#define TCP_CWND_DEBUG LWIP_DBG_OFF +#define TCP_WND_DEBUG LWIP_DBG_OFF +#define TCP_FR_DEBUG LWIP_DBG_OFF +#define TCP_QLEN_DEBUG LWIP_DBG_OFF +#define TCP_RST_DEBUG LWIP_DBG_OFF +#define UDP_DEBUG LWIP_DBG_OFF +#define TCPIP_DEBUG LWIP_DBG_OFF +#define PPP_DEBUG LWIP_DBG_OFF +#define SLIP_DEBUG LWIP_DBG_OFF +#define DHCP_DEBUG LWIP_DBG_OFF + +#endif /* __LWIPOPTS_H__ */ diff --git a/src/net/wifi_ap.c b/src/net/wifi_ap.c new file mode 100644 index 0000000..5afdd5e --- /dev/null +++ b/src/net/wifi_ap.c @@ -0,0 +1,246 @@ +#include "wifi_ap.h" +#include "serial.h" +#include "ap/ap_manager.h" +#include +#include + +#ifndef UNIT_TEST +#include "FreeRTOS.h" +#include "task.h" +#endif + +// Persistent password storage so runtime password rotations remain valid. +// Start passwordless (empty string) so AP initially is OPEN for claim flow. +static char wifi_pass_storage[65] = ""; // will be filled on claim +static wifi_ap_config_t wifi_config = { + .ssid = "MASTR-Token", + .password = wifi_pass_storage, // pointer always kept to storage + .ip_address = 0xC0A80401, // 192.168.4.1 + .is_running = false +}; + +/** + * Initialize WiFi hardware (lightweight - just prepares config) + * Actual CYW43 initialization happens in wifi_ap_init_task after FreeRTOS starts + */ +bool wifi_ap_init(void) { + // Just mark that we're ready to initialize WiFi + // The actual cyw43_arch_init() must happen in a FreeRTOS task + print_dbg("WiFi subsystem ready for initialization\n"); + return true; +} + +/** + * Start WiFi AP with configuration + */ +bool wifi_ap_start(const wifi_ap_config_t *config) { + if (config == NULL) { + return false; + } + // Copy fundamental fields but deep-copy password text into persistent storage. + wifi_config.ssid = config->ssid; // assume lifetime static/const or managed by caller + if (config->password) { + size_t len = strlen(config->password); + if (len >= sizeof(wifi_pass_storage)) len = sizeof(wifi_pass_storage) - 1; + memcpy(wifi_pass_storage, config->password, len); + wifi_pass_storage[len] = '\0'; + } else { + wifi_pass_storage[0] = '\0'; + } + wifi_config.password = wifi_pass_storage; + wifi_config.ip_address = config->ip_address; + wifi_config.is_running = false; + + #ifndef UNIT_TEST + if (start_access_point(wifi_config.ssid, wifi_config.password) != 0) { + print_dbg("ERROR: Failed to start WiFi AP\n"); + return false; + } + + wifi_config.is_running = true; + print_dbg("WiFi AP started: SSID=%s (192.168.4.1)\n", wifi_config.ssid); + #endif + + return true; +} + +/** + * Stop WiFi AP + */ +void wifi_ap_stop(void) { + #ifndef UNIT_TEST + stop_access_point(); + wifi_config.is_running = false; + print_dbg("WiFi AP stopped\n"); + #endif +} + +/** + * Get WiFi configuration + */ +wifi_ap_config_t* wifi_ap_get_config(void) { + return &wifi_config; +} + +// Rotate password and restart AP (synchronous). Returns false on failure; AP left stopped if restart fails. +bool wifi_ap_rotate_password(const char *new_pass) { + if (new_pass == NULL) return false; + // Update stored password text + size_t len = strlen(new_pass); + if (len >= sizeof(wifi_pass_storage)) len = sizeof(wifi_pass_storage) - 1; + memcpy(wifi_pass_storage, new_pass, len); + wifi_pass_storage[len] = '\0'; + wifi_config.password = wifi_pass_storage; + + // Reconfigure AP credentials without full deinit (smoother, safer) + if (reconfigure_access_point(wifi_config.ssid, wifi_config.password) != 0) { + print_dbg("ERROR: AP reconfiguration failed, attempting OPEN fallback\n"); + wifi_pass_storage[0] = '\0'; + wifi_config.password = wifi_pass_storage; + reconfigure_access_point(wifi_config.ssid, ""); + return false; + } + wifi_config.is_running = true; + return true; +} + +/** + * WiFi background task (runs frequently to process network events) + * + * This task is CRITICAL for CYW43 driver and lwIP stack operation. + * It must run regularly (every 50-100ms) to: + * - Process WiFi driver events + * - Handle network timeouts + * - Manage DHCP state + * - Process incoming packets + * + * Priority: 25 (just below serial task at 26) + * Sleep interval: 50ms (allows lwIP to process events regularly) + */ +void wifi_background_task(void *params) { + (void)params; + + print_dbg("WiFi background task started (priority 25, 50ms interval)\n"); + + while (true) { + #ifndef UNIT_TEST + // Sleep briefly to allow CYW43 driver and lwIP to process events + // CYW43_ARCH_THREADSAFE_BACKGROUND automatically handles the background work + // This sleep allows task switching and prevents blocking + vTaskDelay(pdMS_TO_TICKS(50)); + #else + vTaskDelay(pdMS_TO_TICKS(50)); + #endif + } +} + +/** + * HTTP server task (FreeRTOS task function) + * + * Handles HTTP server monitoring and API request processing + * + * Recommended Priority: 10 + * Recommended Stack: 2048 bytes + * + * @param params Unused task parameters + */ +void http_server_task(void *params) { + (void)params; + + print_dbg("HTTP server task started (priority 5, 100ms interval)\n"); + + // Only run if AP is configured + if (!wifi_config.is_running) { + print_dbg("HTTP server: AP not running, task exiting\n"); + vTaskDelete(NULL); // Delete self + return; + } + + while (true) { + #ifndef UNIT_TEST + // lwIP httpd is interrupt/callback-driven through recv callbacks + // This task primarily monitors server health and manages long-running requests + vTaskDelay(pdMS_TO_TICKS(100)); + #else + vTaskDelay(pdMS_TO_TICKS(100)); + #endif + } +} + +/** + * WiFi AP initialization task + * + * Starts the WiFi AP after the scheduler is running. + * This runs once and then exits. + * + * @param params Pointer to wifi_ap_config_t (or NULL to use default) + */ +void wifi_ap_init_task(void *params) { + print_dbg("WiFi AP init task started\n"); + + wifi_ap_config_t *config = (wifi_ap_config_t *)params; + if (config == NULL) { + config = &wifi_config; + } + + if (wifi_ap_start(config)) { + print_dbg("WiFi AP initialization successful\n"); + } else { + print_dbg("ERROR: WiFi AP initialization failed\n"); + } + + // Task completes after starting AP + vTaskDelete(NULL); +} + +// ============================================================================ +// AP Stability and Monitoring Functions +// ============================================================================ + +/** + * Check if WiFi AP is currently active and operational + * @return true if AP is running and healthy, false otherwise + */ +bool wifi_ap_is_active(void) { + #ifndef UNIT_TEST + // Check basic config state + if (!wifi_config.is_running) { + return false; + } + + // Could add more sophisticated health checks here: + // - Check CYW43 link status + // - Verify DHCP server is responding + // - Check for recent client activity + + return true; + #else + return wifi_config.is_running; + #endif +} + +/** + * Attempt to restart the WiFi AP + * @return true if restart was successful, false otherwise + */ +bool wifi_ap_restart(void) { + print_dbg("WiFi AP: Attempting restart...\n"); + + // Stop current AP + wifi_ap_stop(); + + // Brief delay to ensure clean shutdown + #ifndef UNIT_TEST + vTaskDelay(pdMS_TO_TICKS(1000)); + #endif + + // Restart with current configuration + bool success = wifi_ap_start(&wifi_config); + if (success) { + print_dbg("WiFi AP: Restart successful\n"); + } else { + print_dbg("WiFi AP: Restart failed\n"); + } + + return success; +} diff --git a/src/net/wifi_ap.h b/src/net/wifi_ap.h new file mode 100644 index 0000000..7593d5d --- /dev/null +++ b/src/net/wifi_ap.h @@ -0,0 +1,125 @@ +#ifndef WIFI_AP_H +#define WIFI_AP_H + +#include +#include + +/** + * WiFi AP Configuration Structure + */ +typedef struct { + const char *ssid; // WiFi SSID (e.g., "MASTR-Token") + const char *password; // WiFi password (must be >= 8 chars for WPA2) + uint32_t ip_address; // IP address in network byte order (e.g., 0xC0A80401 for 192.168.4.1) + bool is_running; // Is AP currently running +} wifi_ap_config_t; + +/** + * Initialize WiFi hardware (CYW43 driver) + * + * MUST be called BEFORE FreeRTOS scheduler starts + * Sets up: + * - CYW43 driver + * - lwIP stack + * - AP mode capability + * + * @return true if successful, false on error + */ +bool wifi_ap_init(void); + +/** + * Start WiFi Access Point with configuration + * + * Can be called at any time after wifi_ap_init() succeeds + * Sets up: + * - WiFi AP with SSID and WPA2-PSK security + * - DHCP server (192.168.4.0/24) + * - HTTP server on port 80 + * - API endpoints (/api/ping, /api/info) + * + * @param config Pointer to wifi_ap_config_t with desired SSID/password + * @return true if successful, false on error + */ +bool wifi_ap_start(const wifi_ap_config_t *config); + +/** + * Stop WiFi Access Point + * + * Shuts down: + * - WiFi driver + * - Network interfaces + * - HTTP server + * + * @return none + */ +void wifi_ap_stop(void); + +/** + * Get current WiFi configuration + * + * @return Pointer to current wifi_ap_config_t + */ +wifi_ap_config_t* wifi_ap_get_config(void); + +/** + * Rotate AP password and restart AP with WPA2-PSK security. + * If new_pass length < 8, AP will fall back to OPEN. + * Returns true on success (AP running with requested password), false if restart failed. + */ +bool wifi_ap_rotate_password(const char *new_pass); + +/** + * WiFi background task (FreeRTOS task function) + * + * CRITICAL: This task must run regularly (every 50-100ms) for: + * - CYW43 driver event processing + * - lwIP stack operation + * - DHCP server maintenance + * - Incoming packet handling + * + * Recommended Priority: 25 (just below serial task at 26) + * Recommended Stack: 2048 bytes + * + * @param params Unused task parameters + */ +void wifi_background_task(void *params); + +/** + * HTTP server task (FreeRTOS task function) + * + * Handles HTTP server monitoring and API request processing + * + * Recommended Priority: 10 + * Recommended Stack: 2048 bytes + * + * @param params Unused task parameters + */ +void http_server_task(void *params); + +/** + * WiFi AP initialization task (FreeRTOS task function) + * + * Starts the WiFi AP after the scheduler is running. + * This runs once and then exits. + * + * @param params Pointer to wifi_ap_config_t (or NULL to use default) + */ +void wifi_ap_init_task(void *params); + +// ============================================================================ +// AP Stability and Monitoring Functions +// ============================================================================ + +/** + * Check if WiFi AP is currently active and operational + * @return true if AP is running and healthy, false otherwise + */ +bool wifi_ap_is_active(void); + +/** + * Attempt to restart the WiFi AP + * @return true if restart was successful, false otherwise + */ +bool wifi_ap_restart(void); + +#endif // WIFI_AP_H