diff --git a/drivers/isl29020/isl29020_saul.c b/drivers/isl29020/isl29020_saul.c index cd514d3869c9..365838e5b027 100644 --- a/drivers/isl29020/isl29020_saul.c +++ b/drivers/isl29020/isl29020_saul.c @@ -28,7 +28,7 @@ static int read(const void *dev, phydat_t *res) res->val[0] = (int16_t)isl29020_read((const isl29020_t *)dev); res->val[1] = 0; res->val[2] = 0; - res->unit = UNIT_CD; + res->unit = UNIT_LUX; /* https://www.renesas.com/en/document/dst/isl29020-datasheet */ res->scale = 0; return 1; } diff --git a/examples/README.md b/examples/README.md index 030edface304..c387ed436eec 100644 --- a/examples/README.md +++ b/examples/README.md @@ -77,6 +77,7 @@ Here is a quick overview of the examples available in the RIOT: |---------|-------------| | [skald_eddystone](./skald_eddystone/README.md) | This example demonstrates the usage of `Skald` for creating an Google `Eddystone` beacon. | | [skald_ibeacon](./skald_ibeacon/README.md) | This example demonstrates the usage of `Skald` for creating an Apple `iBeacon`. | +| [skald_bthome](./skald_bthome/README.md) | This example demonstrates the usage of `Skald` for sending SAUL measurements via [`BTHome`](https://bthome.io). | ### MQTT diff --git a/examples/skald_bthome/Makefile b/examples/skald_bthome/Makefile new file mode 100644 index 000000000000..a2724fef785e --- /dev/null +++ b/examples/skald_bthome/Makefile @@ -0,0 +1,31 @@ +# name of your application +APPLICATION = skald_bthome + +# If no BOARD is found in the environment, use this default: +BOARD ?= nrf52dk + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../.. + +# include Skald using SAUL +USEMODULE += saul_default +USEMODULE += skald_bthome_saul + +DEVELHELP ?= 1 + +BTHOME_NAME = "RIOT" +BTHOME_ADV_INTERVAL ?= 60000 +ENCRYPTION_KEY ?= + +ifneq ($(ENCRYPTION_KEY),) + USEMODULE += skald_bthome_encrypt + CFLAGS += -DENCRYPTION_KEY="\"$(ENCRYPTION_KEY)\"" +endif + +CFLAGS += -DBTHOME_NAME="\"$(BTHOME_NAME)\"" +CFLAGS += -DBTHOME_ADV_INTERVAL=$(BTHOME_ADV_INTERVAL) + +# Change this to 0 show compiler invocation lines by default: +QUIET ?= 1 + +include $(RIOTBASE)/Makefile.include diff --git a/examples/skald_bthome/README.md b/examples/skald_bthome/README.md new file mode 100644 index 000000000000..9604e04d152e --- /dev/null +++ b/examples/skald_bthome/README.md @@ -0,0 +1,7 @@ +# Skald BTHome Example + +This example demonstrates the usage of `Skald` for creating a BTHome +setup, advertising sensors in the SAUL registry. + +Simply compile and flash, and verify your newly created beacon with any type of +BLE scanner / BTHome receiver (e.g. Home Assistant). diff --git a/examples/skald_bthome/main.c b/examples/skald_bthome/main.c new file mode 100644 index 000000000000..e2c48ecf28e7 --- /dev/null +++ b/examples/skald_bthome/main.c @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2024 Martine S. Lenders + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup examples + * @{ + * + * @file + * @brief BLE BTHome example using Skald + * + * @author Martine S. Lenders + * + * @} + */ + +#include +#include + +#include "saul_reg.h" +#include "ztimer.h" + +#include "net/skald/bthome.h" + +#ifndef CONFIG_BTHOME_SAUL_REG_DEVS +#define CONFIG_BTHOME_SAUL_REG_DEVS (16U) +#endif + +#ifndef BTHOME_ADV_INTERVAL +#define BTHOME_ADV_INTERVAL (60000U) +#endif + +static skald_bthome_ctx_t _ctx; +static skald_bthome_saul_t _saul_devs[CONFIG_BTHOME_SAUL_REG_DEVS]; + +#ifdef ENCRYPTION_KEY +int _get_encryption_key(void) +{ + static const char enc_str[] = ENCRYPTION_KEY; + static uint8_t encryption_key[SKALD_BTHOME_KEY_LEN] = { 0 }; + size_t enc_str_len = strlen(enc_str); + uint8_t key_len = 0; + + uint8_t shift = 4; + for (unsigned i = 0; i < enc_str_len; i++) { + char c = enc_str[i]; + unsigned char offset; + + if (c >= '0' && c <= '9') { + offset = '0'; + } + else if (c >= 'a' && c <= 'f') { + offset = 'a' - 10; + } + else { + continue; + } + encryption_key[i / 2] |= (c - offset) << shift; + shift = (shift) ? 0 : 4; + key_len = (i / 2) + 1; + if (key_len > SKALD_BTHOME_KEY_LEN) { + printf("Key was too long: %u bytes\n", key_len); + return -ENOBUFS; + } + } + + if (key_len < SKALD_BTHOME_KEY_LEN) { + printf("Key was too short: %u bytes\n", key_len); + return -EINVAL; + } + memcpy(_ctx.key, encryption_key, key_len); + _ctx.encrypt = 1; + return key_len; +} +#endif + +static int _add_text(skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + static const char info[] = "RIOT"; + + (void)data; + (void)idx; + return skald_bthome_add_measurement(ctx, obj_id, info, strlen(info)); +} + +int main(void) +{ + saul_reg_t *dev = saul_reg; + unsigned i = 0; + int res; + + ztimer_sleep(ZTIMER_MSEC, 2000); + printf("Skald and the tale of Harald's home\n"); + +#ifdef ENCRYPTION_KEY + int key_len; + + key_len = _get_encryption_key(); + + if (key_len != SKALD_BTHOME_KEY_LEN) { + printf( + "Key should be of length 16 (32 chars), " + "was \"%s\" (%u chars)\n", + ENCRYPTION_KEY, + strlen(ENCRYPTION_KEY) + ); + return 1; + } +#endif + _ctx.skald.update_pkt = NULL; + _ctx.devs = NULL; + _ctx.skald.pkt.len = 0; + if (skald_bthome_init(&_ctx, NULL, BTHOME_NAME, 0) < 0) { + return 1; + } + if (!saul_reg) { + puts("Hark! The board does not know SAUL. :-("); + return 1; + } + while (dev && (i < CONFIG_BTHOME_SAUL_REG_DEVS)) { + _saul_devs[i].saul = *dev; /* copy registry entry */ + _saul_devs[i].saul.next = NULL; + printf("Adding %s (%s) to BTHome.\n", dev->name, saul_class_to_str(dev->driver->type)); + if ((res = skald_bthome_saul_add(&_ctx, &_saul_devs[i])) < 0) { + errno = -res; + perror("Unable to add sensor to BTHome"); + dev = dev->next; + continue; + }; + i++; + dev = dev->next; + } + assert(!saul_reg || _ctx.devs); + if (i < CONFIG_BTHOME_SAUL_REG_DEVS) { + memset(&_saul_devs[i].saul, 0, sizeof(_saul_devs[i].saul)); + _saul_devs[i].obj_id = BTHOME_ID_TEXT; + _saul_devs[i].flags = SKALD_BTHOME_SAUL_FLAGS_CUSTOM; + _saul_devs[i].add_measurement = _add_text; + if ((res = skald_bthome_saul_add(&_ctx, &_saul_devs[i])) < 0) { + errno = -res; + perror("Unable to add text info to BTHome"); + }; + i++; + } + skald_bthome_advertise(&_ctx, BTHOME_ADV_INTERVAL); + return 0; +} diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index c6cb490c27d0..73f7c068d6c5 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -590,8 +590,12 @@ NO_PSEUDOMODULES += periph_common PSEUDOMODULES += pio_autostart_% # Submodules provided by Skald +PSEUDOMODULES += skald_update_pkt_cb PSEUDOMODULES += skald_ibeacon PSEUDOMODULES += skald_eddystone +PSEUDOMODULES += skald_bthome +PSEUDOMODULES += skald_bthome_encrypt +PSEUDOMODULES += skald_bthome_saul PSEUDOMODULES += crypto_aes_128 PSEUDOMODULES += crypto_aes_192 diff --git a/sys/Makefile.dep b/sys/Makefile.dep index 00afb08a25ee..138790ba53ad 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -581,6 +581,22 @@ ifneq (,$(filter skald,$(USEMODULE))) USEMODULE += ztimer_msec endif +ifneq (,$(filter skald_bthome_encrypt,$(USEMODULE))) + USEMODULE += cipher_modes + USEMODULE += skald_bthome +endif + +ifneq (,$(filter skald_bthome_saul,$(USEMODULE))) + USEMODULE += skald_bthome + USEMODULE += skald_update_pkt_cb + USEMODULE += saul_reg +endif + +ifneq (,$(filter skald_update_pkt_cb,$(USEMODULE))) + USEMODULE += event + USEMODULE += skald +endif + ifneq (,$(filter bluetil_addr,$(USEMODULE))) USEMODULE += fmt endif diff --git a/sys/include/net/skald.h b/sys/include/net/skald.h index 91c3cbfa0f58..46c39a793cff 100644 --- a/sys/include/net/skald.h +++ b/sys/include/net/skald.h @@ -46,10 +46,15 @@ #include +#include "kernel_defines.h" #include "ztimer.h" #include "net/ble.h" #include "net/netdev/ble.h" +#if IS_USED(MODULE_SKALD_UPDATE_PKT_CB) +#include "event.h" +#endif + #ifdef __cplusplus extern "C" { #endif @@ -68,16 +73,41 @@ typedef struct { uint8_t u8[16]; /**< UUID with byte-wise access */ } skald_uuid_t; +/** + * @brief Forward declaration + */ +typedef struct skald_ctx skald_ctx_t; + /** * @brief Advertising context holding the advertising data and state */ -typedef struct { - netdev_ble_pkt_t pkt; /**< packet holding the advertisement (GAP) data */ - ztimer_t timer; /**< timer for scheduling advertising events */ - ztimer_now_t last; /**< last timer trigger (for offset compensation) */ - uint8_t cur_chan; /**< keep track of advertising channels */ - uint32_t adv_itvl_ms; /**< advertising interval [ms] */ -} skald_ctx_t; +struct skald_ctx { + netdev_ble_pkt_t pkt; /**< packet holding the advertisement (GAP) data */ +#if IS_USED(MODULE_SKALD_UPDATE_PKT_CB) || defined(DOXYGEN) + /** + * @brief callback to update packet on periodic advertisements + * + * Requires module `skald_pkt_update_cb`. + */ + void (*update_pkt)(skald_ctx_t *); + /** + * @brief Event queue to send periodic advertisements from + * + * Only available and needed with module `skald_pkt_update_cb`. + */ + event_queue_t queue; + /** + * @brief The event to trigger to send periodic advertisements + * + * Only available and needed with module `skald_pkt_update_cb`. + */ + event_t event; +#endif + ztimer_t timer; /**< timer for scheduling advertising events */ + ztimer_now_t last; /**< last timer trigger (for offset compensation) */ + uint8_t cur_chan; /**< keep track of advertising channels */ + uint32_t adv_itvl_ms; /**< advertising interval [ms] */ +}; /** * @brief Initialize Skald and the underlying radio diff --git a/sys/include/net/skald/bthome.h b/sys/include/net/skald/bthome.h new file mode 100644 index 000000000000..249faaa02c0d --- /dev/null +++ b/sys/include/net/skald/bthome.h @@ -0,0 +1,490 @@ +/* + * Copyright (C) 2024 Martine S. Lenders + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +#ifndef NET_SKALD_BTHOME_H +#define NET_SKALD_BTHOME_H +/** + * @defgroup ble_skald_bthome Skald about BTHome + * @ingroup ble_skald + * @brief Skald's BTHome abstraction + * + * # About + * This Skald module supports the creation and advertisement of BTHome messages + * (see https://bthome.io) + * + * # Implementation state + * - BTHome v2 is mostly supported + * - [Encryption](https://bthome.io/encryption/) is supported and the format is + * confirmed to provide the expected output, but Home Assistant does not + * accept the encryption key to decrypt the message. + * - [Trigger based devices](https://bthome.io/format/#bthome-data-format) are not supported + * + * + * @{ + * + * @file + * @brief BTHome interface + * + * @author Martine S. Lenders + */ + +#include "byteorder.h" +#include "kernel_defines.h" +#include "net/skald.h" + +#if IS_USED(MODULE_SKALD_BTHOME_SAUL) +#include "saul_reg.h" +#endif + +#include "net/skald/bthome/defs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Length of the BTHome encryption key + * + * @see [BTHome Encryption Reference](https://bthome.io/encryption/) + */ +#define SKALD_BTHOME_KEY_LEN 16U + +/** + * @brief Forward declaration of @ref skald_bthome_ctx_t. + */ +typedef struct skald_bthome_ctx skald_bthome_ctx_t; + +#if IS_USED(MODULE_SKALD_BTHOME_SAUL) || defined(DOXYGEN) +/** + * @brief Flags for the BTHome-SAUL-adapter + */ +typedef enum { + /** + * @brief Use custom object ID and add-measurement callback + */ + SKALD_BTHOME_SAUL_FLAGS_CUSTOM = 0x01, +} skald_bthome_saul_flags_t; + +/** + * @brief BTHome-SAUL-adapter + */ +typedef struct { + /** + * @brief Copy of the SAUL registry entry. + * + * Must be initialized when calling @ref skald_bthome_saul_add() + * with saul_reg_t::next set to NULL. + */ + saul_reg_t saul; + /** + * @brief Object ID for the SAUL registry entry + * + * @see skald_bthome_id_t + * + * Will be filled by @ref skald_bthome_saul_add() if + * @ref SKALD_BTHOME_SAUL_FLAGS_CUSTOM is unset in + * skald_bthome_saul_t::flags. Otherwise, set to the desired + * object ID for the measurement. + */ + skald_bthome_id_t obj_id; + /** + * @brief Flags for the BTHome-SAUL-adapter + * + * @see skald_bthome_saul_flags_t + * + * May be set before calling @ref skald_bthome_saul_add() + */ + skald_bthome_saul_flags_t flags; + + /** + * @brief Callback to add measurement from SAUL registry entry + * + * Called directly after the measurement was taken via + * @ref saul_reg_read() and should ultimately call + * @ref skald_bthome_add_measurement() * (or one of its wrappers). + * + * Will be filled by @ref skald_bthome_saul_add() if + * @ref SKALD_BTHOME_SAUL_FLAGS_CUSTOM is unset in + * skald_bthome_saul_t::flags. Otherwise, set to the desired + * object ID for the measurement. + * + * @param[in,out] ctx BTHome advertising context. MUST not be NULL. + * @param[in] obj_id The object ID for the measurement. + * @param[in] data The @ref phydat_t to an element from. MUST not be NULL. + * @param[in] idx The index of phydat_t::val to take the measurement from. + * MUST be lesser than PHYDAT_DIM. + * + * @return The result of @ref skald_bthome_add_measurement() or one of its + * wrappers. + */ + int (*add_measurement)( + skald_bthome_ctx_t *ctx, + uint8_t obj_id, + phydat_t *data, + uint8_t idx + ); +} skald_bthome_saul_t; +#endif + +/** + * @brief BTHome advertising context holding the advertising data and state + */ +struct skald_bthome_ctx { + skald_ctx_t skald; /**< Skald context */ + /** + * @brief Pointer to service data length field + * + * Will point to the service data length field within skald_bthome_ctx_t::skald after calling + * @ref skald_bthome_init(). + */ + uint8_t *svc_data_len; +#if IS_USED(MODULE_SKALD_BTHOME_SAUL) || defined(DOXYGEN) + /** + * @brief SAUL devices to take measurements from. + * + * Fill this using @ref skald_bthome_saul_add(). + */ + skald_bthome_saul_t *devs; +#endif +#if IS_USED(MODULE_SKALD_BTHOME_ENCRYPT) || defined(DOXYGEN) + /** + * @brief The encryption key for BTHome + * + * @see [BTHome Encryption Reference](https://bthome.io/encryption/) + */ + uint8_t key[SKALD_BTHOME_KEY_LEN]; + /** + * @brief Enable encryption with BTHome + * + * @see [BTHome Encryption Reference](https://bthome.io/encryption/) + * + * A non-zero key should be set in skald_bthome_ctx_t::key in this case. + */ + uint8_t encrypt; +#endif +#if IS_USED(MODULE_SKALD_BTHOME_SAUL) || defined(DOXYGEN) + /** + * @brief The index of the last device sent in skald_bthome_ctx_t::devs. + * + * Will be updated on each periodic advertisement to allow for fragmenting + * different measurement readings across multiple advertisements (in case all + * measurements from skald_bthome_ctx_t::devs are too large for one + * advertisement). Is initialized to 0. + * + * If a single reading is too big to fit into an advertisement, + * the skald_ctx_t::update_pkt() callback will just return (i.e. BTHome + * payload may be left empty) and skald_bthome_ctx_t::last_dev_sent will + * be reset to 0. + * This can e.g. happen with a @ref BTHOME_ID_TEXT or @ref BTHOME_ID_RAW record + * if the appended bytes are larger than a BLE advertisement. + */ + uint8_t last_dev_sent; +#endif +}; + +/** + * @brief Initialize the next BTHome advertisement + * + * @pre `ctx != NULL` + * + * @param[out] ctx The BTHome context + * @param[in] shortened_name The shortened name for the BTHome advertisement. May be NULL, if no + * shortened name should be advertised. Must end with '\0' and may not + * make the length of the PDU exceed @ref NETDEV_BLE_PDU_MAXLEN + * @param[in] complete_name The complete name for the BTHome advertisement. May be NULL, if no + * complete name should be advertised. Must end with '\0' and may not + * make the length of the PDU exceed @ref NETDEV_BLE_PDU_MAXLEN + * @param[in] dev_info Flags for the device information. Currently, no flags are supported. + * + * @return The current size of the advertisement on success + * @return -EMSGSIZE, when @p shortened_name or @p complete_name cause the message to exceed + * @ref NETDEV_BLE_PDU_MAXLEN. With NDEBUG=0 an assertion is raised in this case. + */ +int skald_bthome_init( + skald_bthome_ctx_t *ctx, + const char *shortened_name, const char *complete_name, + uint8_t dev_info +); + +/** + * @brief Adds a measurement to the next BTHome advertisement + * @warning This does not check if the @p obj_id was inserted in the right order (they have to be + * added in numerical order from low to high) or if @p data_len is the correct data length + * for @p obj_id. It just checks if @p data_len fits within @p NETDEV_BLE_PDU_MAXLEN of the + * PDU. + * + * @pre `ctx != NULL` and `ctx` was initialized with @ref skald_bthome_init(). + * @pre @p data fits within the advertisement in @p ctx. + * + * @param[in,out] ctx The BTHome context. Must not be NULL and must be initialized with + * @ref skald_bthome_init(). + * @param[in] obj_id An object ID (see @ref skald_bthome_id_t). + * @param[in] data The measurement data. Must not be NULL. + * @param[in] data_len The length of @p data. + * + * @return The current size of the advertisement on success + * @return -EMSGSIZE, when @p data_len cause the message to exceed @ref NETDEV_BLE_PDU_MAXLEN. + * With NDEBUG=0 an assertion is raised in this case. + */ +int skald_bthome_add_measurement( + skald_bthome_ctx_t *ctx, + skald_bthome_id_t obj_id, + const void *data, + uint8_t data_len +); + +/** + * @brief Adds a one byte unsigned measurement to the next BTHome advertisement + * @warning This does not check if the @p obj_id was inserted in the right order (they have to be + * added in numerical order from low to high) or if @p data_len is the correct data length + * for @p obj_id. It just checks if @p data_len fits within @p NETDEV_BLE_PDU_MAXLEN of the + * PDU. + * + * @pre `ctx != NULL` and `ctx` was initialized with @ref skald_bthome_init(). + * @pre @p data fits within the advertisement in @p ctx. + * + * @param[in,out] ctx The BTHome context. Must not be NULL and must be initialized with + * @ref skald_bthome_init(). + * @param[in] obj_id An object ID (see @ref skald_bthome_id_t). + * @param[in] data The measurement data. + * + * @return The current size of the advertisement on success + * @return -EMSGSIZE, when @p data cause the message to exceed @ref NETDEV_BLE_PDU_MAXLEN. + * With NDEBUG=0 an assertion is raised in this case. + */ +static inline int skald_bthome_add_uint8_measurement( + skald_bthome_ctx_t *ctx, + skald_bthome_id_t obj_id, + uint8_t data +) +{ + return skald_bthome_add_measurement(ctx, obj_id, &data, sizeof(data)); +} + +/** + * @brief Adds a one byte signed measurement to the next BTHome advertisement + * @warning This does not check if the @p obj_id was inserted in the right order (they have to be + * added in numerical order from low to high) or if @p data_len is the correct data length + * for @p obj_id. It just checks if @p data_len fits within @p NETDEV_BLE_PDU_MAXLEN of the + * PDU. + * + * @pre `ctx != NULL` and `ctx` was initialized with @ref skald_bthome_init(). + * @pre @p data fits within the advertisement in @p ctx. + * + * @param[in,out] ctx The BTHome context. Must not be NULL and must be initialized with + * @ref skald_bthome_init(). + * @param[in] obj_id An object ID (see @ref skald_bthome_id_t). + * @param[in] data The measurement data. + * + * @return The current size of the advertisement on success + * @return -EMSGSIZE, when @p data cause the message to exceed @ref NETDEV_BLE_PDU_MAXLEN. + * With NDEBUG=0 an assertion is raised in this case. + */ +static inline int skald_bthome_add_int8_measurement( + skald_bthome_ctx_t *ctx, + skald_bthome_id_t obj_id, + int8_t data +) +{ + return skald_bthome_add_measurement(ctx, obj_id, &data, sizeof(data)); +} + +/** + * @brief Adds a two byte unsigned measurement to the next BTHome advertisement + * @warning This does not check if the @p obj_id was inserted in the right order (they have to be + * added in numerical order from low to high) or if @p data_len is the correct data length + * for @p obj_id. It just checks if @p data_len fits within @p NETDEV_BLE_PDU_MAXLEN of the + * PDU. + * + * @pre `ctx != NULL` and `ctx` was initialized with @ref skald_bthome_init(). + * @pre @p data fits within the advertisement in @p ctx. + * + * @param[in,out] ctx The BTHome context. Must not be NULL and must be initialized with + * @ref skald_bthome_init(). + * @param[in] obj_id An object ID (see @ref skald_bthome_id_t). + * @param[in] data The measurement data. + * + * @return The current size of the advertisement on success + * @return -EMSGSIZE, when @p data cause the message to exceed @ref NETDEV_BLE_PDU_MAXLEN. + * With NDEBUG=0 an assertion is raised in this case. + */ +static inline int skald_bthome_add_uint16_measurement( + skald_bthome_ctx_t *ctx, + skald_bthome_id_t obj_id, + uint16_t data +) +{ + uint16_t le_data = htole16(data); + return skald_bthome_add_measurement(ctx, obj_id, &le_data, sizeof(data)); +} + +/** + * @brief Adds a two byte signed measurement to the next BTHome advertisement + * @warning This does not check if the @p obj_id was inserted in the right order (they have to be + * added in numerical order from low to high) or if @p data_len is the correct data length + * for @p obj_id. It just checks if @p data_len fits within @p NETDEV_BLE_PDU_MAXLEN of the + * PDU. + * + * @pre `ctx != NULL` and `ctx` was initialized with @ref skald_bthome_init(). + * @pre @p data fits within the advertisement in @p ctx. + * + * @param[in,out] ctx The BTHome context. Must not be NULL and must be initialized with + * @ref skald_bthome_init(). + * @param[in] obj_id An object ID (see @ref skald_bthome_id_t). + * @param[in] data The measurement data. + * + * @return The current size of the advertisement on success + * @return -EMSGSIZE, when @p data cause the message to exceed @ref NETDEV_BLE_PDU_MAXLEN. + * With NDEBUG=0 an assertion is raised in this case. + */ +static inline int skald_bthome_add_int16_measurement( + skald_bthome_ctx_t *ctx, + skald_bthome_id_t obj_id, + int16_t data +) +{ + uint16_t le_data = htole16((uint16_t)data); + return skald_bthome_add_measurement(ctx, obj_id, &le_data, sizeof(data)); +} + +/** + * @brief Adds a three byte unsigned measurement to the next BTHome advertisement + * @warning This does not check if the @p obj_id was inserted in the right order (they have to be + * added in numerical order from low to high) or if @p data_len is the correct data length + * for @p obj_id. It just checks if @p data_len fits within @p NETDEV_BLE_PDU_MAXLEN of the + * PDU. + * + * @pre `ctx != NULL` and `ctx` was initialized with @ref skald_bthome_init(). + * @pre @p data fits within the advertisement in @p ctx. + * + * @param[in,out] ctx The BTHome context. Must not be NULL and must be initialized with + * @ref skald_bthome_init(). + * @param[in] obj_id An object ID (see @ref skald_bthome_id_t). + * @param[in] data The measurement data. + * + * @return The current size of the advertisement on success + * @return -EMSGSIZE, when @p data cause the message to exceed @ref NETDEV_BLE_PDU_MAXLEN. + * With NDEBUG=0 an assertion is raised in this case. + */ +static inline int skald_bthome_add_uint24_measurement( + skald_bthome_ctx_t *ctx, + skald_bthome_id_t obj_id, + uint32_t data +) +{ + uint32_t le_data = htole32(data); + return skald_bthome_add_measurement(ctx, obj_id, &le_data, 3U); +} + +/** + * @brief Adds a four byte unsigned measurement to the next BTHome advertisement + * @warning This does not check if the @p obj_id was inserted in the right order (they have to be + * added in numerical order from low to high) or if @p data_len is the correct data length + * for @p obj_id. It just checks if @p data_len fits within @p NETDEV_BLE_PDU_MAXLEN of the + * PDU. + * + * @pre `ctx != NULL` and `ctx` was initialized with @ref skald_bthome_init(). + * @pre @p data fits within the advertisement in @p ctx. + * + * @param[in,out] ctx The BTHome context. Must not be NULL and must be initialized with + * @ref skald_bthome_init(). + * @param[in] obj_id An object ID (see @ref skald_bthome_id_t). + * @param[in] data The measurement data. + * + * @return The current size of the advertisement on success + * @return -EMSGSIZE, when @p data cause the message to exceed @ref NETDEV_BLE_PDU_MAXLEN. + * With NDEBUG=0 an assertion is raised in this case. + */ +static inline int skald_bthome_add_uint32_measurement( + skald_bthome_ctx_t *ctx, + skald_bthome_id_t obj_id, + uint32_t data +) +{ + uint32_t le_data = htole32(data); + return skald_bthome_add_measurement(ctx, obj_id, &le_data, sizeof(data)); +} + +/** + * @brief Adds a four byte signed measurement to the next BTHome advertisement + * @warning This does not check if the @p obj_id was inserted in the right order (they have to be + * added in numerical order from low to high) or if @p data_len is the correct data length + * for @p obj_id. It just checks if @p data_len fits within @p NETDEV_BLE_PDU_MAXLEN of the + * PDU. + * + * @pre `ctx != NULL` and `ctx` was initialized with @ref skald_bthome_init(). + * @pre @p data fits within the advertisement in @p ctx. + * + * @param[in,out] ctx The BTHome context. Must not be NULL and must be initialized with + * @ref skald_bthome_init(). + * @param[in] obj_id An object ID (see @ref skald_bthome_id_t). + * @param[in] data The measurement data. + * + * @return The current size of the advertisement on success + * @return -EMSGSIZE, when @p data cause the message to exceed @ref NETDEV_BLE_PDU_MAXLEN. + * With NDEBUG=0 an assertion is raised in this case. + */ +static inline int skald_bthome_add_int32_measurement( + skald_bthome_ctx_t *ctx, + skald_bthome_id_t obj_id, + int32_t data +) +{ + uint32_t le_data = htole32((uint32_t)data); + return skald_bthome_add_measurement(ctx, obj_id, &le_data, sizeof(data)); +} + +#if IS_USED(MODULE_SKALD_BTHOME_SAUL) || defined(DOXYGEN) +/** + * @brief Add SAUL registry entry to BTHome + * + * skald_bthome_saul_t::saul of @p saul must be a copy of what is found in @ref sys_saul_reg. + * + * @see @ref sys_saul_reg + * + * @param[in,out] ctx The BTHome context. Must not be NULL and must be initialized with + * @ref skald_bthome_init(). + * @param[in] saul A BTHome-SAUL-adapter. + * + * @retval 0 on success. + * @retval -ENOTSUP if the SAUL registry entry of @p saul has no driver. + * @retval -ENODEV if reading the SAUL device results in an error. + * @retval -ENOENT if the SAUL device and its result are not convertible to a sensible + * object ID. + */ +int skald_bthome_saul_add(skald_bthome_ctx_t *ctx, skald_bthome_saul_t *saul); +#endif + +#if IS_USED(MODULE_SKALD_BTHOME_ENCRYPT) || defined(DOXYGEN) +/** + * @brief Encrypt the packet in the Skald context + * + * @param[in] ctx A BTHome Skald context. Must not be NULL. + * + * @return The total size of the encrypted packet on success. + * @return -ENOBUFS, if the encrypted packet would not fit into a + * BLE advertisement. + */ +int skald_bthome_encrypt(skald_bthome_ctx_t *ctx); +#endif + +/** + * @brief Starts periodically advertising the BTHome advertisement + * + * @param[in] ctx The BTHome context. Must not be NULL and must be initialized with + * @ref skald_bthome_init(). + * @param[in] adv_itvl_ms advertising interval in milliseconds + */ +void skald_bthome_advertise(skald_bthome_ctx_t *ctx, uint32_t adv_itvl_ms); + +#ifdef __cplusplus +} +#endif + +/** @} */ +#endif /* NET_SKALD_BTHOME_H */ diff --git a/sys/include/net/skald/bthome/defs.h b/sys/include/net/skald/bthome/defs.h new file mode 100644 index 000000000000..b59c85afd89f --- /dev/null +++ b/sys/include/net/skald/bthome/defs.h @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2024 Martine S. Lenders + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +#ifndef NET_SKALD_BTHOME_DEFS_H +#define NET_SKALD_BTHOME_DEFS_H +/** + * @defgroup ble_skald_bthome_defs BTHome definitions + * @ingroup ble_skald_bthome + * @brief Numbers for BTHome + * + * @{ + * + * @file + * @brief BTHome definitions interface + * + * @author Martine S. Lenders + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief BTHome object IDs + * + * _v_ is the value send, with the type in parenthesis. + */ +typedef enum { + /* ==================== 0x00 ==================== */ + BTHOME_ID_PACKET_ID = 0x00, /**< Packet ID as _v_ (uint8_t) */ + BTHOME_ID_BATTERY = 0x01, /**< Battery in _v_ % (uint8_t) */ + BTHOME_ID_TEMPERATURE_FACTOR_0_01 = 0x02, /**< Temperature in _v_ * 0.01 °C (int16_t) */ + BTHOME_ID_HUMIDITY_FACTOR_0_01 = 0x03, /**< Humidity in _v_ * 0.01 % (uint16_t) */ + BTHOME_ID_PRESSURE_FACTOR_0_01 = 0x04, /**< Pressure in _v_ * 0.01 hPa (uint24_t) */ + BTHOME_ID_ILLUMINANCE_FACTOR_0_01 = 0x05, /**< Illuminance in _v_ * 0.01 lux (uint24_t) */ + BTHOME_ID_MASS_KG_FACTOR_0_01 = 0x06, /**< Mass in _v_ * 0.01 kg (uint16_t) */ + BTHOME_ID_MASS_LB_FACTOR_0_01 = 0x07, /**< Mass in _v_ * 0.01 lb (uint16_t) */ + BTHOME_ID_DEWPOINT_0_01 = 0x08, /**< Dewpoint in _v_ * 0.01 °C (int16_t) */ + BTHOME_ID_COUNT_UINT8 = 0x09, /**< Count as _v_ (uint8_t) */ + BTHOME_ID_ENERGY_3B_FACTOR_0_001 = 0x0a, /**< Energy in _v_ * 0.001 kWh (uint24_t) */ + BTHOME_ID_POWER_UINT_FACTOR_0_01 = 0x0b, /**< Power in _v_ * 0.01 W (uint24_t) */ + BTHOME_ID_VOLTAGE_FACTOR_0_001 = 0x0c, /**< Voltage in _v_ * 0.001 V (uint16_t) */ + /** + * @brief PM2.5 (very fine particular matter) in _v_ ug/m3 (uint16_t) + */ + BTHOME_ID_PM_2_5 = 0x0d, + /** + * @brief PM10 (fine particular matter) in _v_ ug/m3 (uint16_t) + */ + BTHOME_ID_PM_10 = 0x0e, + /** + * @brief Generic boolean (uint8_t) + * + * 0 = False, 1 = True + */ + BTHOME_ID_BOOLEAN = 0x0f, + /* ==================== 0x10 ==================== */ + /** + * @brief Power as binary state (uint8_t) + * + * 0 = Off, 1 = On + */ + BTHOME_ID_POWER_BINARY = 0x10, + /** + * @brief Opening as binary state (uint8_t) + * + * 0 = Closed, 1 = Open + */ + BTHOME_ID_OPENING_BINARY = 0x11, + BTHOME_ID_CO2 = 0x12, /**< CO2 in _v_ ppm (uint16_t) */ + BTHOME_ID_TVOC = 0x13, /**< TVOC in _v_ ug/m3 (uint16_t) */ + BTHOME_ID_MOISTURE_FACTOR_0_01 = 0x14, /**< Moisture in _v_ * 0.01 % (uint16_t) */ + /** + * @brief Battery as binary state (uint8_t) + * + * 0 = Normal, 1 = Low + */ + BTHOME_ID_BATTERY_BINARY = 0x15, + /** + * @brief Battery charging as binary state (uint8_t) + * + * 0 = Not charging, 1 = Charging + */ + BTHOME_ID_BATTERY_CHARGING_BINARY = 0x16, + /** + * @brief Carbon monoxid as binary state (uint8_t) + * + * 0 = Not detected, 1 = Detected + */ + BTHOME_ID_CARBON_MONOXIDE_BINARY = 0x17, + /** + * @brief Cold as binary state (uint8_t) + * + * 0 = Normal, 1 = Cold + */ + BTHOME_ID_COLD_BINARY = 0x18, + /** + * @brief Connectivity as binary state (uint8_t) + * + * 0 = Disconnected, 1 = Connected + */ + BTHOME_ID_CONNECTIVITY_BINARY = 0x19, + /** + * @brief Door as binary state (uint8_t) + * + * 0 = Closed, 1 = Open + */ + BTHOME_ID_DOOR_BINARY = 0x1a, + /** + * @brief Garage door as binary state (uint8_t) + * + * 0 = Closed, 1 = Open + */ + BTHOME_ID_GARAGE_DOOR_BINARY = 0x1b, + /** + * @brief Gas as binary state (uint8_t) + * + * 0 = Clear, 1 = Detected + */ + BTHOME_ID_GAS_BINARY = 0x1c, + /** + * @brief Heat as binary state (uint8_t) + * + * 0 = Normal, 1 = Hot + */ + BTHOME_ID_HEAT_BINARY = 0x1d, + /** + * @brief Light as binary state (uint8_t) + * + * 0 = No light, 1 = Light detected + */ + BTHOME_ID_LIGHT_BINARY = 0x1e, + /** + * @brief Lock as binary state (uint8_t) + * + * 0 = Locked, 1 = Unlocked + */ + BTHOME_ID_LOCKED_BINARY = 0x1f, + /* ==================== 0x20 ==================== */ + /** + * @brief Moisture as binary state (uint8_t) + * + * 0 = Dry, 1 = Wet + */ + BTHOME_ID_MOISTURE_BINARY = 0x20, + /** + * @brief Motion as binary state (uint8_t) + * + * 0 = Clear, 1 = Detected + */ + BTHOME_ID_MOTION_BINARY = 0x21, + /** + * @brief Moving as binary state (uint8_t) + * + * 0 = Not moving, 1 = Moving + */ + BTHOME_ID_MOVING_BINARY = 0x22, + /** + * @brief Occupancy as binary state (uint8_t) + * + * 0 = Clear, 1 = Detected + */ + BTHOME_ID_OCCUPANCY_BINARY = 0x23, + /** + * @brief Plug as binary state (uint8_t) + * + * 0 = Unplugged, 1 = Plugged in + */ + BTHOME_ID_PLUG_BINARY = 0x24, + /** + * @brief Presence as binary state (uint8_t) + * + * 0 = Away, 1 = Home + */ + BTHOME_ID_PRESENCE_BINARY = 0x25, + /** + * @brief Problem as binary state (uint8_t) + * + * 0 = OK, 1 = Problem + */ + BTHOME_ID_PROBLEM_BINARY = 0x26, + /** + * @brief Running as binary state (uint8_t) + * + * 0 = Not running, 1 = Running + */ + BTHOME_ID_RUNNING_BINARY = 0x27, + /** + * @brief Safety as binary state (uint8_t) + * + * 0 = Unsafe, 1 = Safe + */ + BTHOME_ID_SAFETY_BINARY = 0x28, + /** + * @brief Smoke as binary state (uint8_t) + * + * 0 = Clear, 1 = Detected + */ + BTHOME_ID_SMOKE_BINARY = 0x29, + /** + * @brief Sound as binary state (uint8_t) + * + * 0 = Clear, 1 = Detected + */ + BTHOME_ID_SOUND_BINARY = 0x2a, + /** + * @brief Tamper as binary state (uint8_t) + * + * 0 = Off, 1 = On + */ + BTHOME_ID_TAMPER_BINARY = 0x2b, + /** + * @brief Vibration as binary state (uint8_t) + * + * 0 = Clear, 1 = Detected + */ + BTHOME_ID_VIBRATION_BINARY = 0x2c, + /** + * @brief Window as binary state (uint8_t) + * + * 0 = Closed, 1 = Open + */ + BTHOME_ID_WINDOW_BINARY = 0x2d, + BTHOME_ID_HUMIDITY_FACTOR_1 = 0x2e, /**< Humidity in _v_ % (uint8_t) */ + BTHOME_ID_MOISTURE_FACTOR_1 = 0x2f, /**< Moisture in _v_ % (uint8_t) */ + /* ==================== 0x30 ==================== */ + /** + * @brief Button as _v_ (uint8_t) + * + * @see @ref skald_bthome_btn_t for value of _v_ + */ + BTHOME_ID_BUTTON = 0x3a, + /** + * @brief Dimmer as _v_ (uint16_t) + * + * 0x00XX for none, 0x01XX for XX steps left, 0x02XX for XX steps right + */ + BTHOME_ID_DIMMER = 0x3c, + BTHOME_ID_COUNT_UINT16 = 0x3d, /**< Count as _v_ (uint16_t) */ + BTHOME_ID_COUNT_UINT32 = 0x3e, /**< Count as _v_ (uint32_t) */ + BTHOME_ID_ROTATION_FACTOR_0_1 = 0x3f, /**< Rotation in _v_ (int16_t) */ + /* ==================== 0x40 ==================== */ + BTHOME_ID_DISTANCE_MM = 0x40, /**< Distance in _v_ mm (uint16_t) */ + BTHOME_ID_DISTANCE_M = 0x41, /**< Distance in _v_ m (uint16_t) */ + BTHOME_ID_DURATION_FACTOR_0_001 = 0x42, /**< Distance in _v_ m (uint16_t) */ + BTHOME_ID_CURRENT_UINT_FACTOR_0_001 = 0x43, /**< Current in _v_ * 0.001 A (uint16_t) */ + BTHOME_ID_SPEED_FACTOR_0_01 = 0x44, /**< Speed in _v_ * 0.01 m/s (uint16_t) */ + BTHOME_ID_TEMPERATURE_FACTOR_0_1 = 0x45, /**< Temperature in _v_ * 0.1 °C (int16_t) */ + BTHOME_ID_UV_INDEX_FACTOR_0_1 = 0x44, /**< Volume in _v_ * 0.1 (uint8_t) */ + BTHOME_ID_VOLUME_L_FACTOR_0_1 = 0x47, /**< Volume in _v_ * 0.1 l (uint16_t) */ + BTHOME_ID_VOLUME_ML_FACTOR_0_1 = 0x48, /**< Volume in _v_ * 0.1 ml (uint16_t) */ + /** + * @brief Volume flow rate in _v_ * 0.001 m3/h (uint16_t) + */ + BTHOME_ID_VOLUME_FLOW_RATE_FACTOR_0_001 = 0x49, + BTHOME_ID_VOLTAGE_FACTOR_0_1 = 0x4a, /**< Voltage in _v_ * 0.1 V (uint16_t) */ + BTHOME_ID_GAS_3B_FACTOR_0_001 = 0x4b, /**< Gas in _v_ * 0.001 m3 (uint24_t) */ + BTHOME_ID_GAS_4B_FACTOR_0_001 = 0x4c, /**< Gas in _v_ * 0.001 m3 (uint32_t) */ + BTHOME_ID_ENERGY_4B_FACTOR_0_001 = 0x4d, /**< Energy in _v_ * 0.001 kWh (uint32_t) */ + BTHOME_ID_VOLUME_L_FACTOR_0_001 = 0x4e, /**< Volume in _v_ * 0.001 l (uint32_t) */ + BTHOME_ID_WATER_FACTOR_0_001 = 0x4f, /**< Water in _v_ * 0.001 l (uint32_t) */ + /* ==================== 0x50 ==================== */ + BTHOME_ID_TIMESTAMP = 0x50, /**< Timestamp in _v_ s since UNIX epoch (uint48) */ + BTHOME_ID_ACCELERATION_FACTOR_0_001 = 0x51, /**< Acceleration in _v_ m/s² */ + BTHOME_ID_GYROSCOPE_FACTOR_0_001 = 0x52, /**< Gyroscope in _v_ °/s */ + /** + * @brief Volume storage in _v_ * 0.001 l (uint32_t) + */ + BTHOME_ID_VOLUME_STORAGE_FACTOR_0_001 = 0x55, + BTHOME_ID_TEMPERATURE_FACTOR_1 = 0x57, /**< Temperature in _v_ °C (int8_t) */ + BTHOME_ID_TEMPERATURE_FACTOR_0_35 = 0x58, /**< Temperature in _v_ * 0.35 °C (int8_t) */ + BTHOME_ID_COUNT_SINT8 = 0x59, /**< Count as _v_ (int8_t) */ + BTHOME_ID_COUNT_SINT16 = 0x5a, /**< Count as _v_ (int16_t) */ + BTHOME_ID_COUNT_SINT32 = 0x5b, /**< Count as _v_ (int32_t) */ + BTHOME_ID_POWER_SINT_FACTOR_0_01 = 0x5c, /**< Power in _v_ * 0.01 W (int32_t) */ + BTHOME_ID_CURRENT_SINT_FACTOR_0_001 = 0x5d, /**< Current in _v_ * 0.001 A (int16_t) */ + BTHOME_ID_TEXT = 0x53, /**< Text as _v_ (bytes) */ + BTHOME_ID_RAW = 0x54, /**< Raw as _v_ (bytes) */ + BTHOME_ID_CONDUCTIVITY = 0x56, /**< Conductivity in _v_ uS/cm (uint16_t) */ + /* ==================== 0xf0 ==================== */ + BTHOME_ID_DEVICE_TYPE_ID = 0xf0, /**< Device type id as _v_ (uint16_t) */ + /** + * @brief Firmware version as _v_ (4 bytes, uint32_t) + * + * _v_ is interpreted as v[3].v[2].[1].[0] + */ + BTHOME_ID_FIRMWARE_VERSION_4B_ID = 0xf1, + /** + * @brief Firmware version as _v_ (3 bytes, uint32_t) + * + * _v_ is interpreted as v[2].[1].[0] + */ + BTHOME_ID_FIRMWARE_VERSION_3B_ID = 0xf2, +} skald_bthome_id_t; + +/** + * @brief Value for button. + * + * @see @ref BTHOME_ID_BUTTON. + */ +typedef enum { + BTHOME_BTN_NONE = 0x00, /**< No button press */ + BTHOME_BTN_PRESS = 0x01, /**< Press */ + BTHOME_BTN_DOUBLE_PRESS = 0x02, /**< Double press */ + BTHOME_BTN_TRIPLE_PRESS = 0x03, /**< Triple press */ + BTHOME_BTN_LONG_PRESS = 0x04, /**< Long press */ + BTHOME_BTN_LONG_DOUBLE_PRESS = 0x05, /**< Long double press */ + BTHOME_BTN_LONG_TRIPLE_PRESS = 0x05, /**< Long triple press */ + BTHOME_BTN_HOLD_PRESS = 0x80, /**< Hold press */ +} skald_bthome_btn_t; + +#ifdef __cplusplus +} +#endif + +/** @} */ +#endif /* NET_SKALD_BTHOME_DEFS_H */ diff --git a/sys/net/ble/skald/Makefile b/sys/net/ble/skald/Makefile index c835b4d8ddc5..14ba09c3b9c1 100644 --- a/sys/net/ble/skald/Makefile +++ b/sys/net/ble/skald/Makefile @@ -8,4 +8,16 @@ ifneq (,$(filter skald_eddystone,$(USEMODULE))) SRC += skald_eddystone.c endif +ifneq (,$(filter skald_bthome,$(USEMODULE))) + SRC += skald_bthome.c +endif + +ifneq (,$(filter skald_bthome_saul,$(USEMODULE))) + SRC += skald_bthome_saul.c +endif + +ifneq (,$(filter skald_bthome_encrypt,$(USEMODULE))) + $(shell $(COLOR_ECHO) "\n\n$(COLOR_RED)This currently does not work with Home Assistant!$(COLOR_RESET)\n\n" 1>&2) +endif + include $(RIOTBASE)/Makefile.base diff --git a/sys/net/ble/skald/skald.c b/sys/net/ble/skald/skald.c index 021967355746..c737c6e8780a 100644 --- a/sys/net/ble/skald/skald.c +++ b/sys/net/ble/skald/skald.c @@ -46,6 +46,12 @@ #define ADV_AA (0x8e89bed6) /* access address */ #define ADV_CRC (0x00555555) /* CRC initializer */ +#if IS_USED(MODULE_SKALD_UPDATE_PKT_CB) +#define _on_adv_evt_isr _on_adv_evt_event +#else +#define _on_adv_evt_isr _on_adv_evt +#endif + static const uint8_t _adv_chan[] = SKALD_ADV_CHAN; static netdev_ble_ctx_t _ble_ctx = { @@ -69,7 +75,10 @@ static void _sched_next(skald_ctx_t *ctx) ctx->last += random_uint32_range(JITTER_MIN, JITTER_MAX); /* compensate the time passed since the timer triggered last by using the * current value of the timer */ - ztimer_set(ZTIMER_MSEC, &ctx->timer, (ctx->last - ztimer_now(ZTIMER_MSEC))); + ztimer_now_t next = (ctx->last > ztimer_now(ZTIMER_MSEC)) + ? (ctx->last - ztimer_now(ZTIMER_MSEC)) + : 0; + ztimer_set(ZTIMER_MSEC, &ctx->timer, next); } static void _on_adv_evt(void *arg) @@ -81,6 +90,11 @@ static void _on_adv_evt(void *arg) if ((ctx->cur_chan < ADV_CHAN_NUMOF) && (_radio->context == NULL)) { _radio->context = ctx; _ble_ctx.chan = _adv_chan[ctx->cur_chan]; +#if IS_USED(MODULE_SKALD_UPDATE_PKT_CB) + if (ctx->update_pkt) { + ctx->update_pkt(ctx); + } +#endif netdev_ble_set_ctx(_radio, &_ble_ctx); netdev_ble_send(_radio, &ctx->pkt); ++ctx->cur_chan; @@ -91,6 +105,23 @@ static void _on_adv_evt(void *arg) } } +#if IS_USED(MODULE_SKALD_UPDATE_PKT_CB) +static void _event_handler(event_t *event) +{ + skald_ctx_t *ctx = container_of(event, skald_ctx_t, event); + + _on_adv_evt(ctx); +} + +static void _on_adv_evt_event(void *arg) +{ + skald_ctx_t *ctx = arg; + + ctx->event.handler = _event_handler; + event_post(&ctx->queue, &ctx->event); +} +#endif + static void _on_radio_evt(netdev_t *netdev, netdev_event_t event) { (void)netdev; @@ -98,7 +129,7 @@ static void _on_radio_evt(netdev_t *netdev, netdev_event_t event) if (event == NETDEV_EVENT_TX_COMPLETE) { skald_ctx_t *ctx = _radio->context; _stop_radio(); - _on_adv_evt(ctx); + _on_adv_evt_isr(ctx); } } @@ -122,14 +153,20 @@ void skald_adv_start(skald_ctx_t *ctx) skald_adv_stop(ctx); /* initialize advertising context */ - ctx->timer.callback = _on_adv_evt; + ctx->timer.callback = _on_adv_evt_isr; ctx->timer.arg = ctx; ctx->last = ztimer_now(ZTIMER_MSEC); ctx->cur_chan = 0; ctx->pkt.flags = (BLE_ADV_NONCON_IND | BLE_LL_FLAG_TXADD); +#if IS_USED(MODULE_SKALD_UPDATE_PKT_CB) + event_queue_init(&ctx->queue); +#endif /* start advertising */ _sched_next(ctx); +#if IS_USED(MODULE_SKALD_UPDATE_PKT_CB) + event_loop(&ctx->queue); +#endif } void skald_adv_stop(skald_ctx_t *ctx) diff --git a/sys/net/ble/skald/skald_bthome.c b/sys/net/ble/skald/skald_bthome.c new file mode 100644 index 000000000000..dce28b3cdb5b --- /dev/null +++ b/sys/net/ble/skald/skald_bthome.c @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2024 Martine S. Lenders + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * + * @file + * @author Martine S. Lenders + */ + +#include "net/skald/bthome.h" + +#if IS_USED(MODULE_SKALD_BTHOME_ENCRYPT) +#include "random.h" +#include "crypto/modes/ccm.h" +#endif + +#define AD_TYPE_FLAGS 0x01 +#define AD_TYPE_SVC_DATA 0x16 +#define AD_TYPE_LOCAL_NAME_SHORT 0x08 +#define AD_TYPE_LOCAL_NAME_COMPLETE 0x09 + +#define BT_HOME_DEV_INFO_MASK 0x01 +#define BT_HOME_DEV_INFO_ENCRYPT 0x01 +#define BT_HOME_DEV_INFO_BTHOME_V2 (2 << 5) + +typedef struct __attribute__((packed)) { + uint8_t length; + uint8_t ad_type; +} _ad_element_hdr_t; + +typedef struct __attribute__((packed)) { + _ad_element_hdr_t hdr; + uint8_t flags; +} _flags_t; + +typedef struct __attribute__((packed)) { + uint8_t txadd[BLE_ADDR_LEN]; + _flags_t flags; +} _bthome_msg_hdr_t; + +typedef struct __attribute__((packed)) { + /* MUST be { 0xd2, 0xfc }, see https://bthome.io/images/License_Statement_-_BTHOME.pdf */ + uint8_t uuid[2]; + uint8_t dev_info; +} _bthome_data_hdr_t; + +/* flags: LE General Discoverable Mode | BR/EDR Not Supported */ +#define BTHOME_FLAGS { .hdr = { .length = 0x02, .ad_type = 0x01 }, 0x06 } + +static int _set_name(skald_ctx_t *ctx, const char *name, uint8_t offset, uint8_t type) +{ + if (name) { + _ad_element_hdr_t *name_hdr; + uint8_t name_len = strlen(name); + uint8_t exp_size = (offset + name_len + sizeof(_ad_element_hdr_t)); + + assert(strlen(name) <= NETDEV_BLE_PDU_MAXLEN); + if (exp_size >= NETDEV_BLE_PDU_MAXLEN) { + assert(exp_size < NETDEV_BLE_PDU_MAXLEN); + return -EMSGSIZE; + } + name_hdr = (_ad_element_hdr_t *)(&ctx->pkt.pdu[offset]); + name_hdr->length = name_len + 1; + name_hdr->ad_type = type; + strncpy((char *)(&ctx->pkt.pdu[offset + sizeof(*name_hdr)]), name, name_len); + return sizeof(*name_hdr) + name_len; + } + return 0; +} + +static inline int _set_shortened_name(skald_ctx_t *ctx, const char *name, uint8_t offset) +{ + return _set_name(ctx, name, offset, AD_TYPE_LOCAL_NAME_SHORT); +} + +static inline int _set_complete_name(skald_ctx_t *ctx, const char *name, uint8_t offset) +{ + return _set_name(ctx, name, offset, AD_TYPE_LOCAL_NAME_COMPLETE); +} + +static int _init_svc_data(skald_bthome_ctx_t *ctx, uint8_t dev_info, uint8_t offset) +{ + _ad_element_hdr_t *svc_data_hdr; + _bthome_data_hdr_t *data_hdr; + uint8_t exp_size = (offset + sizeof(*svc_data_hdr) + sizeof(*data_hdr)); + + if (exp_size >= NETDEV_BLE_PDU_MAXLEN) { + assert(exp_size < NETDEV_BLE_PDU_MAXLEN); + return -EMSGSIZE; + } + svc_data_hdr = (_ad_element_hdr_t *)(&ctx->skald.pkt.pdu[offset]); + data_hdr = (_bthome_data_hdr_t *)(&ctx->skald.pkt.pdu[offset + sizeof(*svc_data_hdr)]); + svc_data_hdr->length = sizeof(*data_hdr) + 1; + svc_data_hdr->ad_type = AD_TYPE_SVC_DATA; + ctx->svc_data_len = &svc_data_hdr->length; + /* https://bthome.io/images/License_Statement_-_BTHOME.pdf */ + data_hdr->uuid[0] = 0xd2; + data_hdr->uuid[1] = 0xfc; + data_hdr->dev_info = ((dev_info & BT_HOME_DEV_INFO_MASK) | BT_HOME_DEV_INFO_BTHOME_V2); + return sizeof(*svc_data_hdr) + sizeof(*data_hdr); +} + +int skald_bthome_init( + skald_bthome_ctx_t *ctx, + const char *shortened_name, const char *complete_name, + uint8_t dev_info +) +{ + assert(ctx); + + uint8_t offset = sizeof(_bthome_msg_hdr_t); + int res; + static const _flags_t flags = BTHOME_FLAGS; + _bthome_msg_hdr_t *hdr = (_bthome_msg_hdr_t *)ctx->skald.pkt.pdu; + + skald_generate_random_addr(hdr->txadd); + hdr->flags = flags; + res = _set_shortened_name(&ctx->skald, shortened_name, offset); + if (res < 0) { + return res; + } + offset += res; + res = _set_complete_name(&ctx->skald, complete_name, offset); + if (res < 0) { + return res; + } + offset += res; + res = _init_svc_data(ctx, dev_info, offset); + if (res < 0) { + return res; + } + offset += res; + ctx->skald.pkt.len = offset; + return offset; +} + +int skald_bthome_add_measurement( + skald_bthome_ctx_t *ctx, + skald_bthome_id_t obj_id, + const void *data, + uint8_t data_len +) +{ + /* check if skald_bthome_init() was called */ + assert( + ctx + && (ctx->svc_data_len > (&ctx->skald.pkt.pdu[0])) + && (ctx->svc_data_len < (&ctx->skald.pkt.pdu[NETDEV_BLE_PDU_MAXLEN])) + && (ctx->skald.pkt.len > 0) + && (ctx->skald.pkt.len < NETDEV_BLE_PDU_MAXLEN) + ); + assert(data); + bool data_contains_length = ((obj_id == BTHOME_ID_RAW) || (obj_id == BTHOME_ID_TEXT)); + uint16_t exp_size = ctx->skald.pkt.len + sizeof(obj_id) + data_len + data_contains_length; + uint8_t offset = ctx->skald.pkt.len; + +#ifdef MODULE_SKALD_BTHOME_ENCRYPT + exp_size += 8; +#endif + + if (exp_size >= NETDEV_BLE_PDU_MAXLEN) { + return -EMSGSIZE; + } + ctx->skald.pkt.pdu[offset++] = (uint8_t)obj_id; + if (data_contains_length) { + ctx->skald.pkt.pdu[offset++] = data_len; + } + memcpy(&ctx->skald.pkt.pdu[offset], data, data_len); + offset += data_len; + /* store diff in offset to return it later */ + *ctx->svc_data_len += offset - ctx->skald.pkt.len; + ctx->skald.pkt.len = offset; + return ctx->skald.pkt.len; +} + +#if IS_USED(MODULE_SKALD_BTHOME_ENCRYPT) +static uint32_t _counter = 0; + +int skald_bthome_encrypt(skald_bthome_ctx_t *ctx) +{ + if (!ctx->encrypt) { + return ctx->skald.pkt.len; + } + uint8_t ciphertext[NETDEV_BLE_PDU_MAXLEN]; + cipher_t cipher = { 0 }; + uint8_t *txadd = &ctx->skald.pkt.pdu[0]; + uint8_t *dev_info = ctx->svc_data_len + 4; + uint8_t *uuid = ctx->svc_data_len + 2; + uint8_t *data = dev_info + 1; + int ciphertext_len; + uint8_t nonce[13]; + uint8_t nonce_len = 0; + /* maximum data length - counter */ + uint8_t max_len = &ctx->skald.pkt.pdu[NETDEV_BLE_PDU_MAXLEN] - data - 4; + uint8_t data_len = &ctx->skald.pkt.pdu[ctx->skald.pkt.len] - data; + + if (_counter == 0) { + _counter = random_uint32(); + } + + *dev_info |= BT_HOME_DEV_INFO_ENCRYPT; + + /* set nonce according to spec: MAC Address + UUID + BTHome dev info + counter */ + memcpy(&nonce[nonce_len], txadd, BLE_ADDR_LEN); + nonce_len += BLE_ADDR_LEN; + memcpy(&nonce[nonce_len], uuid, 2); + nonce_len += 2; + nonce[nonce_len++] = *dev_info; + nonce[nonce_len++] = (_counter >> 24) & 0xff; + nonce[nonce_len++] = (_counter >> 16) & 0xff; + nonce[nonce_len++] = (_counter >> 8) & 0xff; + nonce[nonce_len++] = _counter & 0xff; + + if (cipher_init(&cipher, CIPHER_AES, + ctx->key, SKALD_BTHOME_KEY_LEN) != CIPHER_INIT_SUCCESS) { + return -1; + } + + ciphertext_len = cipher_encrypt_ccm(&cipher, NULL, 0, 4, 2, + nonce, nonce_len, + data, data_len, + ciphertext); + + if (ciphertext_len < 0) { + return -1; + } + + if (ciphertext_len > max_len) { + return -ENOBUFS; + } + /* replace original payload with ciphertext */ + data_len = 0; + /* add ciphertext */ + memcpy(&data[data_len], ciphertext, ciphertext_len -4); + data_len += (ciphertext_len - 4); + /* add counter */ + data[data_len++] = (_counter >> 24) & 0xff; + data[data_len++] = (_counter >> 16) & 0xff; + data[data_len++] = (_counter >> 8) & 0xff; + data[data_len++] = _counter & 0xff; + /* add mic */ + memcpy(&data[data_len], &ciphertext[ciphertext_len - 4], 4); + data_len += 4; + + ctx->skald.pkt.len = (dev_info - txadd) + data_len + 1; + *ctx->svc_data_len = data_len + 4; + /* Progress counter a random step but not too big step */ + _counter += random_uint32() & 0x1f; + + return ctx->skald.pkt.len; +} +#endif + +void skald_bthome_advertise(skald_bthome_ctx_t *ctx, uint32_t adv_itvl_ms) +{ + ctx->skald.adv_itvl_ms = adv_itvl_ms; + skald_adv_start(&ctx->skald); +} + +/** @} */ diff --git a/sys/net/ble/skald/skald_bthome_saul.c b/sys/net/ble/skald/skald_bthome_saul.c new file mode 100644 index 000000000000..238598fb5a1b --- /dev/null +++ b/sys/net/ble/skald/skald_bthome_saul.c @@ -0,0 +1,678 @@ +/* + * Copyright (C) 2024 Martine S. Lenders + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * + * @file + * @author Martine S. Lenders + */ + +#include "net/skald/bthome.h" + +static int _add_50perc_to_binary_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_button_press( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_int8_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_int16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_uint8_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_uint24_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_div_10_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_div_10_uint24_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_div_100_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_div_100_uint24_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_div_1000_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_div_10000_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_g_force_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_ppb_to_ugm3_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_times_10_uint8_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_times_10_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_times_10_uint24_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_times_100_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_times_100_uint24_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_times_1000_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); +static int _add_times_10000_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx); + +static int _saul_sense_to_bthome_id(skald_bthome_saul_t *saul, const phydat_t *data) +{ + switch (saul->saul.driver->type & SAUL_ID_MASK) { + case SAUL_SENSE_ID_ANY: + return -1; + case SAUL_SENSE_ID_BTN: + saul->obj_id = BTHOME_ID_BUTTON; + saul->add_measurement = &_add_button_press; + return 0; + case SAUL_SENSE_ID_TEMP: + case SAUL_SENSE_ID_OBJTEMP: + switch (data->unit) { + case UNIT_TEMP_C: + switch (data->scale) { + case -2: + saul->obj_id = BTHOME_ID_TEMPERATURE_FACTOR_0_01; + saul->add_measurement = &_add_int16_measurement; + return 0; + case -1: + saul->obj_id = BTHOME_ID_TEMPERATURE_FACTOR_0_1; + saul->add_measurement = &_add_int16_measurement; + return 0; + case 0: + saul->obj_id = BTHOME_ID_TEMPERATURE_FACTOR_1; + saul->add_measurement = &_add_int8_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + case SAUL_SENSE_ID_HUM: + switch (data->unit) { + case UNIT_PERCENT: + switch (data->scale) { + case -2: + saul->obj_id = BTHOME_ID_HUMIDITY_FACTOR_0_01; + saul->add_measurement = &_add_uint16_measurement; + return 0; + case -1: + saul->obj_id = BTHOME_ID_HUMIDITY_FACTOR_0_01; + saul->add_measurement = &_add_times_10_uint16_measurement; + return 0; + case 0: + saul->obj_id = BTHOME_ID_HUMIDITY_FACTOR_1; + saul->add_measurement = &_add_uint8_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + case SAUL_SENSE_ID_LIGHT: + switch (data->unit) { + case UNIT_LUX: + switch (data->scale) { + case 0: + saul->obj_id = BTHOME_ID_ILLUMINANCE_FACTOR_0_01; + saul->add_measurement = &_add_times_100_uint24_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + case SAUL_SENSE_ID_ACCEL: + switch (data->unit) { + case UNIT_G_FORCE: + switch (data->scale) { + case -3: + saul->obj_id = BTHOME_ID_ACCELERATION_FACTOR_0_001; + saul->add_measurement = &_add_g_force_uint16_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + case SAUL_SENSE_ID_MAG: + return -1; + case SAUL_SENSE_ID_GYRO: + switch (data->unit) { + case UNIT_DPS: + saul->obj_id = BTHOME_ID_GYROSCOPE_FACTOR_0_001; + switch (data->scale) { + case -1: + saul->add_measurement = &_add_times_100_uint16_measurement; + return 0; + case 0: + saul->add_measurement = &_add_times_1000_uint16_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + case SAUL_SENSE_ID_COLOR: + return -1; + case SAUL_SENSE_ID_PRESS: + switch (data->unit) { + case UNIT_BAR: + switch (data->scale) { + case -6: + saul->obj_id = BTHOME_ID_PRESSURE_FACTOR_0_01; + /* 10^-6 bar = 0.000001 bar = 0.1 Pa = 0.001 hPa + * => data / 10 = 10^-6 bar in 0.01 hPA */ + saul->add_measurement = &_add_div_10_uint24_measurement; + return 0; + default: + return -1; + } + case UNIT_PA: + saul->obj_id = BTHOME_ID_PRESSURE_FACTOR_0_01; + switch (data->scale) { + case -2: + /* 10^-2 Pa = 0.0001 hPa + * => data / 100 = 10^-2 Pa in 0.01 hPa */ + saul->add_measurement = &_add_div_100_uint24_measurement; + return 0; + case 0: + saul->add_measurement = &_add_uint24_measurement; + return 0; + case 1: + /* 10^1 Pa = 0.1 hPa */ + saul->add_measurement = &_add_times_10_uint24_measurement; + return 0; + case 2: + /* 10^2 Pa = 1 hPa */ + saul->add_measurement = &_add_times_100_uint24_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + case SAUL_SENSE_ID_ANALOG: + return -1; + case SAUL_SENSE_ID_UV: + switch (data->scale) { + case -1: + saul->obj_id = BTHOME_ID_UV_INDEX_FACTOR_0_1; + saul->add_measurement = &_add_uint8_measurement; + return 0; + case 0: + saul->obj_id = BTHOME_ID_UV_INDEX_FACTOR_0_1; + saul->add_measurement = &_add_times_10_uint8_measurement; + return 0; + default: + return -1; + } + case SAUL_SENSE_ID_COUNT: + switch (data->unit) { + case UNIT_CPM3: + switch (data->scale) { + case 3: + saul->obj_id = BTHOME_ID_COUNT_UINT32; + saul->add_measurement = &_add_times_1000_uint16_measurement; + return 0; + case 4: + saul->obj_id = BTHOME_ID_COUNT_UINT32; + saul->add_measurement = &_add_times_10000_uint16_measurement; + return 0; + default: + return -1; + } + case UNIT_NONE: + switch (data->scale) { + case 0: + saul->obj_id = BTHOME_ID_COUNT_UINT16; + saul->add_measurement = &_add_uint16_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + return -1; + return -1; + case SAUL_SENSE_ID_DISTANCE: + switch (data->unit) { + case UNIT_M: + switch (data->scale) { + case -3: + saul->obj_id = BTHOME_ID_DISTANCE_MM; + saul->add_measurement = &_add_uint16_measurement; + return 0; + case 0: + saul->obj_id = BTHOME_ID_DISTANCE_M; + saul->add_measurement = &_add_uint16_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + return -1; + case SAUL_SENSE_ID_CO2: + switch (data->unit) { + case UNIT_PPM: + saul->obj_id = BTHOME_ID_CO2; + switch (data->scale) { + case -2: + saul->add_measurement = &_add_div_100_uint16_measurement; + return 0; + case 0: + saul->add_measurement = &_add_uint16_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + return -1; + case SAUL_SENSE_ID_TVOC: + switch (data->unit) { + case UNIT_PPB: + switch (data->scale) { + case 0: + saul->obj_id = BTHOME_ID_TVOC; + saul->add_measurement = &_add_ppb_to_ugm3_uint16_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + return -1; + return -1; + case SAUL_SENSE_ID_GAS: + /* only gas sensor so far seems to be a poti that outputs in Ohm */ + return -1; + case SAUL_SENSE_ID_OCCUP: + switch (data->unit) { + case UNIT_PERCENT: + switch (data->scale) { + case 0: + saul->obj_id = BTHOME_ID_OCCUPANCY_BINARY; + saul->add_measurement = &_add_50perc_to_binary_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + return -1; + case SAUL_SENSE_ID_PROXIMITY: + /* proximity sensors return in counts. Don't know how to convert */ + return -1; + case SAUL_SENSE_ID_RSSI: + return -1; + case SAUL_SENSE_ID_CHARGE: + /* none of the sensors express the charge in % as, e.g., BTHOME_ID_BATTERY */ + return -1; + case SAUL_SENSE_ID_CURRENT: + switch (data->unit) { + case UNIT_A: + switch (data->scale) { + case -6: + saul->obj_id = BTHOME_ID_CURRENT_UINT_FACTOR_0_001; + saul->add_measurement = &_add_div_1000_uint16_measurement; + return 0; + case -5: + saul->obj_id = BTHOME_ID_CURRENT_UINT_FACTOR_0_001; + saul->add_measurement = &_add_div_100_uint16_measurement; + return 0; + case -4: + saul->obj_id = BTHOME_ID_CURRENT_UINT_FACTOR_0_001; + saul->add_measurement = &_add_div_10_uint16_measurement; + return 0; + case -3: + saul->obj_id = BTHOME_ID_CURRENT_UINT_FACTOR_0_001; + saul->add_measurement = &_add_uint16_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + return -1; + case SAUL_SENSE_ID_PM: + /* Left as exercise to the reader to convert it to either + * BTHOME_ID_PM_2_5 or BTHOME_ID_PM_10 for UNIT_GPM3 */ + return -1; + case SAUL_SENSE_ID_CAPACITANCE: + return -1; + case SAUL_SENSE_ID_VOLTAGE: + switch (data->unit) { + case UNIT_V: + switch (data->scale) { + case -6: + saul->obj_id = BTHOME_ID_VOLTAGE_FACTOR_0_001; + saul->add_measurement = &_add_div_1000_uint16_measurement; + return 0; + case -3: + saul->obj_id = BTHOME_ID_VOLTAGE_FACTOR_0_001; + saul->add_measurement = &_add_uint16_measurement; + return 0; + case -1: + saul->obj_id = BTHOME_ID_VOLTAGE_FACTOR_0_1; + saul->add_measurement = &_add_uint16_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + return -1; + case SAUL_SENSE_ID_PH: + return -1; + case SAUL_SENSE_ID_POWER: + switch (data->unit) { + case UNIT_W: + switch (data->scale) { + case -6: + saul->obj_id = BTHOME_ID_POWER_UINT_FACTOR_0_01; + saul->add_measurement = &_add_div_10000_uint16_measurement; + return 0; + case -4: + saul->obj_id = BTHOME_ID_POWER_UINT_FACTOR_0_01; + saul->add_measurement = &_add_div_100_uint16_measurement; + return 0; + case -2: + saul->obj_id = BTHOME_ID_POWER_UINT_FACTOR_0_01; + saul->add_measurement = &_add_uint16_measurement; + return 0; + default: + return -1; + } + default: + return -1; + } + return -1; + case SAUL_SENSE_ID_SIZE: + return -1; + default: + return -1; + } +} + +static void _reset_hdr(skald_bthome_ctx_t *ctx) +{ + /* Skip AD type, BTHome UUID, and BTHome Device Information */ + uint8_t *data_start = ctx->svc_data_len + 5U; + ctx->skald.pkt.len = data_start - &ctx->skald.pkt.pdu[0]; + *ctx->svc_data_len = 4U; /* AD type + BTHome UUID + BTHome Device Information */ +} + +static void _update_saul_measurements(skald_ctx_t *skald_ctx) +{ + skald_bthome_ctx_t *ctx = container_of(skald_ctx, skald_bthome_ctx_t, skald); + uint8_t dev_idx = 0; + uint8_t orig_last_dev_sent = ctx->last_dev_sent; + + _reset_hdr(ctx); + skald_bthome_saul_t *ptr = ctx->devs; + while (ptr) { + if ((ctx->last_dev_sent == 0) || + (dev_idx > ctx->last_dev_sent)) { + int dim = 1; + int res = 0; + phydat_t data = { 0 }; + + if (ptr->saul.driver) { + dim = saul_reg_read(&ptr->saul, &data); + if (dim <= 0) { + continue; + } + } + for (uint8_t i = 0; i < dim; i++) { + if ((res = ptr->add_measurement(ctx, ptr->obj_id, &data, i)) < 0) { + break; + } + } + if ((res == -EMSGSIZE) && (dev_idx > 0)) { + ctx->last_dev_sent = dev_idx - 1; + } + if (res < 0) { + break; + } + } + ptr = container_of( + ptr->saul.next, skald_bthome_saul_t, saul + ); + dev_idx++; + } +#if IS_USED(MODULE_SKALD_BTHOME_ENCRYPT) + skald_bthome_encrypt(ctx); +#endif + if ((ptr == NULL) || + /* or value just too big */ + (orig_last_dev_sent == ctx->last_dev_sent)) { + /* reset device train */ + ctx->last_dev_sent = 0; + } +} + +int skald_bthome_saul_add(skald_bthome_ctx_t *ctx, skald_bthome_saul_t *saul) +{ + if ((saul->saul.driver != NULL) && + (saul->saul.driver->type & SAUL_CAT_MASK) != SAUL_CAT_SENSE) { + return -ENOTSUP; + } + + if (!ctx->skald.update_pkt) { + ctx->skald.update_pkt = &_update_saul_measurements; + } + if (!(saul->flags & SKALD_BTHOME_SAUL_FLAGS_CUSTOM)) { + phydat_t sample; + int dim = saul_reg_read(&saul->saul, &sample); + + if (dim <= 0) { + return -ENODEV; + } + if (_saul_sense_to_bthome_id(saul, &sample) < 0) { + return -ENOENT; + } + } + if (ctx->devs) { + skald_bthome_saul_t *ptr = ctx->devs; + + if (ptr->obj_id > saul->obj_id) { + saul->saul.next = &ptr->saul; + ctx->devs = saul; + return 0; + } + while (ptr) { + skald_bthome_saul_t *next = container_of( + ptr->saul.next, skald_bthome_saul_t, saul + ); + /* insert in obj_id order */ + if (next) { + if (next->obj_id > saul->obj_id) { + saul->saul.next = &next->saul; + ptr->saul.next = &saul->saul; + break; + } + } + else { + ptr->saul.next = &saul->saul; + break; + } + ptr = next; + } + } + else { + ctx->devs = saul; + } + return 0; +} + +static int _add_50perc_to_binary_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint8_measurement(ctx, obj_id, (data->val[idx] < 50) ? 0x00 : 0x01); +} + +static int _add_button_press( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint8_measurement(ctx, obj_id, (data->val[idx]) ? 0x01 : 0x00); +} + +static int _add_int8_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_int8_measurement(ctx, obj_id, (int8_t)data->val[idx]); +} + +static int _add_uint8_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint8_measurement(ctx, obj_id, (uint8_t)data->val[idx]); +} + +static int _add_int16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_int16_measurement(ctx, obj_id, data->val[idx]); +} + +static int _add_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)data->val[idx]); +} + +static int _add_uint24_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint24_measurement(ctx, obj_id, (uint32_t)data->val[idx]); +} + +static int _add_g_force_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + int32_t d = data->val[idx]; + /* 1g ~= 9.807 m/s² */ + d *= 98; + d /= 10; + if (d > INT16_MAX) { + d = INT16_MAX; + } + if (d < INT16_MIN) { + d = INT16_MIN; + } + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)d); +} + +static int _add_ppb_to_ugm3_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + int32_t d = data->val[idx]; + /* see https://www.arcskoru.com/sites/default/files/Arc%20Guide%20to%20Re-Entry.pdf page 26 */ + d *= 3767; + d /= 1000; + if (d > INT16_MAX) { + d = INT16_MAX; + } + if (d < INT16_MIN) { + d = INT16_MIN; + } + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)d); +} + +static int _add_div_10_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)(data->val[idx] / 10)); +} + +static int _add_div_10_uint24_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint24_measurement(ctx, obj_id, (uint32_t)(data->val[idx] / 10)); +} + +static int _add_div_100_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)(data->val[idx] / 100)); +} + +static int _add_div_100_uint24_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint24_measurement(ctx, obj_id, (uint32_t)(data->val[idx] / 100)); +} + +static int _add_div_1000_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)(data->val[idx] / 1000)); +} + +static int _add_div_10000_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)(data->val[idx] / 10000)); +} + +static int _add_times_10_uint8_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint8_measurement(ctx, obj_id, (uint8_t)(data->val[idx] * 10)); +} + +static int _add_times_10_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)(data->val[idx] * 10)); +} + +static int _add_times_100_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)(data->val[idx] * 100)); +} + +static int _add_times_10_uint24_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint24_measurement(ctx, obj_id, (uint32_t)(data->val[idx] * 10)); +} + +static int _add_times_100_uint24_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint24_measurement(ctx, obj_id, (uint32_t)(data->val[idx] * 100)); +} + +static int _add_times_1000_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)(data->val[idx] * 1000)); +} + +static int _add_times_10000_uint16_measurement( + skald_bthome_ctx_t *ctx, uint8_t obj_id, phydat_t *data, uint8_t idx) +{ + return skald_bthome_add_uint16_measurement(ctx, obj_id, (uint16_t)(data->val[idx] * 1000)); +} + +/** @} */