/** BLE2Wifi_ESP8266Web_example.ino by Matthew Ford, 2022/04/23 (c)2022 Forward Computing and Control Pty. Ltd. NSW, Australia www.forward.com.au This code may be freely used for both private and commerical use. Provide this copyright is maintained. */ // here only the temperature reading is displayed, but all the values are parsed // This example is for ESP8266 boards -- Adafruit ESP8266 HUZZAH in this example // It is the WiFi side of a BLE2WiFi bridge for BLE very low power temperature sensors // see https://www.forward.com.au/pfod/BLE/LowPower_2022/Temp/index.html // Configure a staticIP so you can connect the the temperature server // Sensor advertised names start with // T_.. for Temp(C) sensors, H_.. for Temp(C),Humditiy(%) sensors, W_.. for Temp(C),Humdity(%),Barometer(hPa) sensors, WL_.. for Temp(C),Humdity(%),Barometer(hPa),Lux level(Lux) // keep this LOCAL_NAME short as it uses up available advertising data space (see below) // Format of advertized name // For T_.. devices LOCAL_NAME,degC // For H_.. devices LOCAL_NAME,degC,%RH // For W_.. devices LOCAL_NAME,degC,%RH,hPa // For WL_.. devices LOCAL_NAME,degC,%RH,hPa,Lux #include "ESPAutoWiFiConfig.h" // download ESPAutoWiFiConfig.zip from https://www.forward.com.au/pfod/ESPAutoWiFiConfig/index.html // includes ESP8266BufferedClient #include #include // install the SafeString library via the Arduino Library manager #include #include "millisDelay.h" // Download pfodParser library from http://www.forward.com.au/pfod/pfodParserLibraries/index.html #include // V2.31 or higher //#define DEBUG // settings for the Auto WiFi connect int ledPin = 0; // for Adafruit ESP8266 HUZZAH - onboard Led connected to GPIO0 // use ledPin = 2 for ESP-01S (Blue led on GPIO2) // use ledPin = 1 for ESP-01 (Blue led on TX (GPIO1), ESP-01 also has a Red power led) bool highForLedOn = false; // need to make output low to turn Adafruit ESP8266 HUZZAH's GPIO0 Led ON size_t eepromOffset = 40; // if you use EEPROM.begin(size) in your code add the size here so AutoWiFi data is written after your data cSF(sensorData, 50); // max name size is 32 so 50 is enough const char DEVICE_NAME[] = "T_1,"; // Note the , so that T_10 does not match T_1 const size_t MAX_BLE_NAME_SIZE = 32; char T_DEVICE[] = "T_"; // T_ devices have temp only (in degC) char H_DEVICE[] = "H_"; // H_ devices have temp(degC),RH(%) char W_DEVICE[] = "W_"; // W_ devices have temp(degC),RH(%),AirPressure(hPa) char WL_DEVICE[] = "WL_"; // WL_ devices have temp(degC),RH(%),AirPressure(hPa),Lux cSF(sfDevice, MAX_BLE_NAME_SIZE); cSF(sf_TempC, MAX_BLE_NAME_SIZE); cSF(sf_RH, MAX_BLE_NAME_SIZE); cSF(sf_hPa, MAX_BLE_NAME_SIZE); cSF(sf_lx, MAX_BLE_NAME_SIZE); float tempC, RH, hPa, lx; // storage for last 24hrs const size_t NO_DATA_POINTS = 870; // size 2 x (4*870bytes) = 7K // 100sec * 870 = 24.17hrs // these arrays are used as circular buffers // add one to array size to allow for startIdx != nextIdx // i.e. if startIdx = 5; and nextIdx = 4; then when next data point stored // nextIdx incremented to 5, and startIdx moved on to 6 so that startIdx is always 'in front' of endIdx const size_t ARRAY_SIZE = NO_DATA_POINTS + 1; float TempC_24hrs[ARRAY_SIZE]; // the filtered temperatures uint32_t Time_24hrs[ARRAY_SIZE]; // the time, in millis() for each temperature size_t startIdx = 0; // where to start outputing measurements, stop when startIdx == endIdx. Do not output the endIdx measurement. size_t nextIdx = 0; // the next idx to save measurement at. If nextIdx++ wrapped == startIdx, then increment startIdx. size_t printIdx = 0; // set to startIdx when plot screen opened stop sending when printIdx == endIdx bool sendingPlotData = false; // set true while sending to void double sends void sendFirstBlockOfReadings(); // increment idx and if >= ARRAY_SIZE, reset it to 0. void incrementIdx(size_t &idx) { idx++; if (idx >= ARRAY_SIZE) { idx = 0; } } float lastFilterValue = 0; float exponentialFactor = 0.2; float maxAbsError = 0.9; float filterRawTemp(float rawTemp) { float filterResult = exponentialFactor * rawTemp + (1 - exponentialFactor) * lastFilterValue; float filterError = rawTemp - filterResult; float absError = ((filterError) > 0 ? (filterError) : -(filterError)); if (absError > maxAbsError) { // step to current value filterResult = rawTemp; } lastFilterValue = filterResult; return filterResult; } // filter and add to array for plotting void addNewData(float _tempC) { TempC_24hrs[nextIdx] = filterRawTemp(_tempC); Time_24hrs[nextIdx] = millis(); incrementIdx(nextIdx); if (nextIdx == startIdx) { // have filled array move start on incrementIdx(startIdx); } } void addData(float _tempC) { if ((nextIdx == startIdx) && (startIdx == 0)) { // else no data yet fill with initial value while (startIdx == 0) { addNewData(_tempC); } return; } // else have initialized data just add this one addNewData(_tempC); } // from pfodDesigner /* ===== pfod Command for Menu_2 ==== pfodApp msg {.} --> {,<+3>~BLE Temperature Sensor`0~V1|!A<+10>~T ℃|B<+7>~Plot of Last 24hrs} */ // Using ESP8266 based board programmed via Arduino IDE // follow the steps given on https://github.com/esp8266/arduino under Installing With Boards Manager // You need to modify the WLAN_SSID, WLAN_PASS settings below // to match your network settings /* Code generated by pfodDesignerV3 V3.0.3931 */ /* (c)2014-2021 Forward Computing and Control Pty. Ltd. NSW Australia, www.forward.com.au This code is not warranted to be fit for any purpose. You may only use it at your own risk. This generated code may be freely used for both private and commercial use provided this copyright is maintained. */ //#include // included above // Download pfodParser library from http://www.forward.com.au/pfod/pfodParserLibraries/index.html //#include // V2.31 or higher // included above // download the libraries from http://www.forward.com.au/pfod/pfodParserLibraries/index.html int swap01(int); // method prototype for slider end swaps // plotVarScaling not used pfodSecurity parser("V1"); // create a parser with menu version string to handle the pfod messages ESPBufferedClient bufferedClient; const int portNo = 4989; // What TCP port to listen on for connections. //not used const char staticIP[] = ""; // set this the static IP you want, e.g. "10.1.1.200" or leave it as "" for DHCP. DHCP is not recommended. // handled by ESPAutoWiFiConfig // add your pfod Password here for 128bit security // eg "b0Ux9akSiwKkwCtcnjTnpWp" but generate your own key, "" means no pfod password #define pfodSecurityCode "" // see http://www.forward.com.au/pfod/ArduinoWiFi_simple_pfodDevice/index.html for more information and an example // and QR image key generator. WiFiServer server(portNo); WiFiClient client; WiFiClient* clientPtr; // non-null if have connected client unsigned long plot_msOffset = 0; // set by {@} response bool clearPlot = false; // set by the {@} response code // the setup routine runs once on reset: void setup() { // setup code that must be run every time Serial.begin(115200); Serial.println(); #ifdef DEBUG for (int i = 10; i > 0; i--) { Serial.print(i); Serial.print(' '); delay(500); } Serial.println(); #endif SafeString::setOutput(Serial); #ifdef DEBUG setESPAutoWiFiConfigDebugOut(Serial); // turns on debug output for the ESPAutoWiFiConfig code #endif if (ESPAutoWiFiConfigSetup(ledPin, highForLedOn, eepromOffset)) { // check if we should start access point to configure WiFi settings return; // in config mode so skip rest of setup } // when we get here the WiFi has connected !! // normal setup code that is run when not configuring the WiFi settings // Start the server server.begin(); #ifdef DEBUG Serial.println("Server started"); #endif // Print the IP address #ifdef DEBUG Serial.println(WiFi.localIP()); #endif // <<<<<<<<< Your extra setup code goes here } // the loop routine runs over and over again forever: void loop() { // .. other code that MUST run all the time if (ESPAutoWiFiConfigLoop()) { // handle WiFi config webpages return; // skip the rest of the loop until config finished } // .. normal loop code here that is run when not configuring the WiFi settings // check Serial for new data if (clientPtr && (!clientPtr->connected())) { closeConnection(parser.getPfodAppStream()); } if (server.hasClient()) { // new connection if (!clientPtr) { // no existing connection client = server.available(); // get any new client clientPtr = &client; // have connected client parser.connect(bufferedClient.connect(clientPtr), F(pfodSecurityCode)); // sets new io stream to read from and write to EEPROM.commit(); // does nothing if nothing to do } else { // already have client so just stop the new one WiFiClient newClient = server.available(); // get any new client newClient.stop(); } } if (clientPtr) { uint8_t cmd = parser.parse(); // parse incoming data from connection // parser returns non-zero when a pfod command is fully parsed if (cmd != 0) { // have parsed a complete msg { to } uint8_t* pfodFirstArg = parser.getFirstArg(); // may point to \0 if no arguments in this msg. pfod_MAYBE_UNUSED(pfodFirstArg); // may not be used, just suppress warning long pfodLongRtn; // used for parsing long return arguments, if any pfod_MAYBE_UNUSED(pfodLongRtn); // may not be used, just suppress warning if ('.' == cmd) { // pfodApp has connected and sent {.} , it is asking for the main menu if (!parser.isRefresh()) { sendMainMenu(); // send back the menu designed } else { sendMainMenuUpdate(); // menu is cached just send update } // handle {@} request } else if ('@' == cmd) { // pfodApp requested 'current' time plot_msOffset = millis(); // capture current millis as offset rawdata timestamps clearPlot = true; parser.print(F("{@`0}")); // return `0 as 'current' raw data milliseconds // now handle commands returned from button/sliders // } else if('A'==cmd) { // this is a label. pfodApp NEVER sends this cmd -- 'T ℃' // // in the main Menu of Menu_2 } else if ('B' == cmd) { // user pressed -- 'Plot of Last 24hrs' // in the main Menu of Menu_2 // return plotting msg. parser.print(F("{=T1 Temperature (24hrs)`")); parser.print(NO_DATA_POINTS); // plot all the data, default is to only plot last 500 parser.print(F("~E HH:mm")); if (!sendingPlotData) { parser.print(F("~C")); // clear last plot data } parser.print(F("|date|Temperature~35~5~\342\204\203||}")); startDataSend(); } else if ('!' == cmd) { // CloseConnection command closeConnection(parser.getPfodAppStream()); } else { // unknown command parser.print(F("{}")); // always send back a pfod msg otherwise pfodApp will disconnect. } } sendData(); } // <<<<<<<<<<< Your other loop() code goes here if (sensorData.readUntil(Serial, '\n')) { // have a new line or sensorData filled up sensorData.trim(); if (sensorData.length() > MAX_BLE_NAME_SIZE) { // not advertising data just echo #ifdef DEBUG Serial.print("Input not sensor data -- "); Serial.println(sensorData); #endif } else { if (checkValidName(sensorData)) { if (decode(sensorData)) { addData(tempC); // uses the gobal tempC } } } sensorData.clear(); // for next read } } void closeConnection(Stream *io) { (void)(io); // unused // add any special code here to force connection to be dropped parser.closeConnection(); // nulls io stream bufferedClient.stop(); // clears client reference clientPtr->stop(); clientPtr = NULL; } // replaced by sendData() below to send a block of data at a time. //void sendData() { // if (plotDataTimer.justFinished()) { // plotDataTimer.repeat(); // restart plot data timer, without drift // //plot_1_var; // use tempC directly without scaling // // plot_2_var plot Hidden so no data assigned here // // plot_3_var plot Hidden so no data assigned here // // send plot data in CSV format // parser.print(millis() - plot_msOffset); // time in milliseconds // parser.print(','); parser.print(tempC, 1); // parser.print(','); // Plot 2 is hidden. No data sent. // parser.print(','); // Plot 3 is hidden. No data sent. // parser.println(); // end of CSV data record // } //} void sendMainMenu() { // !! Remember to change the parser version string // every time you edit this method parser.print(F("{,")); // start a Menu screen pfod message // send menu background, format, prompt, refresh and version parser.print(F("<+3>~T_1 Temperature\nSensor`")); parser.print("15000"); // refresh 15sec parser.sendVersion(); // send the menu version // send menu items parser.print(F("|!A<+10>")); parser.print(F("~")); parser.print(tempC, 1); parser.print(F("\342\204\203")); parser.print(F("|B<+7>")); parser.print(F("~Plot of\nLast 24hrs")); parser.print(F("}")); // close pfod message } void sendMainMenuUpdate() { parser.print(F("{;")); // start an Update Menu pfod message // send menu items parser.print(F("|!A<+10>")); parser.print(F("~")); parser.print(tempC, 1); parser.print(F("\342\204\203")); parser.print(F("|B")); parser.print(F("}")); // close pfod message // ============ end of menu =========== } int swap01(int in) { return (in == 0) ? 1 : 0; } // ============= end generated code ========= void startDataSend() { if (sendingPlotData) { return; // already sending } sendingPlotData = true; printIdx = startIdx; } cSF(sfData, 50); // large enough for millis(),temperature,,\r\n // sends one block of data at a time void sendData() { if (!sendingPlotData) { return; } while (printIdx != nextIdx) { sfData.clear(); sfData.print((int32_t)(Time_24hrs[printIdx] - plot_msOffset)); // time in mS +/-, previous times are -ve sfData.print(','); sfData.print(TempC_24hrs[printIdx]); sfData.print(','); // Plot 2 is hidden. No data sent. sfData.print(','); // Plot 3 is hidden. No data sent. sfData.println(); // end of CSV data record if (bufferedClient.availableForWrite() < ((int)sfData.length())) { // availableForWrite is an int !!*@# parser.flush(); // blocks here until WiFi send this block of data. break; // do not increment printIdx, send it next time } parser.print(sfData); // max size ~30chars 10+sign for ms, 1 for comma, 6+sign for degC, 7 for padding, 2 for newline = 29 incrementIdx(printIdx); // have sent this one } if (printIdx == nextIdx) { sendingPlotData = false; // can start again } } // just record T_1, temps bool checkValidName(SafeString &sfName) { //return (sfName.startsWith(T_DEVICE) || sfName.startsWith(H_DEVICE) || sfName.startsWith(W_DEVICE) || sfName.startsWith(WL_DEVICE)); return sfName.startsWith(DEVICE_NAME); } bool decode(SafeString &sfName) { sfName.trim(); if (!checkValidName(sfName)) { return false; } // no need to clear the previous tokens as nextToken does that if (sfName.startsWith(T_DEVICE)) { // skip first field and pick up temp sfName.nextToken(sfDevice, ','); // remove name sfName.nextToken(sf_TempC, ','); // pick up temp #ifdef DEBUG Serial.print(sfDevice); Serial.print(" Temperature:"); Serial.print(sf_TempC); Serial.println("C"); #endif // convert Temp if (!sf_TempC.toFloat(tempC)) { return false; // last tempC unchanged } } else if (sfName.startsWith(H_DEVICE)) { // skip first field and pick up temp sfName.nextToken(sfDevice, ','); // remove name sfName.nextToken(sf_TempC, ','); // pickup Temp sfName.nextToken(sf_RH, ','); // pickup RH #ifdef DEBUG Serial.print(sfDevice); Serial.print(" Temperature:"); Serial.print(sf_TempC); Serial.print("C"); Serial.print(" RH:"); Serial.print(sf_RH); Serial.print("%"); Serial.println(); #endif if (!sf_TempC.toFloat(tempC)) { return false; // not a valid float, last tempC unchanged } if (!sf_RH.toFloat(RH)) { return false; // not a valid float, last RH unchanged } } else if (sfName.startsWith(W_DEVICE)) { // skip first field and pick up temp sfName.nextToken(sfDevice, ','); // remove name sfName.nextToken(sf_TempC, ','); // pickup Temp sfName.nextToken(sf_RH, ','); // pickup RH sfName.nextToken(sf_hPa, ','); // pickup Air Pressure #ifdef DEBUG Serial.print(sfDevice); Serial.print(" Temperature:"); Serial.print(sf_TempC); Serial.print("C"); Serial.print(" RH:"); Serial.print(sf_RH); Serial.print("%"); Serial.print(" Barometer:"); Serial.print(sf_hPa); Serial.print(" hPa"); Serial.println(); #endif if (!sf_TempC.toFloat(tempC)) { return false; // not a valid float, last tempC unchanged } if (!sf_RH.toFloat(RH)) { return false; // not a valid float, last RH unchanged } if (!sf_hPa.toFloat(hPa)) { return false; // not a valid float, last hPA unchanged } } else if (sfName.startsWith(W_DEVICE)) { // skip first field and pick up temp sfName.nextToken(sfDevice, ','); // remove name sfName.nextToken(sf_TempC, ','); // pickup Temp sfName.nextToken(sf_RH, ','); // pickup RH sfName.nextToken(sf_hPa, ','); // pickup Air Pressure sfName.nextToken(sf_lx, ','); // pickup Air Pressure #ifdef DEBUG Serial.print(sfDevice); Serial.print(" Temperature:"); Serial.print(sf_TempC); Serial.print("C"); Serial.print(" RH:"); Serial.print(sf_RH); Serial.print("%"); Serial.print(" Barometer:"); Serial.print(sf_hPa); Serial.print(" hPa"); Serial.print(" Lux:"); Serial.print(sf_lx); Serial.print(" lx"); Serial.println(); #endif if (!sf_TempC.toFloat(tempC)) { return false; // not a valid float, last tempC unchanged } if (!sf_RH.toFloat(RH)) { return false; // not a valid float, last RH unchanged } if (!sf_hPa.toFloat(hPa)) { return false; // not a valid float, last hPA unchanged } if (!sf_lx.toFloat(lx)) { return false; // not a valid float, last lx unchanged } } else { #ifdef DEBUG Serial.print("Error: Device not supported -- "); Serial.println(sfName); #endif return false; } return true; }