diff --git a/ToDo.md b/ToDo.md
index a45f6f3..2edd6d9 100644
--- a/ToDo.md
+++ b/ToDo.md
@@ -1,5 +1,4 @@
# New features
-- firmware download from git repository where releases are generated
-- define how firmware should be downloaded, identified, etc.
-- add option to download firmware the legacy way but with ability to specify server, password, login and define the structure with device id
+- firmware download from git repository where releases are generated (define how firmware should be downloaded, identified, etc.)
+
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 65ec03b..3499108 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -74,14 +74,13 @@ Five modules encapsulating all business logic.
Key objects:
- `HEADER_SIZE = 48` — total header size on disk.
-- `DEVICE_HEADER_SIZE = 44` — header size sent to the device
- (without the `prevAppVersion` field).
+- `DEVICE_HEADER_SIZE = 48` — header size sent to the device (identical to the file header).
- `FirmwareHeader` — dataclass with fields and helpers (`format_product_id`,
`license_id`, `unique_id`, `payload_size`).
- `parse_header(bytes) -> FirmwareHeader` — parses the first 48 bytes.
- `load_firmware(path) -> (FirmwareHeader, bytes)` — parses from file.
-- `build_device_header(bytes) -> bytes` — strips `prevAppVersion` before
- transmission.
+- `build_device_header(bytes) -> bytes` — returns the 48-byte wire header
+ (identical to the file header).
- `split_pages(payload, page_size) -> list[bytes]` — splits payload into
equal pages; an incomplete final page is dropped (matching C++ behaviour).
@@ -407,7 +406,7 @@ offset size field
4 4 productId (MSB)
8 4 productId (LSB)
12 4 appVersion
- 16 4 prevAppVersion ← stripped before sending to device
+ 16 4 prevAppVersion
20 4 pageCount
24 4 flashPageSize
28 16 IV
@@ -415,7 +414,7 @@ offset size field
48 … encrypted pages
```
-Wire header = bytes `[0:16] + [20:48]` = 44 B (sent with the `START` command).
+Wire header = bytes `[0:48]` = 48 B (sent with the `START` command, identical to the file header).
### Product ID Convention
@@ -459,7 +458,7 @@ operations (compatibility check, device matching) the full 64-bit value is used.
After ACK for `GET_VERSION` the device sends 16 bytes of device info
(`u32 bootloaderVersion`, `u64 productId`, `u32 flashPageSize`).
-The host sends `START` (`0x02`) immediately followed by the 44 B wire header
+The host sends `START` (`0x02`) immediately followed by the 48 B wire header
as a single transmission. The device responds ACK/NAK. Then for each page the
host sends `NEXT_PAGE` (`0x03`) + `flashPageSize` bytes, and the device ACKs.
diff --git a/docs/FIRMWARE_FORMAT.md b/docs/FIRMWARE_FORMAT.md
index 25d0be7..c0d6212 100644
--- a/docs/FIRMWARE_FORMAT.md
+++ b/docs/FIRMWARE_FORMAT.md
@@ -14,7 +14,7 @@ Host implementation: [`src/secure_loader/core/firmware.py`](https://github.com/n
2. [Header Layout (48 B)](#-header-layout-48-b)
3. [Field Semantics](#-field-semantics)
4. [productId Derived Fields](#-productid-derived-fields)
-5. [Wire Header (44 B)](#-wire-header-44-b)
+5. [Wire Header (48 B)](#-wire-header-48-b)
6. [Payload](#-payload)
7. [Host Parsing API](#-host-parsing-api)
@@ -89,7 +89,7 @@ informational purposes. Not validated by the bootloader protocol.
Version of the previous application image. Used by the host to request an
older firmware from the HTTP source (`fetch_previous`).
GitHub Releases support is planned — see [Roadmap](GITHUB_SOURCE_MIGRATION.md).
-This field is **not** sent to the device — see [Wire Header](#-wire-header-44-b).
+This field is transmitted to the device as part of the 48-byte wire header — see [Wire Header](#-wire-header-48-b).
### `pageCount` (u32)
@@ -148,38 +148,36 @@ for the full fetch workflow.
---
-## 📡 Wire Header (44 B)
+## 📡 Wire Header (48 B)
-The header that the device receives during a firmware update is **not** the
-full 48-byte disk header. The `prevAppVersion` field (bytes 16–19) is
-stripped before transmission.
+The header transmitted to the device during a firmware update is identical to
+the full 48-byte file header. All fields — including `prevAppVersion` — are
+sent as-is.
```mermaid
flowchart LR
- subgraph disk["Disk header — 48 B"]
+ subgraph disk["File header — 48 B"]
A["bytes 0–15
protocolVersion
productId MSB + LSB
appVersion"]
B["bytes 16–19
prevAppVersion"]
C["bytes 20–47
pageCount · flashPageSize
IV · crc32"]
end
- subgraph wire["Wire header — 44 B (sent to device)"]
+ subgraph wire["Wire header — 48 B (sent to device)"]
W["START command payload"]
end
A -- "copied" --> W
+ B -- "copied" --> W
C -- "copied" --> W
- B -. "❌ stripped" .- W
-
- style B fill:#ffe0e0,stroke:#cc6666
```
Expressed as a slice operation:
```
-wire_header = disk_header[0:16] + disk_header[20:48]
+wire_header = disk_header[0:48]
= protocolVersion + productId (MSB+LSB) + appVersion
- + pageCount + flashPageSize + IV + crc32
+ + prevAppVersion + pageCount + flashPageSize + IV + crc32
```
-This 44-byte block is the payload of the `START` command.
+This 48-byte block is the payload of the `START` command.
---
@@ -205,7 +203,7 @@ flowchart LR
file["📄 .bin file"] --> lf["load_firmware(path)"]
lf --> hdr["FirmwareHeader\n(frozen dataclass)"]
lf --> raw["raw bytes"]
- raw --> bdh["build_device_header(raw)\n→ 44 B wire header"]
+ raw --> bdh["build_device_header(raw)\n→ 48 B wire header"]
raw --> sp["split_pages(payload, page_size)\n→ list[bytes]"]
hdr --> fields["license_id · unique_id\npayload_size\nformat_product_id() · …"]
```
@@ -214,7 +212,7 @@ flowchart LR
from secure_loader.core.firmware import (
parse_header, # parse_header(data: bytes) -> FirmwareHeader
load_firmware, # load_firmware(path) -> (FirmwareHeader, bytes)
- build_device_header, # build_device_header(raw: bytes) -> bytes (44 B)
+ build_device_header, # build_device_header(raw: bytes) -> bytes (48 B)
split_pages, # split_pages(payload, page_size) -> list[bytes]
)
```
diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md
index 8352713..dde58bf 100644
--- a/docs/PROTOCOL.md
+++ b/docs/PROTOCOL.md
@@ -51,7 +51,7 @@ delimiter.
| Name | Code | Payload | Meaning |
|------|------|---------|---------|
| `GET_VERSION` | `0x01` | — | Request device info |
-| `START` | `0x02` | 44 B wire header | Begin firmware transfer |
+| `START` | `0x02` | 48 B wire header | Begin firmware transfer |
| `NEXT_PAGE` | `0x03` | `flashPageSize` B | One payload page |
| `RESET` | `0x04` | — | Soft-reset the device |
| `NONE` | `0x00` | — | Reserved, unused |
@@ -100,8 +100,8 @@ One rule covers all commands — no translation table needed on either side.
### `START` (`0x02`)
-**Host sends:** `0x02`, then the **44-byte wire header**
-(see [Firmware Format — Wire Header](FIRMWARE_FORMAT.md#-wire-header-44-b)).
+**Host sends:** `0x02`, then the **48-byte wire header**
+(see [Firmware Format — Wire Header](FIRMWARE_FORMAT.md#-wire-header-48-b)).
**Device responds:**
@@ -157,7 +157,7 @@ sequenceDiagram
Note over H: validates compatibility
(bootloaderVersion == protocolVersion, productId match)
- H->>D: 0x02 START + 44 B wire header
+ H->>D: 0x02 START + 48 B wire header
D->>H: 0x42 ACK (flash erased, decryption ready)
loop pageCount pages
@@ -272,7 +272,7 @@ stateDiagram-v2
[*] --> WAIT_CMD : power on / reset
WAIT_CMD --> WAIT_CMD : 0x01 GET_VERSION → reply 0x41 + 16 B info
- WAIT_CMD --> READ_HEADER : 0x02 START → read 44 B wire header
+ WAIT_CMD --> READ_HEADER : 0x02 START → read 48 B wire header
WAIT_CMD --> [*] : 0x04 RESET → reply 0x44 · reset
READ_HEADER --> WAIT_CMD : header invalid → 0x82 NAK
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index b701edc..cfa53bf 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -318,7 +318,7 @@ The command sequence:
1. Opens the serial port and polls `GET_VERSION` every 500 ms until the bootloader responds.
2. Reads device info (bootloader version, product ID, page size).
3. Checks compatibility — product ID and protocol version must match the firmware header (unless `--force`).
-4. Sends `START` with the 44-byte wire header.
+4. Sends `START` with the 48-byte wire header.
5. Streams all `pageCount` pages, reporting progress to stdout.
6. Reports success or failure.
diff --git a/src/secure_loader/core/firmware.py b/src/secure_loader/core/firmware.py
index 0bb2868..bc25318 100644
--- a/src/secure_loader/core/firmware.py
+++ b/src/secure_loader/core/firmware.py
@@ -19,10 +19,9 @@
The 64-bit productId is reconstructed as ``(MSB << 32) | LSB``.
-The header that the device actually receives during a firmware update does
-**not** contain ``prevAppVersion`` — that field is stripped before transmission
-(see :class:`DeviceHeader`). ``prevAppVersion`` is only used by the host-side
-downloader to request the previous version from a remote source.
+The wire header sent to the device during a firmware update is identical to
+the full 48-byte file header. ``prevAppVersion`` is transmitted and the
+bootloader struct now includes it (bootloader v1.2+).
"""
from __future__ import annotations
@@ -38,8 +37,8 @@
HEADER_SIZE: int = 48
"""Total size of the firmware header in bytes."""
-DEVICE_HEADER_SIZE: int = 44
-"""Size of the header sent to the device (header without ``prevAppVersion``)."""
+DEVICE_HEADER_SIZE: int = HEADER_SIZE
+"""Size of the header sent to the device (identical to the file header, 48 bytes)."""
IV_SIZE: int = 16
"""Size of the initialization vector in bytes."""
@@ -214,24 +213,16 @@ def load_firmware(path: str | Path) -> tuple[FirmwareHeader, bytes]:
def build_device_header(raw: bytes | bytearray) -> bytes:
- """Build the header that is actually transmitted to the device.
+ """Return the 48-byte wire header transmitted to the device during CMD_START.
- Wire header = bytes ``[0:16]`` (protocol + productId + appVersion)
- concatenated with bytes ``[20:48]`` (pageCount + pageLen + IV + CRC),
- dropping the 4-byte ``prevAppVersion`` field.
+ The wire header is identical to the file header — all fields including
+ ``prevAppVersion`` are transmitted (bootloader struct is 48 bytes).
"""
if len(raw) < HEADER_SIZE:
raise FirmwareFormatError(
f"firmware too short: need at least {HEADER_SIZE} bytes, got {len(raw)}"
)
- first = bytes(raw[0:16])
- second = bytes(raw[20:HEADER_SIZE])
- wire = first + second
- if len(wire) != DEVICE_HEADER_SIZE:
- raise FirmwareFormatError(
- f"built device header is {len(wire)} bytes, expected {DEVICE_HEADER_SIZE}"
- )
- return wire
+ return bytes(raw[0:HEADER_SIZE])
def split_pages(payload: bytes, page_size: int) -> list[bytes]:
diff --git a/tests/test_firmware.py b/tests/test_firmware.py
index a2f250b..2e5bbf1 100644
--- a/tests/test_firmware.py
+++ b/tests/test_firmware.py
@@ -68,13 +68,19 @@ def test_accepts_extra_trailing_bytes(self, sample_header_bytes: bytes) -> None:
class TestBuildDeviceHeader:
- def test_strips_prev_app_version_field(self, sample_firmware: bytes) -> None:
+ def test_wire_header_is_full_48_byte_file_header(self, sample_firmware: bytes) -> None:
wire = build_device_header(sample_firmware)
+ assert len(wire) == 48
assert len(wire) == DEVICE_HEADER_SIZE
- # First 16 bytes of the file are transmitted verbatim.
- assert wire[:16] == sample_firmware[:16]
- # Then comes [20:48] — the 4 bytes at [16:20] (prevAppVersion) are skipped.
- assert wire[16:] == sample_firmware[20:HEADER_SIZE]
+ # Wire header is identical to the first HEADER_SIZE bytes of the file.
+ assert wire == sample_firmware[:HEADER_SIZE]
+
+ def test_prev_app_version_present_at_offset_16(self, sample_firmware: bytes) -> None:
+ wire = build_device_header(sample_firmware)
+ # prevAppVersion is at bytes [16:20] in both the file and the wire header.
+ assert wire[16:20] == sample_firmware[16:20]
+ # Confirm the value is non-zero (matches the fixture's 0x01020300).
+ assert wire[16:20] != b"\x00\x00\x00\x00"
def test_rejects_short_blobs(self) -> None:
with pytest.raises(FirmwareFormatError):