From 80a1e8c3bde6bc02a4ac7e338b358e8324d8ab1a Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Fri, 6 Feb 2026 14:46:07 +0100 Subject: [PATCH 1/5] Add K-boot flashing analysis documentation Document existing APIs and flows for implementing autonomous module firmware flashing: - configuration-buffer.md: Config buffer APIs and memory layout - usb-communication.md: USB command infrastructure - module-communication.md: I2C/UART slave scheduler and module discovery - kboot-protocol.md: K-boot protocol details for porting from Agent - README.md: Overview and proposed implementation approach This is the analysis phase for moving K-boot protocol from Agent to right half firmware. Co-Authored-By: Claude Opus 4.5 --- doc-dev/other/flashing/README.md | 103 ++++++++ .../other/flashing/configuration-buffer.md | 141 +++++++++++ doc-dev/other/flashing/kboot-protocol.md | 194 +++++++++++++++ .../other/flashing/module-communication.md | 222 ++++++++++++++++++ doc-dev/other/flashing/usb-communication.md | 178 ++++++++++++++ 5 files changed, 838 insertions(+) create mode 100644 doc-dev/other/flashing/README.md create mode 100644 doc-dev/other/flashing/configuration-buffer.md create mode 100644 doc-dev/other/flashing/kboot-protocol.md create mode 100644 doc-dev/other/flashing/module-communication.md create mode 100644 doc-dev/other/flashing/usb-communication.md diff --git a/doc-dev/other/flashing/README.md b/doc-dev/other/flashing/README.md new file mode 100644 index 000000000..3d8579c3c --- /dev/null +++ b/doc-dev/other/flashing/README.md @@ -0,0 +1,103 @@ +# Firmware Flashing Implementation + +This directory contains documentation for implementing autonomous module firmware flashing on the UHK right half. + +## Goal + +Move K-boot protocol implementation from the Agent (desktop tool) to the right half firmware, enabling autonomous module firmware updates without Agent involvement in the flashing process. + +## Documentation Files + +| File | Description | +|------|-------------| +| [configuration-buffer.md](configuration-buffer.md) | Config buffer APIs and memory layout | +| [usb-communication.md](usb-communication.md) | USB command infrastructure | +| [module-communication.md](module-communication.md) | I2C/UART slave scheduler and module discovery | +| [kboot-protocol.md](kboot-protocol.md) | K-boot protocol details for porting | + +## Current Architecture + +``` +┌─────────────┐ USB ┌─────────────┐ I2C ┌─────────────┐ +│ Agent │ ────────────────▶│ Right Half │ ───────────────▶│ Module │ +│ (Desktop) │ │ (Buspal) │ │ (Bootloader)│ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ + │ K-boot protocol │ Forward packets │ + │ implementation │ transparently │ + │ │ │ +``` + +**Problem**: K-boot protocol logic is in the Agent. Right half only forwards packets. + +## Target Architecture + +``` +┌─────────────┐ USB ┌─────────────┐ I2C ┌─────────────┐ +│ Agent │ ────────────────▶│ Right Half │ ───────────────▶│ Module │ +│ (Desktop) │ │ (K-boot) │ │ (Bootloader)│ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ + │ Upload firmware │ K-boot protocol │ + │ binary only │ implementation │ + │ │ │ +``` + +**Goal**: Agent uploads firmware binary. Right half handles K-boot protocol. + +## Two-Phase Implementation + +### Phase 1: USB Transfer to Right Half + +1. Analyze existing configuration buffer APIs (see [configuration-buffer.md](configuration-buffer.md)) +2. Design firmware upload protocol +3. Implement chunked USB transfer (similar to config write) +4. Store firmware in staging buffer or stream directly + +### Phase 2: Right Half to Module + +1. Port K-boot protocol from Agent (see [kboot-protocol.md](kboot-protocol.md)) +2. Implement I2C transport layer +3. Implement flashing state machine: + - Reboot module to bootloader + - Erase flash + - Write firmware chunks + - Verify and reset + +## Proposed USB Commands + +| Command | ID | Description | +|---------|-----|-------------| +| StartModuleFirmwareUpload | 0x20 | Begin firmware upload (module ID, size) | +| WriteModuleFirmwareChunk | 0x21 | Write firmware data chunk | +| FlashModule | 0x22 | Start flashing (uses uploaded firmware) | +| GetFlashingStatus | 0x23 | Query flashing progress | +| AbortFlashing | 0x24 | Cancel flashing operation | + +## Key Challenges + +1. **Memory**: Module firmware (~40-60 KB) exceeds config buffer (~32 KB) + - Solution: Stream directly to module, or use UHK80 external flash + +2. **Timing**: I2C transfers while maintaining keyboard responsiveness + - Solution: Use slave scheduler, low priority for flashing + +3. **Error Recovery**: Handle I2C failures, power loss during flash + - Solution: Verify writes, implement retry logic + +4. **State Management**: Track flashing progress across multiple USB commands + - Solution: Implement state machine with persistent state + +## Technical Constraints + +- Firmware must fit in available RAM or be streamed +- K-boot protocol requires both I2C and UART support +- Must handle communication failures gracefully +- Module must remain functional if flashing fails + +## Success Criteria + +- [ ] Firmware can be uploaded to right half via USB +- [ ] Right half can autonomously flash connected module +- [ ] No dependency on Agent for K-boot protocol execution +- [ ] Agent only uploads firmware binary, doesn't manage flashing diff --git a/doc-dev/other/flashing/configuration-buffer.md b/doc-dev/other/flashing/configuration-buffer.md new file mode 100644 index 000000000..e9d34a18d --- /dev/null +++ b/doc-dev/other/flashing/configuration-buffer.md @@ -0,0 +1,141 @@ +# Configuration Buffer APIs + +This document describes the user configuration buffer system that could potentially be used for firmware image storage during module flashing. + +## Buffer Architecture + +The firmware uses a **three-buffer system** for configuration management: + +```c +typedef enum { + ConfigBufferId_HardwareConfig, + ConfigBufferId_StagingUserConfig, + ConfigBufferId_ValidatedUserConfig, +} config_buffer_id_t; +``` + +### Memory Sizes + +| Platform | Hardware Config | User Config | +|----------|----------------|-------------| +| UHK60 | 64 bytes | 32,704 bytes | +| UHK80 | 64 bytes | 32,768 bytes | + +**Note**: Module firmware images are typically 40-60 KB for UHK60 modules, which exceeds the current user config buffer capacity. Alternative storage strategies may be needed. + +## Core Data Structures + +### Buffer Structure + +From `right/src/config_parser/basic_types.h`: + +```c +typedef struct { + bool isValid; // Flag indicating if buffer contains valid configuration + uint8_t *buffer; // Pointer to raw byte buffer + uint16_t offset; // Current read/write position for parsing +} config_buffer_t; +``` + +### Buffer Instances + +From `right/src/config_parser/config_globals.c`: + +```c +config_buffer_t HardwareConfigBuffer; +config_buffer_t StagingUserConfigBuffer; +config_buffer_t ValidatedUserConfigBuffer; +``` + +## Key Functions + +### Buffer Access + +```c +// Get buffer descriptor from ID +config_buffer_t* ConfigBufferIdToConfigBuffer(config_buffer_id_t configBufferId); + +// Get buffer size +uint16_t ConfigBufferIdToBufferSize(config_buffer_id_t configBufferId); + +// Validate buffer ID +bool IsConfigBufferIdValid(config_buffer_id_t configBufferId); +``` + +### Parsing Primitives + +From `right/src/config_parser/basic_types.h`: + +```c +uint8_t ReadUInt8(config_buffer_t *buffer); +uint16_t ReadUInt16(config_buffer_t *buffer); +uint32_t ReadUInt32(config_buffer_t *buffer); +``` + +## USB Read/Write APIs + +### Read Config Command + +**File**: `right/src/usb_commands/usb_command_read_config.c` + +**Protocol**: +- Byte 0: Command (0x04) +- Byte 1: Config buffer ID +- Byte 2: Length to read (max 62 bytes) +- Bytes 3-4: Offset (little-endian uint16) +- Response: [Status(1)] [Data(length)] + +### Write Config Command + +**File**: `right/src/usb_commands/usb_command_write_config.c` + +**Protocol**: +- Byte 0: Command (0x05 or 0x06) +- Byte 1: Length to write +- Bytes 2-3: Offset (little-endian uint16) +- Bytes 4+: Data to write (max 59 bytes per packet) + +## Storage Operations + +### UHK60 - EEPROM + +**File**: `right/src/eeprom.c` + +```c +status_t EEPROM_LaunchTransfer(storage_operation_t operation, + config_buffer_id_t configBufferId, + void (*successCallback)); +``` + +- I2C-based EEPROM at 100 kHz (I2C1 at 1 MHz) +- 64-byte page writes +- Sequential page writes with callbacks + +### UHK80 - Flash + +**File**: `device/src/flash.c` + +```c +uint8_t Flash_LaunchTransfer(storage_operation_t operation, + config_buffer_id_t configBufferId, + void (*successCallback)); +``` + +- Flash area API with 4K alignment +- Erase before write required + +## Implications for Firmware Storage + +Current configuration buffer capacity (~32 KB) is insufficient for typical module firmware images (~40-60 KB). Options include: + +1. **Streaming approach**: Transfer firmware in chunks directly to module without storing in RAM +2. **External storage**: Use separate flash region on UHK80 +3. **Multiple transfers**: Split firmware and transfer in multiple sessions + +## Key Source Files + +- `right/src/config_manager.h` - Runtime config struct +- `right/src/config_parser/config_globals.h` - Buffer types & IDs +- `right/src/config_parser/basic_types.h` - Parsing primitives +- `right/src/eeprom.c` - UHK60 storage +- `device/src/flash.c` - UHK80 storage diff --git a/doc-dev/other/flashing/kboot-protocol.md b/doc-dev/other/flashing/kboot-protocol.md new file mode 100644 index 000000000..a64bc234a --- /dev/null +++ b/doc-dev/other/flashing/kboot-protocol.md @@ -0,0 +1,194 @@ +# K-Boot Protocol + +This document describes the K-boot bootloader protocol used for flashing module firmware. The protocol is currently implemented in the Agent (TypeScript) and needs to be ported to the right half firmware. + +## Protocol Overview + +K-boot is NXP's bootloader protocol for Kinetis MCUs. The UHK uses it for: +- Direct USB flashing of the right half (KL04 bootloader) +- I2C-forwarded flashing of modules via Buspal mode + +## Command IDs + +From `lib/agent/packages/kboot/src/enums/commands.ts`: + +| ID | Command | Description | +|----|---------|-------------| +| 0x01 | FlashEraseAll | Erase entire flash | +| 0x02 | FlashEraseRegion | Erase region (start + count) | +| 0x03 | ReadMemory | Read from address | +| 0x04 | WriteMemory | Write to address (has data phase) | +| 0x05 | FillMemory | Fill memory region | +| 0x06 | FlashSecurityDisable | Unlock flash (8-byte key) | +| 0x07 | GetProperty | Query bootloader property | +| 0x0B | Reset | Reset to firmware | +| 0x0C | SetProperty | Set bootloader property | +| 0x0D | FlashEraseAllUnsecure | Erase all (unsecured) | +| 0xC1 | ConfigureI2c | Configure I2C for buspal | + +## Packet Structure + +### Command Packet (32 bytes) + +``` +Header (4 bytes): + [0] = 1 // Channel (always 1 for commands) + [1] = 0 // Reserved + [2-3] = Payload length // Little-endian 16-bit + +Payload (variable): + [0] = Command ID + [1] = HasDataPhase flag // 1 if data follows, 0 otherwise + [2] = 0 // Reserved + [3] = Param count // Number of 4-byte parameters + [4+] = Parameters // 32-bit little-endian each + +Padding: Fill to 32 bytes with zeros +``` + +### Response Packet + +``` +Header (4 bytes): + [0] = 3 // Channel (always 3 for response) + [1] = Reserved + [2-3] = Response data length + +Response Data: + [4] = ResponseTag // 0xA0=generic, 0xA3=readMem, 0xA7=property + [5-7] = Reserved + [8-10] = ResponseCode // 24-bit little-endian status + [11+] = Response-specific data +``` + +### Data Packet (WriteMemory data phase) + +``` +[0] = 2 // Channel 2 (data) +[1] = 0 +[2] = Data chunk length // 1-28 bytes typically +[3] = 0 +[4+] = Actual firmware data +``` + +## Response Tags + +| Tag | Description | +|-----|-------------| +| 0xA0 | Generic response | +| 0xA3 | ReadMemory response | +| 0xA7 | Property response | +| 0xAF | FlashReadOnce response | + +## Response Codes + +| Code | Description | +|------|-------------| +| 0 | Success | +| 1 | Fail | +| 2 | ReadOnly | +| 3 | OutOfRange | +| 4 | InvalidArgument | +| 100-106 | Flash driver errors | +| 200-202 | I2C driver errors | +| 10000-10005 | Bootloader errors | + +## Properties + +| ID | Property | +|----|----------| +| 0x01 | BootloaderVersion | +| 0x03 | FlashStartAddress | +| 0x04 | FlashSize | +| 0x05 | FlashSectorSize | +| 0x0B | MaxPacketSize | +| 0x0E | RAMStartAddress | +| 0x0F | RAMSize | +| 0x11 | FlashSecurityState | + +## Flashing State Machine + +### Right Half (Direct USB) + +1. Reenumerate device into bootloader mode +2. Create KBoot USB peripheral instance +3. `flashSecurityDisable([0x01-0x08])` - Unlock flash +4. `flashEraseRegion(0xC000, 475136)` - Erase application region +5. Read firmware from .hex file +6. `writeMemory(startAddress, data)` - Write in chunks +7. `reset()` - Reboot to firmware + +### Module (I2C via Buspal) + +1. Reenumerate to NormalKeyboard mode +2. Send ping to module via I2C (100 retries) +3. Send jump-to-bootloader to module +4. Poll CurrentKbootCommand until idle +5. Reenumerate into Buspal mode +6. `configureI2c(moduleAddress, speed=64)` +7. `flashEraseAllUnsecure()` +8. Read firmware from binary file +9. `writeMemory(0, data)` +10. `reset()` +11. Reenumerate back to normal keyboard +12. Send reset command to module +13. Send idle command to module + +## Module I2C Addresses (Buspal) + +From `lib/agent/packages/uhk-common`: + +| Module | Address | +|--------|---------| +| Left Half | 0x40 | +| Key Cluster Left | 0x41 | +| Trackball Right | 0x42 | +| Trackpoint Right | 0x43 | +| Touchpad Right | 0x44 | + +## WriteMemory Two-Phase Protocol + +1. **Command phase**: Send WriteMemory with address and total size +2. **Data phase**: Send data in 32-byte HID packets (channel 2) +3. **Response phase**: Receive confirmation after all data sent + +## Timeout Handling + +- Response timeout: 2000 ms +- USB read timeout: 1000 ms +- Buspal connection retries: 30 seconds with 2-second intervals + +## Agent Implementation Files + +| File | Purpose | +|------|---------| +| `lib/agent/packages/kboot/src/kboot.ts` | Main KBoot class | +| `lib/agent/packages/kboot/src/usb-peripheral.ts` | USB peripheral | +| `lib/agent/packages/kboot/src/util/usb/encode-command-option.ts` | Command encoding | +| `lib/agent/packages/kboot/src/util/usb/decode-command-response.ts` | Response parsing | +| `lib/agent/packages/uhk-usb/src/uhk-operations.ts` | Firmware update workflows | + +## Porting Considerations + +### For Firmware Implementation + +1. **Packet Encoding**: Implement 32-byte command packet builder +2. **Response Parsing**: Handle 0xA0/0xA3/0xA7 response tags +3. **Data Phase**: Support multi-packet data transfer +4. **I2C Transport**: Replace USB with I2C for module communication +5. **State Machine**: Implement erase → write → reset sequence +6. **Error Handling**: Handle I2C timeouts and retry logic + +### Memory Requirements + +- Command buffer: 32 bytes +- Response buffer: 32 bytes +- Data chunk buffer: 32 bytes +- Firmware staging: Depends on approach (streaming vs buffered) + +### Key Differences from Agent + +1. No USB layer (direct I2C to module bootloader) +2. No async/await (use callbacks or blocking I2C) +3. Limited RAM (streaming approach preferred) +4. CRC verification may be needed for I2C reliability diff --git a/doc-dev/other/flashing/module-communication.md b/doc-dev/other/flashing/module-communication.md new file mode 100644 index 000000000..4564a8546 --- /dev/null +++ b/doc-dev/other/flashing/module-communication.md @@ -0,0 +1,222 @@ +# Module Communication + +This document describes how the right half communicates with modules (key cluster, trackball, trackpad) over I2C and UART. + +## I2C Addresses + +From `shared/i2c_addresses.h`: + +| Module | Firmware Address | Bootloader Address | +|--------|-----------------|-------------------| +| Left Half | 0x08 | 0x10 | +| Left Module | 0x18 | 0x20 | +| Right Module | 0x28 | 0x30 | +| Touchpad | 0x2D | 0x6D | + +## I2C API (UHK60) + +**File**: `right/src/i2c.c` + +```c +// Non-blocking I2C operations +I2cAsyncWrite(uint8_t i2cAddress, uint8_t *data, size_t dataSize); +I2cAsyncRead(uint8_t i2cAddress, uint8_t *data, size_t dataSize); + +// Message-based operations with CRC16 +I2cAsyncWriteMessage(uint8_t i2cAddress, i2c_message_t *message); +I2cAsyncReadMessage(uint8_t i2cAddress, i2c_message_t *message); +``` + +**Hardware Configuration**: +- Main bus: I2C0 at 100 kHz (30 kHz for bootloader) +- EEPROM bus: I2C1 at 1 MHz + +## UART API (UHK80) + +**File**: `device/src/keyboard/uart_modules.c` + +- Runs in separate kernel thread +- Semaphore-based synchronization +- Configurable timeout (MODULE_CONNECTION_TIMEOUT) + +## Slave Scheduler + +**File**: `right/src/slave_scheduler.c` + +The slave scheduler is a **round-robin, interrupt-driven I2C master** managing all module communication. + +### Slave Types + +```c +typedef enum { + SlaveId_LeftKeyboardHalf, + SlaveId_LeftModule, + SlaveId_RightModule, + SlaveId_RightTouchpad, + SlaveId_RightLedDriver, + SlaveId_LeftLedDriver, + SlaveId_ModuleLeftLedDriver, + SlaveId_KbootDriver, +} slave_id_t; +``` + +### Slave Data Structure + +```c +typedef struct { + uint8_t perDriverId; + slave_init_t *init; + slave_update_t *update; + slave_disconnect_t *disconnect; + slave_connect_t *connect; + bool isConnected; + status_t previousStatus; +} uhk_slave_t; + +extern uhk_slave_t Slaves[SLAVE_COUNT]; // 8 slaves +``` + +### Scheduling Flow + +1. `slaveSchedulerCallback()` - I2C interrupt handler +2. Finalize previous transfer, check for errors +3. Update connection status (connect/disconnect callbacks) +4. Try next slave(s) in round-robin order +5. Call `slave->update()` to get next transfer +6. Issue scheduled I2C transfer + +## Module Discovery Protocol + +**File**: `right/src/slave_drivers/uhk_module_driver.c` + +Discovery phases executed in sequence: + +1. **Sync Request**: Validate "SYNC" string +2. **Protocol Version**: Get 3-byte version +3. **Firmware Version**: Get 3-byte version +4. **Module ID**: Identify module type +5. **Key Count**: Number of keys +6. **Pointer Count**: Number of pointer inputs +7. **Git Tag**: Version tag (protocol v4.2+) +8. **Git Repo**: Repository name +9. **Firmware Checksum**: MD5 checksum +10. **Update Loop**: Periodic key state polling + +### Module State Structure + +```c +typedef struct { + uint8_t moduleId; + version_t moduleProtocolVersion; + version_t firmwareVersion; + uhk_module_phase_t phase; + i2c_message_t rxMessage; + uint8_t firmwareI2cAddress; + uint8_t bootloaderI2cAddress; + uint8_t keyCount; + uint8_t pointerCount; + char gitTag[MAX_STRING_PROPERTY]; + char firmwareChecksum[MD5_CHECKSUM]; +} uhk_module_state_t; +``` + +## Slave Protocol + +**File**: `shared/slave_protocol.h` + +### Message Format + +```c +typedef struct { + uint8_t length; + uint16_t crc; + uint8_t data[SLAVE_PROTOCOL_MAX_PAYLOAD]; // max 64 bytes + uint8_t padding; +} ATTR_PACKED i2c_message_t; +``` + +### Commands + +```c +typedef enum { + SlaveCommand_RequestProperty = 0, + SlaveCommand_JumpToBootloader = 1, + SlaveCommand_RequestKeyStates = 2, + SlaveCommand_SetTestLed = 3, + SlaveCommand_SetLedPwmBrightness = 4, + SlaveCommand_ModuleSpecificCommand = 5, +} slave_command_t; +``` + +### Properties + +```c +typedef enum { + SlaveProperty_Sync = 0, + SlaveProperty_ModuleProtocolVersion = 1, + SlaveProperty_FirmwareVersion = 2, + SlaveProperty_ModuleId = 3, + SlaveProperty_KeyCount = 4, + SlaveProperty_PointerCount = 5, + SlaveProperty_GitTag = 6, + SlaveProperty_GitRepo = 7, + SlaveProperty_FirmwareChecksum = 8, +} slave_property_t; +``` + +## Module Reset Mechanisms + +### Jump to Bootloader + +```c +case UhkModulePhase_JumpToBootloader: + txMessage.data[0] = SlaveCommand_JumpToBootloader; + txMessage.length = 1; + res.status = tx(i2cAddress); + break; +``` + +### Trackpoint Reset + +```c +void UhkModuleSlaveDriver_SendTrackpointCommand(module_specific_command_t command); +``` + +Commands: `ResetTrackpoint`, `RunTrackpoint`, `TrackpointSignalData`, `TrackpointSignalClock` + +## Connection Status Management + +```c +void UhkModuleSlaveDriver_Disconnect(uint8_t uhkModuleDriverId) { + // Clear module state + // Clear key states + // Schedule reconnection timeout (350ms) +} +``` + +## Communication Flow Diagram + +``` +Right Half Module (I2C) + | | + |-- SlaveCommand_RequestProperty -->| + | (SlaveProperty_Sync) | + |<--------- "SYNC" response --------| + | | + |-- SlaveCommand_RequestProperty -->| + | (SlaveProperty_ModuleId) | + |<--------- ModuleId (1 byte) ------| + | | + |-- [Loop: RequestKeyStates] ------>| + |<--------- KeyStates + Pointer ----| +``` + +## Key Source Files + +- `right/src/slave_scheduler.c` and `.h` - Round-robin scheduler +- `right/src/slave_drivers/uhk_module_driver.c` - Module discovery/polling +- `right/src/slave_drivers/kboot_driver.c` - Bootloader communication +- `right/src/i2c.c` - I2C primitives (UHK60) +- `device/src/keyboard/uart_modules.c` - UART modules (UHK80) +- `shared/slave_protocol.h` - Protocol definitions +- `shared/i2c_addresses.h` - I2C address mapping diff --git a/doc-dev/other/flashing/usb-communication.md b/doc-dev/other/flashing/usb-communication.md new file mode 100644 index 000000000..50a323bb6 --- /dev/null +++ b/doc-dev/other/flashing/usb-communication.md @@ -0,0 +1,178 @@ +# USB Communication APIs + +This document describes the USB command infrastructure used for host-to-device communication. + +## USB Command Handling + +### Main Protocol Handler + +**File**: `right/src/usb_protocol_handler.c` + +The `UsbProtocolHandler()` function is the central dispatcher that: +- Receives command byte from position 0 in RX buffer +- Routes to appropriate command handler via switch statement +- Clears RX buffer after processing + +### Command IDs + +From `right/src/usb_protocol_handler.h`: + +```c +typedef enum { + UsbCommandId_GetDeviceProperty = 0x00, + UsbCommandId_Reenumerate = 0x01, + UsbCommandId_JumpToModuleBootloader = 0x02, + UsbCommandId_SendKbootCommandToModule = 0x03, + UsbCommandId_ReadConfig = 0x04, + UsbCommandId_WriteHardwareConfig = 0x05, + UsbCommandId_WriteStagingUserConfig = 0x06, + UsbCommandId_ApplyConfig = 0x07, + UsbCommandId_LaunchStorageTransfer = 0x08, + UsbCommandId_GetDeviceState = 0x09, + // ... more commands up to 0x1f +} usb_command_id_t; +``` + +## Buffer Specifications + +From `right/src/usb_interfaces/usb_interface_generic_hid.h`: + +```c +#define USB_GENERIC_HID_INTERRUPT_IN_PACKET_SIZE 63 +#define USB_GENERIC_HID_INTERRUPT_OUT_PACKET_SIZE 63 +#define USB_GENERIC_HID_IN_BUFFER_LENGTH 63 +#define USB_GENERIC_HID_OUT_BUFFER_LENGTH 63 +``` + +- **Packet size**: 63 bytes (USB interrupt endpoint) +- **Available payload**: 62 bytes (after status code) + +## Buffer Access Macros + +From `right/src/usb_protocol_handler.h`: + +```c +#define GetUsbRxBufferUint8(OFFSET) (GetBufferUint8(GenericHidOutBuffer, OFFSET)) +#define GetUsbRxBufferUint16(OFFSET) (GetBufferUint16(GenericHidOutBuffer, OFFSET)) +#define GetUsbRxBufferUint32(OFFSET) (GetBufferUint32(GenericHidOutBuffer, OFFSET)) + +#define SetUsbTxBufferUint8(OFFSET, VALUE) (SetBufferUint8(GenericHidInBuffer, OFFSET, VALUE)) +#define SetUsbTxBufferUint16(OFFSET, VALUE) (SetBufferUint16(GenericHidInBuffer, OFFSET, VALUE)) +#define SetUsbTxBufferUint32(OFFSET, VALUE) (SetBufferUint32(GenericHidInBuffer, OFFSET, VALUE)) +``` + +## Chunked Transfer Pattern + +Large data transfers use a 4-byte header followed by data: + +``` +Header (4 bytes): + [0] = Command ID + [1] = Length + [2] = Offset low byte + [3] = Offset high byte + +Data (up to 59 bytes): + [4+] = Payload +``` + +This allows transfers up to 64 KB using 16-bit offset addressing. + +## Status Codes + +### General Status Codes + +```c +typedef enum { + UsbStatusCode_Success = 0, + UsbStatusCode_InvalidCommand = 1, + UsbStatusCode_Busy = 2, +} usb_status_code_general_t; +``` + +### Command-Specific Status Codes + +Each command defines its own status enum. Example from read config: + +```c +typedef enum { + UsbStatusCode_ReadConfig_InvalidConfigBufferId = 2, + UsbStatusCode_ReadConfig_LengthTooLarge = 3, + UsbStatusCode_ReadConfig_BufferOutOfBounds = 4, +} usb_status_code_read_config_t; +``` + +## Existing Module-Related Commands + +### JumpToModuleBootloader (0x02) + +``` +Request: + [0] = 0x02 + [1] = Module slot ID + +Response: + [0] = Status code +``` + +### SendKbootCommandToModule (0x03) + +``` +Request: + [0] = 0x03 + [1] = K-boot command (0=idle, 1=ping, 2=reset) + [2] = Module I2C address (if command != idle) + +Response: + [0] = Status code +``` + +## Agent-Side Implementation + +### USB Device Wrapper + +**File**: `lib/agent/packages/uhk-usb/src/uhk-hid-device.ts` + +```typescript +public async write(buffer: Buffer): Promise { + const device = await this.getDevice(); + await device.write(sendData); + let receivedData = await device.read(1000); + if (receivedData[0] !== 0) { + throw new Error(`Response code: ${receivedData[0]}`); + } + return Buffer.from(receivedData); +} +``` + +### Fragment Generation + +**File**: `lib/agent/packages/uhk-usb/src/util.ts` + +```typescript +export function getTransferBuffers(usbCommand: UsbCommand, configBuffer: Buffer): Buffer[] { + const MAX_SENDING_PAYLOAD_SIZE = MAX_USB_PAYLOAD_SIZE - 4; // 59 bytes + // Creates array of 63-byte buffers with header + data chunks +} +``` + +## Extension Points for Firmware Upload + +1. **New USB Command IDs**: Reserve 0x20-0x2f for firmware operations +2. **Chunked Transfer**: Use existing 4-byte header pattern +3. **Flash Management**: + - Erase flash region command + - Write flash command with address/data + - Verify checksum command +4. **Status Monitoring**: + - Query transfer progress + - Get remaining space + - Query write status + +## Key Source Files + +- `right/src/usb_protocol_handler.c` and `.h` - Main dispatcher +- `right/src/usb_commands/` - Individual command handlers +- `right/src/usb_interfaces/usb_interface_generic_hid.c` - HID interface +- `shared/buffer.h` - Buffer utilities +- `lib/agent/packages/uhk-usb/src/uhk-operations.ts` - Agent operations From 6ac125248598b2b06eaebe8d57ad405dfc83c17d Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Fri, 6 Feb 2026 14:59:11 +0100 Subject: [PATCH 2/5] Add firmware upload proposal with current config flow Document the actual UserConfig flashing sequence and propose how to extend it for module firmware upload using the same chunked transfer pattern. Co-Authored-By: Claude Opus 4.5 --- .../flashing/firmware-upload-proposal.md | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 doc-dev/other/flashing/firmware-upload-proposal.md diff --git a/doc-dev/other/flashing/firmware-upload-proposal.md b/doc-dev/other/flashing/firmware-upload-proposal.md new file mode 100644 index 000000000..4d77a218c --- /dev/null +++ b/doc-dev/other/flashing/firmware-upload-proposal.md @@ -0,0 +1,156 @@ +# Firmware Upload Proposal + +## Current UserConfig Flashing Flow + +``` +Agent Firmware + | | + | 1. WriteStagingUserConfig (0x06) | + | [cmd, len, offsetLo, offsetHi, data...] + | ----------------------------------------> + | (repeat for each 59-byte chunk) | + | | + | 2. ApplyConfig (0x07) | + | ----------------------------------------> + | Validates & swaps staging→validated | + | | + | 3. LaunchEepromTransfer (0x08) | + | [cmd, operation=write, bufferId] | + | ----------------------------------------> + | Writes validated buffer to EEPROM | + | | + | 4. GetDeviceState (0x09) - poll | + | ----------------------------------------> + | <-- isEepromBusy until done | +``` + +### USB Packet Format (63 bytes max) + +``` +WriteConfig packet: + [0] = UsbCommand (0x05 or 0x06) + [1] = length (1-59) + [2] = offset low byte + [3] = offset high byte + [4-62] = data (up to 59 bytes) +``` + +### Agent Code Reference + +```typescript +// lib/agent/packages/uhk-usb/src/util.ts +function getTransferBuffers(usbCommand, configBuffer) { + const MAX_SENDING_PAYLOAD_SIZE = 59; // 63 - 4 byte header + for (let offset = 0; offset < configBuffer.length; offset += 59) { + const header = [usbCommand, length, offset & 0xFF, offset >> 8]; + fragments.push(concat(header, configBuffer.slice(offset, offset + 59))); + } +} + +// lib/agent/packages/uhk-usb/src/uhk-operations.ts +async saveUserConfiguration(buffer) { + await sendConfigToKeyboard(buffer, true); // WriteStagingUserConfig chunks + await applyConfiguration(); // ApplyConfig + await writeConfigToEeprom(validatedUserConfig); // LaunchEepromTransfer + await waitUntilKeyboardBusy(); // Poll GetDeviceState +} +``` + +## Proposed Firmware Upload API + +Reuse the same chunked transfer pattern, but target module firmware instead of config buffers. + +### New USB Commands + +| ID | Command | Parameters | +|----|---------|------------| +| 0x20 | WriteModuleFirmware | [len, offsetLo, offsetHi, data...] | +| 0x21 | FlashModule | [slotId] | +| 0x22 | GetModuleFlashState | - | + +### Proposed Flow + +``` +Agent Firmware + | | + | 1. WriteModuleFirmware (0x20) | + | [cmd, len, offsetLo, offsetHi, data...] + | ----------------------------------------> + | (repeat for each 59-byte chunk) | + | Streams directly to module via I2C | + | | + | 2. FlashModule (0x21) | + | [cmd, slotId] | + | ----------------------------------------> + | Finalizes flash, resets module | + | | + | 3. GetModuleFlashState (0x22) - poll | + | ----------------------------------------> + | <-- status: idle/erasing/writing/done | +``` + +### Implementation Options + +**Option A: Streaming (preferred for UHK60)** +- Each WriteModuleFirmware chunk is immediately forwarded to module via I2C +- No RAM buffer needed on right half +- Module bootloader receives K-boot WriteMemory commands directly + +**Option B: Buffered (if streaming not possible)** +- Store firmware in staging buffer (limited to ~32KB) +- FlashModule reads from buffer and sends to module +- Requires multiple passes for larger firmware + +### Agent Code Changes + +```typescript +// Minimal changes - reuse existing pattern +async flashModuleFirmware(slotId: number, firmwareBuffer: Buffer) { + // Same chunked transfer, different command + const fragments = getTransferBuffers(0x20, firmwareBuffer); + for (const fragment of fragments) { + await this.device.write(fragment); + } + + // Trigger flash + await this.device.write(Buffer.from([0x21, slotId])); + + // Wait for completion + while (true) { + const state = await this.device.write(Buffer.from([0x22])); + if (state[1] === FlashState.Done) break; + if (state[1] === FlashState.Error) throw new Error(state[2]); + await snooze(100); + } +} +``` + +### Firmware Side Implementation + +```c +// New command handlers in usb_commands/ + +void UsbCommand_WriteModuleFirmware(const uint8_t *in, uint8_t *out) { + uint8_t length = GetUsbRxBufferUint8(1); + uint16_t offset = GetUsbRxBufferUint16(2); + const uint8_t *data = in + 4; + + // Stream to module bootloader via I2C (K-boot WriteMemory) + KbootDriver_WriteMemory(offset, data, length); +} + +void UsbCommand_FlashModule(const uint8_t *in, uint8_t *out) { + uint8_t slotId = GetUsbRxBufferUint8(1); + + // Send K-boot Reset command to module + KbootDriver_Reset(slotId); +} +``` + +## Summary + +The existing config transfer infrastructure (chunked USB, 59-byte payloads, 16-bit offset) can be reused directly for firmware upload. The main work is: + +1. Add 3 new USB command handlers +2. Implement K-boot I2C driver (WriteMemory, Reset commands) +3. Handle the streaming/forwarding in firmware instead of Agent From bd8cbea4a2263cc31dbb5e0f787e3f069475418d Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Tue, 10 Feb 2026 15:29:58 +0100 Subject: [PATCH 3/5] Kboot: Add an overview implementation plan. --- .../flashing/firmware-upload-proposal.md | 331 +++++++++++------- 1 file changed, 200 insertions(+), 131 deletions(-) diff --git a/doc-dev/other/flashing/firmware-upload-proposal.md b/doc-dev/other/flashing/firmware-upload-proposal.md index 4d77a218c..4a84b40cc 100644 --- a/doc-dev/other/flashing/firmware-upload-proposal.md +++ b/doc-dev/other/flashing/firmware-upload-proposal.md @@ -1,156 +1,225 @@ # Firmware Upload Proposal -## Current UserConfig Flashing Flow +## 1. USB Protocol -``` -Agent Firmware - | | - | 1. WriteStagingUserConfig (0x06) | - | [cmd, len, offsetLo, offsetHi, data...] - | ----------------------------------------> - | (repeat for each 59-byte chunk) | - | | - | 2. ApplyConfig (0x07) | - | ----------------------------------------> - | Validates & swaps staging→validated | - | | - | 3. LaunchEepromTransfer (0x08) | - | [cmd, operation=write, bufferId] | - | ----------------------------------------> - | Writes validated buffer to EEPROM | - | | - | 4. GetDeviceState (0x09) - poll | - | ----------------------------------------> - | <-- isEepromBusy until done | -``` +63-byte HID reports. All multi-byte integers are little-endian. -### USB Packet Format (63 bytes max) +Every response starts with a status byte (0 = success). -``` -WriteConfig packet: - [0] = UsbCommand (0x05 or 0x06) - [1] = length (1-59) - [2] = offset low byte - [3] = offset high byte - [4-62] = data (up to 59 bytes) +## 2. Current: UserConfig Flashing + +```mermaid +sequenceDiagram + participant A as Agent + participant R as Right Half + + loop for each 59-byte chunk + A->>R: WriteStagingUserConfig + end + A->>R: ApplyConfig + A->>R: LaunchStorageTransfer + loop poll + A->>R: GetDeviceState + R-->>A: isEepromBusy + end ``` -### Agent Code Reference - -```typescript -// lib/agent/packages/uhk-usb/src/util.ts -function getTransferBuffers(usbCommand, configBuffer) { - const MAX_SENDING_PAYLOAD_SIZE = 59; // 63 - 4 byte header - for (let offset = 0; offset < configBuffer.length; offset += 59) { - const header = [usbCommand, length, offset & 0xFF, offset >> 8]; - fragments.push(concat(header, configBuffer.slice(offset, offset + 59))); - } -} - -// lib/agent/packages/uhk-usb/src/uhk-operations.ts -async saveUserConfiguration(buffer) { - await sendConfigToKeyboard(buffer, true); // WriteStagingUserConfig chunks - await applyConfiguration(); // ApplyConfig - await writeConfigToEeprom(validatedUserConfig); // LaunchEepromTransfer - await waitUntilKeyboardBusy(); // Poll GetDeviceState -} +### WriteStagingUserConfig (0x06) + +| Byte | Field | +|------|---------------| +| 0 | 0x06 | +| 1 | payload length (max 59) | +| 2-3 | offset into staging buffer | +| 4+ | data | + +Response: status byte only. + +### ApplyConfig (0x07) + +| Byte | Field | +|------|-------| +| 0 | 0x07 | + +Validates the staging buffer. On success, swaps staging ↔ validated buffers. + +Response: + +| Byte | Field | +|------|-------| +| 0 | status | +| 1-2 | parser offset (where parsing stopped) | +| 3 | parser stage (0=validate, 1=apply) | + +### LaunchStorageTransfer (0x08) + +| Byte | Field | +|------|-------| +| 0 | 0x08 | +| 1 | operation (0=read, 1=write) | +| 2 | buffer id (0=hardware, 1=stagingUser, 2=validatedUser) | + +Response: status byte only. + +### GetDeviceState (0x09) + +| Byte | Field | +|------|-------| +| 0 | 0x09 | + +Response: + +| Byte | Field | +|------|-------| +| 0 | status | +| 1 | busy flags: bit 0 = isEepromBusy, bit 1 = isModuleFlashBusy (**new**) | +| 2 | flags (halvesMerged, pairing, zephyrLog) | +| 3-5 | connected module IDs (left half, left mod, right mod) | +| 6 | active layer (bits 0-6) + toggled flag (bit 7) | +| 7 | macro status dirty | +| 8 | current keymap index | + +## 3. Current: Module Firmware Flashing + +Currently, module firmware flashing is Agent-driven. The right half acts as a USB-to-I2C bridge ("Buspal" mode) and the Agent speaks K-boot protocol directly over that bridge: + +```mermaid +sequenceDiagram + participant A as Agent + participant R as Right Half (Buspal) + participant M as Module (K-boot bootloader) + + A->>R: SendKbootCommandToModule(ping) + R->>M: K-boot ping (I2C) + A->>R: JumpToModuleBootloader(slotId) + R->>M: jump command (I2C) + Note over R: reenumerate as Buspal USB device + A->>R: K-boot configureI2c(i2cAddr) + A->>R: K-boot flashEraseAllUnsecure + R->>M: forwarded via I2C + A->>R: K-boot writeMemory(0x0000, firmwareData) + R->>M: forwarded via I2C + A->>R: K-boot reset + R->>M: forwarded via I2C + Note over R: reenumerate back to normal keyboard + A->>R: SendKbootCommandToModule(reset) + A->>R: SendKbootCommandToModule(idle) ``` -## Proposed Firmware Upload API +### SendKbootCommandToModule (0x03) -Reuse the same chunked transfer pattern, but target module firmware instead of config buffers. +| Byte | Field | +|------|-------| +| 0 | 0x03 | +| 1 | command (0=idle, 1=ping, 2=reset) | +| 2 | i2c address (omitted for idle) | -### New USB Commands +Triggers the kboot_driver state machine on the right half, which sends K-boot framed ping/reset packets over I2C. -| ID | Command | Parameters | -|----|---------|------------| -| 0x20 | WriteModuleFirmware | [len, offsetLo, offsetHi, data...] | -| 0x21 | FlashModule | [slotId] | -| 0x22 | GetModuleFlashState | - | +### JumpToModuleBootloader (0x02) -### Proposed Flow +| Byte | Field | +|------|-------| +| 0 | 0x02 | +| 1 | slot id | -``` -Agent Firmware - | | - | 1. WriteModuleFirmware (0x20) | - | [cmd, len, offsetLo, offsetHi, data...] - | ----------------------------------------> - | (repeat for each 59-byte chunk) | - | Streams directly to module via I2C | - | | - | 2. FlashModule (0x21) | - | [cmd, slotId] | - | ----------------------------------------> - | Finalizes flash, resets module | - | | - | 3. GetModuleFlashState (0x22) - poll | - | ----------------------------------------> - | <-- status: idle/erasing/writing/done | -``` +Sets the module phase to `JumpToBootloader`, causing the module driver to send a jump command via I2C. + +### Buspal Mode + +After jumping the module to bootloader, the Agent reenumerates the right half into Buspal mode (EnumerationMode 1). In Buspal mode, the right half becomes a transparent USB↔I2C bridge. The Agent then uses a K-boot library (`KBoot` class) to speak the K-boot framing protocol directly, sending `flashEraseAllUnsecure`, `writeMemory`, and `reset` commands. + +## 4. Proposed: Firmware-Driven Module Flashing + +Move K-boot protocol handling from the Agent into the firmware. The Agent sends firmware data using the same chunked transfer pattern as config flashing. The firmware handles K-boot protocol internally. + +```mermaid +sequenceDiagram + participant A as Agent + participant R as Right Half + participant M as Module (K-boot bootloader) -### Implementation Options - -**Option A: Streaming (preferred for UHK60)** -- Each WriteModuleFirmware chunk is immediately forwarded to module via I2C -- No RAM buffer needed on right half -- Module bootloader receives K-boot WriteMemory commands directly - -**Option B: Buffered (if streaming not possible)** -- Store firmware in staging buffer (limited to ~32KB) -- FlashModule reads from buffer and sends to module -- Requires multiple passes for larger firmware - -### Agent Code Changes - -```typescript -// Minimal changes - reuse existing pattern -async flashModuleFirmware(slotId: number, firmwareBuffer: Buffer) { - // Same chunked transfer, different command - const fragments = getTransferBuffers(0x20, firmwareBuffer); - for (const fragment of fragments) { - await this.device.write(fragment); - } - - // Trigger flash - await this.device.write(Buffer.from([0x21, slotId])); - - // Wait for completion - while (true) { - const state = await this.device.write(Buffer.from([0x22])); - if (state[1] === FlashState.Done) break; - if (state[1] === FlashState.Error) throw new Error(state[2]); - await snooze(100); - } -} + loop for each 59-byte chunk + A->>R: WriteModuleFirmware + end + A->>R: ValidateBuffer(moduleFirmware, size, crc) + A->>R: FlashModule(slotId) + R->>M: K-boot: jump to bootloader + R->>M: K-boot: flashEraseAllUnsecure + R->>M: K-boot: writeMemory (from buffer) + R->>M: K-boot: reset + loop poll + A->>R: GetDeviceState + R-->>A: isModuleFlashBusy + opt if detailed state needed + A->>R: GetModuleFlashState + R-->>A: state + error code + end + end ``` -### Firmware Side Implementation +### WriteModuleFirmware (new, 0x20) -```c -// New command handlers in usb_commands/ +| Byte | Field | +|------|-------| +| 0 | 0x20 | +| 1 | payload length (max 59) | +| 2-3 | offset into firmware buffer | +| 4+ | data | -void UsbCommand_WriteModuleFirmware(const uint8_t *in, uint8_t *out) { - uint8_t length = GetUsbRxBufferUint8(1); - uint16_t offset = GetUsbRxBufferUint16(2); - const uint8_t *data = in + 4; +Same chunked transfer pattern as WriteStagingUserConfig. - // Stream to module bootloader via I2C (K-boot WriteMemory) - KbootDriver_WriteMemory(offset, data, length); -} +### FlashModule (new, 0x21) -void UsbCommand_FlashModule(const uint8_t *in, uint8_t *out) { - uint8_t slotId = GetUsbRxBufferUint8(1); +| Byte | Field | +|------|-------| +| 0 | 0x21 | +| 1 | slot id | - // Send K-boot Reset command to module - KbootDriver_Reset(slotId); -} -``` +Triggers the full K-boot flash sequence internally: jump to bootloader → erase → write → reset. + +### GetModuleFlashState (new, 0x22) + +| Byte | Field | +|------|-------| +| 0 | 0x22 | + +Response: + +| Byte | Field | +|------|-------| +| 0 | status | +| 1 | flash state (0=idle, 1=erasing, 2=writing, 3=done, 4=error) | +| 2 | error code (if state=error) | + +### ValidateBuffer (new, 0x23) + +| Byte | Field | +|------|-------| +| 0 | 0x23 | +| 1 | buffer id (0=hardware, 1=stagingUser, 2=validatedUser, 3=moduleFirmware) | +| 2-3 | expected size | +| 4-5 | expected CRC16 | + +Firmware computes CRC over the first `size` bytes of the given buffer and compares it to the expected value. + +Response: + +| Byte | Field | +|------|-------| +| 0 | status (0=match, non-zero=mismatch) | + +### Advantages + +- No Buspal reenumeration needed (disruptive, platform-dependent) +- Agent doesn't need K-boot library or I2C knowledge +- Same pattern as config flashing — simple for Agent authors +- Firmware can manage timing and retries internally -## Summary +### Open question: buffering -The existing config transfer infrastructure (chunked USB, 59-byte payloads, 16-bit offset) can be reused directly for firmware upload. The main work is: +Module firmware can be up to ~128KB. The 16-bit offset field supports up to 64KB. Options: -1. Add 3 new USB command handlers -2. Implement K-boot I2C driver (WriteMemory, Reset commands) -3. Handle the streaming/forwarding in firmware instead of Agent +- **A. Buffer in RAM**: Requires large allocation. Possible on UHK80 (nRF has more RAM), tight on UHK60. +- **B. Stream to module**: Forward each chunk to the module bootloader immediately via K-boot writeMemory. No buffer needed, but the module must already be in bootloader mode during the upload. Requires the Agent to wait for each chunk's I2C completion. +- **C. Use 24-bit offset**: Extend the offset field to 3 bytes (payload drops to 58 bytes) to support >64KB. Only needed if buffering. From d533a2d01914186a27da371d45b88f27360d98c2 Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Tue, 10 Feb 2026 15:50:39 +0100 Subject: [PATCH 4/5] reword. --- doc-dev/other/flashing/firmware-upload-proposal.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc-dev/other/flashing/firmware-upload-proposal.md b/doc-dev/other/flashing/firmware-upload-proposal.md index 4a84b40cc..f6f93ab99 100644 --- a/doc-dev/other/flashing/firmware-upload-proposal.md +++ b/doc-dev/other/flashing/firmware-upload-proposal.md @@ -142,7 +142,7 @@ sequenceDiagram loop for each 59-byte chunk A->>R: WriteModuleFirmware end - A->>R: ValidateBuffer(moduleFirmware, size, crc) + A->>R: ValidateBufferCrc(moduleFirmware, size, crc) A->>R: FlashModule(slotId) R->>M: K-boot: jump to bootloader R->>M: K-boot: flashEraseAllUnsecure @@ -192,7 +192,7 @@ Response: | 1 | flash state (0=idle, 1=erasing, 2=writing, 3=done, 4=error) | | 2 | error code (if state=error) | -### ValidateBuffer (new, 0x23) +### ValidateBufferCrc (new, 0x23) | Byte | Field | |------|-------| From bce3490c5506499af4d8e4f08edfd9dbe72164a0 Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Tue, 10 Feb 2026 17:37:25 +0100 Subject: [PATCH 5/5] Implement new usb flashing Api. --- right/src/CMakeLists.txt | 1 + right/src/config_parser/config_globals.c | 5 ++- right/src/config_parser/config_globals.h | 1 + right/src/module_flash.c | 5 +++ right/src/module_flash.h | 25 +++++++++++++ right/src/usb_commands/CMakeLists.txt | 4 +++ .../usb_commands/usb_command_flash_module.c | 32 +++++++++++++++++ .../usb_commands/usb_command_flash_module.h | 12 +++++++ .../usb_command_get_device_state.c | 8 +++-- .../usb_command_get_device_state.h | 6 +++- .../usb_command_get_module_flash_state.c | 9 +++++ .../usb_command_get_module_flash_state.h | 12 +++++++ .../usb_command_validate_buffer_crc.c | 36 +++++++++++++++++++ .../usb_command_validate_buffer_crc.h | 20 +++++++++++ .../usb_command_write_module_firmware.c | 10 ++++++ .../usb_command_write_module_firmware.h | 12 +++++++ right/src/usb_protocol_handler.c | 16 +++++++++ right/src/usb_protocol_handler.h | 5 +++ 18 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 right/src/module_flash.c create mode 100644 right/src/module_flash.h create mode 100644 right/src/usb_commands/usb_command_flash_module.c create mode 100644 right/src/usb_commands/usb_command_flash_module.h create mode 100644 right/src/usb_commands/usb_command_get_module_flash_state.c create mode 100644 right/src/usb_commands/usb_command_get_module_flash_state.h create mode 100644 right/src/usb_commands/usb_command_validate_buffer_crc.c create mode 100644 right/src/usb_commands/usb_command_validate_buffer_crc.h create mode 100644 right/src/usb_commands/usb_command_write_module_firmware.c create mode 100644 right/src/usb_commands/usb_command_write_module_firmware.h diff --git a/right/src/CMakeLists.txt b/right/src/CMakeLists.txt index 18b5b38ef..c2ae57250 100644 --- a/right/src/CMakeLists.txt +++ b/right/src/CMakeLists.txt @@ -50,6 +50,7 @@ target_sources(${PROJECT_NAME} PRIVATE macro_recorder.c $<$:${CMAKE_CURRENT_SOURCE_DIR}/main.c> $<$:${CMAKE_CURRENT_SOURCE_DIR}/module.c> + module_flash.c mouse_controller.c mouse_keys.c module.c diff --git a/right/src/config_parser/config_globals.c b/right/src/config_parser/config_globals.c index 616d2762c..2e44e76ae 100644 --- a/right/src/config_parser/config_globals.c +++ b/right/src/config_parser/config_globals.c @@ -30,7 +30,7 @@ bool ParserRunDry; bool IsConfigBufferIdValid(config_buffer_id_t configBufferId) { - return ConfigBufferId_HardwareConfig <= configBufferId && configBufferId <= ConfigBufferId_ValidatedUserConfig; + return ConfigBufferId_HardwareConfig <= configBufferId && configBufferId <= ConfigBufferId_ModuleFirmware; } config_buffer_t* ConfigBufferIdToConfigBuffer(config_buffer_id_t configBufferId) @@ -42,6 +42,8 @@ config_buffer_t* ConfigBufferIdToConfigBuffer(config_buffer_id_t configBufferId) return &StagingUserConfigBuffer; case ConfigBufferId_ValidatedUserConfig: return &ValidatedUserConfigBuffer; + case ConfigBufferId_ModuleFirmware: + return &StagingUserConfigBuffer; default: return NULL; } @@ -54,6 +56,7 @@ uint16_t ConfigBufferIdToBufferSize(config_buffer_id_t configBufferId) return HARDWARE_CONFIG_SIZE; case ConfigBufferId_StagingUserConfig: case ConfigBufferId_ValidatedUserConfig: + case ConfigBufferId_ModuleFirmware: return USER_CONFIG_SIZE; default: return 0; diff --git a/right/src/config_parser/config_globals.h b/right/src/config_parser/config_globals.h index 4462e2f1d..7019da237 100644 --- a/right/src/config_parser/config_globals.h +++ b/right/src/config_parser/config_globals.h @@ -19,6 +19,7 @@ ConfigBufferId_HardwareConfig, ConfigBufferId_StagingUserConfig, ConfigBufferId_ValidatedUserConfig, + ConfigBufferId_ModuleFirmware, } config_buffer_id_t; typedef struct { diff --git a/right/src/module_flash.c b/right/src/module_flash.c new file mode 100644 index 000000000..092a71c8b --- /dev/null +++ b/right/src/module_flash.c @@ -0,0 +1,5 @@ +#include "module_flash.h" + +module_flash_state_t ModuleFlashState = ModuleFlashState_Idle; +bool ModuleFlashBusy = false; +uint8_t ModuleFlashErrorCode = 0; diff --git a/right/src/module_flash.h b/right/src/module_flash.h new file mode 100644 index 000000000..3a82c1393 --- /dev/null +++ b/right/src/module_flash.h @@ -0,0 +1,25 @@ +#ifndef __MODULE_FLASH_H__ +#define __MODULE_FLASH_H__ + +// Includes: + + #include + #include + +// Typedefs: + + typedef enum { + ModuleFlashState_Idle = 0, + ModuleFlashState_Erasing = 1, + ModuleFlashState_Writing = 2, + ModuleFlashState_Done = 3, + ModuleFlashState_Error = 4, + } module_flash_state_t; + +// Variables: + + extern module_flash_state_t ModuleFlashState; + extern bool ModuleFlashBusy; + extern uint8_t ModuleFlashErrorCode; + +#endif diff --git a/right/src/usb_commands/CMakeLists.txt b/right/src/usb_commands/CMakeLists.txt index 9ba4fc83c..e1228c2e1 100644 --- a/right/src/usb_commands/CMakeLists.txt +++ b/right/src/usb_commands/CMakeLists.txt @@ -25,4 +25,8 @@ target_sources(${PROJECT_NAME} PRIVATE usb_command_set_variable.c usb_command_switch_keymap.c usb_command_write_config.c + usb_command_write_module_firmware.c + usb_command_flash_module.c + usb_command_get_module_flash_state.c + usb_command_validate_buffer_crc.c ) diff --git a/right/src/usb_commands/usb_command_flash_module.c b/right/src/usb_commands/usb_command_flash_module.c new file mode 100644 index 000000000..edead304b --- /dev/null +++ b/right/src/usb_commands/usb_command_flash_module.c @@ -0,0 +1,32 @@ +#include "usb_commands/usb_command_flash_module.h" +#include "usb_protocol_handler.h" +#include "module_flash.h" +#include "slot.h" + +typedef enum { + UsbStatusCode_FlashModule_InvalidSlotId = 2, + UsbStatusCode_FlashModule_Busy = 3, +} usb_status_code_flash_module_t; + +void UsbCommand_FlashModule(const uint8_t *GenericHidOutBuffer, uint8_t *GenericHidInBuffer) +{ + uint8_t slotId = GetUsbRxBufferUint8(1); + + if (!IS_VALID_MODULE_SLOT(slotId)) { + SetUsbTxBufferUint8(0, UsbStatusCode_FlashModule_InvalidSlotId); + return; + } + + if (ModuleFlashBusy) { + SetUsbTxBufferUint8(0, UsbStatusCode_FlashModule_Busy); + return; + } + + ModuleFlashBusy = true; + ModuleFlashErrorCode = 0; + + // TODO: Trigger actual K-boot flash sequence here. + // For now, stub: immediately mark as done. + ModuleFlashState = ModuleFlashState_Done; + ModuleFlashBusy = false; +} diff --git a/right/src/usb_commands/usb_command_flash_module.h b/right/src/usb_commands/usb_command_flash_module.h new file mode 100644 index 000000000..1da96ba37 --- /dev/null +++ b/right/src/usb_commands/usb_command_flash_module.h @@ -0,0 +1,12 @@ +#ifndef __USB_COMMAND_FLASH_MODULE_H__ +#define __USB_COMMAND_FLASH_MODULE_H__ + +// Includes: + + #include + +// Functions: + + void UsbCommand_FlashModule(const uint8_t *GenericHidOutBuffer, uint8_t *GenericHidInBuffer); + +#endif diff --git a/right/src/usb_commands/usb_command_get_device_state.c b/right/src/usb_commands/usb_command_get_device_state.c index a2c567f17..9b617b7da 100644 --- a/right/src/usb_commands/usb_command_get_device_state.c +++ b/right/src/usb_commands/usb_command_get_device_state.c @@ -21,6 +21,7 @@ #include "config_manager.h" #include "usb_log_buffer.h" +#include "module_flash.h" #ifdef __ZEPHYR__ #include "flash.h" @@ -70,11 +71,14 @@ void UsbCommand_GetKeyboardState(const uint8_t *GenericHidOutBuffer, uint8_t *Ge { detectFreezes(); + uint8_t byte1 = 0; #ifdef __ZEPHYR__ - SetUsbTxBufferUint8(1, Flash_IsBusy()); + byte1 |= (Flash_IsBusy() ? GetDeviceStateByte1_EepromBusy : 0); #else - SetUsbTxBufferUint8(1, IsStorageBusy); + byte1 |= (IsStorageBusy ? GetDeviceStateByte1_EepromBusy : 0); #endif + byte1 |= (ModuleFlashBusy ? GetDeviceStateByte1_ModuleFlashBusy : 0); + SetUsbTxBufferUint8(1, byte1); uint8_t byte2 = 0 | (MergeSensor_IsMerged() == MergeSensorState_Joined ? GetDeviceStateByte2_HalvesMerged : 0) diff --git a/right/src/usb_commands/usb_command_get_device_state.h b/right/src/usb_commands/usb_command_get_device_state.h index 4c146ae56..879c93e24 100644 --- a/right/src/usb_commands/usb_command_get_device_state.h +++ b/right/src/usb_commands/usb_command_get_device_state.h @@ -7,12 +7,16 @@ // Typedefs: +typedef enum { + GetDeviceStateByte1_EepromBusy = 1 << 0, + GetDeviceStateByte1_ModuleFlashBusy = 1 << 1, +} usb_command_get_device_state_byte1_mask_t; + typedef enum { GetDeviceStateByte2_HalvesMerged = 1 << 0, GetDeviceStateByte2_PairingInProgress = 1 << 1, GetDeviceStateByte2_NewPairedDevice = 1 << 2, GetDeviceStateByte2_ZephyrLog = 1 << 3, - } usb_command_get_device_state_byte2_mask_t; typedef enum { diff --git a/right/src/usb_commands/usb_command_get_module_flash_state.c b/right/src/usb_commands/usb_command_get_module_flash_state.c new file mode 100644 index 000000000..f86459b9a --- /dev/null +++ b/right/src/usb_commands/usb_command_get_module_flash_state.c @@ -0,0 +1,9 @@ +#include "usb_commands/usb_command_get_module_flash_state.h" +#include "usb_protocol_handler.h" +#include "module_flash.h" + +void UsbCommand_GetModuleFlashState(const uint8_t *GenericHidOutBuffer, uint8_t *GenericHidInBuffer) +{ + SetUsbTxBufferUint8(1, ModuleFlashState); + SetUsbTxBufferUint8(2, ModuleFlashErrorCode); +} diff --git a/right/src/usb_commands/usb_command_get_module_flash_state.h b/right/src/usb_commands/usb_command_get_module_flash_state.h new file mode 100644 index 000000000..0cbdc697c --- /dev/null +++ b/right/src/usb_commands/usb_command_get_module_flash_state.h @@ -0,0 +1,12 @@ +#ifndef __USB_COMMAND_GET_MODULE_FLASH_STATE_H__ +#define __USB_COMMAND_GET_MODULE_FLASH_STATE_H__ + +// Includes: + + #include + +// Functions: + + void UsbCommand_GetModuleFlashState(const uint8_t *GenericHidOutBuffer, uint8_t *GenericHidInBuffer); + +#endif diff --git a/right/src/usb_commands/usb_command_validate_buffer_crc.c b/right/src/usb_commands/usb_command_validate_buffer_crc.c new file mode 100644 index 000000000..f3d7e7e09 --- /dev/null +++ b/right/src/usb_commands/usb_command_validate_buffer_crc.c @@ -0,0 +1,36 @@ +#include "usb_commands/usb_command_validate_buffer_crc.h" +#include "usb_protocol_handler.h" +#include "config_parser/config_globals.h" +#include "crc16.h" + +void UsbCommand_ValidateBufferCrc(const uint8_t *GenericHidOutBuffer, uint8_t *GenericHidInBuffer) +{ + uint8_t bufferId = GetUsbRxBufferUint8(1); + uint16_t expectedSize = GetUsbRxBufferUint16(2); + uint16_t expectedCrc = GetUsbRxBufferUint16(4); + + if (!IsConfigBufferIdValid(bufferId)) { + SetUsbTxBufferUint8(0, UsbStatusCode_ValidateBufferCrc_InvalidBufferId); + return; + } + + config_buffer_t *buffer = ConfigBufferIdToConfigBuffer(bufferId); + uint16_t bufferSize = ConfigBufferIdToBufferSize(bufferId); + + if (expectedSize > bufferSize) { + SetUsbTxBufferUint8(0, UsbStatusCode_ValidateBufferCrc_SizeOutOfBounds); + return; + } + + crc16_data_t crcData; + crc16_init(&crcData); + crc16_update(&crcData, buffer->buffer, expectedSize); + + uint16_t computedCrc; + crc16_finalize(&crcData, &computedCrc); + + if (computedCrc != expectedCrc) { + SetUsbTxBufferUint8(0, UsbStatusCode_ValidateBufferCrc_CrcMismatch); + return; + } +} diff --git a/right/src/usb_commands/usb_command_validate_buffer_crc.h b/right/src/usb_commands/usb_command_validate_buffer_crc.h new file mode 100644 index 000000000..64fc99f58 --- /dev/null +++ b/right/src/usb_commands/usb_command_validate_buffer_crc.h @@ -0,0 +1,20 @@ +#ifndef __USB_COMMAND_VALIDATE_BUFFER_CRC_H__ +#define __USB_COMMAND_VALIDATE_BUFFER_CRC_H__ + +// Includes: + + #include + +// Typedefs: + + typedef enum { + UsbStatusCode_ValidateBufferCrc_CrcMismatch = 2, + UsbStatusCode_ValidateBufferCrc_InvalidBufferId = 3, + UsbStatusCode_ValidateBufferCrc_SizeOutOfBounds = 4, + } usb_status_code_validate_buffer_crc_t; + +// Functions: + + void UsbCommand_ValidateBufferCrc(const uint8_t *GenericHidOutBuffer, uint8_t *GenericHidInBuffer); + +#endif diff --git a/right/src/usb_commands/usb_command_write_module_firmware.c b/right/src/usb_commands/usb_command_write_module_firmware.c new file mode 100644 index 000000000..20fc1f74e --- /dev/null +++ b/right/src/usb_commands/usb_command_write_module_firmware.c @@ -0,0 +1,10 @@ +#include +#include "usb_commands/usb_command_write_module_firmware.h" +#include "usb_commands/usb_command_write_config.h" +#include "usb_protocol_handler.h" +#include "config_parser/config_globals.h" + +void UsbCommand_WriteModuleFirmware(const uint8_t *GenericHidOutBuffer, uint8_t *GenericHidInBuffer) +{ + UsbCommand_WriteConfig(ConfigBufferId_ModuleFirmware, GenericHidOutBuffer, GenericHidInBuffer); +} diff --git a/right/src/usb_commands/usb_command_write_module_firmware.h b/right/src/usb_commands/usb_command_write_module_firmware.h new file mode 100644 index 000000000..c4f6599b0 --- /dev/null +++ b/right/src/usb_commands/usb_command_write_module_firmware.h @@ -0,0 +1,12 @@ +#ifndef __USB_COMMAND_WRITE_MODULE_FIRMWARE_H__ +#define __USB_COMMAND_WRITE_MODULE_FIRMWARE_H__ + +// Includes: + + #include + +// Functions: + + void UsbCommand_WriteModuleFirmware(const uint8_t *GenericHidOutBuffer, uint8_t *GenericHidInBuffer); + +#endif diff --git a/right/src/usb_protocol_handler.c b/right/src/usb_protocol_handler.c index edae63f14..b3d430de0 100644 --- a/right/src/usb_protocol_handler.c +++ b/right/src/usb_protocol_handler.c @@ -16,6 +16,10 @@ #include "usb_commands/usb_command_launch_storage_transfer.h" #include "usb_commands/usb_command_get_module_property.h" #include "usb_commands/usb_command_exec_shell_command.h" +#include "usb_commands/usb_command_write_module_firmware.h" +#include "usb_commands/usb_command_flash_module.h" +#include "usb_commands/usb_command_get_module_flash_state.h" +#include "usb_commands/usb_command_validate_buffer_crc.h" #ifdef __ZEPHYR__ #include "usb_commands/usb_command_draw_oled.h" @@ -86,6 +90,18 @@ void UsbProtocolHandler(uint8_t *GenericHidOutBuffer, uint8_t *GenericHidInBuffe case UsbCommandId_GetModuleProperty: UsbCommand_GetModuleProperty(GenericHidOutBuffer, GenericHidInBuffer); break; + case UsbCommandId_WriteModuleFirmware: + UsbCommand_WriteModuleFirmware(GenericHidOutBuffer, GenericHidInBuffer); + break; + case UsbCommandId_FlashModule: + UsbCommand_FlashModule(GenericHidOutBuffer, GenericHidInBuffer); + break; + case UsbCommandId_GetModuleFlashState: + UsbCommand_GetModuleFlashState(GenericHidOutBuffer, GenericHidInBuffer); + break; + case UsbCommandId_ValidateBufferCrc: + UsbCommand_ValidateBufferCrc(GenericHidOutBuffer, GenericHidInBuffer); + break; #ifdef __ZEPHYR__ case UsbCommandId_ExecShellCommand: UsbCommand_ExecShellCommand(GenericHidOutBuffer, GenericHidInBuffer); diff --git a/right/src/usb_protocol_handler.h b/right/src/usb_protocol_handler.h index 744923117..44811f732 100644 --- a/right/src/usb_protocol_handler.h +++ b/right/src/usb_protocol_handler.h @@ -69,6 +69,11 @@ UsbCommandId_EraseBleSettings = 0x1d, UsbCommandId_ExecShellCommand = 0x1e, UsbCommandId_ReadOled = 0x1f, + + UsbCommandId_WriteModuleFirmware = 0x20, + UsbCommandId_FlashModule = 0x21, + UsbCommandId_GetModuleFlashState = 0x22, + UsbCommandId_ValidateBufferCrc = 0x23, } usb_command_id_t; typedef enum {