Files
SC-F001/logtool/PLAN.md
Thaddeus Hughes 6ce5dae3a4 logtool plan
2026-03-04 14:17:14 -06:00

173 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Logtool Plan
Python tool for viewing SC-F001 flash logs. Supports CLI table output, matplotlib GUI, and both file and HTTP sources.
---
## On-disk Entry Format
Each entry in flash:
```
[len u8] [payload (len-1 bytes)] [type u8]
```
Total bytes per entry = `len + 1`.
`log_write_blocking` does `len++` before writing, so `len` stored on disk = `payload_size + 1`, and `len - 1 = payload_size`. The write is correct and writes all payload bytes.
Padding bytes (0x00 = sector padding, 0xFF = erased flash) cause the reader to skip to the next sector boundary.
### Entry Types
| Type | Name | Payload (bytes) | Stored len | Total on disk |
|-------|----------|-----------------|------------|---------------|
| 012 | FSM log | 39 | 40 | 41 |
| 100 | BAT log | 12 | 13 | 14 |
| 101 | CRASH | 9 | 10 | 11 |
All values **little-endian** (named `be_*` in source but no byte-swap — the names are misleading legacy).
### FSM payload layout (39 bytes)
```
[0:8] ts_ms u64 — ms since epoch (local-as-UTC, no timezone)
[8:12] bat_V f32 — battery voltage
[12:16] drive_A f32 — drive bridge raw current
[16:20] jack_A f32 — jack bridge raw current
[20:24] aux_A f32 — aux bridge raw current
[24:26] counter i16 — drive encoder counter (SENSOR_DRIVE)
[26] sensors u8 — bits[3:0]=stable state, bits[7:4]=raw state
bit position: SAFETY=0, JACK=1, DRIVE=2, AUX2=3
[27:31] drive_heat f32 — e-fuse thermal accumulator, drive bridge
[31:35] jack_heat f32 — e-fuse thermal accumulator, jack bridge
[35:39] aux_heat f32 — e-fuse thermal accumulator, aux bridge
```
### BAT payload layout (12 bytes)
```
[0:8] ts_ms u64
[8:12] bat_V f32
```
### CRASH payload layout (9 bytes)
```
[0:8] ts_ms u64
[8] reset_reason u8 — esp_reset_reason_t value
```
### FSM state names (from control_fsm.h — single source of truth)
```
0 = STATE_IDLE
1 = STATE_MOVE_START_DELAY
2 = STATE_JACK_UP_START
3 = STATE_JACK_UP
4 = STATE_DRIVE_START_DELAY
5 = STATE_DRIVE
6 = STATE_DRIVE_END_DELAY
7 = STATE_JACK_DOWN
8 = STATE_UNDO_JACK_START
9 = STATE_CALIBRATE_JACK_DELAY
10 = STATE_CALIBRATE_JACK_MOVE
11 = STATE_CALIBRATE_DRIVE_DELAY
12 = STATE_CALIBRATE_DRIVE_MOVE
```
---
## HTTP /log Response Format
```
[4B json_len BE] [json_len bytes JSON] [4B tail_offset BE] [4B head_offset BE] [raw binary]
```
The raw binary is already linearized (tail→head wraparound handled server-side). Parse it sequentially as a stream of entries.
**Streaming**: GET once to get the full log + current head, then POST with the current head as the tail to get only new entries. Repeat on a timer.
---
## File Format Auto-detection
When opening a `.bin` file:
1. Read first 4 bytes as BE uint32 → `candidate_json_len`
2. If `candidate_json_len < 8192` AND byte at offset 4 is `{` → HTTP response format (parse header then binary)
3. Otherwise → raw flash binary (skip header, parse binary directly from offset 0)
---
## Timestamps
All timestamps are `ms since epoch` in **local-as-UTC** encoding: the device stores the user's local time directly as a Unix timestamp (no timezone offset). Display as-is using `datetime.utcfromtimestamp(ts_ms / 1000)` — no conversion needed.
---
## Implementation Plan
### File layout
```
logtool/
├── PLAN.md ← this file
├── logtool.py ← main entry point (CLI + dispatch)
├── parser.py ← binary framing + struct unpack; reads state names from control_fsm.h
├── source.py ← file reader and HTTP fetcher (GET full, POST incremental)
├── cli_view.py ← tabular terminal output (tabulate)
└── gui_view.py ← matplotlib time-series charts
```
### `parser.py`
- `load_fsm_states(fsm_h_path) -> dict[int, str]` — parses `control_fsm.h` to extract the `fsm_state_t` enum. Falls back to hardcoded dict if file not found.
- `parse_entries(data: bytes) -> list[dict]` — iterates raw binary, skips padding (0x00/0xFF), unpacks each entry by type
- `parse_response(blob: bytes) -> (dict, int, int, list[dict])` — parses full HTTP/file blob: json metadata, tail, head, entries
### `source.py`
- `read_file(path: str) -> bytes`
- `fetch_full(url: str) -> bytes` — HTTP GET /log
- `fetch_incremental(url: str, tail: int) -> (bytes, int)` — POST /log with tail as body, returns raw binary + new head
- `stream(url: str, callback, interval_s=2.0)` — poll loop: GET once for full log, then POST repeatedly; calls `callback(new_entries, new_head)` each iteration
### `cli_view.py`
- `print_table(entries: list[dict], fsm_states: dict)` — tabulate output
- Columns: `time`, `type`, `state`, `bat_V`, `drive_A`, `jack_A`, `aux_A`, `counter`, `sensors_stable`, `sensors_raw`
- BAT/CRASH rows show only relevant columns; FSM-only columns shown as `—`
- CRASH rows prefixed with `***`
- `print_summary(entries)` — time range, count by type, min/max battery
### `gui_view.py`
- `show_plots(entries: list[dict])` — matplotlib figure with subplots:
1. Battery voltage over time
2. Currents (drive, jack, aux) over time
3. FSM state over time (step/stairs plot)
4. Thermal accumulators (drive_heat, jack_heat, aux_heat)
- CRASH events: vertical red dashed lines across all subplots
- `live_plot(url: str, interval_s=2.0)``FuncAnimation` + streaming source, updates all axes
### `logtool.py` (CLI)
```
Usage:
logtool.py <file.bin> # file → CLI table
logtool.py <file.bin> --gui # file → GUI
logtool.py http://<ip>/log # fetch once → CLI table
logtool.py http://<ip>/log --gui # fetch once → GUI
logtool.py http://<ip>/log --stream # live CLI (append rows)
logtool.py http://<ip>/log --stream --gui # live GUI
logtool.py <source> --type fsm|bat|crash # filter by entry type
logtool.py <source> --tail <offset> # start from flash offset
logtool.py <source> --fw ../main # path to firmware source (for state names)
```
### Dependencies (`requirements.txt`)
```
requests
matplotlib
tabulate
```
Python 3.8+.
---
## Resolved Questions
1. **Entry size**: No bug. `len++` in `log_write_blocking` makes `len-1 = LOGSIZE = 39`, writing all payload bytes correctly.
2. **Streaming**: GET for initial full fetch, then POST with current head as tail for incremental polling.
3. **File format**: Auto-detect via 4-byte json_len + `{` check.
4. **Timestamps**: local-as-UTC, display with `datetime.utcfromtimestamp()`, no conversion.
5. **FSM state names**: Parse from `control_fsm.h` at startup (`--fw` flag or default `../main`). Fallback to hardcoded dict.
6. **tabulate**: Acceptable dependency.