diff --git a/include/cyclic.h b/include/cyclic.h index 4b402360..ccadbb65 100644 --- a/include/cyclic.h +++ b/include/cyclic.h @@ -18,6 +18,15 @@ #include "display.h" #endif +#if (HAS_SDS011) +#include "sds011read.h" +#endif + +#if (HAS_SDCARD) +#include "sdcard.h" +#endif + + extern Ticker housekeeper; void housekeeping(void); diff --git a/include/payload.h b/include/payload.h index b2d5683f..328c0b4e 100644 --- a/include/payload.h +++ b/include/payload.h @@ -61,6 +61,62 @@ public: void addTime(time_t value); void addPM10(float value); void addPM25(float value); +private: + void addChars( char* string, int len); + +#if (PAYLOAD_ENCODER == 1) // format plain + +private: + uint8_t *buffer; + uint8_t cursor; + +#elif (PAYLOAD_ENCODER == 2) // format packed + +private: + uint8_t *buffer; + uint8_t cursor; + void uintToBytes(uint64_t i, uint8_t byteSize); + void writeUptime(uint64_t unixtime); + void writeLatLng(double latitude, double longitude); + void writeUint64(uint64_t i); + void writeUint32(uint32_t i); + void writeUint16(uint16_t i); + void writeUint8(uint8_t i); + void writeFloat(float value); + void writeUFloat(float value); + void writePressure(float value); + void writeVersion(char *version); + void writeBitmap(bool a, bool b, bool c, bool d, bool e, bool f, bool g, + bool h); + +#elif ((PAYLOAD_ENCODER == 3) || (PAYLOAD_ENCODER == 4)) // format cayenne lpp + +private: + uint8_t *buffer; + uint8_t maxsize; + uint8_t cursor; + +#else +#error No valid payload converter defined! +#endif +}; + +extern PayloadConvert payload; + +#endif // _PAYLOAD_H_ + void addCount(uint16_t value, uint8_t sniffytpe); + void addConfig(configData_t value); + void addStatus(uint16_t voltage, uint64_t uptime, float cputemp, uint32_t mem, + uint8_t reset1, uint8_t reset2); + void addAlarm(int8_t rssi, uint8_t message); + void addVoltage(uint16_t value); + void addGPS(gpsStatus_t value); + void addBME(bmeStatus_t value); + void addButton(uint8_t value); + void addSensor(uint8_t[]); + void addTime(time_t value); + void addPM10(float value); + void addPM25(float value); void addChars( char* string, int len); #if (PAYLOAD_ENCODER == 1) // format plain diff --git a/include/sdcard.h b/include/sdcard.h index 051baa19..99e784cb 100644 --- a/include/sdcard.h +++ b/include/sdcard.h @@ -10,7 +10,7 @@ #define SDCARD_FILE_NAME "paxcount.%02d" #define SDCARD_FILE_HEADER "date, time, wifi, bluet" -bool sdcardInit( void ); +bool sdcard_init( void ); void sdcardWriteData( uint16_t, uint16_t); -#endif \ No newline at end of file +#endif diff --git a/include/sds011read.h b/include/sds011read.h index 734395a7..d278a5fd 100644 --- a/include/sds011read.h +++ b/include/sds011read.h @@ -11,5 +11,7 @@ bool sds011_init(); void sds011_loop(); +void sds011_sleep(void); +void sds011_wakeup(void); #endif // _SDS011READ_H diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 89f8e3e2..6a13a217 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -9,8 +9,14 @@ static const char TAG[] = __FILE__; Ticker housekeeper; +#if (HAS_SDS011) +extern boolean isSDS011Active; +#endif + void housekeeping() { + static int counter = 0; xTaskNotifyFromISR(irqHandlerTask, CYCLIC_IRQ, eSetBits, NULL); +ESP_LOGI( TAG, "in Housekeeping(): %d", counter++); } // do all housekeeping @@ -18,7 +24,6 @@ void doHousekeeping() { // update uptime counter uptime(); - // check if update mode trigger switch was set if (RTC_runmode == RUNMODE_UPDATE) { // check battery status if we can before doing ota @@ -112,6 +117,113 @@ void doHousekeeping() { } #endif +#if (HAS_SDS011) + if ( isSDS011Active ) { + ESP_LOGD(TAG, "SDS011: go to sleep"); + sds011_loop(); + } + else { + ESP_LOGD(TAG, "SDS011: wakeup"); + sds011_wakeup(); + } +#endif + +} // doHousekeeping() + +// uptime counter 64bit to prevent millis() rollover after 49 days +uint64_t uptime() { + static uint32_t low32, high32; + uint32_t new_low32 = millis(); + if (new_low32 < low32) + high32++; + low32 = new_low32; + return (uint64_t)high32 << 32 | low32; +} + +uint32_t getFreeRAM() { +#ifndef BOARD_HAS_PSRAM + return ESP.getFreeHeap(); +#else + return ESP.getFreePsram(); +#endif +} + +void reset_counters() { +#if ((WIFICOUNTER) || (BLECOUNTER)) + macs.clear(); // clear all macs container + macs_total = 0; // reset all counters + macs_wifi = 0; + macs_ble = 0; +#ifdef HAS_DISPLAY + oledPlotCurve(0, true); +#endif + +#endif +} +#endif + +#if (defined HAS_DCF77 || defined HAS_IF482) + ESP_LOGD(TAG, "Clockloop %d bytes left | Taskstate = %d", + uxTaskGetStackHighWaterMark(ClockTask), eTaskGetState(ClockTask)); +#endif + +#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) + ESP_LOGD(TAG, "LEDloop %d bytes left | Taskstate = %d", + uxTaskGetStackHighWaterMark(ledLoopTask), + eTaskGetState(ledLoopTask)); +#endif + +// read battery voltage into global variable +#if (defined BAT_MEASURE_ADC || defined HAS_PMU) + batt_voltage = read_voltage(); + if (batt_voltage == 0xffff) + ESP_LOGI(TAG, "Battery: external power"); + else + ESP_LOGI(TAG, "Battery: %dmV", batt_voltage); +#ifdef HAS_PMU + AXP192_showstatus(); +#endif +#endif + +// display BME680/280 sensor data +#if (HAS_BME) +#ifdef HAS_BME680 + ESP_LOGI(TAG, "BME680 Temp: %.2f°C | IAQ: %.2f | IAQacc: %d", + bme_status.temperature, bme_status.iaq, bme_status.iaq_accuracy); +#elif defined HAS_BME280 + ESP_LOGI(TAG, "BME280 Temp: %.2f°C | Humidity: %.2f | Pressure: %.0f", + bme_status.temperature, bme_status.humidity, bme_status.pressure); +#elif defined HAS_BMP180 + ESP_LOGI(TAG, "BMP180 Temp: %.2f°C | Pressure: %.0f", bme_status.temperature, + bme_status.pressure); +#endif +#endif + + // check free heap memory + if (ESP.getMinFreeHeap() <= MEM_LOW) { + ESP_LOGI(TAG, + "Memory full, counter cleared (heap low water mark = %d Bytes / " + "free heap = %d bytes)", + ESP.getMinFreeHeap(), ESP.getFreeHeap()); + reset_counters(); // clear macs container and reset all counters + get_salt(); // get new salt for salting hashes + + if (ESP.getMinFreeHeap() <= MEM_LOW) // check again + do_reset(true); // memory leak, reset device + } + +// check free PSRAM memory +#ifdef BOARD_HAS_PSRAM + if (ESP.getMinFreePsram() <= MEM_LOW) { + ESP_LOGI(TAG, "PSRAM full, counter cleared"); + reset_counters(); // clear macs container and reset all counters + get_salt(); // get new salt for salting hashes + + if (ESP.getMinFreePsram() <= MEM_LOW) // check again + do_reset(true); // memory leak, reset device + } +#endif + } // doHousekeeping() // uptime counter 64bit to prevent millis() rollover after 49 days @@ -143,4 +255,4 @@ void reset_counters() { #endif #endif -} \ No newline at end of file +} diff --git a/src/main.cpp b/src/main.cpp index 19e45152..09aa66c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -323,6 +323,443 @@ void setup() { assert(spi_init() == ESP_OK); #endif +#ifdef HAS_SDCARD + if (sdcard_init()) + strcat_P(features, " SD"); +#endif + +#if (HAS_SDS011) + ESP_LOGI(TAG, "init fine-dust-sensor"); + if ( sds011_init() ) + strcat_P(features, " SDS"); +#endif + +#if (VENDORFILTER) + strcat_P(features, " FILTER"); +#endif + +// initialize matrix display +#ifdef HAS_MATRIX_DISPLAY + strcat_P(features, " LED_MATRIX"); + MatrixDisplayIsOn = cfg.screenon; + init_matrix_display(); // note: blocking call +#endif + +// show payload encoder +#if PAYLOAD_ENCODER == 1 + strcat_P(features, " PLAIN"); +#elif PAYLOAD_ENCODER == 2 + strcat_P(features, " PACKED"); +#elif PAYLOAD_ENCODER == 3 + strcat_P(features, " LPPDYN"); +#elif PAYLOAD_ENCODER == 4 + strcat_P(features, " LPPPKD"); +#endif + +// initialize RTC +#ifdef HAS_RTC + strcat_P(features, " RTC"); + assert(rtc_init()); +#endif + +#if defined HAS_DCF77 + strcat_P(features, " DCF77"); +#endif + +#if defined HAS_IF482 + strcat_P(features, " IF482"); +#endif + +#if (WIFICOUNTER) + strcat_P(features, " WIFI"); + // start wifi in monitor mode and start channel rotation timer + ESP_LOGI(TAG, "Starting Wifi..."); + wifi_sniffer_init(); +#else + // switch off wifi + esp_wifi_deinit(); +#endif + + // initialize salt value using esp_random() called by random() in + // arduino-esp32 core. Note: do this *after* wifi has started, since + // function gets it's seed from RF noise + get_salt(); // get new 16bit for salting hashes + + // start state machine + ESP_LOGI(TAG, "Starting Interrupt Handler..."); + xTaskCreatePinnedToCore(irqHandler, // task function + "irqhandler", // name of task + 4096, // stack size of task + (void *)1, // parameter of the task + 2, // priority of the task + &irqHandlerTask, // task handle + 1); // CPU core + +// initialize BME sensor (BME280/BME680) +#if (HAS_BME) +#ifdef HAS_BME680 + strcat_P(features, " BME680"); +#elif defined HAS_BME280 + strcat_P(features, " BME280"); +#elif defined HAS_BMP180 + strcat_P(features, " BMP180"); +#endif + if (bme_init()) + ESP_LOGI(TAG, "Starting BME sensor..."); +#endif + + // starting timers and interrupts + assert(irqHandlerTask != NULL); // has interrupt handler task started? + ESP_LOGI(TAG, "Starting Timers..."); + +// display interrupt +#ifdef HAS_DISPLAY + // https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/ + // prescaler 80 -> divides 80 MHz CPU freq to 1 MHz, timer 0, count up + displayIRQ = timerBegin(0, 80, true); + timerAttachInterrupt(displayIRQ, &DisplayIRQ, true); + timerAlarmWrite(displayIRQ, DISPLAYREFRESH_MS * 1000, true); + timerAlarmEnable(displayIRQ); +#endif + +// LED Matrix display interrupt +#ifdef HAS_MATRIX_DISPLAY + // https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/ + // prescaler 80 -> divides 80 MHz CPU freq to 1 MHz, timer 3, count up + matrixDisplayIRQ = timerBegin(3, 80, true); + timerAttachInterrupt(matrixDisplayIRQ, &MatrixDisplayIRQ, true); + timerAlarmWrite(matrixDisplayIRQ, MATRIX_DISPLAY_SCAN_US, true); + timerAlarmEnable(matrixDisplayIRQ); +#endif + +// initialize button +#ifdef HAS_BUTTON + strcat_P(features, " BTN_"); +#ifdef BUTTON_PULLUP + strcat_P(features, "PU"); +#else + strcat_P(features, "PD"); +#endif // BUTTON_PULLUP + button_init(HAS_BUTTON); +#endif // HAS_BUTTON + + // cyclic function interrupts + sendcycler.attach(SENDCYCLE * 2, sendcycle); + housekeeper.attach(HOMECYCLE, housekeeping); + +#if (TIME_SYNC_INTERVAL) + +#if (!(TIME_SYNC_LORAWAN) && !(TIME_SYNC_LORASERVER) && !defined HAS_GPS && \ + !defined HAS_RTC) +#warning you did not specify a time source, time will not be synched +#endif + +// initialize gps time +#if (HAS_GPS) + fetch_gpsTime(); +#endif + +#if (defined HAS_IF482 || defined HAS_DCF77) + ESP_LOGI(TAG, "Starting Clock Controller..."); + clock_init(); +#endif + +#if (TIME_SYNC_LORASERVER) + timesync_init(); // create loraserver time sync task +#endif + + ESP_LOGI(TAG, "Starting Timekeeper..."); + assert(timepulse_init()); // setup pps timepulse + timepulse_start(); // starts pps and cyclic time sync + +#endif // TIME_SYNC_INTERVAL + + // show compiled features + ESP_LOGI(TAG, "Features:%s", features); + + // set runmode to normal + RTC_runmode = RUNMODE_NORMAL; + + vTaskDelete(NULL); + +} // setup() + +void loop() { vTaskDelete(NULL); } +3 MatrixDisplayIRQ -> matrix mux cycle -> 0,5ms (MATRIX_DISPLAY_SCAN_US) + + +// Interrupt routines +------------------------------------------------------------------------------- + +fired by hardware +DisplayIRQ -> esp32 timer 0 -> irqHandlerTask (Core 1) +CLOCKIRQ -> esp32 timer 1 -> ClockTask (Core 1) +ButtonIRQ -> external gpio -> irqHandlerTask (Core 1) +PMUIRQ -> PMU chip gpio -> irqHandlerTask (Core 1) + +fired by software (Ticker.h) +TIMESYNC_IRQ -> timeSync() -> irqHandlerTask (Core 1) +CYCLIC_IRQ -> housekeeping() -> irqHandlerTask (Core 1) +SENDCYCLE_IRQ -> sendcycle() -> irqHandlerTask (Core 1) +BME_IRQ -> bmecycle() -> irqHandlerTask (Core 1) + + +// External RTC timer (if present) +------------------------------------------------------------------------------- +triggers pps 1 sec impulse + +*/ + +// Basic Config +#include "main.h" + +configData_t cfg; // struct holds current device configuration +char lmic_event_msg[LMIC_EVENTMSG_LEN]; // display buffer for LMIC event message +uint8_t volatile channel = 0; // channel rotation counter +uint16_t volatile macs_total = 0, macs_wifi = 0, macs_ble = 0, + batt_voltage = 0; // globals for display + +hw_timer_t *ppsIRQ = NULL, *displayIRQ = NULL, *matrixDisplayIRQ = NULL; + +TaskHandle_t irqHandlerTask = NULL, ClockTask = NULL; +SemaphoreHandle_t I2Caccess; +bool volatile TimePulseTick = false; +time_t userUTCTime = 0; +timesource_t timeSource = _unsynced; + +// container holding unique MAC address hashes with Memory Alloctor using PSRAM, +// if present +std::set, Mallocator> macs; + +// initialize payload encoder +PayloadConvert payload(PAYLOAD_BUFFER_SIZE); + +// set Time Zone for user setting from paxcounter.conf +TimeChangeRule myDST = DAYLIGHT_TIME; +TimeChangeRule mySTD = STANDARD_TIME; +Timezone myTZ(myDST, mySTD); + +// local Tag for logging +static const char TAG[] = __FILE__; + +void setup() { + + char features[100] = ""; + + // create some semaphores for syncing / mutexing tasks + I2Caccess = xSemaphoreCreateMutex(); // for access management of i2c bus + assert(I2Caccess != NULL); + I2C_MUTEX_UNLOCK(); + + // disable brownout detection +#ifdef DISABLE_BROWNOUT + // register with brownout is at address DR_REG_RTCCNTL_BASE + 0xd4 + (*((uint32_t volatile *)ETS_UNCACHED_ADDR((DR_REG_RTCCNTL_BASE + 0xd4)))) = 0; +#endif + + // setup debug output or silence device +#if (VERBOSE) + Serial.begin(115200); + esp_log_level_set("*", ESP_LOG_VERBOSE); +#else + // mute logs completely by redirecting them to silence function + esp_log_level_set("*", ESP_LOG_NONE); +#endif + + do_after_reset(rtc_get_reset_reason(0)); + + // print chip information on startup if in verbose mode after coldstart +#if (VERBOSE) + + if (RTC_runmode == RUNMODE_POWERCYCLE) { + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + ESP_LOGI(TAG, + "This is ESP32 chip with %d CPU cores, WiFi%s%s, silicon revision " + "%d, %dMB %s Flash", + chip_info.cores, + (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", + (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "", + chip_info.revision, spi_flash_get_chip_size() / (1024 * 1024), + (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" + : "external"); + ESP_LOGI(TAG, "Internal Total heap %d, internal Free Heap %d", + ESP.getHeapSize(), ESP.getFreeHeap()); +#ifdef BOARD_HAS_PSRAM + ESP_LOGI(TAG, "SPIRam Total heap %d, SPIRam Free Heap %d", + ESP.getPsramSize(), ESP.getFreePsram()); +#endif + ESP_LOGI(TAG, "ChipRevision %d, Cpu Freq %d, SDK Version %s", + ESP.getChipRevision(), ESP.getCpuFreqMHz(), ESP.getSdkVersion()); + ESP_LOGI(TAG, "Flash Size %d, Flash Speed %d", ESP.getFlashChipSize(), + ESP.getFlashChipSpeed()); + ESP_LOGI(TAG, "Wifi/BT software coexist version %s", + esp_coex_version_get()); + +#if (HAS_LORA) + ESP_LOGI(TAG, "IBM LMIC version %d.%d.%d", LMIC_VERSION_MAJOR, + LMIC_VERSION_MINOR, LMIC_VERSION_BUILD); + ESP_LOGI(TAG, "Arduino LMIC version %d.%d.%d.%d", + ARDUINO_LMIC_VERSION_GET_MAJOR(ARDUINO_LMIC_VERSION), + ARDUINO_LMIC_VERSION_GET_MINOR(ARDUINO_LMIC_VERSION), + ARDUINO_LMIC_VERSION_GET_PATCH(ARDUINO_LMIC_VERSION), + ARDUINO_LMIC_VERSION_GET_LOCAL(ARDUINO_LMIC_VERSION)); + showLoraKeys(); +#endif // HAS_LORA + +#if (HAS_GPS) + ESP_LOGI(TAG, "TinyGPS+ version %s", TinyGPSPlus::libraryVersion()); +#endif + } +#endif // VERBOSE + + // open i2c bus + i2c_init(); + +// setup power on boards with power management logic +#ifdef EXT_POWER_SW + pinMode(EXT_POWER_SW, OUTPUT); + digitalWrite(EXT_POWER_SW, EXT_POWER_ON); + strcat_P(features, " VEXT"); +#endif +#ifdef HAS_PMU + AXP192_init(); + strcat_P(features, " PMU"); +#endif + + // read (and initialize on first run) runtime settings from NVRAM + loadConfig(); // includes initialize if necessary + +// initialize display +#ifdef HAS_DISPLAY + strcat_P(features, " OLED"); + DisplayIsOn = cfg.screenon; + // display verbose info only after a coldstart (note: blocking call!) + init_display(RTC_runmode == RUNMODE_POWERCYCLE ? true : false); +#endif + + // scan i2c bus for devices + i2c_scan(); + +#ifdef BOARD_HAS_PSRAM + assert(psramFound()); + ESP_LOGI(TAG, "PSRAM found and initialized"); + strcat_P(features, " PSRAM"); +#endif + +#ifdef BAT_MEASURE_EN + pinMode(BAT_MEASURE_EN, OUTPUT); +#endif + +// initialize leds +#if (HAS_LED != NOT_A_PIN) + pinMode(HAS_LED, OUTPUT); + strcat_P(features, " LED"); + +#ifdef LED_POWER_SW + pinMode(LED_POWER_SW, OUTPUT); + digitalWrite(LED_POWER_SW, LED_POWER_ON); +#endif + +#ifdef HAS_TWO_LED + pinMode(HAS_TWO_LED, OUTPUT); + strcat_P(features, " LED1"); +#endif + +// use LED for power display if we have additional RGB LED, else for status +#ifdef HAS_RGB_LED + switch_LED(LED_ON); + strcat_P(features, " RGB"); + rgb_set_color(COLOR_PINK); +#endif + +#endif // HAS_LED + +#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) + // start led loop + ESP_LOGI(TAG, "Starting LED Controller..."); + xTaskCreatePinnedToCore(ledLoop, // task function + "ledloop", // name of task + 1024, // stack size of task + (void *)1, // parameter of the task + 3, // priority of the task + &ledLoopTask, // task handle + 0); // CPU core +#endif + +// initialize wifi antenna +#ifdef HAS_ANTENNA_SWITCH + strcat_P(features, " ANT"); + antenna_init(); + antenna_select(cfg.wifiant); +#endif + +// initialize battery status +#if (defined BAT_MEASURE_ADC || defined HAS_PMU) + strcat_P(features, " BATT"); + calibrate_voltage(); + batt_voltage = read_voltage(); +#endif + +#if (USE_OTA) + strcat_P(features, " OTA"); + // reboot to firmware update mode if ota trigger switch is set + if (RTC_runmode == RUNMODE_UPDATE) + start_ota_update(); +#endif + +// start BLE scan callback if BLE function is enabled in NVRAM configuration +// or switch off bluetooth, if not compiled +#if (BLECOUNTER) + strcat_P(features, " BLE"); + if (cfg.blescan) { + ESP_LOGI(TAG, "Starting Bluetooth..."); + start_BLEscan(); + } else + btStop(); +#else + // remove bluetooth stack to gain more free memory + btStop(); + ESP_ERROR_CHECK(esp_bt_mem_release(ESP_BT_MODE_BTDM)); + ESP_ERROR_CHECK(esp_coex_preference_set( + ESP_COEX_PREFER_WIFI)); // configure Wifi/BT coexist lib +#endif + +// initialize gps +#if (HAS_GPS) + strcat_P(features, " GPS"); + if (gps_init()) { + ESP_LOGI(TAG, "Starting GPS Feed..."); + xTaskCreatePinnedToCore(gps_loop, // task function + "gpsloop", // name of task + 2048, // stack size of task + (void *)1, // parameter of the task + 1, // priority of the task + &GpsTask, // task handle + 1); // CPU core + } +#endif + +// initialize sensors +#if (HAS_SENSORS) + strcat_P(features, " SENS"); + sensor_init(); +#endif + +// initialize LoRa +#if (HAS_LORA) + strcat_P(features, " LORA"); + // kick off join, except we come from sleep + assert(lora_stack_init(RTC_runmode == RUNMODE_WAKEUP ? false : true) == + ESP_OK); +#endif + +// initialize SPI +#ifdef HAS_SPI + strcat_P(features, " SPI"); + assert(spi_init() == ESP_OK); +#endif + #ifdef HAS_SDCARD if (sdcardInit()) strcat_P(features, " SD"); diff --git a/src/payload.cpp b/src/payload.cpp index 2624c138..05924897 100644 --- a/src/payload.cpp +++ b/src/payload.cpp @@ -491,6 +491,474 @@ void PayloadConvert::addTime(time_t value) { } #endif // PAYLOAD_ENCODER +void PayloadConvert::addPM10( float value) { +#if (HAS_SDS011) +#if (PAYLOAD_ENCODER == 1) // plain + char tempBuffer[10+1]; + sprintf( tempBuffer, ",%5.1f", value); + addChars(tempBuffer, strlen(tempBuffer)); +#elif (PAYLOAD_ENCODER == 2 ) // packed + writeUint16( (uint16_t) (value*10) ); +#elif (PAYLOAD_ENCODER == 3 ) // Cayenne LPP dynamic +#error not implemented yet +#elif (PAYLOAD_ENCODER == 4 ) // Cayenne LPP packed +#error not implemented yet +#endif +#endif // HAS_SDS011 +} + +void PayloadConvert::addPM25( float value) { +#if (HAS_SDS011) +#if (PAYLOAD_ENCODER == 1) // plain + char tempBuffer[10+1]; + sprintf( tempBuffer, ",%5.1f", value); + addChars(tempBuffer, strlen(tempBuffer)); +#elif (PAYLOAD_ENCODER == 2 ) // packed + writeUint16( (uint16_t) (value*10) ); +#elif (PAYLOAD_ENCODER == 3 ) // Cayenne LPP dynamic +#error not implemented yet +#elif (PAYLOAD_ENCODER == 4 ) // Cayenne LPP packed +#error not implemented yet +#endif +#endif // HAS_SDS011 +} + +void PayloadConvert::addChars( char * string, int len) { + for (int i=0; i < len; i++) + addByte(string[i]); +} + + buffer[cursor++] = highByte(voltage); + buffer[cursor++] = lowByte(voltage); + buffer[cursor++] = (byte)((uptime & 0xFF00000000000000) >> 56); + buffer[cursor++] = (byte)((uptime & 0x00FF000000000000) >> 48); + buffer[cursor++] = (byte)((uptime & 0x0000FF0000000000) >> 40); + buffer[cursor++] = (byte)((uptime & 0x000000FF00000000) >> 32); + buffer[cursor++] = (byte)((uptime & 0x00000000FF000000) >> 24); + buffer[cursor++] = (byte)((uptime & 0x0000000000FF0000) >> 16); + buffer[cursor++] = (byte)((uptime & 0x000000000000FF00) >> 8); + buffer[cursor++] = (byte)((uptime & 0x00000000000000FF)); + buffer[cursor++] = (byte)(cputemp); + buffer[cursor++] = (byte)((mem & 0xFF000000) >> 24); + buffer[cursor++] = (byte)((mem & 0x00FF0000) >> 16); + buffer[cursor++] = (byte)((mem & 0x0000FF00) >> 8); + buffer[cursor++] = (byte)((mem & 0x000000FF)); + buffer[cursor++] = (byte)(reset1); + buffer[cursor++] = (byte)(reset2); +} + +void PayloadConvert::addGPS(gpsStatus_t value) { +#if(HAS_GPS) + buffer[cursor++] = (byte)((value.latitude & 0xFF000000) >> 24); + buffer[cursor++] = (byte)((value.latitude & 0x00FF0000) >> 16); + buffer[cursor++] = (byte)((value.latitude & 0x0000FF00) >> 8); + buffer[cursor++] = (byte)((value.latitude & 0x000000FF)); + buffer[cursor++] = (byte)((value.longitude & 0xFF000000) >> 24); + buffer[cursor++] = (byte)((value.longitude & 0x00FF0000) >> 16); + buffer[cursor++] = (byte)((value.longitude & 0x0000FF00) >> 8); + buffer[cursor++] = (byte)((value.longitude & 0x000000FF)); +#if (!PAYLOAD_OPENSENSEBOX) + buffer[cursor++] = value.satellites; + buffer[cursor++] = highByte(value.hdop); + buffer[cursor++] = lowByte(value.hdop); + buffer[cursor++] = highByte(value.altitude); + buffer[cursor++] = lowByte(value.altitude); +#endif +#endif +} + +void PayloadConvert::addSensor(uint8_t buf[]) { +#if(HAS_SENSORS) + uint8_t length = buf[0]; + memcpy(buffer, buf + 1, length); + cursor += length; // length of buffer +#endif +} + +void PayloadConvert::addBME(bmeStatus_t value) { +#if(HAS_BME) + int16_t temperature = (int16_t)(value.temperature); // float -> int + uint16_t humidity = (uint16_t)(value.humidity); // float -> int + uint16_t pressure = (uint16_t)(value.pressure); // float -> int + uint16_t iaq = (uint16_t)(value.iaq); // float -> int + buffer[cursor++] = highByte(temperature); + buffer[cursor++] = lowByte(temperature); + buffer[cursor++] = highByte(pressure); + buffer[cursor++] = lowByte(pressure); + buffer[cursor++] = highByte(humidity); + buffer[cursor++] = lowByte(humidity); + buffer[cursor++] = highByte(iaq); + buffer[cursor++] = lowByte(iaq); +#endif +} + +void PayloadConvert::addButton(uint8_t value) { +#ifdef HAS_BUTTON + buffer[cursor++] = value; +#endif +} + +void PayloadConvert::addTime(time_t value) { + uint32_t time = (uint32_t)value; + buffer[cursor++] = (byte)((time & 0xFF000000) >> 24); + buffer[cursor++] = (byte)((time & 0x00FF0000) >> 16); + buffer[cursor++] = (byte)((time & 0x0000FF00) >> 8); + buffer[cursor++] = (byte)((time & 0x000000FF)); +} + +/* ---------------- packed format with LoRa serialization Encoder ---------- + */ +// derived from +// https://github.com/thesolarnomad/lora-serialization/blob/master/src/LoraEncoder.cpp + +#elif (PAYLOAD_ENCODER == 2) + +void PayloadConvert::addByte(uint8_t value) { writeUint8(value); } + +void PayloadConvert::addCount(uint16_t value, uint8_t snifftype) { + writeUint16(value); +} + +void PayloadConvert::addAlarm(int8_t rssi, uint8_t msg) { + writeUint8(rssi); + writeUint8(msg); +} + +void PayloadConvert::addVoltage(uint16_t value) { writeUint16(value); } + +void PayloadConvert::addConfig(configData_t value) { + writeUint8(value.loradr); + writeUint8(value.txpower); + writeUint16(value.rssilimit); + writeUint8(value.sendcycle); + writeUint8(value.wifichancycle); + writeUint8(value.blescantime); + writeUint8(value.rgblum); + writeBitmap(value.adrmode ? true : false, value.screensaver ? true : false, + value.screenon ? true : false, value.countermode ? true : false, + value.blescan ? true : false, value.wifiant ? true : false, + value.vendorfilter ? true : false, + value.monitormode ? true : false); + writeBitmap(value.payloadmask && GPS_DATA ? true : false, + value.payloadmask && ALARM_DATA ? true : false, + value.payloadmask && MEMS_DATA ? true : false, + value.payloadmask && COUNT_DATA ? true : false, + value.payloadmask && SENSOR1_DATA ? true : false, + value.payloadmask && SENSOR2_DATA ? true : false, + value.payloadmask && SENSOR3_DATA ? true : false, + value.payloadmask && BATT_DATA ? true : false); + writeVersion(value.version); +} + +void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, float cputemp, + uint32_t mem, uint8_t reset1, uint8_t reset2) { + writeUint16(voltage); + writeUptime(uptime); + writeUint8((byte)cputemp); + writeUint32(mem); + writeUint8(reset1); + writeUint8(reset2); +} + +void PayloadConvert::addGPS(gpsStatus_t value) { +#if(HAS_GPS) + writeLatLng(value.latitude, value.longitude); +#if (!PAYLOAD_OPENSENSEBOX) + writeUint8(value.satellites); + writeUint16(value.hdop); + writeUint16(value.altitude); +#endif +#endif +} + +void PayloadConvert::addSensor(uint8_t buf[]) { +#if(HAS_SENSORS) + uint8_t length = buf[0]; + memcpy(buffer, buf + 1, length); + cursor += length; // length of buffer +#endif +} + +void PayloadConvert::addBME(bmeStatus_t value) { +#if(HAS_BME) + writeFloat(value.temperature); + writePressure(value.pressure); + writeUFloat(value.humidity); + writeUFloat(value.iaq); +#endif +} + +void PayloadConvert::addButton(uint8_t value) { +#ifdef HAS_BUTTON + writeUint8(value); +#endif +} + +void PayloadConvert::addTime(time_t value) { + uint32_t time = (uint32_t)value; + writeUint32(time); +} + +void PayloadConvert::uintToBytes(uint64_t value, uint8_t byteSize) { + for (uint8_t x = 0; x < byteSize; x++) { + byte next = 0; + if (sizeof(value) > x) { + next = static_cast((value >> (x * 8)) & 0xFF); + } + buffer[cursor] = next; + ++cursor; + } +} + +void PayloadConvert::writeUptime(uint64_t uptime) { + writeUint64(uptime); +} + +void PayloadConvert::writeVersion(char *version) { + memcpy(buffer + cursor, version, 10); + cursor += 10; +} + +void PayloadConvert::writeLatLng(double latitude, double longitude) { + // Tested to at least work with int32_t, which are processed correctly. + writeUint32(latitude); + writeUint32(longitude); +} + +void PayloadConvert::writeUint64(uint64_t i) { uintToBytes(i, 8); } + +void PayloadConvert::writeUint32(uint32_t i) { uintToBytes(i, 4); } + +void PayloadConvert::writeUint16(uint16_t i) { uintToBytes(i, 2); } + +void PayloadConvert::writeUint8(uint8_t i) { uintToBytes(i, 1); } + +void PayloadConvert::writeUFloat(float value) { + writeUint16(value * 100); +} + +void PayloadConvert::writePressure(float value) { + writeUint16(value * 10); +} + +/** + * Uses a 16bit two's complement with two decimals, so the range is + * -327.68 to +327.67 degrees + */ +void PayloadConvert::writeFloat(float value) { + int16_t t = (int16_t)(value * 100); + if (value < 0) { + t = ~-t; + t = t + 1; + } + buffer[cursor++] = (byte)((t >> 8) & 0xFF); + buffer[cursor++] = (byte)t & 0xFF; +} + +void PayloadConvert::writeBitmap(bool a, bool b, bool c, bool d, bool e, bool f, + bool g, bool h) { + uint8_t bitmap = 0; + // LSB first + bitmap |= (a & 1) << 7; + bitmap |= (b & 1) << 6; + bitmap |= (c & 1) << 5; + bitmap |= (d & 1) << 4; + bitmap |= (e & 1) << 3; + bitmap |= (f & 1) << 2; + bitmap |= (g & 1) << 1; + bitmap |= (h & 1) << 0; + writeUint8(bitmap); +} + +/* ---------------- Cayenne LPP 2.0 format ---------- */ +// see specs +// http://community.mydevices.com/t/cayenne-lpp-2-0/7510 (LPP 2.0) +// https://github.com/myDevicesIoT/cayenne-docs/blob/master/docs/LORA.md +// (LPP 1.0) PAYLOAD_ENCODER == 3 -> Dynamic Sensor Payload, using channels -> +// FPort 1 PAYLOAD_ENCODER == 4 -> Packed Sensor Payload, not using channels -> +// FPort 2 + +#elif ((PAYLOAD_ENCODER == 3) || (PAYLOAD_ENCODER == 4)) + +void PayloadConvert::addByte(uint8_t value) { + /* + not implemented + */ } + +void PayloadConvert::addCount(uint16_t value, uint8_t snifftype) { + switch (snifftype) { + case MAC_SNIFF_WIFI: +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_COUNT_WIFI_CHANNEL; +#endif + buffer[cursor++] = + LPP_LUMINOSITY; // workaround since cayenne has no data type meter + buffer[cursor++] = highByte(value); + buffer[cursor++] = lowByte(value); + break; + case MAC_SNIFF_BLE: +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_COUNT_BLE_CHANNEL; +#endif + buffer[cursor++] = + LPP_LUMINOSITY; // workaround since cayenne has no data type meter + buffer[cursor++] = highByte(value); + buffer[cursor++] = lowByte(value); + break; + } +} + +void PayloadConvert::addAlarm(int8_t rssi, uint8_t msg) { +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_ALARM_CHANNEL; +#endif + buffer[cursor++] = LPP_PRESENCE; + buffer[cursor++] = msg; +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_MSG_CHANNEL; +#endif + buffer[cursor++] = LPP_ANALOG_INPUT; + buffer[cursor++] = rssi; +} + +void PayloadConvert::addVoltage(uint16_t value) { + uint16_t volt = value / 10; +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_BATT_CHANNEL; +#endif + buffer[cursor++] = LPP_ANALOG_INPUT; + buffer[cursor++] = highByte(volt); + buffer[cursor++] = lowByte(volt); +} + +void PayloadConvert::addConfig(configData_t value) { +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_ADR_CHANNEL; +#endif + buffer[cursor++] = LPP_DIGITAL_INPUT; + buffer[cursor++] = value.adrmode; +} + +void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, float celsius, + uint32_t mem, uint8_t reset1, uint8_t reset2) { + uint16_t temp = celsius * 10; + uint16_t volt = voltage / 10; +#if (defined BAT_MEASURE_ADC || defined HAS_PMU) +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_BATT_CHANNEL; +#endif + buffer[cursor++] = LPP_ANALOG_INPUT; + buffer[cursor++] = highByte(volt); + buffer[cursor++] = lowByte(volt); +#endif // BAT_MEASURE_ADC + +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_TEMPERATURE_CHANNEL; +#endif + buffer[cursor++] = LPP_TEMPERATURE; + buffer[cursor++] = highByte(temp); + buffer[cursor++] = lowByte(temp); +} + +void PayloadConvert::addGPS(gpsStatus_t value) { +#if(HAS_GPS) + int32_t lat = value.latitude / 100; + int32_t lon = value.longitude / 100; + int32_t alt = value.altitude * 100; +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_GPS_CHANNEL; +#endif + buffer[cursor++] = LPP_GPS; + buffer[cursor++] = (byte)((lat & 0xFF0000) >> 16); + buffer[cursor++] = (byte)((lat & 0x00FF00) >> 8); + buffer[cursor++] = (byte)((lat & 0x0000FF)); + buffer[cursor++] = (byte)((lon & 0xFF0000) >> 16); + buffer[cursor++] = (byte)((lon & 0x00FF00) >> 8); + buffer[cursor++] = (byte)(lon & 0x0000FF); + buffer[cursor++] = (byte)((alt & 0xFF0000) >> 16); + buffer[cursor++] = (byte)((alt & 0x00FF00) >> 8); + buffer[cursor++] = (byte)(alt & 0x0000FF); +#endif // HAS_GPS +} + +void PayloadConvert::addSensor(uint8_t buf[]) { +#if(HAS_SENSORS) +// to come +/* + uint8_t length = buf[0]; + memcpy(buffer, buf+1, length); + cursor += length; // length of buffer +*/ +#endif // HAS_SENSORS +} + +void PayloadConvert::addBME(bmeStatus_t value) { +#if(HAS_BME) + + // data value conversions to meet cayenne data type definition + // 0.1°C per bit => -3276,7 .. +3276,7 °C + int16_t temperature = (int16_t)(value.temperature * 10.0); + // 0.1 hPa per bit => 0 .. 6553,6 hPa + uint16_t pressure = (uint16_t)(value.pressure * 10); + // 0.5% per bit => 0 .. 128 %C + uint8_t humidity = (uint8_t)(value.humidity * 2.0); + int16_t iaq = (int16_t)(value.iaq); + +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_TEMPERATURE_CHANNEL; +#endif + buffer[cursor++] = LPP_TEMPERATURE; // 2 bytes 0.1 °C Signed MSB + buffer[cursor++] = highByte(temperature); + buffer[cursor++] = lowByte(temperature); +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_BAROMETER_CHANNEL; +#endif + buffer[cursor++] = LPP_BAROMETER; // 2 bytes 0.1 hPa Unsigned MSB + buffer[cursor++] = highByte(pressure); + buffer[cursor++] = lowByte(pressure); +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_HUMIDITY_CHANNEL; +#endif + buffer[cursor++] = LPP_HUMIDITY; // 1 byte 0.5 % Unsigned + buffer[cursor++] = humidity; +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_AIR_CHANNEL; +#endif + buffer[cursor++] = LPP_LUMINOSITY; // 2 bytes, 1.0 unsigned + buffer[cursor++] = highByte(iaq); + buffer[cursor++] = lowByte(iaq); +#endif // HAS_BME +} + +void PayloadConvert::addButton(uint8_t value) { +#ifdef HAS_BUTTON +#if (PAYLOAD_ENCODER == 3) + buffer[cursor++] = LPP_BUTTON_CHANNEL; +#endif + buffer[cursor++] = LPP_DIGITAL_INPUT; + buffer[cursor++] = value; +#endif // HAS_BUTTON +} + +void PayloadConvert::addTime(time_t value) { +#if (PAYLOAD_ENCODER == 4) + uint32_t t = (uint32_t)value; + uint32_t tx_period = (uint32_t)SENDCYCLE * 2; + buffer[cursor++] = 0x03; // set config mask to UTCTime + TXPeriod + // UTCTime in seconds + buffer[cursor++] = (byte)((t & 0xFF000000) >> 24); + buffer[cursor++] = (byte)((t & 0x00FF0000) >> 16); + buffer[cursor++] = (byte)((t & 0x0000FF00) >> 8); + buffer[cursor++] = (byte)((t & 0x000000FF)); + // TXPeriod in seconds + buffer[cursor++] = (byte)((tx_period & 0xFF000000) >> 24); + buffer[cursor++] = (byte)((tx_period & 0x00FF0000) >> 16); + buffer[cursor++] = (byte)((tx_period & 0x0000FF00) >> 8); + buffer[cursor++] = (byte)((tx_period & 0x000000FF)); +#endif +} +#endif // PAYLOAD_ENCODER + void PayloadConvert::addPM10( float value) { #if (HAS_SDS011) #if (PAYLOAD_ENCODER == 1) // plain diff --git a/src/sdcard.cpp b/src/sdcard.cpp index 8e45ea7c..1c1256a4 100644 --- a/src/sdcard.cpp +++ b/src/sdcard.cpp @@ -19,7 +19,7 @@ static void createFile(void); File fileSDCard; -bool sdcardInit() { +bool sdcard_init() { ESP_LOGD(TAG, "looking for SD-card..."); useSDCard = SD.begin(SDCARD_CS, SDCARD_MOSI, SDCARD_MISO, SDCARD_SCLK); if (useSDCard) @@ -43,7 +43,6 @@ void sdcardWriteData(uint16_t noWifi, uint16_t noBle) { sprintf(tempBuffer, "%d,%d", noWifi, noBle); fileSDCard.print( tempBuffer); #if (HAS_SDS011) - ESP_LOGD(TAG, "fine-dust-values: %5.1f,%4.1f", pm10, pm25); sprintf(tempBuffer, ",%5.1f,%4.1f", pm10, pm25); fileSDCard.print( tempBuffer); #endif @@ -84,4 +83,41 @@ void createFile(void) { return; } +#endif // (HAS_SDCARD) + + if (++counterWrites > 2) { + // force writing to SD-card + ESP_LOGD(TAG, "flushing data to card"); + fileSDCard.flush(); + counterWrites = 0; + } +} + +void createFile(void) { + char bufferFilename[8 + 1 + 3 + 1]; + + useSDCard = false; + + for (int i = 0; i < 100; i++) { + sprintf(bufferFilename, SDCARD_FILE_NAME, i); + ESP_LOGD(TAG, "SD: looking for file <%s>", bufferFilename); + bool fileExists = SD.exists(bufferFilename); + if (!fileExists) { + ESP_LOGD(TAG, "SD: file does not exist: opening"); + fileSDCard = SD.open(bufferFilename, FILE_WRITE); + if (fileSDCard) { + ESP_LOGD(TAG, "SD: name opened: <%s>", bufferFilename); + fileSDCard.print( SDCARD_FILE_HEADER ); +#if (HAS_SDS011) + fileSDCard.print( SDCARD_FILE_HEADER_SDS011 ); +#endif + fileSDCard.println(); + useSDCard = true; + break; + } + } + } + return; +} + #endif // (HAS_SDCARD) diff --git a/src/sds011read.cpp b/src/sds011read.cpp index c55b5741..cf6dccdf 100644 --- a/src/sds011read.cpp +++ b/src/sds011read.cpp @@ -6,12 +6,16 @@ static const char TAG[] = __FILE__; #include // UART(2) is unused in this project +#if (HAS_IF482) +#error cannot use IF482 together with SDS011 (both use UART#2) +#endif static HardwareSerial sdsSerial(2); // so we use it here static SDS011 sdsSensor; // fine dust sensor // the results of the sensor: float pm25; float pm10; +boolean isSDS011Active; // init bool sds011_init() @@ -19,18 +23,40 @@ bool sds011_init() pm25 = pm10 = 0.0; sdsSerial.begin(9600, SERIAL_8N1, ESP_PIN_RX, ESP_PIN_TX); sdsSensor.begin (&sdsSerial); - sdsSensor.contmode(0); // for safety: wakeup/sleep - if we want it we do it by ourselves - sdsSensor.wakeup(); // always wake up + sdsSensor.contmode(0); // for safety: no wakeup/sleep by the sensor + sds011_sleep(); // we do it by ourselves return true; } // reading data: void sds011_loop() { - pm25 = pm10 = 0.0; - int sdsErrorCode = sdsSensor.read(&pm25, &pm10); - if (!sdsErrorCode) - { - ESP_LOGD(TAG, "SDS011 error: %d", sdsErrorCode); - } + if ( isSDS011Active ) { + int sdsErrorCode = sdsSensor.read(&pm25, &pm10); + if (sdsErrorCode) { + pm25 = pm10 = 0.0; + ESP_LOGI(TAG, "SDS011 error: %d", sdsErrorCode); + } + else { + ESP_LOGI(TAG, "fine-dust-values: %5.1f,%4.1f", pm10, pm25); + } + sds011_sleep(); + } return; } + +// putting the SDS-sensor to sleep +void sds011_sleep(void) +{ + sdsSensor.sleep(); + isSDS011Active = false; +} + +// start the SDS-sensor +// needs 30 seconds for warming up +void sds011_wakeup() +{ + if ( !isSDS011Active ) { + sdsSensor.wakeup(); + isSDS011Active = true; + } +} diff --git a/src/senddata.cpp b/src/senddata.cpp index 367a5c68..f89710ca 100644 --- a/src/senddata.cpp +++ b/src/senddata.cpp @@ -9,7 +9,10 @@ extern float pm25; #endif void sendcycle() { + static int counter = 0; xTaskNotifyFromISR(irqHandlerTask, SENDCYCLE_IRQ, eSetBits, NULL); +ESP_LOGI( TAG, "in sendcycle(): %d", counter++); + } // put data to send in RTos Queues used for transmit over channels Lora and SPI @@ -18,9 +21,9 @@ void SendPayload(uint8_t port, sendprio_t prio) { MessageBuffer_t SendBuffer; // contains MessageSize, MessagePort, MessagePrio, Message[] -#if (HAS_SDS011) - sds011_loop(); -#endif +//#if (HAS_SDS011) +// sds011_loop(); +//#endif SendBuffer.MessageSize = payload.getSize(); SendBuffer.MessagePrio = prio; @@ -178,6 +181,144 @@ void sendData() { } // sendData() +void flushQueues() { +#if (HAS_LORA) + lora_queuereset(); +#endif +#ifdef HAS_SPI + spi_queuereset(); +#endif +} + SendBuffer.MessagePort = port; + } + memcpy(SendBuffer.Message, payload.getBuffer(), SendBuffer.MessageSize); + +// enqueue message in device's send queues +#if (HAS_LORA) + lora_enqueuedata(&SendBuffer); +#endif +#ifdef HAS_SPI + spi_enqueuedata(&SendBuffer); +#endif + +// write data to sdcard, if present +#ifdef HAS_SDCARD + sdcardWriteData(macs_wifi, macs_ble); +#endif + +} // SendPayload + +// interrupt triggered function to prepare payload to send +void sendData() { + + uint8_t bitmask = cfg.payloadmask; + uint8_t mask = 1; + #if (HAS_GPS) + gpsStatus_t gps_status; + #endif + + while (bitmask) { + switch (bitmask & mask) { + +#if ((WIFICOUNTER) || (BLECOUNTER)) + case COUNT_DATA: + payload.reset(); +#if !(PAYLOAD_OPENSENSEBOX) + if (cfg.wifiscan) + payload.addCount(macs_wifi, MAC_SNIFF_WIFI); + if (cfg.blescan) + payload.addCount(macs_ble, MAC_SNIFF_BLE); +#endif +#if (HAS_GPS) + if (GPSPORT == COUNTERPORT) { + // send GPS position only if we have a fix + if (gps_hasfix()) { + gps_storelocation(&gps_status); + payload.addGPS(gps_status); + } else + ESP_LOGD(TAG, "No valid GPS position"); + } +#endif +#if (PAYLOAD_OPENSENSEBOX) + if (cfg.wifiscan) + payload.addCount(macs_wifi, MAC_SNIFF_WIFI); + if (cfg.blescan) + payload.addCount(macs_ble, MAC_SNIFF_BLE); +#endif +#if (HAS_SDS011) + payload.addPM10(pm10); + payload.addPM25(pm25); +#endif + SendPayload(COUNTERPORT, prio_normal); + // clear counter if not in cumulative counter mode + if (cfg.countermode != 1) { + reset_counters(); // clear macs container and reset all counters + get_salt(); // get new salt for salting hashes + ESP_LOGI(TAG, "Counter cleared"); + } +#ifdef HAS_DISPLAY + else + oledPlotCurve(macs.size(), true); +#endif + break; +#endif + +#if (HAS_BME) + case MEMS_DATA: + payload.reset(); + payload.addBME(bme_status); + SendPayload(BMEPORT, prio_normal); + break; +#endif + +#if (HAS_GPS) + case GPS_DATA: + if (GPSPORT != COUNTERPORT) { + // send GPS position only if we have a fix + if (gps_hasfix()) { + gps_storelocation(&gps_status); + payload.reset(); + payload.addGPS(gps_status); + SendPayload(GPSPORT, prio_high); + } else + ESP_LOGD(TAG, "No valid GPS position"); + } + break; +#endif + +#if (HAS_SENSORS) + case SENSOR1_DATA: + payload.reset(); + payload.addSensor(sensor_read(1)); + SendPayload(SENSOR1PORT, prio_normal); + break; + case SENSOR2_DATA: + payload.reset(); + payload.addSensor(sensor_read(2)); + SendPayload(SENSOR2PORT, prio_normal); + break; + case SENSOR3_DATA: + payload.reset(); + payload.addSensor(sensor_read(3)); + SendPayload(SENSOR3PORT, prio_normal); + break; +#endif + +#if (defined BAT_MEASURE_ADC || defined HAS_PMU) + case BATT_DATA: + payload.reset(); + payload.addVoltage(read_voltage()); + SendPayload(BATTPORT, prio_normal); + break; +#endif + + } // switch + bitmask &= ~mask; + mask <<= 1; + } // while (bitmask) + +} // sendData() + void flushQueues() { #if (HAS_LORA) lora_queuereset(); diff --git a/src/timekeeper.cpp b/src/timekeeper.cpp index 85f62864..c6540286 100644 --- a/src/timekeeper.cpp +++ b/src/timekeeper.cpp @@ -16,6 +16,9 @@ static const char TAG[] = __FILE__; const char timeSetSymbols[] = {'G', 'R', 'L', '?'}; #ifdef HAS_IF482 +#if (HAS_SDS011) +#error cannot use IF482 together with SDS011 (both use UART#2) +#endif HardwareSerial IF482(2); // use UART #2 (#1 may be in use for serial GPS) #endif @@ -317,4 +320,274 @@ void clock_loop(void *taskparameter) { // ClockTask } // for } // clock_loop() +#endif // HAS_IF482 || defined HAS_DCF77 + if (t) { + timeSource = _rtc; + goto finish; + } +#endif + + goto finish; + +finish: + + setMyTime((uint32_t)t, t_msec, timeSource); // set time + +} // calibrateTime() + +// adjust system time, calibrate RTC and RTC_INT pps +void IRAM_ATTR setMyTime(uint32_t t_sec, uint16_t t_msec, + timesource_t mytimesource) { + + // called with invalid timesource? + if (mytimesource == _unsynced) + return; + + // increment t_sec only if t_msec > 1000 + time_t time_to_set = (time_t)(t_sec + t_msec / 1000); + + // do we have a valid time? + if (timeIsValid(time_to_set)) { + + // if we have msec fraction, then wait until top of second with + // millisecond precision + if (t_msec % 1000) { + time_to_set++; + vTaskDelay(pdMS_TO_TICKS(1000 - t_msec % 1000)); + } + + ESP_LOGD(TAG, "[%0.3f] UTC epoch time: %d.%03d sec", millis() / 1000.0, + time_to_set, t_msec % 1000); + +// if we have got an external timesource, set RTC time and shift RTC_INT pulse +// to top of second +#ifdef HAS_RTC + if ((mytimesource == _gps) || (mytimesource == _lora)) + set_rtctime(time_to_set); +#endif + +// if we have a software pps timer, shift it to top of second +#if (!defined GPS_INT && !defined RTC_INT) + timerWrite(ppsIRQ, 0); // reset pps timer + CLOCKIRQ(); // fire clock pps, this advances time 1 sec +#endif + + setTime(time_to_set); // set the time on top of second + + timeSource = mytimesource; // set global variable + timesyncer.attach(TIME_SYNC_INTERVAL * 60, timeSync); + ESP_LOGI(TAG, "[%0.3f] Timesync finished, time was set | source: %c", + millis() / 1000.0, timeSetSymbols[timeSource]); + } else { + timesyncer.attach(TIME_SYNC_INTERVAL_RETRY * 60, timeSync); + ESP_LOGI(TAG, "[%0.3f] Timesync failed, invalid time fetched | source: %c", + millis() / 1000.0, timeSetSymbols[timeSource]); + } +} + +// helper function to setup a pulse per second for time synchronisation +uint8_t timepulse_init() { + +// use time pulse from GPS as time base with fixed 1Hz frequency +#ifdef GPS_INT + + // setup external interupt pin for rising edge GPS INT + pinMode(GPS_INT, INPUT_PULLDOWN); + // setup external rtc 1Hz clock as pulse per second clock + ESP_LOGI(TAG, "Timepulse: external (GPS)"); + return 1; // success + +// use pulse from on board RTC chip as time base with fixed frequency +#elif defined RTC_INT + + // setup external interupt pin for falling edge RTC INT + pinMode(RTC_INT, INPUT_PULLUP); + + // setup external rtc 1Hz clock as pulse per second clock + if (I2C_MUTEX_LOCK()) { + Rtc.SetSquareWavePinClockFrequency(DS3231SquareWaveClock_1Hz); + Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeClock); + I2C_MUTEX_UNLOCK(); + ESP_LOGI(TAG, "Timepulse: external (RTC)"); + return 1; // success + } else { + ESP_LOGE(TAG, "RTC initialization error, I2C bus busy"); + return 0; // failure + } + return 1; // success + +#else + // use ESP32 hardware timer as time base with adjustable frequency + ppsIRQ = timerBegin(1, 8000, true); // set 80 MHz prescaler to 1/10000 sec + timerAlarmWrite(ppsIRQ, 10000, true); // 1000ms + ESP_LOGI(TAG, "Timepulse: internal (ESP32 hardware timer)"); + return 1; // success + +#endif +} // timepulse_init + +void timepulse_start(void) { + +#ifdef GPS_INT // start external clock gps pps line + attachInterrupt(digitalPinToInterrupt(GPS_INT), CLOCKIRQ, RISING); +#elif defined RTC_INT // start external clock rtc + attachInterrupt(digitalPinToInterrupt(RTC_INT), CLOCKIRQ, FALLING); +#else // start internal clock esp32 hardware timer + timerAttachInterrupt(ppsIRQ, &CLOCKIRQ, true); + timerAlarmEnable(ppsIRQ); +#endif + + // start cyclic time sync + timeSync(); // init systime by RTC or GPS or LORA + timesyncer.attach(TIME_SYNC_INTERVAL * 60, timeSync); +} + +// interrupt service routine triggered by either pps or esp32 hardware timer +void IRAM_ATTR CLOCKIRQ(void) { + + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + + SyncToPPS(); // advance systime, see microTime.h + +// advance wall clock, if we have +#if (defined HAS_IF482 || defined HAS_DCF77) + xTaskNotifyFromISR(ClockTask, uint32_t(now()), eSetBits, + &xHigherPriorityTaskWoken); +#endif + +// flip time pulse ticker, if needed +#ifdef HAS_DISPLAY +#if (defined GPS_INT || defined RTC_INT) + TimePulseTick = !TimePulseTick; // flip pulse ticker +#endif +#endif + + // yield only if we should + if (xHigherPriorityTaskWoken) + portYIELD_FROM_ISR(); +} + +// helper function to check plausibility of a time +time_t timeIsValid(time_t const t) { + // is it a time in the past? we use compile date to guess + return (t >= compiledUTC() ? t : 0); +} + +// helper function to convert compile time to UTC time +time_t compiledUTC(void) { + static time_t t = myTZ.toUTC(RtcDateTime(__DATE__, __TIME__).Epoch32Time()); + return t; +} + +// helper function to calculate serial transmit time +TickType_t tx_Ticks(uint32_t framesize, unsigned long baud, uint32_t config, + int8_t rxPin, int8_t txPins) { + + uint32_t databits = ((config & 0x0c) >> 2) + 5; + uint32_t stopbits = ((config & 0x20) >> 5) + 1; + uint32_t txTime = (databits + stopbits + 1) * framesize * 1000.0 / baud; + // +1 for the startbit + + return round(txTime); +} + +#if (defined HAS_IF482 || defined HAS_DCF77) + +#if (defined HAS_DCF77 && defined HAS_IF482) +#error You must define at most one of IF482 or DCF77! +#endif + +void clock_init(void) { + +// setup clock output interface +#ifdef HAS_IF482 + IF482.begin(HAS_IF482); +#elif defined HAS_DCF77 + pinMode(HAS_DCF77, OUTPUT); +#endif + + userUTCTime = now(); + + xTaskCreatePinnedToCore(clock_loop, // task function + "clockloop", // name of task + 2048, // stack size of task + (void *)&userUTCTime, // start time as task parameter + 4, // priority of the task + &ClockTask, // task handle + 1); // CPU core + + assert(ClockTask); // has clock task started? +} // clock_init + +void clock_loop(void *taskparameter) { // ClockTask + + // caveat: don't use now() in this task, it will cause a race condition + // due to concurrent access to i2c bus when reading/writing from/to rtc chip! + +#define nextmin(t) (t + DCF77_FRAME_SIZE + 1) // next minute + +#ifdef HAS_TWO_LED + static bool led1_state = false; +#endif + uint32_t printtime; + time_t t = *((time_t *)taskparameter), last_printtime = 0; // UTC time seconds + +#ifdef HAS_DCF77 + uint8_t *DCFpulse; // pointer on array with DCF pulse bits + DCFpulse = DCF77_Frame(nextmin(t)); // load first DCF frame before start +#elif defined HAS_IF482 + static TickType_t txDelay = pdMS_TO_TICKS(1000 - IF482_SYNC_FIXUP) - + tx_Ticks(IF482_FRAME_SIZE, HAS_IF482); +#endif + + // output the next second's pulse/telegram after pps arrived + for (;;) { + + // wait for timepulse and store UTC time in seconds got + xTaskNotifyWait(0x00, ULONG_MAX, &printtime, portMAX_DELAY); + t = time_t(printtime); + + // no confident or no recent time -> suppress clock output + if ((timeStatus() == timeNotSet) || !(timeIsValid(t)) || + (t == last_printtime)) + continue; + +#if defined HAS_IF482 + + // wait until moment to fire. Normally we won't get notified during this + // timespan, except when next pps pulse arrives while waiting, because pps + // was adjusted by recent time sync + if (xTaskNotifyWait(0x00, ULONG_MAX, &printtime, txDelay) == pdTRUE) + t = time_t(printtime); // new adjusted UTC time seconds + + // send IF482 telegram + IF482.print(IF482_Frame(t + 1)); // note: telegram is for *next* second + +#elif defined HAS_DCF77 + + if (second(t) == DCF77_FRAME_SIZE - 1) // is it time to load new frame? + DCFpulse = DCF77_Frame(nextmin(t)); // generate frame for next minute + + if (minute(nextmin(t)) == // do we still have a recent frame? + DCFpulse[DCF77_FRAME_SIZE]) // (timepulses could be missed!) + DCF77_Pulse(t, DCFpulse); // then output current second's pulse + + // else we have no recent frame, thus suppressing clock output + +#endif + +// pps blink on secondary LED if we have one +#ifdef HAS_TWO_LED + if (led1_state) + switch_LED1(LED_OFF); + else + switch_LED1(LED_ON); + led1_state = !led1_state; +#endif + + last_printtime = t; + + } // for +} // clock_loop() + #endif // HAS_IF482 || defined HAS_DCF77