From 50e8b4773bd051bd6ddc3f43a84474a9f7b3bcc3 Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:11:11 -0400 Subject: [PATCH 1/5] chore(protocol): vendor opendisplay_structs.h into include/ Pull the new canonical opendisplay_structs.h (config/message payload layout contract, OD_STRUCTS_VERSION 2.0) from opendisplay-protocol into include/, alongside the already-in-sync opendisplay_protocol.h it #includes. sync_protocol_header.py does not yet track structs.h. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/opendisplay_structs.h | 1242 +++++++++++++++++++++++++++++++++ 1 file changed, 1242 insertions(+) create mode 100644 include/opendisplay_structs.h diff --git a/include/opendisplay_structs.h b/include/opendisplay_structs.h new file mode 100644 index 0000000..4293bfb --- /dev/null +++ b/include/opendisplay_structs.h @@ -0,0 +1,1242 @@ +/* ========================================================================== + * opendisplay_structs.h -- OpenDisplay config & message wire-payload contract + * ========================================================================== + * + * PURPOSE + * Single source of truth for the LAYOUT of every payload that travels on the + * OpenDisplay BLE wire: the config-transfer TLV packets (CONFIG_WRITE / + * CONFIG_READ bodies), the fixed-size opcode message payloads (PIPE, partial + * write, auth, LED flash), and the BLE manufacturer-specific advertisement. + * Where opendisplay_protocol.h owns the FRAMING (opcodes, response/status + * bytes, errors, envelope), THIS file owns the CONTENTS -- the real C enums, + * bitfield definitions, and packed structs that describe those bytes. + * + * A new engineer or an AI agent should be able to hand-decode a captured + * config blob or advertisement BYTE-BY-BYTE from THIS FILE ALONE: every field + * is documented in wire order with its meaning, valid values, units, default, + * and endianness. This header is also the CODEGEN SOURCE -- the machine- + * readable `@tag` annotations drive generation of the Swift / Python / JS + * type mirrors (see docs/shared-types-plan.md); codegen must carry the prose + * through as idiomatic doc comments in each target language. + * + * OD_STRUCTS_VERSION 2.0 (MAJOR.MINOR spec marker for THIS file; see + * VERSIONING POLICY below -- NOT sent on the wire) + * LAST CHANGED 2026-07-18 + * + * The two version schemes carried here are DISTINCT (see §6 Q2 of the plan): + * - OD_STRUCTS_VERSION_* : documents this spec file, like protocol.h's + * OD_PROTOCOL_VERSION. A compatibility marker; never transmitted. + * - OD_CONFIG_VERSION / OD_CONFIG_MINOR_VERSION : the ON-WIRE config-format + * version (currently 1 / 4). The OuterPacketHeader.version byte transmits + * the MAJOR; the minor is negotiated/inspected by peers. + * + * -------------------------------------------------------------------------- + * VERSIONING POLICY (same shape as opendisplay_protocol.h, scoped to PAYLOAD + * layout instead of framing) + * -------------------------------------------------------------------------- + * OD_STRUCTS_VERSION is MAJOR.MINOR and describes the payload SPEC in this + * file. It is a compatibility / documentation marker, NOT transmitted. + * + * Bump MAJOR (x.0) for a BREAKING layout change -- one that makes a peer + * built against the previous version misread payload bytes: + * - moving, resizing, renumbering, or re-typing an existing field; + * - changing a field's endianness or a struct's total wire size; + * - removing or renumbering an enum value / bit / packet-type id; + * - changing the packed layout / offset of any field. + * Reset MINOR to 0 on every MAJOR bump. + * + * Bump MINOR (2.x) for a BACKWARD-COMPATIBLE addition -- old peers keep + * working unchanged: + * - naming a byte carved out of a reserved[] block (the bytes were already + * must-be-zero, so old peers still interop) -- e.g. WifiConfig's server + * fields, the DisplayConfig cs_pin_2 promotion; + * - adding a NEW enum value, bitfield bit, or packet-type id that leaves + * existing ones untouched; + * - appending a NEW optional trailing field old parsers ignore. + * + * NO bump for comment / prose clarifications, or renaming a field to its + * canonical spelling when the byte offset and size are unchanged. + * + * Rule of thumb -- "could a peer on the previous version misread the bytes?" + * yes -> MAJOR + * no, but something new added -> MINOR + * neither -> no bump + * + * -------------------------------------------------------------------------- + * CHANGELOG (newest first; entries accrue under "Unreleased" and roll into a + * new version heading on each bump -- see AGENT INSTRUCTIONS below) + * -------------------------------------------------------------------------- + * Unreleased (since 2.0) + * - Doc-only: fixed two comment shapes the codegen parser mis-read and added + * the CODEGEN AUTHORING RULES banner section to prevent recurrence. Split the + * combined BusFlags/PinBitmap @bits comment into one comment per group; folded + * OuterPacketHeader's trailing FOLLOWUP note inside its @doc quote. No wire + * change; regenerated the language mirrors. + * - Named the skipped/reserved bits inside two bitfield groups with explicit + * placeholder macros (no wire change; these bits stay reserved-must-be-0): + * TransmissionModes bit5/bit6 -> OD_TRANSMISSION_MODE_RESERVED_5/_6; + * MsdStatusBits bit3 -> OD_MSD_STATUS_RESERVED_3. Documentation only. + * - Add each payload-layout change here as it lands. On the next version bump, + * move these under a new "MAJOR.MINOR (YYYY-MM-DD)" heading. + * + * 2.0 (2026-07-18) + * - Initial canonical shared payload contract: the wire-payload counterpart + * to opendisplay_protocol.h, reconciling the per-repo opendisplay_structs.h + * / structs.h supersets and the website config.yaml (ble_proto v1.4) into + * one self-documenting header vendored into all firmware repos. + * - 15 config TLV packet structs + 2 framing structs + 9 message/advert + * structs, all packed with sizeof static-asserts; 19 value enums; the + * bitfield groups; and the loose wire constants (OD_CONFIG_VERSION pair, + * CRC constants, OD_PIN_UNUSED). + * - Reconciliations (superset of every source; total sizes unchanged): + * SystemConfig pwr_pin_2/3 named; ManufacturerData simple_config_* named; + * PowerOption charge/min_wake/screen_timeout tail named; DisplayConfig + * cs_pin_2 + full_update_mC + legacy_tag_type; SensorData i2c_addr_7bit + + * msd_data_start_byte; BinaryInputs input_pin_1..8 / pins_used + + * power_off_flags / power_off_hold_sec. + * - ColorScheme value 7 = SEVEN_COLOR (config.yaml "7color"); the former + * firmware COLOR_SCHEME_GRAY8 = 7 was a mistake and is dropped outright + * (no gray8 at any value). + * - WifiConfig extended AHEAD of firmware: server_host[64] + server_port + * (big-endian) promoted out of reserved[] (backward-compatible carve). + * - Message payloads (previously hand-parsed and drifting in Swift/JS) given + * a single machine-readable home: LedFlashPattern, MsdAdvertisement, + * PipeStartRequest/Response, PipePartialExt, PipeSack, + * PartialWriteStartHeader (all-big-endian geometry), AuthChallenge, + * AuthProof. + * + * -------------------------------------------------------------------------- + * AGENT INSTRUCTIONS -- changelog upkeep (perform on EVERY edit to this file) + * -------------------------------------------------------------------------- + * 1. Set LAST CHANGED (top) to the date of your edit. + * 2. Add a bullet describing the change under "Unreleased (since x.y)". + * 3. Classify the change via the VERSIONING POLICY above: layout-breaking => + * MAJOR, backward-compatible addition => MINOR, doc-only => no bump. + * 4. VALIDATE before finishing: confirm the accumulated "Unreleased" entries + * are consistent with OD_STRUCTS_VERSION. When a bump is warranted, update + * OD_STRUCTS_VERSION_MAJOR / _MINOR / _STR, add a new "MAJOR.MINOR + * (YYYY-MM-DD)" heading, move the Unreleased entries beneath it (reset + * MINOR to 0 on a MAJOR bump), and leave "Unreleased" empty. + * 5. If a change alters the ON-WIRE format, also bump OD_CONFIG_VERSION / + * OD_CONFIG_MINOR_VERSION and note it -- that pair is independent of the + * spec marker and is what peers actually negotiate. + * 6. Never delete or rewrite historical entries -- the changelog is append-only. + * 7. Every packed struct MUST keep its trailing + * OD_STATIC_ASSERT(sizeof(struct X) == N, ...); if you change a layout, + * change the asserted size to match (and treat it as a MAJOR change). + * + * CANONICAL LOCATION + * opendisplay-protocol/src/opendisplay_structs.h + * + * VENDORED COPY IN FIRMWARE REPOS -- DO NOT EDIT THERE. + * Sync every copy byte-for-byte via tools/sync_protocol_header.py (same + * mechanism as opendisplay_protocol.h; same include guard so the vendored + * file is a drop-in replacement for each repo's existing opendisplay_structs.h). + * + * -------------------------------------------------------------------------- + * LANGUAGE / LINKAGE RULE (INVERTED from opendisplay_protocol.h) + * -------------------------------------------------------------------------- + * opendisplay_protocol.h is macro-only. THIS header is the opposite: it + * intentionally CONTAINS real `enum`s and packed `struct`s -- that is its + * purpose (they are the codegen source). It must still compile clean as BOTH + * C99 and C++, the same gate as protocol.h: + * cc -std=c99 -fsyntax-only src/opendisplay_structs.h + * c++ -fsyntax-only src/opendisplay_structs.h + * Constraints that keep it portable and drop-in: + * - Enums are PLAIN (not `enum class`); every enumerator has an EXPLICIT + * value and an `OD_` prefix (unprefixed spellings such as + * COLOR_SCHEME_MONO are live macros in the firmware repos, so an + * `OD_`-prefixed enumerator lets both coexist during migration). + * - Struct and field names match the firmware repos EXACTLY (DisplayConfig, + * instance_number, ...) so the vendored header is a drop-in. + * - NO functions. NO typedefs of RAM-only / in-memory types. NO + * repo-specific values (GPIO pin values, buffer sizes, GlobalConfig, + * EncryptionSession, ImageData, PipeWriteState, ButtonState). Those move + * to a repo-local header on adoption -- mirror of protocol.h's "no + * typedefs here" rule, inverted: "no in-memory / RAM-only types here". + * - This header #includes opendisplay_protocol.h and NEVER redeclares a name + * it already `#define`s. protocol.h defines OD_NFC_IC_* as MACROS; an + * `enum` member of the same spelling would be textually rewritten by the + * macro and fail to compile. So the nfc_ic_type field is @enum-bound to + * protocol.h's OD_NFC_IC_* rather than re-enumerated here; likewise pipe + * flags bind to protocol.h's PIPE_FLAG_*. + * + * -------------------------------------------------------------------------- + * BLOB-WIDE CONVENTIONS (from config.yaml `meta`; hold for EVERY struct here) + * -------------------------------------------------------------------------- + * (1) BYTE ORDER is LITTLE-ENDIAN by default. Multi-byte fields are LE unless + * a field carries an `@endian be` tag. The big-endian exceptions in this + * header are: WifiConfig.server_port, and the ENTIRE PartialWriteStartHeader + * (0x76) geometry. (Note the PIPE 0x80 partial extension packs the same + * geometry LITTLE-endian -- see PipePartialExt.) + * (2) All wire structs are PACKED (__attribute__((packed)); no implicit + * padding between fields. + * (3) RESERVED fields MUST be written as 0 and are ignored by older parsers + * (this is what makes carving names out of reserved[] backward-compatible). + * (4) BITFIELDS number from bit 0 = LSB. Unused bits MUST be 0. + * + * Two more file-wide notes: + * - OuterPacketHeader.length FOLLOWUP: the leading u16 `length` is populated + * inconsistently across encoders -- the website toolbox patches it to the + * real total, but some encoders leave it as a zero pad. The CRC is ALWAYS + * computed as if `length` were 0x0000 regardless. Do not rely on `length` + * until the contract is pinned; see OuterPacketHeader below. + * - Silicon-specific pin ENCODINGS (e.g. nRF54 (port<<4)|pin, Silabs 0xPN) + * are target-defined: every pin field here is a raw u8; its bit encoding + * is decided by the receiving firmware, not by this header. + * - The image dither-algorithm ids (DitherMode) are owned by epaper-dithering + * and are NOT wire values -- they appear in no packet here and are omitted. + * + * -------------------------------------------------------------------------- + * TAG CONVENTION (machine-readable annotations for codegen; live in the SAME + * trailing comment as the human prose. grep-able, line-anchored.) + * @packet 0xNN this struct is config TLV packet-type 0xNN + * @message CMD_X this struct is the fixed payload of opcode CMD_X + * @required config packet MUST be present in a valid blob + * @repeatable [max=N] config packet MAY repeat, up to N instances (absent => 1) + * @enum field's values are the named enum (may reference protocol.h) + * @bits field is a bitfield of the named group + * @endian le|be multi-byte field byte order (default le; be must be tagged) + * @reserved must-be-zero forward-compat padding + * @unit physical unit of the value (px, mm, mC, ms, s, mAh, uA) + * @min/@max/@default bounded-integer range / default value + * @since on-wire config version the field/value first appeared in + * @changed "" wire-visible change history for the item + * @width intended on-wire width of an enum + * @external enum mirrors / relates to an outside component + * @doc "" human prose (also the primary deliverable of this file) + * + * -------------------------------------------------------------------------- + * CODEGEN AUTHORING RULES (AGENT INSTRUCTIONS -- keep tools/structs_model.py able + * to parse this file; the stdlib parser hard-errors or MIS-ATTRIBUTES on anything + * outside this shape, and the Python/JS/... mirrors are GENERATED from here) + * -------------------------------------------------------------------------- + * A. One shape per construct, one declaration per line (no `uint8_t a, b;`): + * - enum Name { OD_X = , ... }; explicit values, OD_-prefixed. + * - struct Name { f; f[]; ... } + * __attribute__((packed)); fixed-width scalars/arrays ONLY -- NO C + * bitfields (uint8_t x:3), unions, nested structs, or macro-sized arrays. + * - #define OD_..._NAME (1u << N) for bit flags (mask/shift consts allowed). + * B. Every packed struct ENDS with OD_STATIC_ASSERT(sizeof(struct X) == N, ...); + * the parser sums the field widths and validates them against N -- that assert + * IS the layout oracle (it replaces a compiler front-end). Keep it exact. + * C. Each @bits group gets its OWN group comment. Do NOT describe two @bits groups + * in one comment block: the parser attaches one comment per group, so a shared + * block mis-labels the later group. (Regression fixed 2026-07-18.) + * D. ALL prose that must reach the generated docs goes INSIDE the `@doc "..."` + * quotes. Text AFTER the closing quote (trailing notes, `FOLLOWUP:` lines) is + * DROPPED by the parser -- fold such notes into the quoted @doc. (Fixed 2026-07-18.) + * E. Never redeclare a name opendisplay_protocol.h `#define`s; bind fields to it + * with @enum instead (see LANGUAGE / LINKAGE RULE above). + * F. AFTER ANY EDIT to values/layout/docs, regenerate the mirrors and confirm no + * drift (do this in the same change, like protocol.h's gen tools): + * python3 tools/gen_python_structs.py --write # then commit the .py + * python3 tools/gen_python_structs.py --check # CI gate; exit 1 on drift + * (add the same for future JS/Swift emitters as they land). + * ========================================================================== */ + +#ifndef OPENDISPLAY_STRUCTS_H +#define OPENDISPLAY_STRUCTS_H + +#include +#include +#include "opendisplay_protocol.h" /* framing: CMD_*, RESP_*, OD_NFC_IC_*, PIPE_FLAG_*, ... */ + +/* Payload-spec revision, MAJOR.MINOR. Documents THIS file; NOT sent on the wire. + * Distinct from the on-wire OD_CONFIG_VERSION pair defined in SECTION 1. */ +#define OD_STRUCTS_VERSION_MAJOR 2u +#define OD_STRUCTS_VERSION_MINOR 0u +#define OD_STRUCTS_VERSION_STR "2.0" + +/* -------------------------------------------------------------------------- + * Portable compile-time size check. Defined once here; every packed struct is + * followed by OD_STATIC_ASSERT(sizeof(struct X) == N, "..."). The C99 fallback + * uses a __LINE__-uniquified typedef name so repeated asserts do not collide + * (a fixed typedef name warns "redefinition of typedef is a C11 feature" under + * -std=c99, which is the gate used for this header). + * -------------------------------------------------------------------------- */ +#if defined(__cplusplus) + #define OD_STATIC_ASSERT(expr, msg) static_assert(expr, msg) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L + #define OD_STATIC_ASSERT(expr, msg) _Static_assert(expr, msg) +#else + #define OD_STATIC_ASSERT_CAT_(a, b) a##b + #define OD_STATIC_ASSERT_CAT(a, b) OD_STATIC_ASSERT_CAT_(a, b) + #define OD_STATIC_ASSERT(expr, msg) \ + typedef char OD_STATIC_ASSERT_CAT(od_static_assert_, __LINE__)[(expr) ? 1 : -1] +#endif + +/* ========================================================================== + * SECTION 1 -- CONFIG TRANSFER FRAMING + * ========================================================================== + * A full config transfer is one OUTER packet: + * [OuterPacketHeader][single_packet]...[single_packet][crc:2 LE] + * where each single_packet is: + * [SinglePacketHeader][payload] + * The payload is a fixed-size struct chosen by SinglePacketHeader.id (see the + * ConfigPacketType enum in SECTION 2). The whole outer packet is what a + * CONFIG_WRITE (opcode 0x0041, opendisplay_protocol.h) transmits and a + * CONFIG_READ (0x0040) returns. + * ========================================================================== */ + +/* On-wire config-format version. UNLIKE OD_STRUCTS_VERSION / OD_PROTOCOL_VERSION, + * these ARE transmitted / negotiated: OuterPacketHeader.version carries the + * MAJOR byte. Frozen at 1.4 by this header (the app's bundled config.yaml is at + * minor 3 and must catch up). */ +#define OD_CONFIG_VERSION 1u /* @doc "outer-packet major version byte" */ +#define OD_CONFIG_MINOR_VERSION 4u /* @doc "config-format minor; backward-compatible additions" */ + +/* CRC over the outer packet. CRC16-CCITT, poly 0x1021, init 0xFFFF, computed + * over length+version+packets AS IF the 2-byte length field were 0x0000 (the + * toolbox computes the CRC before patching length), with the 2-byte CRC field + * itself excluded. This "length treated as zero" quirk is mandatory to match. */ +#define OD_CONFIG_CRC_POLY 0x1021u /* @doc "CRC16-CCITT polynomial" */ +#define OD_CONFIG_CRC_INIT 0xFFFFu /* @doc "CRC16-CCITT initial value" */ + +/* The pervasive "pin not present" sentinel. A pin field set to 0xFF means the + * pin is absent / unused (some fields also treat 0 as unused -- noted per field). + * Wire-meaningful, so canonical here (was GPIO_PIN_UNUSED in the repos). */ +#define OD_PIN_UNUSED 0xFFu /* @doc "pin field value meaning 'no pin'" */ + +/** @packet outer @doc "Wraps the whole transfer: a length+version header, then a + * sequence of single_packet entries, then a trailing CRC (NOT part of this + * struct -- it is the last 2 bytes of the outer packet). FOLLOWUP: `length` is + * populated inconsistently (the website toolbox patches it to the real total incl. + * CRC; some encoders leave it a zero pad -> [00 00][version]...). The CRC is always + * computed as if `length` were 0x0000. Verify whether any receiver relies on + * `length` before firmware depends on it." */ +struct OuterPacketHeader { + uint16_t length; /**< @endian le @doc "total outer-packet length INCLUDING the trailing CRC. Inconsistently populated -- may be a real total or a zero pad; the CRC is computed as if this were 0x0000 either way." */ + uint8_t version; /**< @enum OD_CONFIG_VERSION @doc "config-format MAJOR version = OD_CONFIG_VERSION (1). The minor is not carried in the header." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct OuterPacketHeader) == 3, "OuterPacketHeader wire size"); + +/** @packet single @doc "Header preceding each config payload inside the outer + * packet. The payload that follows is a fixed-size struct whose size and layout + * are determined entirely by `id` (see ConfigPacketType); there is no per-packet + * length field -- sizeof the selected struct IS the payload length." */ +struct SinglePacketHeader { + uint8_t number; /**< @doc "0-based sequential packet index within the outer packet." */ + uint8_t id; /**< @enum ConfigPacketType @doc "packet-type id selecting the payload struct." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct SinglePacketHeader) == 2, "SinglePacketHeader wire size"); + +/* ========================================================================== + * SECTION 2 -- CONFIG PACKET-TYPE IDS + * ========================================================================== + * The value of SinglePacketHeader.id. Canonical replacement for the per-repo + * CONFIG_PKT_* macros and the comment-only table in opendisplay_protocol.h + * SECTION 1. Each id's payload struct, required-ness, and repeatability are + * documented on the struct itself in SECTION 4 (via @packet / @required / + * @repeatable), not duplicated here. + * ========================================================================== */ + +/** @enum ConfigPacketType @width 1 + * @doc "Config TLV packet-type identifiers (SinglePacketHeader.id)." */ +enum ConfigPacketType { + OD_PKT_SYSTEM = 0x01, /**< @doc "system_config (required, singleton)" */ + OD_PKT_MANUFACTURER = 0x02, /**< @doc "manufacturer_data (required, singleton)" */ + OD_PKT_POWER = 0x04, /**< @doc "power_option (required, singleton)" */ + OD_PKT_DISPLAY = 0x20, /**< @doc "display (repeatable, max 4)" */ + OD_PKT_LED = 0x21, /**< @doc "led (repeatable, max 4)" */ + OD_PKT_SENSOR = 0x23, /**< @doc "sensor_data (repeatable, max 4)" */ + OD_PKT_DATA_BUS = 0x24, /**< @doc "data_bus (repeatable, max 4)" */ + OD_PKT_BINARY_INPUT = 0x25, /**< @doc "binary_inputs (repeatable, max 4)" */ + OD_PKT_WIFI = 0x26, /**< @doc "wifi_config (singleton)" */ + OD_PKT_SECURITY = 0x27, /**< @doc "security_config (singleton)" */ + OD_PKT_TOUCH = 0x28, /**< @doc "touch_controller (repeatable, max 4)" */ + OD_PKT_BUZZER = 0x29, /**< @doc "buzzer (repeatable, max 4). Canonical name is BUZZER (matches Silabs), resolving the Silabs BUZZER vs NRF54/yaml PASSIVE_BUZZER split; config.yaml's packet name (passive_buzzer) is regenerated to buzzer." */ + OD_PKT_NFC = 0x2A, /**< @doc "nfc_config (repeatable, max 2)" */ + OD_PKT_FLASH = 0x2B, /**< @doc "flash_config (repeatable, max 2)" */ + OD_PKT_DATA_EXTENDED = 0x2C /**< @doc "data_extended (singleton)" */ +}; + +/* ========================================================================== + * SECTION 3 -- SHARED SCALAR ENUMS + * ========================================================================== + * Tiny enums reused across several packets. OD_PIN_UNUSED lives in SECTION 1. + * ========================================================================== */ + +/** @enum ActiveLevel @width 1 + * @doc "GPIO active-level polarity. Bound from NfcConfig.field_detect_active, + * NfcConfig.power_active, FlashConfig.power_active." */ +enum ActiveLevel { + OD_ACTIVE_LOW = 0, /**< @doc "signal asserted / present when the pin is LOW" */ + OD_ACTIVE_HIGH = 1 /**< @doc "signal asserted / present when the pin is HIGH" */ +}; + +/* ========================================================================== + * SECTION 4 -- CONFIG PAYLOAD PACKETS (in packet-id order) + * ========================================================================== + * Each block below = the packet's value enums, then its bitfield groups, then + * the packed payload struct + a sizeof assert. Read top-to-bottom, in wire + * order, to decode a whole config blob. All layouts are the reconciled SUPERSET + * of the firmware repos and config.yaml (ble_proto v1.4): every field any source + * promoted out of reserved[] is named; reserved[] shrinks; total sizes are + * unchanged from the on-wire format. + * ========================================================================== */ + +/* ----------------------------------------------------------------------- + * 0x01 system_config + * ----------------------------------------------------------------------- */ + +/** @enum ICType @width 2 @doc "Host MCU/IC of the device (SystemConfig.ic_type)." */ +enum ICType { + OD_IC_TYPE_NRF52840 = 1, /**< @doc "nRF52840-based boards" */ + OD_IC_TYPE_ESP32S3 = 2, /**< @doc "ESP32-S3-based boards" */ + OD_IC_TYPE_ESP32C3 = 3, /**< @doc "ESP32-C3-based boards" */ + OD_IC_TYPE_ESP32C6 = 4, /**< @doc "ESP32-C6-based boards" */ + OD_IC_TYPE_NRF52811 = 5, /**< @doc "nRF52811-based boards" */ + OD_IC_TYPE_EFR32BG22C222F352GM40 = 6, /**< @doc "Silicon Labs EFR32BG22 boards" */ + OD_IC_TYPE_NRF54L15 = 7, /**< @doc "Seeed XIAO nRF54L15 (Zephyr)" */ + OD_IC_TYPE_NRF54LM20 = 8 /**< @doc "Seeed XIAO nRF54LM20A (Zephyr)" */ +}; + +/** @enum ManufacturerId @width 2 @doc "Device manufacturer (ManufacturerData.manufacturer_id). + * Board-name tables (board_type) are intentionally NOT enumerated here -- they + * live only in config.yaml; board_type stays a raw u8 field." */ +enum ManufacturerId { + OD_MANUFACTURER_DIY = 0, /**< @doc "self-built / DIY devices" */ + OD_MANUFACTURER_SEEED = 1, /**< @doc "Seeed Studio" */ + OD_MANUFACTURER_WAVESHARE = 2, /**< @doc "Waveshare Electronics" */ + OD_MANUFACTURER_SOL = 3, /**< @doc "SOL" */ + OD_MANUFACTURER_OPENDISPLAY = 4 /**< @doc "OpenDisplay (free giveaways only)" */ +}; + +/* SystemConfig.communication_modes @bits CommunicationModes (bits 3-7 reserved). */ +#define OD_COMM_MODE_BLE (1u << 0) /* @doc "BLE transfer supported" */ +#define OD_COMM_MODE_OEPL (1u << 1) /* @doc "OEPL (OpenEPaperLink) transfer supported" */ +#define OD_COMM_MODE_WIFI (1u << 2) /* @doc "WiFi transfer supported" */ + +/* SystemConfig.device_flags @bits DeviceFlags (bits 5-7 reserved). */ +#define OD_DEVICE_FLAG_PWR_PIN (1u << 0) /* @doc "device has external power management for the display etc." */ +#define OD_DEVICE_FLAG_XIAO_INIT (1u << 1) /* @doc "apply Seeed XIAO low-power init" */ +#define OD_DEVICE_FLAG_WS_PP_INIT (1u << 2) /* @doc "apply Waveshare PhotoPainter low-power init" */ +#define OD_DEVICE_FLAG_PWR_LATCH (1u << 3) /* @doc "self-holding MOSFET latch on pwr_pin_2; optional long-press shutdown button on pwr_pin_3" */ +#define OD_DEVICE_FLAG_PWR_LATCH_DFF (1u << 4) /* @doc "74AHC1G79 D-FF latch: pwr_pin_2=D (hold), pwr_pin_3=CP (clock); command 0x0052 releases latch" */ + +/** @struct SystemConfig @packet 0x01 @required + * @doc "Host IC identity, communication capabilities, and power-management pins. + * Required singleton. 22 bytes." */ +struct SystemConfig { + uint16_t ic_type; /**< @enum ICType @endian le @doc "host MCU/IC of this device." */ + uint8_t communication_modes; /**< @bits CommunicationModes @doc "supported transfer modes (bitfield)." */ + uint8_t device_flags; /**< @bits DeviceFlags @doc "misc device / power-latch flags (bitfield)." */ + uint8_t pwr_pin; /**< @doc "primary power-management pin; 0xFF = not present." @default 0xFF */ + uint8_t reserved[15]; /**< @reserved @doc "future use; must be 0." */ + uint8_t pwr_pin_2; /**< @doc "aux enable / battery-latch hold / pwr_latch_dff D input; 0 or 0xFF = firmware default." */ + uint8_t pwr_pin_3; /**< @doc "aux enable / battery-latch shutdown button / pwr_latch_dff CP clock; 0 or 0xFF = none/default." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct SystemConfig) == 22, "SystemConfig wire size"); + +/* ----------------------------------------------------------------------- + * 0x02 manufacturer_data + * ----------------------------------------------------------------------- */ + +/** @struct ManufacturerData @packet 0x02 @required + * @doc "Manufacturer id, board identity, and the 'simple config' preset indices + * the website UI applied. Required singleton. 22 bytes." */ +struct ManufacturerData { + uint16_t manufacturer_id; /**< @enum ManufacturerId @endian le @doc "device manufacturer." */ + uint8_t board_type; /**< @doc "board identifier; meaning depends on manufacturer_id. Value table lives in config.yaml only (NOT enumerated here) -- raw u8." */ + uint8_t board_revision; /**< @doc "board revision number." */ + uint16_t simple_config_driver_index; /**< @endian le @doc "simple-config driver preset index (1-based; 0 = not set)." */ + uint16_t simple_config_display_index; /**< @endian le @doc "simple-config display preset index (1-based; 0 = not set)." */ + uint16_t simple_config_power_index; /**< @endian le @doc "simple-config power preset index (1-based; 0 = not set)." */ + uint8_t simple_config_configured_at[6]; /**< @endian le @unit s @doc "48-bit little-endian Unix time (seconds) when simple config was last applied." */ + uint8_t reserved[6]; /**< @reserved @doc "future use; must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct ManufacturerData) == 22, "ManufacturerData wire size"); + +/* ----------------------------------------------------------------------- + * 0x04 power_option + * ----------------------------------------------------------------------- */ + +/** @enum PowerMode @width 1 @doc "Power source (PowerOption.power_mode)." */ +enum PowerMode { + OD_POWER_MODE_BATTERY = 1, /**< @doc "battery powered" */ + OD_POWER_MODE_USB = 2, /**< @doc "USB powered" */ + OD_POWER_MODE_SOLAR = 3 /**< @doc "solar powered" */ +}; + +/** @enum CapacityEstimator @width 1 + * @doc "Battery chemistry model for state-of-charge estimation (PowerOption.capacity_estimator)." */ +enum CapacityEstimator { + OD_CAPACITY_EST_LI_ION = 1, /**< @doc "1S Li-ion" */ + OD_CAPACITY_EST_LIFEPO4 = 2, /**< @doc "1S LiFePO4" */ + OD_CAPACITY_EST_SUPERCAP = 3, /**< @doc "2S supercapacitor (4.5 V max, 3.6 V cutoff)" */ + OD_CAPACITY_EST_LITHIUM_PRIMARY = 4, /**< @doc "lithium primary (non-rechargeable)" */ + OD_CAPACITY_EST_SEEED_LI_ION = 5 /**< @doc "1S Li-ion, Seeed reTerminal E-series discharge curve" */ +}; + +/* PowerOption.sleep_flags @bits SleepFlags (bits 1-7 reserved). + * Firmware ships bit0; config.yaml still lists it reserved_0 (yaml is behind). */ +#define OD_SLEEP_FLAG_BUTTON_WAKE_DISABLE (1u << 0) /* @doc "opt out of button wake from timer deep sleep (buttons sharing wake-hostile pads)" */ + +/* PowerOption.battery_sense_flags @bits BatterySenseFlags (bits 1-7 reserved). + * Firmware ships bit0; config.yaml still lists it reserved_0 (yaml is behind). */ +#define OD_BATTERY_SENSE_FLAG_ENABLE_INVERTED (1u << 0) /* @doc "battery-sense enable is active-low (e.g. XIAO ~READ_BAT on P0.14)" */ + +/* PowerOption.charger_flags @bits ChargerFlags (bits 2-7 reserved). */ +#define OD_CHARGER_FLAG_ENABLE_ACTIVE_LOW (1u << 0) /* @doc "charge-enable (/CE) is active-low" */ +#define OD_CHARGER_FLAG_STATE_ACTIVE_LOW (1u << 1) /* @doc "charge-state (BQ25616 STAT) is active-low: charging when LOW" */ + +/** @struct PowerOption @packet 0x04 @required + * @doc "Power source, battery model, sleep/advertising timing, battery-sense and + * charger pins, and EPD keep-alive. Required singleton. 30 bytes." */ +struct PowerOption { + uint8_t power_mode; /**< @enum PowerMode @doc "power source type." */ + uint8_t battery_capacity_mah[3]; /**< @endian le @unit mAh @doc "battery capacity, 24-bit LE; 0 = unknown." */ + uint16_t sleep_timeout_ms; /**< @endian le @unit ms @doc "nominal awake/advertising time before sleep (actual may be shorter)." */ + uint8_t tx_power; /**< @doc "BLE transmit-power setting (platform-specific units)." */ + uint8_t sleep_flags; /**< @bits SleepFlags @doc "sleep-related flags (bitfield)." */ + uint8_t battery_sense_pin; /**< @doc "ADC pin measuring battery voltage; 0xFF = none." @default 0xFF */ + uint8_t battery_sense_enable_pin; /**< @doc "pin enabling the battery-sense divider; 0xFF = none." @default 0xFF */ + uint8_t battery_sense_flags; /**< @bits BatterySenseFlags @doc "battery-sense flags (bitfield)." */ + uint8_t capacity_estimator; /**< @enum CapacityEstimator @doc "battery chemistry model." */ + uint16_t voltage_scaling_factor; /**< @endian le @doc "ADC voltage scaling / divider factor (implementation-specific)." */ + uint32_t deep_sleep_current_ua; /**< @endian le @unit uA @doc "board deep-sleep current excl. MCU; 0 = unknown." */ + uint16_t deep_sleep_time_seconds; /**< @endian le @unit s @doc "timer deep-sleep duration (ESP32 on battery); 0 = disabled." */ + uint8_t charge_enable_pin; /**< @doc "BQ25616 charge-enable GPIO; 0 or 0xFF = unused." @default 0xFF */ + uint8_t charge_state_pin; /**< @doc "BQ25616 charge-state input GPIO; 0 or 0xFF = unused." @default 0xFF */ + uint8_t charger_flags; /**< @bits ChargerFlags @doc "charger polarity flags (bitfield)." */ + uint16_t min_wake_time_seconds; /**< @endian le @unit s @doc "min awake window after first boot / button wake; 0 = default 120 s." */ + uint8_t screen_timeout_seconds; /**< @unit s @min 0 @max 30 @doc "EPD keep-alive: seconds the panel stays powered after a refresh before shutdown (values >30 clamped; 0 = power off immediately; forced 0 when an AXP2101 PMIC is present)." */ + uint8_t reserved[4]; /**< @reserved @doc "future use; must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct PowerOption) == 30, "PowerOption wire size"); + +/* ----------------------------------------------------------------------- + * 0x20 display (external-related enums PanelIC + ColorScheme inline) + * ----------------------------------------------------------------------- */ + +/** @enum DisplayTechnology @width 1 @doc "Display technology (DisplayConfig.display_technology)." */ +enum DisplayTechnology { + OD_DISPLAY_TECH_UNDEFINED = 0, /**< @doc "undefined / unspecified" */ + OD_DISPLAY_TECH_E_PAPER = 1, /**< @doc "e-paper / ESL" */ + OD_DISPLAY_TECH_LCD = 2, /**< @doc "LCD" */ + OD_DISPLAY_TECH_LED_MATRIX = 3 /**< @doc "LED-matrix display" */ +}; + +/** @enum Rotation @width 1 @doc "Physical panel rotation (DisplayConfig.rotation)." */ +enum Rotation { + OD_ROTATION_0 = 0, /**< @doc "0 degrees" */ + OD_ROTATION_90 = 1, /**< @doc "90 degrees" */ + OD_ROTATION_180 = 2, /**< @doc "180 degrees" */ + OD_ROTATION_270 = 3 /**< @doc "270 degrees" */ +}; + +/** @enum PartialUpdateSupport @width 1 @doc "Panel partial-update capability (DisplayConfig.partial_update_support)." */ +enum PartialUpdateSupport { + OD_PARTIAL_UPDATE_NONE = 0, /**< @doc "only full updates supported" */ + OD_PARTIAL_UPDATE_SUPPORTED = 1, /**< @doc "partial updates supported" */ + OD_PARTIAL_UPDATE_FULL_FRAME = 2 /**< @doc "partial supported but a full-frame stream is required" */ +}; + +/** @enum ColorScheme @width 1 + * @external mirror: epaper-dithering packages/rust/core/src/palettes.rs (integer + * values MUST match; the crate declares itself a firmware mirror). informed-by: + * bb_epaper. The value->plane/packing meaning is OpenDisplay-owned here. + * @doc "display color/plane scheme (DisplayConfig.color_scheme); drives plane + * count and pixel packing." */ +enum ColorScheme { + OD_COLOR_SCHEME_MONO = 0, /**< @doc "1bpp black/white" */ + OD_COLOR_SCHEME_BWR = 1, /**< @doc "black/white/red" */ + OD_COLOR_SCHEME_BWY = 2, /**< @doc "black/white/yellow" */ + OD_COLOR_SCHEME_BWRY = 3, /**< @doc "black/white/red/yellow" */ + OD_COLOR_SCHEME_BWGBRY = 4, /**< @doc "Spectra 6-color (black/white/green/blue/red/yellow)" */ + OD_COLOR_SCHEME_GRAY4 = 5, /**< @doc "2bpp 4-level gray" */ + OD_COLOR_SCHEME_GRAY16 = 6, /**< @doc "4bpp 16-level gray" */ + OD_COLOR_SCHEME_SEVEN_COLOR = 7, /**< @doc "7-color (Spectra/ACeP 7, config.yaml '7color')" @changed "2.0: value 7 is SEVEN_COLOR; the former COLOR_SCHEME_GRAY8=7 was a mistake (gray8 is not a real scheme) and is removed -- not renumbered." */ + OD_COLOR_SCHEME_BWGBRY_SPLIT = 8, /**< @doc "Spectra 6 nibbles, left-half plane then right-half plane (dual-CS panels, no device framebuffer)" */ + OD_COLOR_SCHEME_RGB565 = 100, /**< @doc "RGB565 (non-epaper)" */ + OD_COLOR_SCHEME_RGB888 = 101, /**< @doc "RGB888 (non-epaper)" */ + OD_COLOR_SCHEME_RGB16BPC = 102 /**< @doc "16 bits per channel (non-epaper)" */ +}; + +/** @enum PanelIC @width 2 + * @external bb_epaper (0-76 names track bb_epaper EP* panel identifiers); the + * 1000-1030 range comes from the M3 / EPD-nRF5 driver line; 3000+ from the + * Seeed_GFX / OpenDisplay runtime. Wire values are OpenDisplay-OWNED; each + * firmware maps value -> its bb_epaper EP* constant via a repo-local + * opendisplay_epd_map.c (NOT part of this header; bb_epaper never dictates the + * wire values). @doc "display controller / panel type (DisplayConfig.panel_ic_type). + * Only representative values are named here; the full list lives in config.yaml." */ +enum PanelIC { + OD_PANEL_IC_EP_PANEL_UNDEFINED = 0 , /**< @doc "undefined / unknown panel" */ + OD_PANEL_IC_EP42_400X300 = 1 , /**< @doc "WFT0420CZ15 4.2\" B/W" */ + OD_PANEL_IC_EP42B_400X300 = 2 , /**< @doc "DEPG0420BN / GDEY042T81 4.2\" B/W" */ + OD_PANEL_IC_EP213_122X250 = 3 , /**< @doc "Waveshare 2.13\"" */ + OD_PANEL_IC_EP213B_122X250 = 4 , /**< @doc "GDEY0213B74 (Inky pHAT 2.13\" B/W newer)" */ + OD_PANEL_IC_EP293_128X296 = 5 , /**< @doc "128x296 panel" */ + OD_PANEL_IC_EP294_128X296 = 6 , /**< @doc "Waveshare newer 2.9\" 1-bit 128x296" */ + OD_PANEL_IC_EP295_128X296 = 7 , /**< @doc "harvested Solum 2.9\" BW ESLs" */ + OD_PANEL_IC_EP295_128X296_4GRAY = 8 , /**< @doc "4-gray variant of EP295" */ + OD_PANEL_IC_EP266_152X296 = 9 , /**< @doc "GDEY0266T90" */ + OD_PANEL_IC_EP102_80X128 = 10 , /**< @doc "GDEW0102T4" */ + OD_PANEL_IC_EP27B_176X264 = 11 , /**< @doc "GDEY027T91" */ + OD_PANEL_IC_EP29R_128X296 = 12 , /**< @doc "tricolor 2.9 panel" */ + OD_PANEL_IC_EP122_192X176 = 13 , /**< @doc "GDEM0122T61" */ + OD_PANEL_IC_EP154R_152X152 = 14 , /**< @doc "1.54\" B/W/R" */ + OD_PANEL_IC_EP42R_400X300 = 15 , /**< @doc "400x300 tricolor" */ + OD_PANEL_IC_EP42R2_400X300 = 16 , /**< @doc "GDEQ042Z21" */ + OD_PANEL_IC_EP37_240X416 = 17 , /**< @doc "GDEY037T03" */ + OD_PANEL_IC_EP37B_240X416 = 18 , /**< @doc "CROWPANEL 3.7\"" */ + OD_PANEL_IC_EP213_104X212 = 19 , /**< @doc "InkyPHAT 2.13 B/W" */ + OD_PANEL_IC_EP75_800X480 = 20 , /**< @doc "GDEY075T7 (older version)" */ + OD_PANEL_IC_EP75_800X480_4GRAY = 21 , /**< @doc "GDEW075T7 (older) 4-gray mode" */ + OD_PANEL_IC_EP75_800X480_4GRAY_V2 = 22 , /**< @doc "GDEY075T7 older panel, darker grays" */ + OD_PANEL_IC_EP29_128X296 = 23 , /**< @doc "Pimoroni Badger2040" */ + OD_PANEL_IC_EP29_128X296_4GRAY = 24 , /**< @doc "Pimoroni Badger2040 4-gray" */ + OD_PANEL_IC_EP213R_122X250 = 25 , /**< @doc "Inky pHAT 2.13 B/W/R" */ + OD_PANEL_IC_EP154_200X200 = 26 , /**< @doc "Waveshare 2.0\" B/W" */ + OD_PANEL_IC_EP154B_200X200 = 27 , /**< @doc "DEPG01540BN" */ + OD_PANEL_IC_EP266YR_184X360 = 28 , /**< @doc "GDEY0266F51" */ + OD_PANEL_IC_EP29YR_128X296 = 29 , /**< @doc "GDEY029F51" */ + OD_PANEL_IC_EP29YR_168X384 = 30 , /**< @doc "GDEY029F51H" */ + OD_PANEL_IC_EP583_648X480 = 31 , /**< @doc "DEPG0583BN" */ + OD_PANEL_IC_EP296_128X296 = 32 , /**< @doc "Waveshare 2.9\" 128x296 B/W V2" */ + OD_PANEL_IC_EP26R_152X296 = 33 , /**< @doc "Solum 2.6\" B/W/R" */ + OD_PANEL_IC_EP73_800X480 = 34 , /**< @doc "GEDY073D46 (slower, EOL 7-color)" */ + OD_PANEL_IC_EP73_SPECTRA_800X480 = 35 , /**< @doc "Spectra 6/7-color 800x480" */ + OD_PANEL_IC_EP74R_640X384 = 36 , /**< @doc "640x384 panel" */ + OD_PANEL_IC_EP583R_600X448 = 37 , /**< @doc "4-bits-per-pixel 5.83\" panel" */ + OD_PANEL_IC_EP75R_800X480 = 38 , /**< @doc "Waveshare 800x480 3-color" */ + OD_PANEL_IC_EP426_800X480 = 39 , /**< @doc "Waveshare 4.26\" B/W 800x480" */ + OD_PANEL_IC_EP426_800X480_4GRAY = 40 , /**< @doc "4.26\" 2-bit grayscale mode" */ + OD_PANEL_IC_EP29R2_128X296 = 41 , /**< @doc "Adafruit 2.9\" Tricolor FeatherWing" */ + OD_PANEL_IC_EP41_640X400 = 42 , /**< @doc "EInk ED040TC1 SPI UC81xx" */ + OD_PANEL_IC_EP81_SPECTRA_1024X576 = 43 , /**< @doc "Spectra 8.1\" 1024x576 6-color" */ + OD_PANEL_IC_EP7_960X640 = 44 , /**< @doc "ED070EC1" */ + OD_PANEL_IC_EP213R2_122X250 = 45 , /**< @doc "UC8151 3-color 2.13\"" */ + OD_PANEL_IC_EP29Z_128X296 = 46 , /**< @doc "SSD1680 (CrowPanel 2.9\")" */ + OD_PANEL_IC_EP29Z_128X296_4GRAY = 47 , /**< @doc "SSD1680 4-gray" */ + OD_PANEL_IC_EP213Z_122X250 = 48 , /**< @doc "SSD1680 CrowPanel 2.13\"" */ + OD_PANEL_IC_EP213Z_122X250_4GRAY = 49 , /**< @doc "CrowPanel 2.13\" 4-gray mode" */ + OD_PANEL_IC_EP154Z_152X152 = 50 , /**< @doc "CrowPanel 1.54\"" */ + OD_PANEL_IC_EP579_792X272 = 51 , /**< @doc "CrowPanel 5.79\"" */ + OD_PANEL_IC_EP213YR_122X250 = 52 , /**< @doc "GDEY0213F52" */ + OD_PANEL_IC_EP37YR_240X416 = 53 , /**< @doc "GDEM037F51" */ + OD_PANEL_IC_EP35YR_184X384 = 54 , /**< @doc "GDEM035F51" */ + OD_PANEL_IC_EP397YR_800X480 = 55 , /**< @doc "GDEM0397F81" */ + OD_PANEL_IC_EP154YR_200X200 = 56 , /**< @doc "GDEM0154F51H" */ + OD_PANEL_IC_EP266YR2_184X360 = 57 , /**< @doc "GDEY0266F52H" */ + OD_PANEL_IC_EP42YR_400X300 = 58 , /**< @doc "GDEM042F52" */ + OD_PANEL_IC_EP75_800X480_GEN2 = 59 , /**< @doc "GEDY075-D2 (Waveshare/Xiao V2 panels)" */ + OD_PANEL_IC_EP75_800X480_4GRAY_GEN2 = 60 , /**< @doc "GDEY075T7-D2 (newer) 4-gray mode" */ + OD_PANEL_IC_EP215YR_160X296 = 61 , /**< @doc "Waveshare 2.15\" 4-color" */ + OD_PANEL_IC_EP1085_1360X480 = 62 , /**< @doc "GDEM1085T51" */ + OD_PANEL_IC_EP31_240X320 = 63 , /**< @doc "GDEQ031T10 LilyGo T-Deck Pro" */ + OD_PANEL_IC_EP75YR_800X480 = 64 , /**< @doc "EP75YR 800x480 7-color" */ + OD_PANEL_IC_EP133A_SPECTRA_1200X1600 = 66 , /**< @doc "T133A01 Spectra 6 13.3\" dual-controller (Seeed reTerminal E1004). config.yaml catches up (it skips 65-66 today); value 65 stays reserved." @since 1.4 */ + OD_PANEL_IC_EP154_200X200_4GRAY = 67 , /**< @doc "Waveshare 1.54\" 4-gray" */ + OD_PANEL_IC_EP42B_400X300_4GRAY = 68 , /**< @doc "DEPG0420BN / GDEY042T81 4-gray" */ + OD_PANEL_IC_EP397_800X480 = 69 , /**< @doc "GDEM0397T81P" */ + OD_PANEL_IC_EP397_800X480_4GRAY = 70 , /**< @doc "GDEM0397T81P 4-gray" */ + OD_PANEL_IC_EP368_792X528 = 71 , /**< @doc "xteink X3 3.68\" B/W" */ + OD_PANEL_IC_EP368_792X528_4GRAY = 72 , /**< @doc "xteink X3 3.68\" 4-gray" */ + OD_PANEL_IC_EP213ZZ_122X250 = 73 , /**< @doc "LilyGo T3S3 2.13\"" */ + OD_PANEL_IC_EP40_SPECTRA_400X600 = 74 , /**< @doc "GDEP040E01 Spectra 6 4\" 400x600" */ + OD_PANEL_IC_EP27_176X264 = 75 , /**< @doc "Badger2350 B/W 2.7\"" */ + OD_PANEL_IC_EP27_176X264_4GRAY = 76 , /**< @doc "Badger2350 4-gray 2.7\"" */ + OD_PANEL_IC_UC8176_420_BW = 1000, /**< @doc "UC8176 4.2\" B/W (M3 / EPD-nRF5 driver line, values 1000-1030)" */ + OD_PANEL_IC_SSD1619_420_BWR = 1001, /**< @doc "SSD1619 4.2\" B/W/R" */ + OD_PANEL_IC_UC8176_420_BWR = 1002, /**< @doc "UC8176 4.2\" B/W/R" */ + OD_PANEL_IC_SSD1619_420_BW = 1003, /**< @doc "SSD1619 4.2\" B/W" */ + OD_PANEL_IC_JD79668_420_BWRY = 1004, /**< @doc "JD79668 4.2\" B/W/R/Y" */ + OD_PANEL_IC_UC8179_750_BW = 1005, /**< @doc "UC8179 7.5\" B/W" */ + OD_PANEL_IC_UC8179_750_BWR = 1006, /**< @doc "UC8179 7.5\" B/W/R" */ + OD_PANEL_IC_UC8159_750_LOW_BW = 1007, /**< @doc "UC8159 7.5\" Low Power B/W" */ + OD_PANEL_IC_UC8159_750_LOW_BWR = 1008, /**< @doc "UC8159 7.5\" Low Power B/W/R" */ + OD_PANEL_IC_SSD1677_750_HD_BW = 1009, /**< @doc "SSD1677 7.5\" HD B/W" */ + OD_PANEL_IC_SSD1677_750_HD_BWR = 1010, /**< @doc "SSD1677 7.5\" HD B/W/R" */ + OD_PANEL_IC_JD79665_750_BWRY = 1011, /**< @doc "JD79665 7.5\" B/W/R/Y" */ + OD_PANEL_IC_JD79665_583_BWRY = 1012, /**< @doc "JD79665 5.83\" B/W/R/Y" */ + OD_PANEL_IC_UC8151_029_BW = 1013, /**< @doc "UC8151 2.9\" 168x384 B/W" */ + OD_PANEL_IC_UC8151_029_BWR = 1014, /**< @doc "UC8151 2.9\" 168x384 B/W/R" */ + OD_PANEL_IC_SSD1619_029_BW = 1015, /**< @doc "SSD1619 2.9\" 168x384 B/W" */ + OD_PANEL_IC_SSD1619_029_BWR = 1016, /**< @doc "SSD1619 2.9\" 168x384 B/W/R" */ + OD_PANEL_IC_SSD1619_016_BW = 1017, /**< @doc "SSD1619 1.6\" 200x200 B/W" */ + OD_PANEL_IC_SSD1619_016_BWR = 1018, /**< @doc "SSD1619 1.6\" 200x200 B/W/R" */ + OD_PANEL_IC_SSD1619_022_BW = 1019, /**< @doc "SSD1619 2.2\" 240x320 B/W" */ + OD_PANEL_IC_SSD1619_022_BWR = 1020, /**< @doc "SSD1619 2.2\" 240x320 B/W/R" */ + OD_PANEL_IC_SSD1619_026_BW = 1021, /**< @doc "SSD1619 2.6\" 296x152 B/W" */ + OD_PANEL_IC_SSD1619_026_BWR = 1022, /**< @doc "SSD1619 2.6\" 296x152 B/W/R" */ + OD_PANEL_IC_UC8151_027_BW = 1023, /**< @doc "UC8151 2.7\" 200x300 B/W" */ + OD_PANEL_IC_UC8151_027_BWR = 1024, /**< @doc "UC8151 2.7\" 200x300 B/W/R" */ + OD_PANEL_IC_UC43_430_BW = 1025, /**< @doc "UC variant 4.3\" 152x522 B/W" */ + OD_PANEL_IC_UC43_430_BWR = 1026, /**< @doc "UC variant 4.3\" 152x522 B/W/R" */ + OD_PANEL_IC_SSD1619_013_BW = 1027, /**< @doc "SSD1619 1.3\" 144x200 B/W" */ + OD_PANEL_IC_SSD1619_013_BWR = 1028, /**< @doc "SSD1619 1.3\" 144x200 B/W/R" */ + OD_PANEL_IC_SSD1619_022_LITE_BW = 1029, /**< @doc "SSD1619 M3 Lite 2.2\" 250x128 B/W" */ + OD_PANEL_IC_SSD1619_022_LITE_BWR = 1030, /**< @doc "SSD1619 M3 Lite 2.2\" 250x128 B/W/R" */ + OD_PANEL_IC_ED103TC2_1872X1404 = 3000, /**< @doc "E Ink ED103TC2 + IT8951 (Seeed 10.3\", 1872x1404, 1bpp; Seeed_GFX/OpenDisplay runtime, values 3000+)" */ + OD_PANEL_IC_ED103TC2_1872X1404_4GRAY = 3001 /**< @doc "same panel as 3000; 4bpp (16-level)" */ +}; + +/* DisplayConfig.transmission_modes @bits TransmissionModes (bits 5-6 reserved -- + * named as placeholders below; bit7 is defined above the gap). */ +#define OD_TRANSMISSION_MODE_STREAMING_DECOMPRESSION (1u << 0) /* @doc "streaming zlib inflate, 512-byte DEFLATE window (requires zip). Canonical name resolves the Firmware/Silabs ZIPXL vs NRF54/yaml streaming_decompression split." */ +#define OD_TRANSMISSION_MODE_ZIP (1u << 1) /* @doc "zip-compressed transfer (full window)" */ +#define OD_TRANSMISSION_MODE_G5 (1u << 2) /* @doc "Group 5 compression (not yet implemented)" */ +#define OD_TRANSMISSION_MODE_DIRECT_WRITE (1u << 3) /* @doc "direct-write mode (bufferless)" */ +#define OD_TRANSMISSION_MODE_PIPE_WRITE (1u << 4) /* @doc "PIPE-write mode: faster transfer via sliding-window pipeline" */ +#define OD_TRANSMISSION_MODE_RESERVED_5 (1u << 5) /* @reserved @doc "reserved; must be 0 (placeholder name for a future transmission mode)" */ +#define OD_TRANSMISSION_MODE_RESERVED_6 (1u << 6) /* @reserved @doc "reserved; must be 0 (placeholder name for a future transmission mode)" */ +#define OD_TRANSMISSION_MODE_CLEAR_ON_BOOT (1u << 7) /* @doc "Clear screen on boot. Synonymous with config.yaml's no_boot_text / NO_BOOT_TEXT -- same bit, same behavior (clearing on boot is why no boot text shows)." */ + +/** @struct DisplayConfig @packet 0x20 @repeatable max=4 + * @doc "Per-panel configuration: geometry, controller, SPI pins, color scheme, + * and supported transmission modes. Up to 4 instances. 46 bytes." */ +struct DisplayConfig { + uint8_t instance_number; /**< @doc "0-based display index (unique per display block)." */ + uint8_t display_technology; /**< @enum DisplayTechnology @doc "panel technology." */ + uint16_t panel_ic_type; /**< @enum PanelIC @endian le @doc "display controller / panel type." */ + uint16_t pixel_width; /**< @endian le @unit px @doc "panel pixel width." */ + uint16_t pixel_height; /**< @endian le @unit px @doc "panel pixel height." */ + uint16_t active_width_mm; /**< @endian le @unit mm @doc "active area width." */ + uint16_t active_height_mm; /**< @endian le @unit mm @doc "active area height." */ + uint16_t legacy_tag_type; /**< @endian le @doc "OEPL legacy tag type (optional; 0 if unused). Canonical snake_case of config.yaml legacy_tagtype / firmware tag_type." */ + uint8_t rotation; /**< @enum Rotation @doc "physical rotation." */ + uint8_t reset_pin; /**< @doc "panel reset GPIO; 0xFF = none." @default 0xFF */ + uint8_t busy_pin; /**< @doc "panel busy-status GPIO; 0xFF = none." @default 0xFF */ + uint8_t dc_pin; /**< @doc "data/command select; doubles as SPI MISO on OpenDisplay-runtime IT8951 / Seeed ED103 panels." */ + uint8_t cs_pin; /**< @doc "SPI chip-select; 0xFF = none." @default 0xFF */ + uint8_t data_pin; /**< @doc "data-out pin (SPI MOSI / data line)." */ + uint8_t partial_update_support; /**< @enum PartialUpdateSupport @doc "partial-update capability." */ + uint8_t color_scheme; /**< @enum ColorScheme @doc "color/plane scheme." */ + uint8_t transmission_modes; /**< @bits TransmissionModes @doc "supported image/data transmission modes (bitfield)." */ + uint8_t clk_pin; /**< @doc "SPI SCLK pin." */ + uint8_t cs_pin_2; /**< @doc "second CS for panels split over 2 drivers (dual-controller Spectra); 0xFF = none. Canonical name (firmware called it reserved_pin_2)." @since 1.4 @default 0xFF */ + uint8_t reserved_pin_3; /**< @reserved @doc "spare pin; must be 0." */ + uint8_t reserved_pin_4; /**< @reserved @doc "spare pin; must be 0." */ + uint8_t reserved_pin_5; /**< @reserved @doc "spare pin; must be 0." */ + uint8_t reserved_pin_6; /**< @reserved @doc "spare pin; must be 0." */ + uint8_t reserved_pin_7; /**< @reserved @doc "spare pin; must be 0." */ + uint8_t reserved_pin_8; /**< @reserved @doc "spare pin; must be 0." */ + uint16_t full_update_mC; /**< @endian le @unit mC @doc "energy consumed by a full refresh; 0 = unknown." */ + uint8_t reserved[13]; /**< @reserved @doc "future use; must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct DisplayConfig) == 46, "DisplayConfig wire size"); + +/* ----------------------------------------------------------------------- + * 0x21 led + * ----------------------------------------------------------------------- */ + +/** @enum LedType @width 1 @doc "LED wiring type (LedConfig.led_type)." */ +enum LedType { + OD_LED_TYPE_RGB = 0, /**< @doc "RGB LED (3 channels)" */ + OD_LED_TYPE_SINGLE = 1, /**< @doc "single LED" */ + OD_LED_TYPE_RY = 2, /**< @doc "red/yellow pair" */ + OD_LED_TYPE_FOUR_SEPARATE = 3 /**< @doc "four separate LEDs" */ +}; + +/* LedConfig.led_flags @bits LedFlags (bits 4-7 reserved). */ +#define OD_LED_FLAG_LED1_INVERT (1u << 0) /* @doc "invert LED channel 1 polarity" */ +#define OD_LED_FLAG_LED2_INVERT (1u << 1) /* @doc "invert LED channel 2 polarity" */ +#define OD_LED_FLAG_LED3_INVERT (1u << 2) /* @doc "invert LED channel 3 polarity" */ +#define OD_LED_FLAG_LED4_INVERT (1u << 3) /* @doc "invert LED channel 4 polarity" */ + +/** @struct LedConfig @packet 0x21 @repeatable max=4 + * @doc "LED channel pins + invert flags. Up to 4 instances. 22 bytes. NOTE: the + * 15-byte reserved[] tail also carries the runtime LED flash pattern (bytes + * 0..11) written by CMD_LED_ACTIVATE 0x0073 -- see struct LedFlashPattern." */ +struct LedConfig { + uint8_t instance_number; /**< @doc "0-based LED-block index." */ + uint8_t led_type; /**< @enum LedType @doc "LED wiring type." */ + uint8_t led_1_r; /**< @doc "channel 1 (red) pin; 0xFF = unused." */ + uint8_t led_2_g; /**< @doc "channel 2 (green) pin; 0xFF = unused." */ + uint8_t led_3_b; /**< @doc "channel 3 (blue) pin; 0xFF = unused." */ + uint8_t led_4; /**< @doc "channel 4 pin (if present); 0xFF = unused." */ + uint8_t led_flags; /**< @bits LedFlags @doc "per-channel invert flags (bitfield)." */ + uint8_t reserved[15]; /**< @reserved @doc "future use; bytes 0..11 hold the runtime LedFlashPattern (see 0x0073). Must be 0 in a fresh config." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct LedConfig) == 22, "LedConfig wire size"); + +/* ----------------------------------------------------------------------- + * 0x23 sensor_data + * ----------------------------------------------------------------------- */ + +/** @enum SensorType @width 2 @doc "Sensor / PMIC type (SensorData.sensor_type)." */ +enum SensorType { + OD_SENSOR_TYPE_TEMPERATURE = 1, /**< @doc "generic temperature sensor" */ + OD_SENSOR_TYPE_HUMIDITY = 2, /**< @doc "generic humidity sensor" */ + OD_SENSOR_TYPE_AXP2101 = 3, /**< @doc "AXP2101 power-management IC" */ + OD_SENSOR_TYPE_SHT40 = 4, /**< @doc "Sensirion SHT40 (I2C); writes 3 LE bytes into MSD dynamic data (21 bits: 0.1C, 0.1% RH)" */ + OD_SENSOR_TYPE_BQ27220 = 5 /**< @doc "TI BQ27220 fuel gauge (I2C 0x55); voltage in MSD bytes 14-15, one packed dynamic byte (bits 0-6 SOC%, bit7 charging)" */ +}; + +/** @struct SensorData @packet 0x23 @repeatable max=4 + * @doc "A sensor bound to a data bus, with its MSD publish slot. Up to 4 + * instances. 30 bytes." */ +struct SensorData { + uint8_t instance_number; /**< @doc "0-based sensor-block index." */ + uint16_t sensor_type; /**< @enum SensorType @endian le @doc "sensor type." */ + uint8_t bus_id; /**< @doc "data_bus instance this sensor lives on." */ + uint8_t i2c_addr_7bit; /**< @doc "I2C 7-bit address; 0 or 0xFF = per-sensor default (SHT40 0x44, BQ27220 0x55)." @default 0xFF */ + uint8_t msd_data_start_byte; /**< @doc "first dynamicreturndata index this sensor publishes into. SHT40 uses 3 bytes (0/0xFF = default 7); BQ27220 uses 1 packed byte (0xFF = do not publish)." @default 0xFF */ + uint8_t reserved[24]; /**< @reserved @doc "future use; must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct SensorData) == 30, "SensorData wire size"); + +/* ----------------------------------------------------------------------- + * 0x24 data_bus + * ----------------------------------------------------------------------- */ + +/** @enum BusType @width 1 @doc "Bus protocol (DataBus.bus_type). Canonical + * replacement for the per-repo OD_BUS_TYPE_I2C macro." */ +enum BusType { + OD_BUS_TYPE_I2C = 1, /**< @doc "I2C bus" */ + OD_BUS_TYPE_SPI = 2 /**< @doc "SPI bus" */ +}; + +/* DataBus.bus_flags @bits BusFlags -- all bits reserved today (must be 0). */ + +/* DataBus.pullups / .pulldowns @bits PinBitmap: bit N-1 = pin N (pin_1..pin_7); + * bit 7 reserved. See the PinBitmap convention note above BinaryInputs. */ + +/** @struct DataBus @packet 0x24 @repeatable max=4 + * @doc "A shared I2C/SPI bus definition (pins, speed, pull config) other packets + * reference by instance. Up to 4 instances. 30 bytes." */ +struct DataBus { + uint8_t instance_number; /**< @doc "0-based bus-block index; referenced by SensorData.bus_id, TouchController.bus_id, etc." */ + uint8_t bus_type; /**< @enum BusType @doc "bus protocol." */ + uint8_t pin_1; /**< @doc "pin 1 (SCL for I2C)." */ + uint8_t pin_2; /**< @doc "pin 2 (SDA for I2C)." */ + uint8_t pin_3; /**< @doc "pin 3 (aux / SPI)." */ + uint8_t pin_4; /**< @doc "pin 4 (aux)." */ + uint8_t pin_5; /**< @doc "pin 5 (aux)." */ + uint8_t pin_6; /**< @doc "pin 6 (aux)." */ + uint8_t pin_7; /**< @doc "pin 7 (aux)." */ + uint32_t bus_speed_hz; /**< @endian le @unit Hz @doc "bus clock speed." */ + uint8_t bus_flags; /**< @bits BusFlags @doc "bus flags (all reserved today; must be 0)." */ + uint8_t pullups; /**< @bits PinBitmap @doc "internal pull-ups, bit N-1 = pin N (pin_1..pin_7); bit 7 reserved." */ + uint8_t pulldowns; /**< @bits PinBitmap @doc "internal pull-downs, bit N-1 = pin N (pin_1..pin_7); bit 7 reserved." */ + uint8_t reserved[14]; /**< @reserved @doc "future use; must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct DataBus) == 30, "DataBus wire size"); + +/* ----------------------------------------------------------------------- + * 0x25 binary_inputs + * ----------------------------------------------------------------------- * + * PinBitmap convention (shared shape): a per-pin bitmap byte where bit index + * i maps to pin i+1 (bit 0 = pin 1 ... bit 7 = pin 8). Used by pins_used, + * invert, pullups, pulldowns, power_off_flags below and by DataBus pull config. + * --------------------------------------------------------------------------- */ + +/** @enum InputType @width 1 @doc "Binary input hardware type (BinaryInputs.input_type)." */ +enum InputType { + OD_INPUT_TYPE_BUTTON = 1, /**< @doc "momentary button" */ + OD_INPUT_TYPE_SWITCH = 2 /**< @doc "toggle switch" */ +}; + +/** @enum InputDisplayAs @width 1 @doc "How a UI should render the input (BinaryInputs.display_as)." */ +enum InputDisplayAs { + OD_INPUT_DISPLAY_AS_BUTTON = 1, /**< @doc "present as a button" */ + OD_INPUT_DISPLAY_AS_SWITCH = 2 /**< @doc "present as a switch" */ +}; + +/** @struct BinaryInputs @packet 0x25 @repeatable max=4 + * @doc "Up to 8 button/switch pins per block, with per-pin used/invert/pull and + * long-press power-off config. Up to 4 instances. 30 bytes. Field names are the + * canonical snake_case (config.yaml inputpinN -> input_pin_N, isused -> pins_used; + * firmware called pins reserved_pin_N)." */ +struct BinaryInputs { + uint8_t instance_number; /**< @doc "0-based input-block index." */ + uint8_t input_type; /**< @enum InputType @doc "input hardware type." */ + uint8_t display_as; /**< @enum InputDisplayAs @doc "how to render the input." */ + uint8_t input_pin_1; /**< @doc "input pin 1 GPIO; 0xFF = not configured." @default 0xFF */ + uint8_t input_pin_2; /**< @doc "input pin 2 GPIO; 0xFF = not configured." @default 0xFF */ + uint8_t input_pin_3; /**< @doc "input pin 3 GPIO; 0xFF = not configured." @default 0xFF */ + uint8_t input_pin_4; /**< @doc "input pin 4 GPIO; 0xFF = not configured." @default 0xFF */ + uint8_t input_pin_5; /**< @doc "input pin 5 GPIO; 0xFF = not configured." @default 0xFF */ + uint8_t input_pin_6; /**< @doc "input pin 6 GPIO; 0xFF = not configured." @default 0xFF */ + uint8_t input_pin_7; /**< @doc "input pin 7 GPIO; 0xFF = not configured." @default 0xFF */ + uint8_t input_pin_8; /**< @doc "input pin 8 GPIO; 0xFF = not configured." @default 0xFF */ + uint8_t pins_used; /**< @bits PinBitmap @doc "which pins are active (bit N-1 = input_pin_N)." */ + uint8_t invert; /**< @bits PinBitmap @doc "per-pin logic invert (bit N-1 = input_pin_N)." */ + uint8_t pullups; /**< @bits PinBitmap @doc "per-pin internal pull-up (bit N-1 = input_pin_N)." */ + uint8_t pulldowns; /**< @bits PinBitmap @doc "per-pin internal pull-down (bit N-1 = input_pin_N)." */ + uint8_t button_data_byte_index; /**< @min 0 @max 10 @doc "dynamicreturndata index this block reports button state into (bounded integer, not an enum). 0xFF = not published." @default 0xFF */ + uint8_t power_off_flags; /**< @bits PinBitmap @doc "bit N-1 = input_pin_N triggers long-press power-off (latched devices only; ignored otherwise)." */ + uint8_t power_off_hold_sec; /**< @unit s @doc "hold time before power-off; 0 = default 3 s (latched devices only)." */ + uint8_t reserved[12]; /**< @reserved @doc "future use; must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct BinaryInputs) == 30, "BinaryInputs wire size"); + +/* ----------------------------------------------------------------------- + * 0x26 wifi_config + * ----------------------------------------------------------------------- */ + +/** @enum WifiEncryptionType @width 1 @doc "WiFi security (WifiConfig.encryption_type)." */ +enum WifiEncryptionType { + OD_WIFI_ENC_NONE = 0, /**< @doc "open network (no encryption)" */ + OD_WIFI_ENC_WEP = 1, /**< @doc "WEP" */ + OD_WIFI_ENC_WPA = 2, /**< @doc "WPA" */ + OD_WIFI_ENC_WPA2 = 3, /**< @doc "WPA2" */ + OD_WIFI_ENC_WPA3 = 4 /**< @doc "WPA3" */ +}; + +/** @struct WifiConfig @packet 0x26 + * @doc "WiFi credentials plus optional upload server host/port. Singleton. 160 + * bytes. This header is AHEAD of current firmware: server_host + server_port are + * promoted out of the former reserved[95] (a backward-compatible carve -- the + * bytes were must-be-zero, so old peers still interop; only a peer that writes a + * real port must honor the big-endian server_port). NOTE server_port is the ONE + * big-endian field in this otherwise little-endian struct." */ +struct WifiConfig { + uint8_t ssid[32]; /**< @doc "WiFi SSID, null-terminated and zero-padded to 32 bytes." */ + uint8_t password[32]; /**< @doc "WiFi password, null-terminated and zero-padded (empty for open networks)." */ + uint8_t encryption_type; /**< @enum WifiEncryptionType @doc "network security type." */ + uint8_t server_host[64]; /**< @doc "upload server URL/hostname, null-terminated and zero-padded to 64 bytes; empty = none. Promoted out of reserved[]." @since 1.4 */ + uint16_t server_port; /**< @endian be @doc "upload server TCP port, BIG-ENDIAN (the single BE field here). 0 = default/none. Promoted out of reserved[] at former indices 64-65." @since 1.4 */ + uint8_t reserved[29]; /**< @reserved @doc "future use; must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct WifiConfig) == 160, "WifiConfig wire size"); + +/* ----------------------------------------------------------------------- + * 0x27 security_config + * ----------------------------------------------------------------------- */ + +/* SecurityConfig.flags @bits SecurityFlags (bits 6-7 reserved). */ +#define OD_SECURITY_FLAG_REWRITE_ALLOWED (1u << 0) /* @doc "allow unauthenticated config writes while encryption is enabled" */ +#define OD_SECURITY_FLAG_SHOW_KEY_ON_SCREEN (1u << 1) /* @doc "show the encryption key on screen (future feature)" */ +#define OD_SECURITY_FLAG_RESET_PIN_ENABLED (1u << 2) /* @doc "reset pin enabled (must be set for the reset pin to work)" */ +#define OD_SECURITY_FLAG_RESET_PIN_POLARITY (1u << 3) /* @doc "reset-pin polarity: 0 = LOW triggers, 1 = HIGH triggers" */ +#define OD_SECURITY_FLAG_RESET_PIN_PULLUP (1u << 4) /* @doc "enable pull-up on the reset pin" */ +#define OD_SECURITY_FLAG_RESET_PIN_PULLDOWN (1u << 5) /* @doc "enable pull-down on the reset pin" */ + +/** @struct SecurityConfig @packet 0x27 + * @doc "Application-layer encryption master key and reset-pin config. Singleton. + * 64 bytes. The AEAD envelope and auth handshake are specified in + * opendisplay_protocol.h; this packet only stores the pre-shared key + policy." */ +struct SecurityConfig { + uint8_t encryption_enabled; /**< @doc "0 = disabled (all commands work unauthenticated); 1 = enabled (sensitive commands require a session). If encryption_key is all-zero, encryption is off regardless." */ + uint8_t encryption_key[16]; /**< @doc "AES-128 master key (16 bytes)." */ + uint16_t session_timeout_seconds; /**< @endian le @unit s @doc "session timeout; 0 = no timeout (persists until disconnect)." */ + uint8_t flags; /**< @bits SecurityFlags @doc "security policy flags (bitfield)." */ + uint8_t reset_pin; /**< @doc "reset GPIO; when enabled and triggered, securely erases config and reboots." @default 0xFF */ + uint8_t reserved[43]; /**< @reserved @doc "future use; must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct SecurityConfig) == 64, "SecurityConfig wire size"); + +/* ----------------------------------------------------------------------- + * 0x28 touch_controller + * ----------------------------------------------------------------------- */ + +/** @enum TouchIcType @width 2 @doc "Touch controller IC (TouchController.touch_ic_type). + * Canonical replacement for the per-repo TOUCH_IC_* macros." */ +enum TouchIcType { + OD_TOUCH_IC_NONE = 0, /**< @doc "disabled / no touch" */ + OD_TOUCH_IC_GT911 = 1 /**< @doc "Goodix GT911 (I2C)" */ +}; + +/* TouchController.flags @bits TouchFlags (bits 3-7 reserved). */ +#define OD_TOUCH_FLAG_INVERT_X (1u << 0) /* @doc "invert X using panel width" */ +#define OD_TOUCH_FLAG_INVERT_Y (1u << 1) /* @doc "invert Y using panel height" */ +#define OD_TOUCH_FLAG_SWAP_XY (1u << 2) /* @doc "swap X and Y before invert/clip" */ + +/** @struct TouchController @packet 0x28 @repeatable max=4 + * @doc "Capacitive touch controller on I2C (e.g. GT911). Publishes the first + * contact (5 bytes) into MSD dynamic data. Up to 4 instances. 32 bytes." */ +struct TouchController { + uint8_t instance_number; /**< @doc "0-based touch-block index." */ + uint16_t touch_ic_type; /**< @enum TouchIcType @endian le @doc "touch controller IC." */ + uint8_t bus_id; /**< @doc "data_bus instance for I2C; 0xFF means bus 0." */ + uint8_t i2c_addr_7bit; /**< @doc "I2C 7-bit address (GT911 0x5D or 0x14); 0 or 0xFF = auto-detect." @default 0xFF */ + uint8_t int_pin; /**< @doc "interrupt GPIO (active low); 0xFF = poll only." @default 0xFF */ + uint8_t rst_pin; /**< @doc "reset GPIO; 0xFF = skip hardware reset (probe only)." @default 0xFF */ + uint8_t display_instance; /**< @doc "DisplayConfig index used for coordinate clip / invert reference." */ + uint8_t flags; /**< @bits TouchFlags @doc "coordinate mapping flags (bitfield)." */ + uint8_t poll_interval_ms; /**< @unit ms @doc "minimum time between polls; 0 = default 25 ms." */ + uint8_t touch_data_start_byte; /**< @min 0 @max 6 @doc "start index in dynamicreturndata for the 5-byte touch block (avoid overlap with binary_inputs)." */ + uint8_t enable_pin; /**< @doc "optional touch-panel power-enable GPIO (active high); 0 or 0xFF = unused." @default 0xFF */ + uint8_t reserved[20]; /**< @reserved @doc "future use (e.g. GT911 register profile); must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct TouchController) == 32, "TouchController wire size"); + +/* ----------------------------------------------------------------------- + * 0x29 buzzer (config.yaml packet name: passive_buzzer) + * ----------------------------------------------------------------------- */ + +/* BuzzerConfig.flags @bits BuzzerFlags (bits 1-7 reserved). */ +#define OD_BUZZER_FLAG_ENABLE_ACTIVE_HIGH (1u << 0) /* @doc "enable pin is active-high when set; otherwise active-low" */ + +/** @struct BuzzerConfig @packet 0x29 @repeatable max=4 + * @doc "Buzzer (passive piezo, PWM-driven). Up to 4 instances. 32 bytes. The tone + * PATTERN is not stored here -- it is carried variable-length by CMD_BUZZER + * 0x0077 ([instance][outer_repeats][n_patterns] then per-pattern n_steps x + * (freq:u8, duration:u8)); that payload stays prose-documented in + * opendisplay_protocol.h (it is variable-length, so not modeled as a struct)." */ +struct BuzzerConfig { + uint8_t instance_number; /**< @doc "0-based buzzer-block index." */ + uint8_t drive_pin; /**< @doc "GPIO driving the PWM / square wave into the buzzer (via transistor). Pin encoding is target-defined (e.g. nRF54 (port<<4)|pin)." */ + uint8_t enable_pin; /**< @doc "optional enable (e.g. FET); 0xFF = unused." @default 0xFF */ + uint8_t flags; /**< @bits BuzzerFlags @doc "buzzer flags (bitfield)." */ + uint8_t duty_percent; /**< @min 1 @max 100 @doc "PWM duty cycle; 0 = default 50." */ + uint8_t reserved[27]; /**< @reserved @doc "future use (incl. internal frequency/time scaling); must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct BuzzerConfig) == 32, "BuzzerConfig wire size"); + +/* ----------------------------------------------------------------------- + * 0x2A nfc_config + * ----------------------------------------------------------------------- */ + +/* NfcConfig.nfc_ic_type binds to opendisplay_protocol.h's OD_NFC_IC_* MACROS + * (OD_NFC_IC_AUTO 0, OD_NFC_IC_TNB132M 1) -- NOT re-enumerated here: an enum + * member of the same spelling would be rewritten by the macro and fail to + * compile. See the LANGUAGE / LINKAGE RULE in the banner. */ + +/** @enum NfcFieldDetectMode @width 1 @doc "How NFC field presence is sensed (NfcConfig.field_detect_mode)." */ +enum NfcFieldDetectMode { + OD_NFC_FIELD_DETECT_DISABLED = 0, /**< @doc "field-detect logic disabled" */ + OD_NFC_FIELD_DETECT_GPIO_LEVEL = 1, /**< @doc "GPIO level sampled / polled" */ + OD_NFC_FIELD_DETECT_IRQ_LATCHED = 2 /**< @doc "interrupt-latched (reserved for future use)" */ +}; + +/* NfcConfig.flags @bits NfcFlags (bits 1-7 reserved). */ +#define OD_NFC_FLAG_ENABLED (1u << 0) /* @doc "must be set to enable NFC init (no fallback init if clear)" */ + +/** @struct NfcConfig @packet 0x2A @repeatable max=2 + * @doc "NFC IC selection, data bus, and optional field-detect GPIO that maps a + * button-like state into the BLE advertisement. Up to 2 instances. 32 bytes." */ +struct NfcConfig { + uint8_t instance_number; /**< @doc "0-based NFC-block index." */ + uint8_t nfc_ic_type; /**< @enum OD_NFC_IC (see opendisplay_protocol.h: OD_NFC_IC_AUTO 0, OD_NFC_IC_TNB132M 1) @doc "NFC IC / flow selector." */ + uint8_t bus_instance; /**< @doc "data_bus instance (I2C) to use." */ + uint8_t flags; /**< @bits NfcFlags @doc "NFC flags (bitfield); bit0 must be set to enable." */ + uint8_t field_detect_pin; /**< @doc "field-detect input GPIO; 0xFF = disabled." @default 0xFF */ + uint8_t field_detect_mode; /**< @enum NfcFieldDetectMode @doc "field-detect sensing mode." */ + uint8_t field_detect_active; /**< @enum ActiveLevel @doc "active level of field_detect_pin." */ + uint8_t field_detect_debounce_ms; /**< @unit ms @doc "debounce time; 0 = no debounce." */ + uint8_t power_pin; /**< @doc "NFC rail enable pin; 0xFF = use data_bus pin_3 if present, else none." @default 0xFF */ + uint8_t power_active; /**< @enum ActiveLevel @doc "power-pin polarity (reserved in current firmware)." */ + uint8_t power_on_delay_ms; /**< @unit ms @doc "delay after enabling power before NFC access." */ + uint8_t power_off_delay_ms; /**< @unit ms @doc "delay before power-off/park (reserved in current firmware)." */ + uint8_t adv_button_byte_index; /**< @min 0 @max 10 @doc "dynamicreturndata index for the field-detect button-like state." */ + uint8_t adv_button_button_id; /**< @doc "3-bit button id (stored in the lower bits) for the advertised state." */ + uint8_t reserved_pin_1; /**< @reserved @doc "spare pin; must be 0." */ + uint8_t reserved_pin_2; /**< @reserved @doc "spare pin; must be 0." */ + uint8_t reserved[16]; /**< @reserved @doc "future NFC IC-specific extensions; must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct NfcConfig) == 32, "NfcConfig wire size"); + +/* ----------------------------------------------------------------------- + * 0x2B flash_config + * ----------------------------------------------------------------------- */ + +/** @enum FlashIcType @width 1 @doc "External-flash IC type (FlashConfig.flash_ic_type)." */ +enum FlashIcType { + OD_FLASH_IC_AUTO = 0 /**< @doc "generic SPI-flash deep-sleep command flow" */ +}; + +/* FlashConfig.flags @bits FlashFlags (bits 1-7 reserved). */ +#define OD_FLASH_FLAG_ENABLED (1u << 0) /* @doc "must be set to enable flash pin config / deep-sleep sequencing" */ + +/** @struct FlashConfig @packet 0x2B @repeatable max=2 + * @doc "External SPI flash deep-sleep pin sequencing. Up to 2 instances. 32 + * bytes. Ignored when the enabled flag is clear." */ +struct FlashConfig { + uint8_t instance_number; /**< @doc "0-based flash-block index." */ + uint8_t flash_ic_type; /**< @enum FlashIcType @doc "flash IC type (reserved for future)." */ + uint8_t bus_instance; /**< @doc "reserved for future bus binding." */ + uint8_t flags; /**< @bits FlashFlags @doc "flash flags (bitfield); bit0 must be set to enable." */ + uint8_t mosi_pin; /**< @doc "SPI MOSI pin (encoding target-defined, e.g. 0xPN)." */ + uint8_t sck_pin; /**< @doc "SPI SCK pin." */ + uint8_t cs_pin; /**< @doc "SPI CS pin." */ + uint8_t power_pin; /**< @doc "optional power-enable pin (reserved in current firmware)." @default 0xFF */ + uint8_t power_active; /**< @enum ActiveLevel @doc "power-pin polarity (reserved in current firmware)." */ + uint8_t power_on_delay_ms; /**< @unit ms @doc "power-on delay before first command (reserved)." */ + uint8_t power_off_delay_ms; /**< @unit ms @doc "power-off delay after command (reserved)." */ + uint8_t mode; /**< @doc "flash mode selector (reserved for future)." */ + uint8_t reserved[20]; /**< @reserved @doc "future flash extensions; must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct FlashConfig) == 32, "FlashConfig wire size"); + +/* ----------------------------------------------------------------------- + * 0x2C data_extended + * ----------------------------------------------------------------------- */ + +/** @struct DataExtended @packet 0x2C + * @doc "Extended device-identity strings. Singleton. 288 bytes = 9 fixed 32-byte + * fields. Each field is a null-terminated, zero-padded UTF-8 string; default is + * the empty string (all zeros)." */ +struct DataExtended { + uint8_t manufacturer_name[32]; /**< @doc "manufacturer name (32-byte UTF-8, null-terminated, zero-padded)." */ + uint8_t model_name[32]; /**< @doc "model name." */ + uint8_t serial_number[32]; /**< @doc "serial number." */ + uint8_t friendly_name[32]; /**< @doc "human-friendly device name." */ + uint8_t device_location[32]; /**< @doc "physical / logical location." */ + uint8_t device_id[32]; /**< @doc "unique device identifier string." */ + uint8_t custom_string_1[32]; /**< @doc "user-defined string 1." */ + uint8_t custom_string_2[32]; /**< @doc "user-defined string 2." */ + uint8_t custom_string_3[32]; /**< @doc "user-defined string 3." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct DataExtended) == 288, "DataExtended wire size"); + +/* ========================================================================== + * SECTION 5 -- MESSAGE PAYLOAD STRUCTS (fixed-size opcode payloads) + * ========================================================================== + * The single machine-readable home for the fixed-layout bodies of the opcodes + * defined in opendisplay_protocol.h. protocol.h's @request/@response blocks are + * the narrative; these structs are the byte offsets (one home per layout -> + * they cannot drift apart). Variable-length / streaming payloads (pipe DATA + * chunks, the 0x0077 buzzer pattern list) are NOT modeled here -- they stay + * prose in protocol.h. + * + * IMPORTANT endianness split: PIPE (0x80/0x81) fields are LITTLE-endian; the + * 0x76 PartialWriteStartHeader geometry is BIG-endian. The two carry the SAME + * geometry fields in opposite byte orders -- see PipePartialExt vs + * PartialWriteStartHeader. + * ========================================================================== */ + +/* LedFlashPattern.mode_brightness low nibble = mode; 1 = flash (0 = off/idle). + * The pipe flags (PipeStartRequest.flags) bind to opendisplay_protocol.h's + * PIPE_FLAG_COMPRESSED / PIPE_FLAG_PARTIAL macros -- not redefined here. */ + +/** @struct LedFlashPattern @message CMD_LED_ACTIVATE @endian le + * @doc "12-byte LED flash program carried in CMD_LED_ACTIVATE 0x0073 after the + * led_instance byte; firmware persists it into LedConfig.reserved[0..11]. Odd + * nibble packing is OEPL-compatible -- do not 'fix' it. Three color/loop stages + * (1,2,3) each contribute a color byte, a packed loop-delay/count byte, and an + * inter-loop delay byte." */ +struct LedFlashPattern { + uint8_t mode_brightness; /**< @bits @doc "low nibble = mode (1 = flash, 0 = off); high nibble = brightness minus 1 (0..15 -> brightness 1..16)." */ + uint8_t color1; /**< @doc "stage-1 color/channel byte." */ + uint8_t loop1_delay_count; /**< @bits @doc "high nibble = stage-1 loop delay (x100 ms); low nibble = stage-1 loop count." */ + uint8_t inter_loop_delay1; /**< @unit ms @doc "stage-1 inter-loop delay (x100 ms); 0 = none." */ + uint8_t color2; /**< @doc "stage-2 color/channel byte." */ + uint8_t loop2_delay_count; /**< @bits @doc "high nibble = stage-2 loop delay (x100 ms); low nibble = stage-2 loop count." */ + uint8_t inter_loop_delay2; /**< @unit ms @doc "stage-2 inter-loop delay (x100 ms); 0 = none." */ + uint8_t color3; /**< @doc "stage-3 color/channel byte." */ + uint8_t loop3_delay_count; /**< @bits @doc "high nibble = stage-3 loop delay (x100 ms); low nibble = stage-3 loop count." */ + uint8_t inter_loop_delay3; /**< @unit ms @doc "stage-3 inter-loop delay (x100 ms); 0 = none." */ + uint8_t group_repeats; /**< @doc "group repeat count minus 1 (value 255 = repeat forever)." */ + uint8_t reserved; /**< @reserved @doc "must be 0." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct LedFlashPattern) == 12, "LedFlashPattern wire size"); + +/** @struct PipeStartRequest @message CMD_PIPE_WRITE_START @endian le + * @doc "10-byte PIPE (0x0080) START header that follows the 2 opcode bytes + * (host->device). If flags bit1 (PIPE_FLAG_PARTIAL) is set, a 12-byte + * PipePartialExt is appended (22 bytes total after the opcode). NOTE: the plan + * inventory listed this as 8 bytes, but the field list and protocol.h's + * '10-byte header (22 when partial)' both give 10 -- 10 is correct." */ +struct PipeStartRequest { + uint8_t version; /**< @doc "PIPE protocol version = PIPE_VERSION (1)." */ + uint8_t flags; /**< @bits PIPE_FLAG (see opendisplay_protocol.h: bit0 PIPE_FLAG_COMPRESSED, bit1 PIPE_FLAG_PARTIAL)." */ + uint8_t req_window; /**< @min 1 @max 32 @doc "requested sliding-window size (frames)." */ + uint8_t req_ack_every; /**< @min 1 @max 32 @doc "requested ACK cadence (frames per SACK)." */ + uint16_t client_max_frame; /**< @endian le @doc "largest on-wire frame the client will send." */ + uint32_t total_size; /**< @endian le @doc "decompressed byte total to transfer (partial: plane_size x 2)." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct PipeStartRequest) == 10, "PipeStartRequest wire size"); + +/** @struct PipePartialExt @message CMD_PIPE_WRITE_START @endian le + * @doc "12-byte partial-region extension appended to PipeStartRequest when + * PIPE_FLAG_PARTIAL is set. LITTLE-endian twin of the 0x76 PartialWriteStartHeader + * geometry (which is big-endian)." */ +struct PipePartialExt { + uint32_t old_etag; /**< @endian le @doc "etag currently displayed; must match or the START is NACKed." */ + uint16_t x; /**< @endian le @unit px @doc "region left edge." */ + uint16_t y; /**< @endian le @unit px @doc "region top edge." */ + uint16_t w; /**< @endian le @unit px @doc "region width." */ + uint16_t h; /**< @endian le @unit px @doc "region height." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct PipePartialExt) == 12, "PipePartialExt wire size"); + +/** @struct PipeStartResponse @message CMD_PIPE_WRITE_START @endian le + * @doc "6-byte PIPE START ACK data (device->host): the device echoes its + * negotiated maxima (min-rule applies against the client's request)." */ +struct PipeStartResponse { + uint8_t version; /**< @doc "PIPE protocol version = PIPE_VERSION (1)." */ + uint8_t max_window; /**< @doc "device-granted maximum window size." */ + uint8_t max_ack_every; /**< @doc "device-granted maximum ACK cadence." */ + uint16_t max_frame; /**< @endian le @doc "device-granted maximum frame size." */ + uint8_t resp_flags; /**< @bits @doc "bit0 = selective-repeat supported; bit1 = partial accepted." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct PipeStartResponse) == 6, "PipeStartResponse wire size"); + +/** @struct PipeSack @message CMD_PIPE_WRITE_DATA @endian le + * @doc "5-byte selective-ACK data in the PIPE 0x0081 ACK frame (device->host). + * highest_seen is implicitly acked; ack_mask bit i (LSB first) acks chunk + * (highest_seen - 1 - i)." */ +struct PipeSack { + uint8_t highest_seen; /**< @doc "highest received seq (mod 256), implicitly acked." */ + uint32_t ack_mask; /**< @endian le @doc "selective-ACK bitmask below highest_seen (bit i = seq highest_seen-1-i)." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct PipeSack) == 5, "PipeSack wire size"); + +/** @struct PartialWriteStartHeader @message CMD_PARTIAL_WRITE_START @endian be + * @doc "17-byte header for CMD_PARTIAL_WRITE_START 0x0076, following the 2 opcode + * bytes (host->device). ENTIRELY BIG-ENDIAN -- the showcase for the per-message + * endianness split: the PIPE 0x80 path carries the same geometry LITTLE-endian + * (see PipePartialExt). x and width must be multiples of 8; old_etag must equal + * the currently displayed etag." */ +struct PartialWriteStartHeader { + uint8_t flags; /**< @doc "partial-write flags (see protocol.h 0x76 @request)." */ + uint32_t old_etag; /**< @endian be @doc "etag currently displayed; must match." */ + uint32_t new_etag; /**< @endian be @doc "etag to store after this write." */ + uint16_t x; /**< @endian be @unit px @doc "region left edge (multiple of 8)." */ + uint16_t y; /**< @endian be @unit px @doc "region top edge." */ + uint16_t width; /**< @endian be @unit px @doc "region width (multiple of 8)." */ + uint16_t height; /**< @endian be @unit px @doc "region height." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct PartialWriteStartHeader) == 17, "PartialWriteStartHeader wire size"); + +/** @struct AuthChallenge @message CMD_AUTHENTICATE @endian le + * @doc "21-byte CMD_AUTHENTICATE 0x0050 step-1 response DATA (device->host), + * after the [status][echo] framing: the device's nonce + id the client signs. + * This handshake is ALWAYS plaintext (bootstraps the session)." */ +struct AuthChallenge { + uint8_t status; /**< @doc "auth status = AUTH_STATUS_CHALLENGE (0x00) on success (see protocol.h)." */ + uint8_t server_nonce[16]; /**< @doc "device nonce; part of the CMAC input." */ + uint32_t device_id; /**< @endian le @doc "device id; part of the CMAC input." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct AuthChallenge) == 21, "AuthChallenge wire size"); + +/** @struct AuthProof @message CMD_AUTHENTICATE + * @doc "32-byte CMD_AUTHENTICATE 0x0050 step-2 request payload (host->device): + * the client nonce and the AES-CMAC over (server_nonce || client_nonce || + * device_id)." */ +struct AuthProof { + uint8_t client_nonce[16]; /**< @doc "client nonce; part of the CMAC input." */ + uint8_t mac[16]; /**< @doc "AES-CMAC(master_key, server_nonce || client_nonce || device_id)." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct AuthProof) == 32, "AuthProof wire size"); + +/* ========================================================================== + * SECTION 6 -- BLE MANUFACTURER-SPECIFIC ADVERTISEMENT (MSD) + * ========================================================================== + * The 16-byte manufacturer-specific data record broadcast in BLE advertisements + * AND returned by CMD_READ_MSD 0x0044. The 11-byte `dynamic` area holds + * config-driven slots (button / touch / SHT40 / BQ27220) at the indices those + * config packets specify (SensorData.msd_data_start_byte, + * BinaryInputs.button_data_byte_index, TouchController.touch_data_start_byte). + * Verified against OD App Models/AdvertisementData.swift. + * ========================================================================== */ + +/* MsdAdvertisement.status @bits MsdStatusBits. bit0 is the 9th (MSB) bit of the + * 10-bit battery voltage; bits 4-7 are a free-running main-loop nibble counter. */ +#define OD_MSD_STATUS_BATTERY_VOLTAGE_BIT8 (1u << 0) /* @doc "high bit of the 10-bit battery voltage (units of 10 mV); combine with battery_voltage_low" */ +#define OD_MSD_STATUS_REBOOT_FLAG (1u << 1) /* @doc "device rebooted since last read" */ +#define OD_MSD_STATUS_CONNECTION_REQUESTED (1u << 2) /* @doc "device is requesting a connection" */ +#define OD_MSD_STATUS_RESERVED_3 (1u << 3) /* @reserved @doc "reserved; must be 0 (placeholder name for a future status flag; sits between the flags and the bits 4-7 counter)" */ +#define OD_MSD_STATUS_MAIN_LOOP_COUNTER_SHIFT 4u /* @doc "bits 4-7: free-running main-loop nibble counter (liveness)" */ +#define OD_MSD_STATUS_MAIN_LOOP_COUNTER_MASK 0xF0u /* @doc "mask for the bits 4-7 main-loop counter nibble" */ + +/** @struct MsdAdvertisement @message CMD_READ_MSD @endian le + * @doc "16-byte OpenDisplay manufacturer-specific data. Broadcast in BLE + * advertisements and returned by CMD_READ_MSD 0x0044 (device->host). Battery + * voltage is 10 bits (10 mV units): the low 8 bits are battery_voltage_low, the + * 9th bit is status bit0; the 10th is implied 0 / reserved by firmware." */ +struct MsdAdvertisement { + uint16_t company_id; /**< @endian le @doc "BLE company identifier." */ + uint8_t dynamic[11]; /**< @doc "config-driven dynamic area: button/touch/SHT40/BQ27220 slots at indices set by the relevant config packets." */ + uint8_t chip_temperature; /**< @unit C @doc "MCU chip temperature (signed degrees C, firmware-defined offset)." */ + uint8_t battery_voltage_low; /**< @unit 10mV @doc "low 8 bits of the 10-bit battery voltage (10 mV units); high bit in status bit0." */ + uint8_t status; /**< @bits MsdStatusBits @doc "status flags + battery voltage MSB + main-loop counter nibble." */ +} __attribute__((packed)); +OD_STATIC_ASSERT(sizeof(struct MsdAdvertisement) == 16, "MsdAdvertisement wire size"); + +#endif /* OPENDISPLAY_STRUCTS_H */ From 6660d944e9f643aa62575f152380ceae88e29c32 Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:41:27 -0400 Subject: [PATCH 2/5] refactor(config): consume canonical opendisplay_structs.h, drop local copies Make src/structs.h a thin shim over the vendored canonical header instead of hand-rolling the config-packet structs, enums, and flag macros a second time. The canonical definitions are the reconciled superset (ble_proto v1.4) and are byte-for-byte size-identical to the old local copies (enforced by OD_STATIC_ASSERT), so this is a pure de-dup with NO wire-format change. - src/structs.h: #include "opendisplay_structs.h"; delete all 14 duplicated config structs and every duplicated macro block; keep only firmware-only runtime types (ImageData, PipeReorderSlot, PipeWriteState, GlobalConfig, ButtonState) and PIPE reorder-window constants. - Migrate ~106 config macro sites to the OD_-prefixed canonical names (COLOR_SCHEME_*, TRANSMISSION_MODE_*, SENSOR_TYPE_*, TOUCH_*, SECURITY_FLAG_*, CHARGER_FLAG_*, SLEEP_FLAG_*, BATTERY_SENSE_FLAG_*, BUZZER_FLAG_*, PANEL_IC_*, FLASH_FLAG_*), including the GRAY8->SEVEN_COLOR (both value 7) and ZIPXL->STREAMING_DECOMPRESSION remaps. - Rename references to promoted/renamed fields: PassiveBuzzerConfig->BuzzerConfig, tag_type->legacy_tag_type, DisplayConfig.reserved_pin_2->cs_pin_2, BinaryInputs.reserved_pin_1..8->input_pin_1..8, input_flags->pins_used. - WifiConfig (ESP32): read the promoted server_host/server_port fields instead of the former reserved[] bytes; server_port stays big-endian. Config-struct scope only; message-payload structs (PIPE/partial/auth/LED/MSD) are now available but their hand-parse sites are unchanged. Builds verified: esp32-s3-N16R8, esp32-N4 (small-DRAM), nrf52840custom all compile clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/boot_screen.cpp | 66 ++++---- src/buzzer_control.cpp | 16 +- src/config_parser.cpp | 77 +++++----- src/device_control.cpp | 6 +- src/display_seeed_gfx.cpp | 10 +- src/display_service.cpp | 52 +++---- src/encryption.cpp | 8 +- src/main.cpp | 4 +- src/sensor_bq27220.cpp | 14 +- src/sensor_sht40.cpp | 6 +- src/structs.h | 310 +++----------------------------------- src/touch_input.cpp | 26 ++-- src/wake_button.cpp | 2 +- src/wake_button.h | 2 +- 14 files changed, 163 insertions(+), 436 deletions(-) diff --git a/src/boot_screen.cpp b/src/boot_screen.cpp index 8ae94a9..48061fa 100644 --- a/src/boot_screen.cpp +++ b/src/boot_screen.cpp @@ -149,7 +149,7 @@ static void formatBootKeyDisplay(char* out, uint16_t outSize) { if (bootKeyIsAllZero()) { for (uint16_t i = 0; i < hexLen; i++) out[i] = '-'; out[hexLen] = '\0'; - } else if (!(securityConfig.flags & SECURITY_FLAG_SHOW_KEY_ON_SCREEN)) { + } else if (!(securityConfig.flags & OD_SECURITY_FLAG_SHOW_KEY_ON_SCREEN)) { for (uint16_t i = 0; i < hexLen; i++) out[i] = 'X'; out[hexLen] = '\0'; } else { @@ -161,7 +161,7 @@ static void bootFormatKeyLine(char* out, size_t outSize, const char* label, const uint8_t* keyBytes, uint8_t numBytes) { if (bootKeyIsAllZero()) { snprintf(out, outSize, "%s not set", label); - } else if (!(securityConfig.flags & SECURITY_FLAG_SHOW_KEY_ON_SCREEN)) { + } else if (!(securityConfig.flags & OD_SECURITY_FLAG_SHOW_KEY_ON_SCREEN)) { snprintf(out, outSize, "%s hidden", label); } else { char hex[17]; @@ -305,15 +305,15 @@ static uint16_t bootMaxTextWidth(const char* const* lines, unsigned n, int scale static void formatBootColorSchemeText(uint8_t scheme, char* out, uint16_t outSize) { if (!out || outSize == 0) return; switch (scheme) { - case COLOR_SCHEME_MONO: snprintf(out, outSize, "BW"); break; - case COLOR_SCHEME_BWR: snprintf(out, outSize, "BWR"); break; - case COLOR_SCHEME_BWY: snprintf(out, outSize, "BWY"); break; - case COLOR_SCHEME_BWRY: snprintf(out, outSize, "BWRY"); break; - case COLOR_SCHEME_BWGBRY: snprintf(out, outSize, "6 color"); break; - case COLOR_SCHEME_BWGBRY_SPLIT: snprintf(out, outSize, "6c split"); break; - case COLOR_SCHEME_GRAY4: snprintf(out, outSize, "4 gray"); break; - case COLOR_SCHEME_GRAY16: snprintf(out, outSize, "16 gray"); break; - case COLOR_SCHEME_GRAY8: snprintf(out, outSize, "7 color"); break; + case OD_COLOR_SCHEME_MONO: snprintf(out, outSize, "BW"); break; + case OD_COLOR_SCHEME_BWR: snprintf(out, outSize, "BWR"); break; + case OD_COLOR_SCHEME_BWY: snprintf(out, outSize, "BWY"); break; + case OD_COLOR_SCHEME_BWRY: snprintf(out, outSize, "BWRY"); break; + case OD_COLOR_SCHEME_BWGBRY: snprintf(out, outSize, "6 color"); break; + case OD_COLOR_SCHEME_BWGBRY_SPLIT: snprintf(out, outSize, "6c split"); break; + case OD_COLOR_SCHEME_GRAY4: snprintf(out, outSize, "4 gray"); break; + case OD_COLOR_SCHEME_GRAY16: snprintf(out, outSize, "16 gray"); break; + case OD_COLOR_SCHEME_SEVEN_COLOR: snprintf(out, outSize, "7 color"); break; default: snprintf(out, outSize, "?"); break; } } @@ -513,15 +513,15 @@ static void writeGray4PlaneRow(const uint8_t* row2bpp, int pitch2bpp, int planeP } static const uint8_t kSchemeWhiteValue[] = { - 0xFF, // 0: COLOR_SCHEME_MONO — 1bpp, bit 1 = white - 0xFF, // 1: COLOR_SCHEME_BWR — 1bpp bitplane, all-ones = white plane - 0xFF, // 2: COLOR_SCHEME_BWY — 1bpp bitplane - 0x55, // 3: COLOR_SCHEME_BWRY — 2bpp, code 01b per pixel = white - 0x11, // 4: COLOR_SCHEME_BWGBRY — 4bpp Spectra6, nibble 1 = white - 0xFF, // 5: COLOR_SCHEME_GRAY4 — 2bpp gray, code 11b per pixel = white - 0xFF, // 6: COLOR_SCHEME_GRAY16 — 4bpp Seeed 16-gray, nibble 15 = white - 0xFF, // 7: COLOR_SCHEME_GRAY8 — 1bpp (default) - 0x11, // 8: COLOR_SCHEME_BWGBRY_SPLIT — same white nibble as BWGBRY + 0xFF, // 0: OD_COLOR_SCHEME_MONO — 1bpp, bit 1 = white + 0xFF, // 1: OD_COLOR_SCHEME_BWR — 1bpp bitplane, all-ones = white plane + 0xFF, // 2: OD_COLOR_SCHEME_BWY — 1bpp bitplane + 0x55, // 3: OD_COLOR_SCHEME_BWRY — 2bpp, code 01b per pixel = white + 0x11, // 4: OD_COLOR_SCHEME_BWGBRY — 4bpp Spectra6, nibble 1 = white + 0xFF, // 5: OD_COLOR_SCHEME_GRAY4 — 2bpp gray, code 11b per pixel = white + 0xFF, // 6: OD_COLOR_SCHEME_GRAY16 — 4bpp Seeed 16-gray, nibble 15 = white + 0xFF, // 7: OD_COLOR_SCHEME_SEVEN_COLOR — 1bpp (default) + 0x11, // 8: OD_COLOR_SCHEME_BWGBRY_SPLIT — same white nibble as BWGBRY }; // Spectra 6 footer swatches: palette order black/white/yellow/red/blue/green → @@ -572,7 +572,7 @@ bool writeBootScreenWithQr() { const int footerY0 = (int)h_log - footerH; const int middleH = footerY0 - headerH; const uint8_t colorScheme = globalConfig.displays[0].color_scheme; - const bool useBitplanes = (colorScheme == COLOR_SCHEME_BWR || colorScheme == COLOR_SCHEME_BWY); + const bool useBitplanes = (colorScheme == OD_COLOR_SCHEME_BWR || colorScheme == OD_COLOR_SCHEME_BWY); const int bitsPerPixel = getBitsPerPixel(); int pitch; if (bitsPerPixel == 4) pitch = w / 2; @@ -598,32 +598,32 @@ bool writeBootScreenWithQr() { int numSwatches = 0; if (useZoneLayout && footerH > footerPadTop + 20) { switch (colorScheme) { - case COLOR_SCHEME_MONO: - case COLOR_SCHEME_GRAY8: + case OD_COLOR_SCHEME_MONO: + case OD_COLOR_SCHEME_SEVEN_COLOR: swatchCode[0] = 0; swatchCode[1] = 1; numSwatches = 2; break; - case COLOR_SCHEME_BWR: - case COLOR_SCHEME_BWY: + case OD_COLOR_SCHEME_BWR: + case OD_COLOR_SCHEME_BWY: swatchCode[0] = 0; swatchIsColor[0] = false; swatchCode[1] = 1; swatchIsColor[1] = false; swatchCode[2] = 1; swatchIsColor[2] = true; // red/yellow: PLANE_1 bit set numSwatches = 3; break; - case COLOR_SCHEME_BWRY: + case OD_COLOR_SCHEME_BWRY: swatchCode[0] = 0; swatchCode[1] = 1; swatchCode[2] = 2; swatchCode[3] = 3; numSwatches = 4; break; - case COLOR_SCHEME_BWGBRY: - case COLOR_SCHEME_BWGBRY_SPLIT: + case OD_COLOR_SCHEME_BWGBRY: + case OD_COLOR_SCHEME_BWGBRY_SPLIT: for (int i = 0; i < 6; i++) swatchCode[i] = kBwgbrySwatchCodes[i]; numSwatches = 6; break; - case COLOR_SCHEME_GRAY4: + case OD_COLOR_SCHEME_GRAY4: bootGray4FillSwatchCodes(globalConfig.displays[0].panel_ic_type, swatchCode); numSwatches = 4; break; - case COLOR_SCHEME_GRAY16: + case OD_COLOR_SCHEME_GRAY16: for (int i = 0; i < 16; i++) swatchCode[i] = (uint8_t)i; numSwatches = 16; break; @@ -641,13 +641,13 @@ bool writeBootScreenWithQr() { for (size_t i = 0; i < last6.length(); i++) last6.setCharAt(i, (char)toupper(last6.charAt(i))); uint8_t payload[23] = {0}; - uint16_t res = globalConfig.displays[0].tag_type; + uint16_t res = globalConfig.displays[0].legacy_tag_type; payload[0] = (uint8_t)((res >> 8) & 0xFF); payload[1] = (uint8_t)(res & 0xFF); payload[2] = (uint8_t)strtoul(last6.substring(0, 2).c_str(), nullptr, 16); payload[3] = (uint8_t)strtoul(last6.substring(2, 4).c_str(), nullptr, 16); payload[4] = (uint8_t)strtoul(last6.substring(4, 6).c_str(), nullptr, 16); - if (securityConfig.flags & SECURITY_FLAG_SHOW_KEY_ON_SCREEN) { + if (securityConfig.flags & OD_SECURITY_FLAG_SHOW_KEY_ON_SCREEN) { memcpy(&payload[5], securityConfig.encryption_key, 16); } else { memset(&payload[5], 0, 16); @@ -890,7 +890,7 @@ bool writeBootScreenWithQr() { // bb_epaper 4-gray (scheme 5) needs the packed 2bpp image split into two // 1-bit controller planes, so render the frame once per plane and // de-interleave. Every other scheme writes a single packed plane in one pass. - const bool gray4Split = (colorScheme == COLOR_SCHEME_GRAY4) + const bool gray4Split = (colorScheme == OD_COLOR_SCHEME_GRAY4) #if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX) && !seeed_driver_used() #endif diff --git a/src/buzzer_control.cpp b/src/buzzer_control.cpp index a8ba8c0..ef5c66e 100644 --- a/src/buzzer_control.cpp +++ b/src/buzzer_control.cpp @@ -7,7 +7,7 @@ extern struct GlobalConfig globalConfig; void sendResponse(uint8_t* response, uint16_t len); -static_assert(sizeof(PassiveBuzzerConfig) == 32, "PassiveBuzzerConfig must be 32 bytes"); +static_assert(sizeof(BuzzerConfig) == 32, "BuzzerConfig must be 32 bytes"); // Playable frequency window (old 400 Hz / 12 000 Hz limits, now centi-Hz). static constexpr uint32_t kBuzzerFreqMinCentiHz = 40000u; // 400.00 Hz @@ -101,18 +101,18 @@ static uint32_t buzzer_index_to_centihz(uint8_t idx) { return kBuzzerCentiHzTable[buzzer_fold_index(idx)]; } -static void buzzer_set_enable(const PassiveBuzzerConfig* b, bool on) { +static void buzzer_set_enable(const BuzzerConfig* b, bool on) { if (b->enable_pin == 0xFF) { return; } bool high = on; - if ((b->flags & BUZZER_FLAG_ENABLE_ACTIVE_HIGH) == 0) { + if ((b->flags & OD_BUZZER_FLAG_ENABLE_ACTIVE_HIGH) == 0) { high = !high; } digitalWrite(b->enable_pin, high ? HIGH : LOW); } -static void buzzer_drive_off(const PassiveBuzzerConfig* b) { +static void buzzer_drive_off(const BuzzerConfig* b) { pinMode(b->drive_pin, OUTPUT); digitalWrite(b->drive_pin, LOW); } @@ -128,7 +128,7 @@ typedef enum { static struct { bool active; - PassiveBuzzerConfig* b; + BuzzerConfig* b; uint8_t melody[256]; // copied payload: [inst][outer][pattern_count][patterns...] uint16_t melody_len; uint8_t outer; // total outer repeats @@ -253,7 +253,7 @@ void buzzerService(void) { void initPassiveBuzzers(void) { for (uint8_t i = 0; i < globalConfig.passive_buzzer_count; i++) { - const PassiveBuzzerConfig* b = &globalConfig.passive_buzzers[i]; + const BuzzerConfig* b = &globalConfig.passive_buzzers[i]; if (b->drive_pin == 0xFF) { continue; } @@ -278,7 +278,7 @@ void handleBuzzerActivate(uint8_t* data, uint16_t len) { sendResponse(err, sizeof(err)); return; } - PassiveBuzzerConfig* b = &globalConfig.passive_buzzers[inst]; + BuzzerConfig* b = &globalConfig.passive_buzzers[inst]; if (b->drive_pin == 0xFF) { uint8_t err[] = {RESP_NACK, RESP_BUZZER_ACK, 0x03, 0x00}; sendResponse(err, sizeof(err)); @@ -352,7 +352,7 @@ void passiveBuzzerPowerOffAlert(void) { // Preempt any active melody so the shutdown chirp always sounds. buzzer_stop_internal(); - const PassiveBuzzerConfig* b = nullptr; + const BuzzerConfig* b = nullptr; for (uint8_t i = 0; i < globalConfig.passive_buzzer_count; i++) { const uint8_t pin = globalConfig.passive_buzzers[i].drive_pin; if (pin != 0 && pin != 0xFF) { diff --git a/src/config_parser.cpp b/src/config_parser.cpp index e29f850..c15c2b2 100644 --- a/src/config_parser.cpp +++ b/src/config_parser.cpp @@ -407,13 +407,13 @@ bool loadGlobalConfig(){ } break; case 0x29: // passive_buzzer - if (globalConfig.passive_buzzer_count < 4 && offset + sizeof(struct PassiveBuzzerConfig) <= configLen - 2) { - memcpy(&globalConfig.passive_buzzers[globalConfig.passive_buzzer_count], &configData[offset], sizeof(struct PassiveBuzzerConfig)); - offset += sizeof(struct PassiveBuzzerConfig); + if (globalConfig.passive_buzzer_count < 4 && offset + sizeof(struct BuzzerConfig) <= configLen - 2) { + memcpy(&globalConfig.passive_buzzers[globalConfig.passive_buzzer_count], &configData[offset], sizeof(struct BuzzerConfig)); + offset += sizeof(struct BuzzerConfig); globalConfig.passive_buzzer_count++; } else if (globalConfig.passive_buzzer_count >= 4) { writeSerial("WARNING: Maximum passive_buzzer count reached, skipping"); - offset += sizeof(struct PassiveBuzzerConfig); + offset += sizeof(struct BuzzerConfig); } else { writeSerial("ERROR: Not enough data for passive_buzzer"); globalConfig.loaded = false; @@ -478,7 +478,7 @@ bool loadGlobalConfig(){ wifiEncryptionType = wc.encryption_type; #ifdef TARGET_ESP32 - memcpy(wifiServerUrl, wc.reserved, 64); + memcpy(wifiServerUrl, wc.server_host, 64); wifiServerUrl[64] = '\0'; bool isStringFormat = false; @@ -496,17 +496,17 @@ bool loadGlobalConfig(){ (wifiServerUrl[0] != 0 || wifiServerUrl[1] != 0 || wifiServerUrl[2] != 0 || wifiServerUrl[3] != 0)) { uint8_t ip[4]; - ip[0] = wc.reserved[0]; - ip[1] = wc.reserved[1]; - ip[2] = wc.reserved[2]; - ip[3] = wc.reserved[3]; + ip[0] = wc.server_host[0]; + ip[1] = wc.server_host[1]; + ip[2] = wc.server_host[2]; + ip[3] = wc.server_host[3]; snprintf(wifiServerUrl, 65, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); writeSerial("Converted numeric IP to string: \"" + String(wifiServerUrl) + "\""); } else if (!isStringFormat && wifiServerUrl[0] != '\0') { - uint32_t ipNum = (uint32_t)wc.reserved[0] | - ((uint32_t)wc.reserved[1] << 8) | - ((uint32_t)wc.reserved[2] << 16) | - ((uint32_t)wc.reserved[3] << 24); + uint32_t ipNum = (uint32_t)wc.server_host[0] | + ((uint32_t)wc.server_host[1] << 8) | + ((uint32_t)wc.server_host[2] << 16) | + ((uint32_t)wc.server_host[3] << 24); uint8_t ip[4]; ip[0] = (ipNum >> 24) & 0xFF; ip[1] = (ipNum >> 16) & 0xFF; @@ -516,7 +516,10 @@ bool loadGlobalConfig(){ writeSerial("Converted 32-bit integer to IP string: \"" + String(wifiServerUrl) + "\""); } - wifiServerPort = (uint16_t)(((uint16_t)wc.reserved[64] << 8) | wc.reserved[65]); + // server_port is the one BIG-ENDIAN field in WifiConfig; read it byte-wise + // (former reserved[64]=MSB, reserved[65]=LSB). + wifiServerPort = (uint16_t)(((uint16_t)((const uint8_t*)&wc.server_port)[0] << 8) | + ((const uint8_t*)&wc.server_port)[1]); if (wifiServerPort == 0) { wifiServerPort = 2446; } @@ -581,17 +584,17 @@ bool loadGlobalConfig(){ writeSerial("Security config: Encryption disabled (flag set to 0)"); } // Log security flags - if (securityConfig.flags & SECURITY_FLAG_REWRITE_ALLOWED) { + if (securityConfig.flags & OD_SECURITY_FLAG_REWRITE_ALLOWED) { writeSerial("Security config: Rewrite allowed (unauthorized config writes permitted)"); } - if (securityConfig.flags & SECURITY_FLAG_SHOW_KEY_ON_SCREEN) { + if (securityConfig.flags & OD_SECURITY_FLAG_SHOW_KEY_ON_SCREEN) { writeSerial("Security config: Show key on screen enabled (future feature)"); } - if (securityConfig.flags & SECURITY_FLAG_RESET_PIN_ENABLED) { + if (securityConfig.flags & OD_SECURITY_FLAG_RESET_PIN_ENABLED) { writeSerial("Security config: Reset pin " + String(securityConfig.reset_pin) + - " enabled (polarity: " + String((securityConfig.flags & SECURITY_FLAG_RESET_PIN_POLARITY) ? "HIGH" : "LOW") + - ", pullup: " + String((securityConfig.flags & SECURITY_FLAG_RESET_PIN_PULLUP) ? "yes" : "no") + - ", pulldown: " + String((securityConfig.flags & SECURITY_FLAG_RESET_PIN_PULLDOWN) ? "yes" : "no") + ")"); + " enabled (polarity: " + String((securityConfig.flags & OD_SECURITY_FLAG_RESET_PIN_POLARITY) ? "HIGH" : "LOW") + + ", pullup: " + String((securityConfig.flags & OD_SECURITY_FLAG_RESET_PIN_PULLUP) ? "yes" : "no") + + ", pulldown: " + String((securityConfig.flags & OD_SECURITY_FLAG_RESET_PIN_PULLDOWN) ? "yes" : "no") + ")"); } else { writeSerial("Security config: Reset pin disabled"); } @@ -682,12 +685,12 @@ void printConfigSummary(){ writeSerial("Min Wake Time: " + String(globalConfig.power_option.min_wake_time_seconds) + " seconds"); writeSerial("TX Power: " + String(globalConfig.power_option.tx_power)); writeSerial("Sleep Flags: 0x" + String(globalConfig.power_option.sleep_flags, HEX)); - writeSerial("Button Wake: " + String((globalConfig.power_option.sleep_flags & SLEEP_FLAG_BUTTON_WAKE_DISABLE) ? "disabled" : "enabled") + " (sleep_flags bit0)"); + writeSerial("Button Wake: " + String((globalConfig.power_option.sleep_flags & OD_SLEEP_FLAG_BUTTON_WAKE_DISABLE) ? "disabled" : "enabled") + " (sleep_flags bit0)"); writeSerial("Screen Timeout: " + String(globalConfig.power_option.screen_timeout_seconds) + " s (EPD keep-alive; 0 = off immediately after refresh)"); writeSerial("Battery Sense Pin: " + String(globalConfig.power_option.battery_sense_pin)); writeSerial("Battery Sense Enable Pin: " + String(globalConfig.power_option.battery_sense_enable_pin)); writeSerial("Battery Sense Flags: 0x" + String(globalConfig.power_option.battery_sense_flags, HEX)); - writeSerial(" ENABLE_INVERTED: " + String((globalConfig.power_option.battery_sense_flags & BATTERY_SENSE_FLAG_ENABLE_INVERTED) ? "yes" : "no")); + writeSerial(" ENABLE_INVERTED: " + String((globalConfig.power_option.battery_sense_flags & OD_BATTERY_SENSE_FLAG_ENABLE_INVERTED) ? "yes" : "no")); writeSerial("Capacity Estimator: " + String(globalConfig.power_option.capacity_estimator)); writeSerial("Voltage Scaling Factor: " + String(globalConfig.power_option.voltage_scaling_factor)); writeSerial("Deep Sleep Current: " + String(globalConfig.power_option.deep_sleep_current_ua) + " uA"); @@ -700,7 +703,7 @@ void printConfigSummary(){ writeSerial(" Panel IC Type: 0x" + String(globalConfig.displays[i].panel_ic_type, HEX)); writeSerial(" Resolution: " + String(globalConfig.displays[i].pixel_width) + "x" + String(globalConfig.displays[i].pixel_height)); writeSerial(" Size: " + String(globalConfig.displays[i].active_width_mm) + "x" + String(globalConfig.displays[i].active_height_mm) + " mm"); - writeSerial(" Tag Type: 0x" + String(globalConfig.displays[i].tag_type, HEX)); + writeSerial(" Tag Type: 0x" + String(globalConfig.displays[i].legacy_tag_type, HEX)); writeSerial(" Rotation: " + String(globalConfig.displays[i].rotation * 90) + " degrees"); writeSerial(" Reset Pin: " + String(globalConfig.displays[i].reset_pin)); writeSerial(" Busy Pin: " + String(globalConfig.displays[i].busy_pin)); @@ -710,11 +713,11 @@ void printConfigSummary(){ writeSerial(" Partial Update: " + String(globalConfig.displays[i].partial_update_support ? "Yes" : "No")); writeSerial(" Color Scheme: 0x" + String(globalConfig.displays[i].color_scheme, HEX)); writeSerial(" Transmission Modes: 0x" + String(globalConfig.displays[i].transmission_modes, HEX)); - writeSerial(" ZIPXL: " + String((globalConfig.displays[i].transmission_modes & TRANSMISSION_MODE_ZIPXL) ? "enabled" : "disabled")); - writeSerial(" ZIP: " + String((globalConfig.displays[i].transmission_modes & TRANSMISSION_MODE_ZIP) ? "enabled" : "disabled")); - writeSerial(" G5: " + String((globalConfig.displays[i].transmission_modes & TRANSMISSION_MODE_G5) ? "enabled" : "disabled")); - writeSerial(" DIRECT_WRITE: " + String((globalConfig.displays[i].transmission_modes & TRANSMISSION_MODE_DIRECT_WRITE) ? "enabled" : "disabled")); - writeSerial(" CLEAR_ON_BOOT: " + String((globalConfig.displays[i].transmission_modes & TRANSMISSION_MODE_CLEAR_ON_BOOT) ? "enabled" : "disabled")); + writeSerial(" ZIPXL: " + String((globalConfig.displays[i].transmission_modes & OD_TRANSMISSION_MODE_STREAMING_DECOMPRESSION) ? "enabled" : "disabled")); + writeSerial(" ZIP: " + String((globalConfig.displays[i].transmission_modes & OD_TRANSMISSION_MODE_ZIP) ? "enabled" : "disabled")); + writeSerial(" G5: " + String((globalConfig.displays[i].transmission_modes & OD_TRANSMISSION_MODE_G5) ? "enabled" : "disabled")); + writeSerial(" DIRECT_WRITE: " + String((globalConfig.displays[i].transmission_modes & OD_TRANSMISSION_MODE_DIRECT_WRITE) ? "enabled" : "disabled")); + writeSerial(" CLEAR_ON_BOOT: " + String((globalConfig.displays[i].transmission_modes & OD_TRANSMISSION_MODE_CLEAR_ON_BOOT) ? "enabled" : "disabled")); writeSerial(" Full update energy (mC): " + String(globalConfig.displays[i].full_update_mC)); writeSerial(""); } @@ -763,15 +766,15 @@ void printConfigSummary(){ writeSerial(" Instance: " + String(globalConfig.binary_inputs[i].instance_number)); writeSerial(" Type: 0x" + String(globalConfig.binary_inputs[i].input_type, HEX)); writeSerial(" Display As: 0x" + String(globalConfig.binary_inputs[i].display_as, HEX)); - writeSerial(" Pins: 1=" + String(globalConfig.binary_inputs[i].reserved_pin_1) + - " 2=" + String(globalConfig.binary_inputs[i].reserved_pin_2) + - " 3=" + String(globalConfig.binary_inputs[i].reserved_pin_3) + - " 4=" + String(globalConfig.binary_inputs[i].reserved_pin_4) + - " 5=" + String(globalConfig.binary_inputs[i].reserved_pin_5) + - " 6=" + String(globalConfig.binary_inputs[i].reserved_pin_6) + - " 7=" + String(globalConfig.binary_inputs[i].reserved_pin_7) + - " 8=" + String(globalConfig.binary_inputs[i].reserved_pin_8)); - writeSerial(" Input Flags: 0x" + String(globalConfig.binary_inputs[i].input_flags, HEX)); + writeSerial(" Pins: 1=" + String(globalConfig.binary_inputs[i].input_pin_1) + + " 2=" + String(globalConfig.binary_inputs[i].input_pin_2) + + " 3=" + String(globalConfig.binary_inputs[i].input_pin_3) + + " 4=" + String(globalConfig.binary_inputs[i].input_pin_4) + + " 5=" + String(globalConfig.binary_inputs[i].input_pin_5) + + " 6=" + String(globalConfig.binary_inputs[i].input_pin_6) + + " 7=" + String(globalConfig.binary_inputs[i].input_pin_7) + + " 8=" + String(globalConfig.binary_inputs[i].input_pin_8)); + writeSerial(" Input Flags: 0x" + String(globalConfig.binary_inputs[i].pins_used, HEX)); writeSerial(" Invert: 0x" + String(globalConfig.binary_inputs[i].invert, HEX)); writeSerial(" Pullups: 0x" + String(globalConfig.binary_inputs[i].pullups, HEX)); writeSerial(" Pulldowns: 0x" + String(globalConfig.binary_inputs[i].pulldowns, HEX)); diff --git a/src/device_control.cpp b/src/device_control.cpp index c58ee80..53d777d 100644 --- a/src/device_control.cpp +++ b/src/device_control.cpp @@ -574,11 +574,11 @@ void initButtons() { if (input->button_data_byte_index > 10) continue; uint16_t instanceHoldMs = (input->power_off_hold_sec == 0) ? 3000u : (uint16_t)input->power_off_hold_sec * 1000u; uint8_t* instancePins[8] = { - &input->reserved_pin_1,&input->reserved_pin_2,&input->reserved_pin_3,&input->reserved_pin_4, - &input->reserved_pin_5,&input->reserved_pin_6,&input->reserved_pin_7,&input->reserved_pin_8 + &input->input_pin_1,&input->input_pin_2,&input->input_pin_3,&input->input_pin_4, + &input->input_pin_5,&input->input_pin_6,&input->input_pin_7,&input->input_pin_8 }; for (uint8_t pinIdx = 0; pinIdx < 8; pinIdx++) { - if (input->input_flags != 0 && (input->input_flags & (1 << pinIdx)) == 0) { + if (input->pins_used != 0 && (input->pins_used & (1 << pinIdx)) == 0) { continue; } uint8_t pin = *instancePins[pinIdx]; diff --git a/src/display_seeed_gfx.cpp b/src/display_seeed_gfx.cpp index 0a9b44f..00f9747 100644 --- a/src/display_seeed_gfx.cpp +++ b/src/display_seeed_gfx.cpp @@ -15,8 +15,8 @@ extern struct GlobalConfig globalConfig; -#ifndef TRANSMISSION_MODE_DIRECT_WRITE -#define TRANSMISSION_MODE_DIRECT_WRITE (1 << 3) +#ifndef OD_TRANSMISSION_MODE_DIRECT_WRITE +#define OD_TRANSMISSION_MODE_DIRECT_WRITE (1 << 3) #endif static int8_t seeed_gfx_aux_pin(uint8_t p, int8_t default_gpio) { @@ -51,8 +51,8 @@ void opendisplay_seeed_gfx_load_pins_from_display(const struct DisplayConfig* d, if (!d) return; switch (panel_ic_type) { - case PANEL_IC_SEEED_ED103TC2_1872X1404: - case PANEL_IC_SEEED_ED103TC2_1872X1404_4GRAY: + case OD_PANEL_IC_ED103TC2_1872X1404: + case OD_PANEL_IC_ED103TC2_1872X1404_4GRAY: if (d->clk_pin != 0xFF) opnd_seeed_runtime_sclk = (int8_t)d->clk_pin; if (d->data_pin != 0xFF) opnd_seeed_runtime_mosi = (int8_t)d->data_pin; if (d->dc_pin != 0xFF) opnd_seeed_runtime_miso = (int8_t)d->dc_pin; @@ -81,7 +81,7 @@ static bool seeed_gfx_hw_initialized = false; static bool seeed_gfx_panel_is_4gray(void) { if (globalConfig.display_count < 1) return false; - return globalConfig.displays[0].panel_ic_type == PANEL_IC_SEEED_ED103TC2_1872X1404_4GRAY; + return globalConfig.displays[0].panel_ic_type == OD_PANEL_IC_ED103TC2_1872X1404_4GRAY; } static size_t fb_byte_size(void) { diff --git a/src/display_service.cpp b/src/display_service.cpp index 4c79178..1f85fa0 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -362,7 +362,7 @@ static bool epdSessionUsesSeeed(void) { static uint32_t epdKeepAliveWindowMs(void) { uint8_t s = globalConfig.power_option.screen_timeout_seconds; for (uint8_t i = 0; i < globalConfig.sensor_count; i++) { - if (globalConfig.sensors[i].sensor_type == SENSOR_TYPE_AXP2101) { + if (globalConfig.sensors[i].sensor_type == OD_SENSOR_TYPE_AXP2101) { if (s != 0) { writeSerial("[EPD session] AXP2101 present - keep-alive forced off (screen_timeout_seconds ignored)", true); } @@ -671,7 +671,7 @@ int mapEpd(int id){ case 0x0040: return EP75YR_800x480; case 0x0041: return EP_PANEL_UNDEFINED; #ifdef BBEP_T133A01 - case PANEL_IC_EP133A_SPECTRA_1200X1600: return EP133A_SPECTRA_1200x1600; // 0x0042, Seeed reTerminal E1004 + case OD_PANEL_IC_EP133A_SPECTRA_1200X1600: return EP133A_SPECTRA_1200x1600; // 0x0042, Seeed reTerminal E1004 #else case 0x0042: return EP_PANEL_UNDEFINED; #endif @@ -695,8 +695,8 @@ bool seeed_driver_used(void) { #else if (globalConfig.display_count < 1) return false; const struct DisplayConfig& d = globalConfig.displays[0]; - if (d.panel_ic_type != PANEL_IC_SEEED_ED103TC2_1872X1404 && - d.panel_ic_type != PANEL_IC_SEEED_ED103TC2_1872X1404_4GRAY) return false; + if (d.panel_ic_type != OD_PANEL_IC_ED103TC2_1872X1404 && + d.panel_ic_type != OD_PANEL_IC_ED103TC2_1872X1404_4GRAY) return false; if (d.display_technology != 0 && d.display_technology != 1) return false; return true; #endif @@ -705,15 +705,15 @@ bool seeed_driver_used(void) { bool e1004_panel_used(void) { #ifdef BBEP_T133A01 if (globalConfig.display_count < 1) return false; - return globalConfig.displays[0].panel_ic_type == PANEL_IC_EP133A_SPECTRA_1200X1600; + return globalConfig.displays[0].panel_ic_type == OD_PANEL_IC_EP133A_SPECTRA_1200X1600; #else return false; #endif } -// reserved_pin_2 / cs_pin_2; 0 or 0xFF defaults to GPIO2. +// cs_pin_2; 0 or 0xFF defaults to GPIO2. uint8_t e1004_cs2_pin(void) { - uint8_t p = globalConfig.displays[0].reserved_pin_2; + uint8_t p = globalConfig.displays[0].cs_pin_2; if (p == 0 || p == 0xFF) return 2; return p; } @@ -1015,19 +1015,19 @@ void initSensors(){ writeSerial("Initializing sensor " + String(i) + " (instance " + String(sensor->instance_number) + ")", true); writeSerial(" Type: 0x" + String(sensor->sensor_type, HEX), true); writeSerial(" Bus ID: " + String(sensor->bus_id), true); - if(sensor->sensor_type == SENSOR_TYPE_AXP2101){ + if(sensor->sensor_type == OD_SENSOR_TYPE_AXP2101){ writeSerial(" Detected AXP2101 PMIC sensor", true); } - else if(sensor->sensor_type == SENSOR_TYPE_TEMPERATURE){ + else if(sensor->sensor_type == OD_SENSOR_TYPE_TEMPERATURE){ writeSerial(" Temperature sensor (initialization not implemented)", true); } - else if(sensor->sensor_type == SENSOR_TYPE_HUMIDITY){ + else if(sensor->sensor_type == OD_SENSOR_TYPE_HUMIDITY){ writeSerial(" Humidity sensor (initialization not implemented)", true); } - else if(sensor->sensor_type == SENSOR_TYPE_SHT40){ + else if(sensor->sensor_type == OD_SENSOR_TYPE_SHT40){ writeSerial(" SHT40 (I2C + MSD slot)", true); } - else if(sensor->sensor_type == SENSOR_TYPE_BQ27220){ + else if(sensor->sensor_type == OD_SENSOR_TYPE_BQ27220){ writeSerial(" BQ27220 fuel gauge (MSD voltage + optional dynamic SOC/status bytes)", true); } else{ @@ -1483,7 +1483,7 @@ static void renderChar_4BPP(uint8_t* rowBuffer, const uint8_t* fontData, int fon } static void renderChar_2BPP(uint8_t* rowBuffer, const uint8_t* fontData, int fontRow, int charIdx, int startX, int charWidth, int pitch, uint8_t colorScheme, int fontScale) { - uint8_t whiteCode = (colorScheme == COLOR_SCHEME_GRAY4) ? 0x03 : 0x01; + uint8_t whiteCode = (colorScheme == OD_COLOR_SCHEME_GRAY4) ? 0x03 : 0x01; int pixelsPerByte = 4; for (int col = 0; col < charWidth; col += pixelsPerByte) { uint8_t pixelByte = 0; @@ -1553,7 +1553,7 @@ void initDisplay(){ } writeSerial(String("Height: ") + String(globalConfig.displays[0].pixel_height), true); writeSerial(String("Width: ") + String(globalConfig.displays[0].pixel_width), true); - if (! (globalConfig.displays[0].transmission_modes & TRANSMISSION_MODE_CLEAR_ON_BOOT)){ + if (! (globalConfig.displays[0].transmission_modes & OD_TRANSMISSION_MODE_CLEAR_ON_BOOT)){ writeBootScreenWithQr(); writeSerial("EPD refresh: FULL (boot, Seeed)", true); touchSuspendForEpdRefresh(); @@ -1582,7 +1582,7 @@ void initDisplay(){ rotation = 0; // host bakes rotation into packed image if (globalConfig.displays[0].pixel_width != bbep.native_width || globalConfig.displays[0].pixel_height != bbep.native_height || - globalConfig.displays[0].color_scheme != COLOR_SCHEME_BWGBRY_SPLIT) { + globalConfig.displays[0].color_scheme != OD_COLOR_SCHEME_BWGBRY_SPLIT) { writeSerial("ERROR: E1004 requires a 1200x1600 bwgbry_split (8) display config", true); } else { e1004GeometryOk = true; @@ -1593,7 +1593,7 @@ void initDisplay(){ writeSerial(String("Height: ") + String(globalConfig.displays[0].pixel_height), true); writeSerial(String("Width: ") + String(globalConfig.displays[0].pixel_width), true); initBbepPanelSession(); - if (! (globalConfig.displays[0].transmission_modes & TRANSMISSION_MODE_CLEAR_ON_BOOT)){ + if (! (globalConfig.displays[0].transmission_modes & OD_TRANSMISSION_MODE_CLEAR_ON_BOOT)){ bool bootOk = refreshBootScreenFull(); if (!bootOk && !nrfVbusPresent()) { writeSerial("Boot refresh failed on battery — re-powering panel and retrying", true); @@ -1626,23 +1626,23 @@ void initDisplay(){ int getplane() { uint8_t colorScheme = globalConfig.displays[0].color_scheme; - if (colorScheme == COLOR_SCHEME_MONO || colorScheme == COLOR_SCHEME_GRAY16) return PLANE_0; - if (colorScheme == COLOR_SCHEME_BWR || colorScheme == COLOR_SCHEME_BWY) return PLANE_0; - if (colorScheme == COLOR_SCHEME_GRAY4) return PLANE_1; + if (colorScheme == OD_COLOR_SCHEME_MONO || colorScheme == OD_COLOR_SCHEME_GRAY16) return PLANE_0; + if (colorScheme == OD_COLOR_SCHEME_BWR || colorScheme == OD_COLOR_SCHEME_BWY) return PLANE_0; + if (colorScheme == OD_COLOR_SCHEME_GRAY4) return PLANE_1; return PLANE_1; } int getBitsPerPixel() { #if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX) if (globalConfig.display_count > 0 && - globalConfig.displays[0].panel_ic_type == PANEL_IC_SEEED_ED103TC2_1872X1404_4GRAY) { + globalConfig.displays[0].panel_ic_type == OD_PANEL_IC_ED103TC2_1872X1404_4GRAY) { return 4; } #endif - if (globalConfig.displays[0].color_scheme == COLOR_SCHEME_BWGBRY || - globalConfig.displays[0].color_scheme == COLOR_SCHEME_BWGBRY_SPLIT) return 4; - if (globalConfig.displays[0].color_scheme == COLOR_SCHEME_BWRY) return 2; - if (globalConfig.displays[0].color_scheme == COLOR_SCHEME_GRAY4) return 2; + if (globalConfig.displays[0].color_scheme == OD_COLOR_SCHEME_BWGBRY || + globalConfig.displays[0].color_scheme == OD_COLOR_SCHEME_BWGBRY_SPLIT) return 4; + if (globalConfig.displays[0].color_scheme == OD_COLOR_SCHEME_BWRY) return 2; + if (globalConfig.displays[0].color_scheme == OD_COLOR_SCHEME_GRAY4) return 2; return 1; } @@ -1899,7 +1899,7 @@ bool handleDirectWriteCompressedData(uint8_t* data, uint16_t len) { // True when the active display uses the bb_epaper 4-gray scheme (two 1-bit // controller planes). The Seeed driver path has its own 4bpp handling. static inline bool directWriteIsGray4(void) { - return (globalConfig.displays[0].color_scheme == COLOR_SCHEME_GRAY4) + return (globalConfig.displays[0].color_scheme == OD_COLOR_SCHEME_GRAY4) #if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX) && !seeed_driver_used() #endif @@ -1985,7 +1985,7 @@ void cleanupDirectWriteState(bool refreshDisplay) { // Plane2,Width,Height,TotalBytes}. No panel I/O, no acks. Shared by 0x70 and 0x80. static void directWriteComputeGeometry(bool compressed) { uint8_t colorScheme = globalConfig.displays[0].color_scheme; - directWriteBitplanes = (colorScheme == COLOR_SCHEME_BWR || colorScheme == COLOR_SCHEME_BWY); + directWriteBitplanes = (colorScheme == OD_COLOR_SCHEME_BWR || colorScheme == OD_COLOR_SCHEME_BWY); directWritePlane2 = false; directWriteCompressed = compressed; directWriteWidth = globalConfig.displays[0].pixel_width; diff --git a/src/encryption.cpp b/src/encryption.cpp index b5ab62e..e67cb86 100644 --- a/src/encryption.cpp +++ b/src/encryption.cpp @@ -795,14 +795,14 @@ void secureEraseConfig() { } void checkResetPin() { - if (!(securityConfig.flags & SECURITY_FLAG_RESET_PIN_ENABLED)) { + if (!(securityConfig.flags & OD_SECURITY_FLAG_RESET_PIN_ENABLED)) { return; } uint8_t pin = securityConfig.reset_pin; - bool polarity = (securityConfig.flags & SECURITY_FLAG_RESET_PIN_POLARITY) != 0; - bool pullup = (securityConfig.flags & SECURITY_FLAG_RESET_PIN_PULLUP) != 0; - bool pulldown = (securityConfig.flags & SECURITY_FLAG_RESET_PIN_PULLDOWN) != 0; + bool polarity = (securityConfig.flags & OD_SECURITY_FLAG_RESET_PIN_POLARITY) != 0; + bool pullup = (securityConfig.flags & OD_SECURITY_FLAG_RESET_PIN_PULLUP) != 0; + bool pulldown = (securityConfig.flags & OD_SECURITY_FLAG_RESET_PIN_PULLDOWN) != 0; writeSerial("Checking reset pin " + String(pin) + " (polarity: " + String(polarity ? "HIGH" : "LOW") + ", pullup: " + String(pullup) + ", pulldown: " + String(pulldown) + ")", true); diff --git a/src/main.cpp b/src/main.cpp index 3ed740f..e06c649 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -619,7 +619,7 @@ void pwrmgm(bool onoff){ uint8_t axp2101_bus_id = 0xFF; bool axp2101_found = false; for(uint8_t i = 0; i < globalConfig.sensor_count; i++){ - if(globalConfig.sensors[i].sensor_type == SENSOR_TYPE_AXP2101){ + if(globalConfig.sensors[i].sensor_type == OD_SENSOR_TYPE_AXP2101){ axp2101_bus_id = globalConfig.sensors[i].bus_id; axp2101_found = true; break; @@ -777,7 +777,7 @@ void powerDownExternalFlashFromConfig(void) { } const FlashConfig* flashCfg = nullptr; for (uint8_t i = 0; i < globalConfig.flash_config_count; i++) { - if ((globalConfig.flash_configs[i].flags & FLASH_CONFIG_FLAG_ENABLED) != 0) { + if ((globalConfig.flash_configs[i].flags & OD_FLASH_FLAG_ENABLED) != 0) { flashCfg = &globalConfig.flash_configs[i]; break; } diff --git a/src/sensor_bq27220.cpp b/src/sensor_bq27220.cpp index fb93a4d..d479d6f 100644 --- a/src/sensor_bq27220.cpp +++ b/src/sensor_bq27220.cpp @@ -11,11 +11,11 @@ void writeSerial(String message, bool newLine = true); static_assert(sizeof(SensorData) == 30, "SensorData must remain 30 bytes"); -#ifndef CHARGER_FLAG_ENABLE_ACTIVE_LOW -#define CHARGER_FLAG_ENABLE_ACTIVE_LOW (1u << 0) +#ifndef OD_CHARGER_FLAG_ENABLE_ACTIVE_LOW +#define OD_CHARGER_FLAG_ENABLE_ACTIVE_LOW (1u << 0) #endif -#ifndef CHARGER_FLAG_STATE_ACTIVE_LOW -#define CHARGER_FLAG_STATE_ACTIVE_LOW (1u << 1) +#ifndef OD_CHARGER_FLAG_STATE_ACTIVE_LOW +#define OD_CHARGER_FLAG_STATE_ACTIVE_LOW (1u << 1) #endif #define BQ27220_CMD_VOLTAGE 0x08u @@ -71,7 +71,7 @@ static bool bq27220_read_block(const SensorData* s, uint8_t cmd, uint8_t* buf, u static const SensorData* bq27220_config(void) { for (uint8_t i = 0; i < globalConfig.sensor_count; i++) { - if (globalConfig.sensors[i].sensor_type == SENSOR_TYPE_BQ27220) { + if (globalConfig.sensors[i].sensor_type == OD_SENSOR_TYPE_BQ27220) { return &globalConfig.sensors[i]; } } @@ -101,7 +101,7 @@ void initChargerGpio(void) { const uint8_t en = globalConfig.power_option.charge_enable_pin; if (validPin(en)) { pinMode(en, OUTPUT); - const bool activeLow = (flags & CHARGER_FLAG_ENABLE_ACTIVE_LOW) != 0; + const bool activeLow = (flags & OD_CHARGER_FLAG_ENABLE_ACTIVE_LOW) != 0; digitalWrite(en, activeLow ? LOW : HIGH); } const uint8_t st = globalConfig.power_option.charge_state_pin; @@ -115,7 +115,7 @@ static bool charger_gpio_charging(void) { if (!validPin(st)) { return false; } - const bool activeLow = (globalConfig.power_option.charger_flags & CHARGER_FLAG_STATE_ACTIVE_LOW) != 0; + const bool activeLow = (globalConfig.power_option.charger_flags & OD_CHARGER_FLAG_STATE_ACTIVE_LOW) != 0; const int level = digitalRead(st); return activeLow ? (level == HIGH) : (level == LOW); } diff --git a/src/sensor_sht40.cpp b/src/sensor_sht40.cpp index 58d19d8..235c17e 100644 --- a/src/sensor_sht40.cpp +++ b/src/sensor_sht40.cpp @@ -215,7 +215,7 @@ static void write_sht40_msd(uint8_t start, int16_t temp_centi, uint16_t rh_centi void initSht40Sensors(void) { for (uint8_t i = 0; i < globalConfig.sensor_count; i++) { const SensorData* s = &globalConfig.sensors[i]; - if (s->sensor_type != SENSOR_TYPE_SHT40) { + if (s->sensor_type != OD_SENSOR_TYPE_SHT40) { continue; } uint8_t addr = sht40_addr_7bit(s); @@ -229,7 +229,7 @@ void initSht40Sensors(void) { } for (uint8_t i = 0; i < globalConfig.sensor_count; i++) { const SensorData* s = &globalConfig.sensors[i]; - if (s->sensor_type == SENSOR_TYPE_SHT40) { + if (s->sensor_type == OD_SENSOR_TYPE_SHT40) { sht40_probe_bus_once(sht40_bus_id(s)); break; } @@ -249,7 +249,7 @@ void pollSht40SensorsForMsd(void) { havePolled = true; for (uint8_t i = 0; i < globalConfig.sensor_count; i++) { const SensorData* s = &globalConfig.sensors[i]; - if (s->sensor_type != SENSOR_TYPE_SHT40) { + if (s->sensor_type != OD_SENSOR_TYPE_SHT40) { continue; } uint8_t start = sht40_msd_start(s); diff --git a/src/structs.h b/src/structs.h index a01d56e..18ba11d 100644 --- a/src/structs.h +++ b/src/structs.h @@ -2,7 +2,14 @@ #define STRUCTS_H #include -#include "opendisplay_protocol.h" // canonical wire-protocol constants (CMD_*, RESP_*, PIPE_*, config limits) +// Canonical wire contract: config + message payload structs, OD_-prefixed enums +// and flag macros, and (transitively) opendisplay_protocol.h framing constants +// (CMD_*, RESP_*, PIPE_*, config limits). This header is the single source of +// truth for every config-packet struct; the firmware-only runtime types below +// are the only definitions that remain local. +#include "opendisplay_structs.h" + +#define BOOT_ROW_BUFFER_SIZE 960 // Image transfer state variables struct ImageData { @@ -22,89 +29,6 @@ struct ImageData { uint32_t* blockPacketsReceived; // Track packets received per block }; -// 0x01: system_config -struct SystemConfig { - uint16_t ic_type; // IC used in this device - uint8_t communication_modes; // Supported communication modes (bitfield) - uint8_t device_flags; // Misc device flags (bitfield) - uint8_t pwr_pin; // Power pin number (0xFF if not present) - uint8_t reserved[15]; // Reserved bytes for future use - uint8_t pwr_pin_2; // Aux enable or latch D (PWR_HOLD) when DEVICE_FLAG_PWR_LATCH_DFF; battery latch when DEVICE_FLAG_BATTERY_LATCH - uint8_t pwr_pin_3; // Aux enable or latch CP (PWR_LOCK) when DEVICE_FLAG_PWR_LATCH_DFF; shutdown button when DEVICE_FLAG_BATTERY_LATCH -} __attribute__((packed)); - -// 0x02: manufacturer_data -struct ManufacturerData { - uint16_t manufacturer_id; // Defines the manufacturer - uint8_t board_type; // Board identifier - uint8_t board_revision; // Board revision number - uint8_t reserved[18]; // Reserved bytes for future use -} __attribute__((packed)); - -// 0x04: power_option -struct PowerOption { - uint8_t power_mode; // Power source type enum - uint8_t battery_capacity_mah[3]; // Battery capacity in mAh (3 bytes) - uint16_t sleep_timeout_ms; // Nominal awake time in milliseconds (advertising timeout) - uint8_t tx_power; // Transmit power setting - uint8_t sleep_flags; // Sleep-related flags (bitfield) - uint8_t battery_sense_pin; // Pin used to measure battery voltage (0xFF if none) - uint8_t battery_sense_enable_pin; // Pin that enables battery sense circuit (0xFF if none) - uint8_t battery_sense_flags; // Battery sense flags (bitfield) - uint8_t capacity_estimator; // Battery chemistry estimator enum - uint16_t voltage_scaling_factor; // Voltage scaling / divider factor - uint32_t deep_sleep_current_ua; // Deep sleep current in microamperes - uint16_t deep_sleep_time_seconds; // Deep sleep duration in seconds (0 if not used) - uint8_t charge_enable_pin; // BQ25616 CE (0 or 0xFF = unused) - uint8_t charge_state_pin; // BQ25616 charge-state GPIO (0 or 0xFF = unused) - uint8_t charger_flags; // bit0 enable active-low; bit1 state active-low when charging - uint16_t min_wake_time_seconds; // Min awake window after first boot or button wake; 0 = default 120 s - uint8_t screen_timeout_seconds; // EPD keep-alive: seconds panel stays powered (WARM) after a - // refresh before shutdown. Clamped to 30 max; 0 = power off - // immediately after refresh (default; matches pre-session behavior) - uint8_t reserved[4]; -} __attribute__((packed)); - -#define CHARGER_FLAG_ENABLE_ACTIVE_LOW (1u << 0) -#define CHARGER_FLAG_STATE_ACTIVE_LOW (1u << 1) - -// sleep_flags (power_option): button wake from timer deep sleep is default-on; -// bit 0 opts a device out (e.g. buttons sharing pads with wake-hostile hardware). -#define SLEEP_FLAG_BUTTON_WAKE_DISABLE (1u << 0) - -// battery_sense_flags (power_option) -#define BATTERY_SENSE_FLAG_ENABLE_INVERTED (1 << 0) // Enable active-low (e.g. XIAO ~READ_BAT on P0.14) - -// Panel IDs must match web/firmware/toolbox/config.yaml display.panel_ic_type enum values. -// Decimal 3000–3999 = Seeed_GFX / OpenDisplay runtime epaper (add new IDs here as panels ship). -#define PANEL_IC_SEEED_ED103TC2_1872X1404 3000u -#define PANEL_IC_SEEED_ED103TC2_1872X1404_4GRAY 3001u -// 13.3" T133A01 Spectra 6 dual-controller (Seeed reTerminal E1004). -#define PANEL_IC_EP133A_SPECTRA_1200X1600 0x0042u - -#define BOOT_ROW_BUFFER_SIZE 960 - -// display.color_scheme (config.yaml); use with matching panel (e.g. gray16 + panel_ic 3001). -// add more entries to match the ColorScheme enum from bb_epaper -#define COLOR_SCHEME_MONO 0u -#define COLOR_SCHEME_BWR 1u -#define COLOR_SCHEME_BWY 2u -#define COLOR_SCHEME_BWRY 3u -#define COLOR_SCHEME_BWGBRY 4u -#define COLOR_SCHEME_GRAY4 5u -#define COLOR_SCHEME_GRAY8 7u -#define COLOR_SCHEME_GRAY16 6u -// Spectra 6 nibbles, left-half plane then right (dual-CS, no FB). -#define COLOR_SCHEME_BWGBRY_SPLIT 8u - -// display.transmission_modes (config.yaml bitfield). -#define TRANSMISSION_MODE_ZIPXL (1u << 0) -#define TRANSMISSION_MODE_ZIP (1u << 1) -#define TRANSMISSION_MODE_G5 (1u << 2) -#define TRANSMISSION_MODE_DIRECT_WRITE (1u << 3) -#define TRANSMISSION_MODE_PIPE_WRITE (1u << 4) -#define TRANSMISSION_MODE_CLEAR_ON_BOOT (1u << 7) - // PIPE_WRITE (0x0080-0x0082) sliding-window receive state. Out-of-order frames // are held in a small reorder queue while the controller stream pauses at a hole; // when the missing frame arrives in-order it is written and the contiguous run of @@ -162,201 +86,35 @@ struct PipeWriteState { uint8_t queue_high_water; // diagnostics: max occupancy seen this transfer }; -// 0x20: display (repeatable, max 4 instances) -struct DisplayConfig { - uint8_t instance_number; // Unique index for multiple display blocks (0-based) - uint8_t display_technology; // Display technology enum - uint16_t panel_ic_type; // Display controller / panel type - uint16_t pixel_width; // Pixel width of panel - uint16_t pixel_height; // Pixel height of panel - uint16_t active_width_mm; // Active width of panel in millimeters - uint16_t active_height_mm; // Active height of panel in millimeters - uint16_t tag_type; // Legacy tag type (optional) - uint8_t rotation; // Physical rotation in degrees (enum) - uint8_t reset_pin; // Pin number for panel reset (0xFF if none) - uint8_t busy_pin; // Pin number to read panel busy status (0xFF if none) - uint8_t dc_pin; // SPI MISO for Seeed ED103/IT8951 (OpenDisplay); else data/command if used - uint8_t cs_pin; // SPI chip select pin (0xFF if none) - uint8_t data_pin; // Data out pin (MOSI / data line) - uint8_t partial_update_support; // Partial update capability (enum) - uint8_t color_scheme; // Color scheme supported by the display - uint8_t transmission_modes; // Supported image/data transmission modes (bitfield) - uint8_t clk_pin; // SPI SCLK (Seeed ePaper) - uint8_t reserved_pin_2; // Spare GPIO (Seeed enables use system_config.pwr_pin_2/3) - uint8_t reserved_pin_3; // Spare GPIO - uint8_t reserved_pin_4; // Reserved / spare pin 4 - uint8_t reserved_pin_5; // Reserved / spare pin 5 - uint8_t reserved_pin_6; // Reserved / spare pin 6 - uint8_t reserved_pin_7; // Reserved / spare pin 7 - uint8_t reserved_pin_8; // Reserved / spare pin 8 - uint16_t full_update_mC; // Energy for full refresh in millicoulombs (0 = unknown) - uint8_t reserved[13]; // Reserved bytes for future use -} __attribute__((packed)); - -// 0x21: led (repeatable, max 4 instances) -struct LedConfig { - uint8_t instance_number; // Unique index for multiple LED blocks (0-based) - uint8_t led_type; // LED type enum (RGB, single, RY, etc.) - uint8_t led_1_r; // LED channel 1 (red) pin number - uint8_t led_2_g; // LED channel 2 (green) pin number - uint8_t led_3_b; // LED channel 3 (blue) pin number - uint8_t led_4; // LED channel 4 pin number (if present) - uint8_t led_flags; // LED flags (bitfield) - uint8_t reserved[15]; // Reserved bytes for future use -} __attribute__((packed)); - -// 0x29: passive_buzzer (repeatable, max 4 instances) -// Frequency in 0x0077 payload: 0 = silence/rest; 1–255 maps linearly to firmware-defined Hz range (not stored in config). -#define BUZZER_FLAG_ENABLE_ACTIVE_HIGH (1u << 0) - -struct PassiveBuzzerConfig { - uint8_t instance_number; - uint8_t drive_pin; // PWM / square wave to buzzer (+ transistor) - uint8_t enable_pin; // Optional enable (e.g. FET); 0xFF = unused - uint8_t flags; // BUZZER_FLAG_* - uint8_t duty_percent; // 1–100 PWM duty; 0 = default 50 - uint8_t reserved[27]; -} __attribute__((packed)); - -// 0x23: sensor_data (repeatable, max 4 instances) -#define SENSOR_TYPE_TEMPERATURE 0x0001u -#define SENSOR_TYPE_HUMIDITY 0x0002u -#define SENSOR_TYPE_AXP2101 0x0003u -#define SENSOR_TYPE_SHT40 0x0004u -#define SENSOR_TYPE_BQ27220 0x0005u - -struct SensorData { - uint8_t instance_number; // Unique index for multiple sensor blocks (0-based) - uint16_t sensor_type; // Sensor type enum (SENSOR_TYPE_*) - uint8_t bus_id; // Instance id of the bus to use for this sensor - uint8_t i2c_addr_7bit; // I2C 7-bit address; 0 or 0xFF = default per sensor (SHT40: 0x44) - uint8_t msd_data_start_byte; // SHT40: 3-byte block (0/0xFF=default 7); BQ27220: 1 packed byte (0xFF=skip) - uint8_t reserved[24]; -} __attribute__((packed)); - -// 0x24: data_bus (repeatable, max 4 instances) -struct DataBus { - uint8_t instance_number; // Unique index for multiple bus blocks (0-based) - uint8_t bus_type; // Bus type enum - uint8_t pin_1; // Pin 1 (SCL for I2C) - uint8_t pin_2; // Pin 2 (SDA for I2C) - uint8_t pin_3; // Pin 3 (aux) - uint8_t pin_4; // Pin 4 (aux) - uint8_t pin_5; // Pin 5 (aux) - uint8_t pin_6; // Pin 6 (aux) - uint8_t pin_7; // Pin 7 (aux) - uint32_t bus_speed_hz; // Bus speed in Hz (32-bit value) - uint8_t bus_flags; // Bus flags (bitfield) - uint8_t pullups; // Internal pullup resistors (bit per pin) - uint8_t pulldowns; // Internal pulldown resistors (bit per pin) - uint8_t reserved[14]; // Reserved bytes for future use -} __attribute__((packed)); - -// 0x25: binary_inputs (repeatable, max 4 instances) -struct BinaryInputs { - uint8_t instance_number; // Unique index for multiple input blocks (0-based) - uint8_t input_type; // Input type enum - uint8_t display_as; // How input should be represented in systems (enum) - uint8_t reserved_pin_1; // Reserved / spare pin 1 - uint8_t reserved_pin_2; // Reserved / spare pin 2 - uint8_t reserved_pin_3; // Reserved / spare pin 3 - uint8_t reserved_pin_4; // Reserved / spare pin 4 - uint8_t reserved_pin_5; // Reserved / spare pin 5 - uint8_t reserved_pin_6; // Reserved / spare pin 6 - uint8_t reserved_pin_7; // Reserved / spare pin 7 - uint8_t reserved_pin_8; // Reserved / spare pin 8 - uint8_t input_flags; // Input flags (bitfield) - uint8_t invert; // Invert flags per pin (bitfield) - uint8_t pullups; // Internal pullup resistors per pin (bitfield) - uint8_t pulldowns; // Internal pulldown resistors per pin (bitfield) - uint8_t button_data_byte_index; // Byte index in dynamicreturndata (0-10) for button data - uint8_t power_off_flags; // Bit N = pin N+1 long-press power-off (latched devices only) - uint8_t power_off_hold_sec; // Hold before power-off; 0 = default 3 s - uint8_t reserved[12]; -} __attribute__((packed)); - -// 0x28: touch_controller (repeatable, max 4 instances) -// touch_ic_type: 0 = disabled / none, 1 = GT911 -#define TOUCH_IC_NONE 0u -#define TOUCH_IC_GT911 1u -#define TOUCH_FLAG_INVERT_X (1u << 0) -#define TOUCH_FLAG_INVERT_Y (1u << 1) -#define TOUCH_FLAG_SWAP_XY (1u << 2) - -struct TouchController { - uint8_t instance_number; - uint16_t touch_ic_type; - uint8_t bus_id; // data_bus index, or 0xFF if I2C already up (e.g. after display init) - uint8_t i2c_addr_7bit; // GT911: 0x5D or 0x14; 0 or 0xFF = auto (try both after reset) - uint8_t int_pin; // GT911 INT, 0xFF = poll only - uint8_t rst_pin; // GT911 RST, 0xFF = skip hardware reset - uint8_t display_instance; // Clip/scale to displays[instance] pixel size - uint8_t flags; // TOUCH_FLAG_* - uint8_t poll_interval_ms; // 0 = default 25 ms - uint8_t touch_data_start_byte; // First of 5 bytes in MSD dynamicreturndata (0–6): byte0 low nibble = contacts 1–5 (down) or 6 (released, last xy kept); high nibble = track id - uint8_t enable_pin; // Optional touch panel power enable; 0 or 0xFF = unused - uint8_t reserved[20]; -} __attribute__((packed)); - -// 0x2B: flash_config (repeatable, max 2 instances) -#define FLASH_CONFIG_FLAG_ENABLED (1u << 0) - -struct FlashConfig { - uint8_t instance_number; - uint8_t flash_ic_type; - uint8_t bus_instance; - uint8_t flags; - uint8_t mosi_pin; - uint8_t sck_pin; - uint8_t cs_pin; - uint8_t power_pin; - uint8_t power_active; - uint8_t power_on_delay_ms; - uint8_t power_off_delay_ms; - uint8_t mode; - uint8_t reserved[20]; -} __attribute__((packed)); - -// 0x2C: data_extended (singleton) -struct DataExtended { - uint8_t manufacturer_name[32]; - uint8_t model_name[32]; - uint8_t serial_number[32]; - uint8_t friendly_name[32]; - uint8_t device_location[32]; - uint8_t device_id[32]; - uint8_t custom_string_1[32]; - uint8_t custom_string_2[32]; - uint8_t custom_string_3[32]; -} __attribute__((packed)); - -// Global configuration structure +// Global configuration structure. Members are the canonical config structs from +// opendisplay_structs.h; only the aggregation, per-type instance arrays/counts, +// and load metadata are firmware-local. struct GlobalConfig { // Required packets (single instances) struct SystemConfig system_config; struct ManufacturerData manufacturer_data; struct PowerOption power_option; - + // Optional repeatable packets (max 4 instances each) struct DisplayConfig displays[4]; uint8_t display_count; // Number of display instances loaded - + struct LedConfig leds[4]; uint8_t led_count; // Number of LED instances loaded - + struct SensorData sensors[4]; uint8_t sensor_count; // Number of sensor instances loaded - + struct DataBus data_buses[4]; uint8_t data_bus_count; // Number of data bus instances loaded - + struct BinaryInputs binary_inputs[4]; uint8_t binary_input_count; // Number of binary input instances loaded struct TouchController touch_controllers[4]; uint8_t touch_controller_count; - struct PassiveBuzzerConfig passive_buzzers[4]; + struct BuzzerConfig passive_buzzers[4]; uint8_t passive_buzzer_count; struct FlashConfig flash_configs[2]; @@ -371,40 +129,6 @@ struct GlobalConfig { bool loaded; // True if config was successfully loaded }; -// 0x26 (decimal 38): wifi_config — matches web/firmware/toolbox/config.yaml packet_types -struct WifiConfig { - uint8_t ssid[32]; - uint8_t password[32]; - uint8_t encryption_type; - uint8_t reserved[95]; -} __attribute__((packed)); - -// 0x27: security_config -struct SecurityConfig { - uint8_t encryption_enabled; // 0 = disabled, 1 = enabled - uint8_t encryption_key[16]; // AES-128 master key (16 bytes) - uint16_t session_timeout_seconds; // Session timeout (0 = no timeout) - // Bitfield flags (reserved[0]): - // Bit 0: rewrite_allowed - Allow unauthenticated config writes when encryption is enabled - // Bit 1: show_key_on_screen - Show encryption key on screen (future feature) - // Bit 2: reset_pin_enabled - Reset pin enabled (must be set for reset pin to work) - // Bit 3: reset_pin_polarity - Reset pin polarity (0 = LOW triggers, 1 = HIGH triggers) - // Bit 4: reset_pin_pullup - Enable pullup on reset pin - // Bit 5: reset_pin_pulldown - Enable pulldown on reset pin - // Bits 6-7: Reserved - uint8_t flags; // Security flags bitfield - uint8_t reset_pin; // Reset pin number - uint8_t reserved[43]; // Reserved bytes for future use -} __attribute__((packed)); - -// Security flags bitfield definitions -#define SECURITY_FLAG_REWRITE_ALLOWED (1 << 0) -#define SECURITY_FLAG_SHOW_KEY_ON_SCREEN (1 << 1) -#define SECURITY_FLAG_RESET_PIN_ENABLED (1 << 2) -#define SECURITY_FLAG_RESET_PIN_POLARITY (1 << 3) -#define SECURITY_FLAG_RESET_PIN_PULLUP (1 << 4) -#define SECURITY_FLAG_RESET_PIN_PULLDOWN (1 << 5) - #define MAX_BUTTONS 32 // Up to 4 instances * 8 pins = 32 buttons max struct ButtonState { uint8_t button_id; // Button ID (0-7, from instance_number + pin offset) diff --git a/src/touch_input.cpp b/src/touch_input.cpp index 065b2f8..925a944 100644 --- a/src/touch_input.cpp +++ b/src/touch_input.cpp @@ -145,7 +145,7 @@ static void touch_detach_int_pin(uint8_t pin) { static void touch_detach_all_configured_ints(void) { for (uint8_t i = 0; i < globalConfig.touch_controller_count && i < 4; i++) { const TouchController* tc = &globalConfig.touch_controllers[i]; - if (tc->touch_ic_type == TOUCH_IC_GT911) { + if (tc->touch_ic_type == OD_TOUCH_IC_GT911) { touch_detach_int_pin(tc->int_pin); } } @@ -357,7 +357,7 @@ static void gt911_clear_status(uint8_t addr7, bool reg_high_first) { } static bool touch_reinit_gt911(uint8_t idx, TouchController* tc, TouchRuntime* rt) { - if (tc->touch_ic_type != TOUCH_IC_GT911) { + if (tc->touch_ic_type != OD_TOUCH_IC_GT911) { return false; } touch_apply_enable_pin(tc); @@ -390,7 +390,7 @@ static bool touch_reinit_gt911(uint8_t idx, TouchController* tc, TouchRuntime* r } static bool touch_light_resume_gt911(uint8_t idx, TouchController* tc, TouchRuntime* rt) { - if (tc->touch_ic_type != TOUCH_IC_GT911 || !rt->ok || rt->addr7 == 0) { + if (tc->touch_ic_type != OD_TOUCH_IC_GT911 || !rt->ok || rt->addr7 == 0) { return false; } if (!touch_ensure_bus(tc)) { @@ -429,7 +429,7 @@ void touchResumeAfterEpdRefresh(void) { for (uint8_t i = 0; i < globalConfig.touch_controller_count; i++) { TouchController* tc = &globalConfig.touch_controllers[i]; TouchRuntime* rt = &s_touch_rt[i]; - if (tc->touch_ic_type != TOUCH_IC_GT911 || rt->disabled) { + if (tc->touch_ic_type != OD_TOUCH_IC_GT911 || rt->disabled) { continue; } if (touch_light_resume_gt911(i, tc, rt)) { @@ -443,7 +443,7 @@ void touchResumeAfterEpdRefresh(void) { } static void apply_touch_map(const TouchController* t, uint16_t* x, uint16_t* y) { - if (t->flags & TOUCH_FLAG_SWAP_XY) { + if (t->flags & OD_TOUCH_FLAG_SWAP_XY) { uint16_t tmp = *x; *x = *y; *y = tmp; @@ -454,10 +454,10 @@ static void apply_touch_map(const TouchController* t, uint16_t* x, uint16_t* y) w = globalConfig.displays[t->display_instance].pixel_width; h = globalConfig.displays[t->display_instance].pixel_height; } - if (t->flags & TOUCH_FLAG_INVERT_X && w > 0) { + if (t->flags & OD_TOUCH_FLAG_INVERT_X && w > 0) { *x = (w > *x) ? (w - 1u - *x) : 0; } - if (t->flags & TOUCH_FLAG_INVERT_Y && h > 0) { + if (t->flags & OD_TOUCH_FLAG_INVERT_Y && h > 0) { *y = (h > *y) ? (h - 1u - *y) : 0; } if (w > 0 && *x >= w) { @@ -480,7 +480,7 @@ void initTouchInput(void) { } uint8_t enabled = 0; for (uint8_t j = 0; j < globalConfig.touch_controller_count; j++) { - if (globalConfig.touch_controllers[j].touch_ic_type != TOUCH_IC_NONE) { + if (globalConfig.touch_controllers[j].touch_ic_type != OD_TOUCH_IC_NONE) { enabled++; } } @@ -493,11 +493,11 @@ void initTouchInput(void) { for (uint8_t i = 0; i < globalConfig.touch_controller_count; i++) { TouchController* tc = &globalConfig.touch_controllers[i]; TouchRuntime* rt = &s_touch_rt[i]; - if (tc->touch_ic_type == TOUCH_IC_NONE) { + if (tc->touch_ic_type == OD_TOUCH_IC_NONE) { continue; } - if (tc->touch_ic_type != TOUCH_IC_GT911) { - if (tc->touch_ic_type != TOUCH_IC_NONE) { + if (tc->touch_ic_type != OD_TOUCH_IC_GT911) { + if (tc->touch_ic_type != OD_TOUCH_IC_NONE) { writeSerial("Touch[" + String(i) + "]: skipped (only GT911=1 implemented, got " + String(tc->touch_ic_type) + ")", true); } continue; @@ -565,7 +565,7 @@ bool touch_input_gpio_is_touch_int(uint8_t pin) { } for (uint8_t i = 0; i < globalConfig.touch_controller_count; i++) { const TouchController* tc = &globalConfig.touch_controllers[i]; - if (tc->touch_ic_type == TOUCH_IC_GT911 && tc->int_pin == pin) { + if (tc->touch_ic_type == OD_TOUCH_IC_GT911 && tc->int_pin == pin) { return true; } } @@ -589,7 +589,7 @@ void processTouchInput(void) { for (uint8_t i = 0; i < globalConfig.touch_controller_count; i++) { TouchController* tc = &globalConfig.touch_controllers[i]; TouchRuntime* rt = &s_touch_rt[i]; - if (tc->touch_ic_type != TOUCH_IC_GT911 || !rt->ok || rt->disabled) { + if (tc->touch_ic_type != OD_TOUCH_IC_GT911 || !rt->ok || rt->disabled) { continue; } diff --git a/src/wake_button.cpp b/src/wake_button.cpp index f00d0a5..93af03b 100644 --- a/src/wake_button.cpp +++ b/src/wake_button.cpp @@ -80,7 +80,7 @@ void warnUnarmedPins(uint64_t mask, const char* reason) { } // namespace void armButtonWakeSources() { - if (globalConfig.power_option.sleep_flags & SLEEP_FLAG_BUTTON_WAKE_DISABLE) { + if (globalConfig.power_option.sleep_flags & OD_SLEEP_FLAG_BUTTON_WAKE_DISABLE) { writeSerial("Button wake disabled (sleep_flags) - timer-only deep sleep"); return; } diff --git a/src/wake_button.h b/src/wake_button.h index 8ea06a0..e7a852f 100644 --- a/src/wake_button.h +++ b/src/wake_button.h @@ -3,7 +3,7 @@ // Button wake from timer deep sleep (ESP32). Arms every wake-capable configured // button — plus the MOSFET-latch power button on DEVICE_FLAG_BATTERY_LATCH // boards — as a deep-sleep wake source alongside the timer, and classifies the -// wake cause on the next boot. Default-on; SLEEP_FLAG_BUTTON_WAKE_DISABLE +// wake cause on the next boot. Default-on; OD_SLEEP_FLAG_BUTTON_WAKE_DISABLE // (power_option.sleep_flags bit 0) opts a device out. // // Both functions are no-ops / false on non-ESP32 targets, so callers in shared From 093eb7b1edae37ef0797aa0c4217fe8b9de066d0 Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:28:20 -0400 Subject: [PATCH 3/5] refactor(msg): adopt canonical message-payload structs where they help MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selectively replace hand-rolled byte arithmetic with the canonical packed structs from opendisplay_structs.h at the sites where they collapse real byte-juggling; leave trivial one-integer payloads as explicit shifts. All structs are little-endian and byte-identical to the prior code on the LE targets (sizes fixed by OD_STATIC_ASSERT), so no wire-format change. - display_service.cpp updatemsdata(): build the 16-byte advertisement via a local struct MsdAdvertisement + OD_MSD_STATUS_* status bits, then memcpy into the existing msd_payload[16] the BLE adv APIs consume. - display_service.cpp handlePipeWriteStart(): parse the 10-byte START header and 12-byte partial extension via struct PipeStartRequest / PipePartialExt (memcpy-in), replacing ~12 lines of shift arithmetic; length guards now use sizeof. - display_service.cpp handlePartialWriteStart(): documented-only — the 0x76 header is all BIG-endian, so keep the hand byte-swap parse; just swap the magic 17 for sizeof(struct PartialWriteStartHeader) and add a comment. - device_control.cpp led_load_config()/handleLedActivate(): overlay-read the persisted 12-byte pattern as struct LedFlashPattern to name the ledcfg[] indices; the nibble packing stays hand-decoded. Copy uses sizeof. Left as explicit bytes (struct would be a wash): PipeSack, PipeStartResponse (single-integer payloads) and the auth challenge/proof memcpys (already clear; not worth adding an include to the crypto path). Builds verified clean: nrf52840custom, esp32-s3-N16R8, esp32-N4. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/device_control.cpp | 38 +++++++++++++----------- src/display_service.cpp | 64 ++++++++++++++++++++++++----------------- 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/src/device_control.cpp b/src/device_control.cpp index 53d777d..b9fd18c 100644 --- a/src/device_control.cpp +++ b/src/device_control.cpp @@ -206,22 +206,25 @@ static void led_schedule_delay_ms(uint16_t ms) { } static void led_load_config(struct LedConfig* led) { - uint8_t* ledcfg = led->reserved; + // The 12-byte flash pattern persisted in LedConfig.reserved[] is struct + // LedFlashPattern (all single bytes, no endianness). Overlay-read to name the + // fields; the nibble packing (mode/brightness, delay/count) stays hand-decoded. + const struct LedFlashPattern* p = (const struct LedFlashPattern*)led->reserved; s_led.led = led; - s_led.brightness = (uint8_t)(((ledcfg[0] >> 4) & 0x0F) + 1); - s_led.c1 = ledcfg[1]; - s_led.c2 = ledcfg[4]; - s_led.c3 = ledcfg[7]; - s_led.loop1delay = (uint8_t)((ledcfg[2] >> 4) & 0x0F); - s_led.loop2delay = (uint8_t)((ledcfg[5] >> 4) & 0x0F); - s_led.loop3delay = (uint8_t)((ledcfg[8] >> 4) & 0x0F); - s_led.loopcnt1 = (uint8_t)(ledcfg[2] & 0x0F); - s_led.loopcnt2 = (uint8_t)(ledcfg[5] & 0x0F); - s_led.loopcnt3 = (uint8_t)(ledcfg[8] & 0x0F); - s_led.ildelay1 = ledcfg[3]; - s_led.ildelay2 = ledcfg[6]; - s_led.ildelay3 = ledcfg[9]; - s_led.grouprepeats = (uint8_t)(ledcfg[10] + 1); + s_led.brightness = (uint8_t)(((p->mode_brightness >> 4) & 0x0F) + 1); + s_led.c1 = p->color1; + s_led.c2 = p->color2; + s_led.c3 = p->color3; + s_led.loop1delay = (uint8_t)((p->loop1_delay_count >> 4) & 0x0F); + s_led.loop2delay = (uint8_t)((p->loop2_delay_count >> 4) & 0x0F); + s_led.loop3delay = (uint8_t)((p->loop3_delay_count >> 4) & 0x0F); + s_led.loopcnt1 = (uint8_t)(p->loop1_delay_count & 0x0F); + s_led.loopcnt2 = (uint8_t)(p->loop2_delay_count & 0x0F); + s_led.loopcnt3 = (uint8_t)(p->loop3_delay_count & 0x0F); + s_led.ildelay1 = p->inter_loop_delay1; + s_led.ildelay2 = p->inter_loop_delay2; + s_led.ildelay3 = p->inter_loop_delay3; + s_led.grouprepeats = (uint8_t)(p->group_repeats + 1); s_led.group_pos = 0; s_led.i1 = 0; s_led.i2 = 0; @@ -387,8 +390,9 @@ void handleLedActivate(uint8_t* data, uint16_t len) { return; } struct LedConfig* led = &globalConfig.leds[ledInstance]; - if (len >= 13) { - memcpy(led->reserved, data + 1, 12); + // Payload is [led_instance:1][LedFlashPattern:12]; stash the pattern in reserved[]. + if (len >= 1 + sizeof(struct LedFlashPattern)) { + memcpy(led->reserved, data + 1, sizeof(struct LedFlashPattern)); } uint8_t mode = (uint8_t)(led->reserved[0] & 0x0F); if (mode != 1) { diff --git a/src/display_service.cpp b/src/display_service.cpp index 1f85fa0..f7081d7 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -1723,17 +1723,20 @@ void updatemsdata(){ else if (tempEncoded > 255) tempEncoded = 255; uint8_t temperatureByte = (uint8_t)tempEncoded; uint8_t batteryVoltageLowByte = (uint8_t)(batteryVoltage10mv & 0xFF); - uint8_t statusByte = ((batteryVoltage10mv >> 8) & 0x01) | - ((rebootFlag & 0x01) << 1) | - ((connectionRequested & 0x01) << 2) | - ((mloopcounter & 0x0F) << 4); - uint16_t msd_cid = 0x2446; - memset(msd_payload, 0, 16); - memcpy(msd_payload, (uint8_t*)&msd_cid, sizeof(msd_cid)); - memcpy(&msd_payload[2], dynamicreturndata, 11); - msd_payload[13] = temperatureByte; - msd_payload[14] = batteryVoltageLowByte; - msd_payload[15] = statusByte; + uint8_t statusByte = (((batteryVoltage10mv >> 8) & 0x01) ? OD_MSD_STATUS_BATTERY_VOLTAGE_BIT8 : 0) | + (rebootFlag ? OD_MSD_STATUS_REBOOT_FLAG : 0) | + (connectionRequested ? OD_MSD_STATUS_CONNECTION_REQUESTED : 0) | + (((uint8_t)(mloopcounter << OD_MSD_STATUS_MAIN_LOOP_COUNTER_SHIFT)) & OD_MSD_STATUS_MAIN_LOOP_COUNTER_MASK); + // Build the 16-byte advertisement via the canonical wire struct (all little-endian), + // then copy into the global msd_payload[16] that the BLE adv APIs below consume. + struct MsdAdvertisement m; + memset(&m, 0, sizeof m); + m.company_id = 0x2446; + memcpy(m.dynamic, dynamicreturndata, sizeof m.dynamic); + m.chip_temperature = temperatureByte; + m.battery_voltage_low = batteryVoltageLowByte; + m.status = statusByte; + memcpy(msd_payload, &m, sizeof m); #ifdef TARGET_NRF static uint8_t prev_msd_payload_nrf[16] = {0xFF}; if (memcmp(prev_msd_payload_nrf, msd_payload, 16) == 0) { @@ -2091,11 +2094,14 @@ void handlePartialWriteStart(uint8_t* data, uint16_t len) { resetPipeWriteState(); imageWriteLogReset(); - if (len < 17) { + if (len < sizeof(struct PartialWriteStartHeader)) { send_direct_write_nack(0x76, OD_ERR_PARTIAL_STREAM, false); return; } + // Layout is struct PartialWriteStartHeader (17 B). It is ALL big-endian, unlike + // the little-endian PipePartialExt twin, so we parse by hand (the shifts ARE the + // byte-swap) rather than overlaying the struct on this LE MCU. uint8_t flags = data[0]; uint32_t oldEtag = parse_be_u32(data + 1); uint32_t newEtag = parse_be_u32(data + 5); @@ -2540,14 +2546,15 @@ void handlePipeWriteStart(uint8_t* data, uint16_t len) { // Fixed 10-byte payload (opcode already stripped by the dispatcher): // ver(1)+flags(1)+req_w(1)+req_n(1)+client_max_frame(2)+total_size(4). // Tolerate trailing bytes (future fields). - if (len < 10) { sendPipeStartNack(OD_ERR_PIPE_START_BAD_HEADER); return; } - uint8_t ver = data[0]; - uint8_t flags = data[1]; - uint8_t req_w = data[2]; - uint8_t req_n = data[3]; - uint16_t client_max_frame = (uint16_t)data[4] | ((uint16_t)data[5] << 8); - uint32_t total_size = (uint32_t)data[6] | ((uint32_t)data[7] << 8) - | ((uint32_t)data[8] << 16) | ((uint32_t)data[9] << 24); + if (len < sizeof(struct PipeStartRequest)) { sendPipeStartNack(OD_ERR_PIPE_START_BAD_HEADER); return; } + struct PipeStartRequest req; + memcpy(&req, data, sizeof req); // canonical 10-byte LE header (byte-identical to the old shifts) + uint8_t ver = req.version; + uint8_t flags = req.flags; + uint8_t req_w = req.req_window; + uint8_t req_n = req.req_ack_every; + uint16_t client_max_frame = req.client_max_frame; + uint32_t total_size = req.total_size; if (ver != PIPE_VERSION) { sendPipeStartNack(OD_ERR_PIPE_START_BAD_HEADER); return; } // Defined flags: bit0 zlib compression, bit1 partial-region refresh. Any other bit unsupported. @@ -2558,17 +2565,20 @@ void handlePipeWriteStart(uint8_t* data, uint16_t len) { // Partial START appends a 12-byte LE extension after total_size (payload len 22 vs 10): // [old_etag:4][x:2][y:2][w:2][h:2]. LE, unlike 0x76's big-endian layout. - if (partial && len < 22) { sendPipeStartNack(OD_ERR_PIPE_START_BAD_HEADER); return; } + if (partial && len < sizeof(struct PipeStartRequest) + sizeof(struct PipePartialExt)) { + sendPipeStartNack(OD_ERR_PIPE_START_BAD_HEADER); return; + } uint32_t old_etag = 0; uint16_t rectX = 0, rectY = 0, rectW = 0, rectH = 0; uint32_t planeBytes = 0; if (partial) { - old_etag = (uint32_t)data[10] | ((uint32_t)data[11] << 8) - | ((uint32_t)data[12] << 16) | ((uint32_t)data[13] << 24); - rectX = (uint16_t)data[14] | ((uint16_t)data[15] << 8); - rectY = (uint16_t)data[16] | ((uint16_t)data[17] << 8); - rectW = (uint16_t)data[18] | ((uint16_t)data[19] << 8); - rectH = (uint16_t)data[20] | ((uint16_t)data[21] << 8); + struct PipePartialExt ext; + memcpy(&ext, data + sizeof(struct PipeStartRequest), sizeof ext); // 12-byte LE extension + old_etag = ext.old_etag; + rectX = ext.x; + rectY = ext.y; + rectW = ext.w; + rectH = ext.h; // Partial validations (plan 1.2, order 5-7). All precede any hardware touch; any // failure clears displayed_etag for parity with send_direct_write_nack. These are From 7b9c005f8db2ff1ab70e623a1151dda3e5e8a9bc Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:55:19 -0400 Subject: [PATCH 4/5] docs(boot): flag OD_COLOR_SCHEME_SEVEN_COLOR as an unimplemented stub Value 7 stays OD_COLOR_SCHEME_SEVEN_COLOR (aligned with the canonical enum, formerly the bogus GRAY8), but the firmware does not actually render 7 colors: the boot screen only prints the "7 color" label while treating scheme 7 as a 1bpp mono stub (kSchemeWhiteValue[7] = 0xFF; footer swatches lumped in with MONO). Add prominent warning comments at all three sites so no one assumes a device reporting scheme 7 renders 7 colors. Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/boot_screen.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/boot_screen.cpp b/src/boot_screen.cpp index 48061fa..c16436d 100644 --- a/src/boot_screen.cpp +++ b/src/boot_screen.cpp @@ -313,6 +313,14 @@ static void formatBootColorSchemeText(uint8_t scheme, char* out, uint16_t outSiz case OD_COLOR_SCHEME_BWGBRY_SPLIT: snprintf(out, outSize, "6c split"); break; case OD_COLOR_SCHEME_GRAY4: snprintf(out, outSize, "4 gray"); break; case OD_COLOR_SCHEME_GRAY16: snprintf(out, outSize, "16 gray"); break; + // WARNING: SEVEN_COLOR (scheme value 7) is NOT properly implemented in this + // firmware. This case only produces the human-readable label "7 color"; there + // is no actual 7-color plane packing or palette rendering anywhere. Everywhere + // else scheme 7 is treated as a 1bpp mono stub (see kSchemeWhiteValue[7] and the + // footer-swatch switch, where it falls in with OD_COLOR_SCHEME_MONO). Value 7 is + // the canonical SEVEN_COLOR slot (formerly the bogus GRAY8); it is kept so the + // enum stays aligned with opendisplay_structs.h, but do NOT assume a device + // reporting scheme 7 will render 7 colors until real support is added. case OD_COLOR_SCHEME_SEVEN_COLOR: snprintf(out, outSize, "7 color"); break; default: snprintf(out, outSize, "?"); break; } @@ -520,7 +528,12 @@ static const uint8_t kSchemeWhiteValue[] = { 0x11, // 4: OD_COLOR_SCHEME_BWGBRY — 4bpp Spectra6, nibble 1 = white 0xFF, // 5: OD_COLOR_SCHEME_GRAY4 — 2bpp gray, code 11b per pixel = white 0xFF, // 6: OD_COLOR_SCHEME_GRAY16 — 4bpp Seeed 16-gray, nibble 15 = white - 0xFF, // 7: OD_COLOR_SCHEME_SEVEN_COLOR — 1bpp (default) + // 7: OD_COLOR_SCHEME_SEVEN_COLOR — NOT properly implemented. There is no 7-color + // rendering path; this entry deliberately treats scheme 7 as a 1bpp mono stub + // (0xFF = all-ones white plane, same as MONO). Kept as a positional placeholder so + // schemes >= 8 index correctly and the value stays aligned with the canonical + // SEVEN_COLOR enum. Replace with a real 7-color white code if/when support lands. + 0xFF, // 7: OD_COLOR_SCHEME_SEVEN_COLOR — 1bpp mono stub (see note above) 0x11, // 8: OD_COLOR_SCHEME_BWGBRY_SPLIT — same white nibble as BWGBRY }; @@ -599,6 +612,12 @@ bool writeBootScreenWithQr() { if (useZoneLayout && footerH > footerPadTop + 20) { switch (colorScheme) { case OD_COLOR_SCHEME_MONO: + // WARNING: SEVEN_COLOR (scheme 7) is NOT properly implemented — it is + // deliberately lumped in with MONO here and emits only 2 black/white footer + // swatches, NOT a 7-color palette. There is no real 7-color swatch/plane + // code anywhere in this firmware. Kept aligned with the canonical + // OD_COLOR_SCHEME_SEVEN_COLOR enum value; add a dedicated case with the true + // 7-color palette when actual support is implemented. case OD_COLOR_SCHEME_SEVEN_COLOR: swatchCode[0] = 0; swatchCode[1] = 1; numSwatches = 2; From 56019e7d827c602312da117c4fbca9a25ef19e55 Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:28:58 -0400 Subject: [PATCH 5/5] feat(power): split CMD_POWER_OFF (0x0052) from CMD_DEEP_SLEEP (0x0053) Re-vendor opendisplay_protocol.h (byte-identical to canonical) for the 2.1 opcode swap: CMD_DEEP_SLEEP moves 0x0052 -> 0x0053, and a new CMD_POWER_OFF takes 0x0052, with RESP_POWER_OFF (0x52) and the scoped OD_ERR_POWER_OFF_* / OD_ERR_DEEP_SLEEP_* NACK namespaces. Then reconcile the firmware source: - Dispatch (communication.cpp): route CMD_POWER_OFF -> handlePowerOffCommand, CMD_DEEP_SLEEP -> handleDeepSleepCommand. Auth-gating is inherited from the pre-dispatch check. - handlePowerOffCommand (0x0052): bare request (no payload); D-FF-latch HW does the fire-and-forget hard rail-cut with RESP_POWER_OFF; every non-latch target NACKs OD_ERR_POWER_OFF_UNSUPPORTED. No #ifdef needed - powerLatch* are no-op stubs off ESP32 (powerLatchDffConfigured() returns false -> falls to the NACK). Carries an ANCHOR() for a future "deep sleep, no wake timer" fallback. - handleDeepSleepCommand (0x0053): timed-deep-sleep path only (latch hard-off branch removed); named OD_ERR_DEEP_SLEEP_* NACK codes; log strings derive from the CMD_ constants; documented (behavior-preserving) nRF #else. - Deep-sleep override: enforce a 60 s floor on host-supplied wake timers (0 still means "no override -> use config"); correct the 0x0052 -> 0x0053 doc references in main.cpp/main.h for the override payload. Builds verified clean on nrf52840custom (stub latch path) and esp32-s3-N16R8 (real latch path). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/opendisplay_protocol.h | 155 ++++++++++++++++++++++++++++----- src/communication.cpp | 3 + src/device_control.cpp | 72 +++++++++++---- src/device_control.h | 1 + src/main.cpp | 4 +- src/main.h | 4 +- 6 files changed, 198 insertions(+), 41 deletions(-) diff --git a/include/opendisplay_protocol.h b/include/opendisplay_protocol.h index 88c0184..67e5e8c 100644 --- a/include/opendisplay_protocol.h +++ b/include/opendisplay_protocol.h @@ -9,8 +9,8 @@ * image PIPE. A new engineer or an AI agent should be able to implement a * fully-correct client from THIS FILE ALONE, without reading firmware. * - * OD_PROTOCOL_VERSION 2.0 (MAJOR.MINOR; see VERSIONING POLICY below) - * LAST CHANGED 2026-07-16 + * OD_PROTOCOL_VERSION 2.1 (MAJOR.MINOR; see VERSIONING POLICY below) + * LAST CHANGED 2026-07-19 * * -------------------------------------------------------------------------- * VERSIONING POLICY @@ -52,13 +52,33 @@ * CHANGELOG (newest first; entries accrue under "Unreleased" and roll into a * new version heading on each bump -- see AGENT INSTRUCTIONS below) * -------------------------------------------------------------------------- - * Unreleased (since 2.0) - * - Doc-only: LANGUAGE / LINKAGE RULE now requires macro VALUES to stay - * simple (literal or reference to a prior macro) so the header stays - * parseable by tools/gen_python_protocol.py. No wire bytes change; no bump. + * Unreleased (since 2.1) * - Add each further wire-spec change here as it lands. On the next version * bump, move these under a new "MAJOR.MINOR (YYYY-MM-DD)" heading. * + * 2.1 (2026-07-18) + * - MINOR: new CMD_POWER_OFF (0x0052) -- explicit hard rail-cut via the + * D-FF power latch; wakes ONLY on a physical button. Fire-and-forget + * (ACK queued best-effort, usually dies before the rail is cut). Adds + * RESP_POWER_OFF (0x52) and the scoped OD_ERR_POWER_OFF_* namespace + * (SECTION 4d). + * - CMD_DEEP_SLEEP is opcode 0x0053. It documents its optional one-shot + * [seconds:2 BE] wake-timer payload (originally shipped on 0x0052, + * Firmware PR #97) and gains the scoped OD_ERR_DEEP_SLEEP_* NACK + * namespace (SECTION 4c: 0x00 unsupported, 0x01 disabled, 0x02 + * not-battery). Client rule: 0x01/0x02 mean "rejected, still awake". + * - BREAKING vs shipped firmware: CMD_DEEP_SLEEP moved 0x0052 -> 0x0053, + * swapped with CMD_POWER_OFF so power-off takes the lower number. Since + * 0x0052 = deep-sleep shipped in Firmware PR #97, a peer on the old + * mapping now hits the wrong command. Corrected in place within the + * (unreleased) 2.1 line rather than a MAJOR bump (amended 2026-07-19). + * - BEHAVIORAL: target design for CMD_DEEP_SLEEP (0x0053) on D-FF-latch + * ESP32 changes from power-off to timer-wake deep sleep (latch held, like + * non-latch boards); the hard rail-cut is CMD_POWER_OFF (0x0052). + * - Doc-only: LANGUAGE / LINKAGE RULE now requires macro VALUES to stay + * simple (literal or reference to a prior macro) so the header stays + * parseable by tools/gen_python_protocol.py. No wire bytes change. + * * 2.0 (2026-07-15) * - Initial shared protocol contract: one self-documenting header vendored * into all firmware repos (Firmware, NRF54, Silabs, NRF52811). @@ -208,8 +228,8 @@ * when to bump which number. This marker describes the spec and is NOT sent on * the wire (the negotiated 0x0080 PIPE_VERSION is a separate field). */ #define OD_PROTOCOL_VERSION_MAJOR 2u -#define OD_PROTOCOL_VERSION_MINOR 0u -#define OD_PROTOCOL_VERSION_STR "2.0" +#define OD_PROTOCOL_VERSION_MINOR 1u +#define OD_PROTOCOL_VERSION_STR "2.1" /* ========================================================================== * SECTION 1 -- COMMAND OPCODES (16-bit, big-endian on the wire) @@ -347,19 +367,89 @@ #define CMD_ENTER_DFU 0x0051u /* -------------------------------------------------------------------------- - * @opcode: 0x0052 @name: CMD_DEEP_SLEEP @dir: host->device - * @request: [0x00][0x52] OR [0x00][0x52][seconds:2 BE] (optional wake timer) - * @response: best-effort / target-specific; tolerate the link dropping: - * ESP32 w/ power latch : [0x00][0x52] then powers off (~100 ms). - * ESP32 w/o latch : sleeps immediately, no ACK, link drops. - * Silabs Flex : [0x00][0x52] then closes link, enters EM4. - * nRF : logs only (deep sleep unsupported). - * @errors: none - * @state: session required when security enabled. - * @limits: optional [seconds:2 BE] wake interval. - * @targets: Firmware (ESP32) | Silabs (nRF targets: no-op) + * @opcode: 0x0052 @name: CMD_POWER_OFF @dir: host->device + * @request: [0x00][0x52] (bare). Any trailing payload is RESERVED and + * ignored today; senders SHOULD send none. + * @response: FIRE-AND-FORGET (same acknowledgement model as CMD_DEEP_SLEEP + * 0x0053): + * [0x00][0x52] ACK is QUEUED on latch HW but usually + * dies in the TX buffer before the rail + * is cut (~100 ms later). Expected, NOT + * a bug; senders MUST NOT wait for it -- + * treat transmit as "delivered". + * [0xFF][0x52][0x00][0x00] no power latch on this target. + * [0xFE][0x52] auth required (no live session). + * @errors: NACK data[0] scoped to opcode 0x52 (OD_ERR_POWER_OFF_*, see + * SECTION 4d): 0x00 UNSUPPORTED (no D-FF latch). A target that + * refuses 0x52 may still accept 0x53 deep sleep -- do not conflate + * "power-off unsupported" with "cannot sleep". + * @state: session required when security enabled. Cuts the power rail via + * the D-FF latch; the device wakes ONLY on a physical button press + * (fresh cold boot). No timer, no wake interval -- power-off is + * absolute. + * @limits: trailing payload reserved/ignored. + * @targets: Firmware (ESP32, D-FF power-latch HW ONLY). All other targets + * (non-latch/mains ESP32, Silabs, nRF) NACK [0xFF][0x52][0x00]. + * @since: new in 2.1 -- splits the hard rail-cut out of CMD_DEEP_SLEEP so + * latch HW gets timed-wake sleep via 0x0053 and an explicit, + * capability-gated hard-off via 0x0052. + * @changed: opcode is 0x0052 -- swapped with CMD_DEEP_SLEEP within the + * (unreleased) 2.1 line so power-off takes the lower number. NOTE: + * 0x0052 was CMD_DEEP_SLEEP in shipped firmware (PR #97); a peer + * still on the old mapping will hit deep-sleep here. + * -------------------------------------------------------------------------- */ +#define CMD_POWER_OFF 0x0052u + +/* -------------------------------------------------------------------------- + * @opcode: 0x0053 @name: CMD_DEEP_SLEEP @dir: host->device + * @request: [0x00][0x53] -> sleep now, configured cadence. + * [0x00][0x53][seconds:2 BE] -> sleep now; wake timer = seconds + * for THIS cycle only (one-shot, + * never persisted). + * - seconds is a BIG-ENDIAN uint16 (deliberate, documented + * exception to the LE payload default). Range 1..65535 s + * (~18.2 h; the wire width is the ceiling). + * - seconds == 0x0000 or empty payload = "no override" (use + * configured deep_sleep_time_seconds); NOT an error. + * - len == 1 is malformed -> logged, treated as no override + * (not rejected). Bytes beyond the 2 seconds bytes are ignored + * (forward compatibility). + * @response: FIRE-AND-FORGET. ACK/NACK are OPTIONAL / best-effort; a sender + * MUST NOT expect, block on, or infer success/failure from any + * frame. The SUCCESS path emits NO frame: the device enters deep + * sleep and BLE drops -- treat disconnect/silence as "delivered". + * [0xFF][0x53][0x01][0x00] deep sleep disabled in config. + * [0xFF][0x53][0x02][0x00] mains-powered; refuses to sleep. + * [0xFF][0x53][0x00][0x00] unsupported target (nRF; may instead + * stay silent and log). + * [0xFE][0x53] auth required (no live session). + * @errors: NACK data[0] scoped to opcode 0x53 (OD_ERR_DEEP_SLEEP_*, see + * SECTION 4c): 0x00 UNSUPPORTED, 0x01 DISABLED + * (deep_sleep_time_seconds == 0), 0x02 NOT_BATTERY (power_mode + * != 1). CRITICAL client rule: 0x01/0x02 mean "rejected -- device + * still AWAKE and reachable"; ONLY 0x00 means the target lacks deep + * sleep. Never conflate them. + * @state: session required when security enabled. The override is consumed + * at sleep entry; the next wake (and every later one) reverts to the + * configured cadence. An aborted sleep discards the override. + * @limits: optional [seconds:2 BE]; ceiling 65535 s (wire width). Recommended + * client-side floor >= 10 s (wake-storm guard). + * @targets: Firmware (ESP32: honors duration, incl. D-FF-latch HW -- target + * design, see @changed; reTerminal E1001/2/3 have no latch and + * already plain timer-sleep) | Silabs (ACKs, IGNORES payload, enters + * EM4) | nRF targets: unsupported. + * @changed: BREAKING vs shipped firmware -- opcode moved 0x0052 -> 0x0053 + * (swapped with CMD_POWER_OFF within the unreleased 2.1 line). The + * [seconds:2 BE] payload shipped on 0x0052 (Firmware PR #97), so a + * peer still on 0x0052 will now hit CMD_POWER_OFF. Also target + * design: D-FF-latch ESP32 timer-sleeps (latch HELD) instead of + * powering off; the hard rail-cut is CMD_POWER_OFF (0x0052). The + * request/response byte layout is otherwise UNCHANGED. + * @since: [seconds:2 BE] one-shot payload shipped in-2.x on 0x0052 (Firmware + * PR #97); documented, moved to 0x0053, and error namespace added + * in 2.1. * -------------------------------------------------------------------------- */ -#define CMD_DEEP_SLEEP 0x0052u +#define CMD_DEEP_SLEEP 0x0053u /* -------------------------------------------------------------------------- * @opcode: 0x0070 @name: CMD_DIRECT_WRITE_START @dir: host->device @@ -582,7 +672,8 @@ #define RESP_CONFIG_CLEAR 0x45u #define RESP_AUTHENTICATE 0x50u #define RESP_ENTER_DFU 0x51u -#define RESP_DEEP_SLEEP 0x52u +#define RESP_POWER_OFF 0x52u +#define RESP_DEEP_SLEEP 0x53u /* Direct-write family (Firmware / NRF52811 + shared). */ #define RESP_DIRECT_WRITE_START_ACK 0x70u @@ -644,6 +735,9 @@ * OD_ERR_PIPE_START_* codes. * - The NFC handler (CMD_NFC_ENDPOINT 0x83) MUST emit ONLY NFC_ERR_* codes * (SECTION 5). + * - The CMD_POWER_OFF handler (0x52) MUST emit ONLY OD_ERR_POWER_OFF_* + * codes (SECTION 4d); the CMD_DEEP_SLEEP handler (0x53) ONLY + * OD_ERR_DEEP_SLEEP_* codes (SECTION 4c). * A code from one namespace must NEVER appear in another handler's NACK. The * overlapping raw byte values are intentional and safe ONLY because the echoed * opcode scopes them -- pick the constant by which opcode you are answering. @@ -674,6 +768,25 @@ #define OD_ERR_PIPE_START_PARTIAL_UNSUPPORTED 0x06u /* partial mode not supported */ #define OD_ERR_PIPE_START_RECT_INVALID 0x07u /* partial: invalid rectangle */ +/* -------------------------------------------------------------------------- + * 4c. DEEP-SLEEP errors -- scope: CMD_DEEP_SLEEP (0x0053) ONLY. + * NACK frame: [0xFF][0x53][err][0x00]. Byte values 0x00..0x02 are reused + * from other namespaces -- scoped by the echoed opcode, as always. Note + * 0x00 IS a valid error code here (a NACK's data[0], never a success + * marker). CLIENT RULE: 0x01/0x02 mean "rejected, device still AWAKE and + * reachable"; ONLY 0x00 means deep sleep is unsupported on the target. + * Never conflate the two classes. + * -------------------------------------------------------------------------- */ +#define OD_ERR_DEEP_SLEEP_UNSUPPORTED 0x00u /* target has no deep sleep (nRF) */ +#define OD_ERR_DEEP_SLEEP_DISABLED 0x01u /* deep_sleep_time_seconds == 0 in config */ +#define OD_ERR_DEEP_SLEEP_NOT_BATTERY 0x02u /* power_mode != 1 (mains-powered) */ + +/* -------------------------------------------------------------------------- + * 4d. POWER-OFF errors -- scope: CMD_POWER_OFF (0x0052) ONLY. + * NACK frame: [0xFF][0x52][err][0x00]. + * -------------------------------------------------------------------------- */ +#define OD_ERR_POWER_OFF_UNSUPPORTED 0x00u /* no D-FF power latch on this target */ + /* ========================================================================== * SECTION 5 -- NFC SUB-PROTOCOL (rides CMD_NFC_ENDPOINT 0x0083) * ========================================================================== */ diff --git a/src/communication.cpp b/src/communication.cpp index f533900..2559e2b 100644 --- a/src/communication.cpp +++ b/src/communication.cpp @@ -657,6 +657,9 @@ void imageDataWritten(BLEConnHandle conn_hdl, BLECharPtr chr, uint8_t* data, uin writeSerial("=== ENTER DFU MODE COMMAND (0x0051) ==="); enterDFUMode(); break; + case CMD_POWER_OFF: + handlePowerOffCommand(data + 2, len - 2); + break; case CMD_DEEP_SLEEP: handleDeepSleepCommand(data + 2, len - 2); break; diff --git a/src/device_control.cpp b/src/device_control.cpp index b9fd18c..0e797bf 100644 --- a/src/device_control.cpp +++ b/src/device_control.cpp @@ -707,7 +707,7 @@ void enterDFUMode() { } void handleDeepSleepCommand(const uint8_t* payload, uint16_t payloadLen) { - writeSerial("=== DEEP SLEEP COMMAND (0x0052) ===", true); + writeSerial("=== DEEP SLEEP COMMAND (0x" + String(CMD_DEEP_SLEEP, HEX) + ") ===", true); #ifdef TARGET_ESP32 // Optional 2-byte big-endian seconds payload overrides the configured // deep-sleep duration for exactly one cycle. 0x0000 = explicit no-override. @@ -716,35 +716,75 @@ void handleDeepSleepCommand(const uint8_t* payload, uint16_t payloadLen) { overrideSeconds = ((uint16_t)payload[0] << 8) | payload[1]; // Bytes beyond 2 ignored for forward compatibility. } else if (payloadLen == 1) { - writeSerial("WARNING: malformed 0x0052 payload length 1 - ignoring", true); - } - if (powerLatchDffConfigured()) { - if (overrideSeconds != 0) { - writeSerial("0x0052 duration payload ignored (D-FF hard power off has no timer)", true); - } - uint8_t ok[] = {RESP_ACK, RESP_DEEP_SLEEP, 0x00, 0x00}; - sendResponse(ok, sizeof(ok)); - delay(100); - powerLatchPowerOff(); - return; + writeSerial("WARNING: malformed 0x" + String(CMD_DEEP_SLEEP, HEX) + " payload length 1 - ignoring", true); + } + // Enforce a 60 s floor on host overrides: a very short wake timer risks a rapid + // sleep/wake churn that never stays awake long enough to service a client. This + // applies to the OVERRIDE only; overrideSeconds == 0 means "no override" and defers + // to the configured deep_sleep_time_seconds, which is not subject to this floor. + constexpr uint16_t MIN_DEEP_SLEEP_OVERRIDE_SECONDS = 60; + if (overrideSeconds != 0 && overrideSeconds < MIN_DEEP_SLEEP_OVERRIDE_SECONDS) { + writeSerial("Override " + String(overrideSeconds) + "s below " + + String(MIN_DEEP_SLEEP_OVERRIDE_SECONDS) + "s floor - clamping", true); + overrideSeconds = MIN_DEEP_SLEEP_OVERRIDE_SECONDS; } if (globalConfig.power_option.power_mode != 1) { - writeSerial("Device not battery powered - 0x0052 rejected", true); - uint8_t errorResponse[] = {RESP_NACK, RESP_DEEP_SLEEP, 0x02, 0x00}; + writeSerial("Device not battery powered - 0x" + String(CMD_DEEP_SLEEP, HEX) + " rejected", true); + uint8_t errorResponse[] = {RESP_NACK, RESP_DEEP_SLEEP, OD_ERR_DEEP_SLEEP_NOT_BATTERY, 0x00}; sendResponse(errorResponse, sizeof(errorResponse)); return; } if (globalConfig.power_option.deep_sleep_time_seconds == 0) { - writeSerial("Deep sleep disabled in config - 0x0052 rejected", true); - uint8_t errorResponse[] = {RESP_NACK, RESP_DEEP_SLEEP, 0x01, 0x00}; + writeSerial("Deep sleep disabled in config - 0x" + String(CMD_DEEP_SLEEP, HEX) + " rejected", true); + uint8_t errorResponse[] = {RESP_NACK, RESP_DEEP_SLEEP, OD_ERR_DEEP_SLEEP_DISABLED, 0x00}; sendResponse(errorResponse, sizeof(errorResponse)); return; } // Explicit host request: sleep even though the requesting client is connected. enterDeepSleep(true, overrideSeconds); #else + // Non-ESP32 (nRF etc.) have no timer deep sleep. The protocol permits a NACK here + // ([0xFF][0x53][OD_ERR_DEEP_SLEEP_UNSUPPORTED][0x00]), but we intentionally stay + // silent to preserve existing behavior — leave as-is unless a caller needs the NACK. (void)payload; (void)payloadLen; writeSerial("Deep sleep command not supported on this target", true); #endif } + +void handlePowerOffCommand(const uint8_t* payload, uint16_t payloadLen) { + writeSerial("=== POWER OFF COMMAND (0x" + String(CMD_POWER_OFF, HEX) + ") ===", true); + // CMD_POWER_OFF request is bare [0x00][0x52]; any trailing payload is RESERVED + // and ignored (unlike CMD_DEEP_SLEEP 0x0053, this has no duration payload). + (void)payload; + (void)payloadLen; + // powerLatch* are defined on every target (no-op stubs off ESP32; the header + // guarantees callers need no guards), so no #ifdef TARGET_ESP32 is required here: + // powerLatchDffConfigured() returns false off-ESP32 -> falls through to the NACK. + if (powerLatchDffConfigured()) { + // Fire-and-forget hard rail-cut: queue the ACK, then release the D-FF latch. + // On latch HW the rail usually drops before the ACK is actually transmitted. + uint8_t ok[] = {RESP_ACK, RESP_POWER_OFF, 0x00, 0x00}; + sendResponse(ok, sizeof(ok)); + delay(100); + powerLatchPowerOff(); + return; + } + // ANCHOR(power-off-no-latch-fallback): FUTURE WORK for a later agent/implementer. + // On non-latch BATTERY targets, implement "enter deep sleep with NO wake timer" + // (sleep indefinitely; wake only on button/reset) as the closest equivalent to a + // hard power-off, instead of the unsupported NACK below. Caveats before doing so: + // 1. This DEVIATES from the current protocol, which mandates + // OD_ERR_POWER_OFF_UNSUPPORTED (0x00) for non-latch targets — update the + // 0x0052 @response contract in opendisplay-protocol/src/opendisplay_protocol.h + // FIRST, then propagate the header and implement here. + // 2. enterDeepSleep(true, 0) currently means "use the configured duration", NOT + // "no timer" — a genuinely timer-less deep-sleep entry path must be added. + // 3. Mains-powered targets (power_option.power_mode != 1) must STILL NACK — never + // sleep a device that cannot self-repower. + // Until that lands: capability-gated NACK. Scope: OD_ERR_POWER_OFF_* only — do NOT + // conflate with 0x53 deep sleep (a device that refuses 0x52 may still accept 0x53). + writeSerial("No power latch on this target - 0x" + String(CMD_POWER_OFF, HEX) + " rejected", true); + uint8_t errorResponse[] = {RESP_NACK, RESP_POWER_OFF, OD_ERR_POWER_OFF_UNSUPPORTED, 0x00}; + sendResponse(errorResponse, sizeof(errorResponse)); +} diff --git a/src/device_control.h b/src/device_control.h index c68dc92..3010e77 100644 --- a/src/device_control.h +++ b/src/device_control.h @@ -14,5 +14,6 @@ void handleLedActivate(uint8_t* data, uint16_t len); void handleLedStop(uint8_t* data, uint16_t len); void enterDFUMode(); void handleDeepSleepCommand(const uint8_t* payload, uint16_t payloadLen); +void handlePowerOffCommand(const uint8_t* payload, uint16_t payloadLen); #endif diff --git a/src/main.cpp b/src/main.cpp index e06c649..cb129a5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -526,7 +526,7 @@ void enterDeepSleep(bool force, uint16_t overrideSleepSeconds) { // Defense in depth for the min-wake hold (first boot / button wake). MUST // stay ahead of the advertising stop below: everything past that point // commits to esp_deep_sleep_start(), so a late abort would leave the device - // awake with the radio dark. force (host 0x0052) bypasses the hold. + // awake with the radio dark. force (host 0x0053) bypasses the hold. if (!force && minWakeHoldActive()) { writeSerial("Skipping deep sleep - minimum wake window active"); return; @@ -553,7 +553,7 @@ void enterDeepSleep(bool force, uint16_t overrideSleepSeconds) { esp32_ble_clear_handles(); delay(100); writeSerial("BLE deinitialized"); - // Host override (0x0052 payload) applies to this one cycle only: it is a + // Host override (0x0053 payload) applies to this one cycle only: it is a // parameter, never stored, so an aborted or later sleep reverts to config. uint16_t sleepSeconds = overrideSleepSeconds ? overrideSleepSeconds : globalConfig.power_option.deep_sleep_time_seconds; diff --git a/src/main.h b/src/main.h index c8d248b..46fb59a 100644 --- a/src/main.h +++ b/src/main.h @@ -188,9 +188,9 @@ void connect_callback(uint16_t conn_handle); void disconnect_callback(uint16_t conn_handle, uint8_t reason); #ifdef TARGET_ESP32 void fullSetupAfterConnection(); -// force: sleep even with a client connected (explicit host request 0x0052). +// force: sleep even with a client connected (explicit host request 0x0053). // overrideSleepSeconds: nonzero replaces deep_sleep_time_seconds for this one -// sleep cycle (0x0052 duration payload); never changes sleep eligibility. +// sleep cycle (0x0053 duration payload); never changes sleep eligibility. void enterDeepSleep(bool force = false, uint16_t overrideSleepSeconds = 0); extern bool advertising_timeout_active; extern uint32_t advertising_start_time;