init: initial commit

This commit is contained in:
2026-05-13 14:44:38 +02:00
commit 3fc50e797d
26 changed files with 4845 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "src/ble_scanner.c"
INCLUDE_DIRS "include"
REQUIRES bt esp_event
)
@@ -0,0 +1,22 @@
#pragma once
#include <stdint.h>
/**
* Called for every unique BLE advertisement received.
* tag_id is a null-terminated string: "aa:bb:cc:dd:ee:ff"
* rssi is in dBm (negative).
*/
typedef void (*ble_scanner_cb_t)(const char *tag_id, int8_t rssi);
/**
* Initialise the Bluedroid BLE stack and register the scan callback.
* Must be called once after esp_bt_controller_init / esp_bluedroid_init.
*/
void ble_scanner_init(ble_scanner_cb_t cb);
/** Start passive BLE scanning. */
void ble_scanner_start(void);
/** Stop BLE scanning. */
void ble_scanner_stop(void);
+89
View File
@@ -0,0 +1,89 @@
#include "ble_scanner.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_gap_ble_api.h"
#include "esp_log.h"
#include <stdio.h>
#include <string.h>
#define TAG "ble_scanner"
static ble_scanner_cb_t s_cb = NULL;
/* Passive scan: 100ms interval, 50ms window (50% duty cycle) */
static esp_ble_scan_params_t s_scan_params = {
.scan_type = BLE_SCAN_TYPE_PASSIVE,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_interval = 0xA0, /* 100ms: 0xA0 * 0.625ms */
.scan_window = 0x50, /* 50ms: 0x50 * 0.625ms */
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE,
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) {
esp_ble_gap_start_scanning(0); /* 0 = scan indefinitely */
} else {
ESP_LOGE(TAG, "Scan param set failed: %d", param->scan_param_cmpl.status);
}
break;
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(TAG, "Scan start failed: %d", param->scan_start_cmpl.status);
}
break;
case ESP_GAP_BLE_SCAN_RESULT_EVT: {
esp_ble_gap_cb_param_t *p = param;
if (p->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT && s_cb) {
char tag_id[18];
uint8_t *a = p->scan_rst.bda;
snprintf(tag_id, sizeof(tag_id),
"%02x:%02x:%02x:%02x:%02x:%02x",
a[0], a[1], a[2], a[3], a[4], a[5]);
s_cb(tag_id, p->scan_rst.rssi);
}
break;
}
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
ESP_LOGI(TAG, "Scan stopped");
break;
default:
break;
}
}
void ble_scanner_init(ble_scanner_cb_t cb)
{
s_cb = cb;
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg));
ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE));
ESP_ERROR_CHECK(esp_bluedroid_init());
ESP_ERROR_CHECK(esp_bluedroid_enable());
ESP_ERROR_CHECK(esp_ble_gap_register_callback(gap_event_handler));
ESP_LOGI(TAG, "BLE scanner initialised");
}
void ble_scanner_start(void)
{
ESP_LOGI(TAG, "Starting BLE scan");
ESP_ERROR_CHECK(esp_ble_gap_set_scan_params(&s_scan_params));
/* Scanning begins in the param-set callback once confirmed */
}
void ble_scanner_stop(void)
{
esp_ble_gap_stop_scanning();
}
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "src/config_store.c"
INCLUDE_DIRS "include"
REQUIRES nvs_flash
)
@@ -0,0 +1,22 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#define MQTT_HOST_MAX_LEN 128
#define MQTT_URI_MAX_LEN 256
esp_err_t config_store_init(void);
bool config_store_is_provisioned(void);
esp_err_t config_store_set_provisioned(void);
esp_err_t config_store_clear_provisioned(void);
/* Store an optional manual MQTT broker override (used if mDNS fails). */
esp_err_t config_store_set_mqtt_override(const char *host, uint16_t port);
/* Returns ESP_ERR_NVS_NOT_FOUND if no override is stored. */
esp_err_t config_store_get_mqtt_override(char host_out[MQTT_HOST_MAX_LEN], uint16_t *port_out);
/* Returns ESP_ERR_NVS_NOT_FOUND if no override is stored. */
esp_err_t config_store_get_mqtt_override_uri(char *uri_out, size_t uri_max_len);
@@ -0,0 +1,98 @@
#include "config_store.h"
#include "esp_err.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"
#include <string.h>
#define NS "anchor_cfg"
#define KEY_PROV "provisioned"
#define KEY_HOST "mqtt_host"
#define KEY_PORT "mqtt_port"
static const char *TAG = "config_store";
esp_err_t config_store_init(void)
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "NVS needs erase, erasing...");
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
return err;
}
bool config_store_is_provisioned(void)
{
nvs_handle_t h;
if (nvs_open(NS, NVS_READONLY, &h) != ESP_OK) return false;
uint8_t val = 0;
nvs_get_u8(h, KEY_PROV, &val);
nvs_close(h);
return val == 1;
}
esp_err_t config_store_set_provisioned(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_set_u8(h, KEY_PROV, 1);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
esp_err_t config_store_clear_provisioned(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
nvs_erase_key(h, KEY_PROV);
nvs_erase_key(h, KEY_HOST);
nvs_erase_key(h, KEY_PORT);
err = nvs_commit(h);
nvs_close(h);
return err;
}
esp_err_t config_store_set_mqtt_override(const char *host, uint16_t port)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_set_str(h, KEY_HOST, host);
if (err == ESP_OK) err = nvs_set_u16(h, KEY_PORT, port);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
esp_err_t config_store_get_mqtt_override(char host_out[MQTT_HOST_MAX_LEN], uint16_t *port_out)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NS, NVS_READONLY, &h);
if (err != ESP_OK) return err;
size_t len = MQTT_HOST_MAX_LEN;
err = nvs_get_str(h, KEY_HOST, host_out, &len);
if (err == ESP_OK) {
err = nvs_get_u16(h, KEY_PORT, port_out);
}
nvs_close(h);
return err;
}
esp_err_t config_store_get_mqtt_override_uri(char *uri_out, size_t uri_max_len)
{
char broker_host[MQTT_HOST_MAX_LEN] = {0};
uint16_t broker_port;
bool resolved
= (config_store_get_mqtt_override(broker_host, &broker_port) == ESP_OK);
if(!resolved) return ESP_ERR_NVS_NOT_FOUND;
snprintf(uri_out, uri_max_len, "mqtt://%s:%u", broker_host, broker_port);
return ESP_OK;
}
+6
View File
@@ -0,0 +1,6 @@
idf_component_register(
SRCS "src/led_indicator.c"
INCLUDE_DIRS "include"
REQUIRES driver esp_timer
PRIV_REQUIRES esp_driver_gpio
)
@@ -0,0 +1,14 @@
#pragma once
typedef enum {
LED_OFF,
LED_PROVISIONING, /* 200ms toggle — awaiting provisioning */
LED_CONNECTING, /* 1000ms toggle — connecting to WiFi/MQTT */
LED_SCANNING, /* solid on — normal BLE scanning */
LED_CALIBRATING, /* 500ms toggle — calibration in progress */
LED_SELECTED, /* triple-flash loop — physical identification */
LED_ERROR, /* 50ms rapid blink */
} led_state_t;
void led_indicator_init(void);
void led_indicator_set(led_state_t state);
@@ -0,0 +1,95 @@
#include "led_indicator.h"
#include "driver/gpio.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <stdint.h>
#define LED_GPIO GPIO_NUM_2
static esp_timer_handle_t s_timer;
static led_state_t s_state = LED_OFF;
static int s_phase = 0; /* generic phase counter for multi-step patterns */
static void set_level(int lvl) { gpio_set_level(LED_GPIO, lvl); }
static void IRAM_ATTR timer_cb(void *arg)
{
switch (s_state) {
case LED_OFF:
set_level(0);
break;
case LED_PROVISIONING:
set_level(s_phase ^= 1);
esp_timer_start_once(s_timer, 200 * 1000);
break;
case LED_CONNECTING:
set_level(s_phase ^= 1);
esp_timer_start_once(s_timer, 1000 * 1000);
break;
case LED_SCANNING:
set_level(1);
break;
case LED_CALIBRATING:
set_level(s_phase ^= 1);
esp_timer_start_once(s_timer, 500 * 1000);
break;
case LED_SELECTED: {
/* Triple flash: on/off/on/off/on/off, then 1s dark. s_phase 0-5 = flashes, 6 = pause */
static const uint64_t us[] = {100000,100000,100000,100000,100000,100000,1000000};
if (s_phase < 6) {
set_level(s_phase % 2 == 0 ? 1 : 0);
} else {
set_level(0);
}
uint64_t delay = us[s_phase];
s_phase = (s_phase + 1) % 7;
esp_timer_start_once(s_timer, delay);
break;
}
case LED_ERROR:
set_level(s_phase ^= 1);
esp_timer_start_once(s_timer, 50 * 1000);
break;
}
}
void led_indicator_init(void)
{
gpio_config_t io = {
.pin_bit_mask = (1ULL << LED_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io);
esp_timer_create_args_t args = {
.callback = timer_cb,
.name = "led",
};
esp_timer_create(&args, &s_timer);
}
void led_indicator_set(led_state_t state)
{
esp_timer_stop(s_timer);
s_state = state;
s_phase = 0;
if (state == LED_SCANNING) {
set_level(1);
} else if (state == LED_OFF) {
set_level(0);
} else {
/* Kick off the timer-driven pattern immediately */
esp_timer_start_once(s_timer, 0);
}
}
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "src/mqtt_publisher.c"
INCLUDE_DIRS "include"
REQUIRES mqtt esp_event
)
@@ -0,0 +1,4 @@
dependencies:
espressif/cjson: "*"
idf:
version: ">=5.1.0"
@@ -0,0 +1,29 @@
#pragma once
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
/* Event bits set by the MQTT publisher into the shared event group */
#define MQTT_CONNECTED_BIT BIT0
#define MQTT_CALIBRATE_START BIT1
#define MQTT_CALIBRATE_STOP BIT2
#define MQTT_SELECTED_BIT BIT3
#define MQTT_DESELECTED_BIT BIT4
/**
* Initialise and connect the MQTT client.
*
* sensor_id — stable anchor ID string (e.g. "anchor_a1b2c3")
* broker_uri — e.g. "mqtt://192.168.1.100:1883"
* evt_group — FreeRTOS event group; publisher sets bits above on events
*/
esp_err_t mqtt_publisher_init(const char *sensor_id,
const char *broker_uri,
EventGroupHandle_t evt_group);
/** Publish an RSSI reading. Non-blocking (QoS 1). */
void mqtt_publisher_send_rssi(const char *tag_id, int8_t rssi);
/** Publish the announce message (empty payload). */
void mqtt_publisher_announce(void);
@@ -0,0 +1,122 @@
#include "mqtt_publisher.h"
#include "mqtt_client.h"
#include "esp_log.h"
#include "cJSON.h"
#include <string.h>
#include <stdio.h>
#define TAG "mqtt_publisher"
#define TOPIC_PREFIX "localiser/sensor"
static esp_mqtt_client_handle_t s_client = NULL;
static char s_sensor_id[32];
static EventGroupHandle_t s_evt = NULL;
/* Pre-built topic strings */
static char s_topic_rssi[96];
static char s_topic_announce[96];
static char s_topic_cmd[96];
static void build_topics(void)
{
snprintf(s_topic_rssi, sizeof(s_topic_rssi),
"%s/%s/rssi", TOPIC_PREFIX, s_sensor_id);
snprintf(s_topic_announce, sizeof(s_topic_announce),
"%s/%s/announce", TOPIC_PREFIX, s_sensor_id);
snprintf(s_topic_cmd, sizeof(s_topic_cmd),
"%s/%s/cmd", TOPIC_PREFIX, s_sensor_id);
}
static void handle_cmd(const char *data, int data_len)
{
char *buf = strndup(data, data_len);
if (!buf) return;
cJSON *root = cJSON_Parse(buf);
free(buf);
if (!root) return;
cJSON *action = cJSON_GetObjectItemCaseSensitive(root, "action");
if (cJSON_IsString(action)) {
const char *a = action->valuestring;
if (strcmp(a, "calibrate_start") == 0) xEventGroupSetBits(s_evt, MQTT_CALIBRATE_START);
else if (strcmp(a, "calibrate_stop") == 0) xEventGroupSetBits(s_evt, MQTT_CALIBRATE_STOP);
else if (strcmp(a, "selected") == 0) xEventGroupSetBits(s_evt, MQTT_SELECTED_BIT);
else if (strcmp(a, "deselected") == 0) xEventGroupSetBits(s_evt, MQTT_DESELECTED_BIT);
else ESP_LOGW(TAG, "Unknown cmd action: %s", a);
}
cJSON_Delete(root);
}
static void mqtt_event_handler(void *arg, esp_event_base_t base,
int32_t id, void *data)
{
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)data;
switch ((esp_mqtt_event_id_t)id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "Connected to broker");
esp_mqtt_client_subscribe(s_client, s_topic_cmd, 1);
xEventGroupSetBits(s_evt, MQTT_CONNECTED_BIT);
mqtt_publisher_announce();
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "Disconnected from broker");
xEventGroupClearBits(s_evt, MQTT_CONNECTED_BIT);
break;
case MQTT_EVENT_DATA:
if (strncmp(event->topic, s_topic_cmd, event->topic_len) == 0) {
handle_cmd(event->data, event->data_len);
}
break;
case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT error");
break;
default:
break;
}
}
esp_err_t mqtt_publisher_init(const char *sensor_id,
const char *broker_uri,
EventGroupHandle_t evt_group)
{
strncpy(s_sensor_id, sensor_id, sizeof(s_sensor_id) - 1);
s_evt = evt_group;
build_topics();
esp_mqtt_client_config_t cfg = {
.broker.address.uri = broker_uri,
.credentials.client_id = sensor_id,
.session.keepalive = 30,
.network.reconnect_timeout_ms = 5000,
};
s_client = esp_mqtt_client_init(&cfg);
if (!s_client) return ESP_FAIL;
ESP_ERROR_CHECK(esp_mqtt_client_register_event(
s_client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL));
return esp_mqtt_client_start(s_client);
}
void mqtt_publisher_announce(void)
{
if (!s_client) return;
esp_mqtt_client_publish(s_client, s_topic_announce, "", 0, 1, 0);
ESP_LOGI(TAG, "Announced on %s", s_topic_announce);
}
void mqtt_publisher_send_rssi(const char *tag_id, int8_t rssi)
{
if (!s_client) return;
char payload[80];
int len = snprintf(payload, sizeof(payload),
"{\"tag_id\":\"%s\",\"rssi\":%d}", tag_id, (int)rssi);
esp_mqtt_client_publish(s_client, s_topic_rssi, payload, len, 1, 0);
}
+12
View File
@@ -0,0 +1,12 @@
idf_component_register(
SRCS "src/provisioning.c"
INCLUDE_DIRS "include"
REQUIRES
network_provisioning
esp_wifi
esp_event
nvs_flash
config_store
led_indicator
bt
)
@@ -0,0 +1,5 @@
dependencies:
espressif/network_provisioning: "^1.2.4"
espressif/cjson: "*"
idf:
version: ">=5.1.0"
@@ -0,0 +1,12 @@
#pragma once
#include "esp_err.h"
/**
* Start BLE provisioning mode. Blocks until provisioning is complete,
* then deinits the provisioning manager so BLE memory can be reclaimed.
*
* WiFi credentials are stored automatically by wifi_prov_mgr.
* MQTT broker override (if sent by companion app) is stored via config_store.
*/
esp_err_t provisioning_run(void);
+133
View File
@@ -0,0 +1,133 @@
#include "provisioning.h"
#include "config_store.h"
#include "led_indicator.h"
#include "network_provisioning/manager.h"
#include "network_provisioning/scheme_ble.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "freertos/event_groups.h"
#include "cJSON.h"
#include <string.h>
#define TAG "provisioning"
/* Custom GATT endpoint name for MQTT settings. The companion app sends:
* {"host":"192.168.1.100","port":1883} */
#define MQTT_ENDPOINT "custom-mqtt-config"
static EventGroupHandle_t s_prov_evt;
#define PROV_DONE_BIT BIT0
/* Called by wifi_prov_mgr when data arrives on the custom endpoint. */
static esp_err_t mqtt_endpoint_handler(uint32_t session_id,
const uint8_t *inbuf, ssize_t inlen,
uint8_t **outbuf, ssize_t *outlen,
void *priv)
{
if (!inbuf || inlen <= 0) return ESP_OK;
char *json = strndup((const char *)inbuf, inlen);
if (!json) return ESP_ERR_NO_MEM;
cJSON *root = cJSON_Parse(json);
free(json);
if (!root) {
ESP_LOGW(TAG, "Failed to parse MQTT config JSON");
return ESP_OK;
}
cJSON *host_j = cJSON_GetObjectItemCaseSensitive(root, "host");
cJSON *port_j = cJSON_GetObjectItemCaseSensitive(root, "port");
if (cJSON_IsString(host_j) && cJSON_IsNumber(port_j)) {
uint16_t port = (uint16_t)port_j->valuedouble;
esp_err_t err = config_store_set_mqtt_override(host_j->valuestring, port);
if (err == ESP_OK) {
ESP_LOGI(TAG, "MQTT override stored: %s:%u", host_j->valuestring, port);
} else {
ESP_LOGW(TAG, "Failed to store MQTT override: %s", esp_err_to_name(err));
}
} else {
ESP_LOGW(TAG, "MQTT config missing 'host' or 'port'");
}
cJSON_Delete(root);
/* Respond with a simple ack */
const char *ack = "{\"status\":\"ok\"}";
*outlen = strlen(ack);
*outbuf = (uint8_t *)strdup(ack);
return ESP_OK;
}
static void prov_event_handler(void *arg, esp_event_base_t base,
int32_t id, void *data)
{
if (base == NETWORK_PROV_EVENT) {
switch (id) {
case NETWORK_PROV_START:
ESP_LOGI(TAG, "Provisioning started");
break;
case NETWORK_PROV_WIFI_CRED_RECV:
ESP_LOGI(TAG, "WiFi credentials received");
break;
case NETWORK_PROV_WIFI_CRED_FAIL: {
network_prov_wifi_sta_fail_reason_t *reason = (network_prov_wifi_sta_fail_reason_t *)data;
ESP_LOGE(TAG, "Provisioning failed: %s",
(*reason == NETWORK_PROV_WIFI_STA_AUTH_ERROR) ? "auth error" : "AP not found");
break;
}
case NETWORK_PROV_WIFI_CRED_SUCCESS:
ESP_LOGI(TAG, "WiFi credentials applied successfully");
break;
case NETWORK_PROV_END:
ESP_LOGI(TAG, "Provisioning complete");
network_prov_mgr_deinit();
xEventGroupSetBits(s_prov_evt, PROV_DONE_BIT);
break;
default:
break;
}
}
}
esp_err_t provisioning_run(void)
{
s_prov_evt = xEventGroupCreate();
esp_event_handler_register(NETWORK_PROV_EVENT, ESP_EVENT_ANY_ID,
prov_event_handler, NULL);
network_prov_mgr_config_t config = {
.scheme = network_prov_scheme_ble,
.scheme_event_handler = NETWORK_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BTDM,
};
ESP_ERROR_CHECK(network_prov_mgr_init(config));
/* Register custom endpoint for MQTT broker config */
network_prov_mgr_endpoint_create(MQTT_ENDPOINT);
/* Derive device name and PoP from WiFi MAC */
uint8_t mac[6];
esp_wifi_get_mac(WIFI_IF_STA, mac);
char device_name[32];
snprintf(device_name, sizeof(device_name),
"anchor_%02x%02x%02x", mac[3], mac[4], mac[5]);
led_indicator_set(LED_PROVISIONING);
ESP_ERROR_CHECK(network_prov_mgr_start_provisioning(
NETWORK_PROV_SECURITY_0, NULL, device_name, NULL));
network_prov_mgr_endpoint_register(MQTT_ENDPOINT, mqtt_endpoint_handler, NULL);
/* Block until provisioning completes */
xEventGroupWaitBits(s_prov_evt, PROV_DONE_BIT, pdTRUE, pdTRUE, portMAX_DELAY);
vEventGroupDelete(s_prov_evt);
config_store_set_provisioned();
ESP_LOGI(TAG, "Provisioning done, NVS flag set");
return ESP_OK;
}