diff --git a/platforms/Zephyr/Kconfig b/platforms/Zephyr/Kconfig index d713cf80d..d1759466b 100644 --- a/platforms/Zephyr/Kconfig +++ b/platforms/Zephyr/Kconfig @@ -37,4 +37,15 @@ config NET_MAX_CONTEXTS endif # WARDUINO_NETWORKING +config WARDUINO_BLE + bool "Enable WARDuino Bluetooth LE primitives" + default y + select BT + select BT_PERIPHERAL + select BT_GATT_DYNAMIC_DB + select BT_DEVICE_NAME_DYNAMIC + help + Enables primitives for defining a custom BLE GATT peripheral + (services and characteristics) from WASM code. + endmenu diff --git a/platforms/Zephyr/prj.conf b/platforms/Zephyr/prj.conf index 9262530eb..56e5554be 100644 --- a/platforms/Zephyr/prj.conf +++ b/platforms/Zephyr/prj.conf @@ -39,7 +39,7 @@ CONFIG_STM32_ENABLE_DEBUG_SLEEP_STOP=y #CONFIG_ESP32_USE_UNSUPPORTED_REVISION=y # Display drivers -CONFIG_DISPLAY=y +CONFIG_DISPLAY=n CONFIG_ENTROPY_GENERATOR=y @@ -49,7 +49,11 @@ CONFIG_SYS_HEAP_RUNTIME_STATS=y CONFIG_USB_DEVICE_PRODUCT="WARDuino Microcontroller" CONFIG_USB_DEVICE_MANUFACTURER="TOPLLab" -CONFIG_WARDUINO_NETWORKING=y +CONFIG_WARDUINO_NETWORKING=n + +CONFIG_WARDUINO_BLE=y +CONFIG_BT_DEVICE_NAME="WARDuino" +CONFIG_BT_DEVICE_NAME_MAX=32 # Disable these options, they will stay disabled even when networking is enabled CONFIG_NET_SHELL=n diff --git a/src/Primitives/Networking/bluetooth.h b/src/Primitives/Networking/bluetooth.h new file mode 100644 index 000000000..94b9b60b0 --- /dev/null +++ b/src/Primitives/Networking/bluetooth.h @@ -0,0 +1,261 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +// Lets WASM code build a custom BLE GATT peripheral at runtime instead of the +// static BT_GATT_SERVICE_DEFINE tables Zephyr normally expects, since the set +// of services isn't known at compile time. +namespace ble { + +constexpr int MAX_SERVICES = 4; +constexpr int MAX_CHARACTERISTICS_PER_SERVICE = 6; +constexpr int MAX_CHARACTERISTICS = + MAX_SERVICES * MAX_CHARACTERISTICS_PER_SERVICE; +constexpr int MAX_VALUE_LEN = 64; +// primary service decl + (characteristic decl + value + ccc) per characteristic +constexpr int MAX_ATTRS_PER_SERVICE = 1 + MAX_CHARACTERISTICS_PER_SERVICE * 3; + +struct Characteristic { + bt_uuid_128 uuid; + bt_gatt_chrc decl; + bt_gatt_ccc_managed_user_data ccc; + uint8_t value[MAX_VALUE_LEN]; + uint16_t value_len; + bt_gatt_attr *value_attr; + bool dirty; +}; + +struct Service { + bt_uuid_128 uuid; + bt_gatt_attr attrs[MAX_ATTRS_PER_SERVICE]; + bt_gatt_service service; + bool registered; +}; + +inline Service services[MAX_SERVICES]; +inline int service_count = 0; + +inline Characteristic characteristics[MAX_CHARACTERISTICS]; +inline int characteristic_count = 0; + +inline const bt_uuid_16 uuid_gatt_primary = + BT_UUID_INIT_16(BT_UUID_GATT_PRIMARY_VAL); +inline const bt_uuid_16 uuid_gatt_chrc = BT_UUID_INIT_16(BT_UUID_GATT_CHRC_VAL); +inline const bt_uuid_16 uuid_gatt_ccc = BT_UUID_INIT_16(BT_UUID_GATT_CCC_VAL); + +// WASM passes UUID bytes in standard (most significant byte first) order, +// Zephyr stores 128-bit UUIDs least significant byte first. +inline void bytes_to_uuid(const uint8_t *bytes, bt_uuid_128 *uuid) { + uuid->uuid.type = BT_UUID_TYPE_128; + for (int i = 0; i < 16; i++) { + uuid->val[i] = bytes[15 - i]; + } +} + +inline ssize_t characteristic_gatt_read(bt_conn *conn, const bt_gatt_attr *attr, + void *buf, uint16_t len, + uint16_t offset) { + const auto *chr = static_cast(attr->user_data); + return bt_gatt_attr_read(conn, attr, buf, len, offset, chr->value, + chr->value_len); +} + +inline ssize_t characteristic_gatt_write(bt_conn *conn, + const bt_gatt_attr *attr, + const void *buf, uint16_t len, + uint16_t offset, uint8_t flags) { + auto *chr = static_cast(attr->user_data); + if (offset + len > MAX_VALUE_LEN) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + memcpy(chr->value + offset, buf, len); + chr->value_len = offset + len; + chr->dirty = true; + return len; +} + +inline int service_create(const uint8_t uuid[16]) { + if (service_count >= MAX_SERVICES) { + return -1; + } + Service *svc = &services[service_count]; + bytes_to_uuid(uuid, &svc->uuid); + svc->registered = false; + svc->service.attrs = svc->attrs; + svc->service.attr_count = 0; + return service_count++; +} + +inline int characteristic_create(int service_index, const uint8_t uuid[16], + uint8_t properties, uint16_t permissions) { + if (service_index < 0 || service_index >= service_count) { + return -1; + } + Service *svc = &services[service_index]; + bool needs_ccc = properties & (BT_GATT_CHRC_NOTIFY | BT_GATT_CHRC_INDICATE); + size_t needed = + (svc->service.attr_count == 0 ? 1 : 0) + 2 + (needs_ccc ? 1 : 0); + if (svc->registered || characteristic_count >= MAX_CHARACTERISTICS || + svc->service.attr_count + needed > MAX_ATTRS_PER_SERVICE) { + return -1; + } + + Characteristic *chr = &characteristics[characteristic_count]; + bytes_to_uuid(uuid, &chr->uuid); + chr->decl = { + .uuid = &chr->uuid.uuid, .value_handle = 0U, .properties = properties}; + chr->ccc = {.cfg = {}, + .cfg_changed = nullptr, + .cfg_write = nullptr, + .cfg_match = nullptr}; + chr->value_len = 0; + chr->dirty = false; + + if (svc->service.attr_count == 0) { + svc->attrs[svc->service.attr_count++] = { + .uuid = &uuid_gatt_primary.uuid, + .read = bt_gatt_attr_read_service, + .write = nullptr, + .user_data = &svc->uuid, + .handle = 0, + .perm = BT_GATT_PERM_READ, + }; + } + + svc->attrs[svc->service.attr_count++] = { + .uuid = &uuid_gatt_chrc.uuid, + .read = bt_gatt_attr_read_chrc, + .write = nullptr, + .user_data = &chr->decl, + .handle = 0, + .perm = BT_GATT_PERM_READ, + }; + chr->value_attr = &svc->attrs[svc->service.attr_count]; + svc->attrs[svc->service.attr_count++] = { + .uuid = &chr->uuid.uuid, + .read = characteristic_gatt_read, + .write = characteristic_gatt_write, + .user_data = chr, + .handle = 0, + .perm = permissions, + }; + + if (needs_ccc) { + svc->attrs[svc->service.attr_count++] = { + .uuid = &uuid_gatt_ccc.uuid, + .read = bt_gatt_attr_read_ccc, + .write = bt_gatt_attr_write_ccc, + .user_data = &chr->ccc, + .handle = 0, + .perm = BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, + }; + } + + return characteristic_count++; +} + +inline int characteristic_write(int index, const uint8_t *data, uint16_t len) { + if (index < 0 || index >= characteristic_count) { + return -1; + } + Characteristic *chr = &characteristics[index]; + if (len > MAX_VALUE_LEN) { + len = MAX_VALUE_LEN; + } + memcpy(chr->value, data, len); + chr->value_len = len; + + if (chr->decl.properties & (BT_GATT_CHRC_NOTIFY | BT_GATT_CHRC_INDICATE)) { + // No subscribers is not an error for the WASM caller, ignore it. + bt_gatt_notify(nullptr, chr->value_attr, chr->value, chr->value_len); + } + return 0; +} + +inline int characteristic_read(int index, uint8_t *buf, uint16_t max_len) { + if (index < 0 || index >= characteristic_count) { + return -1; + } + Characteristic *chr = &characteristics[index]; + uint16_t len = chr->value_len < max_len ? chr->value_len : max_len; + memcpy(buf, chr->value, len); + chr->dirty = false; + return len; +} + +inline int characteristic_available(int index) { + if (index < 0 || index >= characteristic_count) { + return -1; + } + return characteristics[index].dirty ? 1 : 0; +} + +inline int enable(const char *name) { + int err = bt_enable(nullptr); + if (err) { + printf("Bluetooth init failed (err %d)\n", err); + return err; + } + return bt_set_name(name); +} + +// Advertising stops as soon as a central connects and Zephyr doesn't resume +// it automatically, so we track connection state so that you can poll if we +// are still advertising from wasm, if not you can call advertise_start() again. +inline int connection_count = 0; + +inline void connected_cb(bt_conn *conn, uint8_t err) { + if (!err) { + connection_count++; + } +} + +inline void disconnected_cb(bt_conn *conn, uint8_t reason) { + connection_count--; +} + +BT_CONN_CB_DEFINE(callbacks) = { + .connected = connected_cb, + .disconnected = disconnected_cb, +}; + +inline bool connected() { return connection_count > 0; } + +// Advertisement data: flags, device name and every registered service's UUID. +inline bt_data ad[2 + MAX_SERVICES]; + +inline int advertise_start() { + static uint8_t flags = BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR; + int ad_count = 0; + ad[ad_count++] = BT_DATA(BT_DATA_FLAGS, &flags, sizeof(flags)); + + const char *name = bt_get_name(); + ad[ad_count++] = BT_DATA(BT_DATA_NAME_COMPLETE, name, + static_cast(strlen(name))); + + for (int i = 0; i < service_count; i++) { + Service *svc = &services[i]; + if (!svc->registered) { + int err = bt_gatt_service_register(&svc->service); + if (err) { + printf("Failed to register BLE service %d (err %d)\n", i, err); + return err; + } + svc->registered = true; + } + ad[ad_count++] = BT_DATA(BT_DATA_UUID128_ALL, svc->uuid.val, 16); + } + + return bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ad_count, nullptr, 0); +} + +inline int advertise_stop() { return bt_le_adv_stop(); } + +} // namespace ble diff --git a/src/Primitives/primitive_macros.h b/src/Primitives/primitive_macros.h index fc4f3ecf9..e32c265bf 100644 --- a/src/Primitives/primitive_macros.h +++ b/src/Primitives/primitive_macros.h @@ -187,6 +187,16 @@ inline Type fourToOneU32 = { .mask = 0x8101111 /* 0x8 1=I32 0=endRet ; 1=I32; 1=I32; 1=I32; 1=I32*/ }; +inline Type fiveToOneU32 = { + .form = FUNC, + .param_count = 5, + .params = param_I32_arr_len5, + .result_count = 1, + .results = param_I32_arr_len1, + .mask = + 0x81011111 /* 0x8 1=I32 0=endRet ; 1=I32; 1=I32; 1=I32; 1=I32; 1=I32*/ +}; + inline Type tenToOneU32 = { .form = FUNC, .param_count = 10, diff --git a/src/Primitives/zephyr.cpp b/src/Primitives/zephyr.cpp index 467d42ed0..7d8fb9f3a 100644 --- a/src/Primitives/zephyr.cpp +++ b/src/Primitives/zephyr.cpp @@ -653,6 +653,90 @@ def_prim(socket_close, oneToOneI32) { #endif +#if IS_ENABLED(CONFIG_BT) +#include "Networking/bluetooth.h" + +def_prim(ble_enable, twoToOneU32) { + uint32_t addr = arg1.uint32; + uint32_t len = arg0.uint32; + std::string name = parse_utf8_string(m->memory.bytes, len, addr); + pop_args(2); + pushInt32(ble::enable(name.c_str())); + return true; +} + +def_prim(ble_service_create, twoToOneU32) { + uint32_t addr = arg1.uint32; + uint32_t len = arg0.uint32; + pop_args(2); + if (len != 16) { + printf("ble_service_create: UUID must be 16 bytes\n"); + pushInt32(-1); + return true; + } + pushInt32(ble::service_create(&m->memory.bytes[addr])); + return true; +} + +def_prim(ble_characteristic_create, fiveToOneU32) { + uint32_t service_index = arg4.uint32; + uint32_t addr = arg3.uint32; + uint32_t len = arg2.uint32; + uint32_t properties = arg1.uint32; + uint32_t permissions = arg0.uint32; + pop_args(5); + if (len != 16) { + printf("ble_characteristic_create: UUID must be 16 bytes\n"); + pushInt32(-1); + return true; + } + pushInt32(ble::characteristic_create(service_index, &m->memory.bytes[addr], + properties, permissions)); + return true; +} + +def_prim(ble_characteristic_write, threeToOneU32) { + uint32_t index = arg2.uint32; + uint32_t addr = arg1.uint32; + uint32_t len = arg0.uint32; + pop_args(3); + pushInt32(ble::characteristic_write(index, &m->memory.bytes[addr], len)); + return true; +} + +def_prim(ble_characteristic_read, threeToOneU32) { + uint32_t index = arg2.uint32; + uint32_t addr = arg1.uint32; + uint32_t len = arg0.uint32; + pop_args(3); + pushInt32(ble::characteristic_read(index, &m->memory.bytes[addr], len)); + return true; +} + +def_prim(ble_characteristic_available, oneToOneU32) { + uint32_t index = arg0.uint32; + pop_args(1); + pushInt32(ble::characteristic_available(index)); + return true; +} + +def_prim(ble_advertise_start, NoneToOneU32) { + pushInt32(ble::advertise_start()); + return true; +} + +def_prim(ble_advertise_stop, NoneToOneU32) { + pushInt32(ble::advertise_stop()); + return true; +} + +def_prim(ble_connected, NoneToOneU32) { + pushUInt32(ble::connected()); + return true; +} + +#endif + //------------------------------------------------------ // Installing all the primitives //------------------------------------------------------ @@ -710,6 +794,18 @@ void install_primitives(Interpreter *interpreter) { install_primitive(socket_receive); install_primitive(socket_close); #endif + +#if IS_ENABLED(CONFIG_BT) + install_primitive(ble_enable); + install_primitive(ble_service_create); + install_primitive(ble_characteristic_create); + install_primitive(ble_characteristic_write); + install_primitive(ble_characteristic_read); + install_primitive(ble_characteristic_available); + install_primitive(ble_advertise_start); + install_primitive(ble_advertise_stop); + install_primitive(ble_connected); +#endif } Memory external_mem = {0, 0, 0, nullptr};