1100 lines
29 KiB
C
1100 lines
29 KiB
C
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
#include "sdkconfig.h"
|
|
#ifdef CONFIG_NODEMCU_CMODULE_BLE
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
|
|
#include "module.h"
|
|
#include "lauxlib.h"
|
|
#include "task/task.h"
|
|
#include "platform.h"
|
|
#include "esp_bt.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <esp_log.h>
|
|
#define TAG "ble"
|
|
|
|
#ifndef CONFIG_BT_NIMBLE_ENABLED
|
|
#error You must enable NIMBLE if you want the Lua ble module. Hopefully this can be made automatic some day.
|
|
#endif
|
|
|
|
/* BLE */
|
|
#include "esp_nimble_hci.h"
|
|
#include "nimble/nimble_port.h"
|
|
#include "nimble/nimble_port_freertos.h"
|
|
#include "host/ble_hs.h"
|
|
#include "host/util/util.h"
|
|
#include "console/console.h"
|
|
#include "services/gap/ble_svc_gap.h"
|
|
#include "services/gatt/ble_svc_gatt.h"
|
|
#include "nimble/ble.h"
|
|
#include "task/task.h"
|
|
|
|
#if 0
|
|
#undef MODLOG_DFLT
|
|
#define MODLOG_DFLT(level, ...) printf(__VA_ARGS__)
|
|
#endif
|
|
|
|
#define PERROR() MODLOG_DFLT(INFO, "pcall failed: %s\n", lua_tostring(L, -1))
|
|
|
|
#ifdef CONFIG_LUA_VERSION_51
|
|
#define lua_rawlen lua_objlen
|
|
static int lua_geti (lua_State *L, int index, lua_Integer i) {
|
|
index = lua_absindex(L, index);
|
|
lua_pushinteger(L, i);
|
|
lua_gettable(L, index);
|
|
return lua_type(L, -1);
|
|
}
|
|
#endif
|
|
|
|
extern void ble_hs_lock();
|
|
extern void ble_hs_unlock();
|
|
|
|
static volatile enum { IDLE, SYNCED, READY_TO_ADVERTISE, ADVERTISING } synced;
|
|
static void lble_start_advertising();
|
|
static int lble_gap_event(struct ble_gap_event *event, void *arg);
|
|
|
|
static const char *gadget_name;
|
|
static uint8_t *gadget_mfg;
|
|
static size_t gadget_mfg_len;
|
|
|
|
static const struct ble_gatt_svc_def *gatt_svr_svcs;
|
|
static const uint16_t *notify_handles;
|
|
|
|
static task_handle_t task_handle;
|
|
static QueueHandle_t response_queue;
|
|
|
|
static bool already_inited;
|
|
|
|
static int struct_pack_index;
|
|
static int struct_unpack_index;
|
|
|
|
static enum { STOPPED, RUNNING, SHUTTING } inited;
|
|
|
|
static int seqno;
|
|
|
|
// Note that the buffer should be freed
|
|
typedef struct {
|
|
int seqno;
|
|
int errcode;
|
|
char *buffer;
|
|
size_t length;
|
|
} response_message_t;
|
|
|
|
typedef struct {
|
|
int seqno;
|
|
struct ble_gatt_access_ctxt *ctxt;
|
|
void *arg;
|
|
char *buffer;
|
|
size_t length;
|
|
} task_block_t;
|
|
|
|
/**
|
|
* Utility function to log an array of bytes.
|
|
*/
|
|
static void
|
|
print_addr(const void *addr)
|
|
{
|
|
const uint8_t *u8p;
|
|
|
|
u8p = addr;
|
|
MODLOG_DFLT(INFO, "%02x:%02x:%02x:%02x:%02x:%02x",
|
|
u8p[5], u8p[4], u8p[3], u8p[2], u8p[1], u8p[0]);
|
|
}
|
|
|
|
static int
|
|
gethexval(char c) {
|
|
if (c < '0') {
|
|
return -1;
|
|
}
|
|
if (c <= '9') {
|
|
return c - '0';
|
|
}
|
|
if (c < 'a') {
|
|
c += 'a' - 'A';
|
|
}
|
|
if (c < 'a') {
|
|
return -1;
|
|
}
|
|
if (c <= 'f') {
|
|
return c - 'a' + 10;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
decodehex(const char *s) {
|
|
// two characters
|
|
int v1 = gethexval(s[0]);
|
|
int v2 = gethexval(s[1]);
|
|
|
|
if (v1 < 0 || v2 < 0) {
|
|
return -1;
|
|
}
|
|
return (v1 << 4) + v2;
|
|
}
|
|
|
|
static bool
|
|
convert_uuid(ble_uuid_any_t *uuid, const char *s) {
|
|
int len = strlen(s);
|
|
char decodebuf[32];
|
|
|
|
const char *sptr = s + len;
|
|
int i;
|
|
|
|
for (i = 0; sptr > s && i < sizeof(decodebuf); ) {
|
|
if (sptr < s + 2) {
|
|
return false;
|
|
}
|
|
if (sptr[-1] == '-') {
|
|
sptr--;
|
|
continue;
|
|
}
|
|
sptr -= 2;
|
|
|
|
int val = decodehex(sptr);
|
|
if (val < 0) {
|
|
return false;
|
|
}
|
|
decodebuf[i++] = val;
|
|
}
|
|
|
|
if (sptr != s) {
|
|
return false;
|
|
}
|
|
|
|
if (ble_uuid_init_from_buf(uuid, decodebuf, i) != 0) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
free_gatt_svcs(lua_State *L, const struct ble_gatt_svc_def * svcs) {
|
|
void *tofree = (void *) svcs;
|
|
// Need to unref anything
|
|
if (!svcs) {
|
|
return;
|
|
}
|
|
|
|
for (; svcs->characteristics; svcs++) {
|
|
const struct ble_gatt_chr_def *chrs = svcs->characteristics;
|
|
for (; chrs->arg; chrs++) {
|
|
luaL_unref(L, LUA_REGISTRYINDEX, (int) chrs->arg);
|
|
}
|
|
}
|
|
|
|
free(tofree);
|
|
}
|
|
|
|
static int
|
|
lble_access_cb(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) {
|
|
UNUSED(conn_handle);
|
|
UNUSED(attr_handle);
|
|
|
|
size_t task_block_size = sizeof(task_block_t);
|
|
|
|
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
|
task_block_size += OS_MBUF_PKTLEN(ctxt->om);
|
|
}
|
|
|
|
task_block_t *task_block = malloc(task_block_size);
|
|
if (!task_block) {
|
|
return BLE_ATT_ERR_UNLIKELY;
|
|
}
|
|
|
|
memset(task_block, 0, sizeof(*task_block));
|
|
|
|
task_block->ctxt = ctxt;
|
|
task_block->arg = arg;
|
|
task_block->seqno = seqno++;
|
|
task_block->buffer = (char *) (task_block + 1);
|
|
|
|
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
|
task_block->length = OS_MBUF_PKTLEN(ctxt->om);
|
|
uint16_t outlen;
|
|
if (ble_hs_mbuf_to_flat(ctxt->om, task_block->buffer, task_block->length, &outlen)) {
|
|
free(task_block);
|
|
return BLE_ATT_ERR_UNLIKELY;
|
|
}
|
|
}
|
|
|
|
if (!task_post(TASK_PRIORITY_HIGH, task_handle, (task_param_t) task_block)) {
|
|
free(task_block);
|
|
return BLE_ATT_ERR_UNLIKELY;
|
|
}
|
|
|
|
response_message_t message;
|
|
|
|
while (1) {
|
|
if (xQueueReceive(response_queue, &message, (TickType_t) (2000/portTICK_PERIOD_MS) ) != pdPASS) {
|
|
free(task_block);
|
|
return BLE_ATT_ERR_UNLIKELY;
|
|
}
|
|
|
|
if (message.seqno == task_block->seqno) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
int rc = BLE_ATT_ERR_UNLIKELY;
|
|
|
|
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR ||
|
|
ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
|
rc = message.errcode;
|
|
if (rc == 0) {
|
|
if (os_mbuf_append(ctxt->om, message.buffer, message.length)) {
|
|
rc = BLE_ATT_ERR_INSUFFICIENT_RES;
|
|
}
|
|
}
|
|
} else if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
|
rc = message.errcode;
|
|
}
|
|
|
|
free(message.buffer);
|
|
free(task_block);
|
|
|
|
return rc;
|
|
}
|
|
|
|
static void
|
|
lble_task_cb(task_param_t param, task_prio_t prio) {
|
|
task_block_t *task_block = (task_block_t *) param;
|
|
|
|
response_message_t message;
|
|
memset(&message, 0, sizeof(message));
|
|
message.errcode = BLE_ATT_ERR_UNLIKELY;
|
|
|
|
lua_State *L = lua_getstate();
|
|
int top = lua_gettop(L);
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, (int) task_block->arg);
|
|
// Now we have the characteristic table in -1
|
|
lua_getfield(L, -1, "type");
|
|
// -1 is the struct mapping (if any), -2 is the table
|
|
|
|
size_t datalen;
|
|
const char *data = 0;
|
|
if (task_block->ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
|
// if there is a read method, then invoke it
|
|
lua_getfield(L, -2, "read");
|
|
if (!lua_isnoneornil (L, -1)) {
|
|
lua_pushvalue(L, -3); // dup the table onto the top
|
|
if (lua_pcall(L, 1, 1, 0)) {
|
|
// error.
|
|
PERROR();
|
|
message.errcode = BLE_ATT_ERR_UNLIKELY;
|
|
lua_pop(L, 1);
|
|
goto cleanup;
|
|
}
|
|
} else {
|
|
lua_pop(L, 1); // get rid of the null
|
|
lua_getfield(L, -2, "value");
|
|
}
|
|
// Now we have the value (-1), struct (-2), table (-3)
|
|
if (!lua_isnoneornil(L, -2)) {
|
|
// need to convert value
|
|
if (!lua_istable(L, -1)) {
|
|
// wrap it in a table
|
|
lua_createtable(L, 1, 0);
|
|
lua_pushvalue(L, -2); // Now have value, table, value, struct, table
|
|
lua_rawseti(L, -2, 1);
|
|
lua_remove(L, -2); // now have table, struct, chr table
|
|
}
|
|
|
|
// Now call struct.pack
|
|
// The arguments are the format string, and then the values
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, struct_pack_index);
|
|
lua_pushvalue(L, -3); // dup the format
|
|
int nv = lua_rawlen(L, -3);
|
|
for (int i = 1; i <= nv; i++) {
|
|
lua_rawgeti(L, -2 - i, i);
|
|
}
|
|
if (lua_pcall(L, nv + 1, 1, 0)) {
|
|
PERROR();
|
|
message.errcode = BLE_ATT_ERR_UNLIKELY;
|
|
lua_pop(L, 2);
|
|
goto cleanup;
|
|
}
|
|
lua_remove(L, -2); // remove the old value
|
|
// now have string (-1), struct(-2), chrtable (-3)
|
|
}
|
|
data = lua_tolstring(L, -1, &datalen);
|
|
}
|
|
|
|
if (task_block->ctxt->op == BLE_GATT_ACCESS_OP_READ_DSC) {
|
|
lua_getfield(L, -2, "name");
|
|
// Now we have the name (-1), struct (-2), table (-3)
|
|
data = lua_tolstring(L, -1, &datalen);
|
|
}
|
|
|
|
if (task_block->ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
|
// Push the value
|
|
lua_pushlstring(L, task_block->buffer, task_block->length);
|
|
|
|
// value, struct, chrtable
|
|
// If we have a struct, then unpack the values
|
|
if (!lua_isnoneornil(L, -2)) {
|
|
lua_createtable(L, 0, 0);
|
|
int stack_size = lua_gettop(L);
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, struct_unpack_index);
|
|
lua_pushvalue(L, -4); // dup the format
|
|
lua_pushvalue(L, -4); // dup the string
|
|
if (lua_pcall(L, 2, LUA_MULTRET, 0)) {
|
|
PERROR();
|
|
message.errcode = BLE_ATT_ERR_UNLIKELY;
|
|
lua_pop(L, 2);
|
|
goto cleanup;
|
|
}
|
|
int vals = lua_gettop(L) - stack_size;
|
|
|
|
// Note that the last entry is actually the string offset
|
|
for (int i = 1; i < vals; i++) {
|
|
lua_pushvalue(L, -(vals - i + 1));
|
|
lua_rawseti(L, -(vals + 2), i);
|
|
}
|
|
lua_pop(L, vals);
|
|
lua_remove(L, -2);
|
|
// Now have table, struct, chrtable
|
|
}
|
|
// If the value is a table of a single value, then
|
|
// treat as value
|
|
if (lua_istable(L, -1) && lua_rawlen(L, -1) == 1) {
|
|
lua_rawgeti(L, -1, 1);
|
|
lua_remove(L, -2); // and throw away the table
|
|
}
|
|
// value, struct, chr table
|
|
// save to `value` if present
|
|
lua_getfield(L, -3, "value");
|
|
if (!lua_isnoneornil(L, -1)) {
|
|
lua_pop(L, 1);
|
|
lua_pushvalue(L, -1);
|
|
lua_setfield(L, -4, "value");
|
|
} else {
|
|
lua_pop(L, 1);
|
|
}
|
|
lua_getfield(L, -3, "write");
|
|
if (!lua_isnoneornil(L, -1)) {
|
|
lua_pushvalue(L, -4); // the characterstics table
|
|
lua_pushvalue(L, -3); // the value
|
|
if (lua_pcall(L, 2, 0, 0)) {
|
|
PERROR();
|
|
message.errcode = BLE_ATT_ERR_UNLIKELY;
|
|
lua_pop(L, 2);
|
|
goto cleanup;
|
|
}
|
|
} else {
|
|
lua_pop(L, 1); // Throw away the null write pointer
|
|
}
|
|
}
|
|
|
|
if (data) {
|
|
message.buffer = malloc(datalen);
|
|
if (message.buffer) {
|
|
message.length = datalen;
|
|
memcpy(message.buffer, data, datalen);
|
|
}
|
|
}
|
|
|
|
lua_pop(L, 1); // Throw away the value
|
|
message.errcode = 0;
|
|
|
|
cleanup:
|
|
lua_settop(L, top);
|
|
message.seqno = task_block->seqno;
|
|
|
|
xQueueSend(response_queue, &message, (TickType_t) 0);
|
|
}
|
|
|
|
static int
|
|
lble_build_gatt_svcs(lua_State *L, struct ble_gatt_svc_def **resultp, const uint16_t **handlep) {
|
|
// We have to first figure out how big the allocated memory is.
|
|
// This is the number of services (ns) + 1 * sizeof(ble_gatt_svc_def)
|
|
// + number of characteristics (nc) + ns * sizeof(ble_gatt_chr_def)
|
|
// + ns + nc * sizeof(ble_uuid_any_t)
|
|
|
|
lua_getfield(L, 1, "services");
|
|
if (!lua_istable(L, -1)) {
|
|
return luaL_error(L, "services entry must be a table");
|
|
}
|
|
int ns = lua_rawlen(L, -1);
|
|
|
|
// -1 is the services list
|
|
int nc = 0;
|
|
int nd = 0;
|
|
for (int i = 1; i <= ns; i++) {
|
|
MODLOG_DFLT(INFO, "Counting -- service %d (top %d)\n", i, lua_gettop(L));
|
|
lua_geti(L, -1, i);
|
|
// -1 is now the service which should be a table. It must have a uuid
|
|
if (lua_type(L, -1) != LUA_TTABLE) {
|
|
luaL_error(L, "The services list must contain tables");
|
|
}
|
|
if (lua_getfield(L, -1, "uuid") != LUA_TSTRING) {
|
|
luaL_error(L, "The service uuid must be a string");
|
|
}
|
|
lua_pop(L, 1);
|
|
// -1 is the service again
|
|
lua_getfield(L, -1, "characteristics");
|
|
int sccnt = lua_rawlen(L, -1);
|
|
nc += sccnt;
|
|
// Now count the number of descriptors that we need
|
|
for (int j = 1; j <= sccnt; j++) {
|
|
lua_geti(L, -1, j);
|
|
if (lua_getfield(L, -1, "name") != LUA_TNIL) {
|
|
nd++;
|
|
}
|
|
lua_pop(L, 2);
|
|
}
|
|
|
|
lua_pop(L, 2);
|
|
}
|
|
|
|
MODLOG_DFLT(INFO, "Discovered %d services with %d characteristics with %d descriptors\n", ns, nc, nd);
|
|
|
|
int size = (ns + 1) * sizeof(struct ble_gatt_svc_def) + (nc + ns) * sizeof(struct ble_gatt_chr_def) + (ns + nc) * sizeof(ble_uuid_any_t) + (nc + 1) * sizeof(uint16_t) + nd * 2 * sizeof(struct ble_gatt_dsc_def);
|
|
|
|
|
|
struct ble_gatt_svc_def *svcs = malloc(size);
|
|
if (!svcs) {
|
|
luaL_error(L, "Unable to allocate memory: %d", size);
|
|
}
|
|
|
|
memset(svcs, 0, size);
|
|
struct ble_gatt_svc_def *result = svcs;
|
|
void *eom = ((char *) svcs) + size;
|
|
struct ble_gatt_chr_def *chrs = (struct ble_gatt_chr_def *) (svcs + ns + 1);
|
|
ble_uuid_any_t *uuids = (ble_uuid_any_t *) (chrs + ns + nc);
|
|
struct ble_gatt_dsc_def *dsc = (struct ble_gatt_dsc_def *) (uuids + ns + nc);
|
|
uint16_t *handles = (uint16_t *) (dsc + nd * 2);
|
|
|
|
handles[0] = 0; // number of slots used
|
|
|
|
// Now fill out the data structure
|
|
// -1 is the services list
|
|
for (int i = 1; i <= ns; i++) {
|
|
MODLOG_DFLT(INFO, "Processing service %d (top %d)\n", i, lua_gettop(L));
|
|
struct ble_gatt_svc_def *svc = svcs++;
|
|
lua_geti(L, -1, i);
|
|
// -1 is now the service which should be a table. It must have a uuid
|
|
lua_getfield(L, -1, "uuid");
|
|
// Convert the uuid
|
|
if ((void *) (uuids + 1) > eom) {
|
|
free_gatt_svcs(L, result);
|
|
return luaL_error(L, "Miscalculated memory requirements");
|
|
}
|
|
if (!convert_uuid(uuids, lua_tostring(L, -1))) {
|
|
free_gatt_svcs(L, result);
|
|
return luaL_error(L, "Unable to convert UUID: %s", lua_tostring(L, -1));
|
|
}
|
|
svc->type = BLE_GATT_SVC_TYPE_PRIMARY;
|
|
svc->uuid = (ble_uuid_t *) uuids++;
|
|
svc->characteristics = chrs;
|
|
lua_pop(L, 1);
|
|
// -1 is the service again
|
|
lua_getfield(L, -1, "characteristics");
|
|
int nc = lua_rawlen(L, -1);
|
|
for (int j = 1; j <= nc; j++) {
|
|
MODLOG_DFLT(INFO, "Processing characteristic %d (top %d)\n", j, lua_gettop(L));
|
|
struct ble_gatt_chr_def *chr = chrs++;
|
|
lua_geti(L, -1, j);
|
|
|
|
// -1 is now the characteristic
|
|
lua_getfield(L, -1, "uuid");
|
|
// Convert the uuid
|
|
if ((void *) (uuids + 1) > eom) {
|
|
free_gatt_svcs(L, result);
|
|
return luaL_error(L, "Miscalculated memory requirements");
|
|
}
|
|
if (!convert_uuid(uuids, lua_tostring(L, -1))) {
|
|
free_gatt_svcs(L, result);
|
|
return luaL_error(L, "Unable to convert UUID: %s", lua_tostring(L, -1));
|
|
}
|
|
chr->uuid = (ble_uuid_t *) uuids++;
|
|
lua_pop(L, 1); // pop off the uuid
|
|
|
|
if (lua_getfield(L, -1, "value") != LUA_TNIL) {
|
|
chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE;
|
|
|
|
int flags = 0;
|
|
lua_getfield(L, -2, "read");
|
|
if (lua_isboolean(L, -1) && lua_toboolean(L, -1)) {
|
|
flags = BLE_GATT_CHR_F_READ;
|
|
}
|
|
lua_getfield(L, -3, "write");
|
|
if (lua_isboolean(L, -1) && lua_toboolean(L, -1)) {
|
|
flags |= BLE_GATT_CHR_F_WRITE;
|
|
}
|
|
if (flags) {
|
|
chr->flags = flags;
|
|
}
|
|
|
|
lua_pop(L, 3); // pop off value, read, write
|
|
} else {
|
|
lua_getfield(L, -2, "read");
|
|
if (!lua_isnoneornil (L, -1)) {
|
|
luaL_checkfunction (L, -1);
|
|
chr->flags |= BLE_GATT_CHR_F_READ;
|
|
}
|
|
|
|
lua_getfield(L, -3, "write");
|
|
if (!lua_isnoneornil (L, -1)) {
|
|
luaL_checkfunction (L, -1);
|
|
chr->flags |= BLE_GATT_CHR_F_WRITE;
|
|
}
|
|
|
|
lua_pop(L, 3); // pop off value, read, write
|
|
}
|
|
|
|
if (lua_getfield(L, -1, "notify") != LUA_TNIL) {
|
|
chr->flags |= BLE_GATT_CHR_F_NOTIFY;
|
|
|
|
handles[0]++;
|
|
lua_pushinteger(L, handles[0]);
|
|
lua_setfield(L, -3, "notify");
|
|
chr->val_handle = &handles[handles[0]];
|
|
}
|
|
lua_pop(L, 1);
|
|
|
|
// -1 is now the characteristic again
|
|
chr->arg = (void *) luaL_ref(L, LUA_REGISTRYINDEX);
|
|
chr->access_cb = lble_access_cb;
|
|
|
|
if (lua_getfield(L, -1, "name") != LUA_TNIL) {
|
|
if ((void *) (dsc + 2) > eom) {
|
|
free_gatt_svcs(L, result);
|
|
return luaL_error(L, "Miscalculated memory requirements");
|
|
}
|
|
|
|
chr->descriptors = dsc;
|
|
|
|
dsc->uuid = (const ble_uuid_t*)(ble_uuid16_t[]) {
|
|
BLE_UUID16_INIT(0x2901)
|
|
};
|
|
dsc->att_flags = BLE_ATT_F_READ;
|
|
dsc->access_cb = lble_access_cb;
|
|
dsc->arg = chr->arg;
|
|
|
|
dsc += 2;
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
lua_pop(L, 2);
|
|
chrs++; // terminate the list of characteristics for this service
|
|
}
|
|
lua_pop(L, 1);
|
|
|
|
*resultp = result;
|
|
*handlep = handles;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
gatt_svr_init(lua_State *L) {
|
|
int rc;
|
|
|
|
// Now we have to build the gatt_svr_svcs data structure
|
|
|
|
|
|
struct ble_gatt_svc_def *svcs = NULL;
|
|
lble_build_gatt_svcs(L, &svcs, ¬ify_handles);
|
|
gatt_svr_svcs = svcs;
|
|
|
|
rc = ble_gatts_count_cfg(gatt_svr_svcs);
|
|
if (rc != 0) {
|
|
return luaL_error(L, "Failed to count gatts: %d", rc);
|
|
}
|
|
|
|
rc = ble_gatts_add_svcs(gatt_svr_svcs);
|
|
if (rc != 0) {
|
|
return luaL_error(L, "Failed to add gatts: %d", rc);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Logs information about a connection to the console.
|
|
*/
|
|
static void
|
|
lble_print_conn_desc(struct ble_gap_conn_desc *desc)
|
|
{
|
|
MODLOG_DFLT(INFO, "handle=%d our_ota_addr_type=%d our_ota_addr=",
|
|
desc->conn_handle, desc->our_ota_addr.type);
|
|
print_addr(desc->our_ota_addr.val);
|
|
MODLOG_DFLT(INFO, " our_id_addr_type=%d our_id_addr=",
|
|
desc->our_id_addr.type);
|
|
print_addr(desc->our_id_addr.val);
|
|
MODLOG_DFLT(INFO, " peer_ota_addr_type=%d peer_ota_addr=",
|
|
desc->peer_ota_addr.type);
|
|
print_addr(desc->peer_ota_addr.val);
|
|
MODLOG_DFLT(INFO, " peer_id_addr_type=%d peer_id_addr=",
|
|
desc->peer_id_addr.type);
|
|
print_addr(desc->peer_id_addr.val);
|
|
MODLOG_DFLT(INFO, " conn_itvl=%d conn_latency=%d supervision_timeout=%d "
|
|
"encrypted=%d authenticated=%d bonded=%d\n",
|
|
desc->conn_itvl, desc->conn_latency,
|
|
desc->supervision_timeout,
|
|
desc->sec_state.encrypted,
|
|
desc->sec_state.authenticated,
|
|
desc->sec_state.bonded);
|
|
}
|
|
|
|
static int
|
|
lble_sys_init(lua_State *L) {
|
|
task_handle = task_get_id(lble_task_cb);
|
|
response_queue = xQueueCreate(2, sizeof(response_message_t));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
lble_host_task(void *param)
|
|
{
|
|
nimble_port_run(); //This function will return only when nimble_port_stop() is executed.
|
|
nimble_port_freertos_deinit();
|
|
}
|
|
|
|
static void
|
|
lble_init_stack(lua_State *L) {
|
|
static char stack_inited;
|
|
if (!stack_inited) {
|
|
stack_inited = 1;
|
|
int ret = esp_nimble_hci_and_controller_init();
|
|
if (ret != ESP_OK) {
|
|
luaL_error(L, "esp_nimble_hci_and_controller_init() failed with error: %d", ret);
|
|
return;
|
|
}
|
|
}
|
|
|
|
nimble_port_init();
|
|
|
|
//Initialize the NimBLE Host configuration
|
|
|
|
nimble_port_freertos_init(lble_host_task);
|
|
}
|
|
|
|
static int
|
|
lble_gap_event(struct ble_gap_event *event, void *arg)
|
|
{
|
|
struct ble_gap_conn_desc desc;
|
|
int rc;
|
|
|
|
switch (event->type) {
|
|
case BLE_GAP_EVENT_CONNECT:
|
|
/* A new connection was established or a connection attempt failed. */
|
|
MODLOG_DFLT(INFO, "connection %s; status=%d ",
|
|
event->connect.status == 0 ? "established" : "failed",
|
|
event->connect.status);
|
|
if (event->connect.status == 0) {
|
|
rc = ble_gap_conn_find(event->connect.conn_handle, &desc);
|
|
assert(rc == 0);
|
|
lble_print_conn_desc(&desc);
|
|
|
|
}
|
|
MODLOG_DFLT(INFO, "\n");
|
|
|
|
if (event->connect.status != 0) {
|
|
/* Connection failed; resume advertising. */
|
|
lble_start_advertising();
|
|
}
|
|
return 0;
|
|
|
|
case BLE_GAP_EVENT_DISCONNECT:
|
|
MODLOG_DFLT(INFO, "disconnect; reason=%d ", event->disconnect.reason);
|
|
lble_print_conn_desc(&event->disconnect.conn);
|
|
MODLOG_DFLT(INFO, "\n");
|
|
|
|
/* Connection terminated; resume advertising. */
|
|
lble_start_advertising();
|
|
return 0;
|
|
|
|
case BLE_GAP_EVENT_CONN_UPDATE:
|
|
/* The central has updated the connection parameters. */
|
|
MODLOG_DFLT(INFO, "connection updated; status=%d ",
|
|
event->conn_update.status);
|
|
rc = ble_gap_conn_find(event->conn_update.conn_handle, &desc);
|
|
assert(rc == 0);
|
|
lble_print_conn_desc(&desc);
|
|
MODLOG_DFLT(INFO, "\n");
|
|
return 0;
|
|
|
|
case BLE_GAP_EVENT_ADV_COMPLETE:
|
|
MODLOG_DFLT(INFO, "advertise complete; reason=%d",
|
|
event->adv_complete.reason);
|
|
lble_start_advertising();
|
|
return 0;
|
|
|
|
case BLE_GAP_EVENT_ENC_CHANGE:
|
|
/* Encryption has been enabled or disabled for this connection. */
|
|
MODLOG_DFLT(INFO, "encryption change event; status=%d ",
|
|
event->enc_change.status);
|
|
rc = ble_gap_conn_find(event->connect.conn_handle, &desc);
|
|
assert(rc == 0);
|
|
lble_print_conn_desc(&desc);
|
|
MODLOG_DFLT(INFO, "\n");
|
|
return 0;
|
|
|
|
case BLE_GAP_EVENT_SUBSCRIBE:
|
|
MODLOG_DFLT(INFO, "subscribe event; conn_handle=%d attr_handle=%d "
|
|
"reason=%d prevn=%d curn=%d previ=%d curi=%d\n",
|
|
event->subscribe.conn_handle,
|
|
event->subscribe.attr_handle,
|
|
event->subscribe.reason,
|
|
event->subscribe.prev_notify,
|
|
event->subscribe.cur_notify,
|
|
event->subscribe.prev_indicate,
|
|
event->subscribe.cur_indicate);
|
|
return 0;
|
|
|
|
case BLE_GAP_EVENT_MTU:
|
|
MODLOG_DFLT(INFO, "mtu update event; conn_handle=%d cid=%d mtu=%d\n",
|
|
event->mtu.conn_handle,
|
|
event->mtu.channel_id,
|
|
event->mtu.value);
|
|
return 0;
|
|
|
|
case BLE_GAP_EVENT_REPEAT_PAIRING:
|
|
/* We already have a bond with the peer, but it is attempting to
|
|
* establish a new secure link. This app sacrifices security for
|
|
* convenience: just throw away the old bond and accept the new link.
|
|
*/
|
|
|
|
/* Delete the old bond. */
|
|
rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
|
|
assert(rc == 0);
|
|
ble_store_util_delete_peer(&desc.peer_id_addr);
|
|
|
|
/* Return BLE_GAP_REPEAT_PAIRING_RETRY to indicate that the host should
|
|
* continue with the pairing operation.
|
|
*/
|
|
return BLE_GAP_REPEAT_PAIRING_RETRY;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
lble_update_adv_fields() {
|
|
struct ble_hs_adv_fields fields;
|
|
const char *name = gadget_name;
|
|
/**
|
|
* Set the advertisement data included in our advertisements:
|
|
* o Flags (indicates advertisement type and other general info).
|
|
* o Advertising tx power.
|
|
* o Device name.
|
|
* o 16-bit service UUIDs (alert notifications).
|
|
*/
|
|
|
|
memset(&fields, 0, sizeof fields);
|
|
|
|
/* Advertise two flags:
|
|
* o Discoverability in forthcoming advertisement (general)
|
|
* o BLE-only (BR/EDR unsupported).
|
|
*/
|
|
fields.flags = BLE_HS_ADV_F_DISC_GEN |
|
|
BLE_HS_ADV_F_BREDR_UNSUP;
|
|
|
|
/* Indicate that the TX power level field should be included; have the
|
|
* stack fill this value automatically. This is done by assiging the
|
|
* special value BLE_HS_ADV_TX_PWR_LVL_AUTO.
|
|
*/
|
|
fields.tx_pwr_lvl_is_present = 1;
|
|
fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
|
|
|
fields.mfg_data = gadget_mfg;
|
|
fields.mfg_data_len = gadget_mfg_len;
|
|
|
|
size_t name_length = strlen(name);
|
|
fields.name = (uint8_t *)name;
|
|
fields.name_len = name_length;
|
|
fields.name_is_complete = 1;
|
|
|
|
// See if we can fit the whole name or need to truncate
|
|
while (1) {
|
|
int rc = ble_gap_adv_set_fields(&fields);
|
|
if (!rc) {
|
|
break;
|
|
}
|
|
if (!fields.name_len) {
|
|
MODLOG_DFLT(INFO, "error setting advertisement data; rc=%d", rc);
|
|
return rc;
|
|
}
|
|
fields.name_len = fields.name_len - 1;
|
|
fields.name_is_complete = 0;
|
|
}
|
|
|
|
if (!fields.name_is_complete) {
|
|
struct ble_hs_adv_fields scan_response_fields;
|
|
memset(&scan_response_fields, 0, sizeof scan_response_fields);
|
|
scan_response_fields.name = (uint8_t *)name;
|
|
scan_response_fields.name_len = name_length;
|
|
scan_response_fields.name_is_complete = 1;
|
|
int rc = ble_gap_adv_rsp_set_fields(&scan_response_fields);
|
|
if (rc) {
|
|
MODLOG_DFLT(INFO, "gap_adv_rsp_set_fields failed: %d", rc);
|
|
return rc;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
lble_start_advertising() {
|
|
uint8_t own_addr_type;
|
|
struct ble_gap_adv_params adv_params;
|
|
int rc;
|
|
|
|
if (inited != RUNNING) {
|
|
return;
|
|
}
|
|
|
|
/* Figure out address to use while advertising (no privacy for now) */
|
|
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
|
if (rc != 0) {
|
|
MODLOG_DFLT(INFO, "error determining address type; rc=%d", rc);
|
|
return;
|
|
}
|
|
|
|
rc = lble_update_adv_fields();
|
|
if (rc != 0) {
|
|
return;
|
|
}
|
|
|
|
/* Begin advertising. */
|
|
memset(&adv_params, 0, sizeof adv_params);
|
|
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
|
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
|
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
|
|
&adv_params, lble_gap_event, NULL);
|
|
if (rc != 0) {
|
|
MODLOG_DFLT(INFO, "error enabling advertisement; rc=%d", rc);
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
static void
|
|
lble_on_sync(void)
|
|
{
|
|
int rc;
|
|
|
|
rc = ble_hs_util_ensure_addr(0);
|
|
assert(rc == 0);
|
|
|
|
if (synced == READY_TO_ADVERTISE) {
|
|
lble_start_advertising();
|
|
synced = ADVERTISING;
|
|
}
|
|
if (synced == IDLE) {
|
|
synced = SYNCED;
|
|
}
|
|
}
|
|
|
|
void
|
|
gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg)
|
|
{
|
|
char buf[BLE_UUID_STR_LEN];
|
|
|
|
switch (ctxt->op) {
|
|
case BLE_GATT_REGISTER_OP_SVC:
|
|
MODLOG_DFLT(DEBUG, "registered service %s with handle=%d\n",
|
|
ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf),
|
|
ctxt->svc.handle);
|
|
break;
|
|
|
|
case BLE_GATT_REGISTER_OP_CHR:
|
|
MODLOG_DFLT(DEBUG, "registering characteristic %s with "
|
|
"def_handle=%d val_handle=%d\n",
|
|
ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf),
|
|
ctxt->chr.def_handle,
|
|
ctxt->chr.val_handle);
|
|
break;
|
|
|
|
case BLE_GATT_REGISTER_OP_DSC:
|
|
MODLOG_DFLT(DEBUG, "registering descriptor %s with handle=%d\n",
|
|
ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf),
|
|
ctxt->dsc.handle);
|
|
break;
|
|
|
|
default:
|
|
assert(0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int
|
|
lble_update_adv_change(lua_State *L) {
|
|
free((void *) gadget_mfg);
|
|
gadget_mfg = NULL;
|
|
gadget_mfg_len = 0;
|
|
|
|
if (!lua_isnoneornil(L, -1)) {
|
|
size_t len;
|
|
const char *mfg = lua_tolstring(L, 1, &len);
|
|
gadget_mfg = malloc(len);
|
|
if (!gadget_mfg) {
|
|
return luaL_error(L, "out of memory");
|
|
}
|
|
gadget_mfg_len = len;
|
|
memcpy(gadget_mfg, mfg, len);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
lble_update_adv(lua_State *L) {
|
|
lua_pushvalue(L, 1);
|
|
lble_update_adv_change(L);
|
|
|
|
if (lble_update_adv_fields()) {
|
|
return luaL_error(L, "Unable to update advertisement");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int lble_notify(lua_State *L) {
|
|
if (inited != RUNNING) {
|
|
return luaL_error(L, "ble is not yet running");
|
|
}
|
|
int handle = luaL_checkinteger(L, 1);
|
|
|
|
luaL_argcheck(L, handle > 0 && handle <= notify_handles[0], 1, "handle out of range");
|
|
|
|
ble_gatts_chr_updated(notify_handles[handle]);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int lble_init(lua_State *L) {
|
|
if (inited != STOPPED) {
|
|
return luaL_error(L, "ble is already running");
|
|
}
|
|
if (already_inited) {
|
|
return luaL_error(L, "Can only call ble.init once. Internal stack problem.");
|
|
}
|
|
already_inited = true;
|
|
if (!struct_pack_index) {
|
|
lua_getglobal(L, "struct");
|
|
lua_getfield(L, -1, "pack");
|
|
struct_pack_index = luaL_ref(L, LUA_REGISTRYINDEX);
|
|
lua_getfield(L, -1, "unpack");
|
|
struct_unpack_index = luaL_ref(L, LUA_REGISTRYINDEX);
|
|
lua_pop(L, 1);
|
|
}
|
|
// Passed the config table
|
|
luaL_checktype(L, 1, LUA_TTABLE);
|
|
|
|
lua_getfield(L, 1, "name");
|
|
const char *name = strdup(luaL_checkstring(L, -1));
|
|
|
|
free((void *) gadget_name);
|
|
gadget_name = name;
|
|
|
|
int rc;
|
|
|
|
lua_getfield(L, 1, "advertisement");
|
|
|
|
rc = lble_update_adv_change(L);
|
|
|
|
lua_pop(L, 1);
|
|
|
|
synced = IDLE;
|
|
|
|
lble_init_stack(L);
|
|
|
|
/* Initialize the NimBLE host configuration. */
|
|
// ble_hs_cfg.reset_cb = bleprph_on_reset;
|
|
ble_hs_cfg.sync_cb = lble_on_sync;
|
|
ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb;
|
|
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
|
|
|
rc = gatt_svr_init(L);
|
|
if (rc) {
|
|
luaL_error(L, "Failed to gatt_svr_init: %d", rc);
|
|
}
|
|
|
|
bool seen1800 = false;
|
|
// See if we already have the 1800 service registered
|
|
for (const struct ble_gatt_svc_def *svcs = gatt_svr_svcs; svcs->type; svcs++) {
|
|
if (!ble_uuid_cmp(svcs->uuid, BLE_UUID16_DECLARE(0x1800))) {
|
|
seen1800 = true;
|
|
}
|
|
}
|
|
|
|
if (!seen1800) {
|
|
ble_svc_gap_init();
|
|
}
|
|
ble_svc_gatt_init();
|
|
|
|
ble_gatts_start();
|
|
|
|
inited = RUNNING;
|
|
|
|
if (synced == SYNCED) {
|
|
lble_start_advertising();
|
|
synced = ADVERTISING;
|
|
} else {
|
|
synced = READY_TO_ADVERTISE;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int lble_shutdown(lua_State *L) {
|
|
// It seems that shutting down the stack corrupts some critical data structures
|
|
// so, for now, don't allow it.
|
|
luaL_error(L, "Shutting down the BLE stack is currently not possible");
|
|
|
|
inited = SHUTTING;
|
|
|
|
ble_gap_adv_stop();
|
|
|
|
if (nimble_port_stop()) {
|
|
return luaL_error(L, "Failed to stop the NIMBLE task");
|
|
}
|
|
|
|
nimble_port_deinit();
|
|
|
|
if (ESP_OK != esp_nimble_hci_and_controller_deinit()) {
|
|
return luaL_error(L, "Failed to shutdown the BLE controller");
|
|
}
|
|
|
|
inited = STOPPED;
|
|
|
|
return 0;
|
|
}
|
|
|
|
LROT_BEGIN(lble, NULL, 0)
|
|
LROT_FUNCENTRY( init, lble_init )
|
|
LROT_FUNCENTRY( notify, lble_notify )
|
|
LROT_FUNCENTRY( advertise, lble_update_adv )
|
|
LROT_FUNCENTRY( shutdown, lble_shutdown )
|
|
LROT_END(lble, NULL, 0)
|
|
|
|
NODEMCU_MODULE(BLE, "ble", lble, lble_sys_init);
|
|
#endif
|