# Bluetooth LED Name Badge device spec
# Copyright 2026 Pigs Can Fly Labs LLC
# SPDX-License-Identifier: Apache-2.0

device:
  name: "Bluetooth LED Name Badge"
  manufacturer: "Generic (multiple OEMs)"
  manufacturer_status: "abandoned"
  protocol: "ble"
  notes: >
    Monochrome LED dot-matrix name badge. Sold under many brands on AliExpress.
    Matrix height 11, 12, or 16 rows; width is variable per text/image content.
    Protocol is write-only (no reads). Data is written in 16-byte chunks: a 64-byte
    header (4 chunks) followed by 1-bit column-major bitmap data.
    Writes are ACKNOWLEDGED (writeCharacteristic, NOT write_without_response), with a
    3-second per-write ack timeout, 300 ms delay before the first data chunk and 25 ms
    between subsequent chunks. Up to 8 message slots with per-slot flash and marquee.
    NO CRC/checksum anywhere in the send path, and NO length prefix at the BLE write
    level (raw 16-byte chunks). Device stores messages in flash and persists across
    power cycles. No pairing required. Advertised name is filtered by contains("LS").
    CONFIDENCE: HIGH (full jadx decompile of com.yannis.ledcard).
    Evidence: BleDevice.java:18-20 (name "LS", service 0000fee0, char 0000fee1);
    LedDataUtil.java get64()/bitmapToByteArray() (header + 1-bit conversion);
    MainMode.java (16-byte chunking); BLEService.java writValue (acknowledged write).
  identification:
    local_name_prefix: "LS"
    service_uuids:
      - "0000fee0-0000-1000-8000-00805f9b34fb"

  discovery:
    methods:
      - type: "ble_scan"
        ble:
          local_name:
            match: "prefix"
            value: "LS"
          service_uuids:
            - "0000fee0-0000-1000-8000-00805f9b34fb"
    identity:
      stable_keys: ["address"]
      display: "local_name"
    static_ip_required: false

  setup:
    required: false
    confidence: "medium"
    notes: >
      No provisioning. These badges are widely reported as finicky to connect:
      they accept one central at a time and often need a power cycle between
      clients.
    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:
          advertised_name: "LS"
          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: assembles the 64-byte header + 1-bit column-major
# bitmap and drives the acknowledged 16-byte chunked writes. Declarative.
protocol_handler: "ledbadge_bitmap"

# 1-bit monochrome bitmap upload (white pixel -> bit 0, any other color -> bit 1;
# each row padded to a byte boundary). Height = matrix size (11/12/16), width variable.
features:
  - type: "image_upload"
    format: "1bit-bitmap"
    max_height: 16

services:
  - uuid: "0000fee0-0000-1000-8000-00805f9b34fb"
    name: "Badge Service"
    characteristics:
      - uuid: "0000fee1-0000-1000-8000-00805f9b34fb"
        name: "Badge Data"
        properties: ["write"]
        framing:
          length_prefix: false
          max_chunk_size: 16
        notes: >
          checksum OMITTED: the device uses NO CRC/checksum (confirmed absent).
          length_prefix false: raw 16-byte chunks with no BLE-level length prefix.
        commands:
          write_badge_data:
            description: >
              Write badge payload in 16-byte chunks. First 4 chunks are the 64-byte
              header; subsequent chunks are 1-bit column-major bitmap pixel data.
              64-byte header layout (LedDataUtil.get64):
              Bytes 0-3: Magic "wang" = 0x77 0x61 0x6E 0x67.
              Byte 4: reserved (0x00).
              Byte 5: brightness (0x00=100%, 0x10=75%, 0x20=50%, 0x40=25%) -- reads as
              0x00 (100%) in the captured get64 trace, per the FOSSASIA badgemagic layout.
              Byte 6: flash bits (bit N = slot N blinks).
              Byte 7: marquee bits (bit N = slot N marquee/animated border).
              Bytes 8-15: mode+speed per slot, one byte each
              (upper nibble = speed-1, lower nibble = mode-1).
              Bytes 16-31: message length per slot (uint16 little-endian / LSB-first, 8 slots).
              Bytes 32-37: reserved (zeros).
              Bytes 38-43: date (year, month, day, hour, minute, second).
              Bytes 44-62: reserved (zeros). Byte 63: 0x00.
            value: [0x77, 0x61, 0x6E, 0x67]

entities:
  - platform: "light"
    name: "Badge Display"
    features: ["brightness"]
    commands:
      set_text: "write_badge_data"
