#include #include #include #include #include #include #include #include #include #include #include // Capture and persist reset cause as early as possible // Store MCUSR in a noinit section so C runtime doesn't clear it static uint8_t mcusr_mirror __attribute__((section(".noinit"))); void wdt_init(void) __attribute__((naked)) __attribute__((section(".init3"))); void wdt_init(void) { mcusr_mirror = MCUSR; MCUSR = 0; wdt_disable(); __asm__ __volatile__ ("ret"); } static uint8_t lastResetFlagsGlobal = 0; // Battery Optimization Settings #define ENABLE_CHANGE_DETECTION 1 // Only transmit when values change #define TEMP_CHANGE_THRESHOLD 0.1 // °C - transmit if temp changes by this amount (reduced from 0.2 for better noise filtering) #define HUM_CHANGE_THRESHOLD 2.0 // % - transmit if humidity changes by this amount #define PRES_CHANGE_THRESHOLD 1.0 // hPa - transmit if pressure changes by this amount #define ALIVE_INTERVAL 10 // Force transmission every N cycles even if no change (10*30s = 5min) #define SENSOR_INTERVAL 30 // Read sensors every N seconds (30s = good balance) // CC1101 Register addresses #define CC1101_IOCFG0 0x02 #define CC1101_PKTLEN 0x06 #define CC1101_PKTCTRL1 0x07 #define CC1101_SYNC1 0x04 #define CC1101_SYNC0 0x05 #define CC1101_PKTCTRL0 0x08 #define CC1101_CHANNR 0x0A #define CC1101_FSCTRL1 0x0B #define CC1101_FREQ2 0x0D #define CC1101_FREQ1 0x0E #define CC1101_FREQ0 0x0F #define CC1101_MDMCFG4 0x10 #define CC1101_MDMCFG3 0x11 #define CC1101_MDMCFG2 0x12 #define CC1101_MDMCFG1 0x13 #define CC1101_MDMCFG0 0x14 #define CC1101_DEVIATN 0x15 #define CC1101_MCSM1 0x17 #define CC1101_MCSM0 0x18 #define CC1101_FOCCFG 0x19 #define CC1101_BSCFG 0x1A #define CC1101_AGCCTRL2 0x1B #define CC1101_AGCCTRL1 0x1C #define CC1101_AGCCTRL0 0x1D #define CC1101_FREND1 0x21 #define CC1101_FREND0 0x22 #define CC1101_FSCAL3 0x23 #define CC1101_FSCAL2 0x24 #define CC1101_FSCAL1 0x25 #define CC1101_FSCAL0 0x26 #define CC1101_TEST2 0x2C #define CC1101_TEST1 0x2D #define CC1101_TEST0 0x2E #define CC1101_PATABLE 0x3E #define CC1101_TXFIFO 0x3F // Command strobes #define CC1101_SRES 0x30 #define CC1101_SCAL 0x33 #define CC1101_STX 0x35 #define CC1101_SIDLE 0x36 #define CC1101_SFTX 0x3B // Status registers #define CC1101_PARTNUM 0x30 #define CC1101_VERSION 0x31 #define CC1101_RSSI 0x34 #define CC1101_MARCSTATE 0x35 // Pin definitions #define CC1101_CS 10 #define LED_PIN 4 #define ONE_WIRE_BUS 3 #define PRES_OFFSET 41.0F #define MYNODEID 1 // Change detection variables float lastTxDsTempC = -999.0; float lastTxBmeTempC = -999.0; float lastTxBmeHum = -999.0; float lastTxBmePres = -999.0; uint8_t cyclesSinceLastTx = 0; // Sensor interval tracking uint16_t wakeCount = 0; volatile uint8_t watchdog_wakeups = 0; // Counter for watchdog interrupts // Watchdog ISR - wakes MCU from sleep ISR(WDT_vect) { watchdog_wakeups++; // Note: do NOT disable watchdog here - it will be re-enabled before next sleep } void sleep_1s(void) { // Ensure serial is flushed before sleep Serial.flush(); // Disable UART to save power during sleep (~2-3mA) power_usart0_disable(); // Configure watchdog for 1-second interrupt-based wake cli(); // Disable interrupts during setup wdt_reset(); WDTCSR |= (1 << WDCE) | (1 << WDE); // Enable watchdog configuration change WDTCSR = (1 << WDIE) | (1 << WDP2) | (1 << WDP1); // 1-second, interrupt only sei(); // Re-enable interrupts // Set sleep mode to Power Down (lowest power: ~5-10µA) set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_enable(); // Enter power-down sleep - wakes on watchdog interrupt sleep_cpu(); // Execution resumes here after watchdog interrupt sleep_disable(); wdt_disable(); // Stop watchdog after waking // Re-enable UART after sleep power_usart0_enable(); // Small delay to let UART stabilize delayMicroseconds(100); } // Payload structure struct Payload { uint8_t magic1; uint8_t magic2; uint8_t version; uint8_t nodeId; uint16_t seq; int16_t t_ds10; int16_t t_bme10; uint16_t hum10; uint16_t pres1; uint16_t vcc; // VCC in millivolts uint8_t crc; }; // Ensure payload size matches radio PKTLEN expectations (17 bytes) static_assert(sizeof(Payload) == 17, "Payload size must be 17 bytes"); Payload p; static uint16_t seq = 0; OneWire oneWire(ONE_WIRE_BUS); DallasTemperature ds18(&oneWire); Adafruit_BME280 bme; static bool bme_ok = false; struct BmeSample { float tempC; float hum; float presHpa; }; uint8_t calcCrc(const uint8_t *data, uint8_t len) { uint8_t c = 0; for (uint8_t i = 0; i < len; i++) c ^= data[i]; return c; } // Read VCC voltage in millivolts using internal 1.1V reference uint16_t readVcc() { // Set ADC reference to internal 1.1V and measure VCC ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1); delay(2); // Wait for Vref to settle ADCSRA |= _BV(ADSC); // Start conversion while (bit_is_set(ADCSRA, ADSC)); // Wait for completion uint16_t result = ADC; // Calculate VCC in millivolts: internal_ref * 1024 / ADC reading // Calibrated: 1450512 gives 3300mV on this chip (internal ref ~1.42V) uint16_t vcc = (1450512L / result); // Disable ADC to save power (~200µA) power_adc_disable(); return vcc; } // Check if sensor values have changed enough to warrant transmission // NOTE: This function ONLY checks for change. Alive/heartbeat logic is handled in loop(). bool valuesChanged(float dsTempC, float bmeTempC, float bmeHum, float bmePres) { // Note: Pressure changes do NOT trigger a transmission. Pressure is sent only // when temperature/humidity trigger or during alive/heartbeat transmissions. #if ENABLE_CHANGE_DETECTION if (abs(dsTempC - lastTxDsTempC) >= TEMP_CHANGE_THRESHOLD) return true; if (abs(bmeTempC - lastTxBmeTempC) >= TEMP_CHANGE_THRESHOLD) return true; if (abs(bmeHum - lastTxBmeHum) >= HUM_CHANGE_THRESHOLD) return true; return false; // No significant changes in temp/humidity #else return true; // Always transmit if change detection disabled #endif } // CC1101 SPI functions void cc1101_WriteReg(uint8_t addr, uint8_t value) { digitalWrite(CC1101_CS, LOW); delayMicroseconds(10); SPI.transfer(addr); SPI.transfer(value); delayMicroseconds(10); digitalWrite(CC1101_CS, HIGH); } uint8_t cc1101_ReadReg(uint8_t addr) { digitalWrite(CC1101_CS, LOW); delayMicroseconds(10); SPI.transfer(addr | 0x80); // Read bit uint8_t val = SPI.transfer(0); delayMicroseconds(10); digitalWrite(CC1101_CS, HIGH); return val; } void cc1101_WriteStrobe(uint8_t strobe) { digitalWrite(CC1101_CS, LOW); delayMicroseconds(10); SPI.transfer(strobe); delayMicroseconds(10); digitalWrite(CC1101_CS, HIGH); } uint8_t cc1101_ReadStatus(uint8_t addr) { digitalWrite(CC1101_CS, LOW); delayMicroseconds(10); SPI.transfer(addr | 0xC0); // Burst read bit uint8_t val = SPI.transfer(0); delayMicroseconds(10); digitalWrite(CC1101_CS, HIGH); return val; } void cc1101_SetPower(uint8_t paValue) { digitalWrite(CC1101_CS, LOW); delayMicroseconds(10); SPI.transfer(CC1101_PATABLE | 0x40); // Burst write to PATABLE for (uint8_t i = 0; i < 8; i++) { SPI.transfer(paValue); } delayMicroseconds(10); digitalWrite(CC1101_CS, HIGH); } // Select TX power based on battery voltage to prevent brown-out uint8_t getAdaptiveTxPower(uint16_t vccMv) { if (vccMv >= 3000) return 0xC0; // Full power at 3.0V+ else if (vccMv >= 2800) return 0x60; // Medium power at 2.8-3.0V (lowered from 0x84) else return 0x40; // Low power below 2.8V } void cc1101_WriteTxFifo(uint8_t *data, uint8_t len) { digitalWrite(CC1101_CS, LOW); delayMicroseconds(10); SPI.transfer(CC1101_TXFIFO | 0x40); // Burst write for (uint8_t i = 0; i < len; i++) { SPI.transfer(data[i]); } delayMicroseconds(10); digitalWrite(CC1101_CS, HIGH); } BmeSample readBmeOnce() { if (!bme_ok) { BmeSample s{}; s.tempC = NAN; s.hum = NAN; s.presHpa = NAN; return s; } BmeSample s{}; // Force a single reading bme.setSampling(Adafruit_BME280::MODE_FORCED, Adafruit_BME280::SAMPLING_X2, Adafruit_BME280::SAMPLING_X16, Adafruit_BME280::SAMPLING_X1); delay(10); s.tempC = bme.readTemperature(); s.hum = bme.readHumidity(); s.presHpa = (bme.readPressure() / 100.0F) + PRES_OFFSET; // Return to sleep to save power bme.setSampling(Adafruit_BME280::MODE_SLEEP, Adafruit_BME280::SAMPLING_X2, Adafruit_BME280::SAMPLING_X16, Adafruit_BME280::SAMPLING_X1); return s; } // Attempt to recover a stuck I2C bus by toggling SCL until SDA is released static void i2cBusRecover() { pinMode(A4, INPUT_PULLUP); // SDA pinMode(A5, INPUT_PULLUP); // SCL delay(5); if (digitalRead(A4) == LOW) { pinMode(A5, OUTPUT); for (uint8_t i = 0; i < 9 && digitalRead(A4) == LOW; i++) { digitalWrite(A5, LOW); delayMicroseconds(5); digitalWrite(A5, HIGH); delayMicroseconds(5); } } } float readDs18Median(uint8_t samples = 3) { float buf[5]; // up to 5 samples samples = (samples > 5) ? 5 : samples; for (uint8_t i = 0; i < samples; i++) { wdt_reset(); // Feed watchdog during slow sensor reads ds18.requestTemperatures(); ds18.setWaitForConversion(true); // block until done buf[i] = ds18.getTempCByIndex(0); } // simple insertion sort for small N for (uint8_t i = 1; i < samples; i++) { float v = buf[i]; uint8_t j = i; while (j > 0 && buf[j - 1] > v) { buf[j] = buf[j - 1]; j--; } buf[j] = v; } return buf[samples / 2]; } float smoothEma(float raw, float alpha = 0.2f) { static float ema = NAN; if (isnan(ema)) ema = raw; else ema = alpha * raw + (1.0f - alpha) * ema; return ema; } bool waitForIdle(uint16_t timeoutMs) { unsigned long start = millis(); while (millis() - start < timeoutMs) { if ((cc1101_ReadStatus(CC1101_MARCSTATE) & 0x1F) == 0x01) return true; // 0x01 = IDLE delay(1); } return false; } void cc1101_InitFSK() { Serial.println("Initializing CC1101 in GFSK mode (1.2 kBaud, 5 kHz dev, 58 kHz BW)..."); // Reset CC1101 cc1101_WriteStrobe(CC1101_SRES); delay(10); // Configure for GFSK modulation at 868.3 MHz, 1.2 kBaud, 5 kHz deviation cc1101_WriteReg(CC1101_IOCFG0, 0x06); cc1101_WriteReg(CC1101_PKTCTRL0, 0x00); cc1101_WriteReg(CC1101_PKTCTRL1, 0x00); cc1101_WriteReg(CC1101_PKTLEN, sizeof(Payload)); // Debug check: confirm PKTLEN register matches struct size uint8_t pktlenReg = cc1101_ReadReg(CC1101_PKTLEN); Serial.print("PKTLEN(struct/reg): "); Serial.print(sizeof(Payload)); Serial.print("/"); Serial.println(pktlenReg); cc1101_WriteReg(CC1101_SYNC1, 0xD3); cc1101_WriteReg(CC1101_SYNC0, 0x91); cc1101_WriteReg(CC1101_CHANNR, 0x00); cc1101_WriteReg(CC1101_FSCTRL1, 0x06); // Set frequency to 868.3 MHz cc1101_WriteReg(CC1101_FREQ2, 0x21); cc1101_WriteReg(CC1101_FREQ1, 0x65); cc1101_WriteReg(CC1101_FREQ0, 0x6A); // Modem configuration cc1101_WriteReg(CC1101_MDMCFG4, 0xF8); cc1101_WriteReg(CC1101_MDMCFG3, 0x83); cc1101_WriteReg(CC1101_MDMCFG2, 0x12); cc1101_WriteReg(CC1101_MDMCFG1, 0x22); cc1101_WriteReg(CC1101_MDMCFG0, 0xF8); cc1101_WriteReg(CC1101_DEVIATN, 0x15); cc1101_WriteReg(CC1101_MCSM0, 0x18); cc1101_WriteReg(CC1101_FOCCFG, 0x1D); cc1101_WriteReg(CC1101_BSCFG, 0x1C); cc1101_WriteReg(CC1101_MCSM1, 0x30); // AGC control cc1101_WriteReg(CC1101_AGCCTRL2, 0xC7); cc1101_WriteReg(CC1101_AGCCTRL1, 0x00); cc1101_WriteReg(CC1101_AGCCTRL0, 0xB0); // Front-end configuration cc1101_WriteReg(CC1101_FREND1, 0xB6); cc1101_WriteReg(CC1101_FREND0, 0x17); cc1101_WriteReg(CC1101_FSCAL3, 0xEA); cc1101_WriteReg(CC1101_FSCAL2, 0x2A); cc1101_WriteReg(CC1101_FSCAL1, 0x00); cc1101_WriteReg(CC1101_FSCAL0, 0x1F); cc1101_WriteReg(CC1101_TEST2, 0x88); cc1101_WriteReg(CC1101_TEST1, 0x31); cc1101_WriteReg(CC1101_TEST0, 0x09); // Set PA table for maximum TX power (~10dBm) cc1101_SetPower(0xC0); // Calibrate frequency synthesizer cc1101_WriteStrobe(CC1101_SIDLE); delay(1); cc1101_WriteStrobe(CC1101_SCAL); delay(3); for (uint16_t i = 0; i < 500; i++) { uint8_t st = cc1101_ReadStatus(CC1101_MARCSTATE) & 0x1F; if (st != 0x13 && st != 0x14) break; delay(1); } Serial.println("CC1101 initialization complete"); } void setup() { // Disable watchdog immediately wdt_reset(); wdt_disable(); wdt_reset(); // Power optimization - disable unused peripherals power_twi_disable(); // Will re-enable for I2C power_spi_disable(); // Will re-enable for SPI ACSR |= (1 << ACD); // Disable analog comparator (~25µA saved) // Set up LED pin FIRST pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, HIGH); wdt_reset(); // Initialize serial WITHOUT any delay Serial.begin(9600); // NO DELAY - immediate operation wdt_reset(); Serial.println("\n========== BOOT =========="); wdt_reset(); Serial.flush(); wdt_reset(); // Re-enable I2C and SPI for sensor initialization power_twi_enable(); power_spi_enable(); // Report and persist the last reset cause uint8_t lastResetFlags = mcusr_mirror; lastResetFlagsGlobal = lastResetFlags; EEPROM.update(0, lastResetFlags); Serial.print("Reset flags: 0x"); Serial.print(lastResetFlags, HEX); Serial.print(" ("); if (lastResetFlags & (1<= ALIVE_INTERVAL) || (lastTxDsTempC == -999.0); bool shouldTransmit = changed || alive; wdt_reset(); if (!shouldTransmit) { Serial.print("No change (cycle "); Serial.print(cyclesSinceLastTx); Serial.println("), skipping TX"); wdt_reset(); // Flash LED briefly to show we're alive but not transmitting digitalWrite(LED_PIN, HIGH); delay(50); digitalWrite(LED_PIN, LOW); wdt_reset(); // Sleep for remaining time of this cycle Serial.flush(); sleep_1s(); return; // Skip transmission } // Values changed or alive signal - proceed with transmission if (changed) { Serial.print("Values changed, transmitting seq="); } else { Serial.print("ALIVE signal, transmitting seq="); } wdt_reset(); Serial.println(seq); wdt_reset(); // Update last transmitted values lastTxDsTempC = dsTempC; lastTxBmeTempC = bmeSample.tempC; lastTxBmeHum = bmeSample.hum; lastTxBmePres = bmeSample.presHpa; cyclesSinceLastTx = 0; wdt_reset(); // Set fixed TX power to 0xC0 (~10 dBm - maximum) cc1101_SetPower(0xC0); // Transmit cc1101_WriteStrobe(CC1101_SIDLE); delay(5); wdt_reset(); // Flush TX FIFO cc1101_WriteStrobe(CC1101_SFTX); delay(2); // Write data to TX FIFO cc1101_WriteTxFifo((uint8_t*)&p, sizeof(Payload)); wdt_reset(); // Start transmission cc1101_WriteStrobe(CC1101_STX); // Wait until radio returns to IDLE or timeout bool txDone = waitForIdle(150); wdt_reset(); if (!txDone) { Serial.print("[tx-timeout]"); } else { Serial.print("."); } wdt_reset(); // Go back to idle cc1101_WriteStrobe(CC1101_SIDLE); delay(5); // Long blink to indicate TX occurred digitalWrite(LED_PIN, HIGH); delay(300); digitalWrite(LED_PIN, LOW); delay(100); wdt_reset(); Serial.println(" Done!"); Serial.print("DS: "); Serial.print(dsTempC, 1); Serial.print(" BME: "); Serial.print(bmeSample.tempC, 1); Serial.print(" H: "); Serial.print(bmeSample.hum, 1); Serial.print(" P: "); Serial.print(bmeSample.presHpa, 1); Serial.print(" VCC: "); Serial.print(vccMv); Serial.println("mV"); Serial.flush(); wdt_reset(); sleep_1s(); // Sleep for 1 second after transmission }