Bluetooth LE comes in four separately-gated pieces — passive scanning, advertising, the GATT client, and the GATT server — plus connectionless ESP-NOW over the Wi-Fi radio. Declare only the ones you use: each is its own capability and its own consent prompt.
BLE scan¶
Capability: ble.scan.
Tier 1 — prompted once, then persisted.
ble_scan¶
Perform a passive BLE scan and collect advertisement packets.
jpp_sdk_status_t jpp_sdk_ble_scan(jpp_sdk_context_t *ctx,
uint32_t duration_ms,
jpp_sdk_ble_scan_result_t *results,
size_t max_results,
size_t *out_count,
jpp_broker_result_t *result);
jppsdk.ble_scan(duration_ms: int) -> list[dict]
Parameters:
| Name | Description |
|---|---|
duration_ms |
How long to scan, in milliseconds. |
results |
C only: array to fill. |
max_results |
C only: capacity of results. Maximum 20. |
out_count |
C only: number of results written. |
Returns:
- C: JPP_SDK_OK; *out_count devices found; results[i] has address, name, rssi, ad_data, ad_data_len.
- Python: list[dict], each with keys address (str "AA:BB:CC:DD:EE:FF"), name (str, empty if unnamed), rssi (int dBm), ad_data (bytes up to 31).
C struct — jpp_sdk_ble_scan_result_t:
typedef struct {
char address[18]; /* "AA:BB:CC:DD:EE:FF\0" */
char name[32]; /* local name, or "" if none */
int8_t rssi; /* signal strength in dBm */
uint8_t ad_data[31]; /* raw advertisement payload */
size_t ad_data_len; /* bytes used in ad_data */
} jpp_sdk_ble_scan_result_t;
BLE advertise¶
Capability: ble.advertise.
Tier 1 — prompted once, then persisted. Also covers
ble_set_connectable.
ble_advertise_start¶
Begin broadcasting a BLE advertisement payload.
jpp_sdk_status_t jpp_sdk_ble_advertise_start(jpp_sdk_context_t *ctx,
const uint8_t *payload,
size_t payload_len,
jpp_broker_result_t *result);
jppsdk.ble_advertise_start(payload: bytes) -> None
Parameters:
| Name | Description |
|---|---|
payload |
Raw advertisement payload. Maximum 31 bytes. |
ble_advertise_stop¶
Stop broadcasting.
jpp_sdk_status_t jpp_sdk_ble_advertise_stop(jpp_sdk_context_t *ctx,
jpp_broker_result_t *result);
jppsdk.ble_advertise_stop() -> None
ble_set_connectable (C only)¶
Set whether inbound BLE connections are accepted while advertising.
Capability: ble.advertise
jpp_sdk_status_t jpp_sdk_ble_set_connectable(jpp_sdk_context_t *ctx,
bool connectable,
jpp_broker_result_t *result);
Notes: Python binding not available — use ble_host for accepting inbound connections from Python.
BLE GATT client¶
Capability: ble.connect.
Tier 2 — prompted on first use of every launch, never persisted.
Establish an outbound connection to a BLE peripheral and read/write its characteristics. Maximum 2 simultaneous connections per session.
One characteristic value is capped at 512 bytes.
This is BLE_ATT_ATTR_MAX_LEN, a fixed Bluetooth spec limit, not a firmware
tunable, and it applies in both directions. ble_read_char reassembles long
values via ATT Read Blob and ble_write_char splits large writes via ATT
Prepare/Execute, but neither can exceed 512 bytes for one value; a larger
write is rejected by the peer and a read is truncated.
To move a payload larger than that, chunk it at the app layer — see the
reusable jpp_ble_msg helper (apps/common/jpp_ble_msg.{c,h}), which
frames a message into ordered chunks over ble_write_char and reassembles
them on the peer from ble_host_wait_write.
Reconnecting to a peer works reliably across rounds: ble_disconnect blocks
until the link is fully torn down, and a ble.host peripheral keeps
advertising after a peer disconnects.
ble_connect¶
Connect to a BLE peripheral by address.
jpp_sdk_status_t jpp_sdk_ble_connect(jpp_sdk_context_t *ctx,
const char *address,
jpp_sdk_ble_conn_t *out_conn);
jppsdk.ble_connect(address: str) -> int
Parameters:
| Name | Description |
|---|---|
address |
BLE address string in format "AA:BB:CC:DD:EE:FF". |
out_conn / return |
Connection handle for subsequent ble_read_char/ble_write_char/ble_disconnect calls. |
ble_read_char¶
Read a GATT characteristic value.
jpp_sdk_status_t jpp_sdk_ble_read_char(jpp_sdk_context_t *ctx,
jpp_sdk_ble_conn_t conn,
const char *svc_uuid,
const char *char_uuid,
jpp_broker_result_t *result);
jppsdk.ble_read_char(conn: int, svc_uuid: str, char_uuid: str) -> dict
Parameters:
| Name | Description |
|---|---|
svc_uuid |
Service UUID, full format "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX". |
char_uuid |
Characteristic UUID, same format. |
Returns: result->text / result["text"] — the characteristic value as a string.
ble_write_char¶
Write a value to a GATT characteristic.
jpp_sdk_status_t jpp_sdk_ble_write_char(jpp_sdk_context_t *ctx,
jpp_sdk_ble_conn_t conn,
const char *svc_uuid,
const char *char_uuid,
const char *text,
jpp_broker_result_t *result);
jppsdk.ble_write_char(conn: int, svc_uuid: str, char_uuid: str, text: str) -> None
ble_disconnect¶
Close a BLE connection.
jpp_sdk_status_t jpp_sdk_ble_disconnect(jpp_sdk_context_t *ctx,
jpp_sdk_ble_conn_t conn);
jppsdk.ble_disconnect(conn: int) -> None
BLE GATT server¶
Capability: ble.host.
Tier 2 — prompted on first use of every launch, never persisted.
The firmware pre-registers a generic app-host GATT service. An app claims it with ble_service_register, publishes data via the TX characteristic, and receives data via the RX characteristic. There are no per-app GATT definitions in the firmware — apps identify their protocol through the advertisement payload.
Fixed UUIDs:
| Symbol | UUID | Direction |
|---|---|---|
JPP_SDK_BLE_HOST_SVC_UUID |
4a505300-0000-0000-0000-000000000000 |
Service |
JPP_SDK_BLE_HOST_TX_UUID |
4a505301-0000-0000-0000-000000000000 |
Readable by peer |
JPP_SDK_BLE_HOST_RX_UUID |
4a505302-0000-0000-0000-000000000000 |
Writable by peer |
ble_service_register¶
Claim the firmware's app-host GATT service for this session.
jpp_sdk_status_t jpp_sdk_ble_service_register(jpp_sdk_context_t *ctx,
const char *svc_uuid,
jpp_broker_result_t *result);
jppsdk.ble_service_register(svc_uuid: str) -> None
Notes: Pass JPP_SDK_BLE_HOST_SVC_UUID as the UUID. Only one app can claim the service at a time.
ble_service_unregister¶
Release the GATT service.
jpp_sdk_status_t jpp_sdk_ble_service_unregister(jpp_sdk_context_t *ctx,
jpp_broker_result_t *result);
jppsdk.ble_service_unregister() -> None
ble_host_set_value (C only)¶
Publish bytes to the TX characteristic so connected peers can read them.
jpp_sdk_status_t jpp_sdk_ble_host_set_value(jpp_sdk_context_t *ctx,
const uint8_t *data,
size_t len,
jpp_broker_result_t *result);
ble_host_wait_write (C only)¶
Block until a peer writes to the RX characteristic, or the timeout elapses.
jpp_sdk_status_t jpp_sdk_ble_host_wait_write(jpp_sdk_context_t *ctx,
uint8_t *buf,
size_t *len_inout,
uint32_t timeout_ms,
bool *out_received,
jpp_broker_result_t *result);
Parameters:
| Name | Description |
|---|---|
buf |
Buffer to receive the written bytes. |
len_inout |
In: buffer capacity. Out: bytes received. |
timeout_ms |
Maximum wait time. |
out_received |
Set to true if a write was received within the timeout. |
ble_host_clear (C only)¶
Clear the RX write buffer.
jpp_sdk_status_t jpp_sdk_ble_host_clear(jpp_sdk_context_t *ctx,
jpp_broker_result_t *result);
ESP-NOW¶
Capability: esp_now.
Tier 1 — prompted once, then persisted.
Connectionless peer-to-peer messaging over WiFi. Shares the STA-mode WiFi radio with http.request, WebDAV, and the LRV server.
espnow_send¶
Send data to a peer, identified by its 6-byte MAC address. The peer is added to the ESP-NOW peer list automatically on first use (open, unencrypted, current WiFi channel). Blocks until the send completes or an internal timeout elapses.
jpp_sdk_status_t jpp_sdk_espnow_send(jpp_sdk_context_t *ctx,
const uint8_t peer_mac[6],
const uint8_t *data,
size_t data_len,
jpp_broker_result_t *result);
jppsdk.espnow_send(peer_mac: bytes, data: bytes) -> None
Parameters:
| Name | Description |
|---|---|
peer_mac |
6-byte peer MAC address. |
data |
Payload to send. Maximum 250 bytes. |
espnow_recv¶
Wait up to timeout_ms for an incoming ESP-NOW packet.
jpp_sdk_status_t jpp_sdk_espnow_recv(jpp_sdk_context_t *ctx,
uint8_t out_peer_mac[6],
uint8_t *out_data,
size_t max_len,
size_t *out_len,
uint32_t timeout_ms,
jpp_broker_result_t *result);
jppsdk.espnow_recv(timeout_ms: int) -> tuple[bytes, bytes] | None
Returns:
- C: JPP_SDK_STATUS_OK with out_peer_mac/out_data/out_len filled on a received packet; JPP_SDK_STATUS_NO_DATA (not an error) if nothing arrived within timeout_ms.
- Python: (peer_mac, data) tuple of bytes, or None on timeout.
Notes: Received packets are buffered in a small internal queue (depth 8); a packet is dropped if the app doesn't call espnow_recv often enough to drain it.