Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 303 additions & 2 deletions docs/protocolV2.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,37 @@ Configuration total size is 1024 bytes.

Structure: 12 bytes header + 1012 bytes data.

Header:

<table>
<tr>
<th>0-3</th>
<th>4-5</th>
<th>6-7</th>
<th>8-9</th>
<th>10-11</th>
</tr>
<tr>
<td>const</td>
<td>data checksum</td>
<td></td>
<td>data checksum ^ 0xffff</td>
<td>const</td>
</tr>
<tr>
<td>75 56 34 12</td>
<td>xx xx</td>
<td>00 00</td>
<td>xx xx</td>
<td>ff ff</td>
</tr>
</table>

**data checksum** is the sum of the 1012 data bytes.

The official app keeps both header checksum fields consistent with the data on
every write; update them after modifying the data.

## Back buttons

### mini 25
Expand Down Expand Up @@ -178,6 +209,14 @@ Unless stated otherwise, checksum is the sum of all bytes after it.

**bytes count** is the number of bytes set, after the header.

> NOTE:
>
> All packets go over the control pipe. The official app sends them as
> SET_REPORT(Output); on Linux use HID **feature** reports
> (`hid_send_feature_report`) — interface 0 also has an interrupt OUT
> endpoint, so output reports are routed down the interrupt pipe there and
> the command handler silently ignores them.

## Init communication

Send
Expand Down Expand Up @@ -3636,6 +3675,56 @@ Response

Successful initialization returns **0xaa** in byte **8**.

The payload-less init above is enough for writes that only reach controller
RAM (lost on controller reset). For a write that is going to be flushed to
flash, the official app always sends this init with a fixed 56 bytes unlock
payload instead:

Send

<table>
<tr>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4-5</th>
<th>6</th>
<th>7</th>
<th>8-63</th>
</tr>
<tr>
<td>ID</td>
<td>cmd</td>
<td>bytes count</td>
<td></td>
<td>page</td>
<td colspan="2">checksum</td>
<td>unlock payload</td>
</tr>
<tr>
<td>01</td>
<td>21</td>
<td>38</td>
<td>00</td>
<td>00 00</td>
<td>84</td>
<td>22</td>
<td>see below</td>
</tr>
</table>

Unlock payload (fixed):

```
a2 a3 a0 a1 a6 a7 a4 a5 ba bb b8 b9 be bf bc bd
b2 b3 b0 b1 b6 b7 b4 b5 8a 8b 88 89 8e 8f 8c 8d
82 83 80 81 86 87 84 85 9a 9b 98 99 9e 9f 9c 9d
92 93 90 91 96 97 94 95
```

The response is the same as for the payload-less init.

## Write config to memory

You must send all 1024 bytes, writes don't have a response.
Expand Down Expand Up @@ -6624,6 +6713,9 @@ Send

## Prepare for flush

The official app sends this init with the same fixed 56 bytes unlock payload
described in [Prepare for write](#prepare-for-write).

Send

<table>
Expand Down Expand Up @@ -6694,9 +6786,26 @@ Successful initialization returns **0xaa** in byte **8**.

## Flush config to controller

> [!WARNING]:
Flush persists the current configuration to controller flash, so it survives a
controller reset (reboot / suspend) without toggling the controller mode
switch.

> [!IMPORTANT]
>
> The flush command is acknowledged even when it has no effect. To actually
> persist, the write and the flush must run between two
> [flash session brackets](#flash-session-bracket):
>
> ```
> bracket : the full sequence below
> write : prepare for write (with unlock payload) → write config → checksum validation → end
> flush : prepare for flush → flush → end
> bracket : again — the flash burn happens in this window
> ```
>
> Not working for some reason, help is needed!
> Verified on Win Mini 2025 (firmware 1.22): with the brackets the config
> survives suspend/resume and a full power cycle; without them the device
> reverts to its previous flash config on the next reset.

Send

Expand Down Expand Up @@ -6763,3 +6872,195 @@ Send
<td>00</td>
</tr>
</table>

## Flash session bracket

The official app wraps every write + flush between two of these sequences;
the flash burn happens in the window after the flush (see
[Flush config to controller](#flush-config-to-controller)).

```
init (0x21, with unlock payload) → keep-alive (0x2b) → 0x29 →
N × keep-alive (2 s apart) → 0x2a → end (0x22)
```

`0x29` responds **0x01** in byte **8**, `0x2a` responds **0x00**. The exact
semantics are unknown; from the ordering, `0x29` appears to close/commit a
flash session and `0x2a` to (re)arm config writes. The official app keeps a
bracket open with keep-alives for as long as it runs; N = 2–3 keep-alives per
bracket is verified to be sufficient.

### Keep-alive

Inside a flash session the official app sends this every 2000 ms; the payload
is the interval itself (u32, 0x07d0 = 2000).

Send

<table>
<tr>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4-5</th>
<th>6</th>
<th>7</th>
<th>8-11</th>
<th>12-63</th>
</tr>
<tr>
<td>ID</td>
<td>cmd</td>
<td>bytes count</td>
<td></td>
<td>page</td>
<td colspan="2">checksum</td>
<td>interval ms</td>
<td></td>
</tr>
<tr>
<td>01</td>
<td>2b</td>
<td>04</td>
<td>00</td>
<td>00 00</td>
<td>d7</td>
<td>00</td>
<td>d0 07 00 00</td>
<td>00</td>
</tr>
</table>

The response is the same as for [Init communication](#init-communication)
(**0xaa** in byte **8**).

### 0x29

Send

<table>
<tr>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3-63</th>
</tr>
<tr>
<td>ID</td>
<td>cmd</td>
<td>bytes count</td>
<td></td>
</tr>
<tr>
<td>01</td>
<td>29</td>
<td>00</td>
<td>00</td>
</tr>
</table>

Response

<table>
<tr>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
<th>9-63</th>
</tr>
<tr>
<td>ID</td>
<td>cmd</td>
<td>bytes count</td>
<td></td>
<td>unk</td>
<td></td>
<td colspan="2">checksum</td>
<td>status</td>
<td></td>
</tr>
<tr>
<td>01</td>
<td>29</td>
<td>01</td>
<td>00</td>
<td>xx</td>
<td>04</td>
<td>01</td>
<td>00</td>
<td>01</td>
<td>00</td>
</tr>
</table>

### 0x2a

Send

<table>
<tr>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3-63</th>
</tr>
<tr>
<td>ID</td>
<td>cmd</td>
<td>bytes count</td>
<td></td>
</tr>
<tr>
<td>01</td>
<td>2a</td>
<td>00</td>
<td>00</td>
</tr>
</table>

Response

<table>
<tr>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
<th>9-63</th>
</tr>
<tr>
<td>ID</td>
<td>cmd</td>
<td>bytes count</td>
<td></td>
<td>unk</td>
<td></td>
<td colspan="2">checksum</td>
<td>status</td>
<td></td>
</tr>
<tr>
<td>01</td>
<td>2a</td>
<td>01</td>
<td>00</td>
<td>xx</td>
<td>04</td>
<td>00</td>
<td>00</td>
<td>00</td>
<td>00</td>
</tr>
</table>
9 changes: 9 additions & 0 deletions src/controller/Controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ namespace OWC {
*/
[[nodiscard]] virtual bool writeConfig() const = 0;

/*!
* @brief persist the current in-RAM config to device flash
* @details commits changes so they survive a controller reset / reboot
* without physically toggling the mode switch. Default no-op for
* controllers that persist on write.
* @return true on success
*/
[[nodiscard]] virtual bool flushConfig() const { return true; }

/*!
* @brief reset the whole controller configuration memory
* @details writes a known working configuration buffer
Expand Down
Loading