19 KiB
BME680 Homelab Telemetry Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add MQTT publishing to the ESP32 BME680 project and stand up a homelab ingestion pipeline that stores telemetry in InfluxDB through Telegraf.
Architecture: Keep the ESP32 focused on sensor reads, Wi-Fi, and MQTT publishing only. On the homelab side, validate Mosquitto -> Telegraf -> InfluxDB first using a synthetic MQTT message, then wire the real ESP32 publisher into the same topic and payload contract.
Tech Stack: ESP-IDF, esp-idf-lib/bme680, espressif/mqtt, Wi-Fi station mode, Mosquitto, Telegraf, InfluxDB 2, Docker Compose
Repo note: /home/tonio/Workspace/perso/esp is not a git repository right now, so this plan intentionally omits commit steps.
File Map
- Modify:
bme680-demo/main/CMakeLists.txt - Modify:
bme680-demo/main/idf_component.yml - Modify:
bme680-demo/main/main.c - Create:
bme680-demo/main/Kconfig.projbuild - Create:
bme680-demo/main/wifi_mqtt.h - Create:
bme680-demo/main/wifi_mqtt.c - Create:
homelab/telemetry/compose.yaml - Create:
homelab/telemetry/.env.example - Create:
homelab/telemetry/telegraf/telegraf.conf
Task 1: Create The Homelab Telemetry Stack
Files:
-
Create:
homelab/telemetry/compose.yaml -
Create:
homelab/telemetry/.env.example -
Create:
homelab/telemetry/telegraf/telegraf.conf -
Step 1: Create the environment template for InfluxDB and Telegraf
Write homelab/telemetry/.env.example:
INFLUXDB_ADMIN_USERNAME=admin
INFLUXDB_ADMIN_PASSWORD=change-me-please
INFLUXDB_ADMIN_TOKEN=replace-with-long-random-token
INFLUXDB_ORG=homelab
INFLUXDB_BUCKET=bme680
MQTT_BROKER_URL=tcp://192.168.1.10:1883
MQTT_USERNAME=
MQTT_PASSWORD=
- Step 2: Create the Docker Compose file for InfluxDB and Telegraf
Write homelab/telemetry/compose.yaml:
services:
influxdb:
image: influxdb:2
container_name: bme680-influxdb
restart: unless-stopped
ports:
- "8086:8086"
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUXDB_ADMIN_USERNAME}
DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUXDB_ADMIN_PASSWORD}
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUXDB_ADMIN_TOKEN}
DOCKER_INFLUXDB_INIT_ORG: ${INFLUXDB_ORG}
DOCKER_INFLUXDB_INIT_BUCKET: ${INFLUXDB_BUCKET}
volumes:
- ./influxdb/data:/var/lib/influxdb2
- ./influxdb/config:/etc/influxdb2
telegraf:
image: telegraf:1.31
container_name: bme680-telegraf
restart: unless-stopped
depends_on:
- influxdb
environment:
MQTT_BROKER_URL: ${MQTT_BROKER_URL}
MQTT_USERNAME: ${MQTT_USERNAME}
MQTT_PASSWORD: ${MQTT_PASSWORD}
INFLUXDB_URL: http://influxdb:8086
INFLUXDB_ORG: ${INFLUXDB_ORG}
INFLUXDB_BUCKET: ${INFLUXDB_BUCKET}
INFLUXDB_TOKEN: ${INFLUXDB_ADMIN_TOKEN}
volumes:
- ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro
- Step 3: Create the Telegraf MQTT-to-Influx configuration
Write homelab/telemetry/telegraf/telegraf.conf:
[agent]
interval = "10s"
flush_interval = "10s"
omit_hostname = true
[[inputs.mqtt_consumer]]
servers = ["${MQTT_BROKER_URL}"]
topics = ["sensors/+/+/telemetry"]
qos = 1
username = "${MQTT_USERNAME}"
password = "${MQTT_PASSWORD}"
data_format = "json"
tag_keys = ["device_id", "room"]
topic_tag = "topic"
name_override = "environment"
[[outputs.influxdb_v2]]
urls = ["${INFLUXDB_URL}"]
token = "${INFLUXDB_TOKEN}"
organization = "${INFLUXDB_ORG}"
bucket = "${INFLUXDB_BUCKET}"
- Step 4: Validate the Compose configuration before starting containers
Run:
cp "/home/tonio/Workspace/perso/esp/homelab/telemetry/.env.example" "/home/tonio/Workspace/perso/esp/homelab/telemetry/.env" && docker compose --env-file "/home/tonio/Workspace/perso/esp/homelab/telemetry/.env" -f "/home/tonio/Workspace/perso/esp/homelab/telemetry/compose.yaml" config
Expected:
-
rendered Compose output prints successfully
-
no missing-variable or invalid-YAML errors appear
-
Step 5: Start the homelab services
Run:
docker compose --env-file "/home/tonio/Workspace/perso/esp/homelab/telemetry/.env" -f "/home/tonio/Workspace/perso/esp/homelab/telemetry/compose.yaml" up -d
Expected:
-
bme680-influxdbis running -
bme680-telegrafis running -
Step 6: Verify Telegraf can ingest a synthetic MQTT message before touching device code
Run:
mosquitto_pub -h 192.168.1.10 -t "sensors/office/bme680-1/telemetry" -m '{"device_id":"bme680-1","room":"office","temperature":24.8,"humidity":46.2,"pressure":1007.3,"gas_resistance":125430,"uptime_s":3812}'
Then query InfluxDB:
set -a && source "/home/tonio/Workspace/perso/esp/homelab/telemetry/.env" && set +a && docker exec bme680-influxdb influx query --org "$INFLUXDB_ORG" --token "$INFLUXDB_ADMIN_TOKEN" 'from(bucket: "'"$INFLUXDB_BUCKET"'" ) |> range(start: -10m) |> filter(fn: (r) => r._measurement == "environment") |> limit(n: 10)'
Expected:
- query output shows
environmentpoints - tags include
device_id=bme680-1androom=office - fields include temperature, humidity, pressure, gas_resistance, and uptime_s
Task 2: Add ESP32 Telemetry Configuration And MQTT Dependency
Files:
-
Modify:
bme680-demo/main/idf_component.yml -
Create:
bme680-demo/main/Kconfig.projbuild -
Modify:
bme680-demo/main/CMakeLists.txt -
Step 1: Add the MQTT client dependency to the main component
Write bme680-demo/main/idf_component.yml:
dependencies:
esp-idf-lib/bme680: "^1.0.7"
espressif/mqtt: "*"
- Step 2: Add menuconfig entries for Wi-Fi, MQTT, and topic metadata
Write bme680-demo/main/Kconfig.projbuild:
menu "BME680 Telemetry Configuration"
config BME680_WIFI_SSID
string "Wi-Fi SSID"
default ""
config BME680_WIFI_PASSWORD
string "Wi-Fi password"
default ""
config BME680_MQTT_URI
string "MQTT broker URI"
default "mqtt://192.168.1.10:1883"
config BME680_MQTT_USERNAME
string "MQTT username"
default ""
config BME680_MQTT_PASSWORD
string "MQTT password"
default ""
config BME680_DEVICE_ID
string "Device ID"
default "bme680-1"
config BME680_ROOM
string "Room"
default "office"
config BME680_TOPIC_PREFIX
string "MQTT topic prefix"
default "sensors"
endmenu
- Step 3: Register the future Wi-Fi/MQTT source file in the component build
Write bme680-demo/main/CMakeLists.txt:
idf_component_register(SRCS "main.c" "wifi_mqtt.c"
INCLUDE_DIRS ".")
- Step 4: Reconfigure the project so the new dependency and Kconfig entries are visible
Run:
source "/home/tonio/esp/esp-idf/export.sh" && idf.py -C "/home/tonio/Workspace/perso/esp/bme680-demo" reconfigure
Expected:
espressif/mqttis resolved by the component manager if not already cachedidf.py menuconfigwould now showBME680 Telemetry Configuration
Task 3: Add A Focused Wi-Fi And MQTT Transport Module
Files:
-
Create:
bme680-demo/main/wifi_mqtt.h -
Create:
bme680-demo/main/wifi_mqtt.c -
Step 1: Create the public transport interface header
Write bme680-demo/main/wifi_mqtt.h:
#pragma once
#include <stdbool.h>
#include "esp_err.h"
esp_err_t wifi_mqtt_start(void);
bool wifi_mqtt_is_ready(void);
esp_err_t wifi_mqtt_publish(const char *topic, const char *payload);
- Step 2: Implement Wi-Fi station setup, MQTT lifecycle, and publish helper
Write bme680-demo/main/wifi_mqtt.c:
#include <string.h>
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "mqtt_client.h"
#include "nvs_flash.h"
#include "sdkconfig.h"
#include "wifi_mqtt.h"
#define WIFI_CONNECTED_BIT BIT0
#define MQTT_CONNECTED_BIT BIT1
static const char *TAG = "wifi-mqtt";
static EventGroupHandle_t s_event_group;
static esp_mqtt_client_handle_t s_mqtt_client;
static bool s_mqtt_started;
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
esp_mqtt_event_handle_t event = event_data;
if (event_id == MQTT_EVENT_CONNECTED) {
xEventGroupSetBits(s_event_group, MQTT_CONNECTED_BIT);
ESP_LOGI(TAG, "MQTT connected");
} else if (event_id == MQTT_EVENT_DISCONNECTED) {
xEventGroupClearBits(s_event_group, MQTT_CONNECTED_BIT);
ESP_LOGW(TAG, "MQTT disconnected");
}
}
static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
xEventGroupClearBits(s_event_group, WIFI_CONNECTED_BIT | MQTT_CONNECTED_BIT);
ESP_LOGW(TAG, "Wi-Fi disconnected, retrying");
esp_wifi_connect();
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
xEventGroupSetBits(s_event_group, WIFI_CONNECTED_BIT);
ESP_LOGI(TAG, "Wi-Fi connected");
if (!s_mqtt_started) {
esp_mqtt_client_start(s_mqtt_client);
s_mqtt_started = true;
}
}
}
esp_err_t wifi_mqtt_start(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
s_event_group = xEventGroupCreate();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL));
wifi_config_t wifi_config = {
.sta = {
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
},
};
strncpy((char *)wifi_config.sta.ssid, CONFIG_BME680_WIFI_SSID, sizeof(wifi_config.sta.ssid));
strncpy((char *)wifi_config.sta.password, CONFIG_BME680_WIFI_PASSWORD, sizeof(wifi_config.sta.password));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = CONFIG_BME680_MQTT_URI,
.credentials.username = CONFIG_BME680_MQTT_USERNAME,
.credentials.authentication.password = CONFIG_BME680_MQTT_PASSWORD,
};
s_mqtt_client = esp_mqtt_client_init(&mqtt_cfg);
esp_mqtt_client_register_event(s_mqtt_client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
return ESP_OK;
}
bool wifi_mqtt_is_ready(void)
{
EventBits_t bits = xEventGroupGetBits(s_event_group);
return (bits & WIFI_CONNECTED_BIT) && (bits & MQTT_CONNECTED_BIT);
}
esp_err_t wifi_mqtt_publish(const char *topic, const char *payload)
{
if (!wifi_mqtt_is_ready()) {
return ESP_ERR_INVALID_STATE;
}
int msg_id = esp_mqtt_client_publish(s_mqtt_client, topic, payload, 0, 1, 0);
return msg_id >= 0 ? ESP_OK : ESP_FAIL;
}
- Step 3: Build just enough to verify the new transport module compiles
Run:
source "/home/tonio/esp/esp-idf/export.sh" && idf.py -C "/home/tonio/Workspace/perso/esp/bme680-demo" build
Expected:
- build passes with the new dependency and new source file
- no undefined references to MQTT or Wi-Fi APIs remain
Task 4: Publish Real Sensor Readings From The Main Loop
Files:
-
Modify:
bme680-demo/main/main.c -
Step 1: Update the main app to start transport, build the topic, and publish JSON telemetry
Write bme680-demo/main/main.c:
#include <stdio.h>
#include <string.h>
#include "bme680.h"
#include "cJSON.h"
#include "driver/gpio.h"
#include "driver/i2c_master.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "i2cdev.h"
#include "sdkconfig.h"
#include "wifi_mqtt.h"
#define BME680_I2C_PORT I2C_NUM_0
#define BME680_SDA_GPIO GPIO_NUM_21
#define BME680_SCL_GPIO GPIO_NUM_22
#define BME680_ADDR BME680_I2C_ADDR_0
#define BME680_I2C_CLOCK_HZ 100000
#define BME680_INIT_RETRY_MS 2000
#define BME680_READ_INTERVAL_MS 3000
static const char *TAG = "bme680-demo";
static esp_err_t init_i2c_subsystem(void)
{
esp_err_t err;
do {
err = i2cdev_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "I2C subsystem init failed: %s. Retrying in %d ms", esp_err_to_name(err), BME680_INIT_RETRY_MS);
vTaskDelay(pdMS_TO_TICKS(BME680_INIT_RETRY_MS));
}
} while (err != ESP_OK);
return ESP_OK;
}
static esp_err_t init_sensor(bme680_t *sensor)
{
memset(sensor, 0, sizeof(*sensor));
esp_err_t err = bme680_init_desc(sensor, BME680_ADDR, BME680_I2C_PORT, BME680_SDA_GPIO, BME680_SCL_GPIO);
if (err != ESP_OK) {
return err;
}
sensor->i2c_dev.cfg.master.clk_speed = BME680_I2C_CLOCK_HZ;
err = bme680_init_sensor(sensor);
if (err != ESP_OK) {
bme680_free_desc(sensor);
return err;
}
return bme680_set_ambient_temperature(sensor, 25);
}
static void wait_for_sensor(bme680_t *sensor)
{
esp_err_t err;
do {
err = init_sensor(sensor);
if (err != ESP_OK) {
ESP_LOGE(TAG, "BME680 init failed on SDA=%d SCL=%d addr=0x%02x: %s. Retrying in %d ms", BME680_SDA_GPIO, BME680_SCL_GPIO, BME680_ADDR, esp_err_to_name(err), BME680_INIT_RETRY_MS);
vTaskDelay(pdMS_TO_TICKS(BME680_INIT_RETRY_MS));
}
} while (err != ESP_OK);
}
static esp_err_t publish_reading(const bme680_values_float_t *values, uint32_t uptime_s)
{
char topic[128];
snprintf(topic, sizeof(topic), "%s/%s/%s/telemetry", CONFIG_BME680_TOPIC_PREFIX, CONFIG_BME680_ROOM, CONFIG_BME680_DEVICE_ID);
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "device_id", CONFIG_BME680_DEVICE_ID);
cJSON_AddStringToObject(root, "room", CONFIG_BME680_ROOM);
cJSON_AddNumberToObject(root, "temperature", values->temperature);
cJSON_AddNumberToObject(root, "humidity", values->humidity);
cJSON_AddNumberToObject(root, "pressure", values->pressure);
cJSON_AddNumberToObject(root, "gas_resistance", values->gas_resistance);
cJSON_AddNumberToObject(root, "uptime_s", uptime_s);
char *payload = cJSON_PrintUnformatted(root);
esp_err_t err = wifi_mqtt_publish(topic, payload);
cJSON_free(payload);
cJSON_Delete(root);
return err;
}
void app_main(void)
{
bme680_t sensor;
uint32_t uptime_s = 0;
ESP_ERROR_CHECK(wifi_mqtt_start());
init_i2c_subsystem();
wait_for_sensor(&sensor);
while (1) {
bme680_values_float_t values;
esp_err_t err = bme680_measure_float(&sensor, &values);
if (err == ESP_OK) {
ESP_LOGI(TAG, "Temp: %.2f C Humidity: %.2f %% Pressure: %.2f hPa Gas: %.0f ohm", values.temperature, values.humidity, values.pressure, values.gas_resistance);
err = publish_reading(&values, uptime_s);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Telemetry publish skipped or failed: %s", esp_err_to_name(err));
}
} else {
ESP_LOGE(TAG, "BME680 read failed: %s", esp_err_to_name(err));
}
uptime_s += BME680_READ_INTERVAL_MS / 1000;
vTaskDelay(pdMS_TO_TICKS(BME680_READ_INTERVAL_MS));
}
}
- Step 2: Open menuconfig and set the real Wi-Fi and MQTT values
Run:
source "/home/tonio/esp/esp-idf/export.sh" && idf.py -C "/home/tonio/Workspace/perso/esp/bme680-demo" menuconfig
Set:
-
BME680 Telemetry Configuration -> Wi-Fi SSID -
BME680 Telemetry Configuration -> Wi-Fi password -
BME680 Telemetry Configuration -> MQTT broker URI -
BME680 Telemetry Configuration -> MQTT usernameif needed -
BME680 Telemetry Configuration -> MQTT passwordif needed -
BME680 Telemetry Configuration -> Room -
BME680 Telemetry Configuration -> Device ID -
Step 3: Build the integrated firmware
Run:
source "/home/tonio/esp/esp-idf/export.sh" && idf.py -C "/home/tonio/Workspace/perso/esp/bme680-demo" build
Expected:
- build passes with BME680, Wi-Fi, MQTT, and cJSON linked correctly
Task 5: Verify End-To-End Telemetry Flow
Files:
-
No file changes
-
Step 1: Subscribe to the live telemetry topic from the homelab side
Run:
mosquitto_sub -h 192.168.1.10 -t 'sensors/+/+/telemetry' -v
Expected:
-
terminal waits for live telemetry messages
-
Step 2: Flash the firmware and observe the first MQTT message
Run:
source "/home/tonio/esp/esp-idf/export.sh" && idf.py -C "/home/tonio/Workspace/perso/esp/bme680-demo" -p /dev/ttyUSB0 flash monitor
Expected on the ESP32 side:
- Wi-Fi connect log
- MQTT connected log
- repeated BME680 readings
- no crash if the broker is temporarily unavailable
Expected on the mosquitto_sub side:
sensors/office/bme680-1/telemetry {"device_id":"bme680-1","room":"office","temperature":24.8,"humidity":46.2,"pressure":1007.3,"gas_resistance":125430,"uptime_s":3812}
- Step 3: Query InfluxDB for the live device data
Run:
set -a && source "/home/tonio/Workspace/perso/esp/homelab/telemetry/.env" && set +a && docker exec bme680-influxdb influx query --org "$INFLUXDB_ORG" --token "$INFLUXDB_ADMIN_TOKEN" 'from(bucket: "'"$INFLUXDB_BUCKET"'" ) |> range(start: -10m) |> filter(fn: (r) => r._measurement == "environment") |> filter(fn: (r) => r.device_id == "bme680-1") |> limit(n: 20)'
Expected:
-
recent rows exist for
_measurement=environment -
tags include
roomanddevice_id -
field rows exist for
temperature,humidity,pressure,gas_resistance, anduptime_s -
Step 4: If no rows arrive, isolate the failing layer before editing code
Run these checks in order:
docker compose --env-file "/home/tonio/Workspace/perso/esp/homelab/telemetry/.env" -f "/home/tonio/Workspace/perso/esp/homelab/telemetry/compose.yaml" logs telegraf --tail 100
docker compose --env-file "/home/tonio/Workspace/perso/esp/homelab/telemetry/.env" -f "/home/tonio/Workspace/perso/esp/homelab/telemetry/compose.yaml" logs influxdb --tail 100
Interpretation:
- message appears in
mosquitto_subbut not InfluxDB: Telegraf config or credentials issue - message never appears in
mosquitto_sub: ESP32 publish or broker reachability issue - Telegraf shows auth failures: fix MQTT or Influx credentials, not device code
Self-Review
- Spec coverage: this plan adds MQTT publishing to the ESP32, validates
Mosquitto -> Telegraf -> InfluxDB, uses the approved topic shape and payload fields, and keeps Postgres out of the telemetry path. - Placeholder scan: all file paths, commands, config content, and verification steps are explicit.
- Type consistency:
wifi_mqtt_start(),wifi_mqtt_is_ready(), andwifi_mqtt_publish()are defined once and referenced consistently; the payload keys match the approved spec exactly.