diff --git a/tasmota/support_flash_log.ino b/tasmota/support_flash_log.ino
new file mode 100644
index 000000000..8d02ec2e8
--- /dev/null
+++ b/tasmota/support_flash_log.ino
@@ -0,0 +1,432 @@
+/*
+ support_flash_log.ino - log to flash support for Sonoff-Tasmota
+
+ Copyright (C) 2019 Theo Arends & Christian Baars
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+ --------------------------------------------------------------------------------------------
+ Version Date Action Description
+ --------------------------------------------------------------------------------------------
+
+
+ ---
+ 1.0.0.0 20190923 started - further development by Christian Baars - https://github.com/Staars/Sonoff-Tasmota
+ forked - from arendst/tasmota - https://github.com/arendst/Sonoff-Tasmota
+ base - code base from arendst and - written from scratch
+
+*/
+
+/********************************************************************************************\
+| * Generic helper class to log arbitrary data to the OTA-partition
+| * Working principle: Add preferrable small chunks of data to the sector buffer, which will
+| * be written to FLASH when full automatically. The next sector will be
+| * erased and is the anchor point for downloading and state configuration
+| * after reboot.
+\*********************************************************************************************/
+
+#ifdef USE_FLOG
+
+class FLOG
+
+#define MAGIC_WORD_FL 0x464c //F, L
+
+{
+
+struct header_t{
+ uint16_t magic_word; // FL
+ uint16_t padding; // leave something for the future
+ uint32_t physical_start_sector:10; //first used sector of the current FLOG
+ uint32_t number:10; // number of this sector, starting with 0 for the first sector
+ uint32_t buf_pointer:12; //internal pointer to the next free position in the buffer = first empty byte when reading
+ }; // should be 4-byte-aligned
+
+private:
+void _readSector(uint8_t one_sector);
+void _eraseSector(uint8_t one_sector);
+void _writeSector(uint8_t one_sector);
+void _clearBuffer(void);
+void _searchSaves(void);
+void _findFirstErasedSector(void);
+void _showBuffer(void);
+void _initBuffer(void);
+void _saveBufferToSector(void);
+header_t _saved_header;
+
+public:
+ uint32_t size; // size of OTA-partition
+ uint32_t start; // start position of OTA-partition in bytes
+ uint32_t end; // end position of OTA-partition in bytes
+ uint16_t num_sectors; // calculated number of sectors with a size of 4096 bytes
+
+ uint16_t first_erased_sector; // this will be our new start
+ uint16_t current_sector; // always point to next sector, where data from the buffer will be written to
+
+ uint16_t bytes_left; // byte per buffer (of sector size 4096 bytes - 8 byte header size)
+ uint16_t sectors_left; // number of saved sectors for download
+
+ uint8_t mode = 0; // 0 - write once on all sectors, then stop, 1 - write infinitely through the sectors
+ bool found_saved_data = false; // possible saved data has been found
+ bool ready = false; // the FLOG is initialized
+ bool running_download = false; // a download operation is running
+ bool recording = false; // ready for recording
+
+ union sector_t{
+ uint32_t dword_buffer[FLASH_SECTOR_SIZE/4];
+ uint8_t byte_buffer[FLASH_SECTOR_SIZE];
+ header_t header; // should be 4-byte-aligned
+ } sector; // the global buffer of 4096 bytes, used for reading and writing
+
+ void init(void);
+ void addToBuffer(uint8_t src[], uint32_t size);
+ void startRecording(bool append);
+ void stopRecording(void);
+
+ typedef void (*CallbackNoArgs) (); // simple typedef for a callback
+ typedef void (*CallbackWithArgs) (uint8_t *_record); // typedef for a callback with one argument
+
+ void startDownload(size_t size, CallbackNoArgs sendHeader, CallbackWithArgs sendRecord, CallbackNoArgs sendFooter);
+};
+
+extern "C" uint32_t _SPIFFS_start; // we make shure later, that only one of the two is really used ...
+extern "C" uint32_t _FS_start; // ... depending on core-sdk-version
+
+/**
+ * @brief Will examine the start and end of the OTA-partition. Then the sector size will be computed, saved data should be found and the initial state will be configured.
+ */
+void FLOG::init(void)
+{
+DEBUG_SENSOR_LOG(PSTR("FLOG: init ..."));
+size = ESP.getSketchSize();
+// round one sector up
+start = (size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
+#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_1) || defined(ARDUINO_ESP8266_RELEASE_2_4_2) || defined(ARDUINO_ESP8266_RELEASE_2_5_0) || defined(ARDUINO_ESP8266_RELEASE_2_5_1) || defined(ARDUINO_ESP8266_RELEASE_2_5_2)
+end = (uint32_t)&_SPIFFS_start - 0x40200000;
+#else // Core > 2.5.2 and STAGE
+end = (uint32_t)&_FS_start - 0x40200000;
+#endif
+num_sectors = (end - start)/FLASH_SECTOR_SIZE;
+DEBUG_SENSOR_LOG(PSTR("FLOG: size: 0x%lx, start: 0x%lx, end: 0x%lx, num_sectors(dec): %lu"), size, start, end, num_sectors );
+_findFirstErasedSector();
+if(first_erased_sector == 0xffff){
+ _eraseSector(0);
+ first_erased_sector = 0; // start with sector 0, could be first run or after crash
+}
+_searchSaves();
+_initBuffer();
+ready = true;
+}
+
+/********************************************************************************************\
+| *
+| * private helper functions
+| *
+\*********************************************************************************************/
+
+/**
+ * @brief Read a sector into the global buffer
+ *
+ * @param one_sector as an uint8_t
+ */
+void FLOG::_readSector(uint8_t one_sector){
+ DEBUG_SENSOR_LOG(PSTR("FLOG: read sector number: %u" ), one_sector);
+ ESP.flashRead(start+(one_sector * FLASH_SECTOR_SIZE),(uint32_t *)§or.dword_buffer, FLASH_SECTOR_SIZE);
+}
+/**
+ * @brief Erase the given sector og the OTA-partition
+ *
+ * @param one_sector as an uint8_t
+ */
+void FLOG::_eraseSector(uint8_t one_sector){ // Erase sector of FLOG/OTA
+ DEBUG_SENSOR_LOG(PSTR("FLOG: erasing sector number: %u" ), one_sector);
+ ESP.flashEraseSector((start/FLASH_SECTOR_SIZE)+one_sector);
+}
+/**
+ * @brief Write the global buffer to the given sector
+ *
+ * @param one_sector as an uint8_t
+ */
+void FLOG::_writeSector(uint8_t one_sector){ // Write sector of FLOG/OTA
+ DEBUG_SENSOR_LOG(PSTR("FLOG: write buffer to sector number: %u" ), one_sector);
+ ESP.flashWrite(start+(one_sector * FLASH_SECTOR_SIZE),(uint32_t *)§or.dword_buffer, FLASH_SECTOR_SIZE);
+}
+/**
+ * @brief Clear the global buffer, but leave the header intact
+ *
+ */
+void FLOG::_clearBuffer(){ //not the header
+ for (uint32_t i = sizeof(sector.header)/4; i<(sizeof(sector.dword_buffer)/4); i++){
+ sector.dword_buffer[i] = 0;
+ }
+ sector.header.buf_pointer = sizeof(sector.header);
+ // _showBuffer();
+}
+/**
+ * @brief Write global buffer to FLASH and set the current sector to the next valid position, maybe to 0
+ *
+ */
+void FLOG::_saveBufferToSector(){ // save buffer to already erased(!) sector, erase next sector, clear buffer, increment number
+ DEBUG_SENSOR_LOG(PSTR("FLOG: write buffer to current sector: %u" ),current_sector);
+ _writeSector(current_sector);
+ if(current_sector == num_sectors){ // 1 MB means ~110 sectors in OTA-partition, if we reach this, start a again
+ current_sector = 0;
+ }
+ else{
+ current_sector++;
+ }
+ _eraseSector(current_sector); // we always erase the next sector, to find out were we are after restart
+ _clearBuffer();
+ sector.header.number++;
+ DEBUG_SENSOR_LOG(PSTR("FLOG: new sector header number: %u" ),sector.header.number);
+}
+
+/**
+ * @brief Typically after restart find the first erased sector as a starting point for further operations
+ *
+ */
+void FLOG::_findFirstErasedSector(){
+ for (uint32_t i = 0; i3){
+ break;
+ }
+ }
+}
+
+/**
+ * @brief pass a data entry/record as uint8_t array with its size
+ *
+ * @param src uint8_t array
+ * @param size uint32_t size of the array
+ */
+void FLOG::addToBuffer(uint8_t src[], uint32_t size){
+ if(mode == 0){
+ if(sector.header.number == num_sectors && !ready){
+ return; // we ignore additional calls and are done, TODO: maybe use meaningful return values
+ }
+ }
+ if((FLASH_SECTOR_SIZE-sector.header.buf_pointer-sizeof(sector.header))>size){
+ // DEBUG_SENSOR_LOG(PSTR("FLOG: enough space left in buffer: %u"), FLASH_SECTOR_SIZE - sector.header.buf_pointer - sizeof(sector.header));
+ // DEBUG_SENSOR_LOG(PSTR("FLOG: current buf_pointer: %u, size of added: %u"), sector.header.buf_pointer, size);
+
+ memcpy(sector.byte_buffer + sector.header.buf_pointer, src, size);
+ sector.header.buf_pointer+=size; // this is the next free spot
+ // DEBUG_SENSOR_LOG(PSTR("FLOG: current buf_pointer: %u"), sector.header.buf_pointer);
+ }
+ else{
+ DEBUG_SENSOR_LOG(PSTR("FLOG: save buffer to flash sector: %u"), current_sector);
+ DEBUG_SENSOR_LOG(PSTR("FLOG: current buf_pointer: %u"), sector.header.buf_pointer);
+ _saveBufferToSector();
+ sectors_left++;
+ // but now save the data to the fresh buffer
+ if((FLASH_SECTOR_SIZE-sector.header.buf_pointer-sizeof(sector.header))>size){
+ memcpy(sector.byte_buffer + sector.header.buf_pointer, src, size);
+ sector.header.buf_pointer+=size; // this is the next free spot
+ }
+ }
+}
+
+/**
+ * @brief shows that it is ready to accept recording
+ *
+ * @param append - if true append to current log, else start a new log
+ */
+void FLOG::startRecording(bool append){
+ if(recording){
+ DEBUG_SENSOR_LOG(PSTR("FLOG: already recording"));
+ return;
+ }
+ recording = true;
+ DEBUG_SENSOR_LOG(PSTR("FLOG: start recording"));
+ _initBuffer();
+ if(!found_saved_data) {
+ append = false; // nothing to append to, we silently start a new log
+ }
+ if(append){
+ sector.header.number = _saved_header.number+1; // continue with the next number
+ sector.header.physical_start_sector = _saved_header.physical_start_sector; // keep the old start sector
+ }
+ else{ //new log, old data is lost
+ sector.header.physical_start_sector = (uint16_t)first_erased_sector;
+ found_saved_data = false;
+ sectors_left = 0;
+ }
+ }
+
+/**
+ * @brief stop recording including saving current buffer to FLASH
+ *
+ */
+void FLOG::stopRecording(void){
+ _saveBufferToSector();
+ _findFirstErasedSector();
+ _searchSaves();
+ _initBuffer();
+ recording = false;
+ found_saved_data = true;
+ }
+
+/**
+ * @brief Will start a downloads, needs the correct implementation of 3 callback functions
+ *
+ * @param size: size of the data entry/record in bytes, i.e. sizeof(myStruct)
+ * @param sendHeader: should implement at least something like:
+ * @example WebServer->setContentLength(CONTENT_LENGTH_UNKNOWN); // This is very likely unknown!!
+ * WebServer->sendHeader(F("Content-Disposition"), F("attachment; filename=myfile.txt"));
+ * @param sendRecord: will receive the memory address as "uint8_t* addr" and should consume the current entry/record
+ * @example myStruct_t *entry = (myStruct_t*)addr;
+ * Then make useful Strings and send it, i.e.: WebServer->sendContent_P(myString);
+ * @param sendFooter: finish the download, should implement at least:
+ * @example WebServer->sendContent("");
+ */
+ void FLOG::startDownload(size_t size, CallbackNoArgs sendHeader, CallbackWithArgs sendRecord, CallbackNoArgs sendFooter){
+
+ _readSector(sector.header.physical_start_sector);
+ uint32_t next_sector = sector.header.physical_start_sector;
+ bytes_left = sector.header.buf_pointer - sizeof(sector.header);
+ DEBUG_SENSOR_LOG(PSTR("FLOG: create file for download, will process %u bytes"), bytes_left);
+ running_download = true;
+ // Callback 1: Create the header incl. file name, content length (probably unknown!!) and additional header stuff
+ sendHeader();
+
+ while(sectors_left){
+ DEBUG_SENSOR_LOG(PSTR("FLOG: next sector: %u"), next_sector);
+ //initially we have the first sector already loaded, so we do it at the bottom
+ uint32_t k = sizeof(sector.header);
+ while(bytes_left){
+ // DEBUG_SENSOR_LOG(PSTR("FLOG: DL %u %u"), Flog->sector.byte_buffer[k],Flog->sector.byte_buffer[k+1]);
+ uint8_t *_record_start = (uint8_t*)§or.byte_buffer[k]; // this is basically the start address of the current record/entry of the Log
+ // Callback 2: send the pointer for consuming the next record/entry and doing something useful to create a file
+ sendRecord(_record_start);
+ if(k%128 == 0){ // give control to the system every x iteration, TODO: This will fail, when record/entry-size is not 8
+ // DEBUG_SENSOR_LOG(PSTR("FLOG: now loop(), %u bytes left"), Flog->bytes_left);
+ OsWatchLoop();
+ delay(sleep);
+ }
+ k+=size;
+ if(bytes_left>7){
+ bytes_left-=size;
+ }
+ else{
+ bytes_left = 0;
+ DEBUG_SENSOR_LOG(PSTR("FLOG: Flog->bytes_left not dividable by 8 ??????"));
+ }
+ }
+ next_sector++;
+ if(next_sector>num_sectors){
+ next_sector = 0;
+ }
+ sectors_left--;
+ _readSector(next_sector);
+ bytes_left = sector.header.buf_pointer - sizeof(sector.header);
+ OsWatchLoop();
+ delay(sleep);
+ }
+ running_download = false;
+ // Callback 3: create a footer or simply finish the download with an empty payload
+ sendFooter();
+ // refresh settings for another download
+ _searchSaves();
+ _initBuffer();
+ }
+
+ #endif // USE_FLOG
\ No newline at end of file
diff --git a/tasmota/xsns_58_GPS.ino b/tasmota/xsns_58_GPS.ino
new file mode 100644
index 000000000..f2808f8fd
--- /dev/null
+++ b/tasmota/xsns_58_GPS.ino
@@ -0,0 +1,858 @@
+/*
+ xsns_92_GPS_UBX.ino - GPS UBLOX support for Sonoff-Tasmota
+
+ Copyright (C) 2019 Theo Arends, Christian Baars and Adrian Scillato
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+ --------------------------------------------------------------------------------------------
+ Version Date Action Description
+ --------------------------------------------------------------------------------------------
+
+ 0.9.1.0 20191216 integrate - Added pin specifications from Tasmota WEB UI
+ ---
+ 0.9.0.0 20190817 started - further development by Christian Baars - https://github.com/Staars/Sonoff-Tasmota
+ forked - from arendst/tasmota - https://github.com/arendst/Sonoff-Tasmota
+ base - code base from arendst and - https://www.youtube.com/watch?v=TwhCX0c8Xe0
+
+## GPS-driver for the Ublox-series 6-8
+Driver is tested on a NEO-6m and a Beitian-220. Series 7 should work too. This adds only about 6kb to the program size, because the efficient UBX-protocol is used. These modules are quite cheap, starting at about 3.50€ for the NEO-6m.
+
+## Features:
+- get position and time data
+- sets system time automatically and Settings.latitude and Settings.longitude via command
+- can log postion data with timestamp to flash with a small memory footprint of only 12 Bytes per record
+- constructs a GPX-file for download of this data
+- Web-UI
+- simplified NTP-server
+- command interface
+
+## Usage:
+The serial pins are GPX_RX and GPS_TX, no further installation steps needed. To get more debug information compile it with option "DEBUG_TASMOTA_SENSOR".
+
+
+## Commands:
+
++ sensor92 0
+ write to all available sectors, then restart and overwrite the older ones
+
++ sensor92 1
+ write to all available sectors, then restart and overwrite the older ones
+
++ sensor92 2
+ filter out horizontal drift noise
+
++ sensor92 3
+ turn off noise filter
+
++ sensor92 4
+ start recording, new data will be appended
+
++ sensor92 5
+ start new recording, old data will lost
+
++ sensor92 6
+ stop recording, download link will be visible in Web-UI
+
++ sensor92 7
+ send mqtt on new postion + TELE -> consider to set TELE to a very high value
+
++ sensor92 8
+ only TELE message
+
++ sensor92 9
+ start NTP-server
+
++ sensor92 10
+ deactivate NTP-server
+
++ sensor92 11
+ force update of Tasmota-system-UTC with every new GPS-time-message
+
++ sensor92 12
+ do not update of Tasmota-system-UTC with every new GPS-time-message
+
++ sensor92 13
+ set latitude and longitude in settings
+
+
+
+## Rules examples for SSD1306 32x128
+
+
+rule1 on tele-GPS#lat do DisplayText [s1p21c1l01f1]LAT: %value% endon on tele-GPS#lon do DisplayText [s1p21c1l2]LON: %value% endon on switch1#state==3 do sensor92 4 endon on switch1#state==2 do sensor92 6 endon
+
+rule2 on tele-GPS#int>9 do DisplayText [f0c9l4]I%value% endon on tele-GPS#int<10 do DisplayText [f0c9l4]I0%value% endon on tele-GPS#fil==1 do DisplayText [f0c18l4]F endon on tele-GPS#fil==0 do DisplayText [f0c18l4]N endon
+
+rule3 on tele-FLOG#sec do DisplayText [f0c1l4]SAV:%value% endon on tele-FLOG#rec==1 do DisplayText [f0c1l4]REC: endon on tele-FLOG#mode do DisplayText [f0c14l4]M%value% endon
+
+*/
+
+#ifdef USE_GPS
+
+#include "NTPServer.h"
+#include "NTPPacket.h"
+
+
+/*********************************************************************************************\
+ * constants
+\*********************************************************************************************/
+
+#define D_CMND_UBX "UBX"
+
+const char S_JSON_UBX_COMMAND_NVALUE[] PROGMEM = "{\"" D_CMND_UBX "%s\":%d}";
+
+const char kUBXTypes[] PROGMEM = "UBX";
+
+#define UBX_LAT_LON_THRESHOLD 1000 // filter out some noise of local drift
+
+
+/********************************************************************************************\
+| *globals
+\*********************************************************************************************/
+
+const char UBLOX_INIT[] PROGMEM = {
+ // Disable NMEA
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x24, // GxGGA off
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0xF0,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x2B, // GxGLL off
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0xF0,0x02,0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x32, // GxGSA off
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0xF0,0x03,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x39, // GxGSV off
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0xF0,0x04,0x00,0x00,0x00,0x00,0x00,0x01,0x04,0x40, // GxRMC off
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0xF0,0x05,0x00,0x00,0x00,0x00,0x00,0x01,0x05,0x47, // GxVTG off
+
+ // Disable UBX
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0x01,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x17,0xDC, //NAV-PVT off
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0x01,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x12,0xB9, //NAV-POSLLH off
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0x01,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xC0, //NAV-STATUS off
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0x01,0x21,0x00,0x00,0x00,0x00,0x00,0x00,0x31,0x92, //NAV-TIMEUTC off
+
+ // Enable UBX
+ // 0xB5,0x62,0x06,0x01,0x08,0x00,0x01,0x07,0x00,0x01,0x00,0x00,0x00,0x00,0x18,0xE1, //NAV-PVT on
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0x01,0x02,0x00,0x01,0x00,0x00,0x00,0x00,0x13,0xBE, //NAV-POSLLH on
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0x01,0x03,0x00,0x01,0x00,0x00,0x00,0x00,0x14,0xC5, //NAV-STATUS on
+ 0xB5,0x62,0x06,0x01,0x08,0x00,0x01,0x21,0x00,0x01,0x00,0x00,0x00,0x00,0x32,0x97, //NAV-TIMEUTC on
+
+ // Rate - we will not reset it for the moment after restart
+ // 0xB5,0x62,0x06,0x08,0x06,0x00,0x64,0x00,0x01,0x00,0x01,0x00,0x7A,0x12, //(10Hz)
+ // 0xB5,0x62,0x06,0x08,0x06,0x00,0xC8,0x00,0x01,0x00,0x01,0x00,0xDE,0x6A, //(5Hz)
+ // 0xB5,0x62,0x06,0x08,0x06,0x00,0xE8,0x03,0x01,0x00,0x01,0x00,0x01,0x39 //(1Hz)
+ // 0xB5,0x62,0x06,0x08,0x06,0x00,0xD0,0x07,0x01,0x00,0x01,0x00,0xED,0xBD //(0.5Hz)
+};
+
+char UBX_name[4];
+
+struct UBX_t{
+ const char UBX_HEADER[2] = { 0xB5, 0x62 }; // TODO: Check if we really save space here inside the struct
+ const char NAV_POSLLH_HEADER[2] = { 0x01, 0x02 };
+ const char NAV_STATUS_HEADER[2] = { 0x01, 0x03 };
+ const char NAV_TIME_HEADER[2] = { 0x01, 0x21 };
+
+ struct entry_t{
+ int32_t lat; //raw sensor value
+ int32_t lon; //raw sensor value
+ uint32_t time; //local time from system (maybe provided by the sensor)
+ };
+
+ union {
+ entry_t values;
+ uint8_t bytes[sizeof(entry_t)];
+ } rec_buffer;
+
+ struct POLL_MSG {
+ uint8_t cls;
+ uint8_t id;
+ uint16_t zero;
+ };
+
+ struct NAV_POSLLH {
+ uint8_t cls;
+ uint8_t id;
+ uint16_t len;
+ uint32_t iTOW;
+ int32_t lon;
+ int32_t lat;
+ int32_t height;
+ int32_t hMSL;
+ uint32_t hAcc;
+ uint32_t vAcc;
+ };
+
+ struct NAV_STATUS {
+ uint8_t cls;
+ uint8_t id;
+ uint16_t len;
+ uint32_t iTOW;
+ uint8_t gpsFix;
+ uint8_t flags; //bit 0 - gpsfix valid
+ uint8_t fixStat;
+ uint8_t flags2;
+ uint32_t ttff;
+ uint32_t msss;
+ };
+
+ struct NAV_TIME_UTC {
+ uint8_t cls;
+ uint8_t id;
+ uint16_t len;
+ uint32_t iTOW;
+ uint32_t tAcc;
+ int32_t nano; // Nanoseconds of second, range -1e9 .. 1e9 (UTC)
+ uint16_t year;
+ uint8_t month;
+ uint8_t day;
+ uint8_t hour;
+ uint8_t min;
+ uint8_t sec;
+ struct {
+ uint8_t UTC:1;
+ uint8_t WKN:1; // week number
+ uint8_t TOW:1; // time of week
+ uint8_t padding:5;
+ } valid;
+ };
+
+ struct CFG_RATE {
+ uint8_t cls; //0x06
+ uint8_t id; //0x08
+ uint16_t len; // 6 bytes
+ uint16_t measRate; // in every ms -> 1 Hz = 1000 ms; 10 Hz = 100 ms -> x = 1000 ms / Hz
+ uint16_t navRate; // x measurements for 1 navigation event
+ uint16_t timeRef; // align to time system: 0= UTC, 1 = GPS, 2 = GLONASS, ...
+ char CK[2]; // checksum
+ };
+
+ struct {
+ uint32_t last_iTOW;
+ int32_t last_lat;
+ int32_t last_lon;
+ int32_t last_height;
+ uint32_t last_hAcc;
+ uint32_t last_vAcc;
+ uint8_t gpsFix;
+ uint8_t non_empty_loops; // in case of an unintended reset of the GPS, the serial interface will get flooded with NMEA
+ uint16_t log_interval; // in tenth of seconds
+ } state;
+
+ struct {
+ uint32_t filter_noise:1;
+ uint32_t send_when_new:1; // no teleinterval
+ uint32_t send_UI_only:1;
+ uint32_t runningNTP:1;
+ uint32_t forceUTCupdate:1;
+ // TODO: more to come
+ } mode;
+
+ union {
+ NAV_POSLLH navPosllh;
+ NAV_STATUS navStatus;
+ NAV_TIME_UTC navTime;
+ POLL_MSG pollMsg;
+ CFG_RATE cfgRate;
+ } Message;
+
+} UBX;
+
+enum UBXMsgType {
+ MT_NONE,
+ MT_NAV_POSLLH,
+ MT_NAV_STATUS,
+ MT_NAV_TIME,
+ MT_POLL
+};
+
+#ifdef USE_FLOG
+FLOG *Flog = nullptr;
+#endif //USE_FLOG
+TasmotaSerial *UBXSerial;
+
+NtpServer timeServer(PortUdp);
+
+/*********************************************************************************************\
+ * helper function
+\*********************************************************************************************/
+void UBXcalcChecksum(char* CK, size_t msgSize) {
+ memset(CK, 0, 2);
+ for (int i = 0; i < msgSize; i++) {
+ CK[0] += ((char*)(&UBX.Message))[i];
+ CK[1] += CK[0];
+ }
+}
+
+boolean UBXcompareMsgHeader(const char* msgHeader) {
+ char* ptr = (char*)(&UBX.Message);
+ return ptr[0] == msgHeader[0] && ptr[1] == msgHeader[1];
+}
+
+void UBXinitCFG(void){
+ for(uint32_t i = 0; i < sizeof(UBLOX_INIT); i++) {
+ UBXSerial->write( pgm_read_byte(UBLOX_INIT+i) );
+ }
+ DEBUG_SENSOR_LOG(PSTR("UBX: turn off NMEA"));
+
+/*
+ AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_KNX D_ADD " GA #%d: %s " D_TO " %d/%d/%d"),
+ Settings.knx_GA_registered,
+ device_param_ga[GAop-1],
+ GA_FNUM, GA_AREA, GA_FDEF );
+*/
+
+
+
+
+}
+
+void UBXTriggerTele(void){
+ mqtt_data[0] = '\0';
+ if (MqttShowSensor()) {
+ MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain);
+#ifdef USE_RULES
+ RulesTeleperiod(); // Allow rule based HA messages
+#endif // USE_RULES
+ }
+}
+
+/********************************************************************************************/
+
+
+void UBXDetect(void)
+{
+ if ((pin[GPIO_GPS_RX] < 99) && (pin[GPIO_GPS_TX] < 99)) {
+ UBXSerial = new TasmotaSerial(pin[GPIO_GPS_RX], pin[GPIO_GPS_TX], 1, 0, 96); // 64 byte buffer is NOT enough
+ if (UBXSerial->begin(9600)) {
+ DEBUG_SENSOR_LOG(PSTR("UBX: started serial"));
+ if (UBXSerial->hardwareSerial()) {
+ ClaimSerial();
+ DEBUG_SENSOR_LOG(PSTR("UBX: claim HW"));
+ }
+ }
+ }
+
+ UBXinitCFG(); // turn of NMEA, only use "our" UBX-messages
+#ifdef USE_FLOG
+ if(!Flog){
+ Flog = new FLOG; // init Flash Log
+ Flog->init();
+ }
+#endif // USE_FLOG
+
+ UBX.state.log_interval = 10; // 1 second
+ UBX.mode.send_UI_only = true; // send UI data ...
+ UBXTriggerTele(); // ... once at after start
+}
+
+uint32_t UBXprocessGPS() {
+ static uint32_t fpos = 0;
+ static char checksum[2];
+ static uint8_t currentMsgType = MT_NONE;
+ static size_t payloadSize = sizeof(UBX.Message);
+
+ // DEBUG_SENSOR_LOG(PSTR("UBX: check for serial data"));
+ uint32_t data_bytes = 0;
+ while ( UBXSerial->available() ) {
+ data_bytes++;
+ byte c = UBXSerial->read();
+ if ( fpos < 2 ) {
+ // For the first two bytes we are simply looking for a match with the UBX header bytes (0xB5,0x62)
+ if ( c == UBX.UBX_HEADER[fpos] )
+ fpos++;
+ else
+ fpos = 0; // Reset to beginning state.
+ }
+ else {
+ // If we come here then fpos >= 2, which means we have found a match with the UBX_HEADER
+ // and we are now reading in the bytes that make up the payload.
+
+ // Place the incoming byte into the ubxMessage struct. The position is fpos-2 because
+ // the struct does not include the initial two-byte header (UBX_HEADER).
+ if ( (fpos-2) < payloadSize )
+ ((char*)(&UBX.Message))[fpos-2] = c;
+
+ fpos++;
+
+ if ( fpos == 4 ) {
+ // We have just received the second byte of the message type header,
+ // so now we can check to see what kind of message it is.
+ if ( UBXcompareMsgHeader(UBX.NAV_POSLLH_HEADER) ) {
+ currentMsgType = MT_NAV_POSLLH;
+ payloadSize = sizeof(UBX_t::NAV_POSLLH);
+ DEBUG_SENSOR_LOG(PSTR("UBX: got NAV_POSLLH"));
+ }
+ else if ( UBXcompareMsgHeader(UBX.NAV_STATUS_HEADER) ) {
+ currentMsgType = MT_NAV_STATUS;
+ payloadSize = sizeof(UBX_t::NAV_STATUS);
+ DEBUG_SENSOR_LOG(PSTR("UBX: got NAV_STATUS"));
+ }
+ else if ( UBXcompareMsgHeader(UBX.NAV_TIME_HEADER) ) {
+ currentMsgType = MT_NAV_TIME;
+ payloadSize = sizeof(UBX_t::NAV_TIME_UTC);
+ DEBUG_SENSOR_LOG(PSTR("UBX: got NAV_TIME_UTC"));
+ }
+ else {
+ // unknown message type, bail
+ fpos = 0;
+ continue;
+ }
+ }
+
+ if ( fpos == (payloadSize+2) ) {
+ // All payload bytes have now been received, so we can calculate the
+ // expected checksum value to compare with the next two incoming bytes.
+ UBXcalcChecksum(checksum, payloadSize);
+ }
+ else if ( fpos == (payloadSize+3) ) {
+ // First byte after the payload, ie. first byte of the checksum.
+ // Does it match the first byte of the checksum we calculated?
+ if ( c != checksum[0] ) {
+ // Checksum doesn't match, reset to beginning state and try again.
+ fpos = 0;
+ }
+ }
+ else if ( fpos == (payloadSize+4) ) {
+ // Second byte after the payload, ie. second byte of the checksum.
+ // Does it match the second byte of the checksum we calculated?
+ fpos = 0; // We will reset the state regardless of whether the checksum matches.
+ if ( c == checksum[1] ) {
+ // Checksum matches, we have a valid message.
+ return currentMsgType;
+ }
+ }
+ else if ( fpos > (payloadSize+4) ) {
+ // We have now read more bytes than both the expected payload and checksum
+ // together, so something went wrong. Reset to beginning state and try again.
+ fpos = 0;
+ }
+ }
+ }
+ // DEBUG_SENSOR_LOG(PSTR("UBX: got none or unknown Message"));
+ if(data_bytes!=0){
+ UBX.state.non_empty_loops++;
+ DEBUG_SENSOR_LOG(PSTR("UBX: got %u bytes, non-empty-loop: %u"), data_bytes, UBX.state.non_empty_loops);
+ }
+ else{
+ UBX.state.non_empty_loops = 0; // now a hidden GPS-device reset is unlikely
+ }
+ return MT_NONE;
+}
+
+/********************************************************************************************\
+| * callback functions for the download
+\*********************************************************************************************/
+#ifdef USE_FLOG
+void UBXsendHeader(void){
+ WebServer->setContentLength(CONTENT_LENGTH_UNKNOWN);
+ WebServer->sendHeader(F("Content-Disposition"), F("attachment; filename=TASMOTA.gpx"));
+ WSSend(200, CT_STREAM, F(
+ "\r\n"
+ "\r\n"
+ "\r\n\r\n"));
+}
+
+void UBXsendRecord(uint8_t *buf){
+ char record[100];
+ char stime[32];
+ UBX_t::entry_t *entry = (UBX_t::entry_t*)buf;
+ snprintf_P(stime, sizeof(stime), GetDT(entry->time).c_str());
+ char lat[12];
+ char lon[12];
+ dtostrfd((double)entry->lat/10000000.0f,7,lat);
+ dtostrfd((double)entry->lon/10000000.0f,7,lon);
+ snprintf_P(record, sizeof(record),PSTR("\n\t\n\n"),lat ,lon, stime);
+ // DEBUG_SENSOR_LOG(PSTR("FLOG: DL %u %u"), Flog->sector.dword_buffer[k+j],Flog->sector.dword_buffer[k+j+1]);
+ WebServer->sendContent_P(record);
+}
+
+void UBXsendFooter(void){
+ WebServer->sendContent(F("\n\n"));
+ WebServer->sendContent("");
+ Rtc.user_time_entry = false; // we have blocked the main loop and want a new valid time
+}
+
+/********************************************************************************************/
+void UBXsendFile(void){
+ if (!HttpCheckPriviledgedAccess()) { return; }
+ Flog->startDownload(sizeof(UBX.rec_buffer),UBXsendHeader,UBXsendRecord,UBXsendFooter);
+}
+#endif //USE_FLOG
+/********************************************************************************************/
+
+void UBXSetRate(uint16_t interval){
+ UBX.Message.cfgRate.cls = 0x06;
+ UBX.Message.cfgRate.id = 0x08;
+ UBX.Message.cfgRate.len = 6;
+ uint32_t measRate = (1000*(uint32_t)interval); //seconds to milliseconds
+ if (measRate > 0xffff) {
+ measRate = 0xffff; // max. 65535 ms interval
+ }
+ UBX.Message.cfgRate.measRate = (uint16_t)measRate;
+ UBX.Message.cfgRate.navRate = 1;
+ UBX.Message.cfgRate.timeRef = 1;
+ UBXcalcChecksum(UBX.Message.cfgRate.CK, sizeof(UBX.Message.cfgRate)-sizeof(UBX.Message.cfgRate.CK));
+ DEBUG_SENSOR_LOG(PSTR("UBX: requested interval: %u seconds measRate: %u ms"), interval, UBX.Message.cfgRate.measRate);
+ UBXSerial->write(UBX.UBX_HEADER[0]);
+ UBXSerial->write(UBX.UBX_HEADER[1]);
+ for(uint32_t i =0; iwrite(((uint8_t*)(&UBX.Message.cfgRate))[i]);
+ DEBUG_SENSOR_LOG(PSTR("UBX: cfgRate byte %u: %x"), i, ((uint8_t*)(&UBX.Message.cfgRate))[i]);
+ }
+ UBX.state.log_interval = 10*interval;
+}
+
+
+void UBXSelectMode(uint16_t mode){
+ DEBUG_SENSOR_LOG(PSTR("UBX: set mode to %u"),mode);
+ switch(mode){
+#ifdef USE_FLOG
+ case 0:
+ Flog->mode = 0; // write once to all available sectors, then stop
+ break;
+ case 1:
+ Flog->mode = 1; // write to all available sectors, then restart and overwrite the older ones
+ break;
+ case 2:
+ UBX.mode.filter_noise = true; // filter out horizontal drift noise, TODO: find useful values
+ break;
+ case 3:
+ UBX.mode.filter_noise = false;
+ break;
+ case 4:
+ Flog->startRecording(true);
+ AddLog_P(LOG_LEVEL_INFO, PSTR("UBX: start recording - appending"));
+ break;
+ case 5:
+ Flog->startRecording(false);
+ AddLog_P(LOG_LEVEL_INFO, PSTR("UBX: start recording - new log"));
+ break;
+ case 6:
+ if(Flog->recording == true){
+ Flog->stopRecording();
+ }
+ AddLog_P(LOG_LEVEL_INFO, PSTR("UBX: stop recording"));
+ break;
+ case 7:
+ UBX.mode.send_when_new = 1; // send mqtt on new postion + TELE -> consider to set TELE to a very high value
+ break;
+ case 8:
+ UBX.mode.send_when_new = 0; // only TELE
+ break;
+ case 9:
+ if (timeServer.beginListening()){
+ UBX.mode.runningNTP = true;
+ }
+ break;
+ case 10:
+ UBX.mode.runningNTP = false;
+ break;
+ case 11:
+ UBX.mode.forceUTCupdate = true;
+ break;
+ case 12:
+ UBX.mode.forceUTCupdate = false;
+ break;
+ case 13:
+ Settings.latitude = UBX.state.last_lat;
+ Settings.longitude = UBX.state.last_lon;
+ break;
+
+
+#endif //USE_FLOG
+ default:
+ if(mode>1000 && mode <1066) {
+ // UBXSetRate(mode-1000); // min. 1001 = 0.001 Hz, but will be converted to 1/65535 anyway ~0.015 Hz, max. 2000 = 1.000 Hz
+ UBXSetRate(mode-1000); // set interval between measurements in seconds from 1 to 65
+ }
+ break;
+ }
+ UBX.mode.send_UI_only = true;
+ UBXTriggerTele();
+}
+/********************************************************************************************/
+
+bool UBXHandlePOSLLH(){
+ DEBUG_SENSOR_LOG(PSTR("UBX: iTOW: %u"),UBX.Message.navPosllh.iTOW);
+ if(UBX.state.gpsFix>1){
+ if(UBX.mode.filter_noise){
+ if((UBX.Message.navPosllh.lat-UBX.rec_buffer.values.lat6){ // we expect only 4-5 non-empty loops in a row, could change with other sensor speed (Hz)
+ UBXinitCFG(); // this should only happen with lots of NMEA-messages, but it is only a guess!!
+ AddLog_P(LOG_LEVEL_ERROR, PSTR("UBX: possible device-reset, will re-init"));
+ UBXSerial->flush();
+ UBX.state.non_empty_loops = 0;
+ }
+}
+
+/********************************************************************************************/
+
+void UBXTimeServer(){
+ if(UBX.mode.runningNTP){
+ timeServer.processOneRequest(Rtc.utc_time, UBX.state.last_iTOW%1000);
+ }
+}
+
+void UBXLoop(void)
+{
+ static uint16_t counter; //count up every 100 msec
+ static bool new_position;
+
+ uint32_t msgType = UBXprocessGPS();
+
+ switch(msgType){
+ case MT_NAV_POSLLH:
+ new_position = UBXHandlePOSLLH();
+ break;
+ case MT_NAV_STATUS:
+ UBXHandleSTATUS();
+ break;
+ case MT_NAV_TIME:
+ UBXHandleTIME();
+ break;
+ default:
+ UBXHandleOther();
+ break;
+ }
+
+#ifdef USE_FLOG
+ if(counter>UBX.state.log_interval){
+ if(Flog->recording && new_position){
+ UBX.rec_buffer.values.time = Rtc.local_time;
+ Flog->addToBuffer(UBX.rec_buffer.bytes, sizeof(UBX.rec_buffer.bytes));
+ counter = 0;
+ }
+ }
+#endif // USE_FLOG
+
+counter++;
+}
+
+/********************************************************************************************/
+// normaly in i18n.h
+
+#ifdef USE_WEBSERVER
+ // {s} =