tcl-ble-osx

Bluetooth LE for Tcl/Tk on macOS via CoreBluetooth, exposing the same ble command as AndroWish.

tcl-ble-osx on GitHub →

What it is

tcl-ble-osx is a Bluetooth Low Energy library for Tcl/Tk on macOS. It provides a ble command that is API-compatible with AndroWish, whose built-in ble command exists only on Android and Linux.

macOS has no such command, so Tcl code written against the AndroWish BLE API — for example the Decent Espresso de1app — cannot reach Bluetooth devices on the Mac. This library implements the API using Apple's CoreBluetooth, so that code runs unmodified.

It runs under plain tclsh (no Tk), standard Aqua wish, and undroidwish. It has been tested end-to-end against a Decent Espresso DE1 machine and an Atomax Skale scale: scan, connect, service and characteristic discovery, notification enable plus ACK, live notifications, and reads.

How it works (CoreBluetooth helper)

ble.tcl installs the ble command and presents the AndroWish event dictionaries (scan, connection, characteristic, descriptor), invoking the callback as {*}$callback $event $datadict. It has two interchangeable CoreBluetooth backends and selects one automatically:

Rationale for the separate process: a loadable extension runs inside the interpreter, so its Bluetooth access takes on the interpreter's TCC (privacy) identity. That works for a signed app or on iOS, but an unsignable host such as undroidwish cannot get Bluetooth that way — and an in-process attempt there can wedge the process. The subprocess helper re-spawns itself with responsibility disclaimed (responsibility_spawnattrs_setdisclaim), so on hosts that need it the helper becomes its own TCC identity. It is also architecture- and stubs-independent.

Two macOS constraints the library handles:

  1. No MAC addresses. CoreBluetooth never exposes a peripheral's hardware address; it uses an opaque, host-stable NSUUID. The library uses that UUID string as the "address" used to scan, store, and reconnect. It is stable across launches for a given Mac + peripheral pair.
  2. Bluetooth permission (TCC). macOS attributes a Bluetooth request to the "responsible" app and needs an NSBluetoothAlwaysUsageDescription usage string to prompt. The library arranges attribution so the first-run prompt appears; it is approved once and the grant persists.

Status

Tested end-to-end against a DE1 + Skale on x86_64 undroidwish, arm64 (Apple Silicon) undroidwish, iWish (Catalyst), and standard Aqua wish: scan, connect, characteristic discovery, notification enable and descriptor ACK, streaming notifications, and reads.

On macOS the default is the subprocess helper, which runs on any interpreter. The native in-process extension is opt-in and intended for signed apps and iOS.

Using it (API, env vars)

Load the package and drive it from the Tcl event loop:

package require ble

proc cb {event data} {
    if {$event eq "scan"} {
        puts "[dict get $data rssi] dBm  [dict get $data name]  [dict get $data address]"
    }
}
ble scanner cb        ;# start scanning; cb fires for every device
vwait forever         ;# in a script (or tclsh) you must run the event loop

BLE is asynchronous, so the callback only fires while Tcl's event loop is running. wish/undroidwish enter it automatically once the script finishes, but tclsh does not — end a script with vwait forever (or vwait somevar).

Core subcommands:

ble scanner   <callback>                          -> scanner token; starts scanning
ble start     <token>                             -> idempotent re-scan
ble stop      <token>                             -> stop scanning
ble connect   <address> <callback> ?<reconnect>?  -> connection handle (e.g. "ble1")
ble reconnect <handle>                            -> reconnect an existing handle
ble close     <handle>                            -> disconnect (or stop a scanner)
ble info      ?<handle>?                          -> open handles / info for one
ble enable    <h> <suuid> <si> <cuuid> <ci>       -> enable notifications
ble disable   <h> <suuid> <si> <cuuid> <ci>
ble write     <h> <suuid> <si> <cuuid> <ci> ?<writetype>? <data>
ble read      <h> <suuid> <si> <cuuid> <ci>
ble mtu       <h> ?<value>?                        -> negotiated MTU
ble userdata  <h> ?<value>?                        -> per-handle scratch store
ble state                                          -> central manager state

The callback receives $event $datadict:

event datadict keys
scan address name rssi
connection handle address state (connected/disconnected), mtu on connect
characteristic (state=discovery) handle address suuid sinstance cuuid cinstance
characteristic (state=connected) access (r read / w write-ack / c notification), value (binary), cuuid
descriptor (state=connected access=w) the notification-enable (CCCD) acknowledgement

The sinstance/cinstance integers are assigned during discovery and echoed back; store them keyed by UUID and pass them to enable/write/read.

Environment variables:

Building

Requires the Xcode command-line tools (swiftc).

git clone https://github.com/johnbuckman/tcl-ble-osx
cd tcl-ble-osx
./build.sh            # builds + signs bin/ble_helper.bin (universal)

A prebuilt helper is included. Rebuilding locally binds the Bluetooth grant to a locally controlled signature. native/build.sh builds the in-process native dylib.

package require ble finds the package only if its directory is on Tcl's auto_path. /usr/local/lib is on the auto_path of both the system tclsh/wish and undroidwish, so a single symlink there makes it available everywhere:

ln -s /path/to/tcl-ble-osx /usr/local/lib/tcl-ble-osx

Always move the whole directory — the package locates bin/ble_helper.bin relative to ble.tcl.

First run shows a one-time macOS Bluetooth prompt; approve it and the grant persists (keyed to the helper's code signature, so rebuilding re-prompts). For distribution, sign the helper with a Developer ID and notarize the containing app so the grant is stable and Gatekeeper-friendly; keep NSBluetoothAlwaysUsageDescription in the embedded Info.plist.

Repository

Presented at EuroTcl 2026 (Vienna) — "BLE for Tcl: one Bluetooth Low Energy API, from desktop to phone, wired into the Tcl event loop." Originally written to run the Decent Espresso de1app on macOS; usable by any Tcl program that needs Bluetooth LE on the Mac.