Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
4194 lines (3776 sloc)
138 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright (c) 2014 Nordic Semiconductor. All Rights Reserved. | |
* | |
* The information contained herein is property of Nordic Semiconductor ASA. | |
* Terms and conditions of usage are described in detail in NORDIC | |
* SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT. | |
* | |
* Licensees are granted free, non-transferable use of the information. NO | |
* WARRANTY of ANY KIND is provided. This heading must NOT be removed from | |
* the file. | |
* | |
*/ | |
#include "jswrap_bluetooth.h" | |
#include "jsinteractive.h" | |
#include "jsdevices.h" | |
#include "jswrap_promise.h" | |
#include "jswrap_interactive.h" | |
#include "jswrap_string.h" | |
#include "jsnative.h" | |
#include "bluetooth_utils.h" | |
#include "bluetooth.h" | |
#include <stdint.h> | |
#include <string.h> | |
#include <stdbool.h> | |
#ifdef NRF5X | |
#include "nrf5x_utils.h" | |
#include "nordic_common.h" | |
#include "nrf.h" | |
#include "ble_gap.h" | |
#include "ble_hci.h" | |
#include "ble_advdata.h" | |
#include "ble_conn_params.h" | |
#include "app_timer.h" | |
#include "ble_nus.h" | |
#include "app_util_platform.h" | |
#if NRF_SD_BLE_API_VERSION<5 | |
#include "softdevice_handler.h" | |
#endif | |
#ifdef USE_NFC | |
#include "nfc_uri_msg.h" | |
#include "nfc_ble_pair_msg.h" | |
#include "nfc_launchapp_msg.h" | |
#endif | |
#if ESPR_BLUETOOTH_ANCS | |
#include "bluetooth_ancs.h" | |
#endif | |
#endif | |
#ifdef ESP32 | |
#include "BLE/esp32_gap_func.h" | |
#include "BLE/esp32_gatts_func.h" | |
#include "BLE/esp32_gattc_func.h" | |
#define BLE_CONN_HANDLE_INVALID -1 | |
#endif | |
// ------------------------------------------------------------------------------ | |
// ------------------------------------------------------------------------------ | |
JsVar *blePromise = 0; | |
JsVar *bleTaskInfo = 0; | |
JsVar *bleTaskInfo2 = 0; | |
BleTask bleTask = BLETASK_NONE; | |
/// Get the string value of the given task | |
const char *bleGetTaskString(BleTask task) { | |
#ifndef SAVE_ON_FLASH_EXTREME | |
const char *str = BLETASK_STRINGS; // 0 separated, with two 0s at the end | |
while (task && *str) { | |
if (!str) return "?"; | |
str += strlen(str)+1; | |
task--; | |
} | |
if (!*str) return "?"; | |
return str; | |
#else | |
return "?"; | |
#endif | |
} | |
bool bleInTask(BleTask task) { | |
return bleTask==task; | |
} | |
BleTask bleGetCurrentTask() { | |
return bleTask; | |
} | |
bool bleNewTask(BleTask task, JsVar *taskInfo) { | |
if (bleTask) { | |
jsExceptionHere(JSET_ERROR, "BLE task %s is already in progress", bleGetTaskString(bleTask)); | |
return false; | |
} | |
/* if (blePromise) { | |
jsiConsolePrintf("Existing blePromise!\n"); | |
jsvTrace(blePromise,2); | |
} | |
if (bleTaskInfo) { | |
jsiConsolePrintf("Existing bleTaskInfo!\n"); | |
jsvTrace(bleTaskInfo,2); | |
}*/ | |
assert(!blePromise && !bleTaskInfo && !bleTaskInfo2); | |
blePromise = jspromise_create(); | |
bleTask = task; | |
bleTaskInfo = jsvLockAgainSafe(taskInfo); | |
bleTaskInfo2 = NULL; | |
return true; | |
} | |
void bleCompleteTask(BleTask task, bool ok, JsVar *data) { | |
//jsiConsolePrintf(ok?"RES %d %v\n":"REJ %d %q\n", task, data); | |
if (task != bleTask) { | |
jsExceptionHere(JSET_INTERNALERROR, "BLE task completed that wasn't scheduled (%s/%s)", bleGetTaskString(task), bleGetTaskString(bleTask)); | |
return; | |
} | |
bleTask = BLETASK_NONE; | |
if (blePromise) { | |
if (ok) jspromise_resolve(blePromise, data); | |
else jspromise_reject(blePromise, data); | |
jsvUnLock(blePromise); | |
blePromise = 0; | |
} | |
jsvUnLock(bleTaskInfo); | |
bleTaskInfo = 0; | |
jsvUnLock(bleTaskInfo2); | |
bleTaskInfo2 = 0; | |
jshHadEvent(); | |
} | |
void bleCompleteTaskSuccess(BleTask task, JsVar *data) { | |
bleCompleteTask(task, true, data); | |
} | |
void bleCompleteTaskSuccessAndUnLock(BleTask task, JsVar *data) { | |
bleCompleteTask(task, true, data); | |
jsvUnLock(data); | |
} | |
void bleCompleteTaskFail(BleTask task, JsVar *data) { | |
bleCompleteTask(task, false, data); | |
} | |
void bleCompleteTaskFailAndUnLock(BleTask task, JsVar *data) { | |
bleCompleteTask(task, false, data); | |
jsvUnLock(data); | |
} | |
void bleSwitchTask(BleTask task) { | |
bleTask = task; | |
} | |
// ------------------------------------------------------------------------------ | |
// ------------------------------------------------------------------------------ | |
#ifdef NRF52_SERIES | |
void bleSetActiveBluetoothGattServer(int idx, JsVar *var) { | |
assert(idx >=0 && idx < CENTRAL_LINK_COUNT); | |
if (idx<0) return; | |
char name[BLE_NAME_GATT_SERVER_LEN] = BLE_NAME_GATT_SERVER; | |
name[BLE_NAME_GATT_SERVER_LEN-2] = '0'+idx; | |
jsvObjectSetChild(execInfo.hiddenRoot, name, var); | |
} | |
JsVar *bleGetActiveBluetoothGattServer(int idx) { | |
assert(idx < CENTRAL_LINK_COUNT); | |
if (idx<0) return 0; | |
char name[BLE_NAME_GATT_SERVER_LEN] = BLE_NAME_GATT_SERVER; | |
name[BLE_NAME_GATT_SERVER_LEN-2] = '0'+idx; | |
return jsvObjectGetChild(execInfo.hiddenRoot, name, 0); | |
} | |
#endif | |
uint16_t jswrap_ble_BluetoothRemoteGATTServer_getHandle(JsVar *parent) { | |
JsVar *handle = jsvObjectGetChild(parent, "handle", 0); | |
if (!jsvIsInt(handle)) return BLE_CONN_HANDLE_INVALID; | |
return jsvGetIntegerAndUnLock(handle); | |
} | |
uint16_t jswrap_ble_BluetoothDevice_getHandle(JsVar *parent) { | |
JsVar *gatt = jswrap_BluetoothDevice_gatt(parent); | |
uint16_t handle = BLE_CONN_HANDLE_INVALID; | |
if (gatt) handle = jswrap_ble_BluetoothRemoteGATTServer_getHandle(gatt); | |
return handle; | |
} | |
uint16_t jswrap_ble_BluetoothRemoteGATTService_getHandle(JsVar *parent) { | |
JsVar *device = jsvObjectGetChild(parent, "device", 0); | |
uint16_t handle = BLE_CONN_HANDLE_INVALID; | |
if (device) handle = jswrap_ble_BluetoothDevice_getHandle(device); | |
return handle; | |
} | |
uint16_t jswrap_ble_BluetoothRemoteGATTCharacteristic_getHandle(JsVar *parent) { | |
JsVar *service = jsvObjectGetChild(parent, "service", 0); | |
uint16_t handle = BLE_CONN_HANDLE_INVALID; | |
if (service) handle = jswrap_ble_BluetoothRemoteGATTService_getHandle(service); | |
return handle; | |
} | |
// ------------------------------------------------------------------------------ | |
// ------------------------------------------------------------------------------ | |
/*JSON{ | |
"type" : "init", | |
"generate" : "jswrap_ble_init" | |
}*/ | |
void jswrap_ble_init() { | |
// Turn off sleeping if it was on before | |
jsiStatus &= ~BLE_IS_SLEEPING; | |
if (jsiStatus & JSIS_COMPLETELY_RESET) { | |
#if defined(USE_NFC) && defined(NFC_DEFAULT_URL) | |
// By default Puck.js's NFC will send you to the PuckJS website | |
// address is included so Web Bluetooth can connect to the correct one | |
JsVar *addr = jswrap_ble_getAddress(); | |
JsVar *uri = jsvVarPrintf(NFC_DEFAULT_URL"?a=%v", addr); | |
jsvUnLock(addr); | |
jswrap_nfc_URL(uri); | |
jsvUnLock(uri); | |
#endif | |
} else { | |
#ifdef USE_NFC | |
// start NFC, if it had been set | |
JsVar *flatStr = jsvObjectGetChild(execInfo.hiddenRoot, "NfcEnabled", 0); | |
if (flatStr) { | |
uint8_t *flatStrPtr = (uint8_t*)jsvGetFlatStringPointer(flatStr); | |
if (flatStrPtr) jsble_nfc_start(flatStrPtr, jsvGetLength(flatStr)); | |
jsvUnLock(flatStr); | |
} | |
#endif | |
} | |
// Set advertising interval back to default | |
bleAdvertisingInterval = MSEC_TO_UNITS(BLUETOOTH_ADVERTISING_INTERVAL, UNIT_0_625_MS); /**< The advertising interval (in units of 0.625 ms). */ | |
// Now set up whatever advertising we were doing before | |
jswrap_ble_reconfigure_softdevice(); | |
} | |
/** Reconfigure the softdevice (on init or after restart) to have all the services/advertising we need */ | |
void jswrap_ble_reconfigure_softdevice() { | |
JsVar *v,*o; | |
// restart various | |
v = jsvObjectGetChild(execInfo.root, BLE_SCAN_EVENT,0); | |
if (v) jsble_set_scanning(true, false); | |
jsvUnLock(v); | |
v = jsvObjectGetChild(execInfo.root, BLE_RSSI_EVENT,0); | |
if (v) jsble_set_rssi_scan(true); | |
jsvUnLock(v); | |
// advertising | |
v = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_ADVERTISE_DATA, 0); | |
o = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_ADVERTISE_OPTIONS, 0); | |
jswrap_ble_setAdvertising(v, o); | |
jsvUnLock2(v,o); | |
// services | |
v = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_SERVICE_DATA, 0); | |
jsble_set_services(v); | |
jsvUnLock(v); | |
// If we had scan response data set, update it | |
JsVar *scanData = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_SCAN_RESPONSE_DATA, 0); | |
if (scanData) jswrap_ble_setScanResponse(scanData); | |
jsvUnLock(scanData); | |
// Set up security related stuff | |
jsble_update_security(); | |
// If not awake, wake up | |
if (bleStatus & BLE_IS_SLEEPING) | |
jswrap_ble_wake(); | |
} | |
/*JSON{ | |
"type" : "idle", | |
"generate" : "jswrap_ble_idle" | |
}*/ | |
bool jswrap_ble_idle() { | |
return false; | |
} | |
/*JSON{ | |
"type" : "kill", | |
"generate" : "jswrap_ble_kill" | |
}*/ | |
void jswrap_ble_kill() { | |
#ifdef USE_NFC | |
// stop NFC emulation | |
jsble_nfc_stop(); // not a problem to call this if NFC isn't started | |
#endif | |
// stop any BLE tasks | |
bleTask = BLETASK_NONE; | |
if (blePromise) jsvUnLock(blePromise); | |
blePromise = 0; | |
if (bleTaskInfo) jsvUnLock(bleTaskInfo); | |
bleTaskInfo = 0; | |
if (bleTaskInfo2) jsvUnLock(bleTaskInfo2); | |
bleTaskInfo2 = 0; | |
// if we were scanning, make sure we stop | |
jsble_set_scanning(false, false); | |
jsble_set_rssi_scan(false); | |
#if CENTRAL_LINK_COUNT>0 | |
// if we were connected to something, disconnect | |
for (int i=0;i<CENTRAL_LINK_COUNT;i++) | |
if (m_central_conn_handles[i] != BLE_CONN_HANDLE_INVALID) | |
jsble_disconnect(m_central_conn_handles[i]); | |
#endif | |
} | |
void jswrap_ble_dumpBluetoothInitialisation(vcbprintf_callback user_callback, void *user_data) { | |
JsVar *v,*o; | |
v = jsvObjectGetChild(execInfo.root, BLE_SCAN_EVENT,0); | |
if (v) { | |
user_callback("NRF.setScan(", user_data); | |
jsiDumpJSON(user_callback, user_data, v, 0); | |
user_callback(");\n", user_data); | |
} | |
jsvUnLock(v); | |
v = jsvObjectGetChild(execInfo.root, BLE_RSSI_EVENT,0); | |
if (v) { | |
user_callback("NRF.setRSSIHandler(", user_data); | |
jsiDumpJSON(user_callback, user_data, v, 0); | |
user_callback(");\n", user_data); | |
} | |
jsvUnLock(v); | |
// advertising | |
v = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_ADVERTISE_DATA, 0); | |
o = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_ADVERTISE_OPTIONS, 0); | |
if (v || o) | |
cbprintf(user_callback, user_data, "NRF.setAdvertising(%j, %j);\n",v,o); | |
jsvUnLock2(v,o); | |
// services | |
v = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_SERVICE_DATA, 0); | |
o = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_SERVICE_OPTIONS, 0); | |
if (v || o) | |
cbprintf(user_callback, user_data, "NRF.setServices(%j, %j);\n",v,o); | |
jsvUnLock2(v,o); | |
// security | |
v = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_SECURITY, 0); | |
if (v) | |
cbprintf(user_callback, user_data, "NRF.setSecurity(%j);\n",v); | |
jsvUnLock(v); | |
// mac address | |
v = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_MAC_ADDRESS, 0); | |
if (v) | |
cbprintf(user_callback, user_data, "NRF.setAddress(%j);\n",v); | |
jsvUnLock(v); | |
} | |
// ------------------------------------------------------------------------------ | |
// ------------------------------------------------------------------------------ | |
/*JSON{ | |
"type": "class", | |
"class" : "NRF" | |
} | |
The NRF class is for controlling functionality of the Nordic nRF51/nRF52 chips. | |
Most functionality is related to Bluetooth Low Energy, however there are also some functions related to NFC that apply to NRF52-based devices. | |
*/ | |
// ------------------------------------------------------------------------------ | |
// ------------------------------------------------------------------------------ | |
/*JSON{ | |
"type" : "event", | |
"class" : "NRF", | |
"name" : "connect", | |
"params" : [ | |
["addr","JsVar","The address of the device that has connected"] | |
] | |
} | |
Called when a host device connects to Espruino. The first argument contains the address. | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "NRF", | |
"name" : "disconnect", | |
"params" : [ | |
["reason","int","The reason code reported back by the BLE stack - see Nordic's [`ble_hci.h` file](https://github.com/espruino/Espruino/blob/master/targetlibs/nrf5x_12/components/softdevice/s132/headers/ble_hci.h#L71) for more information"] | |
] | |
} | |
Called when a host device disconnects from Espruino. | |
The most common reason is: | |
* 19 - `REMOTE_USER_TERMINATED_CONNECTION` | |
* 22 - `LOCAL_HOST_TERMINATED_CONNECTION` | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "NRF", | |
"name" : "security", | |
"params" : [ | |
["status","JsVar","An object containing `{auth_status,bonded,lv4,kdist_own,kdist_peer}"] | |
] | |
} | |
Contains updates on the security of the current Bluetooth link. | |
See Nordic's `ble_gap_evt_auth_status_t` structure for more information. | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "NRF", | |
"name" : "HID", | |
"#if" : "defined(NRF52_SERIES)" | |
} | |
Called with a single byte value when Espruino is set up as | |
a HID device and the computer it is connected to sends a | |
HID report back to Espruino. This is usually used for handling | |
indications such as the Caps Lock LED. | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "NRF", | |
"name" : "servicesDiscover", | |
"#if" : "defined(NRF52_SERIES) || defined(ESP32)" | |
} | |
Called with discovered services when discovery is finished | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "NRF", | |
"name" : "characteristicsDiscover", | |
"#if" : "defined(NRF52_SERIES) || defined(ESP32)" | |
} | |
Called with discovered characteristics when discovery is finished | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "NRF", | |
"name" : "NFCon", | |
"#if" : "defined(NRF52_SERIES) && defined(USE_NFC)" | |
} | |
Called when an NFC field is detected | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "NRF", | |
"name" : "NFCoff", | |
"#if" : "defined(NRF52_SERIES) && defined(USE_NFC)" | |
} | |
Called when an NFC field is no longer detected | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "NRF", | |
"name" : "NFCrx", | |
"params" : [ | |
["arr","JsVar","An ArrayBuffer containign the received data"] | |
], | |
"#if" : "defined(NRF52_SERIES) && defined(USE_NFC)" | |
} | |
When NFC is started with `NRF.nfcStart`, this is fired | |
when NFC data is received. It doesn't get called if | |
NFC is started with `NRF.nfcURL` or `NRF.nfcRaw` | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "BluetoothDevice", | |
"name" : "gattserverdisconnected", | |
"params" : [ | |
["reason","int","The reason code reported back by the BLE stack - see Nordic's `ble_hci.h` file for more information"] | |
], | |
"ifdef" : "NRF52_SERIES" | |
} | |
Called when the device gets disconnected. | |
To connect and then print `Disconnected` when the device is | |
disconnected, just do the following: | |
``` | |
var gatt; | |
NRF.connect("aa:bb:cc:dd:ee:ff").then(function(gatt) { | |
gatt.device.on('gattserverdisconnected', function(reason) { | |
console.log("Disconnected ",reason); | |
}); | |
}); | |
``` | |
Or: | |
``` | |
var gatt; | |
NRF.requestDevice(...).then(function(device) { | |
device.on('gattserverdisconnected', function(reason) { | |
console.log("Disconnected ",reason); | |
}); | |
}); | |
``` | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "BluetoothRemoteGATTCharacteristic", | |
"name" : "characteristicvaluechanged", | |
"ifdef" : "NRF52_SERIES" | |
} | |
Called when a characteristic's value changes, *after* `BluetoothRemoteGATTCharacteristic.startNotifications` has been called. | |
``` | |
... | |
return service.getCharacteristic("characteristic_uuid"); | |
}).then(function(c) { | |
c.on('characteristicvaluechanged', function(event) { | |
console.log("-> "+event.target.value); | |
}); | |
return c.startNotifications(); | |
}).then(... | |
``` | |
The first argument is of the form `{target : BluetoothRemoteGATTCharacteristic}`, and `BluetoothRemoteGATTCharacteristic.value` | |
will then contain the new value (as a DataView). | |
*/ | |
/*JSON{ | |
"type" : "object", | |
"name" : "Bluetooth", | |
"instanceof" : "Serial", | |
"ifdef" : "BLUETOOTH" | |
} | |
The Bluetooth Serial port - used when data is sent or received over Bluetooth Smart on nRF51/nRF52 chips. | |
*/ | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "disconnect", | |
"generate" : "jswrap_ble_disconnect" | |
} | |
If a device is connected to Espruino, disconnect from it. | |
*/ | |
void jswrap_ble_disconnect() { | |
uint32_t err_code; | |
if (jsble_has_peripheral_connection()) { | |
err_code = jsble_disconnect(m_peripheral_conn_handle); | |
jsble_check_error(err_code); | |
} | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "sleep", | |
"generate" : "jswrap_ble_sleep" | |
} | |
Disable Bluetooth advertising and disconnect from any device that | |
connected to Puck.js as a peripheral (this won't affect any devices | |
that Puck.js initiated connections to). | |
This makes Puck.js undiscoverable, so it can't be connected to. | |
Use `NRF.wake()` to wake up and make Puck.js connectable again. | |
*/ | |
void jswrap_ble_sleep() { | |
// set as sleeping | |
bleStatus |= BLE_IS_SLEEPING; | |
// stop advertising | |
jsble_advertising_stop(); | |
// If connected, disconnect. | |
// when we disconnect, we'll see BLE_IS_SLEEPING and won't advertise | |
jswrap_ble_disconnect(); | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "wake", | |
"generate" : "jswrap_ble_wake" | |
} | |
Enable Bluetooth advertising (this is enabled by default), which | |
allows other devices to discover and connect to Puck.js. | |
Use `NRF.sleep()` to disable advertising. | |
*/ | |
void jswrap_ble_wake() { | |
bleStatus &= ~BLE_IS_SLEEPING; | |
jsble_check_error(jsble_advertising_start()); | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "restart", | |
"generate" : "jswrap_ble_restart", | |
"params" : [ | |
["callback","JsVar","An optional function to be called while the softdevice is uninitialised. Use with caution - accessing console/bluetooth will almost certainly result in a crash."] | |
] | |
} | |
Restart the Bluetooth softdevice (if there is currently a BLE connection, | |
it will queue a restart to be done when the connection closes). | |
You shouldn't need to call this function in normal usage. However, Nordic's | |
BLE softdevice has some settings that cannot be reset. For example there | |
are only a certain number of unique UUIDs. Once these are all used the | |
only option is to restart the softdevice to clear them all out. | |
*/ | |
void jswrap_ble_restart(JsVar *callback) { | |
if (jsble_has_connection()) { | |
jsiConsolePrintf("BLE Connected, queueing BLE restart for later\n"); | |
bleStatus |= BLE_NEEDS_SOFTDEVICE_RESTART; | |
return; | |
} else { | |
// Not connected, so we can restart now | |
jsble_restart_softdevice(jsvIsFunction(callback)?callback:NULL); | |
return; | |
} | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "getAddress", | |
"generate" : "jswrap_ble_getAddress", | |
"return" : ["JsVar", "MAC address - a string of the form 'aa:bb:cc:dd:ee:ff'" ] | |
} | |
Get this device's default Bluetooth MAC address. | |
For Puck.js, the last 5 characters of this (eg. `ee:ff`) | |
are used in the device's advertised Bluetooth name. | |
*/ | |
JsVar *jswrap_ble_getAddress() { | |
#ifdef NRF5X | |
uint32_t addr0 = NRF_FICR->DEVICEADDR[0]; | |
uint32_t addr1 = NRF_FICR->DEVICEADDR[1]; | |
#else | |
uint32_t addr0 = 0xDEADDEAD; | |
uint32_t addr1 = 0xDEAD; | |
#endif | |
return jsvVarPrintf("%02x:%02x:%02x:%02x:%02x:%02x", | |
((addr1>>8 )&0xFF)|0xC0, | |
((addr1 )&0xFF), | |
((addr0>>24)&0xFF), | |
((addr0>>16)&0xFF), | |
((addr0>>8 )&0xFF), | |
((addr0 )&0xFF)); | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "setAddress", | |
"#if" : "defined(NRF52_SERIES)", | |
"generate" : "jswrap_ble_setAddress", | |
"params" : [ | |
["addr","JsVar","The address to use (as a string)"] | |
] | |
} | |
Set this device's default Bluetooth MAC address: | |
``` | |
NRF.setAddress("ff:ee:dd:cc:bb:aa random"); | |
``` | |
Addresses take the form: | |
* `"ff:ee:dd:cc:bb:aa"` or `"ff:ee:dd:cc:bb:aa public"` for a public address | |
* `"ff:ee:dd:cc:bb:aa random"` for a random static address (the default for Espruino) | |
This may throw a `INVALID_BLE_ADDR` error if the upper two bits | |
of the address don't match the address type. | |
To change the address, Espruino must restart the softdevice. It will only do | |
so when it is disconnected from other devices. | |
*/ | |
void jswrap_ble_setAddress(JsVar *address) { | |
#ifdef NRF52_SERIES | |
ble_gap_addr_t p_addr; | |
if (!bleVarToAddr(address, &p_addr)) { | |
jsExceptionHere(JSET_ERROR, "Expecting a mac address of the form aa:bb:cc:dd:ee:ff"); | |
return; | |
} | |
jsvObjectSetChild(execInfo.hiddenRoot, BLE_NAME_MAC_ADDRESS, address); | |
jswrap_ble_restart(NULL); | |
#else | |
jsExceptionHere(JSET_ERROR, "Not implemented"); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "getBattery", | |
"generate" : "jswrap_ble_getBattery", | |
"return" : ["float", "Battery level in volts" ] | |
} | |
Get the battery level in volts (the voltage that the NRF chip is running off of). | |
This is the battery level of the device itself - it has nothing to with any | |
device that might be connected. | |
*/ | |
JsVarFloat jswrap_ble_getBattery() { | |
return jshReadVRef(); | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "setAdvertising", | |
"generate" : "jswrap_ble_setAdvertising", | |
"params" : [ | |
["data","JsVar","The service data to advertise as an object - see below for more info"], | |
["options","JsVar","An optional object of options"] | |
] | |
} | |
Change the data that Espruino advertises. | |
Data can be of the form `{ UUID : data_as_byte_array }`. The UUID should be | |
a [Bluetooth Service ID](https://developer.bluetooth.org/gatt/services/Pages/ServicesHome.aspx). | |
For example to return battery level at 95%, do: | |
``` | |
NRF.setAdvertising({ | |
0x180F : [95] // Service data 0x180F = 95 | |
}); | |
``` | |
Or you could report the current temperature: | |
``` | |
setInterval(function() { | |
NRF.setAdvertising({ | |
0x1809 : [Math.round(E.getTemperature())] | |
}); | |
}, 30000); | |
``` | |
If you specify a value for the object key, Service Data is advertised. However | |
if you specify `undefined`, the Service UUID is advertised: | |
``` | |
NRF.setAdvertising({ | |
0x180D : undefined // Advertise service UUID 0x180D (HRM) | |
}); | |
``` | |
Service UUIDs can also be supplied in the second argument of | |
`NRF.setServices`, but those go in the scan response packet. | |
You can also supply the raw advertising data in an array. For example | |
to advertise as an Eddystone beacon: | |
``` | |
NRF.setAdvertising([0x03, // Length of Service List | |
0x03, // Param: Service List | |
0xAA, 0xFE, // Eddystone ID | |
0x13, // Length of Service Data | |
0x16, // Service Data | |
0xAA, 0xFE, // Eddystone ID | |
0x10, // Frame type: URL | |
0xF8, // Power | |
0x03, // https:// | |
'g','o','o','.','g','l','/','B','3','J','0','O','c'], | |
{interval:100}); | |
``` | |
(However for Eddystone we'd advise that you use the [Espruino Eddystone library](/Puck.js+Eddystone)) | |
**Note:** When specifying data as an array, certain advertising options such as | |
`discoverable` and `showName` won't have any effect. | |
**Note:** The size of Bluetooth LE advertising packets is limited to 31 bytes. If | |
you want to advertise more data, consider using an array for `data` (See below), or | |
`NRF.setScanResponse`. | |
You can even specify an array of arrays or objects, in which case each advertising packet | |
will be used in turn - for instance to make your device advertise battery level and its name | |
as well as both Eddystone and iBeacon : | |
``` | |
NRF.setAdvertising([ | |
{0x180F : [Puck.getBatteryPercentage()]}, // normal advertising, with battery % | |
require("ble_ibeacon").get(...), // iBeacon | |
require("ble_eddystone").get(...), // eddystone | |
], {interval:300}); | |
``` | |
`options` is an object, which can contain: | |
``` | |
{ | |
name: "Hello" // The name of the device | |
showName: true/false // include full name, or nothing | |
discoverable: true/false // general discoverable, or limited - default is limited | |
connectable: true/false // whether device is connectable - default is true | |
scannable : true/false // whether device can be scanned for scan response packets - default is true | |
interval: 600 // Advertising interval in msec, between 20 and 10000 (default is 375ms) | |
manufacturer: 0x0590 // IF sending manufacturer data, this is the manufacturer ID | |
manufacturerData: [...] // IF sending manufacturer data, this is an array of data | |
} | |
``` | |
Setting `connectable` and `scannable` to false gives the lowest power consumption | |
as the BLE radio doesn't have to listen after sending advertising. | |
**NOTE:** Non-`connectable` advertising can't have an advertising interval less than 100ms | |
according to the BLE spec. | |
So for instance to set the name of Puck.js without advertising any | |
other data you can just use the command: | |
``` | |
NRF.setAdvertising({},{name:"Hello"}); | |
``` | |
You can also specify 'manufacturer data', which is another form of advertising data. | |
We've registered the Manufacturer ID 0x0590 (as Pur3 Ltd) for use with *Official | |
Espruino devices* - use it to advertise whatever data you'd like, but we'd recommend | |
using JSON. | |
For example by not advertising a device name you can send up to 24 bytes of JSON on | |
Espruino's manufacturer ID: | |
``` | |
var data = {a:1,b:2}; | |
NRF.setAdvertising({},{ | |
showName:false, | |
manufacturer:0x0590, | |
manufacturerData:JSON.stringify(data) | |
}); | |
``` | |
If you're using [EspruinoHub](https://github.com/espruino/EspruinoHub) then it will | |
automatically decode this into the folling MQTT topics: | |
* `/ble/advertise/ma:c_:_a:dd:re:ss/espruino` -> `{"a":10,"b":15}` | |
* `/ble/advertise/ma:c_:_a:dd:re:ss/a` -> `1` | |
* `/ble/advertise/ma:c_:_a:dd:re:ss/b` -> `2` | |
Note that **you only have 24 characters available for JSON**, so try to use | |
the shortest field names possible and avoid floating point values that can | |
be very long when converted to a String. | |
*/ | |
void jswrap_ble_setAdvertising(JsVar *data, JsVar *options) { | |
uint32_t err_code = 0; | |
bool isAdvertising = bleStatus & BLE_IS_ADVERTISING; | |
if (jsvIsObject(options)) { | |
JsVar *v; | |
v = jsvObjectGetChild(options, "interval", 0); | |
if (v) { | |
uint16_t new_advertising_interval = MSEC_TO_UNITS(jsvGetIntegerAndUnLock(v), UNIT_0_625_MS); | |
if (new_advertising_interval<0x0020) new_advertising_interval=0x0020; | |
if (new_advertising_interval>0x4000) new_advertising_interval=0x4000; | |
if (new_advertising_interval != bleAdvertisingInterval) { | |
bleAdvertisingInterval = new_advertising_interval; | |
} | |
} | |
v = jsvObjectGetChild(options, "connectable", 0); | |
if (v) { | |
if (jsvGetBoolAndUnLock(v)) bleStatus &= ~BLE_IS_NOT_CONNECTABLE; | |
else bleStatus |= BLE_IS_NOT_CONNECTABLE; | |
} | |
v = jsvObjectGetChild(options, "scannable", 0); | |
if (v) { | |
if (jsvGetBoolAndUnLock(v)) bleStatus &= ~BLE_IS_NOT_SCANNABLE; | |
else bleStatus |= BLE_IS_NOT_SCANNABLE; | |
} | |
v = jsvObjectGetChild(options, "name", 0); | |
if (v) { | |
JSV_GET_AS_CHAR_ARRAY(namePtr, nameLen, v); | |
if (namePtr) { | |
#ifdef NRF5X | |
ble_gap_conn_sec_mode_t sec_mode; | |
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&sec_mode); | |
err_code = sd_ble_gap_device_name_set(&sec_mode, | |
(const uint8_t *)namePtr, | |
nameLen); | |
//#else | |
// err_code = 0xDEAD; | |
// jsiConsolePrintf("FIXME\n"); | |
#endif | |
#ifdef ESP32 | |
bluetooth_setDeviceName(v); | |
#endif | |
jsble_check_error(err_code); | |
} | |
jsvUnLock(v); | |
} | |
} else if (!jsvIsUndefined(options)) { | |
jsExceptionHere(JSET_TYPEERROR, "Expecting 'options' to be object or undefined, got %t", options); | |
return; | |
} | |
JsVar *advArray = 0; | |
if (jsvIsObject(data) || jsvIsUndefined(data)) { | |
// if it's an object, work out what the advertising data for it is | |
advArray = jswrap_ble_getAdvertisingData(data, options); | |
// if undefined, make sure we *save* undefined | |
if (jsvIsUndefined(data)) { | |
advArray = 0; | |
} | |
} else if (jsvIsArray(data)) { | |
advArray = jsvLockAgain(data); | |
// Check if it's nested arrays - if so we alternate between advertising types | |
bleStatus &= ~(BLE_IS_ADVERTISING_MULTIPLE|BLE_ADVERTISING_MULTIPLE_MASK); | |
// check for nested, and if so then preconvert the objects into arrays | |
bool isNested = false; | |
int elements = 0; | |
JsvObjectIterator it; | |
jsvObjectIteratorNew(&it, advArray); | |
while (jsvObjectIteratorHasValue(&it)) { | |
JsVar *v = jsvObjectIteratorGetValue(&it); | |
if (jsvIsObject(v) || jsvIsUndefined(v)) { | |
JsVar *newv = jswrap_ble_getAdvertisingData(v, options); | |
jsvObjectIteratorSetValue(&it, newv); | |
jsvUnLock(newv); | |
isNested = true; | |
} else if (jsvIsArray(v) || jsvIsArrayBuffer(v)) { | |
isNested = true; | |
if (jsvIsArray(v)) { | |
/* don't store sparse arrays for advertising data. It's inefficient but also | |
in SWI1_IRQHandler they need decoding which is slow *and* will cause jsvNew... to be | |
called, which may interfere with what happens in the main thread (eg. GC). | |
Instead convert them to ArrayBuffers */ | |
uint8_t advdata[BLE_GAP_ADV_MAX_SIZE]; | |
unsigned int advdatalen = jsvIterateCallbackToBytes(v, advdata, BLE_GAP_ADV_MAX_SIZE); | |
JsVar *newv = jsvNewArrayBufferWithData(advdatalen, advdata); | |
jsvObjectIteratorSetValue(&it, newv); | |
jsvUnLock(newv); | |
} | |
} | |
elements++; | |
jsvUnLock(v); | |
jsvObjectIteratorNext(&it); | |
} | |
jsvObjectIteratorFree(&it); | |
// it's nested - set multiple advertising mode | |
if (isNested) { | |
// nested - enable multiple advertising - start at index 0 | |
if (elements>1) | |
bleStatus |= BLE_IS_ADVERTISING_MULTIPLE; | |
} | |
} else if (jsvIsArrayBuffer(data)) { | |
// it's just data - no multiple advertising | |
advArray = jsvLockAgain(data); | |
bleStatus &= ~(BLE_IS_ADVERTISING_MULTIPLE|BLE_ADVERTISING_MULTIPLE_MASK); | |
} | |
// Save the current service data | |
jsvObjectSetOrRemoveChild(execInfo.hiddenRoot, BLE_NAME_ADVERTISE_DATA, advArray); | |
jsvObjectSetOrRemoveChild(execInfo.hiddenRoot, BLE_NAME_ADVERTISE_OPTIONS, options); | |
jsvUnLock(advArray); | |
// now actually update advertising | |
if (isAdvertising) | |
jsble_advertising_stop(); | |
#ifdef ESP32 | |
err_code = bluetooth_gap_setAdvertizing(advArray); | |
#endif | |
jsble_check_error(err_code); | |
if (isAdvertising) | |
jsble_check_error(jsble_advertising_start()); // sets up advertising data again | |
} | |
/// Used by bluetooth.c internally when it needs to set up advertising at first | |
JsVar *jswrap_ble_getCurrentAdvertisingData() { | |
// This is safe if JS not initialised, jsvObjectGetChild returns 0 | |
JsVar *adv = jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_ADVERTISE_DATA, 0); | |
if (!adv) adv = jswrap_ble_getAdvertisingData(NULL, NULL); // use the defaults | |
else { | |
if (bleStatus&BLE_IS_ADVERTISING_MULTIPLE) { | |
int idx = (bleStatus&BLE_ADVERTISING_MULTIPLE_MASK)>>BLE_ADVERTISING_MULTIPLE_SHIFT; | |
JsVar *v = jsvGetArrayItem(adv, idx); | |
jsvUnLock(adv); | |
adv = v; | |
} | |
} | |
return adv; | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "getAdvertisingData", | |
"generate" : "jswrap_ble_getAdvertisingData", | |
"params" : [ | |
["data","JsVar","The data to advertise as an object"], | |
["options","JsVar","An optional object of options"] | |
], | |
"return" : ["JsVar", "An array containing the advertising data" ] | |
} | |
This is just like `NRF.setAdvertising`, except instead of advertising | |
the data, it returns the packet that would be advertised as an array. | |
*/ | |
JsVar *jswrap_ble_getAdvertisingData(JsVar *data, JsVar *options) { | |
uint32_t err_code; | |
#ifdef ESP32 | |
JsVar *r; | |
r = bluetooth_gap_getAdvertisingData(data,options); | |
return r; | |
#endif | |
#ifdef NRF5X | |
ble_advdata_t advdata; | |
jsble_setup_advdata(&advdata); | |
#endif | |
if (jsvIsObject(options)) { | |
JsVar *v; | |
#ifdef NRF5X | |
v = jsvObjectGetChild(options, "showName", 0); | |
if (v) advdata.name_type = jsvGetBoolAndUnLock(v) ? | |
BLE_ADVDATA_FULL_NAME : | |
BLE_ADVDATA_NO_NAME; | |
v = jsvObjectGetChild(options, "discoverable", 0); | |
if (v) advdata.flags = jsvGetBoolAndUnLock(v) ? | |
BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE : | |
BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE; | |
v = jsvObjectGetChild(options, "manufacturerData", 0); | |
if (v) { | |
JSV_GET_AS_CHAR_ARRAY(dPtr, dLen, v); | |
if (dPtr && dLen) { | |
advdata.p_manuf_specific_data = (ble_advdata_manuf_data_t*)alloca(sizeof(ble_advdata_manuf_data_t)); | |
advdata.p_manuf_specific_data->company_identifier = 0xFFFF; // pre-fill with test manufacturer data | |
advdata.p_manuf_specific_data->data.size = dLen; | |
advdata.p_manuf_specific_data->data.p_data = (uint8_t*)dPtr; | |
} | |
jsvUnLock(v); | |
} | |
v = jsvObjectGetChild(options, "manufacturer", 0); | |
if (v) { | |
if (advdata.p_manuf_specific_data) | |
advdata.p_manuf_specific_data->company_identifier = jsvGetInteger(v); | |
else | |
jsExceptionHere(JSET_TYPEERROR, "'manufacturer' specified without 'manufacturerdata'"); | |
jsvUnLock(v); | |
} | |
#endif | |
} else if (!jsvIsUndefined(options)) { | |
jsExceptionHere(JSET_TYPEERROR, "Expecting 'options' to be object or undefined, got %t", options); | |
return 0; | |
} | |
if (jsvIsArray(data) || jsvIsArrayBuffer(data)) { | |
return jsvLockAgain(data); | |
} else if (jsvIsObject(data)) { | |
#ifdef NRF5X | |
// we may not use all of service_data/adv_uuids - but allocate the max we can | |
int maxServices = jsvGetChildren(data); | |
ble_advdata_service_data_t *service_data = (ble_advdata_service_data_t*)alloca(maxServices*sizeof(ble_advdata_service_data_t)); | |
int service_data_cnt = 0; | |
ble_uuid_t *adv_uuid = (ble_uuid_t*)alloca(maxServices*sizeof(ble_uuid_t)); | |
int adv_uuid_cnt = 0; | |
if (maxServices && (!service_data || !adv_uuid)) | |
return 0; // allocation error | |
#endif | |
JsvObjectIterator it; | |
jsvObjectIteratorNew(&it, data); | |
while (jsvObjectIteratorHasValue(&it)) { | |
JsVar *v = jsvObjectIteratorGetValue(&it); | |
JSV_GET_AS_CHAR_ARRAY(dPtr, dLen, v); | |
const char *errorStr; | |
ble_uuid_t ble_uuid; | |
if ((errorStr=bleVarToUUIDAndUnLock(&ble_uuid, jsvObjectIteratorGetKey(&it)))) { | |
jsExceptionHere(JSET_ERROR, "Invalid Service UUID: %s", errorStr); | |
break; | |
} | |
#ifdef NRF5X | |
if (jsvIsUndefined(v)) { | |
adv_uuid[adv_uuid_cnt] = ble_uuid; | |
adv_uuid_cnt++; | |
} else { | |
service_data[service_data_cnt].service_uuid = ble_uuid.uuid; | |
service_data[service_data_cnt].data.size = dLen; | |
service_data[service_data_cnt].data.p_data = (uint8_t*)dPtr; | |
service_data_cnt++; | |
} | |
#endif | |
jsvUnLock(v); | |
jsvObjectIteratorNext(&it); | |
} | |
jsvObjectIteratorFree(&it); | |
#ifdef NRF5X | |
advdata.service_data_count = service_data_cnt; | |
advdata.p_service_data_array = service_data; | |
advdata.uuids_complete.uuid_cnt = adv_uuid_cnt; | |
advdata.uuids_complete.p_uuids = adv_uuid; | |
#endif | |
} else if (!jsvIsUndefined(data)) { | |
jsExceptionHere(JSET_TYPEERROR, "Expecting object, array or undefined, got %t", data); | |
return 0; | |
} | |
#if ESPR_BLUETOOTH_ANCS | |
if (bleStatus & BLE_ANCS_OR_AMS_INITED) { | |
static ble_uuid_t m_adv_uuids[1]; /**< Universally unique service identifiers. */ | |
ble_ancs_get_adv_uuid(m_adv_uuids); | |
advdata.uuids_solicited.uuid_cnt = sizeof(m_adv_uuids) / sizeof(m_adv_uuids[0]); | |
advdata.uuids_solicited.p_uuids = m_adv_uuids; | |
} | |
#endif | |
uint16_t len_advdata = BLE_GAP_ADV_MAX_SIZE; | |
uint8_t encoded_advdata[BLE_GAP_ADV_MAX_SIZE]; | |
#ifdef NRF5X | |
#if NRF_SD_BLE_API_VERSION<5 | |
err_code = adv_data_encode(&advdata, encoded_advdata, &len_advdata); | |
#else | |
err_code = ble_advdata_encode(&advdata, encoded_advdata, &len_advdata); | |
#endif | |
#else | |
err_code = 0xDEAD; | |
jsiConsolePrintf("FIXME\n"); | |
#endif | |
if (err_code && !execInfo.hiddenRoot) return 0; // don't error if JS not initialised | |
if (jsble_check_error(err_code)) return 0; | |
return jsvNewArrayBufferWithData(len_advdata, encoded_advdata); | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "setScanResponse", | |
"generate" : "jswrap_ble_setScanResponse", | |
"params" : [ | |
["data","JsVar","The data to for the scan response"] | |
] | |
} | |
The raw scan response data should be supplied as an array. For example to return "Sample" for the device name: | |
``` | |
NRF.setScanResponse([0x07, // Length of Data | |
0x09, // Param: Complete Local Name | |
'S', 'a', 'm', 'p', 'l', 'e']); | |
``` | |
**Note:** `NRF.setServices(..., {advertise:[ ... ]})` writes advertised | |
services into the scan response - so you can't use both `advertise` | |
and `NRF.setServices` or one will overwrite the other. | |
*/ | |
void jswrap_ble_setScanResponse(JsVar *data) { | |
uint32_t err_code = 0; | |
if (jsvIsUndefined(data)) { | |
jsvObjectRemoveChild(execInfo.hiddenRoot, BLE_NAME_SCAN_RESPONSE_DATA); | |
} else if (jsvIsArray(data) || jsvIsArrayBuffer(data)) { | |
JSV_GET_AS_CHAR_ARRAY(respPtr, respLen, data); | |
if (!respPtr) { | |
jsExceptionHere(JSET_TYPEERROR, "Unable to convert data argument to an array"); | |
return; | |
} | |
// only set data if we managed to decode it ok | |
jsvObjectSetOrRemoveChild(execInfo.hiddenRoot, BLE_NAME_SCAN_RESPONSE_DATA, data); | |
#ifdef NRF5X | |
#if NRF_SD_BLE_API_VERSION<5 | |
err_code = sd_ble_gap_adv_data_set(NULL, 0, (uint8_t *)respPtr, respLen); | |
#else | |
extern uint8_t m_adv_handle; | |
// Get existing advertising data as on SDK15 we have to be able to re-supply this | |
// when changing the scan response | |
JsVar *advData = jswrap_ble_getCurrentAdvertisingData(); | |
JSV_GET_AS_CHAR_ARRAY(advPtr, advLen, advData); | |
ble_gap_adv_data_t d; | |
d.adv_data.p_data = (uint8_t *)advPtr; | |
d.adv_data.len = advLen; | |
d.scan_rsp_data.p_data = (uint8_t *)respPtr; | |
d.scan_rsp_data.len = respLen; | |
if (bleStatus & BLE_IS_ADVERTISING) sd_ble_gap_adv_stop(m_adv_handle); | |
err_code = sd_ble_gap_adv_set_configure(&m_adv_handle, &d, NULL); | |
if (bleStatus & BLE_IS_ADVERTISING) sd_ble_gap_adv_start(m_adv_handle, APP_BLE_CONN_CFG_TAG); | |
jsvUnLock(advData); | |
#endif | |
#else | |
err_code = 0xDEAD; | |
jsiConsolePrintf("FIXME\n"); | |
#endif | |
jsble_check_error(err_code); | |
} else { | |
jsExceptionHere(JSET_TYPEERROR, "Expecting array-like object or undefined, got %t", data); | |
} | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "setServices", | |
"generate" : "jswrap_ble_setServices", | |
"params" : [ | |
["data","JsVar","The service (and characteristics) to advertise"], | |
["options","JsVar","Optional object containing options"] | |
] | |
} | |
Change the services and characteristics Espruino advertises. | |
If you want to **change** the value of a characteristic, you need | |
to use `NRF.updateServices()` instead | |
To expose some information on Characteristic `ABCD` on service `BCDE` you could do: | |
``` | |
NRF.setServices({ | |
0xBCDE : { | |
0xABCD : { | |
value : "Hello", | |
readable : true | |
} | |
} | |
}); | |
``` | |
Or to allow the 3 LEDs to be controlled by writing numbers 0 to 7 to a | |
characteristic, you can do the following. `evt.data` is an ArrayBuffer. | |
``` | |
NRF.setServices({ | |
0xBCDE : { | |
0xABCD : { | |
writable : true, | |
onWrite : function(evt) { | |
digitalWrite([LED3,LED2,LED1], evt.data[0]); | |
} | |
} | |
} | |
}); | |
``` | |
You can supply many different options: | |
``` | |
NRF.setServices({ | |
0xBCDE : { | |
0xABCD : { | |
value : "Hello", // optional | |
maxLen : 5, // optional (otherwise is length of initial value) | |
broadcast : false, // optional, default is false | |
readable : true, // optional, default is false | |
writable : true, // optional, default is false | |
notify : true, // optional, default is false | |
indicate : true, // optional, default is false | |
description: "My Characteristic", // optional, default is null, | |
security: { // optional - see NRF.setSecurity | |
read: { // optional | |
encrypted: false, // optional, default is false | |
mitm: false, // optional, default is false | |
lesc: false, // optional, default is false | |
signed: false // optional, default is false | |
}, | |
write: { // optional | |
encrypted: true, // optional, default is false | |
mitm: false, // optional, default is false | |
lesc: false, // optional, default is false | |
signed: false // optional, default is false | |
} | |
}, | |
onWrite : function(evt) { // optional | |
console.log("Got ", evt.data); // an ArrayBuffer | |
} | |
} | |
// more characteristics allowed | |
} | |
// more services allowed | |
}); | |
``` | |
**Note:** UUIDs can be integers between `0` and `0xFFFF`, strings of | |
the form `"ABCD"`, or strings of the form `"ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD"` | |
`options` can be of the form: | |
``` | |
NRF.setServices(undefined, { | |
hid : new Uint8Array(...), // optional, default is undefined. Enable BLE HID support | |
uart : true, // optional, default is true. Enable BLE UART support | |
advertise: [ '180D' ] // optional, list of service UUIDs to advertise | |
ancs : true, // optional, Bangle.js-only, enable Apple ANCS support for notifications | |
ams : true // optional, Bangle.js-only, enable Apple AMS support for media control | |
}); | |
``` | |
To enable BLE HID, you must set `hid` to an array which is the BLE report | |
descriptor. The easiest way to do this is to use the `ble_hid_controls` | |
or `ble_hid_keyboard` modules. | |
**Note:** Just creating a service doesn't mean that the service will | |
be advertised. It will only be available after a device connects. To | |
advertise, specify the UUIDs you wish to advertise in the `advertise` | |
field of the second `options` argument. For example this will create | |
and advertise a heart rate service: | |
``` | |
NRF.setServices({ | |
0x180D: { // heart_rate | |
0x2A37: { // heart_rate_measurement | |
notify: true, | |
value : [0x06, heartrate], | |
} | |
} | |
}, { advertise: [ '180D' ] }); | |
``` | |
You may specify 128 bit UUIDs to advertise, however you may get a `DATA_SIZE` | |
exception because there is insufficient space in the Bluetooth LE advertising | |
packet for the 128 bit UART UUID as well as the UUID you specified. In this | |
case you can add `uart:false` after the `advertise` element to disable the | |
UART, however you then be unable to connect to Puck.js's console via Bluetooth. | |
If you absolutely require two or more 128 bit UUIDs then you will have to | |
specify your own raw advertising data packets with `NRF.setAdvertising` | |
**Note:** The services on Espruino can only be modified when there is | |
no device connected to it as it requires a restart of the Bluetooth stack. | |
**iOS devices will 'cache' the list of services** so apps like | |
NRF Connect may incorrectly display the old services even after you | |
have modified them. To fix this, disable and re-enable Bluetooth on your | |
iOS device, or use an Android device to run NRF Connect. | |
**Note:** Not all combinations of security configuration values are valid, the valid combinations are: encrypted, | |
encrypted + mitm, lesc, signed, signed + mitm. See `NRF.setSecurity` for more information. | |
*/ | |
void jswrap_ble_setServices(JsVar *data, JsVar *options) { | |
if (!(jsvIsObject(data) || jsvIsUndefined(data))) { | |
jsExceptionHere(JSET_TYPEERROR, "Expecting object or undefined, got %t", data); | |
return; | |
} | |
#if BLE_HIDS_ENABLED | |
JsVar *use_hid = 0; | |
#endif | |
bool use_uart = true; | |
#if ESPR_BLUETOOTH_ANCS | |
bool use_ancs = false; | |
bool use_ams = false; | |
#endif | |
JsVar *advertise = 0; | |
jsvConfigObject configs[] = { | |
#if BLE_HIDS_ENABLED | |
{"hid", JSV_ARRAY, &use_hid}, | |
#endif | |
{"uart", JSV_BOOLEAN, &use_uart}, | |
#if ESPR_BLUETOOTH_ANCS | |
{"ancs", JSV_BOOLEAN, &use_ancs}, | |
{"ams", JSV_BOOLEAN, &use_ams}, | |
#endif | |
{"advertise", JSV_ARRAY, &advertise}, | |
}; | |
if (!jsvReadConfigObject(options, configs, sizeof(configs) / sizeof(jsvConfigObject))) { | |
return; | |
} | |
#if BLE_HIDS_ENABLED | |
// Handle turning on/off of HID | |
if (jsvIsIterable(use_hid)) { | |
jsvObjectSetChild(execInfo.hiddenRoot, BLE_NAME_HID_DATA, use_hid); | |
bleStatus |= BLE_NEEDS_SOFTDEVICE_RESTART; | |
} else if (!use_hid) { | |
jsvObjectRemoveChild(execInfo.hiddenRoot, BLE_NAME_HID_DATA); | |
if (bleStatus & BLE_HID_INITED) | |
bleStatus |= BLE_NEEDS_SOFTDEVICE_RESTART; | |
} else { | |
jsExceptionHere(JSET_TYPEERROR, "'hid' must be undefined, or an array"); | |
} | |
jsvUnLock(use_hid); | |
#endif | |
if (use_uart) { | |
if (!(bleStatus & BLE_NUS_INITED)) | |
bleStatus |= BLE_NEEDS_SOFTDEVICE_RESTART; | |
jsvObjectRemoveChild(execInfo.hiddenRoot, BLE_NAME_NUS); | |
} else { | |
if (bleStatus & BLE_NUS_INITED) | |
bleStatus |= BLE_NEEDS_SOFTDEVICE_RESTART; | |
jsvObjectSetChildAndUnLock(execInfo.hiddenRoot, BLE_NAME_NUS, jsvNewFromBool(false)); | |
} | |
#if ESPR_BLUETOOTH_ANCS | |
if (use_ancs) { | |
if (!(bleStatus & BLE_ANCS_INITED)) | |
bleStatus |= BLE_NEEDS_SOFTDEVICE_RESTART; | |
jsvObjectSetChildAndUnLock(execInfo.hiddenRoot, BLE_NAME_ANCS, jsvNewFromBool(true)); | |
} else { | |
if (bleStatus & BLE_ANCS_INITED) | |
bleStatus |= BLE_NEEDS_SOFTDEVICE_RESTART; | |
jsvObjectRemoveChild(execInfo.hiddenRoot, BLE_NAME_ANCS); | |
} | |
if (use_ams) { | |
if (!(bleStatus & BLE_AMS_INITED)) | |
bleStatus |= BLE_NEEDS_SOFTDEVICE_RESTART; | |
jsvObjectSetChildAndUnLock(execInfo.hiddenRoot, BLE_NAME_AMS, jsvNewFromBool(true)); | |
} else { | |
if (bleStatus & BLE_AMS_INITED) | |
bleStatus |= BLE_NEEDS_SOFTDEVICE_RESTART; | |
jsvObjectRemoveChild(execInfo.hiddenRoot, BLE_NAME_AMS); | |
} | |
#endif | |
// Save the current service data and options | |
jsvObjectSetOrRemoveChild(execInfo.hiddenRoot, BLE_NAME_SERVICE_DATA, data); | |
jsvObjectSetOrRemoveChild(execInfo.hiddenRoot, BLE_NAME_SERVICE_OPTIONS, options); | |
// Service UUIDs to advertise | |
if (advertise) bleStatus|=BLE_NEEDS_SOFTDEVICE_RESTART; | |
jsvObjectSetOrRemoveChild(execInfo.hiddenRoot, BLE_NAME_SERVICE_ADVERTISE, advertise); | |
jsvUnLock(advertise); | |
// work out whether to apply changes | |
if (bleStatus & (BLE_SERVICES_WERE_SET|BLE_NEEDS_SOFTDEVICE_RESTART)) { | |
jswrap_ble_restart(NULL); | |
} else { | |
/* otherwise, we can set the services now, since we're only adding | |
* and not changing anything we don't need a restart. */ | |
jsble_set_services(data); | |
} | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "updateServices", | |
"generate" : "jswrap_ble_updateServices", | |
"params" : [ | |
["data","JsVar","The service (and characteristics) to update"] | |
] | |
} | |
Update values for the services and characteristics Espruino advertises. | |
Only services and characteristics previously declared using `NRF.setServices` are affected. | |
To update the '0xABCD' characteristic in the '0xBCDE' service: | |
``` | |
NRF.updateServices({ | |
0xBCDE : { | |
0xABCD : { | |
value : "World" | |
} | |
} | |
}); | |
``` | |
You can also use 128 bit UUIDs, for example `"b7920001-3c1b-4b40-869f-3c0db9be80c6"`. | |
To define a service and characteristic and then notify connected clients of a | |
change to it when a button is pressed: | |
``` | |
NRF.setServices({ | |
0xBCDE : { | |
0xABCD : { | |
value : "Hello", | |
maxLen : 20, | |
notify: true | |
} | |
} | |
}); | |
setWatch(function() { | |
NRF.updateServices({ | |
0xBCDE : { | |
0xABCD : { | |
value : "World!", | |
notify: true | |
} | |
} | |
}); | |
}, BTN, { repeat:true, edge:"rising", debounce: 50 }); | |
``` | |
This only works if the characteristic was created with `notify: true` using `NRF.setServices`, | |
otherwise the characteristic will be updated but no notification will be sent. | |
Also note that `maxLen` was specified. If it wasn't then the maximum length of | |
the characteristic would have been 5 - the length of `"Hello"`. | |
To indicate (i.e. notify with ACK) connected clients of a change to the '0xABCD' characteristic in the '0xBCDE' service: | |
``` | |
NRF.updateServices({ | |
0xBCDE : { | |
0xABCD : { | |
value : "World", | |
indicate: true | |
} | |
} | |
}); | |
``` | |
This only works if the characteristic was created with `indicate: true` using `NRF.setServices`, | |
otherwise the characteristic will be updated but no notification will be sent. | |
**Note:** See `NRF.setServices` for more information | |
*/ | |
void jswrap_ble_updateServices(JsVar *data) { | |
uint32_t err_code; | |
bool ok = true; | |
if (bleStatus & BLE_NEEDS_SOFTDEVICE_RESTART) { | |
jsExceptionHere(JSET_ERROR, "Can't update services until BLE restart"); | |
/* TODO: We could conceivably update hiddenRoot->BLE_NAME_SERVICE_DATA so that | |
when the softdevice restarts it contains the updated data, but this seems like | |
overkill and potentially could cause nasty hidden bugs. */ | |
return; | |
} | |
#ifdef NRF5X | |
jsble_peripheral_activity(); // flag that we've been busy | |
#endif | |
if (jsvIsObject(data)) { | |
JsvObjectIterator it; | |
jsvObjectIteratorNew(&it, data); | |
while (jsvObjectIteratorHasValue(&it)) { | |
ble_uuid_t ble_uuid; | |
memset(&ble_uuid, 0, sizeof(ble_uuid)); | |
const char *errorStr; | |
if ((errorStr = bleVarToUUIDAndUnLock(&ble_uuid, | |
jsvObjectIteratorGetKey(&it)))) { | |
jsExceptionHere(JSET_ERROR, "Invalid Service UUID: %s", errorStr); | |
break; | |
} | |
JsVar *serviceVar = jsvObjectIteratorGetValue(&it); | |
JsvObjectIterator serviceit; | |
jsvObjectIteratorNew(&serviceit, serviceVar); | |
while (ok && jsvObjectIteratorHasValue(&serviceit)) { | |
ble_uuid_t char_uuid; | |
if ((errorStr = bleVarToUUIDAndUnLock(&char_uuid, | |
jsvObjectIteratorGetKey(&serviceit)))) { | |
jsExceptionHere(JSET_ERROR, "Invalid Characteristic UUID: %s", | |
errorStr); | |
break; | |
} | |
uint16_t char_handle = bleGetGATTHandle(char_uuid); | |
if (char_handle != BLE_GATT_HANDLE_INVALID) { | |
JsVar *charVar = jsvObjectIteratorGetValue(&serviceit); | |
JsVar *charValue = jsvObjectGetChild(charVar, "value", 0); | |
bool notification_requested = jsvGetBoolAndUnLock(jsvObjectGetChild(charVar, "notify", 0)); | |
bool indication_requested = jsvGetBoolAndUnLock(jsvObjectGetChild(charVar, "indicate", 0)); | |
if (charValue) { | |
JSV_GET_AS_CHAR_ARRAY(vPtr, vLen, charValue); | |
if (vPtr && vLen) { | |
#ifdef NRF5X | |
ble_gatts_hvx_params_t hvx_params; | |
ble_gatts_value_t gatts_value; | |
// Update the value for subsequent reads even if no client is currently connected | |
memset(&gatts_value, 0, sizeof(gatts_value)); | |
gatts_value.len = vLen; | |
gatts_value.offset = 0; | |
gatts_value.p_value = (uint8_t*)vPtr; | |
err_code = sd_ble_gatts_value_set(m_peripheral_conn_handle, char_handle, &gatts_value); | |
if (jsble_check_error(err_code)) { | |
ok = false; | |
} if ((notification_requested || indication_requested) && jsble_has_peripheral_connection()) { | |
// Notify/indicate connected clients if necessary | |
memset(&hvx_params, 0, sizeof(hvx_params)); | |
uint16_t len = (uint16_t)vLen; | |
hvx_params.handle = char_handle; | |
hvx_params.type = indication_requested ? BLE_GATT_HVX_INDICATION : BLE_GATT_HVX_NOTIFICATION; | |
hvx_params.offset = 0; | |
hvx_params.p_len = &len; | |
hvx_params.p_data = (uint8_t*)vPtr; | |
err_code = sd_ble_gatts_hvx(m_peripheral_conn_handle, &hvx_params); | |
if ((err_code != NRF_SUCCESS) | |
&& (err_code != NRF_ERROR_INVALID_STATE) | |
#if NRF_SD_BLE_API_VERSION<5 | |
&& (err_code != BLE_ERROR_NO_TX_PACKETS) | |
#else | |
&& (err_code != NRF_ERROR_RESOURCES) | |
#endif | |
&& (err_code != BLE_ERROR_GATTS_SYS_ATTR_MISSING)) { | |
if (jsble_check_error(err_code)) | |
ok = false; | |
} | |
} | |
#endif | |
} | |
} | |
jsvUnLock(charValue); | |
jsvUnLock(charVar); | |
} else { | |
JsVar *str = bleUUIDToStr(char_uuid); | |
jsExceptionHere(JSET_ERROR, "Unable to find service with UUID %v", str); | |
jsvUnLock(str); | |
} | |
jsvObjectIteratorNext(&serviceit); | |
} | |
jsvObjectIteratorFree(&serviceit); | |
jsvUnLock(serviceVar); | |
jsvObjectIteratorNext(&it); | |
} | |
jsvObjectIteratorFree(&it); | |
} else if (!jsvIsUndefined(data)) { | |
jsExceptionHere(JSET_TYPEERROR, "Expecting object or undefined, got %t", data); | |
} | |
} | |
/// Filter device based on a list of filters (like .requestDevice. Return true if it matches ANY of the filters | |
bool jswrap_ble_filter_device(JsVar *filters, JsVar *device) { | |
bool matches = false; | |
JsvObjectIterator fit; | |
jsvObjectIteratorNew(&fit, filters); | |
while (!matches && jsvObjectIteratorHasValue(&fit)) { | |
JsVar *filter = jsvObjectIteratorGetValue(&fit); | |
matches = true; | |
JsVar *v; | |
if ((v = jsvObjectGetChild(filter, "services", 0))) { | |
// Find one service in the device's service | |
JsVar *deviceServices = jsvObjectGetChild(device, "services", 0); | |
JsvObjectIterator it; | |
jsvObjectIteratorNew(&it, v); | |
while (jsvObjectIteratorHasValue(&it)) { | |
bool foundService = false; | |
if (deviceServices) { | |
JsVar *uservice = jsvObjectIteratorGetValue(&it); | |
ble_uuid_t userviceUuid; | |
bleVarToUUIDAndUnLock(&userviceUuid, uservice); | |
JsvObjectIterator dit; | |
jsvObjectIteratorNew(&dit, deviceServices); | |
while (jsvObjectIteratorHasValue(&dit)) { | |
JsVar *deviceService = jsvObjectIteratorGetValue(&dit); | |
ble_uuid_t deviceServiceUuid; | |
bleVarToUUIDAndUnLock(&deviceServiceUuid, deviceService); | |
if (bleUUIDEqual(userviceUuid, deviceServiceUuid)) | |
foundService = true; | |
jsvObjectIteratorNext(&dit); | |
} | |
jsvObjectIteratorFree(&dit); | |
} | |
if (!foundService) matches = false; | |
jsvObjectIteratorNext(&it); | |
} | |
jsvObjectIteratorFree(&it); | |
jsvUnLock2(v, deviceServices); | |
} | |
if ((v = jsvObjectGetChild(filter, "name", 0))) { | |
// match name exactly | |
JsVar *deviceName = jsvObjectGetChild(device, "name", 0); | |
if (!jsvIsEqual(v, deviceName)) | |
matches = false; | |
jsvUnLock2(v, deviceName); | |
} | |
if ((v = jsvObjectGetChild(filter, "namePrefix", 0))) { | |
// match start of name | |
JsVar *deviceName = jsvObjectGetChild(device, "name", 0); | |
if (!jsvIsString(v) || | |
!jsvIsString(deviceName) || | |
jsvGetStringLength(v)>jsvGetStringLength(deviceName) || | |
jsvCompareString(v, deviceName,0,0,true)!=0) | |
matches = false; | |
jsvUnLock2(v, deviceName); | |
} | |
// Non-standard 'id' element | |
if ((v = jsvObjectGetChild(filter, "id", 0))) { | |
JsVar *w = jsvObjectGetChild(device, "id", 0); | |
if (!jsvIsBasicVarEqual(v,w)) | |
matches = false; | |
jsvUnLock2(v,w); | |
} | |
// match service data | |
if ((v = jsvObjectGetChild(filter, "serviceData", 0))) { | |
if (jsvIsObject(v)) { | |
JsvObjectIterator it; | |
jsvObjectIteratorNew(&it,v); | |
while (jsvObjectIteratorHasValue(&it)) { | |
JsVar *childName = jsvObjectIteratorGetKey(&it); | |
JsVar *serviceData = jsvObjectGetChild(device, "serviceData", 0); | |
if (!serviceData) matches = false; | |
else { | |
JsVar *child = jsvFindChildFromVar(serviceData, childName, false); | |
if (!child) matches = false; | |
jsvUnLock(child); | |
} | |
jsvUnLock2(childName, serviceData); | |
jsvObjectIteratorNext(&it); | |
} | |
jsvObjectIteratorFree(&it); | |
} | |
jsvUnLock(v); | |
} | |
// match manufacturer data | |
if ((v = jsvObjectGetChild(filter, "manufacturerData", 0))) { | |
if (jsvIsObject(v)) { | |
JsvObjectIterator it; | |
jsvObjectIteratorNew(&it,v); | |
while (jsvObjectIteratorHasValue(&it)) { | |
JsVar* manfacturera = jsvObjectIteratorGetKey(&it); | |
JsVar* manfacturerb = jsvObjectGetChild(device, "manufacturer", 0); | |
if (!jsvIsBasicVarEqual(manfacturera, manfacturerb)) | |
matches = false; | |
jsvUnLock2(manfacturera, manfacturerb); | |
jsvObjectIteratorNext(&it); | |
} | |
jsvObjectIteratorFree(&it); | |
} | |
jsvUnLock(v); | |
} | |
// check if all ok | |
jsvUnLock(filter); | |
jsvObjectIteratorNext(&fit); | |
} | |
jsvObjectIteratorFree(&fit); | |
return matches; | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "setScan", | |
"generate" : "jswrap_ble_setScan", | |
"params" : [ | |
["callback","JsVar","The callback to call with received advertising packets, or undefined to stop"], | |
["options","JsVar","An optional object `{filters: ...}` (as would be passed to `NRF.requestDevice`) to filter devices by"] | |
] | |
} | |
Start/stop listening for BLE advertising packets within range. Returns a | |
`BluetoothDevice` for each advertsing packet. **By default this is not an active scan, so | |
Scan Response advertising data is not included (see below)** | |
``` | |
// Start scanning | |
packets=10; | |
NRF.setScan(function(d) { | |
packets--; | |
if (packets<=0) | |
NRF.setScan(); // stop scanning | |
else | |
console.log(d); // print packet info | |
}); | |
``` | |
Each `BluetoothDevice` will look a bit like: | |
``` | |
BluetoothDevice { | |
"id": "aa:bb:cc:dd:ee:ff", // address | |
"rssi": -89, // signal strength | |
"services": [ "128bit-uuid", ... ], // zero or more service UUIDs | |
"data": new Uint8Array([ ... ]).buffer, // ArrayBuffer of returned data | |
"serviceData" : { "0123" : [ 1 ] }, // if service data is in 'data', it's extracted here | |
"manufacturer" : 0x1234, // if manufacturer data is in 'data', the 16 bit manufacturer ID is extracted here | |
"manufacturerData" : [...], // if manufacturer data is in 'data', the data is extracted here | |
"name": "DeviceName" // the advertised device name | |
} | |
``` | |
You can also supply a set of filters (as described in `NRF.requestDevice`) as a second argument, which will | |
allow you to filter the devices you get a callback for. This helps | |
to cut down on the time spent processing JavaScript code in areas with | |
a lot of Bluetooth advertisements. For example to find only devices | |
with the manufacturer data `0x0590` (Espruino's ID) you could do: | |
``` | |
NRF.setScan(function(d) { | |
console.log(d.manufacturerData); | |
}, { filters: [{ manufacturerData:{0x0590:{}} }] }); | |
``` | |
You can also specify `active:true` in the second argument to perform | |
active scanning (this requests scan response packets) from any | |
devices it finds. | |
**Note:** Using a filter in `setScan` filters each advertising packet individually. As | |
a result, if you filter based on a service UUID and a device advertises with multiple packets | |
(or a scan response when `active:true`) only the packets matching the filter are returned. To | |
aggregate multiple packets you can use `NRF.findDevices`. | |
**Note:** BLE advertising packets can arrive quickly - faster than you'll | |
be able to print them to the console. It's best only to print a few, or | |
to use a function like `NRF.findDevices(..)` which will collate a list | |
of available devices. | |
**Note:** Using setScan turns the radio's receive mode on constantly. This | |
can draw a *lot* of power (12mA or so), so you should use it sparingly or | |
you can run your battery down quickly. | |
*/ | |
void jswrap_ble_setScan_cb(JsVar *callback, JsVar *filters, JsVar *adv) { | |
/* This is called when we get data - do some processing here in the main loop | |
then call the callback with it (it avoids us doing more allocations than | |
needed inside the IRQ) */ | |
if (!adv) return; | |
// Create a proper BluetoothDevice object | |
JsVar *device = jspNewObject(0, "BluetoothDevice"); | |
JsVar *deviceAddr = jsvObjectGetChild(adv, "id", 0); | |
jsvObjectSetChild(device, "id", deviceAddr); | |
jsvObjectSetChildAndUnLock(device, "rssi", jsvObjectGetChild(adv, "rssi", 0)); | |
JsVar *services = jsvNewEmptyArray(); | |
JsVar *serviceData = jsvNewObject(); | |
JsVar *data = jsvObjectGetChild(adv, "data", 0); | |
if (data) { | |
jsvObjectSetChild(device, "data", data); | |
JSV_GET_AS_CHAR_ARRAY(dPtr, dLen, data); | |
if (dPtr && dLen) { | |
if (services && serviceData) { | |
uint32_t i = 0; | |
while (i < dLen) { | |
uint8_t field_length = dPtr[i]; | |
uint8_t field_type = dPtr[i + 1]; | |
if (field_type == BLE_GAP_AD_TYPE_SHORT_LOCAL_NAME) { // 0x08 - Short Name | |
jsvObjectSetChildAndUnLock(device, "shortName", jsvNewStringOfLength(field_length-1, (char*)&dPtr[i+2])); | |
} else if (field_type == BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME) { // 0x09 - Complete Name | |
jsvObjectSetChildAndUnLock(device, "name", jsvNewStringOfLength(field_length-1, (char*)&dPtr[i+2])); | |
} else if (field_type == BLE_GAP_AD_TYPE_16BIT_SERVICE_UUID_MORE_AVAILABLE || // 0x02, 0x03 - 16 bit UUID | |
field_type == BLE_GAP_AD_TYPE_16BIT_SERVICE_UUID_COMPLETE) { | |
for (int svc_idx = 2; svc_idx < field_length + 1; svc_idx += 2) { | |
JsVar *s = jsvVarPrintf("%04x", UNALIGNED_UINT16(&dPtr[i+svc_idx])); | |
jsvArrayPushAndUnLock(services, s); | |
} | |
} else if (field_type == BLE_GAP_AD_TYPE_128BIT_SERVICE_UUID_MORE_AVAILABLE || // 0x06, 0x07 - 128 bit UUID | |
field_type == BLE_GAP_AD_TYPE_128BIT_SERVICE_UUID_COMPLETE) { | |
JsVar *s = bleUUID128ToStr((uint8_t*)&dPtr[i+2]); | |
jsvArrayPushAndUnLock(services, s); | |
} else if (field_type == BLE_GAP_AD_TYPE_SERVICE_DATA || // 0x16 - service data 16 bit UUID | |
field_type == BLE_GAP_AD_TYPE_SERVICE_DATA_128BIT_UUID) { | |
bool is128bit = field_type == BLE_GAP_AD_TYPE_SERVICE_DATA_128BIT_UUID; | |
int dataOffset; | |
JsVar *childName; | |
if (is128bit) { | |
childName = bleUUID128ToStr((uint8_t*)&dPtr[i+2]); | |
dataOffset = 2+16; | |
} else { | |
childName = jsvAsArrayIndexAndUnLock(jsvVarPrintf("%04x", UNALIGNED_UINT16(&dPtr[i+2]))); | |
dataOffset = 2+2; | |
} | |
if (childName) { | |
JsVar *child = jsvFindChildFromVar(serviceData, childName, true); | |
JsVar *value = jsvNewArrayBufferWithData(field_length+1-dataOffset, (unsigned char*)&dPtr[i+dataOffset]); | |
if (child && value) jsvSetValueOfName(child, value); | |
jsvUnLock2(child, value); | |
} | |
jsvUnLock(childName); | |
} else if (field_type == BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA) { | |
jsvObjectSetChildAndUnLock(device, "manufacturer", | |
jsvNewFromInteger((dPtr[i+3]<<8) | dPtr[i+2])); | |
jsvObjectSetChildAndUnLock(device, "manufacturerData", | |
jsvNewArrayBufferWithData(field_length-3, (unsigned char*)&dPtr[i+4])); | |
} // or unknown... | |
i += field_length + 1; | |
} | |
} | |
} | |
} | |
if (jsvGetArrayLength(services)) | |
jsvObjectSetChild(device, "services", services); | |
if (jsvGetLength(serviceData)) | |
jsvObjectSetChild(device, "serviceData", serviceData); | |
jsvUnLock3(data, services, serviceData); | |
bool deviceMatchedFilters = !filters || jswrap_ble_filter_device(filters, device); | |
/* If BLEADV exists then we're using 'NRF.findDevices' | |
https://github.com/espruino/Espruino/issues/2178 means that if we have | |
a device that advertises its name in a scan response (or other advertising packet) | |
BUT we are filtering by a service in the main advertisement, we want to pass this | |
advertisement through *if any packet from this device previously matched the filters* | |
even if this one doesn't. */ | |
if (!deviceMatchedFilters) { | |
JsVar *arr = jsvObjectGetChild(execInfo.hiddenRoot, "BLEADV", JSV_ARRAY); | |
if (arr) { | |
JsvObjectIterator it; | |
jsvObjectIteratorNew(&it, arr); | |
while (!deviceMatchedFilters && jsvObjectIteratorHasValue(&it)) { | |
JsVar *obj = jsvObjectIteratorGetValue(&it); | |
JsVar *addr = jsvObjectGetChild(obj, "id", 0); | |
if (jsvCompareString(addr, deviceAddr, 0, 0, true) == 0) | |
deviceMatchedFilters = true; // we have already matched - so match this one | |
jsvUnLock2(addr, obj); | |
jsvObjectIteratorNext(&it); | |
} | |
jsvObjectIteratorFree(&it); | |
} | |
jsvUnLock(arr); | |
} | |
if (deviceMatchedFilters) | |
jspExecuteFunction(callback, 0, 1, &device); | |
jsvUnLock2(deviceAddr, device); | |
} | |
void jswrap_ble_setScan(JsVar *callback, JsVar *options) { | |
JsVar *filters = 0; | |
bool activeScan = false; | |
if (jsvIsObject(options)) { | |
activeScan = jsvGetBoolAndUnLock(jsvObjectGetChild(options, "active", 0)); | |
filters = jsvObjectGetChild(options, "filters", 0); | |
if (filters && !jsvIsArray(filters)) { | |
jsvUnLock(filters); | |
jsExceptionHere(JSET_TYPEERROR, "requestDevice expecting an array of filters, got %t", filters); | |
return; | |
} | |
} else if (options) | |
jsExceptionHere(JSET_TYPEERROR, "Expecting Object got %t\n", options); | |
// set the callback event variable | |
if (!jsvIsFunction(callback)) callback=0; | |
if (callback) { | |
JsVar *fn = jsvNewNativeFunction((void (*)(void))jswrap_ble_setScan_cb, JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS)|(JSWAT_JSVAR<<(JSWAT_BITS*2))); | |
if (fn) { | |
jsvAddFunctionParameter(fn, 0, filters); // bind param 1 | |
jsvObjectSetChild(fn, JSPARSE_FUNCTION_THIS_NAME, callback); // bind 'this' | |
jsvObjectSetChild(execInfo.root, BLE_SCAN_EVENT, fn); | |
jsvUnLock(fn); | |
} | |
} else { | |
jsvObjectRemoveChild(execInfo.root, BLE_SCAN_EVENT); | |
} | |
// either start or stop scanning | |
uint32_t err_code = jsble_set_scanning(callback != 0, activeScan); | |
jsble_check_error(err_code); | |
jsvUnLock(filters); | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "filterDevices", | |
"generate" : "jswrap_ble_filterDevices", | |
"params" : [ | |
["devices","JsVar","An array of `BluetoothDevice` objects, from `NRF.findDevices` or similar"], | |
["filters","JsVar","A list of filters (as would be passed to `NRF.requestDevice`) to filter devices by"] | |
], | |
"return" : ["JsVar","An array of `BluetoothDevice` objects that match the given filters"] | |
} | |
This function can be used to quickly filter through Bluetooth devices. | |
For instance if you wish to scan for multiple different types of device at the same time | |
then you could use `NRF.findDevices` with all the filters you're interested in. When scanning | |
is finished you can then use `NRF.filterDevices` to pick out just the devices of interest. | |
``` | |
// the two types of device we're interested in | |
var filter1 = [{serviceData:{"fe95":{}}}]; | |
var filter2 = [{namePrefix:"Pixl.js"}]; | |
// the following filter will return both types of device | |
var allFilters = filter1.concat(filter2); | |
// now scan for both types of device, and filter them out afterwards | |
NRF.findDevices(function(devices) { | |
var devices1 = NRF.filterDevices(devices, filter1); | |
var devices2 = NRF.filterDevices(devices, filter2); | |
// ... | |
}, {filters : allFilters}); | |
``` | |
*/ | |
JsVar *jswrap_ble_filterDevices(JsVar *devices, JsVar *filters) { | |
if (!jsvIsArray(devices) || !jsvIsArray(filters)) { | |
jsExceptionHere(JSET_TYPEERROR, "Expecting both arguments to be arrays"); | |
return 0; | |
} | |
JsVar *result = jsvNewEmptyArray(); | |
if (!result) return 0; | |
JsvObjectIterator it; | |
jsvObjectIteratorNew(&it, devices); | |
while (jsvObjectIteratorHasValue(&it)) { | |
JsVar *device = jsvObjectIteratorGetValue(&it); | |
if (jswrap_ble_filter_device(filters, device)) | |
jsvArrayPush(result, device); | |
jsvUnLock(device); | |
jsvObjectIteratorNext(&it); | |
} | |
jsvObjectIteratorFree(&it); | |
return result; | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "findDevices", | |
"generate" : "jswrap_ble_findDevices", | |
"params" : [ | |
["callback","JsVar","The callback to call with received advertising packets (as `BluetoothDevice`), or undefined to stop"], | |
["options","JsVar","A time in milliseconds to scan for (defaults to 2000), Or an optional object `{filters: ..., timeout : ..., active: bool}` (as would be passed to `NRF.requestDevice`) to filter devices by"] | |
] | |
} | |
Utility function to return a list of BLE devices detected in range. Behind the scenes, | |
this uses `NRF.setScan(...)` and collates the results. | |
``` | |
NRF.findDevices(function(devices) { | |
console.log(devices); | |
}, 1000); | |
``` | |
prints something like: | |
``` | |
[ | |
BluetoothDevice { | |
"id": "e7:e0:57:ad:36:a2 random", | |
"rssi": -45, | |
"services": [ "4567" ], | |
"serviceData" : { "0123" : [ 1 ] }, | |
"manufacturerData" : [...], | |
"data": new ArrayBuffer([ ... ]), | |
"name": "Puck.js 36a2" | |
}, | |
BluetoothDevice { | |
"id": "c0:52:3f:50:42:c9 random", | |
"rssi": -65, | |
"data": new ArrayBuffer([ ... ]), | |
"name": "Puck.js 8f57" | |
} | |
] | |
``` | |
For more information on the structure returned, see `NRF.setScan`. | |
If you want to scan only for specific devices you can replace the timeout with an object | |
of the form `{filters: ..., timeout : ..., active: bool}` using the filters | |
described in `NRF.requestDevice`. For example to search for devices with Espruino's `manufacturerData`: | |
``` | |
NRF.findDevices(function(devices) { | |
... | |
}, {timeout : 2000, filters : [{ manufacturerData:{0x0590:{}} }] }); | |
``` | |
You could then use [`BluetoothDevice.gatt.connect(...)`](/Reference#l_BluetoothRemoteGATTServer_connect) on | |
the device returned to make a connection. | |
You can also use [`NRF.connect(...)`](/Reference#l_NRF_connect) on just the `id` string returned, which | |
may be useful if you always want to connect to a specific device. | |
**Note:** Using findDevices turns the radio's receive mode on for 2000ms (or however long you specify). This | |
can draw a *lot* of power (12mA or so), so you should use it sparingly or you can run your battery down quickly. | |
**Note:** The 'data' field contains the data of *the last packet received*. There may have been more | |
packets. To get data for each packet individually use `NRF.setScan` instead. | |
*/ | |
void jswrap_ble_findDevices_found_cb(JsVar *device) { | |
JsVar *arr = jsvObjectGetChild(execInfo.hiddenRoot, "BLEADV", JSV_ARRAY); | |
if (!arr) return; | |
JsVar *deviceAddr = jsvObjectGetChild(device, "id", 0); | |
JsVar *found = 0; | |
JsvObjectIterator it; | |
jsvObjectIteratorNew(&it, arr); | |
while (!found && jsvObjectIteratorHasValue(&it)) { | |
JsVar *obj = jsvObjectIteratorGetValue(&it); | |
JsVar *addr = jsvObjectGetChild(obj, "id", 0); | |
if (jsvCompareString(addr, deviceAddr, 0, 0, true) == 0) | |
found = jsvLockAgain(obj); | |
jsvUnLock2(addr, obj); | |
jsvObjectIteratorNext(&it); | |
} | |
jsvObjectIteratorFree(&it); | |
if (found) { | |
JsvObjectIterator oit; | |
jsvObjectIteratorNew(&oit, device); | |
while (jsvObjectIteratorHasValue(&oit)) { | |
JsVar *key = jsvObjectIteratorGetKey(&oit); | |
JsVar *value = jsvObjectIteratorGetValue(&oit); | |
JsVar *existingKey = jsvFindChildFromVar(found, key, true); | |
bool isServices = jsvIsStringEqual(key,"services"); | |
bool isServiceData = jsvIsStringEqual(key,"serviceData"); | |
JsVar *existingValue = 0; | |
if (isServices || isServiceData) { | |
// for services or servicedata we append to the array/object | |
existingValue = jsvSkipName(existingKey); | |
if (existingValue) { | |
if (isServices) { | |
jsvArrayPushAll(existingValue, value, true); | |
} else { | |
jsvObjectAppendAll(existingValue, value); | |
} | |
jsvUnLock(existingValue); | |
} | |
} | |
if (!existingValue) // nothing already - just copy | |
jsvSetValueOfName(existingKey, value); | |
jsvUnLock3(existingKey, key, value); | |
jsvObjectIteratorNext(&oit); | |
} | |
jsvObjectIteratorFree(&oit); | |
} else | |
jsvArrayPush(arr, device); | |
jsvUnLock3(found, deviceAddr, arr); | |
} | |
void jswrap_ble_findDevices_timeout_cb() { | |
jswrap_ble_setScan(0,0); | |
JsVar *arr = jsvObjectGetChild(execInfo.hiddenRoot, "BLEADV", JSV_ARRAY); | |
JsVar *cb = jsvObjectGetChild(execInfo.hiddenRoot, "BLEADVCB", 0); | |
jsvObjectRemoveChild(execInfo.hiddenRoot, "BLEADV"); | |
jsvObjectRemoveChild(execInfo.hiddenRoot, "BLEADVCB"); | |
if (arr && cb) { | |
jsiQueueEvents(0, cb, &arr, 1); | |
} | |
jsvUnLock2(arr,cb); | |
} | |
void jswrap_ble_findDevices(JsVar *callback, JsVar *options) { | |
JsVarFloat time = 2000; | |
if (!jsvIsFunction(callback)) { | |
jsExceptionHere(JSET_ERROR, "Expecting function for first argument, got %t", callback); | |
return; | |
} | |
if (jsvIsNumeric(options)) { | |
time = jsvGetFloat(options); | |
options = 0; | |
} else if (jsvIsObject(options)) { | |
JsVar *v = jsvObjectGetChild(options,"timeout",0); | |
if (v) time = jsvGetFloatAndUnLock(v); | |
} else if (options) { | |
jsExceptionHere(JSET_ERROR, "Expecting number or object, got %t", options); | |
return; | |
} | |
if (isnan(time) || time < 10) { | |
jsExceptionHere(JSET_ERROR, "Invalid timeout"); | |
return; | |
} | |
jsvObjectSetChildAndUnLock(execInfo.hiddenRoot, "BLEADV", jsvNewEmptyArray()); | |
jsvObjectSetChild(execInfo.hiddenRoot, "BLEADVCB", callback); | |
JsVar *fn; | |
fn = jsvNewNativeFunction((void (*)(void))jswrap_ble_findDevices_found_cb, JSWAT_VOID|(JSWAT_JSVAR<<JSWAT_BITS)); | |
if (fn) { | |
jswrap_ble_setScan(fn, options); | |
jsvUnLock(fn); | |
} | |
jsvUnLock(jsiSetTimeout(jswrap_ble_findDevices_timeout_cb, time)); | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "setRSSIHandler", | |
"generate" : "jswrap_ble_setRSSIHandler", | |
"params" : [ | |
["callback","JsVar","The callback to call with the RSSI value, or undefined to stop"] | |
] | |
} | |
Start/stop listening for RSSI values on the currently active connection | |
(where This device is a peripheral and is being connected to by a 'central' device) | |
``` | |
// Start scanning | |
NRF.setRSSIHandler(function(rssi) { | |
console.log(rssi); // prints -85 (or similar) | |
}); | |
// Stop Scanning | |
NRF.setRSSIHandler(); | |
``` | |
RSSI is the 'Received Signal Strength Indication' in dBm | |
*/ | |
void jswrap_ble_setRSSIHandler(JsVar *callback) { | |
// set the callback event variable | |
if (!jsvIsFunction(callback)) callback=0; | |
jsvObjectSetChild(execInfo.root, BLE_RSSI_EVENT, callback); | |
// either start or stop scanning | |
uint32_t err_code = jsble_set_rssi_scan(callback != 0); | |
jsble_check_error(err_code); | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "setTxPower", | |
"generate" : "jswrap_ble_setTxPower", | |
"params" : [ | |
["power","int","Transmit power. Accepted values are -40(nRF52 only), -30(nRF51 only), -20, -16, -12, -8, -4, 0, and 4 dBm. On nRF52840 (eg Bangle.js 2) 5/6/7/8 dBm are available too. Others will give an error code."] | |
] | |
} | |
Set the BLE radio transmit power. The default TX power is 0 dBm, and | |
*/ | |
void jswrap_ble_setTxPower(JsVarInt pwr) { | |
jsble_set_tx_power(pwr); | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "setLowPowerConnection", | |
"generate" : "jswrap_ble_setLowPowerConnection", | |
"params" : [ | |
["lowPower","bool","Whether the connection is low power or not"] | |
] | |
} | |
**THIS IS DEPRECATED** - please use `NRF.setConnectionInterval` for | |
peripheral and `NRF.connect(addr, options)`/`BluetoothRemoteGATTServer.connect(options)` | |
for central connections. | |
This sets the connection parameters - these affect the transfer speed and | |
power usage when the device is connected. | |
* When not low power, the connection interval is between 7.5 and 20ms | |
* When low power, the connection interval is between 500 and 1000ms | |
When low power connection is enabled, transfers of data over Bluetooth | |
will be very slow, however power usage while connected will be drastically | |
decreased. | |
This will only take effect after the connection is disconnected and | |
re-established. | |
*/ | |
void jswrap_ble_setLowPowerConnection(bool lowPower) { | |
BLEFlags oldflags = jsvGetIntegerAndUnLock(jsvObjectGetChild(execInfo.hiddenRoot, BLE_NAME_FLAGS, 0)); | |
BLEFlags flags = oldflags; | |
if (lowPower) | |
flags |= BLE_FLAGS_LOW_POWER; | |
else | |
flags &= ~BLE_FLAGS_LOW_POWER; | |
if (flags != oldflags) { | |
jsvObjectSetChildAndUnLock(execInfo.hiddenRoot, BLE_NAME_FLAGS, jsvNewFromInteger(flags)); | |
jswrap_ble_restart(NULL); | |
} | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "nfcURL", | |
"#if" : "defined(NRF52_SERIES) && defined(USE_NFC)", | |
"generate" : "jswrap_nfc_URL", | |
"params" : [ | |
["url","JsVar","The URL string to expose on NFC, or `undefined` to disable NFC"] | |
] | |
} | |
Enables NFC and starts advertising the given URL. For example: | |
``` | |
NRF.nfcURL("http://espruino.com"); | |
``` | |
*/ | |
void jswrap_nfc_URL(JsVar *url) { | |
#ifdef USE_NFC | |
// Check for disabling NFC | |
if (jsvIsUndefined(url)) { | |
jsvObjectRemoveChild(execInfo.hiddenRoot, "NfcData"); | |
jswrap_nfc_stop(); | |
return; | |
} | |
if (!jsvIsString(url)) { | |
jsExceptionHere(JSET_TYPEERROR, "Expecting a String, got %t", url); | |
return; | |
} | |
JSV_GET_AS_CHAR_ARRAY(urlPtr, urlLen, url); | |
if (!urlPtr || !urlLen) | |
return jsExceptionHere(JSET_ERROR, "Unable to get URL data"); | |
nfc_uri_id_t uriType = NFC_URI_NONE; | |
if (memcmp(urlPtr, "http://", 7)==0) { | |
urlPtr+=7; | |
urlLen-=7; | |
uriType = NFC_URI_HTTP; | |
} else if (memcmp(urlPtr, "https://", 8)==0) { | |
urlPtr+=8; | |
urlLen-=8; | |
uriType = NFC_URI_HTTPS; | |
} | |
/* Encode NDEF message into a flat string - we need this to store the | |
* data so it hangs around. Avoid having a static var so we have RAM | |
* available if not using NFC. NFC data is read by nfc_callback */ | |
JsVar *flatStr = jsvNewFlatStringOfLength(NDEF_FULL_URL_HEADER_LEN + urlLen + NDEF_TERM_TLV_LEN); | |
if (!flatStr) | |
return jsExceptionHere(JSET_ERROR, "Unable to create string with URI data in"); | |
jsvObjectSetChild(execInfo.hiddenRoot, "NfcData", flatStr); | |
uint8_t *flatStrPtr = (uint8_t*)jsvGetFlatStringPointer(flatStr); | |
jsvUnLock(flatStr); | |
/* assemble NDEF Message */ | |
memcpy(flatStrPtr, NDEF_HEADER, NDEF_FULL_URL_HEADER_LEN); /* fill header */ | |
flatStrPtr[NDEF_IC_OFFSET] = uriType; /* set URI Identifier Code */ | |
memcpy(flatStrPtr+NDEF_FULL_URL_HEADER_LEN, urlPtr, urlLen); /* add payload */ | |
/* inject length fields into header */ | |
flatStrPtr[NDEF_MSG_LEN_OFFSET] = NDEF_RECORD_HEADER_LEN + urlLen; | |
flatStrPtr[NDEF_PL_LEN_LSB_OFFSET] = NDEF_IC_LEN + urlLen; | |
/* write terminator TLV block */ | |
flatStrPtr[NDEF_FULL_URL_HEADER_LEN + urlLen] = NDEF_TERM_TLV; | |
/* start nfc peripheral */ | |
JsVar* uid = jswrap_nfc_start(NULL); | |
/* inject UID/BCC */ | |
size_t len; | |
char *uidPtr = jsvGetDataPointer(uid, &len); | |
if(uidPtr) memcpy(flatStrPtr, uidPtr, TAG_HEADER_LEN); | |
jsvUnLock(uid); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "nfcPair", | |
"#if" : "defined(NRF52_SERIES) && defined(USE_NFC)", | |
"generate" : "jswrap_nfc_pair", | |
"params" : [ | |
["key","JsVar","16 byte out of band key"] | |
] | |
} | |
Enables NFC and with an out of band 16 byte pairing key. | |
For example the following will enable out of band pairing on BLE | |
such that the device will pair when you tap the phone against it: | |
``` | |
var bleKey = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00]; | |
NRF.on('security',s=>print("security",JSON.stringify(s))); | |
NRF.nfcPair(bleKey); | |
NRF.setSecurity({oob:bleKey, mitm:true}); | |
``` | |
*/ | |
void jswrap_nfc_pair(JsVar *key) { | |
#ifdef USE_NFC | |
// Check for disabling NFC | |
if (jsvIsUndefined(key)) { | |
jsvObjectRemoveChild(execInfo.hiddenRoot, "NfcData"); | |
jswrap_nfc_stop(); | |
return; | |
} | |
JSV_GET_AS_CHAR_ARRAY(keyPtr, keyLen, key); | |
if (!keyPtr || keyLen!=BLE_GAP_SEC_KEY_LEN) | |
return jsExceptionHere(JSET_ERROR, "Unable to get key data or key isn't 16 bytes long"); | |
/* assemble NDEF Message */ | |
/* Encode BLE pairing message into the buffer. */ | |
uint8_t buf[256]; | |
uint32_t ndef_msg_len = sizeof(buf); | |
uint32_t err_code = nfc_ble_pair_default_msg_encode(NFC_BLE_PAIR_MSG_FULL, | |
(ble_advdata_tk_value_t *)keyPtr, | |
NULL, | |
buf, | |
&ndef_msg_len); | |
if (jsble_check_error(err_code)) return; | |
/* Encode NDEF message into a flat string - we need this to store the | |
* data so it hangs around. Avoid having a static var so we have RAM | |
* available if not using NFC. NFC data is read by nfc_callback */ | |
JsVar *flatStr = jsvNewFlatStringOfLength(ndef_msg_len); | |
if (!flatStr) | |
return jsExceptionHere(JSET_ERROR, "Unable to create string with pairing data in"); | |
uint8_t *flatStrPtr = (uint8_t*)jsvGetFlatStringPointer(flatStr); | |
memcpy(flatStrPtr, buf, ndef_msg_len); | |
jswrap_nfc_raw(flatStr); | |
jsvUnLock(flatStr); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "nfcAndroidApp", | |
"#if" : "defined(NRF52_SERIES) && defined(USE_NFC)", | |
"generate" : "jswrap_nfc_androidApp", | |
"params" : [ | |
["app","JsVar","The unique identifier of the given Android App"] | |
] | |
} | |
Enables NFC with a record that will launch the given android app. | |
For example: | |
``` | |
NRF.nfcAndroidApp("no.nordicsemi.android.nrftoolbox") | |
``` | |
*/ | |
void jswrap_nfc_androidApp(JsVar *appName) { | |
#ifdef USE_NFC | |
// Check for disabling NFC | |
if (jsvIsUndefined(appName)) { | |
jsvObjectRemoveChild(execInfo.hiddenRoot, "NfcData"); | |
jswrap_nfc_stop(); | |
return; | |
} | |
JSV_GET_AS_CHAR_ARRAY(appNamePtr, appNameLen, appName); | |
if (!appNamePtr || !appNameLen) | |
return jsExceptionHere(JSET_ERROR, "Unable to get app name"); | |
/* assemble NDEF Message */ | |
/* Encode BLE pairing message into the buffer. */ | |
uint8_t buf[512]; | |
uint32_t ndef_msg_len = sizeof(buf); | |
/* Encode launchapp message into the buffer. */ | |
uint32_t err_code = nfc_launchapp_msg_encode((uint8_t*)appNamePtr, | |
appNameLen, | |
0, | |
0, | |
buf, | |
&ndef_msg_len); | |
if (jsble_check_error(err_code)) return; | |
/* Encode NDEF message into a flat string - we need this to store the | |
* data so it hangs around. Avoid having a static var so we have RAM | |
* available if not using NFC. NFC data is read by nfc_callback */ | |
JsVar *flatStr = jsvNewFlatStringOfLength(ndef_msg_len); | |
if (!flatStr) | |
return jsExceptionHere(JSET_ERROR, "Unable to create string with pairing data in"); | |
uint8_t *flatStrPtr = (uint8_t*)jsvGetFlatStringPointer(flatStr); | |
memcpy(flatStrPtr, buf, ndef_msg_len); | |
jswrap_nfc_raw(flatStr); | |
jsvUnLock(flatStr); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "nfcRaw", | |
"#if" : "defined(NRF52_SERIES) && defined(USE_NFC)", | |
"generate" : "jswrap_nfc_raw", | |
"params" : [ | |
["payload","JsVar","The NFC NDEF message to deliver to the reader"] | |
] | |
} | |
Enables NFC and starts advertising with Raw data. For example: | |
``` | |
NRF.nfcRaw(new Uint8Array([193, 1, 0, 0, 0, 13, 85, 3, 101, 115, 112, 114, 117, 105, 110, 111, 46, 99, 111, 109])); | |
// same as NRF.nfcURL("http://espruino.com"); | |
``` | |
*/ | |
void jswrap_nfc_raw(JsVar *payload) { | |
#ifdef USE_NFC | |
// Check for disabling NFC | |
if (jsvIsUndefined(payload)) { | |
jsvObjectRemoveChild(execInfo.hiddenRoot, "NfcData"); | |
jswrap_nfc_stop(); | |
return; | |
} | |
JSV_GET_AS_CHAR_ARRAY(dataPtr, dataLen, payload); | |
if (!dataPtr || !dataLen) | |
return jsExceptionHere(JSET_ERROR, "Unable to get NFC data"); | |
/* Create a flat string - we need this to store the NFC data so it hangs around. | |
* Avoid having a static var so we have RAM available if not using NFC. | |
* NFC data is read by nfc_callback in bluetooth.c */ | |
JsVar *flatStr = jsvNewFlatStringOfLength(NDEF_FULL_RAW_HEADER_LEN + dataLen + NDEF_TERM_TLV_LEN); | |
if (!flatStr) | |
return jsExceptionHere(JSET_ERROR, "Unable to create string with NFC data in"); | |
jsvObjectSetChild(execInfo.hiddenRoot, "NfcData", flatStr); | |
uint8_t *flatStrPtr = (uint8_t*)jsvGetFlatStringPointer(flatStr); | |
jsvUnLock(flatStr); | |
/* assemble NDEF Message */ | |
memcpy(flatStrPtr, NDEF_HEADER, NDEF_FULL_RAW_HEADER_LEN); /* fill header */ | |
memcpy(flatStrPtr+NDEF_FULL_RAW_HEADER_LEN, dataPtr, dataLen); /* add payload */ | |
/* inject length fields into header */ | |
flatStrPtr[NDEF_MSG_LEN_OFFSET] = dataLen; | |
/* write terminator TLV block */ | |
flatStrPtr[NDEF_FULL_RAW_HEADER_LEN + dataLen] = NDEF_TERM_TLV; | |
/* start nfc peripheral */ | |
JsVar* uid = jswrap_nfc_start(NULL); | |
/* inject UID/BCC */ | |
size_t len; | |
char *uidPtr = jsvGetDataPointer(uid, &len); | |
if(uidPtr) memcpy(flatStrPtr, uidPtr, TAG_HEADER_LEN); | |
jsvUnLock(uid); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "nfcStart", | |
"#if" : "defined(NRF52_SERIES) && defined(USE_NFC)", | |
"generate" : "jswrap_nfc_start", | |
"params" : [ | |
["payload","JsVar","Optional 7 byte UID"] | |
], | |
"return" : ["JsVar", "Internal tag memory (first 10 bytes of tag data)" ] | |
} | |
**Advanced NFC Functionality.** If you just want to advertise a URL, use `NRF.nfcURL` instead. | |
Enables NFC and starts advertising. `NFCrx` events will be | |
fired when data is received. | |
``` | |
NRF.nfcStart(); | |
``` | |
*/ | |
JsVar *jswrap_nfc_start(JsVar *payload) { | |
#ifdef USE_NFC | |
/* Turn off NFC */ | |
jsble_nfc_stop(); | |
/* Create a flat string - we need this to store the NFC data so it hangs around. | |
* Avoid having a static var so we have RAM available if not using NFC */ | |
JsVar *flatStr = 0; | |
if (!jsvIsUndefined(payload)) { | |
/* Custom UID */ | |
JSV_GET_AS_CHAR_ARRAY(dataPtr, dataLen, payload); | |
if (!dataPtr || !dataLen) { | |
jsExceptionHere(JSET_ERROR, "Unable to get NFC data"); | |
return 0; | |
} | |
flatStr = jsvNewFlatStringOfLength(dataLen); | |
if (!flatStr) { | |
jsExceptionHere(JSET_ERROR, "Unable to create string with NFC data in"); | |
return 0; | |
} | |
jsvObjectSetChild(execInfo.hiddenRoot, "NfcEnabled", flatStr); | |
jsvUnLock(flatStr); | |
uint8_t *flatStrPtr = (uint8_t*)jsvGetFlatStringPointer(flatStr); | |
memcpy(flatStrPtr, dataPtr, dataLen); | |
} else { | |
/* Default UID */ | |
flatStr = jsvNewFlatStringOfLength(0); | |
if (!flatStr) { | |
jsExceptionHere(JSET_ERROR, "Unable to create string with NFC data in"); | |
return 0; | |
} | |
jsvObjectSetChild(execInfo.hiddenRoot, "NfcEnabled", flatStr); | |
jsvUnLock(flatStr); | |
} | |
/* start nfc */ | |
uint8_t *flatStrPtr = (uint8_t*)jsvGetFlatStringPointer(flatStr); | |
jsble_nfc_start(flatStrPtr, jsvGetLength(flatStr)); | |
/* return internal tag header */ | |
char *ptr = 0; size_t size = TAG_HEADER_LEN; | |
JsVar *arr = jsvNewArrayBufferWithPtr(size, &ptr); | |
if (ptr) jsble_nfc_get_internal((uint8_t *)ptr, &size); | |
return arr; | |
#else | |
return 0; | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "nfcStop", | |
"#if" : "defined(NRF52_SERIES) && defined(USE_NFC)", | |
"generate" : "jswrap_nfc_stop", | |
"params" : [ ] | |
} | |
**Advanced NFC Functionality.** If you just want to advertise a URL, use `NRF.nfcURL` instead. | |
Disables NFC. | |
``` | |
NRF.nfcStop(); | |
``` | |
*/ | |
void jswrap_nfc_stop() { | |
#ifdef USE_NFC | |
jsvObjectRemoveChild(execInfo.hiddenRoot, "NfcEnabled"); | |
jsble_nfc_stop(); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "nfcSend", | |
"#if" : "defined(NRF52_SERIES) && defined(USE_NFC)", | |
"generate" : "jswrap_nfc_send", | |
"params" : [ | |
["payload","JsVar","Optional tx data"] | |
] | |
} | |
**Advanced NFC Functionality.** If you just want to advertise a URL, use `NRF.nfcURL` instead. | |
Acknowledges the last frame and optionally transmits a response. | |
If payload is an array, then a array.length byte nfc frame is sent. | |
If payload is a int, then a 4bit ACK/NACK is sent. | |
**Note:** ```nfcSend``` should always be called after an ```NFCrx``` event. | |
``` | |
NRF.nfcSend(new Uint8Array([0x01, 0x02, ...])); | |
// or | |
NRF.nfcSend(0x0A); | |
// or | |
NRF.nfcSend(); | |
``` | |
*/ | |
void jswrap_nfc_send(JsVar *payload) { | |
#ifdef USE_NFC | |
/* Switch to RX */ | |
if (jsvIsUndefined(payload)) | |
return jsble_nfc_send_rsp(0, 0); | |
/* Send 4 bit ACK/NACK */ | |
if (jsvIsInt(payload)) | |
return jsble_nfc_send_rsp(jsvGetInteger(payload), 4); | |
/* Send n byte payload */ | |
JSV_GET_AS_CHAR_ARRAY(dataPtr, dataLen, payload); | |
if (!dataPtr || !dataLen) | |
return jsExceptionHere(JSET_ERROR, "Unable to get NFC data"); | |
jsble_nfc_send((uint8_t*)dataPtr, dataLen); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "sendHIDReport", | |
"ifdef" : "NRF52_SERIES", | |
"generate" : "jswrap_ble_sendHIDReport", | |
"params" : [ | |
["data","JsVar","Input report data as an array"], | |
["callback","JsVar","A callback function to be called when the data is sent"] | |
] | |
} | |
Send a USB HID report. HID must first be enabled with `NRF.setServices({}, {hid: hid_report})` | |
*/ | |
void jswrap_ble_sendHIDReport(JsVar *data, JsVar *callback) { | |
#if BLE_HIDS_ENABLED | |
JSV_GET_AS_CHAR_ARRAY(vPtr, vLen, data) | |
if (vPtr && vLen) { | |
if (jsvIsFunction(callback)) | |
jsvObjectSetChild(execInfo.root, BLE_HID_SENT_EVENT, callback); | |
jsble_send_hid_input_report((uint8_t*)vPtr, vLen); | |
} else { | |
jsExceptionHere(JSET_ERROR, "Expecting array, got %t", data); | |
} | |
#endif | |
} | |
/*JSON{ | |
"type" : "event", | |
"class" : "E", | |
"name" : "ANCS", | |
"params" : [["info","JsVar","An object (see below)"]], | |
"ifdef" : "BANGLEJS" | |
} | |
Called when a notification arrives on an Apple iOS device Bangle.js is connected to | |
``` | |
{ | |
event:"add", | |
uid:42, | |
category:4, | |
categoryCnt:42, | |
silent:true, | |
important:false, | |
preExisting:true, | |
positive:false, | |
negative:true | |
} | |
``` | |
You can then get more information with something like: | |
``` | |
NRF.ancsGetNotificationInfo( event.uid ).then(a=>print("Notify",E.toJS(a))); | |
``` | |
*/ | |
/*JSON{ | |
"type" : "event", | |
"class" : "E", | |
"name" : "AMS", | |
"params" : [["info","JsVar","An object (see below)"]], | |
"ifdef" : "BANGLEJS" | |
} | |
Called when a media event arrives on an Apple iOS device Bangle.js is connected to | |
``` | |
{ | |
id : "artist"/"album"/"title"/"duration", | |
value : "Some text", | |
truncated : bool // the 'value' was too big to be sent completely | |
} | |
``` | |
*/ | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "ancsIsActive", | |
"ifdef" : "NRF52_SERIES", | |
"generate" : "jswrap_ble_ancsIsActive", | |
"params" : [ ], | |
"return" : ["bool", "True if Apple Notification Center Service (ANCS) has been initialised and is active" ] | |
} | |
Check if Apple Notification Center Service (ANCS) is currently active on the BLE connection | |
*/ | |
bool jswrap_ble_ancsIsActive() { | |
#if ESPR_BLUETOOTH_ANCS | |
return ((bleStatus & BLE_ANCS_INITED) && ble_ancs_is_active()); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "ancsAction", | |
"ifdef" : "NRF52_SERIES", | |
"generate" : "jswrap_ble_ancsAction", | |
"params" : [ | |
["uid","int","The UID of the notification to respond to"], | |
["positive","bool","`true` for positive action, `false` for negative"] | |
] | |
} | |
Send an ANCS action for a specific Notification UID. Corresponds to posaction/negaction in the 'ANCS' event that was received | |
*/ | |
void jswrap_ble_ancsAction(int uid, bool isPositive) { | |
#if ESPR_BLUETOOTH_ANCS | |
if (!(bleStatus & BLE_ANCS_INITED) || !ble_ancs_is_active()) { | |
jsExceptionHere(JSET_ERROR, "ANCS not active"); | |
return; | |
} | |
ble_ancs_action(uid, isPositive); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "ancsGetNotificationInfo", | |
"ifdef" : "NRF52_SERIES", | |
"generate" : "jswrap_ble_ancsGetNotificationInfo", | |
"params" : [ | |
["uid","int","The UID of the notification to get information for"] | |
], | |
"return" : ["JsVar", "A `Promise` that is resolved (or rejected) when the connection is complete" ], | |
"return_object" : "Promise" | |
} | |
Get ANCS info for a notification, eg: | |
*/ | |
JsVar *jswrap_ble_ancsGetNotificationInfo(JsVarInt uid) { | |
JsVar *promise = 0; | |
#if ESPR_BLUETOOTH_ANCS | |
if (!(bleStatus & BLE_ANCS_INITED) || !ble_ancs_is_active()) { | |
jsExceptionHere(JSET_ERROR, "ANCS not active"); | |
return 0; | |
} | |
if (ble_ancs_request_notif(uid)) { // if fails, it'll create an exception | |
if (bleNewTask(BLETASK_ANCS_NOTIF_ATTR, 0)) { | |
promise = jsvLockAgainSafe(blePromise); | |
} | |
} | |
#endif | |
return promise; | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "ancsGetAppInfo", | |
"ifdef" : "NRF52_SERIES", | |
"generate" : "jswrap_ble_ancsGetAppInfo", | |
"params" : [ | |
["id","JsVar","The app ID to get information for"] | |
], | |
"return" : ["JsVar", "A `Promise` that is resolved (or rejected) when the connection is complete" ], | |
"return_object" : "Promise" | |
} | |
Get ANCS info for an app (add id is available via `ancsGetNotificationInfo`) | |
Promise returns: | |
``` | |
{ | |
"uid" : int, | |
"appId" : string, | |
"title" : string, | |
"subtitle" : string, | |
"message" : string, | |
"messageSize" : string, | |
"date" : string, | |
"posAction" : string, | |
"negAction" : string, | |
"name" : string, | |
} | |
``` | |
*/ | |
JsVar *jswrap_ble_ancsGetAppInfo(JsVar *appId) { | |
JsVar *promise = 0; | |
#if ESPR_BLUETOOTH_ANCS | |
if (!(bleStatus & BLE_ANCS_INITED) || !ble_ancs_is_active()) { | |
jsExceptionHere(JSET_ERROR, "ANCS not active"); | |
return 0; | |
} | |
char appIdStr[48]; | |
jsvGetString(appId, appIdStr, sizeof(appIdStr)); | |
if (ble_ancs_request_app(appIdStr, strlen(appIdStr))) { // if fails, it'll create an exception | |
if (bleNewTask(BLETASK_ANCS_APP_ATTR, appId)) { | |
promise = jsvLockAgainSafe(blePromise); | |
} | |
} | |
#endif | |
return promise; | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "amsIsActive", | |
"ifdef" : "NRF52_SERIES", | |
"generate" : "jswrap_ble_amsIsActive", | |
"params" : [ ], | |
"return" : ["bool", "True if Apple Media Service (AMS) has been initialised and is active" ] | |
} | |
Check if Apple Media Service (AMS) is currently active on the BLE connection | |
*/ | |
bool jswrap_ble_amsIsActive() { | |
#if ESPR_BLUETOOTH_ANCS | |
return ((bleStatus & BLE_AMS_INITED) && ble_ams_is_active()); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "amsGetPlayerInfo", | |
"ifdef" : "NRF52_SERIES", | |
"generate" : "jswrap_ble_amsGetPlayerInfo", | |
"params" : [ | |
["id","JsVar","Either 'name', 'playbackinfo' or 'volume'"] | |
], | |
"return" : ["JsVar", "A `Promise` that is resolved (or rejected) when the connection is complete" ], | |
"return_object" : "Promise" | |
} | |
Get Apple Media Service (AMS) info for the current media player. | |
"playbackinfo" returns a concatenation of three comma-separated values: | |
- PlaybackState: a string that represents the integer value of the playback state: | |
- PlaybackStatePaused = 0 | |
- PlaybackStatePlaying = 1 | |
- PlaybackStateRewinding = 2 | |
- PlaybackStateFastForwarding = 3 | |
- PlaybackRate: a string that represents the floating point value of the playback rate. | |
- ElapsedTime: a string that represents the floating point value of the elapsed time of the current track, in seconds | |
*/ | |
JsVar *jswrap_ble_amsGetPlayerInfo(JsVar *id) { | |
JsVar *promise = 0; | |
#if ESPR_BLUETOOTH_ANCS | |
if (!(bleStatus & BLE_AMS_INITED) || !ble_ams_is_active()) { | |
jsExceptionHere(JSET_ERROR, "AMS not active"); | |
return 0; | |
} | |
ble_ams_c_player_attribute_id_val_t cmd; | |
if (jsvIsStringEqual(id,"name")) cmd=BLE_AMS_PLAYER_ATTRIBUTE_ID_NAME; | |
else if (jsvIsStringEqual(id,"playbackinfo")) cmd=BLE_AMS_PLAYER_ATTRIBUTE_ID_PLAYBACK_INFO; | |
else if (jsvIsStringEqual(id,"volume")) cmd=BLE_AMS_PLAYER_ATTRIBUTE_ID_VOLUME; | |
else { | |
jsExceptionHere(JSET_ERROR, "Unknown id %q", id); | |
return promise; | |
} | |
if (ble_ams_request_player_info(cmd)) { // if fails, it'll create an exception | |
if (bleNewTask(BLETASK_AMS_ATTR, 0)) { | |
promise = jsvLockAgainSafe(blePromise); | |
} | |
} | |
#endif | |
return promise; | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "amsGetTrackInfo", | |
"ifdef" : "NRF52_SERIES", | |
"generate" : "jswrap_ble_amsGetTrackInfo", | |
"params" : [ | |
["id","JsVar","Either 'artist', 'album', 'title' or 'duration'"] | |
], | |
"return" : ["JsVar", "A `Promise` that is resolved (or rejected) when the connection is complete" ], | |
"return_object" : "Promise" | |
} | |
Get Apple Media Service (AMS) info for the currently-playing track | |
*/ | |
JsVar *jswrap_ble_amsGetTrackInfo(JsVar *id) { | |
JsVar *promise = 0; | |
#if ESPR_BLUETOOTH_ANCS | |
if (!(bleStatus & BLE_AMS_INITED) || !ble_ams_is_active()) { | |
jsExceptionHere(JSET_ERROR, "AMS not active"); | |
return 0; | |
} | |
ble_ams_c_track_attribute_id_val_t cmd; | |
if (jsvIsStringEqual(id,"artist")) cmd=BLE_AMS_TRACK_ATTRIBUTE_ID_ARTIST; | |
else if (jsvIsStringEqual(id,"album")) cmd=BLE_AMS_TRACK_ATTRIBUTE_ID_ALBUM; | |
else if (jsvIsStringEqual(id,"title")) cmd=BLE_AMS_TRACK_ATTRIBUTE_ID_TITLE; | |
else if (jsvIsStringEqual(id,"duration")) cmd=BLE_AMS_TRACK_ATTRIBUTE_ID_DURATION; | |
else { | |
jsExceptionHere(JSET_ERROR, "Unknown id %q", id); | |
return promise; | |
} | |
if (ble_ams_request_track_info(cmd)) { // if fails, it'll create an exception | |
if (bleNewTask(BLETASK_AMS_ATTR, 0)) { | |
promise = jsvLockAgainSafe(blePromise); | |
} | |
} | |
#endif | |
return promise; | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "amsCommand", | |
"ifdef" : "NRF52_SERIES", | |
"generate" : "jswrap_ble_amsCommand", | |
"params" : [ | |
["id","JsVar","For example, 'play', 'pause', 'volup' or 'voldown'"] | |
] | |
} | |
Send an AMS command to an Apple Media Service device to control music playback | |
Command is one of play, pause, playpause, next, prev, volup, voldown, repeat, shuffle, skipforward, skipback, like, dislike, bookmark | |
*/ | |
void jswrap_ble_amsCommand(JsVar *id) { | |
#if ESPR_BLUETOOTH_ANCS | |
if (!(bleStatus & BLE_AMS_INITED) || !ble_ams_is_active()) { | |
jsExceptionHere(JSET_ERROR, "AMS not active"); | |
return; | |
} | |
ble_ams_c_remote_control_id_val_t cmd; | |
if (jsvIsStringEqual(id,"play")) cmd=BLE_AMS_REMOTE_COMMAND_ID_PLAY; | |
else if (jsvIsStringEqual(id,"pause")) cmd=BLE_AMS_REMOTE_COMMAND_ID_PAUSE; | |
else if (jsvIsStringEqual(id,"playpause")) cmd=BLE_AMS_REMOTE_COMMAND_ID_TOGGLE_PLAY_PAUSE; | |
else if (jsvIsStringEqual(id,"next")) cmd=BLE_AMS_REMOTE_COMMAND_ID_NEXT_TRACK; | |
else if (jsvIsStringEqual(id,"prev")) cmd=BLE_AMS_REMOTE_COMMAND_ID_PREVIOUS_TRACK; | |
else if (jsvIsStringEqual(id,"volup")) cmd=BLE_AMS_REMOTE_COMMAND_ID_VOLUME_UP; | |
else if (jsvIsStringEqual(id,"voldown")) cmd=BLE_AMS_REMOTE_COMMAND_ID_VOLUME_DOWN; | |
else if (jsvIsStringEqual(id,"repeat")) cmd=BLE_AMS_REMOTE_COMMAND_ID_ADVANCE_REPEAT_MODE; | |
else if (jsvIsStringEqual(id,"shuffle")) cmd=BLE_AMS_REMOTE_COMMAND_ID_ADVANCE_SHUFFLE_MODE; | |
else if (jsvIsStringEqual(id,"skipforward")) cmd=BLE_AMS_REMOTE_COMMAND_ID_SKIP_FORWARD; | |
else if (jsvIsStringEqual(id,"skipback")) cmd=BLE_AMS_REMOTE_COMMAND_ID_SKIP_BACKWARD; | |
else if (jsvIsStringEqual(id,"like")) cmd=BLE_AMS_REMOTE_COMMAND_ID_LIKE_TRACK; | |
else if (jsvIsStringEqual(id,"dislike")) cmd=BLE_AMS_REMOTE_COMMAND_ID_DISLIKE_TRACK; | |
else if (jsvIsStringEqual(id,"bookmark")) cmd=BLE_AMS_REMOTE_COMMAND_ID_BOOKMARK_TRACK; | |
else { | |
jsExceptionHere(JSET_ERROR, "Unknown command %q", cmd); | |
return; | |
} | |
ble_ams_command(cmd); | |
#endif | |
} | |
/*JSON{ | |
"type" : "staticmethod", | |
"class" : "NRF", | |
"name" : "requestDevice", | |
"#if" : "defined(NRF52_SERIES) || defined(ESP32)", | |
"generate" : "jswrap_ble_requestDevice", | |
"params" : [ | |
["options","JsVar","Options used to filter the device to use"] | |
], | |
"return" : ["JsVar", "A `Promise` that is resolved (or rejected) when the connection is complete" ], | |
"return_object" : "Promise" | |
} | |
Search for available devices matching the given filters. Since we have no UI here, | |
Espruino will pick the FIRST device it finds, or it'll call `catch`. | |
`options` can have the following fields: | |
* `filters` - a list of filters that a device must match before it is returned (see below) | |
* `timeout` - the maximum time to scan for in milliseconds (scanning stops when a match | |
is found. eg. `NRF.requestDevice({ timeout:2000, filters: [ ... ] })` | |
* `active` - whether to perform active scanning (requesting 'scan response' packets from any | |
devices that are found). eg. `NRF.requestDevice({ active:true, filters: [ ... ] })` | |
**NOTE:** `timeout` and `active` are not part of the Web Bluetooth standard. | |
The following filter types are implemented: | |
* `services` - list of services as strings (all of which must match). 128 bit services must be in the form '01230123-0123-0123-0123-012301230123' | |
* `name` - exact device name | |
* `namePrefix` - starting characters of device name | |
* `id` - exact device address (`id:"e9:53:86:09:89:99 random"`) (this is Espruino-specific, and is not part of the Web Bluetooth spec) | |
* `serviceData` - an object containing service characteristics which must all match (`serviceData:{"1809":{}}`). Matching of actual service data is not supported yet. | |
* `manufacturerData` - an object containing manufacturer UUIDs which must all match (`manufacturerData:{0x0590:{}}`). Matching of actual manufacturer data is not supported yet. | |
``` | |
NRF.requestDevice({ filters: [{ namePrefix: 'Puck.js' }] }).then(function(device) { ... }); | |
// or | |
NRF.requestDevice({ filters: [{ services: ['1823'] }] }).then(function(device) { ... }); | |
// or | |
NRF.requestDevice({ filters: [{ manufacturerData:{0x0590:{}} }] }).then(function(device) { ... }); | |
``` | |
As a full example, to send data to another Puck.js to turn an LED on: | |
``` | |
var gatt; | |
NRF.requestDevice({ filters: [{ namePrefix: 'Puck.js' }] }).then(function(device) { | |
return device.gatt.connect(); | |
}).then(function(g) { | |
gatt = g; | |
return gatt.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e"); | |
}).then(function(service) { | |
return service.getCharacteristic("6e400002-b5a3-f393-e0a9-e50e24dcca9e"); | |
}).then(function(characteristic) { | |
return characteristic.writeValue("LED1.set()\n"); | |
}).then(function() { | |
gatt.disconnect(); | |
console.log("Done!"); | |
}); | |
``` | |
Or slightly more concisely, using ES6 arrow functions: | |