openapi: "3.0.3"

info:
  title: GX1 Interface
  version: "1.0"
  description: |
    JSON interface for the **GX1 device** (RP2350 microcontroller) over USB CDC serial.

    ## Transport
    Communication uses a newline-delimited JSON protocol over a virtual USB serial port
    (USB CDC / ACM).  Each message is a single JSON object terminated by `\n`.

    ### Request format
    Every request must contain a `"cmd"` field identifying the command.
    Additional fields depend on the specific command.

    ### Response format
    Every response contains a `"status"` field:
    - `"ok"` — command succeeded; may include additional result fields.
    - `"error"` — command failed; the `"message"` field explains the reason.

    ## Physical connection
    | Parameter | Value         |
    |-----------|---------------|
    | Baud rate | 115 200       |
    | Data bits | 8             |
    | Stop bits | 1             |
    | Parity    | None          |
    | Flow ctrl | None (USB CDC handles framing) |

tags:
  - name: System
    description: Device health and diagnostics
  - name: LED
    description: Built-in LED control (PWM brightness + blink)
  - name: RGB
    description: WS2812 on-board RGB pixel
  - name: Sensors
    description: On-chip sensors
  - name: Inputs
    description: On-board switches and discrete inputs
  - name: Firmware
    description: Firmware management (BOOTSEL / re-flash)
  - name: X10i I/O
    description: |
      X10i-compatible bulk I/O: 24 IP inputs via three MCP23008 expanders
      and 24 OP outputs via three TLE75008 low-side switches in daisy chain.
  - name: ccTalk
    description: ccTalk bus pass-through (UART0 on GP28/29, 9600 8N1)

# ---------------------------------------------------------------------------
# NOTE: This YAML describes the raw JSON protocol, not an HTTP API.
# The "paths" section is used here as a structured way to document each
# command / response pair.  In practice messages are exchanged over serial.
# ---------------------------------------------------------------------------

paths:

  # -------------------------------------------------------------------------
  # ping
  # -------------------------------------------------------------------------
  /cmd/ping:
    post:
      operationId: ping
      tags: [System]
      summary: Check device is alive
      description: |
        The simplest liveness check.  The device echoes `{"status": "ok"}` as
        quickly as possible, allowing the host to verify the connection and
        measure round-trip latency.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PingRequest"
            examples:
              basic:
                summary: Ping request
                value:
                  cmd: ping
      responses:
        "200":
          description: Device is alive.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
              examples:
                pong:
                  summary: Successful pong
                  value:
                    status: ok
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  # -------------------------------------------------------------------------
  # version — report firmware protocol version and build identifier
  # -------------------------------------------------------------------------
  /cmd/version:
    post:
      operationId: getVersion
      tags: [System]
      summary: Read protocol version and build identifier
      description: |
        Returns the firmware's protocol version (integer, bumped each time a
        command is added or changes shape) and a build identifier (UTC
        timestamp string regenerated on every compile via CMake).

        Use this from the host to verify that a freshly flashed firmware is
        the one you just compiled, or to gate features on a minimum protocol
        version.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VersionRequest"
            examples:
              basic:
                summary: Version request
                value:
                  cmd: version
      responses:
        "200":
          description: Firmware version information.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VersionResponse"
              examples:
                v2:
                  summary: Protocol v2, built 2026-01-18
                  value:
                    status: ok
                    protocol: 2
                    build: "2026-01-18 14:32:11 UTC"
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  # -------------------------------------------------------------------------
  # led
  # -------------------------------------------------------------------------
  /cmd/led:
    post:
      operationId: ledControl
      tags: [LED]
      summary: Control the built-in LED
      description: |
        Controls the RP2350 built-in LED (GPIO 25) via PWM.

        Three actions are supported:
        - **on** — illuminate at a specified brightness.
        - **off** — extinguish immediately (duty cycle = 0).
        - **blink** — start an indefinite blink cycle; a subsequent `on` or
          `off` command cancels it.

        ### Brightness
        Brightness is expressed as a floating-point value in [0.0, 1.0].
        It maps linearly to the PWM duty cycle (0 – 65535 at 1 kHz).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LedRequest"
            examples:
              led_on_full:
                summary: Turn LED on at full brightness
                value:
                  cmd: led
                  action: on
                  brightness: 1.0
              led_on_half:
                summary: Turn LED on at 50% brightness
                value:
                  cmd: led
                  action: on
                  brightness: 0.5
              led_off:
                summary: Turn LED off
                value:
                  cmd: led
                  action: off
              led_blink:
                summary: Blink LED (500 ms on / 500 ms off, full brightness)
                value:
                  cmd: led
                  action: blink
                  on_ms: 500
                  off_ms: 500
                  brightness: 1.0
              led_blink_fast:
                summary: Fast blink at half brightness
                value:
                  cmd: led
                  action: blink
                  on_ms: 100
                  off_ms: 100
                  brightness: 0.5
      responses:
        "200":
          description: LED command accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
              examples:
                ok:
                  value:
                    status: ok
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  # -------------------------------------------------------------------------
  # temperature
  # -------------------------------------------------------------------------
  /cmd/temperature:
    post:
      operationId: readTemperature
      tags: [Sensors]
      summary: Read internal CPU temperature
      description: |
        Reads the RP2350 built-in temperature sensor (ADC channel 4) and
        returns the computed value in degrees Celsius.

        ### Conversion formula (from RP2350 datasheet)
        ```
        V    = adc_raw * (3.3 / 65535)
        T_°C = 27 - (V - 0.706) / 0.001721
        ```

        The returned value is rounded to two decimal places.
        Typical idle temperature on a Pico board is 25–35 °C.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TemperatureRequest"
            examples:
              basic:
                summary: Temperature request
                value:
                  cmd: temperature
      responses:
        "200":
          description: Temperature reading.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemperatureResponse"
              examples:
                reading:
                  summary: Typical reading
                  value:
                    status: ok
                    temperature: 28.41
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  # -------------------------------------------------------------------------
  # sw — read on-board DIP switch
  # -------------------------------------------------------------------------
  /cmd/sw:
    post:
      operationId: readSwitches
      tags: [Inputs]
      summary: Read the three on-board DIP switches
      description: |
        Reads the state of the three on-board DIP switches (SW0/SW1/SW2 on
        GPIO22/23/24). Switches use internal pull-ups and short to GND when
        ON; the firmware returns logical state (ON = true).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SwitchRequest"
            examples:
              basic:
                summary: Switch read
                value:
                  cmd: sw
      responses:
        "200":
          description: Switch states.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SwitchResponse"
              examples:
                reading:
                  summary: Two switches on
                  value:
                    status: ok
                    sw: [true, false, true]
                    mask: 5
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  # -------------------------------------------------------------------------
  # inputs — read all 24 IPs + 4 security switches
  # -------------------------------------------------------------------------
  /cmd/inputs:
    post:
      operationId: readInputs
      tags: [X10i I/O]
      summary: Read all IP inputs from the three MCP23008 expanders
      description: |
        Reads the GPIO register of each MCP23008 (U4=0x20, U5=0x21, U6=0x24)
        and returns both the raw chip bytes and an unpacked 24-element
        array of IP states plus the 4 IP-SW security switches.

        Internal pull-ups are enabled on every expander pin, so an idle or
        disconnected input reads 1. Whether 0 means "pressed", "closed" or
        "active" depends on the X10i function assigned to the pin — see
        the IP_NAMES / SEC_SW_NAMES tables on the host.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InputsRequest" }
            examples:
              basic: { value: { cmd: inputs } }
      responses:
        "200":
          description: Input snapshot.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InputsResponse" }
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  # -------------------------------------------------------------------------
  # outputs — full 24-bit mask read/write
  # -------------------------------------------------------------------------
  /cmd/outputs:
    post:
      operationId: outputsMask
      tags: [X10i I/O]
      summary: Read or write the full 24-bit OP mask
      description: |
        Bit n of *mask* corresponds to OP(n + 8): bit 0 = OP8 … bit 23 = OP31.
        Setting a bit to 1 turns that low-side switch ON. Omit *mask* to read.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OutputsRequest" }
            examples:
              read:  { value: { cmd: outputs } }
              all_off: { value: { cmd: outputs, mask: 0 } }
              lamps_only: { value: { cmd: outputs, mask: 0x00FF00 } }
      responses:
        "200":
          description: Current output mask after the operation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OutputsResponse" }

  # -------------------------------------------------------------------------
  # output — set a single OP
  # -------------------------------------------------------------------------
  /cmd/output:
    post:
      operationId: outputOne
      tags: [X10i I/O]
      summary: Set a single OP output (8..31)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OutputRequest" }
            examples:
              lamp_start_on: { value: { cmd: output, op: 16, state: true } }
      responses:
        "200":
          description: New mask after the change.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OutputsResponse" }

  # -------------------------------------------------------------------------
  # pulse — short ON pulse (counters)
  # -------------------------------------------------------------------------
  /cmd/pulse:
    post:
      operationId: outputPulse
      tags: [X10i I/O]
      summary: Pulse an OP output for N milliseconds
      description: |
        Drives the output ON for *ms* milliseconds, then back OFF. The
        firmware blocks until the pulse completes (so the response timing
        is a useful sanity check on the pulse width).

        Typical use: electromechanical counters (OP8..OP14) require a
        ~50 ms pulse per increment.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PulseRequest" }
            examples:
              count_in:  { value: { cmd: pulse, op: 8,  ms: 50 } }
              count_win: { value: { cmd: pulse, op: 13, ms: 50 } }
      responses:
        "200":
          description: Pulse complete.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OkResponse" }

  # -------------------------------------------------------------------------
  # diag — TLE75008 Output Status Monitor across all 24 OPs
  # -------------------------------------------------------------------------
  /cmd/diag:
    post:
      operationId: outputDiagnose
      tags: [X10i I/O]
      summary: Detect blown lamps and disconnected counters
      description: |
        Runs the TLE75008 Output Status Monitor on all 24 OPs and reports:

          - **open_load** — bit n set means OP(n+8) is OFF and the load
            (lamp, counter coil, wire) is disconnected. Result is masked
            against currently-ON channels because their V_DS ≈ 0 V
            triggers the OSM unconditionally (datasheet §9.2).
          - **over_err** — bit n set means OP(n+8) is latched OFF by the
            chip's protection (over-current or over-temperature) and must
            be re-enabled via `clear_latch` before it can drive again.
          - **was_on** — OP shadow mask captured at measurement time.

        The firmware briefly enables a ~85 µA diagnostic current source on
        every channel (≈2 mA total), waits ~200 µs for the comparators to
        settle, reads DIAG_OSM, then disables the current sources again.
        Total sequence takes < 0.5 ms.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DiagRequest" }
            examples:
              run: { value: { cmd: diag } }
      responses:
        "200":
          description: Per-OP diagnostic snapshot.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DiagResponse" }

  # -------------------------------------------------------------------------
  # clear_latch — re-enable an OP that protection latched off
  # -------------------------------------------------------------------------
  /cmd/clear_latch:
    post:
      operationId: outputClearLatch
      tags: [X10i I/O]
      summary: Clear the over-current / over-temperature latch on one OP
      description: |
        Writes `HWCR_OCL.OUTn = 1` on the appropriate TLE75008 chip. The
        bit auto-clears inside the device after de-latching; after this
        call the OP can be driven again with `output` / `pulse`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ClearLatchRequest" }
            examples:
              clear_op8:  { value: { cmd: clear_latch, op:  8 } }
              clear_op24: { value: { cmd: clear_latch, op: 24 } }
      responses:
        "200":
          description: Latch cleared.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ClearLatchResponse" }

  # -------------------------------------------------------------------------
  # cctalk — UART pass-through for coin acceptor / banknote validator
  # -------------------------------------------------------------------------
  /cmd/cctalk:
    post:
      operationId: cctalkTransact
      tags: [ccTalk]
      summary: Transmit + receive on the ccTalk UART
      description: |
        Sends *tx* (hex bytes or byte array) at 9600 8N1 on UART0 and then
        listens for up to *timeout_ms* milliseconds. Returns whatever bytes
        arrived, hex-encoded.

        The ccTalk physical layer is single-wire half-duplex, so the host's
        own transmission may echo back in *rx*. The caller is expected to
        strip the echo (length = *tx_len*) before parsing the reply.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CcTalkRequest" }
            examples:
              poll_addr_2:
                summary: ccTalk header 254 (poll) to address 2
                value: { cmd: cctalk, tx: "020000fe00", timeout_ms: 100 }
              listen_only:
                summary: Just listen for unsolicited traffic
                value: { cmd: cctalk, timeout_ms: 500 }
      responses:
        "200":
          description: ccTalk transaction result.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CcTalkResponse" }

  # -------------------------------------------------------------------------
  # bootsel — reboot into USB BOOTSEL / mass-storage mode
  # -------------------------------------------------------------------------
  /cmd/bootsel:
    post:
      operationId: enterBootsel
      tags: [Firmware]
      summary: Reboot into USB BOOTSEL (mass-storage) mode
      description: |
        Calls the RP2350 ROM routine `reset_usb_boot(0, 0)` so that the
        device disconnects from USB CDC and reappears as the **RPI-RP2**
        removable drive — no need to physically hold the BOOTSEL button.

        The firmware replies `{"status":"ok"}` first and schedules the reset
        ~100 ms later, giving the host time to receive the response before
        the USB connection drops. Drop a UF2 file onto the RPI-RP2 volume to
        flash, or power-cycle the board to return to the previous firmware.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BootselRequest"
            examples:
              basic:
                summary: Enter BOOTSEL
                value:
                  cmd: bootsel
      responses:
        "200":
          description: Reboot scheduled; device will disconnect shortly.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
              examples:
                ok:
                  value:
                    status: ok
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  # -------------------------------------------------------------------------
  # rgb — set on-board WS2812 pixel
  # -------------------------------------------------------------------------
  /cmd/rgb:
    post:
      operationId: setRgb
      tags: [RGB]
      summary: Set on-board WS2812 RGB LED
      description: |
        Drives the single on-board WS2812 pixel (GPIO 21, "LED_WS") via PIO.
        Channel values are 8-bit (0–255).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RgbRequest"
            examples:
              red:
                summary: Solid red
                value:
                  cmd: rgb
                  r: 255
                  g: 0
                  b: 0
              dim_white:
                summary: Dim white
                value:
                  cmd: rgb
                  r: 32
                  g: 32
                  b: 32
              off:
                summary: Turn off
                value:
                  cmd: rgb
                  r: 0
                  g: 0
                  b: 0
      responses:
        "200":
          description: RGB command accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  /cmd/wled_get:
    post:
      operationId: wledGet
      tags: [RGB]
      summary: Read current WLED-style state of the WS2812 strip
      description: |
        Returns the firmware's mirror of the WLED `/json/state` object plus a
        small `effects` catalogue (effect name indexed by `seg.fx`).
        Only segment 0 is supported (the strip is a single contiguous run).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WledGetRequest"
      responses:
        "200":
          description: Current WLED state and effect catalogue.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WledGetResponse"
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  /cmd/wled_set:
    post:
      operationId: wledSet
      tags: [RGB]
      summary: Apply a partial WLED-style state update
      description: |
        Accepts a WLED-shaped `state` object. Any field omitted keeps its
        current value (matches WLED POST /json/state semantics).
        Supported keys:
          * top-level: `on` (bool), `bri` (0..255)
          * `seg[0]`: `on`, `bri`, `len`, `fx`, `sx`, `ix`, `pal`,
            `col` (`[[r,g,b],[r,g,b],[r,g,b]]`),
            `i` (`[[r,g,b], ...]` — direct pixel writes, auto-sets fx=7 Static).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WledSetRequest"
            examples:
              solid_red:
                summary: Solid red across 100 LEDs
                value:
                  cmd: wled_set
                  state:
                    on: true
                    bri: 200
                    seg:
                      - len: 100
                        fx: 0
                        col: [[255,0,0],[0,0,0],[0,0,0]]
              rainbow:
                summary: Slow rainbow
                value:
                  cmd: wled_set
                  state:
                    seg:
                      - fx: 3
                        sx: 60
              direct_pixels:
                summary: Set the first three pixels manually
                value:
                  cmd: wled_set
                  state:
                    seg:
                      - i: [[255,0,0],[0,255,0],[0,0,255]]
      responses:
        "200":
          description: New WLED state after the update.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WledGetResponse"
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  # -------------------------------------------------------------------------
  # fram_info — total size + per-request cap + JEDEC ID
  # -------------------------------------------------------------------------
  /cmd/fram_info:
    post:
      operationId: framInfo
      tags: [FRAM]
      summary: Report FRAM size and JEDEC ID
      description: |
        Returns the on-board FRAM total size (524 288 B for the MB85RS4MT),
        the per-request payload cap (a single read/write may carry at most
        this many bytes; chunk larger transfers), and the 4-byte JEDEC ID
        hex-encoded.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FramInfoRequest" }
            examples:
              basic:
                value: { cmd: fram_info }
      responses:
        "200":
          description: FRAM identity block.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FramInfoResponse" }

  # -------------------------------------------------------------------------
  # fram_read — read bytes, overflow-checked
  # -------------------------------------------------------------------------
  /cmd/fram_read:
    post:
      operationId: framRead
      tags: [FRAM]
      summary: Read bytes from the on-board FRAM
      description: |
        Reads *len* bytes starting at *addr* and returns them hex-encoded in
        the `data` field. *addr* must be in `[0, size)` and `addr + len` must
        not exceed `size` — otherwise the firmware answers with
        `status: "error"` and a descriptive message. *len* per single request
        is also capped at `max_xfer`; split larger reads at the client.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FramReadRequest" }
            examples:
              first_16:
                summary: First 16 bytes
                value: { cmd: fram_read, addr: 0, len: 16 }
              tail_4:
                summary: Last 4 bytes
                value: { cmd: fram_read, addr: 524284, len: 4 }
      responses:
        "200":
          description: Read result, hex-encoded.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FramReadResponse" }
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  # -------------------------------------------------------------------------
  # fram_write — write bytes, overflow-checked
  # -------------------------------------------------------------------------
  /cmd/fram_write:
    post:
      operationId: framWrite
      tags: [FRAM]
      summary: Write bytes to the on-board FRAM
      description: |
        Writes the bytes encoded as hex in *data* to FRAM at *addr*. The
        firmware automatically asserts the Write Enable Latch before the
        WRITE opcode, so no separate `wren` step is needed.

        Overflow protection: `addr ∈ [0, size)` and `addr + len(data)/2`
        must be `≤ size`; *data* length per request must be `≤ max_xfer`
        bytes (its hex length is twice that). Violations return
        `status: "error"`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FramWriteRequest" }
            examples:
              four_bytes:
                summary: Write DE AD BE EF at address 0
                value: { cmd: fram_write, addr: 0, data: "deadbeef" }
              tail:
                summary: Last byte of the device
                value: { cmd: fram_write, addr: 524287, data: "42" }
      responses:
        "200":
          description: Number of bytes written.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FramWriteResponse" }
        "4XX":
          $ref: "#/components/responses/ErrorResponse"

  # ---------------------------------------------------------------------------
  # X10i-replacement protocol (firmware ≥ v9). These commands exist so the
  # GS2 client can drive the GX1 as a drop-in replacement for the FireFly
  # X10i board. State (sec-switch latch, hw counters, polled-ccTalk buffers)
  # lives in the firmware so the host can stay stateless.
  # ---------------------------------------------------------------------------
  /cmd/poll:
    post:
      operationId: poll
      tags: [X10i-compat]
      summary: Atomic per-tick snapshot
      description: |
        Returns the inputs (24-bit IP mask + 4-bit sec-switch), the
        sec-switch transitions since the previous poll (on / off), the OP
        shadow mask and the bitmask of OPs currently being pulsed
        asynchronously. Replaces four separate X10i calls per tick.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd]
              properties:
                cmd: { type: string, enum: [poll] }
            examples:
              poll: { value: { cmd: poll } }
      responses:
        "200":
          description: Snapshot.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PollResponse" }

  /cmd/random:
    post:
      operationId: random
      tags: [X10i-compat]
      summary: 32-bit hardware random number (RP2350 TRNG)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd]
              properties:
                cmd: { type: string, enum: [random] }
      responses:
        "200":
          description: Random value.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  value:  { type: integer, minimum: 0, maximum: 4294967295 }

  /cmd/uptime:
    post:
      operationId: uptime
      tags: [X10i-compat]
      summary: Firmware uptime in milliseconds
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd]
              properties: { cmd: { type: string, enum: [uptime] } }
      responses:
        "200":
          description: Uptime.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:    { type: string, enum: [ok] }
                  uptime_ms: { type: integer, minimum: 0 }

  /cmd/lock:
    post:
      operationId: lock
      tags: [X10i-compat]
      summary: Try to acquire the advisory bus lock
      description: |
        Cooperative single-holder lock. The firmware does not enforce it on
        other commands — it's a coordination primitive for multiple host
        tools sharing the device.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd]
              properties:
                cmd:    { type: string, enum: [lock] }
                holder: { type: string, description: Identification stored on firmware. }
      responses:
        "200":
          description: Lock result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:   { type: string, enum: [ok] }
                  acquired: { type: boolean }
                  held:     { type: boolean }
                  holder:   { type: string }

  /cmd/unlock:
    post:
      operationId: unlock
      tags: [X10i-compat]
      summary: Release the advisory bus lock
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd]
              properties: { cmd: { type: string, enum: [unlock] } }
      responses:
        "200":
          description: OK.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AckResponse" }

  /cmd/pulse_async:
    post:
      operationId: pulseAsync
      tags: [X10i-compat]
      summary: Non-blocking OP pulse
      description: |
        Starts a pulse on `op` (8..31) for `ms` milliseconds and returns
        immediately. Multiple OPs can pulse concurrently. Use `pulse_status`
        or the `pulse_busy` field of `poll` to observe completion.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd, op]
              properties:
                cmd: { type: string, enum: [pulse_async] }
                op:  { type: integer, minimum: 8, maximum: 31 }
                ms:  { type: integer, minimum: 1, maximum: 2000, default: 80 }
      responses:
        "200":
          description: Pulse scheduled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  op:     { type: integer }
                  ms:     { type: integer }

  /cmd/pulse_status:
    post:
      operationId: pulseStatus
      tags: [X10i-compat]
      summary: Which OPs are currently being pulsed asynchronously
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd]
              properties: { cmd: { type: string, enum: [pulse_status] } }
      responses:
        "200":
          description: Pulse status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:    { type: string, enum: [ok] }
                  any_busy:  { type: boolean }
                  busy_mask: { type: integer, description: "Bit (op-8) = OP busy." }

  /cmd/nvram_read:
    post:
      operationId: nvramRead
      tags: [X10i-compat]
      summary: Read bytes from the GS2 NvRam region (FRAM-backed)
      description: |
        `addr` is the offset relative to the start of the generic NvRam
        region (FRAM offset 0x1000). Per-request payload capped at 256 B.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd, addr, len]
              properties:
                cmd:  { type: string, enum: [nvram_read] }
                addr: { type: integer, minimum: 0 }
                len:  { type: integer, minimum: 0, maximum: 256 }
      responses:
        "200":
          description: Bytes read.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  addr:   { type: integer }
                  len:    { type: integer }
                  data:   { type: string, description: "Hex-encoded bytes." }

  /cmd/nvram_write:
    post:
      operationId: nvramWrite
      tags: [X10i-compat]
      summary: Write bytes to the GS2 NvRam region (FRAM-backed)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd, addr, data]
              properties:
                cmd:  { type: string, enum: [nvram_write] }
                addr: { type: integer, minimum: 0 }
                data: { type: string, description: "Hex-encoded bytes (even length)." }
      responses:
        "200":
          description: Bytes written.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  addr:   { type: integer }
                  len:    { type: integer }

  /cmd/hw_counters_get:
    post:
      operationId: hwCountersGet
      tags: [X10i-compat]
      summary: Read the NvHwCounters block from FRAM
      description: |
        `valid=false` means no record has ever been written, or its CRC32
        does not match. The fields are zero-filled in both cases.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd]
              properties: { cmd: { type: string, enum: [hw_counters_get] } }
      responses:
        "200":
          description: Counters block.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/HwCountersResponse" }

  /cmd/hw_counters_set:
    post:
      operationId: hwCountersSet
      tags: [X10i-compat]
      summary: Persist the NvHwCounters block to FRAM
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd]
              properties:
                cmd:     { type: string, enum: [hw_counters_set] }
                in:      { type: integer, minimum: 0, default: 0 }
                out:     { type: integer, minimum: 0, default: 0 }
                games:   { type: integer, minimum: 0, default: 0 }
                win:     { type: integer, minimum: 0, default: 0 }
                bet:     { type: integer, minimum: 0, default: 0 }
                new_win: { type: integer, minimum: 0, default: 0 }
      responses:
        "200":
          description: Written.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AckResponse" }

  /cmd/cctalk_polled_open:
    post:
      operationId: cctalkPolledOpen
      tags: [X10i-compat]
      summary: Configure background polled-mode ccTalk for a device
      description: |
        Up to 4 devices may be polled concurrently. The firmware sends
        `poll_msg` every `poll_interval_ms` ms, waits up to `max_response_ms`
        for a reply, and buffers it for pickup via `cctalk_polled_recv`. If
        `host_timeout_ms` > 0 and the host has not picked up a reply within
        that window, the firmware switches to sending `inhibit_msg` instead.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd, device, poll_msg]
              properties:
                cmd:              { type: string, enum: [cctalk_polled_open] }
                device:           { type: integer, minimum: 0, maximum: 255 }
                poll_msg:         { type: string, description: "Hex-encoded poll message." }
                inhibit_msg:      { type: string, description: "Hex-encoded inhibit message (optional)." }
                poll_interval_ms: { type: integer, minimum: 1, default: 100 }
                max_response_ms:  { type: integer, minimum: 1, default: 100 }
                host_timeout_ms:  { type: integer, minimum: 0, default: 0 }
      responses:
        "200":
          description: Opened.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  device: { type: integer }

  /cmd/cctalk_polled_recv:
    post:
      operationId: cctalkPolledRecv
      tags: [X10i-compat]
      summary: Pick up the latest buffered reply for a polled device
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd, device]
              properties:
                cmd:    { type: string, enum: [cctalk_polled_recv] }
                device: { type: integer, minimum: 0, maximum: 255 }
      responses:
        "200":
          description: Buffered reply (rx_len=0 if nothing pending).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CCTalkPolledRecvResponse" }

  /cmd/cctalk_polled_drop:
    post:
      operationId: cctalkPolledDrop
      tags: [X10i-compat]
      summary: Discard the buffered reply (keep polling)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd, device]
              properties:
                cmd:    { type: string, enum: [cctalk_polled_drop] }
                device: { type: integer }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/AckResponse" } } } }

  /cmd/cctalk_polled_empty:
    post:
      operationId: cctalkPolledEmpty
      tags: [X10i-compat]
      summary: Stop polling and discard the buffer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd, device]
              properties:
                cmd:    { type: string, enum: [cctalk_polled_empty] }
                device: { type: integer }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/AckResponse" } } } }

  /cmd/cctalk_polled_timeout:
    post:
      operationId: cctalkPolledTimeout
      tags: [X10i-compat]
      summary: Update host-pickup timeout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cmd, device, timeout_ms]
              properties:
                cmd:        { type: string, enum: [cctalk_polled_timeout] }
                device:     { type: integer }
                timeout_ms: { type: integer, minimum: 0 }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/AckResponse" } } } }

# ---------------------------------------------------------------------------
# Components
# ---------------------------------------------------------------------------
components:

  schemas:

    # --- Requests -----------------------------------------------------------

    PingRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd:
          type: string
          enum: [ping]
          description: Command identifier.
      example:
        cmd: ping

    VersionRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd:
          type: string
          enum: [version]
          description: Command identifier.
      example:
        cmd: version

    LedRequest:
      type: object
      required: [cmd, action]
      properties:
        cmd:
          type: string
          enum: [led]
          description: Command identifier.
        action:
          type: string
          enum: [on, off, blink]
          description: LED action to perform.
        brightness:
          type: number
          format: float
          minimum: 0.0
          maximum: 1.0
          default: 1.0
          description: |
            LED brightness expressed as a fraction of maximum intensity.
            Required for `on` and `blink` actions.
            Ignored for `off`.
        on_ms:
          type: integer
          minimum: 1
          default: 500
          description: |
            Duration the LED stays **on** during each blink cycle (milliseconds).
            Only used when `action` is `blink`.
        off_ms:
          type: integer
          minimum: 1
          default: 500
          description: |
            Duration the LED stays **off** during each blink cycle (milliseconds).
            Only used when `action` is `blink`.
      discriminator:
        propertyName: action
      example:
        cmd: led
        action: blink
        on_ms: 500
        off_ms: 500
        brightness: 0.8

    TemperatureRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd:
          type: string
          enum: [temperature]
          description: Command identifier.
      example:
        cmd: temperature

    SwitchRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd:
          type: string
          enum: [sw]
          description: Command identifier.
      example:
        cmd: sw

    InputsRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd: { type: string, enum: [inputs] }
      example: { cmd: inputs }

    InputsResponse:
      type: object
      required: [status, ip, sec_sw, chips]
      properties:
        status: { type: string, enum: [ok] }
        chips:
          type: object
          description: Raw GPIO register read from each MCP23008 expander.
          properties:
            u4:
              type: object
              properties:
                ok:  { type: boolean }
                raw: { type: integer, minimum: 0, maximum: 255 }
            u5:
              type: object
              properties:
                ok:  { type: boolean }
                raw: { type: integer, minimum: 0, maximum: 255 }
            u6:
              type: object
              properties:
                ok:  { type: boolean }
                raw: { type: integer, minimum: 0, maximum: 255 }
        ip:
          type: array
          minItems: 24
          maxItems: 24
          items: { type: integer, enum: [0, 1] }
          description: |
            Unpacked electrical state of IP0..IP23 (0 or 1). Indices that
            are not wired on the schematic (e.g. IP10..IP15) read as 0.
        sec_sw:
          type: array
          minItems: 4
          maxItems: 4
          items: { type: integer, enum: [0, 1] }
          description: Electrical state of IP-SW1..IP-SW4 (0 = closed, 1 = open).
      example:
        status: ok
        chips: { u4: { ok: true, raw: 255 }, u5: { ok: true, raw: 63 }, u6: { ok: true, raw: 15 } }
        ip:    [1,1,1,0,0,1, 1,1,1,1, 0,0,0,0,0,0, 1,1,1,1,1,1,1,1]
        sec_sw: [0, 0, 0, 0]

    OutputsRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd: { type: string, enum: [outputs] }
        mask:
          type: integer
          minimum: 0
          maximum: 16777215
          description: |
            Optional. 24-bit mask to write. Bit n controls OP(n+8): bit 0 = OP8
            (lowest counter), bit 23 = OP31 (last HC lamp output). Omit to
            just read the current shadow register.
      example: { cmd: outputs, mask: 0x00FF00 }

    OutputsResponse:
      type: object
      required: [status, mask]
      properties:
        status: { type: string, enum: [ok] }
        mask:
          type: integer
          minimum: 0
          maximum: 16777215
          description: Current 24-bit output mask after the operation.
      example: { status: ok, mask: 0x00FF00 }

    OutputRequest:
      type: object
      required: [cmd, op, state]
      additionalProperties: false
      properties:
        cmd:   { type: string, enum: [output] }
        op:    { type: integer, minimum: 8, maximum: 31 }
        state:
          oneOf: [{ type: boolean }, { type: integer, enum: [0, 1] }]
      example: { cmd: output, op: 16, state: true }

    PulseRequest:
      type: object
      required: [cmd, op]
      additionalProperties: false
      properties:
        cmd: { type: string, enum: [pulse] }
        op:  { type: integer, minimum: 8, maximum: 31 }
        ms:
          type: integer
          minimum: 1
          maximum: 2000
          default: 50
      example: { cmd: pulse, op: 8, ms: 50 }

    DiagRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd: { type: string, enum: [diag] }
      example: { cmd: diag }

    DiagOpEntry:
      type: object
      required: [op, on, open_load, over_err]
      properties:
        op:        { type: integer, minimum: 8, maximum: 31 }
        on:        { type: boolean, description: "OP was driven ON at measurement time." }
        open_load:
          type: boolean
          description: |
            Open-load detected while channel was OFF: load (lamp / counter
            coil / wire) is disconnected. Always *false* if `on` is *true*.
        over_err:
          type: boolean
          description: |
            ERRn latched: TLE75008 protection has switched the OP off due to
            over-current or over-temperature. Use `clear_latch` to re-enable.

    DiagResponse:
      type: object
      required: [status, open_load, over_err, was_on, ops]
      properties:
        status:    { type: string, enum: [ok] }
        open_load:
          type: integer
          minimum: 0
          maximum: 16777215
          description: |
            24-bit mask. Bit n=1 → OP(n+8) was OFF and reports an
            open / disconnected load. Bits for currently-ON OPs are always
            zeroed (their OSM bit is meaningless while V_DS ≈ 0 V).
        over_err:
          type: integer
          minimum: 0
          maximum: 16777215
          description: |
            24-bit mask. Bit n=1 → OP(n+8) is latched OFF by over-current
            or over-temperature protection. Clear with `clear_latch`.
        was_on:
          type: integer
          minimum: 0
          maximum: 16777215
          description: Shadow OUT mask captured at the start of the diagnosis.
        ops:
          type: array
          minItems: 24
          maxItems: 24
          items: { $ref: "#/components/schemas/DiagOpEntry" }
      example:
        status:    ok
        open_load: 0x000040          # OP14 = počítadlo VTP not connected
        over_err:  0x000000
        was_on:    0x000000
        ops:
          - { op:  8, on: false, open_load: false, over_err: false }
          - { op: 14, on: false, open_load: true,  over_err: false }

    ClearLatchRequest:
      type: object
      required: [cmd, op]
      additionalProperties: false
      properties:
        cmd: { type: string, enum: [clear_latch] }
        op:  { type: integer, minimum: 8, maximum: 31 }
      example: { cmd: clear_latch, op: 16 }

    ClearLatchResponse:
      type: object
      required: [status, op]
      properties:
        status: { type: string, enum: [ok] }
        op:     { type: integer, minimum: 8, maximum: 31 }
      example: { status: ok, op: 16 }

    CcTalkRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd: { type: string, enum: [cctalk] }
        tx:
          description: |
            Bytes to transmit. Either a hex string (preferred) or an array
            of integers 0..255. Omit / empty to listen only.
          oneOf:
            - type: string
              pattern: "^([0-9a-fA-F]{2})*$"
            - type: array
              items: { type: integer, minimum: 0, maximum: 255 }
        timeout_ms:
          type: integer
          minimum: 0
          maximum: 5000
          default: 100
      example: { cmd: cctalk, tx: "020000fe00", timeout_ms: 100 }

    CcTalkResponse:
      type: object
      required: [status, tx_len, rx_len, rx]
      properties:
        status: { type: string, enum: [ok] }
        tx_len: { type: integer, minimum: 0 }
        rx_len: { type: integer, minimum: 0 }
        rx:
          type: string
          pattern: "^([0-9a-fA-F]{2})*$"
          description: Hex-encoded received bytes.
      example:
        status: ok
        tx_len: 5
        rx_len: 4
        rx: "0200fe00"

    BootselRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd:
          type: string
          enum: [bootsel]
          description: Command identifier.
      example:
        cmd: bootsel

    RgbRequest:
      type: object
      required: [cmd, r, g, b]
      additionalProperties: false
      properties:
        cmd:
          type: string
          enum: [rgb]
          description: Command identifier.
        r:
          type: integer
          minimum: 0
          maximum: 255
          description: Red channel intensity (0–255).
        g:
          type: integer
          minimum: 0
          maximum: 255
          description: Green channel intensity (0–255).
        b:
          type: integer
          minimum: 0
          maximum: 255
          description: Blue channel intensity (0–255).
      example:
        cmd: rgb
        r: 255
        g: 64
        b: 0

    WledSegmentInput:
      type: object
      description: |
        Single segment, WLED-shaped. Only segment 0 is honoured; everything
        else is ignored. All keys optional — omit to keep the current value.
      additionalProperties: false
      properties:
        on:
          type: boolean
          description: Segment power.
        bri:
          type: integer
          minimum: 0
          maximum: 255
          description: Segment brightness (multiplied by master `bri`).
        len:
          type: integer
          minimum: 1
          maximum: 300
          description: Active pixel count on the strip (clamped to MAX_LEDS).
        fx:
          type: integer
          minimum: 0
          maximum: 7
          description: |
            Effect id. 0=Solid 1=Blink 2=Breathe 3=Rainbow 4=Chase 5=Wipe
            6=Fade 7=Static (renderer freezes; pixel buffer used as-is).
        sx:
          type: integer
          minimum: 0
          maximum: 255
          description: Effect speed.
        ix:
          type: integer
          minimum: 0
          maximum: 255
          description: Effect intensity.
        pal:
          type: integer
          description: Palette id (accepted but currently no visual effect).
        col:
          type: array
          maxItems: 3
          description: Up to three `[r,g,b]` colour slots.
          items:
            type: array
            minItems: 3
            maxItems: 3
            items: { type: integer, minimum: 0, maximum: 255 }
        i:
          type: array
          description: |
            Per-pixel override, starting at index 0. Each entry is `[r,g,b]`.
            Posting this auto-switches the segment to fx=7 (Static).
          items:
            type: array
            minItems: 3
            maxItems: 3
            items: { type: integer, minimum: 0, maximum: 255 }

    WledStateInput:
      type: object
      additionalProperties: false
      properties:
        on:
          type: boolean
          description: Master power.
        bri:
          type: integer
          minimum: 0
          maximum: 255
          description: Master brightness.
        seg:
          type: array
          maxItems: 1
          items: { $ref: "#/components/schemas/WledSegmentInput" }

    WledGetRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd:
          type: string
          enum: [wled_get]
      example:
        cmd: wled_get

    WledSetRequest:
      type: object
      required: [cmd, state]
      additionalProperties: false
      properties:
        cmd:
          type: string
          enum: [wled_set]
        state:
          $ref: "#/components/schemas/WledStateInput"
      example:
        cmd: wled_set
        state:
          on: true
          bri: 200
          seg:
            - len: 100
              fx: 3
              sx: 60

    WledGetResponse:
      type: object
      required: [status, state]
      properties:
        status:
          type: string
          enum: [ok]
        state:
          $ref: "#/components/schemas/WledStateInput"
        effects:
          type: array
          description: Effect names indexed by `seg.fx`.
          items: { type: string }

    FramInfoRequest:
      type: object
      required: [cmd]
      additionalProperties: false
      properties:
        cmd: { type: string, enum: [fram_info] }
      example:
        cmd: fram_info

    FramInfoResponse:
      type: object
      required: [status, size, max_xfer, id]
      properties:
        status:   { type: string, enum: [ok] }
        size:
          type: integer
          minimum: 0
          description: Total FRAM size in bytes (524 288 for MB85RS4MT).
        max_xfer:
          type: integer
          minimum: 1
          description: Maximum bytes carried by a single read/write request.
        id:
          type: string
          pattern: "^[0-9a-f]+$"
          description: Hex-encoded 4-byte JEDEC ID.
      example:
        status: ok
        size: 524288
        max_xfer: 256
        id: "047f4803"

    FramReadRequest:
      type: object
      required: [cmd, addr, len]
      additionalProperties: false
      properties:
        cmd:  { type: string, enum: [fram_read] }
        addr:
          type: integer
          minimum: 0
          maximum: 524287
          description: Byte address to start reading from.
        len:
          type: integer
          minimum: 0
          maximum: 256
          description: |
            Number of bytes to read. Must satisfy `addr + len ≤ size` and
            `len ≤ max_xfer`; otherwise the firmware returns an error.
      example:
        cmd: fram_read
        addr: 0
        len: 16

    FramReadResponse:
      type: object
      required: [status, addr, len, data]
      properties:
        status: { type: string, enum: [ok] }
        addr:   { type: integer, minimum: 0 }
        len:    { type: integer, minimum: 0 }
        data:
          type: string
          pattern: "^[0-9a-f]*$"
          description: Hex-encoded bytes; length is `2 * len` characters.
      example:
        status: ok
        addr: 0
        len: 4
        data: "deadbeef"

    FramWriteRequest:
      type: object
      required: [cmd, addr, data]
      additionalProperties: false
      properties:
        cmd:  { type: string, enum: [fram_write] }
        addr:
          type: integer
          minimum: 0
          maximum: 524287
        data:
          type: string
          pattern: "^([0-9a-fA-F]{2})*$"
          description: |
            Hex-encoded bytes to write. Must have even length; decoded byte
            count must satisfy `addr + bytes ≤ size` and be `≤ max_xfer`.
      example:
        cmd: fram_write
        addr: 0
        data: "deadbeef"

    FramWriteResponse:
      type: object
      required: [status, addr, len]
      properties:
        status: { type: string, enum: [ok] }
        addr:   { type: integer, minimum: 0 }
        len:    { type: integer, minimum: 0, description: Bytes written. }
      example:
        status: ok
        addr: 0
        len: 4

    # --- Responses ----------------------------------------------------------

    OkResponse:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [ok]
          description: Indicates the command completed successfully.
      example:
        status: ok

    ErrorResponse:
      type: object
      required: [status, message]
      properties:
        status:
          type: string
          enum: [error]
          description: Indicates the command failed.
        message:
          type: string
          description: Human-readable description of the failure reason.
      example:
        status: error
        message: "Unknown command: foo"

    VersionResponse:
      type: object
      required: [status, protocol, build]
      properties:
        status:
          type: string
          enum: [ok]
        protocol:
          type: integer
          minimum: 1
          description: Protocol version. Bumped each time a command is added
            or changes shape.
        fw_minor:
          type: integer
          minimum: 0
          description: Firmware minor revision. Bumped on every firmware change
            (independent of protocol/build); logged by the GS2 client at startup
            so the flashed build can be identified per cabinet.
        build:
          type: string
          description: Firmware build identifier (UTC timestamp, regenerated
            on every compile).
      example:
        status: ok
        protocol: 9
        fw_minor: 2
        build: "2026-01-18 14:32:11 UTC"

    TemperatureResponse:
      type: object
      required: [status, temperature]
      properties:
        status:
          type: string
          enum: [ok]
        temperature:
          type: number
          format: float
          description: CPU temperature in degrees Celsius, rounded to 2 decimal places.
      example:
        status: ok
        temperature: 28.41

    SwitchResponse:
      type: object
      required: [status, sw, mask]
      properties:
        status:
          type: string
          enum: [ok]
        sw:
          type: array
          items:
            type: boolean
          minItems: 3
          maxItems: 3
          description: |
            Per-switch boolean state [SW0, SW1, SW2]. `true` means the switch
            is in the ON (closed) position.
        mask:
          type: integer
          minimum: 0
          maximum: 7
          description: Bit-packed switch state (bit 0 = SW0, bit 1 = SW1, bit 2 = SW2).
      example:
        status: ok
        sw: [true, false, true]
        mask: 5

    # --- X10i-replacement responses (firmware ≥ v9) -------------------------

    PollResponse:
      type: object
      required: [status, ip_mask, sec_sw, sec_sw_on, sec_sw_off, out_mask, pulse_busy, chips_ok]
      properties:
        status:     { type: string, enum: [ok] }
        ip_mask:    { type: integer, description: "24-bit packed IP0..IP23 (bit n = IPn)." }
        sec_sw:     { type: integer, description: "4-bit IP-SW1..IP-SW4 mask." }
        sec_sw_on:  { type: integer, description: "Sec-switches transitioned 0→1 since previous poll." }
        sec_sw_off: { type: integer, description: "Sec-switches transitioned 1→0 since previous poll." }
        out_mask:   { type: integer, description: "24-bit OP8..OP31 shadow mask." }
        pulse_busy: { type: integer, description: "Bitmask of OPs currently being pulsed asynchronously." }
        chips_ok:   { type: boolean, description: "All three MCP23008 expanders answered. When false, ip_mask/sec_sw are unreliable (bits of a missing chip read 0) — the host should ignore the sample." }
        chips_bad:
          type: array
          description: "Present only when chips_ok=false. Names of the expander(s) that did not answer on I2C (u4=IP16..23, u5=IP0..5, u6=IP6..9 + IP-SW1..4)."
          items: { type: string, enum: [u4, u5, u6] }
        chips:
          type: object
          description: "Present only when chips_ok=false. Per-chip diagnostics (answered flag + last raw GPIO byte)."
          properties:
            u4: { type: object, properties: { ok: { type: boolean }, raw: { type: integer } } }
            u5: { type: object, properties: { ok: { type: boolean }, raw: { type: integer } } }
            u6: { type: object, properties: { ok: { type: boolean }, raw: { type: integer } } }
      example:
        status:     ok
        ip_mask:    16777215
        sec_sw:     15
        sec_sw_on:  0
        sec_sw_off: 0
        out_mask:   0
        pulse_busy: 0
        chips_ok:   true

    HwCountersResponse:
      type: object
      required: [status, valid, in, out, games, win, bet, new_win]
      properties:
        status:  { type: string, enum: [ok] }
        valid:   { type: boolean, description: "False = no record stored yet, or CRC32 mismatch (fields zero-filled)." }
        in:      { type: integer, minimum: 0 }
        out:     { type: integer, minimum: 0 }
        games:   { type: integer, minimum: 0 }
        win:     { type: integer, minimum: 0 }
        bet:     { type: integer, minimum: 0 }
        new_win: { type: integer, minimum: 0 }

    CCTalkPolledRecvResponse:
      type: object
      required: [status, device, rx_len, inhibited, rx]
      properties:
        status:    { type: string, enum: [ok] }
        device:    { type: integer }
        rx_len:    { type: integer, description: "0 if no reply has been buffered since the last call." }
        inhibited: { type: boolean, description: "True if the buffered reply was triggered by the inhibit message." }
        rx:        { type: string,  description: "Hex-encoded reply bytes." }

  # --- Reusable response ---------------------------------------------------

  responses:
    ErrorResponse:
      description: The device returned an error (invalid command, missing field, etc.)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            unknown_cmd:
              summary: Unknown command
              value:
                status: error
                message: "Unknown command: foo"
            missing_field:
              summary: Missing required field
              value:
                status: error
                message: "Missing 'action' field"
            bad_brightness:
              summary: Brightness out of range
              value:
                status: error
                message: "brightness must be 0.0-1.0"
            json_error:
              summary: Malformed JSON
              value:
                status: error
                message: "JSON parse error: ..."
