diff --git a/devdocs/decode-overrun-investigation.md b/devdocs/decode-overrun-investigation.md new file mode 100644 index 0000000..147ed9b --- /dev/null +++ b/devdocs/decode-overrun-investigation.md @@ -0,0 +1,167 @@ +# Decode Overrun Error Investigation + +## Summary + +The raftjs example dashboard is receiving an `AttributeHandler decode overrun` error when processing light sensor data from a Cog device. The firmware is sending 9 bytes of sensor data, but the registered schema expects only 8 bytes. + +## Error Details + +**Error Message:** +``` +AttributeHandler decode overrun (msgBuffer): + deviceKey=0_0 + deviceType=Cog Light Sensors + debugMsgIndex=1 + attr.n=amb0 + attr.t=>H + attrTypeSize=2 + curFieldBufIdx=45 + msgBuffer.length=46 + sampleStartIdx=37 + sampleEndIdx=46 + availableInSample=1 + availableInBuffer=1 +``` + +**Key Facts:** +- Message buffer total length: 46 bytes +- Sample data location: bytes 37-46 (9 bytes) +- Declared sample size in schema: 8 bytes (`pollRespMetadata.b=8`) +- **Mismatch: Firmware sends 9 bytes, schema expects 8 bytes** + +## Message Structure Analysis + +### Binary Message Format +The firmware sends a binary message with this structure: +1. **Timestamp** (2 bytes, big-endian) - handled separately before attribute decoding +2. **Sample length** (1 byte) - value is 9, meaning 9 bytes of sensor data follow +3. **Sensor data** (9 bytes) - the actual data being decoded + - IR sensor 0 (ir0): `5c 60` (0x5c60) + - IR sensor 1 (ir1): `00 05` (0x0005) + - IR sensor 2 (ir2): `03 00` (0x0300) + - Ambient sensor 0 (amb0): `02 57` (0x0257) + - **Extra byte**: `01` (unknown origin) + +### Expected vs Actual Schema + +**Schema Definition (from firmware):** +```json +{ + "name": "Cog Light Sensors", + "desc": "Light Sensors", + "type": "RoboCogLightV1", + "resp": { + "b": 8, + "a": [ + {"n": "ir0", "t": ">H", "u": "", "r": [0, 4095], "d": 1, "f": ".0f"}, + {"n": "ir1", "t": ">H", "u": "", "r": [0, 4095], "d": 1, "f": ".0f"}, + {"n": "ir2", "t": ">H", "u": "", "r": [918, 2259], "d": 1, "f": ".0f"}, + {"n": "amb0", "t": ">H", "u": "L", "r": [0, 4095], "d": 1, "f": ".0f"} + ] + } +} +``` + +**Expected data (based on schema):** +- ir0: 2 bytes +- ir1: 2 bytes +- ir2: 2 bytes +- amb0: 2 bytes +- **Total: 8 bytes** + +**Actual data received:** +- All 4 fields as expected (8 bytes) +- **Plus 1 extra byte** = 9 bytes total + +## Root Cause + +The firmware's `DeviceLightSensors::formDeviceDataResponse()` function in `components/DeviceLightSensors/DeviceLightSensors.cpp` is generating sensor data, and there is a mismatch between: + +1. **What the firmware sends**: 9 bytes of sensor data (timestamp + data in the binary message) +2. **What the schema declares**: 8 bytes of sensor data (`"b": 8`) + +The schema is generated dynamically by the firmware in `getDeviceTypeRecord()` based on the number of configured sensors: +- 3 IR sensors × 2 bytes = 6 bytes +- 1 ambient sensor × 2 bytes = 2 bytes +- **Total: 8 bytes** (correct calculation) + +But the actual `formDeviceDataResponse()` is sending 9 bytes. + +## Investigation Points + +### 1. Extra Byte Origin +The mysterious `01` byte at the end needs to be identified: +- Is it padding? +- Is it a sample count or indicator? +- Is it an uninitialized buffer value? +- Was it added in a recent firmware change? + +**Check:** +- `git log -p components/DeviceLightSensors/DeviceLightSensors.cpp` around `formDeviceDataResponse()` +- Look for recent changes that add bytes (new sensor data, flags, etc.) +- Compare the byte count calculation in `getDeviceTypeRecord()` with actual `formDeviceDataResponse()` logic + +### 2. Binary Message Encoding +The binary message encoding happens in `RaftDevice::genBinaryDataMsg()` (in RaftCore). Verify: +- Is the framework adding extra metadata or padding? +- Are sample boundaries being correctly calculated? +- Is there a mismatch between the sample length byte and actual data written? + +**Check:** +- `RaftDevice::genBinaryDataMsg()` implementation +- Recent changes to binary message encoding +- Sample length byte calculation + +### 3. Configuration/Build State +- Check if the firmware was built with debug flags that add extra bytes +- Verify that sensor configuration hasn't changed (more sensors added?) +- Check if conditional compilation (#ifdef) is affecting byte counts + +## raftjs Side + +The raftjs library correctly detects the mismatch through the diagnostic context added in recent commits: +- `AttrDecodeDiagContext` interface tracks sample boundaries +- `RaftAttributeHandler.processMsgAttribute()` bounds-checks against these boundaries +- `RaftDeviceManager` passes diagnostic context with `sampleStartIdx` and `sampleEndIdx` + +This is working as intended—the error indicates a real firmware/schema mismatch, not a bug in raftjs. + +## Recommended Fixes + +### Short Term (Workaround) +Update the schema to match reality: +```json +"resp": { + "b": 9, // Changed from 8 to 9 + ... +} +``` +This will suppress the error but doesn't fix the root cause. + +### Long Term (Proper Fix) +1. **Identify the source** of the extra byte in `formDeviceDataResponse()` +2. **Either:** + - Remove the extra byte if it's unintended + - Properly account for it in the schema and sensor processing if it's intentional +3. **Update documentation** if there's a reason for the extra byte +4. **Add unit tests** to prevent this mismatch in future changes + +## Related Code Locations + +**Firmware:** +- Schema definition: `RoboticalCogFW/components/DeviceLightSensors/DeviceLightSensors.cpp` - `getDeviceTypeRecord()` +- Data encoding: `RoboticalCogFW/components/DeviceLightSensors/DeviceLightSensors.cpp` - `formDeviceDataResponse()` +- Binary message: `RoboticalCogFW/raftdevlibs/RaftCore/components/core/RaftDevice/RaftDevice.cpp` - `genBinaryDataMsg()` + +**raftjs:** +- Attribute handler: `raftjs/src/RaftAttributeHandler.ts` - `processMsgAttribute()` +- Device manager: `raftjs/src/RaftDeviceManager.ts` - `handleClientMsgBinary()` +- Diagnostic logging: Added in recent commit with `AttrDecodeDiagContext` + +## Next Steps + +1. Run `git log -p` on DeviceLightSensors to find when the extra byte was introduced +2. Check the firmware build/compile to ensure no debug additions +3. Examine `formDeviceDataResponse()` line-by-line against sensor configuration +4. Test with a debugger to verify what bytes are actually being transmitted +5. Once identified, decide whether to remove the byte or update the schema diff --git a/devdocs/message-panel-design.md b/devdocs/message-panel-design.md new file mode 100644 index 0000000..4d07725 --- /dev/null +++ b/devdocs/message-panel-design.md @@ -0,0 +1,320 @@ +# Dashboard Message Panel — Cog-to-Cog IR Comms + +## Purpose + +Add a **Message Panel** to the RaftJS example dashboard so a developer can exercise the +cog-to-cog IR communications feature described in the Cog firmware design document +(`RoboticalCogFW/devdocs/cog-to-cog-ir-comms-design.md`). + +The panel sits alongside the existing Command Panel and provides: + +- A text box to enter an ASCII message. +- A drop-down list of destinations to send the message to. Initially the only entry is the + IR channel of a Cog (side 2, the validated comms axis). +- A button to send the message. +- A scrolling log of messages received over the same IR channel by the connected device. + +**No code has been changed yet.** This document records the investigation and describes the +work required to implement the panel. + +## Background + +### How the firmware exposes IR comms + +The Cog firmware feature is implemented by the `CogIRComms` SysMod and exposed through the +standard RaftCore REST endpoint mechanism, so the same endpoints work over BLE RICREST, +WebSocket, and serial. The relevant endpoints (from the firmware design doc, "Useful REST +commands" and the 2026-05-15 validation section) are: + +| Endpoint | Purpose | +| --- | --- | +| `ircomms/status` | Returns state plus rx/tx counters and config. Use it to confirm the feature is built/enabled. | +| `ircomms/tx?side=2&type=<0-15>&seq=<0-15>&payload=<0-255>` | Send a tiny diagnostic packet (single payload byte). | +| `ircomms/send?side=2&type=<0-15>&seq=<0-15>&hex=` | Send a framed datagram with an arbitrary payload (up to 80 bytes). **This is the endpoint the Message Panel should use.** | +| `ircomms/rx?pop=1` | Pop the oldest received tiny packet. | +| `ircomms/rx?popFrame=1` | Pop the oldest received framed datagram. | +| `ircomms/rx?clear=1` | Clear the receive queue. | +| `ircomms/cfg?frameMarkUs=&frameSpaceUs=` | Runtime frame-timing tweak (not needed for v1). | + +The framed payload is the right fit for an ASCII message: `frameMaxPayloadBytes` is 80, i.e. +160 hex characters, validated on hardware up to 48 bytes with overhead headroom. + +Side 2 (`IRTX2`/`IRRX2`, mux channel 2) is the only validated bidirectional axis; side 1 did +not couple in the tested orientations. The destination drop-down should therefore default to +side 2. + +### How the firmware notifies of received messages + +When `rxReportEnable` is true (it is in the default `Cog1` systype config), each CRC-valid +received message also produces a **RICREST report frame** sent through `SysManager`. For the +tiny-packet diagnostic the report looks like: + +```json +{"msgType":"ircomms","msgName":"rx","rxMs":11134,"type":7,"seq":8,"payload":55,"crc":55883} +``` + +The firmware design doc's "Receive Notification Recommendation" section proposes a richer +report for framed messages with `source`, `type`, and a `payload` field. The exact shape of +the report emitted for **framed** `ircomms/send` datagrams is not pinned down in the firmware +doc — see [Open questions](#open-questions-firmware-coordination). + +### How RaftJS sends and receives + +The dashboard talks to one device at a time through a single `RaftConnector` instance +(`ConnManager.getInstance().getConnector()`). + +**Sending** — `CommandPanel.tsx` already shows the pattern: + +```ts +connManager.getConnector().sendRICRESTMsg(commandName, params) +``` + +`sendRICRESTMsg(commandName, params)` (in `src/RaftConnector.ts:412`) builds a query string +from `params`, URL-encodes each value, appends it to `commandName`, sends it as a RICREST URL +message, and resolves to the parsed JSON response (typed `RaftOKFail`, but the full JSON of +the device response is returned). So a framed send is simply: + +```ts +const resp = await connManager.getConnector().sendRICRESTMsg('ircomms/send', { + side: 2, type: 1, seq: seqNo, hex: asciiToHex(messageText), +}); +// resp e.g. { rslt: "ok", seq: 42, queued: 1 } +``` + +Polling the receive queue uses the same call: + +```ts +const resp = await connManager.getConnector().sendRICRESTMsg('ircomms/rx', { popFrame: 1 }); +``` + +**Receiving reports** — `RaftMsgHandler` decodes `MSG_TYPE_REPORT` frames, parses the JSON, +and dispatches it to every callback registered via `reportMsgCallbacksSet` +(`src/RaftMsgHandler.ts:144`). The connector exposes the handler publicly: + +```ts +const handler = connManager.getConnector().getRaftMsgHandler(); // RaftConnector.ts:220 +handler.reportMsgCallbacksSet('messagePanel', async (report) => { ... }); +// on unmount: +handler.reportMsgCallbacksDelete('messagePanel'); +``` + +The callback receives a `RaftReportMsg` object (exported from `src/main.ts` via +`export * from './RaftTypes'`). Note `RaftReportMsg` only declares a fixed set of fields; +the `ircomms` report carries extra fields (`type`, `seq`, `payload`, `crc`, `rxMs`, …) that +must be read by extending/casting the type. + +The connector already registers an internal `"eventHandler"` report callback for `sysevent` +shutdown handling — adding a second `"messagePanel"` callback is independent and supported. + +## Proposed design + +### New component: `MessagePanel.tsx` + +Add `examples/dashboard/src/MessagePanel.tsx`, structured like `CommandPanel.tsx`: + +- A two-column `info-box` (reuse the existing `info-boxes` / `info-box` / `info-columns` + layout classes). +- **Left column** — message composer: ASCII text input, destination `