commit 7a18e2d88abab6b65015bdc869d5e9997e23b255 Author: Jarrad Brown Date: Tue Feb 24 18:17:18 2026 -0600 Added folder structure and current project files thus far diff --git a/SupportProjects/TextToSpeech/main.py b/SupportProjects/TextToSpeech/main.py new file mode 100644 index 0000000..382d2de --- /dev/null +++ b/SupportProjects/TextToSpeech/main.py @@ -0,0 +1,29 @@ + + + +from gtts import gTTS +import os + +def text_to_speech(text, lang="en", filename="output.mp3"): + """ + Convert text to speech and save as MP3. + + Args: + text (str): Text to convert + lang (str): Language code ('en' for English, 'es' for Spanish) + filename (str): Output file name + """ + try: + tts = gTTS(text=text, lang=lang) + tts.save(filename) + print(f"Saved: {filename}") + except Exception as e: + print(f"Error: {e}") + +# --- Examples --- + +# English +text_to_speech("Hello, this is a test.", lang="en", filename="english.mp3") + +# Spanish +text_to_speech("Hola, esto es una prueba.", lang="es", filename="spanish.mp3") \ No newline at end of file diff --git a/SupportProjects/TextToSpeech/readme.md b/SupportProjects/TextToSpeech/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/SupportProjects/TextToSpeech/requirements.txt b/SupportProjects/TextToSpeech/requirements.txt new file mode 100644 index 0000000..fc8dfeb --- /dev/null +++ b/SupportProjects/TextToSpeech/requirements.txt @@ -0,0 +1 @@ +gTTS==2.5.1 \ No newline at end of file diff --git a/uC/EnviromentSensor_uC/EnvSensorModbus/EnvSensorModbus.ino b/uC/EnviromentSensor_uC/EnvSensorModbus/EnvSensorModbus.ino new file mode 100644 index 0000000..1b26d9c --- /dev/null +++ b/uC/EnviromentSensor_uC/EnvSensorModbus/EnvSensorModbus.ino @@ -0,0 +1,392 @@ +/* + ESP32-C6 Umbrella Env Sensor (FLL) + - BME680 (LIVE) OR BLE-controlled SIM mode + - Modbus RTU (Holding Registers) clean map HR0..HR7 + - BLE GATT control (NimBLE) with compact binary packets + + Modbus HR map (Holding Registers): + HR0 counter + HR1 status (bitfield) + HR2 mode (0=LIVE, 1=SIM) + HR3 error_count + HR4 temp_x100 (int16 stored in 16-bit register) + HR5 rh_x100 (uint16) + HR6 press_x10 (uint16) + HR7 iaq (uint16, 0..500, higher=worse, AQI-style) + + BLE control packets (little-endian): + CMD 0x01 (set mode): [0]=0x01, [1]=mode(0/1) + CMD 0x02 (set sim): [0]=0x02, + [1..2]=temp_x100 (int16 LE), + [3..4]=rh_x100 (uint16 LE), + [5..6]=press_x10 (uint16 LE), + [7..8]=iaq (uint16 LE) OPTIONAL + If iaq not provided, device computes a simple heuristic IAQ for SIM. + + Libraries: + - Adafruit BME680 library + - modbus-esp8266 (ModbusRTU.h) + - NimBLE-Arduino + + Pins (match your working setup): + I2C SDA=22, SCL=21 + RS-485 UART: RX=4, TX=5 @ 9600 +*/ + +#include +#include +#include + +#include // modbus-esp8266 library +#include // NimBLE-Arduino + +// -------------------- I2C -------------------- +static const int I2C_SDA = 22; +static const int I2C_SCL = 21; +static const uint32_t I2C_FREQ = 100000; +static const uint8_t BME_ADDR_1 = 0x76; +static const uint8_t BME_ADDR_2 = 0x77; + +// -------------------- MODBUS UART ----------- +static const int MB_RX = 4; +static const int MB_TX = 5; +static const uint32_t MB_BAUD = 9600; +static const uint8_t SLAVE_ID = 10; + +// -------------------- Modbus registers ------- +enum { + HR_COUNTER = 0, + HR_STATUS = 1, + HR_MODE = 2, + HR_ERROR_COUNT = 3, + HR_TEMP_X100 = 4, + HR_RH_X100 = 5, + HR_PRESS_X10 = 6, + HR_IAQ = 7, + HR_REG_COUNT = 8 +}; + +// status bits (HR1) +static const uint16_t ST_OK = (1 << 0); +static const uint16_t ST_FALLBACK = (1 << 1); +static const uint16_t ST_NEW_SAMPLE = (1 << 2); +static const uint16_t ST_SIM_ACTIVE = (1 << 3); +static const uint16_t ST_BLE_CONNECTED = (1 << 4); + +// mode (HR2) +static const uint16_t MODE_LIVE = 0; +static const uint16_t MODE_SIM = 1; + +// -------------------- Data model ------------- +struct EnvData { + uint16_t counter; + uint16_t status; + uint16_t mode; + uint16_t error_count; + int16_t temp_x100; + uint16_t rh_x100; + uint16_t press_x10; + uint16_t iaq; +}; + +EnvData env; // published +EnvData sim; // simulated target values + +// -------------------- BME680 ----------------- +Adafruit_BME680 bme; +bool bme_ok = false; + +// -------------------- Modbus ----------------- +ModbusRTU mb; + +// -------------------- Timing ----------------- +static const uint32_t SAMPLE_PERIOD_MS = 1000; +uint32_t last_sample_ms = 0; +bool heartbeat = false; + +// -------------------- IAQ-ish baseline -------- +// NOTE: This is NOT official AQI (PM2.5 etc.). It's an AQI-style 0..500 indicator +// derived from BME680 gas resistance relative to a slowly moving baseline. +float gas_baseline = 0.0f; +bool baseline_ready = false; + +static const float BASELINE_ALPHA = 0.005f; // slower drift baseline (0.5% new per sample) +static const float HUM_TARGET = 40.0f; // %RH target +static const float HUM_PENALTY_GAIN = 0.5f; // penalty per %RH away from target (added directly) + +// -------------------- BLE UUIDs -------------- +// Keep stable once deployed. +static const char* BLE_DEV_NAME = "FLL-Umbrella-Env"; +static const char* UUID_SVC_CTRL = "12345678-1234-5678-1234-56789abcdef0"; +static const char* UUID_CHR_CTRL = "12345678-1234-5678-1234-56789abcdef1"; + +volatile bool ble_connected = false; + +// ---------- Helpers ---------- +static inline uint16_t clamp_u16(int32_t v, uint16_t lo, uint16_t hi) { + if (v < (int32_t)lo) return lo; + if (v > (int32_t)hi) return hi; + return (uint16_t)v; +} + +// AQI-style 0..500 (higher=worse) based on gas relative to baseline + humidity penalty +uint16_t computeIAQFromGas(uint32_t gas_ohms, float humidity) { + if (gas_ohms < 100) gas_ohms = 100; // avoid divide weirdness + + if (!baseline_ready) { + gas_baseline = (float)gas_ohms; + baseline_ready = true; + } else { + gas_baseline = gas_baseline * (1.0f - BASELINE_ALPHA) + (float)gas_ohms * BASELINE_ALPHA; + } + + // worse air tends to LOWER gas resistance => baseline/gas increases + float ratio = (gas_baseline > 1.0f) ? (gas_baseline / (float)gas_ohms) : 1.0f; + + // map ratio to 0..500-ish (demo-friendly) + float score = (ratio - 1.0f) * 120.0f; + + // humidity compensation (far from ~40% adds penalty) + score += fabsf(humidity - HUM_TARGET) * HUM_PENALTY_GAIN; + + if (score < 0) score = 0; + if (score > 500) score = 500; + return (uint16_t)(score + 0.5f); +} + +// If SIM IAQ not provided, compute a heuristic so it still "moves" +uint16_t computeIAQHeuristicSIM(int16_t temp_x100, uint16_t rh_x100, uint16_t press_x10) { + int32_t score = 25; // start good-ish + + float t = temp_x100 / 100.0f; + float rh = rh_x100 / 100.0f; + float p = press_x10 / 10.0f; + + if (t > 30) score += (int32_t)((t - 30) * 12); + if (t < 15) score += (int32_t)((15 - t) * 8); + + score += (int32_t)(fabsf(rh - 45.0f) * 2.0f); + + if (p < 990) score += (int32_t)((990 - p) * 1.5f); + + return clamp_u16(score, 0, 500); +} + +void publishModbus() { + mb.Hreg(HR_COUNTER, env.counter); + mb.Hreg(HR_STATUS, env.status); + mb.Hreg(HR_MODE, env.mode); + mb.Hreg(HR_ERROR_COUNT, env.error_count); + mb.Hreg(HR_TEMP_X100, (uint16_t)env.temp_x100); // int16 stored as uint16 + mb.Hreg(HR_RH_X100, env.rh_x100); + mb.Hreg(HR_PRESS_X10, env.press_x10); + mb.Hreg(HR_IAQ, env.iaq); +} + +void setup_bme() { + Wire.begin(I2C_SDA, I2C_SCL, I2C_FREQ); + + if (bme.begin(BME_ADDR_1)) bme_ok = true; + else if (bme.begin(BME_ADDR_2)) bme_ok = true; + else bme_ok = false; + + if (!bme_ok) return; + + bme.setTemperatureOversampling(BME680_OS_8X); + bme.setHumidityOversampling(BME680_OS_2X); + bme.setPressureOversampling(BME680_OS_4X); + bme.setGasHeater(320, 150); // needed for gas resistance +} + +void setup_modbus() { + // ESP32-C6: Serial0 is USB serial; Serial1 for RS-485 UART + Serial1.begin(MB_BAUD, SERIAL_8N1, MB_RX, MB_TX); + + mb.begin(&Serial1); + mb.slave(SLAVE_ID); + + for (uint16_t r = 0; r < HR_REG_COUNT; r++) { + mb.addHreg(r, 0); + } + publishModbus(); +} + +// ---------- BLE Server (newer NimBLE-Arduino signatures) ---------- +class CtrlCallbacks : public NimBLECharacteristicCallbacks { + void onWrite(NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo) override { + (void)connInfo; + + std::string v = pChr->getValue(); + if (v.size() < 2) return; + + const uint8_t* b = (const uint8_t*)v.data(); + uint8_t cmd = b[0]; + + if (cmd == 0x01) { // set mode + env.mode = b[1] ? MODE_SIM : MODE_LIVE; + + } else if (cmd == 0x02) { // set sim values + if (v.size() < 7) return; + + auto u16le = [&](int idx) -> uint16_t { + return (uint16_t)b[idx] | ((uint16_t)b[idx + 1] << 8); + }; + auto i16le = [&](int idx) -> int16_t { + return (int16_t)u16le(idx); + }; + + sim.temp_x100 = i16le(1); + sim.rh_x100 = u16le(3); + sim.press_x10 = u16le(5); + + if (v.size() >= 9) sim.iaq = u16le(7); + else sim.iaq = computeIAQHeuristicSIM(sim.temp_x100, sim.rh_x100, sim.press_x10); + } + } +}; + +class ServerCallbacks : public NimBLEServerCallbacks { + void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override { + (void)pServer; + (void)connInfo; + ble_connected = true; + } + + void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override { + (void)pServer; + (void)connInfo; + (void)reason; + ble_connected = false; + NimBLEDevice::startAdvertising(); + } +}; + +void setup_ble() { + Serial.println("[BLE] init..."); + + NimBLEDevice::init(BLE_DEV_NAME); + NimBLEDevice::setPower(ESP_PWR_LVL_P9); + + NimBLEServer* server = NimBLEDevice::createServer(); + server->setCallbacks(new ServerCallbacks()); + + NimBLEService* svc = server->createService(UUID_SVC_CTRL); + + NimBLECharacteristic* chr = svc->createCharacteristic( + UUID_CHR_CTRL, + NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR + ); + chr->setCallbacks(new CtrlCallbacks()); + + // (Optional) not required for writes; harmless if you omit it: + // chr->createDescriptor("2902"); + + svc->start(); + + NimBLEAdvertising* adv = NimBLEDevice::getAdvertising(); + adv->addServiceUUID(UUID_SVC_CTRL); + adv->start(); + + Serial.println("[BLE] Advertising started"); +} + +// ---------- Main ---------- +void setup() { + Serial.begin(115200); + delay(200); + + Serial.println("\n=== ESP32-C6 BME680 + BLE SIM -> Modbus RTU (HR0..HR7) ==="); + Serial.printf("I2C SDA/SCL: %d/%d\n", I2C_SDA, I2C_SCL); + Serial.printf("Modbus RX/TX: %d/%d @ %lu\n", MB_RX, MB_TX, (unsigned long)MB_BAUD); + Serial.printf("Slave ID: %u\n", SLAVE_ID); + + // init defaults + env.counter = 0; + env.status = 0; + env.mode = MODE_LIVE; + env.error_count = 0; + env.temp_x100 = 2500; // 25.00 C + env.rh_x100 = 5000; // 50.00 % + env.press_x10 = 10132; // 1013.2 hPa + env.iaq = 25; // "good" + + // default sim values = same as env + sim = env; + + setup_bme(); + Serial.printf("BME680 OK: %s\n", bme_ok ? "YES" : "NO"); + + setup_modbus(); + setup_ble(); + + Serial.println("Modbus ready (FC03 HR0..HR7). BLE control ready."); +} + +void loop() { + mb.task(); + + uint32_t now = millis(); + if (now - last_sample_ms < SAMPLE_PERIOD_MS) return; + last_sample_ms = now; + + heartbeat = !heartbeat; + env.counter++; + + // rebuild status fresh each tick + uint16_t st = 0; + if (heartbeat) st |= ST_NEW_SAMPLE; + if (ble_connected) st |= ST_BLE_CONNECTED; + if (env.mode == MODE_SIM) st |= ST_SIM_ACTIVE; + + bool used_fallback = false; + bool update_ok = false; + + if (env.mode == MODE_SIM) { + // publish simulated values + env.temp_x100 = sim.temp_x100; + env.rh_x100 = sim.rh_x100; + env.press_x10 = sim.press_x10; + env.iaq = sim.iaq; + update_ok = true; + + } else { + // LIVE mode + if (bme_ok && bme.performReading()) { + float t = bme.temperature; // C + float h = bme.humidity; // % + float p_hpa = bme.pressure / 100.0f; // Pa -> hPa + uint32_t gas = (uint32_t)bme.gas_resistance; + + env.temp_x100 = (int16_t)(t * 100.0f); + env.rh_x100 = (uint16_t)(h * 100.0f); + env.press_x10 = (uint16_t)(p_hpa * 10.0f); + env.iaq = computeIAQFromGas(gas, h); + + update_ok = true; + } else { + env.error_count++; + used_fallback = true; + update_ok = false; + // keep last good env values + } + } + + if (update_ok) st |= ST_OK; + if (used_fallback) st |= ST_FALLBACK; + + env.status = st; + publishModbus(); + + // debug + Serial.printf("HR0=%u HR2=%u T=%.2f RH=%.2f P=%.1f IAQ=%u ST=0x%04X Err=%u BLE=%c\n", + env.counter, + env.mode, + env.temp_x100 / 100.0f, + env.rh_x100 / 100.0f, + env.press_x10 / 10.0f, + env.iaq, + env.status, + env.error_count, + ble_connected ? 'Y' : 'N' + ); +} diff --git a/uC/EnviromentSensor_uC/EnvSensorRemoteCtrl/EnvSensorRemoteCtrl.ino b/uC/EnviromentSensor_uC/EnvSensorRemoteCtrl/EnvSensorRemoteCtrl.ino new file mode 100644 index 0000000..95c2b6e --- /dev/null +++ b/uC/EnviromentSensor_uC/EnvSensorRemoteCtrl/EnvSensorRemoteCtrl.ino @@ -0,0 +1,9 @@ +void setup() { + // put your setup code here, to run once: + +} + +void loop() { + // put your main code here, to run repeatedly: + +}