npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nsd-ble

v0.4.2

Published

A TypeScript library for communicating with NotSoDoom Bluetooth standees via the Web Bluetooth API. Connect to one standee and communicate with the entire mesh network.

Downloads

90

Readme

nsd-ble

A TypeScript library for communicating with NotSoDoom Bluetooth standees via the Web Bluetooth API. Connect to one standee and communicate with the entire mesh network.

Installation

npm install nsd-ble

Quick Start

import { useMesh, useStandees } from "nsd-ble";

const mesh = useMesh();
const { standees, onChange } = useStandees();

// connect() must be called from a user gesture (e.g. button click)
document.getElementById("connect").addEventListener("click", async () => {
    await mesh.connect();
});

// React to standees joining the mesh
onChange.attach((list) => {
    console.log(
        "Standees:",
        list.map((s) => s.name()),
    );
});

Requirements

  • A browser that supports the Web Bluetooth API (Chrome, Edge, Opera)
  • connect() must be called from a user-initiated event (click, tap, etc.) due to Web Bluetooth security requirements

Imports

The package exposes four entry points:

// Core composables
import {
    useMesh,
    useStandees,
    useGraphics,
    useLoader,
    useDisplay,
    useStandeeElements,
    useActions,
    useButtons,
    useConverter,
    useBinary,
    useImage,
    useUtils,
    useDebug,
    useMockBluetooth,
} from "nsd-ble";

// Display elements
import { Rectangle, Image, Text, DisplayElement } from "nsd-ble/elements";

// Object types
import { Standee, ButtonResult, Buffer } from "nsd-ble/objects";

// Enums
import {
    Font,
    Button,
    ActionType,
    ElementData,
    ActionCondition,
    ActionDataType,
    HandshakeType,
    DataType,
    FloodTypes,
    Data,
} from "nsd-ble/enums";

Core Concepts

Mesh Network

Standees form a Bluetooth mesh network. You only need to connect to a single standee — messages are automatically flooded to all reachable nodes.

Composable Pattern

All functionality is accessed through useX() composable functions. Some composables are global (useMesh, useDisplay) while others are scoped to a specific standee (useGraphics(standee), useLoader(standee)).

Buffer System

Most operations return a Buffer object. Call .send() on it to transmit the data over BLE:

const buffer = useDisplay().draw(standee);
await buffer.send();

Buffers can also be chained with .then():

useDisplay()
    .clear(standee)
    .then(() => {
        console.log("Screen cleared");
    })
    .send();

API Reference

useMesh()

Main entry point. Manages the BLE connection and mesh-level events. Auto-reconnect with exponential backoff is enabled automatically after the first successful connection.

const {
    connect,           // () => Promise<void>  — opens the BLE device picker
    reconnect,         // () => Promise<void>  — reconnects to the last device
    onConnect,         // SyncEvent<void>      — fires when connected
    onDisconnect,      // SyncEvent<void>      — fires when disconnected
    onReconnecting,    // SyncEvent<ReconnectStatus> — auto-reconnect attempt in progress
    onReconnectFailed, // SyncEvent<void>      — all auto-reconnect retries exhausted
    onNodes,           // SyncEvent<Map<number, number[]>> — all discovered nodes
    onNode,            // SyncEvent<Map<number, number[]>> — new node joined
    onNodeLost,        // SyncEvent<number>    — node left the mesh
    onPlusButton,      // SyncEvent<ButtonResult>
    onMinButton,       // SyncEvent<ButtonResult>
    onEitherButton,    // SyncEvent<ButtonResult>
    onMessageDone,     // SyncEvent<number>    — message transmission complete
} = useMesh();

Auto-reconnect uses exponential backoff starting at 2 seconds, doubling each attempt up to 30 seconds, with a maximum of 5 retries. Subscribe to onReconnecting to track reconnect attempts:

mesh.onReconnecting.attach((status) => {
    // status.attempt    — current attempt number
    // status.maxRetries — maximum retries (5)
    // status.delay      — delay before this attempt in ms
    console.log(`Reconnecting: attempt ${status.attempt}/${status.maxRetries}`);
});

mesh.onReconnectFailed.attach(() => {
    console.log("All reconnect attempts failed");
});

useStandees()

Tracks discovered standees as a reactive list.

const {
    standees, // Standee[] — current list of standees
    onChange,  // SyncEvent<Standee[]> — fires when the list changes
} = useStandees();

useGraphics(standee)

Upload images to a standee's memory. Images are stored by numeric ID (0-255) and can later be referenced by display elements.

const { upload, clear } = useGraphics(standee);

// Convert an image to bytes and upload it as ID 0
const bytes = await useConverter().imageToBytes("/images/icon.png");
await upload(0, bytes).send();

// Clear all uploaded graphics
await clear().send();

useLoader(standee)

Sends multiple buffers in sequence with progress tracking. Useful for uploading a batch of images.

const loader = useLoader(standee).load(buffers);

loader.onProgress.attach((progress) => {
    // progress.current    — buffers completed so far
    // progress.total      — total buffers
    // progress.percentage — 0 to 1
    console.log(`${Math.round(progress.percentage * 100)}%`);
});

loader.onError.attach((error) => {
    // error.buffer — index of the buffer that failed
    // error.error  — the Error object
    console.error(`Buffer ${error.buffer} failed:`, error.error);
});

await loader.send();

useStandeeElements(standee)

Add and remove display elements on a standee. Elements are auto-assigned an ID.

const { add, remove } = useStandeeElements(standee);

const rect = add(new Rectangle(0, 0, 128, 64));
const text = add(new Text(10, 30, "Hello", Font.u8g2_font_5x7_mf));
const img = add(new Image(80, 10, 32, 32, 0)); // references uploaded image ID 0

remove(rect.id);

useDisplay()

Draw elements to the standee's screen or clear it.

const { draw, clear } = useDisplay();

// Draw all dirty elements
await draw(standee).send();

// Clear the screen
await clear(standee).send();

Only elements marked as dirty are redrawn. Modifying any element property (e.g. rect.x = 50) automatically marks it dirty.

useActions() and useButtons(standee)

Define interactive behavior triggered by the standee's physical buttons.

const actions = useActions();
const { setButton } = useButtons(standee);

// Create a target: which element property to affect
const target = actions.target(textElement.id, ElementData.X);

// Assign actions to the plus button
await setButton(Button.PLUS, [actions.increment(target)]).send();

Available actions:

| Action | Description | | ------------------------------------------------------ | ---------------------------------- | | increment(target) | Increment a property value | | decrement(target) | Decrement a property value | | show(elementId) | Show an element | | hide(elementId) | Hide an element | | set(target, value) | Set a property to a specific value | | broadcast(target?) | Broadcast a value across the mesh | | stop() | Break the action chain | | timer(deciseconds, resetable, onComplete) | Run actions after a delay (1/10s) | | condition(left, op, right, actions) | Conditional action execution | | map(value, fromLow, fromHigh, toLow, toHigh, target) | Map a value between ranges |

The condition action supports comparing targets, numbers, and strings. The op parameter uses ActionCondition values:

// Show an element when HP equals 0
const hp = actions.target(label.id, ElementData.TEXT);
actions.condition(hp, ActionCondition.EQUAL, 0, [actions.show(gameOverElement.id)]);

Receiving Broadcast Data

The broadcast() action sends data from the standee back to your app through the mesh. When a physical button press triggers a broadcast(), the standee reads the targeted element property and floods it as a BUTTON_PRESS message. The library delivers this via the button events on useMesh().

const { onPlusButton, onMinButton, onEitherButton } = useMesh();

onPlusButton.attach((result) => {
    console.log(`Standee ${result.id} broadcast: ${result.data}`);
});

The ButtonResult contains:

  • id — the standee ID that sent the broadcast
  • data — the element property value as a string

Which event fires depends on which physical button was pressed. Actions on Button.PLUS trigger onPlusButton, Button.MINUS triggers onMinButton, and Button.EITHER triggers onEitherButton.

Broadcast with a target sends the current value of that element property. For a Text element targeted at ElementData.DATA, result.data contains the text content (e.g. "42"):

const { target, increment, broadcast } = useActions();
const { setButton } = useButtons(standee);

const hp = target(counter.id, ElementData.DATA);
await setButton(Button.PLUS, [increment(hp), broadcast(hp)]).send();

onPlusButton.attach((result) => {
    const value = parseInt(result.data, 10);
    console.log(`HP is now: ${value}`);  // "1", "2", "3", ...
});

Broadcast without a target (broadcast() with no arguments) still triggers the button event, but result.data will be an empty string. This is useful for simple "button was pressed" notifications:

await setButton(Button.PLUS, [increment(hp), broadcast()]).send();

onPlusButton.attach((result) => {
    // result.data === ""
    console.log(`Standee ${result.id} plus pressed`);
});

useConverter()

Converts images to the monochrome byte format the standees expect.

const { imageToBytes } = useConverter();
const bytes = await imageToBytes("/path/to/image.png");

useBinary()

Low-level binary utilities for pixel data conversion.

const { pixelsToBytes, bitswap, flattenUint8Arrays } = useBinary();

// Convert pixel array to monochrome byte format
const bytes = pixelsToBytes(pixels, width);

// Concatenate multiple Uint8Arrays into one
const combined = flattenUint8Arrays([array1, array2]);

useImage()

Low-level image processing utilities used internally by useConverter().

const { createImage, getImageData, imageDataToPixels } = useImage();

// Load an image from URL
const image = await createImage("/path/to/image.png");

// Get raw ImageData from an HTMLImageElement
const imageData = getImageData(image);

// Convert ImageData to monochrome pixel array
const pixels = imageDataToPixels(imageData);
// { width, height, pixels: number[] }  — pixels are 0 (black) or 1 (white)

useUtils()

General-purpose utility functions.

const { map } = useUtils();

// Map a value from one range to another
const mapped = map(value, fromLow, fromHigh, toLow, toHigh);

useDebug()

Message inspector and debug mode. Logs decoded packet contents, retransmission events, and message timing in a structured format. Emits events for building debug UIs.

const debug = useDebug();

// Enable debug logging (no overhead when disabled)
debug.enable();

// Check if debug mode is active
debug.isEnabled(); // true

// Subscribe to all debug events in real-time
debug.onDebugEvent.attach((entry) => {
    console.log(`[${entry.type}]`, entry.data);
});

// Track message latency
debug.onTimingUpdate.attach((timing) => {
    if (timing.latencyMs !== null) {
        console.log(`Cache ${timing.cacheId}: ${timing.latencyMs}ms`);
    }
});

// Read the full event log
const log = debug.getLog();

// Get timing records for all messages
const timings = debug.getTimings();

// Get timing for a specific cache ID
const timing = debug.getTimingForCache(cacheId);

// Decode a raw BLE packet into a structured object
const decoded = debug.decodePacket(rawPacket);
// { cacheId, targetId, ttl, position, dataSize, typeName, typeValue, payloadBytes, raw }

// Control
debug.disable();           // Pause logging
debug.clear();             // Clear log and timing data
debug.setMaxLogSize(1000); // Adjust log buffer size (default: 500)

Event types logged: message_queued, packet_sent, retransmit, timeout, message_complete, message_done, received_packets, node_discovered, node_lost, connected, disconnected.

useMockBluetooth()

Offline simulator that replaces the real BLE layer with an in-memory mock. Enables development without physical standees and makes integration testing possible.

const mock = useMockBluetooth();

// Activate the mock before connecting
mock.activate({
    standees: [
        { id: 1, neighbors: [2, 3] },
        { id: 2, neighbors: [1] },
        { id: 3, neighbors: [1] },
    ],
    responseDelay: 10,      // ms before MESSAGE_DONE response (default: 10)
    dropRate: 0,            // 0-1 packet drop probability (default: 0)
    partialDelivery: false, // simulate DATA_FEEDBACK for incomplete messages (default: false)
});

// Now connect normally — all BLE calls go through the mock
await useMesh().connect();
// onNodes fires with the configured standee topology

// Simulate button presses from virtual standees
mock.pressButton(1, "plus");           // triggers onPlusButton
mock.pressButton(2, "minus", "data");  // triggers onMinButton with text data
mock.pressButton(3, "either");         // triggers onEitherButton

// Simulate mesh topology changes
mock.simulateNodeJoin({ id: 4, neighbors: [1] }); // triggers onNode
mock.simulateNodeLost(3);                          // triggers onNodeLost

// Simulate disconnection
mock.disconnect();                     // triggers onDisconnect

// Control reconnect behavior
mock.reconnectable(false);             // makes reconnect() reject
mock.reconnectable(true);              // restore (default)

// Update config at runtime
mock.updateConfig({ responseDelay: 50, dropRate: 0.2 });

// Check state
mock.isActive();  // true
mock.getConfig(); // current config

// Restore real BLE
mock.deactivate();

When writeCharacteristic is called (via Buffer.send()), the mock tracks incoming packets and automatically fires MESSAGE_DONE through the flood characteristic once all packets for a message are received. With partialDelivery: true, it sends DATA_FEEDBACK after a batch of writes, triggering the retransmission flow.

Display Elements

All elements extend DisplayElement and share common properties:

| Property | Type | Description | | ------------ | ------- | ----------------------------------------------------- | | id | number | Auto-assigned by useStandeeElements().add() | | x | number | X position | | y | number | Y position | | show | boolean | Visibility (default: true) | | colorBlack | boolean | Draw in black when true, white when false | | dirty | boolean | Automatically set to true when any property changes |

Setting any property automatically marks the element as dirty, so it will be included in the next draw() call.

Rectangle

new Rectangle(x, y, width, height, radius?, fill?, show?, colorBlack?)

| Property | Type | Default | Description | | -------- | ------- | ------- | ------------------------------- | | width | number | | Rectangle width | | height | number | | Rectangle height | | radius | number | 0 | Corner radius for rounded edges | | fill | boolean | false | Fill the rectangle when true |

Text

new Text(x, y, text, font, center?, show?, colorBlack?)

| Property | Type | Default | Description | | -------- | ------- | ------- | ---------------------------------------- | | text | string | | The text content | | font | number | | Font from the Font enum | | center | boolean | false | Center the text at the given coordinates |

Available fonts (from Font enum):

  • u8g2_font_5x7_mf — small
  • u8g2_font_crox2c_tr — medium
  • u8g2_font_10x20_tn — numeric
  • u8g2_font_inr33_t_cyrillic — large

Image

new Image(x, y, width, height, data, show?, colorBlack?)

| Property | Type | Description | | -------- | ------ | ------------------------------------------------- | | width | number | Image width | | height | number | Image height | | data | number | Image ID (0-255) matching a previously uploaded graphic |

Objects

Standee

Represents a discovered standee in the mesh network.

| Property | Type | Description | | ------------ | ----------------- | ---------------------------------------- | | id | number | Unique standee identifier | | neighbors | number[] | IDs of neighboring standees in the mesh | | elements | DisplayElement[] | Display elements assigned to this standee| | onPlus | SyncEvent<void> | Fires when the plus button is pressed | | onMin | SyncEvent<void> | Fires when the minus button is pressed |

| Method | Returns | Description | | -------- | ------- | -------------------------------------------- | | name() | string | Hex-formatted ID (e.g. "0a" for id 10) |

ButtonResult

Returned by button press events (onPlusButton, onMinButton, onEitherButton).

| Property | Type | Description | | -------- | ------ | ------------------------------- | | id | number | ID of the standee that was pressed | | data | string | Optional data sent with the press |

Buffer

Wraps BLE message data for transmission. Created internally by composable methods.

| Property | Type | Description | | --------- | ------------ | --------------------------------------- | | cache | number | Auto-generated cache ID for tracking | | type | DataType | The message type | | id | number | Target standee ID | | packets | Uint8Array[] | The message split into BLE-sized packets|

| Method | Returns | Description | | ------------------------- | -------------- | ------------------------------------ | | send() | Promise<void>| Transmit the buffer over BLE | | then(callback) | Buffer | Chain a callback for after send completes |

Enums

Core Enums

Font

| Value | ID | Description | | ----------------------------- | -- | ----------- | | u8g2_font_5x7_mf | 0 | Small | | u8g2_font_inr33_t_cyrillic | 1 | Large | | u8g2_font_10x20_tn | 2 | Numeric | | u8g2_font_crox2c_tr | 3 | Medium |

Button

| Value | ID | Description | | ------- | -- | ------------- | | MINUS | 0 | Minus button | | PLUS | 1 | Plus button | | EITHER| 2 | Either button |

ActionType

| Value | ID | Description | | ----------- | -- | -------------------- | | INCREMENT | 0 | Increment a value | | DECREMENT | 1 | Decrement a value | | SHOW | 2 | Show an element | | HIDE | 3 | Hide an element | | SET | 4 | Set a value | | BROADCAST | 5 | Broadcast to mesh | | TIMER | 6 | Delayed action | | CONDITION | 7 | Conditional action | | BREAK | 8 | Break action chain | | MAP | 9 | Map value to range |

ElementData

Target properties for actions.

| Value | ID | Description | | -------- | -- | ---------------- | | TYPE | 0 | Element type | | ID | 1 | Element ID | | SHOW | 2 | Visibility | | X | 3 | X position | | Y | 4 | Y position | | WIDTH | 5 | Width | | HEIGHT | 6 | Height | | RADIUS | 7 | Corner radius | | FILL | 8 | Fill mode | | DATA | 9 | Data/image ID | | FONT | 10 | Font |

ActionCondition

Comparison operators for condition() actions.

| Value | ID | Description | | --------------- | -- | ---------------- | | EQUAL | 0 | Equal to | | GREATER | 1 | Greater than | | LESS | 2 | Less than | | GREATER_EQUAL | 3 | Greater or equal | | LESS_EQUAL | 4 | Less or equal |

ActionDataType

Value types used in set() and condition() actions.

| Value | ID | Description | | --------- | -- | --------------------- | | STRING | 0 | String value | | NUMBER | 1 | Numeric value | | ELEMENT | 2 | Element property ref |

Protocol Enums

These enums are used internally by the BLE protocol layer but are exported for advanced use cases and debugging.

DataType

Message types sent over BLE.

| Value | ID | Description | | ---------------- | -- | ------------------------ | | UPLOAD | 0 | Image upload | | DRAW_IMAGE | 1 | Draw an image element | | DRAW_RECTANGLE | 2 | Draw a rectangle element | | BUTTON_PRESS | 3 | Button press event | | DRAW_TEXT | 4 | Draw a text element | | ALL | 5 | Draw all elements | | CLEAR_GRAPHICS | 6 | Clear uploaded graphics | | ACTIONS | 7 | Button action assignment | | CLEAR_SCREEN | 8 | Clear the screen | | LOADER | 10 | Loader header |

FloodTypes

Flood message types for mesh-level communication.

| Value | ID | Description | | ---------------- | -- | ---------------------------------- | | NODES | 0 | Node discovery | | LOST_NODE | 1 | Node left the mesh | | MESSAGE_DONE | 2 | Message fully received by target | | DATA_FEEDBACK | 3 | Partial delivery feedback |

HandshakeType

Handshake protocol types for initial connection.

| Value | ID | Description | | ------------ | -- | --------------------- | | FLOOD_NODES| 2 | Flood node discovery | | STEP_ONE | 4 | Handshake step one | | STEP_TWO | 5 | Handshake step two |

Data

Byte positions within a BLE packet. Useful for manual packet inspection with useDebug().decodePacket().

| Value | Byte Position | Description | | ----------- | ------------- | --------------------------------- | | ID | 0 | Target standee ID | | CACHE | 1 | Cache ID for message tracking | | TTL | 2 | Time to live (mesh hops) | | POSITION | 3-4 | Packet position (uint16 LE) | | DATA_SIZE | 5-6 | Total data size (uint16 LE) | | TYPE | 7 | Message type (DataType) | | DATA | 8+ | Payload data |

TypeScript Types

The library exports the following type definitions for use in your application:

Connection & Reconnect

interface ReconnectStatus {
    attempt: number;      // Current attempt number
    maxRetries: number;   // Maximum retries allowed
    delay: number;        // Delay before this attempt in ms
}

Debug Types

interface DebugLogEntry {
    type: DebugEventType;
    timestamp: number;
    data: Record<string, unknown>;
}

type DebugEventType =
    | "packet_sent" | "message_queued" | "retransmit" | "timeout"
    | "message_complete" | "message_done" | "received_packets"
    | "node_discovered" | "node_lost" | "connected" | "disconnected";

interface DecodedPacket {
    cacheId: number;
    targetId: number;
    ttl: number;
    position: number;
    dataSize: number;
    typeName: string;
    typeValue: number;
    payloadBytes: number;
    raw: number[];
}

interface MessageTimingRecord {
    cacheId: number;
    queuedAt: number;
    firstPacketAt: number | null;
    completedAt: number | null;
    latencyMs: number | null;
    retransmitCount: number;
    timeoutCount: number;
    packetsSent: number;
    totalPackets: number;
}

MeshWriter Events

interface PacketSentEvent {
    cacheId: number;
    position: number;
    dataSize: number;
    packetBytes: number;
    batchIndex: number;
    timestamp: number;
}

interface MessageQueuedEvent {
    cacheId: number;
    type: number;
    targetId: number;
    packetCount: number;
    totalBytes: number;
    timestamp: number;
}

interface RetransmitEvent {
    cacheId: number;
    packetCount: number;
    reason: "feedback" | "timeout";
    timestamp: number;
}

interface TimeoutEvent {
    cacheId: number;
    retryCount: number;
    maxRetries: number;
    willRetry: boolean;
    timestamp: number;
}

interface MessageCompleteEvent {
    cacheId: number;
    timestamp: number;
}

Loader Types

interface LoaderProgress {
    current: number;     // Buffers completed so far
    total: number;       // Total buffers
    percentage: number;  // 0 to 1
}

interface LoaderError {
    buffer: number;      // Index of the buffer that failed
    error: Error;        // The Error object
}

Mock Types

interface VirtualStandee {
    id: number;
    neighbors: number[];
}

interface MockMeshConfig {
    standees: VirtualStandee[];
    responseDelay?: number;      // Default: 10
    dropRate?: number;           // Default: 0
    partialDelivery?: boolean;   // Default: false
}

Image Types

type ImagePixels = {
    width: number;
    height: number;
    pixels: number[];   // 0 (black) or 1 (white)
}

Configuration

The BLE protocol uses these internal constants (via useBluetoothSettings()):

| Constant | Value | Description | | ----------------------- | ------ | ------------------------------------------------- | | PACKET_SIZE | 13 | Max payload bytes per BLE packet | | BATCH_SIZE | 20 | Packets sent per batch before waiting for ack | | ACK_TIMEOUT | 5000 | Milliseconds to wait for acknowledgment | | MAX_RETRIES | 3 | Retransmission attempts before giving up | | TTL | 10 | Time to live (max mesh hops) | | RECONNECT_DELAY | 2000 | Base delay for auto-reconnect (doubles each try) | | RECONNECT_MAX_RETRIES | 5 | Maximum auto-reconnect attempts |

Events

The library uses ts-events for event handling:

// Subscribe
mesh.onConnect.attach(() => console.log("Connected"));

// Unsubscribe
const handler = () => console.log("Connected");
mesh.onConnect.attach(handler);
mesh.onConnect.detach(handler);

Full Example

A complete example that connects to the mesh, uploads images, creates a UI, and configures button interaction:

import {
    useMesh,
    useStandees,
    useStandeeElements,
    useDisplay,
    useGraphics,
    useLoader,
    useConverter,
    useActions,
    useButtons,
} from "nsd-ble";
import { Rectangle, Text, Image } from "nsd-ble/elements";
import { Font, Button, ElementData } from "nsd-ble/enums";

const mesh = useMesh();
const { standees, onChange } = useStandees();
const { draw, clear } = useDisplay();

// Connect on button click
document.getElementById("connect").addEventListener("click", () => {
    mesh.connect();
});

onChange.attach(async (list) => {
    const standee = list[0];

    // 1. Upload images
    const images = ["/img/icon-a.png", "/img/icon-b.png"];
    const buffers = await Promise.all(
        images.map(async (path, i) => {
            const bytes = await useConverter().imageToBytes(path);
            return useGraphics(standee).upload(i, bytes);
        }),
    );

    const loader = useLoader(standee).load(buffers);
    loader.onProgress.attach((p) => {
        console.log(`Uploading: ${Math.round(p.percentage * 100)}%`);
    });
    await loader.send();

    // 2. Build the display
    const { add } = useStandeeElements(standee);
    const bg = add(new Rectangle(0, 0, 128, 64, 0, true));
    const icon = add(new Image(10, 16, 32, 32, 0));
    const label = add(new Text(64, 40, "HP: 10", Font.u8g2_font_5x7_mf, true));

    await draw(standee).send();

    // 3. Wire up buttons
    const actions = useActions();
    const hpTarget = actions.target(label.id, ElementData.TEXT);

    await useButtons(standee)
        .setButton(Button.PLUS, [actions.increment(hpTarget)])
        .send();

    await useButtons(standee)
        .setButton(Button.MINUS, [actions.decrement(hpTarget)])
        .send();
});

// Listen for button presses from the app side
mesh.onPlusButton.attach((result) => {
    console.log(`Plus pressed on standee ${result.id}`);
});