// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 A.M. Rowsell /****************************************************************************** * _ _ * ___ _ __ ___ __ _| |__ | |_ * / _ \ '_ \/ __|/ _` | '_ \| __| * | __/ | | \__ \ (_| | | | | |_ * \___|_| |_|___/\__,_|_| |_|\__| * * Program Name: ensaht * Author: A.M Rowsell * License: MIT License (Free use, modification, and distribution permitted * as long as the original copyright notice is included. Provided * "AS IS" without warranty). * * Description: Reads environmental data (temperature, humidity, AQI, TVOC, * eCO2, EtOH) via I2C from AHT and ENS sensors and publishes * the readings to Adafruit IO over MQTT using the Pico W. ******************************************************************************/ #include "./aht.h" #include "./bmp.h" #include "./ens.h" #include "./mqtt.h" #include "hardware/gpio.h" #include "hardware/i2c.h" #include "pico/cyw43_arch.h" #include "pico/stdlib.h" // IWYU pragma: keep #include #include #include #include #include #include #define AVERAGES 8 #define I2C1_SCL_PIN 19 #define I2C1_SDA_PIN 18 // MQTT and WiFi defines #define ADAFRUIT_IO_HOST "io.adafruit.com" #define ADAFRUIT_IO_USERNAME "ADAFRUIT_IO_USERNAME" #define ADAFRUIT_IO_PASSWORD "ADAFRUIT_IO_KEY" #define WIFI_SSID "Meosjin" #define WIFI_PASSWORD "WIFI_PASSWORD" #define MQTT_KEEP_ALIVE_SECONDS 60 // MQTT Topics static const char temp_topic[] = "ADAFRUIT_IO_USERNAME/feeds/ensaht.temperature"; static const char humi_topic[] = "ADAFRUIT_IO_USERNAME/feeds/ensaht.humidity"; static const char aqi_topic[] = "ADAFRUIT_IO_USERNAME/feeds/ensaht.aqi"; static const char tvoc_topic[] = "ADAFRUIT_IO_USERNAME/feeds/ensaht.tvoc"; static const char etoh_topic[] = "ADAFRUIT_IO_USERNAME/feeds/ensaht.etoh"; static const char eco2_topic[] = "ADAFRUIT_IO_USERNAME/feeds/ensaht.eco2"; static void mqtt_connection_cb(mqtt_client_t *client, void *arg, mqtt_connection_status_t status) { LWIP_UNUSED_ARG(client); LWIP_UNUSED_ARG(arg); printf("MQTT status: %d\n", status); } static float average_float(const float *samples, uint8_t count) { float sum = 0.0; for (uint8_t i = 0; i < count; i++) { sum += samples[i]; } return sum / count; } static double average_double(const double *samples, uint8_t count) { double sum = 0.0; for (uint8_t i = 0; i < count; i++) { sum += samples[i]; } return sum / count; } static uint16_t average_uint16(const uint16_t *samples, uint8_t count) { uint32_t sum = 0; for (uint8_t i = 0; i < count; i++) { sum += samples[i]; } return (uint16_t)((sum + (uint32_t)count / 2) / (uint32_t)count); } int main() { uint16_t tvoc, eco2, etoh, avg_tvoc, avg_eco2, avg_etoh; uint8_t aqi, validData; double t, h, avg_t, avg_h, avg_bmp_t, avg_bmp_p; uint16_t tvoc_avg[AVERAGES], eco2_avg[AVERAGES], etoh_avg[AVERAGES]; double t_avg[AVERAGES], h_avg[AVERAGES]; float bmpt_avg[AVERAGES]; float bmpp_avg[AVERAGES]; char payload[32]; float bmpT; float bmpP; // MQTT setup ensaht_mqtt_t mqtt; struct mqtt_connect_client_info_t mqtt_client_info = { .client_id = ADAFRUIT_IO_USERNAME, .client_user = ADAFRUIT_IO_USERNAME, .client_pass = ADAFRUIT_IO_PASSWORD, .keep_alive = MQTT_KEEP_ALIVE_SECONDS, }; // setup stdio stdio_init_all(); // wifi chip initialization if (cyw43_arch_init_with_country(CYW43_COUNTRY_CANADA)) { printf("failed to initialise\n"); return 1; } cyw43_arch_enable_sta_mode(); int wifierr = 1; wifierr = cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 30000); if (wifierr) { printf("failed to connect to Wi-Fi, reason %d\n", wifierr); switch (wifierr) { case PICO_ERROR_BADAUTH: printf("Password incorrect.\n"); break; case PICO_ERROR_TIMEOUT: printf("Timeout ran out before connection.\n"); break; case PICO_ERROR_CONNECT_FAILED: printf("A damn generic error code.\n"); break; default: printf("Not a known error code.\n"); break; } } ensaht_mqtt_init(&mqtt); err_t mqtt_err = ensaht_mqtt_connect(&mqtt, ADAFRUIT_IO_HOST, MQTT_PORT, &mqtt_client_info, mqtt_connection_cb, NULL); if (mqtt_err != ERR_OK) { printf("MQTT connect start failed: %d\n", mqtt_err); } i2c_init(i2c_default, 100 * 1000); // start i2c at 100khz gpio_set_function(PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C); gpio_set_function(PICO_DEFAULT_I2C_SDA_PIN, GPIO_FUNC_I2C); gpio_pull_up(PICO_DEFAULT_I2C_SCL_PIN); gpio_pull_up(PICO_DEFAULT_I2C_SDA_PIN); bi_decl(bi_2pins_with_func(PICO_DEFAULT_I2C_SDA_PIN, PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C)); i2c_init(i2c1, 100 * 1000); // start i2c1 at 100kHz -- this is the stemma chain gpio_set_function(I2C1_SDA_PIN, GPIO_FUNC_I2C); gpio_set_function(I2C1_SCL_PIN, GPIO_FUNC_I2C); gpio_pull_up(I2C1_SDA_PIN); gpio_pull_up(I2C1_SCL_PIN); bi_decl(bi_2pins_with_func(I2C1_SDA_PIN, I2C1_SCL_PIN, GPIO_FUNC_I2C)); initBMP(i2c1); printf("\x1b[2J"); // clear screen uint8_t i = 0; uint8_t sample_count = 0; activateENS(); while (1) { // cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); getAHTData(&t, &h); getBMPData(i2c1, &bmpT, &bmpP); t_avg[i] = t; h_avg[i] = h; bmpt_avg[i] = bmpT; bmpp_avg[i] = bmpP; if (sample_count < AVERAGES) { sample_count++; } avg_t = average_double(t_avg, sample_count); avg_h = average_double(h_avg, sample_count); avg_bmp_t = average_float(bmpt_avg, sample_count); avg_bmp_p = average_float(bmpp_avg, sample_count); printf("\x1b[H\x1b[JTemperature %0.2f C Humidity %0.2f%%\n", avg_t, avg_h); printf("BMP Temperature %0.2f C Pressure %0.2f kPa\n", avg_bmp_t, avg_bmp_p / 100.0); validData = getENSData(&avg_bmp_t, &avg_h, &aqi, &tvoc, &eco2, &etoh); tvoc_avg[i] = tvoc; eco2_avg[i] = eco2; etoh_avg[i] = etoh; avg_tvoc = average_uint16(tvoc_avg, sample_count); avg_eco2 = average_uint16(eco2_avg, sample_count); avg_etoh = average_uint16(etoh_avg, sample_count); if (!validData) { printf("!! "); } printf("AQI: %d TVOC: %d eCO2: %d EtOH: %d\n", aqi, avg_tvoc, avg_eco2, avg_etoh); // cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0); i++; if (i == AVERAGES) i = 0; snprintf(payload, sizeof(payload), "%.2f", avg_t); ensaht_mqtt_publish_client(&mqtt, temp_topic, payload, strlen(payload), NULL, NULL); snprintf(payload, sizeof(payload), "%.2f", avg_h); ensaht_mqtt_publish_client(&mqtt, humi_topic, payload, strlen(payload), NULL, NULL); snprintf(payload, sizeof(payload), "%d", avg_tvoc); ensaht_mqtt_publish_client(&mqtt, tvoc_topic, payload, strlen(payload), NULL, NULL); snprintf(payload, sizeof(payload), "%d", avg_eco2); ensaht_mqtt_publish_client(&mqtt, eco2_topic, payload, strlen(payload), NULL, NULL); snprintf(payload, sizeof(payload), "%d", avg_etoh); ensaht_mqtt_publish_client(&mqtt, etoh_topic, payload, strlen(payload), NULL, NULL); snprintf(payload, sizeof(payload), "%d", aqi); ensaht_mqtt_publish_client(&mqtt, aqi_topic, payload, strlen(payload), NULL, NULL); sleep_ms(30000); } return 0; }