juno.bluetooth¶
Talk to Bluetooth Low Energy peripherals (central role).
Wraps Apple’s Core Bluetooth central API. Open scans yield
Advertisement events, Peripheral.connect() returns a
context-manager Connection, and characteristic reads /
writes / notifications are exposed as plain Python calls and
iterators.
Typical usage — Nordic UART pattern:
from juno import bluetooth as bt
NUS_SERVICE = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
NUS_TX = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" # write to device
NUS_RX = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" # notify from device
bt.request_authorization() # blocks for prompt
# Find the device by name, scanning for advertisements that
# include the UART service UUID.
target = None
for adv in bt.scan(services=[NUS_SERVICE], timeout=10):
if adv.name == "M5-BB-Cardputer":
target = adv.peripheral
break
if target is None:
raise RuntimeError("device not in range")
with target.connect() as conn:
service = conn.service(NUS_SERVICE)
rx = service.characteristic(NUS_RX)
tx = service.characteristic(NUS_TX)
# Send a payload (no ack required).
tx.write(b"hello\n", response=False)
# Receive incoming notifications.
for chunk in rx.notifications():
print("got:", chunk)
if chunk == b"bye\n":
break
Authorization:
CBManager.authorizationis read-only — there is no explicit Apple-provided request method, so the system prompt fires on the first real radio operation (scan / connect).request_authorization()triggers a brief scan to provoke the prompt up-front and waits until the status resolves.
Streaming queue & overflow:
scan()andCharacteristic.notifications()are backed by bounded queues with drop-oldest overflow (advertisements: 128, notifications: 1024). The radio is never stalled — when the consumer can’t keep up, the oldest events are discarded. Iterators expose adroppedcounter so a slow loop can detect this.
Lifecycle:
Peripheral,Connection,Service,Characteristic, andDescriptorrepresent native iOS resources. Usewithblocks (or callclose/disconnectexplicitly) to release them deterministically;__del__is a best-effort safety net only.
Stop is responsive throughout: long-running scans, notification
iterators, and authorization waits all surface as
KeyboardInterrupt when Juno’s Stop button is pressed.
- class juno.bluetooth.AuthorizationStatus¶
Bases:
objectBluetooth authorization status string constants.
- exception juno.bluetooth.DisconnectedError¶
Bases:
BluetoothErrorRaised from a notifications iterator when the peer disconnects mid-stream. Lets reconnect-on-disconnect loops distinguish a user-initiated unsubscribe (StopIteration) from an unexpected drop.
- exception juno.bluetooth.NotConnectedError¶
Bases:
BluetoothErrorOperation requires an active connection but the connection is closed or the peripheral has disconnected.
- exception juno.bluetooth.BluetoothTimeoutError¶
Bases:
BluetoothErrorA blocking operation exceeded its timeout. Named explicitly (rather than reusing the builtin
TimeoutError) so users can handle Bluetooth timeouts separately from filesystem / network ones.
- class juno.bluetooth.Advertisement(peripheral, name=None, rssi=0, is_connectable=True, service_uuids=(), manufacturer_data=None, service_data=None, tx_power_level=None)¶
Bases:
objectA single advertisement received during a scan.
- Parameters:
- peripheral¶
The advertising peripheral. Pass to
Peripheral.connect()to open a session.
- name¶
Local name from the advertisement, or the peripheral’s GAP name if available;
Noneif neither is present.- Type:
str | None
- is_connectable¶
Trueif the peripheral advertised itself as connectable. Some beacon-style devices advertise read-only.- Type:
- service_uuids¶
Service UUIDs the peripheral advertises. UUIDs are returned in the canonical 36-char form (or 4-char short form for SIG-defined services depending on what the peripheral advertised).
- manufacturer_data¶
Raw vendor-specific bytes (the first two bytes are the company identifier per the Bluetooth SIG list);
Noneif not present.- Type:
bytes | None
- service_data¶
Per-service-UUID payload bytes (e.g. iBeacon data, sensor frames). Empty dict when absent.
- class juno.bluetooth.Peripheral¶
Bases:
objectA Bluetooth peripheral.
Peripheral instances are deduped by their identifier UUID: multiple advertisements for the same device produce the same Peripheral. Hold a reference across scans / disconnects to skip rescanning when reconnecting.
Construct via
scan()(advertised) orPeripheral.from_identifier()(cached identifier from a previous run).- classmethod from_identifier(identifier)¶
Resolve a peripheral by its identifier UUID (the value of
Peripheral.identifierfrom a previous run).Backed by
CBCentralManager.retrievePeripherals(withIdentifiers:), so no scan is required — useful for reconnecting to a device without burning power on a discovery cycle.- Parameters:
identifier (str) – Full 36-character UUID string.
- Raises:
TypeError – If
identifieris not a string.ValueError – If
identifieris not a valid UUID.BluetoothError – If the system has no record of that identifier (typical after device-side bonding wipe, factory reset, or first-time pairing).
- Return type:
- connect(*, timeout=15.0)¶
Open a connection to this peripheral.
- Parameters:
timeout (float) – Seconds to wait for
didConnect/didFailToConnect.0waits indefinitely.- Returns:
A
Connectioncontext manager. Usewithblocks for deterministic teardown.- Raises:
BluetoothTimeoutError – If the connection didn’t complete in time.
NotConnectedError – If the system rejected the connect.
- Return type:
- class juno.bluetooth.Connection¶
Bases:
objectAn active Core Bluetooth connection (context manager).
Construct via
Peripheral.connect(). Usewithblocks so the connection is torn down deterministically:with peripheral.connect() as conn: for service in conn.services(): ...
- property peripheral: Peripheral¶
The peripheral this connection was opened against.
- services(*uuids)¶
Discover services on the peripheral.
- Parameters:
*uuids (str) – Optional UUID filter. Forwarded to
CBPeripheral.discoverServices(_:)— passing UUIDs is faster and avoids discovering hidden services some peripherals expose only when the filter is nil. Pass no arguments to discover all.- Returns:
List of
Servicerecords.- Raises:
NotConnectedError – If the connection is closed.
BluetoothTimeoutError – If discovery doesn’t complete in 30 seconds.
- Return type:
- service(uuid)¶
Convenience:
services(uuid)[0].- Raises:
KeyError – If no service with that UUID is exposed.
ValueError – If multiple services advertise the same UUID (rare; call
services()to disambiguate).
- Parameters:
uuid (str)
- Return type:
- read_rssi(*, timeout=5.0)¶
Read the live RSSI in dBm.
- Returns:
Signal strength. Negative values close to 0 mean strong signal; -100 means barely connected.
- Raises:
NotConnectedError – If the connection is closed.
BluetoothTimeoutError – If the read doesn’t complete in time.
- Parameters:
timeout (float)
- Return type:
- wait_ready_to_write(*, timeout=5.0)¶
Block until
peripheralIsReadyToSendWriteWithoutResponsefires (or untiltimeoutelapses).Pair with
Characteristic.write()calls that useresponse=Falseto avoid dropped packets when the controller’s outgoing buffer fills.- Returns:
Trueon signal,Falseon timeout.- Raises:
NotConnectedError – If the connection is closed.
- Parameters:
timeout (float)
- Return type:
- wait_disconnect(*, timeout=None)¶
Block until the peripheral disconnects.
- class juno.bluetooth.Service¶
Bases:
objectA GATT service exposed by a connected peripheral.
- property connection: Connection¶
The owning connection.
- characteristics(*uuids)¶
Discover characteristics for this service.
- Parameters:
*uuids (str) – Optional UUID filter (forwarded to
CBPeripheral.discoverCharacteristics(_:for:)).- Return type:
- characteristic(uuid)¶
Convenience:
characteristics(uuid)[0].- Raises:
KeyError – If no characteristic with that UUID exists.
ValueError – If multiple characteristics share the UUID (rare; use
characteristics()).
- Parameters:
uuid (str)
- Return type:
- class juno.bluetooth.Characteristic¶
Bases:
objectA GATT characteristic on a service.
- property properties: frozenset[str]¶
Subset of
{"read", "write", "writeWithoutResponse", "notify", "indicate", "broadcast", "authenticatedSignedWrites", "extended"}.
- read(*, timeout=30.0)¶
Read the current value (blocking).
- Raises:
NotConnectedError – If the connection is closed.
BluetoothTimeoutError – If no value arrives in time.
- Parameters:
timeout (float)
- Return type:
- write(data, *, response=True, timeout=30.0)¶
Write a value to the characteristic.
- Parameters:
data (bytes | bytearray | memoryview) – Bytes payload.
response (bool) – When
True, wait for the peripheral’s ack viadidWriteValueFor:. WhenFalse, fire-and-forget (faster; pair withConnection.wait_ready_to_write()for backpressure on tight streams).timeout (float) – Seconds to wait for the ack. Ignored when
responseisFalse.
- Raises:
TypeError – If
datais not bytes-like.NotConnectedError – If the connection is closed.
BluetoothTimeoutError – If
response=Trueand no ack arrives in time.
- Return type:
None
- notifications(*, subscribe_timeout=30.0)¶
Subscribe to value-change notifications.
Returns an iterator that yields raw bytes payloads. The underlying
setNotifyValue:true:is sent on iteration start;setNotifyValue:false:runs on iterator close.Pre-flight rejects characteristics without notify or indicate properties (would otherwise park the iterator indefinitely on a silent Core Bluetooth error). The wrapper also blocks for the
didUpdateNotificationStateFor:ack — peer-side rejection (peripheral refused subscription) surfaces here as a BluetoothError instead of a hung iterator.- Parameters:
subscribe_timeout (float) – Seconds to wait for the system / peer to confirm the subscription. Default 30s comfortably covers slow stacks; tune down for tighter UX requirements.
- Return type:
_NotificationIterator
Closing semantics:
Iterating with a
forloop andbreak: the iterator’sclose()runs (Python’s generator-close protocol), which unsubscribes.Peer-initiated disconnect: the iterator raises
DisconnectedErrorfrom the next__next__. Catch this to drive a reconnect loop.
- Raises:
NotConnectedError – If the connection is closed.
BluetoothError – If the characteristic doesn’t support notifications, the peer rejects the subscription, or the confirmation times out.
- Parameters:
subscribe_timeout (float)
- Return type:
_NotificationIterator
- class juno.bluetooth.Descriptor¶
Bases:
objectA GATT descriptor on a characteristic.
Mostly relevant for advanced cases — Client Characteristic Configuration (CCCD) management is handled implicitly when you use
Characteristic.notifications(). Read / write are exposed for vendor-defined descriptors and Characteristic User Description.
- juno.bluetooth.authorization_status()¶
Return the current Bluetooth authorization status.
One of the
AuthorizationStatusstring constants.- Return type:
- juno.bluetooth.is_authorized()¶
Trueiff the app hasallowedAlwaysBluetooth permission.- Return type:
- juno.bluetooth.request_authorization()¶
Provoke the Bluetooth permission prompt and block until the status resolves.
Core Bluetooth has no explicit “request” API — the prompt fires when a real radio operation runs against a freshly- instantiated central. This call instantiates the central and runs a brief throwaway scan to trigger the prompt, then polls
authorization_status()until it stops beingnotDetermined(timeout-free; the user may dismiss the prompt minutes later).- Returns:
Resolved status, one of the
AuthorizationStatusstring constants.- Return type:
Note
Calling this method when status is already determined is a no-op — it returns the existing status without instantiating anything new.
- juno.bluetooth.state()¶
Return the adapter state string.
Useful before a scan or connect to verify the radio is up:
if bt.state() != bt.State.POWERED_ON: raise RuntimeError("Bluetooth is off")
The very first call from a fresh process instantiates the CBCentralManager lazily, and iOS delivers the initial centralManagerDidUpdateState: callback asynchronously a few hundred microseconds later. So the first call can return “unknown” even when Bluetooth is on and authorization is granted. This wrapper polls briefly (capped at _STATE_SETTLE_TIMEOUT) so callers see the settled state instead of the transient placeholder. Subsequent calls return immediately because the central is already instantiated.
- Return type:
- juno.bluetooth.reset()¶
Cancel every in-flight scan and connection and release every open handle.
Mirrors Pythonista’s
cb.reset(). Safe to call from a notebook teardown cell to return the module to a clean state for the next run.- Return type:
None
- juno.bluetooth.scan(*, services=None, allow_duplicates=False, timeout=None)¶
Iterate advertisements until the iterator is closed.
- Parameters:
services (list[str] | None) – If given, only peripherals advertising one of these service UUIDs are reported. Equivalent to CoreBluetooth’s scanForPeripherals(withServices:).
allow_duplicates (bool) – When
False(the default), the system reports each peripheral once per scan. WhenTrue, the same peripheral can be reported multiple times with updated RSSI — useful for proximity loops at the cost of higher battery use.timeout (float | None) – Stop scanning after this many seconds (in addition to closing the iterator on
break).Nonemeans scan indefinitely.
- Yields:
Advertisementrecords as they arrive.- Raises:
TypeError – If
servicesis not a list of strings, ortimeoutis not a number.ValueError – If a UUID is malformed or
timeoutis negative.BluetoothError – If the adapter is not powered on at scan-start time.
- Return type:
Note
The iterator is backed by a bounded queue with drop-oldest overflow — the underlying central queue must not block. The
_ScanIteratorexposes adroppedcount so callers can detect a slow consumer.