# Magic Display device spec
# Copyright 2026 Pigs Can Fly Labs LLC
# SPDX-License-Identifier: Apache-2.0

device:
  name: "Magic Display"
  manufacturer: "tirohk / AiTURE"
  manufacturer_status: "abandoned"
  protocol: "ble"
  notes: >
    Bluetooth-controlled LED displays for shoes, bags, hats, crafts.
    Uses Quintic (NXP QN-series) BLE chipset with QPP (Quintic Private Profile) on service 0xFEE9.
    All command traffic is AES-128-ECB encrypted via the native libAES.so
    (aes.keyExpansionDefault() is a JNI native method; the 16-byte key lives in
    the .so binary, NOT in Java — extract libAES.so to recover it).
    Commands are fixed 16-byte ASCII-named packets: [length, ASCII_CMD..., params..., zero_pad].
    CORRECTION: there is NO connection-time 3-way handshake (the earlier claim was
    wrong). After connecting, the app simply queries the LED matrix type (STYPE) and
    parses the response. Bulk image transfer uses DATS (data_transfer_start) framing
    followed by 16-byte data frames and DATCP (data_transfer_complete) — that is
    transfer framing, not an authentication handshake.
    Supports multiple display sizes: 5x36, 12x48, 14x56, 16x64 (reported via STYPE response).
    Monochrome 1-bit-per-pixel bitmap encoding (threshold: any RGB channel >= 128 = ON).
    Device identification by manufacturer-specific adv data: {0x54, 0x52, 0x00, 0x27} ("TR" + 0x0027).
    No pairing required. Shares the com.cdbwsoft BLE codebase with Shining Glasses.
    A secondary Texas Instruments profile (service 0xFFF0 / char 0xFFF1) is also
    supported for TI-based hardware variants.
    Also has OTA service at 0xFEE8 (Quintic OTA protocol).
    CONFIDENCE: command/UUID mapping HIGH (jadx of com.tirohk.magicdisplay);
    AES key UNRECOVERABLE from the Java APK (native lib not captured).

    Enrich-branch evidence: version 1.5.6 (versionCode 156) was inspected with
    jadx. Java class csh.tiro.cc.aes exposes cipher / invCipher / keyExpansion /
    keyExpansionDefault backed by lib/arm64-v8a/libAES.so; Agreement.getEncryptData()
    encrypts every command before write and Agreement.getDecodeData() decrypts
    notifications. Bulk pixel/image upload uses DATS on WRITE1, waits for DATSOK,
    sends encrypted 16-byte chunks on WRITE2 with about 60 ms delay, then sends
    DATCP and waits for DATCPOK. Agreement.parseType maps STYPE5X36, STYPE12X48,
    STYPE14X56, STYPE16X64 and N-suffixed variants. Additional observed app
    features include LIGHTON/LIGHTOFF torch control, MODE 7 music/rhythm mode
    backed by libint16fft.so, SMVEW DIY sync draw, LEDFIRST/LEDSECOND chained
    display selection, CALL notification passthrough, SCHD/STSC scheduling, and
    ANIM animation selection.
  identification:
    service_uuids:
      - "0000fee9-0000-1000-8000-00805f9b34fb"

  discovery:
    methods:
      - type: "ble_scan"
        ble:
          service_uuids:
            - "0000fee9-0000-1000-8000-00805f9b34fb"
          manufacturer_data:
            company_id: 21076
            match: "prefix"
            pattern: "54520027"
            description: "Manufacturer-specific advertising data starts with 0x54 0x52 0x00 0x27."
    identity:
      stable_keys: ["address"]
      display: "service_uuid"
    static_ip_required: false

  setup:
    required: false
    confidence: "medium"
    notes: >
      No provisioning. Identify by service UUID and the manufacturer-data prefix
      rather than by name, since these units advertise without a stable local
      name.
    methods:
      - type: "ble_direct"
        verified: false
        description: >
          No provisioning step: the device advertises as soon as it is powered
          on and accepts a connection from any central. There is no account, no
          network credential exchange and no pairing PIN, so a replacement
          client only has to scan, connect and write.
        ble:
          pairing_required: false
        steps:
          - action: "Power the device and scan for its advertisement."
            actor: "client"
            request:
              protocol: "ble_gatt"
          - action: "Connect and run the documented initialization sequence, if the spec defines one."
            actor: "client"
    factory_reset:
      confidence: "low"
      effect: >
        No credential state to clear. Power-cycling drops the current
        connection, which is the actual remedy for the common failure mode:
        the device already being connected to another central.
      procedures:
        - name: "Power cycle"
          steps:
            - action: "Remove power (or battery) for a few seconds and reapply."
              actor: "user"
              expect: "Device advertises again and accepts a new connection."
    rejoin:
      in_place_supported: true
      requires_factory_reset: false
      notes: >
        Nothing binds this device to a network or an owner, so switching
        controllers is just connecting from the new one. If the old client is
        an Android or iOS phone, remove the device from the OS Bluetooth list
        as well — a cached bond can keep the phone reconnecting automatically
        and holding the single available link.
    credentials:
      wifi_passphrase_protection: "not_applicable"
      stored_on_device: []
      issued_to_client: []
      notes: >
        Anything within radio range can connect and issue commands. Physical
        proximity is the only access control.
# Custom consumer-side handler for the cdbwsoft AES-128-ECB command framing
# (shared by Magic Display and Shining Glasses). Declarative.
protocol_handler: "cdbwsoft_ecb"

# Monochrome bitmap image upload (1 bit/pixel; any RGB channel >= 128 = ON).
features:
  - type: "image_upload"
    format: "1bit-bitmap"
    max_width: 64
    max_height: 16

services:
  - uuid: "0000fee9-0000-1000-8000-00805f9b34fb"
    name: "QPP Service"
    characteristics:
      - uuid: "d44bc439-abfd-45a2-b575-925416129600"
        name: "Command (WRITE1)"
        properties: ["write"]
        encryption:
          algorithm: "aes-128-ecb"
          key_derivation: "static"
        notes: >
          AES-128-ECB, fixed static key. static_key OMITTED: the key is in the
          native libAES.so (loaded via aes.keyExpansionDefault, a JNI method) and
          is not present in the decompiled Java APK. Recover it by extracting the
          16-byte key from libAES.so. Confidence: HIGH structure, key UNRECOVERABLE.
          Evidence: com/tirohk/magicdisplay/MainApplication.java:26 (keyExpansionDefault);
          csh/tiro/cc/aes.java:11 (native); com/cdbwsoft/library/ble/BleManager.java:50-57.
        commands:
          query_device_type:
            description: "Query display dimensions. Response on notify: 'STYPE5X36', 'STYPE12X48', 'STYPE16X64', etc."
            value: [0x05, 0x53, 0x54, 0x59, 0x50, 0x45]
          led_on:
            description: "Turn LED display on"
            value: [0x05, 0x4C, 0x45, 0x44, 0x4F, 0x4E]
          led_off:
            description: "Turn LED display off"
            value: [0x06, 0x4C, 0x45, 0x44, 0x4F, 0x46, 0x46]
          set_brightness:
            description: "Set brightness level"
            template: [0x06, 0x4C, 0x49, 0x47, 0x48, 0x54, "{brightness}"]
            parameters:
              brightness:
                type: "uint8"
                min: 0
                max: 255
          set_speed:
            description: "Set animation speed"
            template: [0x06, 0x53, 0x50, 0x45, 0x45, 0x44, "{speed}"]
            parameters:
              speed:
                type: "uint8"
                min: 0
                max: 255
          set_mode_static:
            description: "Static display mode"
            value: [0x05, 0x4D, 0x4F, 0x44, 0x45, 0x01]
          set_mode_scroll_left:
            description: "Scroll left mode"
            template: [0x06, 0x4D, 0x4F, 0x44, 0x45, 0x03, "{speed}"]
            parameters:
              speed:
                type: "uint8"
                min: 0
                max: 255
          set_mode_scroll_right:
            description: "Scroll right mode"
            template: [0x06, 0x4D, 0x4F, 0x44, 0x45, 0x04, "{speed}"]
            parameters:
              speed:
                type: "uint8"
                min: 0
                max: 255
          set_time:
            description: "Set device clock"
            template: [0x07, 0x54, 0x49, 0x4D, 0x45, "{hour}", "{minute}", "{second}"]
            parameters:
              hour:
                type: "uint8"
                min: 0
                max: 23
              minute:
                type: "uint8"
                min: 0
                max: 59
              second:
                type: "uint8"
                min: 0
                max: 59
          invert_display:
            description: "Invert/flip display"
            value: [0x05, 0x45, 0x56, 0x45, 0x52, 0x54]
          data_transfer_start:
            description: "Start bulk data transfer. hi/lo = big-endian 16-bit data length."
            template: [0x08, 0x44, 0x41, 0x54, 0x53, "{len_hi}", "{len_lo}", 0x00, "{link_flag}"]
            parameters:
              len_hi:
                type: "uint8"
                min: 0
                max: 255
              len_lo:
                type: "uint8"
                min: 0
                max: 255
              link_flag:
                type: "uint8"
                min: 0
                max: 1
          data_transfer_complete:
            description: "Signal bulk data transfer complete"
            value: [0x05, 0x44, 0x41, 0x54, 0x43, 0x50]

      - uuid: "d44bc439-abfd-45a2-b575-925416129601"
        name: "Notification"
        properties: ["notify"]

      - uuid: "d44bc439-abfd-45a2-b575-92541612960a"
        name: "Bulk Data (WRITE2)"
        properties: ["write"]
        encryption:
          algorithm: "aes-128-ecb"
          key_derivation: "static"
        notes: "Bulk image frames; same AES-128-ECB native key as the command char (key OMITTED, in libAES.so)."

      - uuid: "d44bc439-abfd-45a2-b575-92541612960b"
        name: "Auxiliary (WRITE3)"
        properties: ["write"]
        encryption:
          algorithm: "aes-128-ecb"
          key_derivation: "static"
        notes: "Auxiliary write path; same AES-128-ECB native key (key OMITTED, in libAES.so)."

  # Texas Instruments profile used by TI-based hardware variants (BleManager.java:52,58).
  - uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
    name: "TI Service"
    characteristics:
      - uuid: "0000fff1-0000-1000-8000-00805f9b34fb"
        name: "TI Command"
        properties: ["write", "notify"]
        encryption:
          algorithm: "aes-128-ecb"
          key_derivation: "static"
        notes: >
          TI-variant command characteristic; same AES-128-ECB native key
          (key OMITTED, in libAES.so). Evidence: BleManager.java:52,58.

entities:
  - platform: "light"
    name: "LED Display"
    features: ["brightness"]
    commands:
      set_brightness: "set_brightness"
