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
+4
View File
@@ -0,0 +1,4 @@
.cache/
build/
managed_components/
sdkconfig.old
+6
View File
@@ -0,0 +1,6 @@
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(esp-anchor)
+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;
}
+60
View File
@@ -0,0 +1,60 @@
dependencies:
espressif/cjson:
component_hash: e788323270d90738662d66fffa910bfe1fba019bba087f01557e70c40485b469
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.7.19~2
espressif/mdns:
component_hash: 8bcf12e37c58c1d584aef32a02b92548124c7a3a9fcf548d3235c844a035e0f0
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.11.1
espressif/mqtt:
component_hash: ffdad5659706b4dc14bc63f8eb73ef765efa015bf7e9adf71c813d52a2dc9342
dependencies:
- name: idf
require: private
version: '>=5.3'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.0.0
espressif/network_provisioning:
component_hash: 72d27784e3daf807418a34fb00be136ec50c6db49d989ce981d22e031fc0e7f8
dependencies:
- name: espressif/cjson
registry_url: https://components.espressif.com
require: private
rules:
- if: idf_version >= 6.0
version: ^1.7.19
- name: idf
require: private
version: '>=5.1'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.2.4
idf:
source:
type: idf
version: 6.0.0
direct_dependencies:
- espressif/cjson
- espressif/mdns
- espressif/mqtt
- espressif/network_provisioning
- idf
manifest_hash: c33499c91a18b09118ae65bbf2c53ea2ed5169f05d8a71dd7b7c5f46ef33c7c2
target: esp32
version: 3.0.0
+15
View File
@@ -0,0 +1,15 @@
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES
nvs_flash
esp_wifi
esp_event
esp_netif
mdns
config_store
provisioning
ble_scanner
mqtt_publisher
led_indicator
)
+5
View File
@@ -0,0 +1,5 @@
dependencies:
espressif/mqtt: '*'
idf:
version: '>=5.1.0'
espressif/mdns: '*'
+212
View File
@@ -0,0 +1,212 @@
#include "config_store.h"
#include "provisioning.h"
#include "ble_scanner.h"
#include "mqtt_publisher.h"
#include "led_indicator.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "mdns.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/task.h"
#include <stdio.h>
#include <string.h>
#define TAG "main"
#define WIFI_CONNECTED_BIT BIT5 /* offset to avoid clashing with mqtt_publisher bits */
#define DEFAULT_MQTT_PORT 1883
#define MDNS_QUERY_TIMEOUT_MS 3000
#define MDNS_RETRY_INTERVAL_MS 30000
static EventGroupHandle_t s_evt;
static void wifi_event_handler(void *arg, esp_event_base_t base,
int32_t id, void *data)
{
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
ESP_LOGW(TAG, "WiFi disconnected, reconnecting...");
led_indicator_set(LED_CONNECTING);
xEventGroupClearBits(s_evt, WIFI_CONNECTED_BIT);
esp_wifi_connect();
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *evt = (ip_event_got_ip_t *)data;
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&evt->ip_info.ip));
xEventGroupSetBits(s_evt, WIFI_CONNECTED_BIT);
}
}
static void on_ble_scan_result(const char *tag_id, int8_t rssi)
{
mqtt_publisher_send_rssi(tag_id, rssi);
}
static void wifi_init_sta(void)
{
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL);
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL);
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_connect());
}
static bool resolve_mqtt_broker_mdns(char *host_out, size_t host_len, uint16_t *port_out)
{
ESP_ERROR_CHECK(mdns_init());
mdns_result_t *results = NULL;
bool found = false;
esp_err_t err = mdns_query_ptr("_mqtt", "_tcp", MDNS_QUERY_TIMEOUT_MS, 4, &results);
if (err != ESP_OK || !results) {
ESP_LOGW(TAG, "mDNS query for _mqtt._tcp failed");
goto free_results;
}
mdns_result_t *r = results;
while (r) {
if (r->hostname && r->port) {
strncpy(host_out, r->hostname, host_len - 1);
*port_out = r->port;
ESP_LOGI(TAG, "mDNS found broker: %s:%u", host_out, *port_out);
found = true;
goto free_results;
}
r = r->next;
}
free_results:
mdns_query_results_free(results);
mdns_free();
return found;
}
void app_main(void)
{
ESP_ERROR_CHECK(config_store_init());
led_indicator_init();
led_indicator_set(LED_CONNECTING);
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
s_evt = xEventGroupCreate();
// Provisioning
if (!config_store_is_provisioned()) {
ESP_LOGI(TAG, "Not provisioned - starting BLE provisioning");
/* WiFi must be started (STA mode) before provisioning manager */
esp_netif_create_default_wifi_sta();
wifi_init_config_t wcfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&wcfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
provisioning_run(); /* blocks until complete */
/* Release BT memory now that provisioning used it; we reinit below for scanning */
esp_bt_mem_release(ESP_BT_MODE_BTDM);
}
// connect to wifi
ESP_LOGI(TAG, "Connecting to WiFi...");
led_indicator_set(LED_CONNECTING);
// wifi_init_sta registers events and calls connect; skip if already inited
wifi_init_sta();
xEventGroupWaitBits(s_evt, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
ESP_LOGI(TAG, "WiFi connected");
// derive sensor_id from WiFi MAC
uint8_t mac[6];
esp_wifi_get_mac(WIFI_IF_STA, mac);
char sensor_id[32];
snprintf(sensor_id, sizeof(sensor_id),
"anchor_%02x%02x%02x", mac[3], mac[4], mac[5]);
ESP_LOGI(TAG, "Sensor ID: %s", sensor_id);
// resolve MQTT broker
char broker_host[MQTT_HOST_MAX_LEN] = {0};
uint16_t broker_port = DEFAULT_MQTT_PORT;
char broker_uri[160];
bool resolved = false;
while (!resolved) {
// favour config store override over mdns
resolved = (config_store_get_mqtt_override(broker_host, &broker_port) == ESP_OK);
if (resolved) {
ESP_LOGI(TAG, "Using NVS MQTT override: %s:%u", broker_host, broker_port);
} else {
resolved = resolve_mqtt_broker_mdns(broker_host, sizeof(broker_host), &broker_port);
}
if (!resolved) {
ESP_LOGW(TAG, "No broker found, retrying in %ds...", MDNS_RETRY_INTERVAL_MS / 1000);
led_indicator_set(LED_ERROR);
vTaskDelay(pdMS_TO_TICKS(MDNS_RETRY_INTERVAL_MS));
led_indicator_set(LED_CONNECTING);
}
}
snprintf(broker_uri, sizeof(broker_uri), "mqtt://%s:%u", broker_host, broker_port);
// init MQTT
ESP_ERROR_CHECK(mqtt_publisher_init(sensor_id, broker_uri, s_evt));
xEventGroupWaitBits(s_evt, MQTT_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
// init BLE scanner
ble_scanner_init(on_ble_scan_result);
ble_scanner_start();
led_indicator_set(LED_SCANNING);
ESP_LOGI(TAG, "Anchor running — scanning BLE and publishing RSSI");
// main event loop
const EventBits_t cmd_bits =
MQTT_CALIBRATE_START | MQTT_CALIBRATE_STOP |
MQTT_SELECTED_BIT | MQTT_DESELECTED_BIT;
bool calibrating = false;
bool selected = false;
for (;;) {
EventBits_t bits = xEventGroupWaitBits(
s_evt, cmd_bits, pdTRUE, pdFALSE, pdMS_TO_TICKS(100));
if (bits & MQTT_CALIBRATE_START) {
ESP_LOGI(TAG, "Calibration started");
calibrating = true;
led_indicator_set(LED_CALIBRATING);
}
if (bits & MQTT_CALIBRATE_STOP) {
ESP_LOGI(TAG, "Calibration stopped");
calibrating = false;
led_indicator_set(selected ? LED_SELECTED : LED_SCANNING);
}
if (bits & MQTT_SELECTED_BIT) {
ESP_LOGI(TAG, "Anchor selected");
selected = true;
if (!calibrating) led_indicator_set(LED_SELECTED);
}
if (bits & MQTT_DESELECTED_BIT) {
ESP_LOGI(TAG, "Anchor deselected");
selected = false;
if (!calibrating) led_indicator_set(LED_SCANNING);
}
}
}
+5
View File
@@ -0,0 +1,5 @@
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x4000
ota_data, data, ota, 0xd000, 0x2000
ota_0, app, ota_0, 0x10000, 0x180000
ota_1, app, ota_1, 0x190000, 0x180000
1 # Name Type SubType Offset Size
2 nvs data nvs 0x9000 0x4000
3 ota_data data ota 0xd000 0x2000
4 ota_0 app ota_0 0x10000 0x180000
5 ota_1 app ota_1 0x190000 0x180000
+3839
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
CONFIG_BT_ENABLED=y
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# WiFi
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
# MQTT
CONFIG_MQTT_BUFFER_SIZE=512
# Logging
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
# Partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
# Enable OTA
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y