diff --git a/.gitmodules b/.gitmodules index 0c7b52e67..a2bf9ffbd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "external/TinyWebsockets"] path = external/TinyWebsockets url = https://github.com/gilmaimon/TinyWebsockets.git +[submodule "external/influxdb-cpp"] + path = external/influxdb-cpp + url = https://github.com/orca-zhang/influxdb-cpp.git diff --git a/Dockerfile b/Dockerfile index 8b2cf2a54..f7efcbbab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN echo "deb [signed-by=/usr/share/keyrings/raspberrypi-archive-keyring.gpg] ht ## 1st stage compiles OpenSprinkler code FROM base AS os-build -ARG BUILD_VERSION="DEMO" +ARG BUILD_VERSION="OSPI" ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y bash g++ make libmosquittopp-dev libssl-dev libi2c-dev liblgpio-dev diff --git a/EMailSender.h b/EMailSender.h index 36c42f580..df806dd48 100644 --- a/EMailSender.h +++ b/EMailSender.h @@ -508,7 +508,7 @@ class EMailSender { bool isSASLLogin = false; bool useAuth = true; - bool isCramMD5Login = false; + bool isCramMD5Login = false; String _serverResponce; diff --git a/I2CRTC.cpp b/I2CRTC.cpp index 6832a5580..d1d76dc37 100644 --- a/I2CRTC.cpp +++ b/I2CRTC.cpp @@ -24,9 +24,6 @@ 23 Dec 2013 -- modified by Ray Wang (Rayshobby LLC) to add support for MCP7940 */ - -#if defined(ARDUINO) - #include "I2CRTC.h" #include @@ -159,5 +156,3 @@ uint8_t I2CRTC::bcd2dec(uint8_t num) } I2CRTC RTC = I2CRTC(); // create an instance for the user - -#endif diff --git a/I2CRTC.h b/I2CRTC.h index ca575feab..17660e6b4 100644 --- a/I2CRTC.h +++ b/I2CRTC.h @@ -4,8 +4,7 @@ */ -#ifndef I2CRTC_h -#define I2CRTC_h +#pragma once #define DS1307_ADDR 0x68 #define MCP7940_ADDR 0x6F @@ -35,5 +34,3 @@ class I2CRTC extern I2CRTC RTC; -#endif - diff --git a/LiquidCrystal.cpp b/LiquidCrystal.cpp deleted file mode 100644 index 88050bd35..000000000 --- a/LiquidCrystal.cpp +++ /dev/null @@ -1,383 +0,0 @@ -#if defined(__AVR_ATmega1284P__) || defined(__AVR_ATmega1284__) - -#include "LiquidCrystal.h" -#include -#include -#include - -// When the display powers up, it is configured as follows: -// -// 1. Display clear -// 2. Function set: -// DL = 1; 8-bit interface data -// N = 0; 1-line display -// F = 0; 5x8 dot character font -// 3. Display on/off control: -// D = 0; Display off -// C = 0; Cursor off -// B = 0; Blinking off -// 4. Entry mode set: -// I/D = 1; Increment by 1 -// S = 0; No shift -// -// Note, however, that resetting the Arduino doesn't reset the LCD, so we -// can't assume that its in that state when a sketch starts (and the -// LiquidCrystal constructor is called) - -void LiquidCrystal::begin() { - if (_type == LCD_I2C) { - _displayfunction = LCD_4BITMODE | LCD_2LINE | LCD_5x8DOTS; - - // SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION! - // according to datasheet, we need at least 40ms after power rises above 2.7V - // before sending commands. Arduino can turn on way befer 4.5V so we'll wait 50 - delay(50); - - // Now we pull both RS and R/W low to begin commands - expanderWrite(_backlightval); // reset expanderand turn backlight off (Bit 8 =1) - delay(1000); - - //put the LCD into 4 bit mode - // this is according to the hitachi HD44780 datasheet - // figure 24, pg 46 - - // we start in 8bit mode, try to set 4 bit mode - write4bits(0x03 << 4); - delayMicroseconds(4500); // wait min 4.1ms - - // second try - write4bits(0x03 << 4); - delayMicroseconds(4500); // wait min 4.1ms - - // third go! - write4bits(0x03 << 4); - delayMicroseconds(150); - - // finally, set to 4-bit interface - write4bits(0x02 << 4); - - // set # lines, font size, etc. - command(LCD_FUNCTIONSET | _displayfunction); - - // turn the display on with no cursor or blinking default - _displaycontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF; - display(); - - // clear it off - clear(); - - // Initialize to default text direction (for roman languages) - _displaymode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT; - - // set the entry mode - command(LCD_ENTRYMODESET | _displaymode); - - home(); - } - - if (_type == LCD_STD) { - _displayfunction |= LCD_2LINE; - _numlines = 2; - _currline = 0; - - // SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION! - // according to datasheet, we need at least 40ms after power rises above 2.7V - // before sending commands. Arduino can turn on way befer 4.5V so we'll wait 50 - delayMicroseconds(50000); - // Now we pull both RS and R/W low to begin commands - digitalWrite(_rs_pin, LOW); - digitalWrite(_enable_pin, LOW); - if (_rw_pin != 255) { - digitalWrite(_rw_pin, LOW); - } - - //put the LCD into 4 bit or 8 bit mode - if (! (_displayfunction & LCD_8BITMODE)) { - // this is according to the hitachi HD44780 datasheet - // figure 24, pg 46 - - // we start in 8bit mode, try to set 4 bit mode - write4bits(0x03); - delayMicroseconds(4500); // wait min 4.1ms - - // second try - write4bits(0x03); - delayMicroseconds(4500); // wait min 4.1ms - - // third go! - write4bits(0x03); - delayMicroseconds(150); - - // finally, set to 4-bit interface - write4bits(0x02); - } else { - // this is according to the hitachi HD44780 datasheet - // page 45 figure 23 - - // Send function set command sequence - command(LCD_FUNCTIONSET | _displayfunction); - delayMicroseconds(4500); // wait more than 4.1ms - - // second try - command(LCD_FUNCTIONSET | _displayfunction); - delayMicroseconds(150); - - // third go - command(LCD_FUNCTIONSET | _displayfunction); - } - - // finally, set # lines, font size, etc. - command(LCD_FUNCTIONSET | _displayfunction); - - // turn the display on with no cursor or blinking default - _displaycontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF; - display(); - - // clear it off - clear(); - - // Initialize to default text direction (for romance languages) - _displaymode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT; - // set the entry mode - command(LCD_ENTRYMODESET | _displaymode); - } -} - -void LiquidCrystal::init(uint8_t fourbitmode, uint8_t rs, uint8_t rw, uint8_t enable, - uint8_t d0, uint8_t d1, uint8_t d2, uint8_t d3, - uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7) -{ - _rs_pin = rs; - _rw_pin = rw; - _enable_pin = enable; - - _data_pins[0] = d0; - _data_pins[1] = d1; - _data_pins[2] = d2; - _data_pins[3] = d3; - _data_pins[4] = d4; - _data_pins[5] = d5; - _data_pins[6] = d6; - _data_pins[7] = d7; - - Wire.begin(); - _type = LCD_STD; - - // detect I2C and assign _type variable accordingly - Wire.beginTransmission(LCD_I2C_ADDR1); // check type 1 - //Wire.write(0x00); - uint8_t ret1 = Wire.endTransmission(); - Wire.beginTransmission(LCD_I2C_ADDR2); // check type 2 - //Wire.write(0x00); - uint8_t ret2 = Wire.endTransmission(); - - if (!ret1 || !ret2) _type = LCD_I2C; - if (_type == LCD_I2C) { - if(!ret1) _addr = LCD_I2C_ADDR1; - else _addr = LCD_I2C_ADDR2; - _cols = 16; - _rows = 2; - _charsize = LCD_5x8DOTS; - _backlightval = LCD_BACKLIGHT; - } - - if (_type == LCD_STD) { - pinMode(_rs_pin, OUTPUT); - // we can save 1 pin by not using RW. Indicate by passing 255 instead of pin# - if (_rw_pin != 255) { - pinMode(_rw_pin, OUTPUT); - } - pinMode(_enable_pin, OUTPUT); - - } - _displayfunction = LCD_4BITMODE | LCD_2LINE | LCD_5x8DOTS; - -} - -/********** high level commands, for the user! */ -void LiquidCrystal::clear() -{ - command(LCD_CLEARDISPLAY);// clear display, set cursor position to zero - delayMicroseconds(2000); // this command takes a long time! -} - -void LiquidCrystal::home() -{ - command(LCD_RETURNHOME); // set cursor position to zero - delayMicroseconds(2000); // this command takes a long time! -} - -void LiquidCrystal::setCursor(uint8_t col, uint8_t row) -{ - int row_offsets[] = { 0x00, 0x40, 0x14, 0x54 }; - if (_type == LCD_I2C) { - if (row > _rows) { - row = _rows-1; // we count rows starting w/0 - } - } - if (_type == LCD_STD) { - if (row >= _numlines) { - row = _numlines-1; - } - } - command(LCD_SETDDRAMADDR | (col + row_offsets[row])); -} - -// Turn the display on/off (quickly) -void LiquidCrystal::noDisplay() { - _displaycontrol &= ~LCD_DISPLAYON; - command(LCD_DISPLAYCONTROL | _displaycontrol); -} -void LiquidCrystal::display() { - _displaycontrol |= LCD_DISPLAYON; - command(LCD_DISPLAYCONTROL | _displaycontrol); -} - -// Turns the underline cursor on/off -void LiquidCrystal::noCursor() { - _displaycontrol &= ~LCD_CURSORON; - command(LCD_DISPLAYCONTROL | _displaycontrol); -} -void LiquidCrystal::cursor() { - _displaycontrol |= LCD_CURSORON; - command(LCD_DISPLAYCONTROL | _displaycontrol); -} - -// Turn on and off the blinking cursor -void LiquidCrystal::noBlink() { - _displaycontrol &= ~LCD_BLINKON; - command(LCD_DISPLAYCONTROL | _displaycontrol); -} -void LiquidCrystal::blink() { - _displaycontrol |= LCD_BLINKON; - command(LCD_DISPLAYCONTROL | _displaycontrol); -} - -// These commands scroll the display without changing the RAM -void LiquidCrystal::scrollDisplayLeft(void) { - command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVELEFT); -} -void LiquidCrystal::scrollDisplayRight(void) { - command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVERIGHT); -} - -// This is for text that flows Left to Right -void LiquidCrystal::leftToRight(void) { - _displaymode |= LCD_ENTRYLEFT; - command(LCD_ENTRYMODESET | _displaymode); -} - -// This is for text that flows Right to Left -void LiquidCrystal::rightToLeft(void) { - _displaymode &= ~LCD_ENTRYLEFT; - command(LCD_ENTRYMODESET | _displaymode); -} - -// This will 'right justify' text from the cursor -void LiquidCrystal::autoscroll(void) { - _displaymode |= LCD_ENTRYSHIFTINCREMENT; - command(LCD_ENTRYMODESET | _displaymode); -} - -// This will 'left justify' text from the cursor -void LiquidCrystal::noAutoscroll(void) { - _displaymode &= ~LCD_ENTRYSHIFTINCREMENT; - command(LCD_ENTRYMODESET | _displaymode); -} - -// Allows us to fill the first 8 CGRAM locations -// with custom characters -//void LiquidCrystal::createChar(uint8_t location, uint8_t charmap[]) { -void LiquidCrystal::createChar(uint8_t location, PGM_P ptr) { - location &= 0x7; // we only have 8 locations 0-7 - command(LCD_SETCGRAMADDR | (location << 3)); - for (int i=0; i<8; i++) { - //write(charmap[i]); - write(pgm_read_byte(ptr++)); - } -} - -// Turn the (optional) backlight off/on -void LiquidCrystal::noBacklight(void) { - _backlightval=LCD_NOBACKLIGHT; - expanderWrite(0); -} - -void LiquidCrystal::backlight(void) { - _backlightval=LCD_BACKLIGHT; - expanderWrite(0); -} - -/*********** mid level commands, for sending data/cmds */ - -inline void LiquidCrystal::command(uint8_t value) { - send(value, 0); -} - -inline size_t LiquidCrystal::write(uint8_t value) { - send(value, Rs); - return 1; // assume sucess -} - -/************ low level data pushing commands **********/ - -// write either command or data -void LiquidCrystal::send(uint8_t value, uint8_t mode) { - if (_type == LCD_I2C) { - uint8_t highnib=value&0xf0; - uint8_t lownib=(value<<4)&0xf0; - write4bits((highnib)|mode); - write4bits((lownib)|mode); - } - if (_type == LCD_STD) { - digitalWrite(_rs_pin, mode); - - // if there is a RW pin indicated, set it low to Write - if (_rw_pin != 255) { - digitalWrite(_rw_pin, LOW); - } - - write4bits(value>>4); - write4bits(value); - } -} - -void LiquidCrystal::write4bits(uint8_t value) { - if (_type == LCD_I2C) { - expanderWrite(value); - pulseEnable(value); - } - if (_type == LCD_STD) { - for (int i = 0; i < 4; i++) { - pinMode(_data_pins[i], OUTPUT); - digitalWrite(_data_pins[i], (value >> i) & 0x01); - } - - pulseEnable(); - } -} - -void LiquidCrystal::expanderWrite(uint8_t _data){ - Wire.beginTransmission(_addr); - Wire.write((int)(_data) | _backlightval); - Wire.endTransmission(); -} - -void LiquidCrystal::pulseEnable(uint8_t _data){ - expanderWrite(_data | En); // En high - delayMicroseconds(1); // enable pulse must be >450ns - - expanderWrite(_data & ~En); // En low - delayMicroseconds(50); // commands need > 37us to settle -} - -void LiquidCrystal::pulseEnable(void) { - digitalWrite(_enable_pin, LOW); - delayMicroseconds(1); - digitalWrite(_enable_pin, HIGH); - delayMicroseconds(1); // enable pulse must be >450ns - digitalWrite(_enable_pin, LOW); - delayMicroseconds(100); // commands need > 37us to settle -} - -#endif diff --git a/LiquidCrystal.h b/LiquidCrystal.h deleted file mode 100644 index ab964da77..000000000 --- a/LiquidCrystal.h +++ /dev/null @@ -1,127 +0,0 @@ -#ifndef LIQUID_CRYSTAL_DUAL_H -#define LIQUID_CRYSTAL_DUAL_H - -#if defined(__AVR_ATmega1284P__) || defined(__AVR_ATmega1284__) - -#include -#include - -// commands -#define LCD_CLEARDISPLAY 0x01 -#define LCD_RETURNHOME 0x02 -#define LCD_ENTRYMODESET 0x04 -#define LCD_DISPLAYCONTROL 0x08 -#define LCD_CURSORSHIFT 0x10 -#define LCD_FUNCTIONSET 0x20 -#define LCD_SETCGRAMADDR 0x40 -#define LCD_SETDDRAMADDR 0x80 - -// flags for display entry mode -#define LCD_ENTRYRIGHT 0x00 -#define LCD_ENTRYLEFT 0x02 -#define LCD_ENTRYSHIFTINCREMENT 0x01 -#define LCD_ENTRYSHIFTDECREMENT 0x00 - -// flags for display on/off control -#define LCD_DISPLAYON 0x04 -#define LCD_DISPLAYOFF 0x00 -#define LCD_CURSORON 0x02 -#define LCD_CURSOROFF 0x00 -#define LCD_BLINKON 0x01 -#define LCD_BLINKOFF 0x00 - -// flags for display/cursor shift -#define LCD_DISPLAYMOVE 0x08 -#define LCD_CURSORMOVE 0x00 -#define LCD_MOVERIGHT 0x04 -#define LCD_MOVELEFT 0x00 - -// flags for function set -#define LCD_8BITMODE 0x10 -#define LCD_4BITMODE 0x00 -#define LCD_2LINE 0x08 -#define LCD_1LINE 0x00 -#define LCD_5x10DOTS 0x04 -#define LCD_5x8DOTS 0x00 - -// flags for backlight control -#define LCD_BACKLIGHT 0x08 -#define LCD_NOBACKLIGHT 0x00 - -#define En B00000100 // Enable bit -#define Rw B00000010 // Read/Write bit -#define Rs B00000001 // Register select bit - -#define LCD_STD 0 // Standard LCD -#define LCD_I2C 1 // I2C LCD -#define LCD_I2C_ADDR1 0x27 // type using PCF8574, at address 0x27 -#define LCD_I2C_ADDR2 0x3F // type using PCF8574A, at address 0x3F - -class LiquidCrystal : public Print { -public: - LiquidCrystal() {} - void init(uint8_t fourbitmode, uint8_t rs, uint8_t rw, uint8_t enable, - uint8_t d0, uint8_t d1, uint8_t d2, uint8_t d3, - uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7); - - void begin(); - - void clear(); - void clear(int start, int end) { clear(); } - void home(); - - void noDisplay(); - void display(); - void noBlink(); - void blink(); - void noCursor(); - void cursor(); - void scrollDisplayLeft(); - void scrollDisplayRight(); - void leftToRight(); - void rightToLeft(); - void autoscroll(); - void noAutoscroll(); - - //void createChar(uint8_t, uint8_t[]); - void createChar(uint8_t, PGM_P ptr); - void setCursor(uint8_t, uint8_t); - virtual size_t write(uint8_t); - void command(uint8_t); - - inline uint8_t type() { return _type; } - void noBacklight(); - void backlight(); - - using Print::write; -private: - void send(uint8_t, uint8_t); - void write4bits(uint8_t); - void pulseEnable(); - - void expanderWrite(uint8_t); - void pulseEnable(uint8_t); - uint8_t _addr; - uint8_t _cols; - uint8_t _rows; - uint8_t _charsize; - uint8_t _backlightval; - - uint8_t _type; // LCD type. 0: standard; 1: I2C - uint8_t _rs_pin; // LOW: command. HIGH: character. - uint8_t _rw_pin; // LOW: write to LCD. HIGH: read from LCD. - uint8_t _enable_pin; // activated by a HIGH pulse. - uint8_t _data_pins[8]; - - uint8_t _displayfunction; - uint8_t _displaycontrol; - uint8_t _displaymode; - - uint8_t _initialized; - - uint8_t _numlines,_currline; -}; - -#endif - -#endif // LIQUID_CRYSTAL_DUAL_H diff --git a/Makefile b/Makefile index b1318456c..df10045cb 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,8 @@ LD=$(CXX) LIBS=pthread mosquitto ssl crypto i2c lgpio LDFLAGS=$(addprefix -l,$(LIBS)) BINARY=OpenSprinkler -SOURCES=main.cpp OpenSprinkler.cpp notifier.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp mqtt.cpp smtp.c RCSwitch.cpp $(wildcard external/TinyWebsockets/tiny_websockets_lib/src/*.cpp) $(wildcard external/OpenThings-Framework-Firmware-Library/*.cpp) -HEADERS=$(wildcard *.h) $(wildcard *.hpp) +SOURCES=main.cpp OpenSprinkler.cpp notifier.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp mqtt.cpp smtp.c RCSwitch.cpp i2cd.cpp ads1115.cpp $(wildcard sensors/*.cpp) $(wildcard external/TinyWebsockets/tiny_websockets_lib/src/*.cpp) $(wildcard external/OpenThings-Framework-Firmware-Library/*.cpp) +HEADERS=$(wildcard *.h) $(wildcard *.hpp) $(wildcard sensors/*.h) OBJECTS=$(addsuffix .o,$(basename $(SOURCES))) .PHONY: all diff --git a/OpenSprinkler.cpp b/OpenSprinkler.cpp index 206ce5a50..3e7c53bf5 100644 --- a/OpenSprinkler.cpp +++ b/OpenSprinkler.cpp @@ -29,6 +29,7 @@ #include "ArduinoJson.hpp" /** Declare static data members */ +sensor_memory_t OpenSprinkler::sensors[64] = {0}; OSMqtt OpenSprinkler::mqtt; NVConData OpenSprinkler::nvdata; ConStatus OpenSprinkler::status; @@ -38,6 +39,7 @@ unsigned char OpenSprinkler::hw_type; unsigned char OpenSprinkler::hw_rev; unsigned char OpenSprinkler::nboards; unsigned char OpenSprinkler::nstations; +unsigned char OpenSprinkler::nsensors; unsigned char OpenSprinkler::station_bits[MAX_NUM_BOARDS]; unsigned char OpenSprinkler::engage_booster; uint16_t OpenSprinkler::baseline_current; @@ -49,10 +51,10 @@ time_os_t OpenSprinkler::sensor2_on_timer; time_os_t OpenSprinkler::sensor2_off_timer; time_os_t OpenSprinkler::sensor2_active_lasttime; time_os_t OpenSprinkler::raindelay_on_lasttime; -ulong OpenSprinkler::pause_timer; +uint32_t OpenSprinkler::pause_timer; -ulong OpenSprinkler::flowcount_log_start; -ulong OpenSprinkler::flowcount_rt; +uint32_t OpenSprinkler::flowcount_log_start; +uint32_t OpenSprinkler::flowcount_rt; unsigned char OpenSprinkler::button_timeout; time_os_t OpenSprinkler::checkwt_lasttime; time_os_t OpenSprinkler::checkwt_success_lasttime; @@ -64,6 +66,8 @@ unsigned char OpenSprinkler::weather_update_flag; unsigned char OpenSprinkler::attrib_mas[MAX_NUM_BOARDS]; unsigned char OpenSprinkler::attrib_igs[MAX_NUM_BOARDS]; unsigned char OpenSprinkler::attrib_mas2[MAX_NUM_BOARDS]; +unsigned char OpenSprinkler::attrib_mas3[MAX_NUM_BOARDS]; +unsigned char OpenSprinkler::attrib_mas4[MAX_NUM_BOARDS]; unsigned char OpenSprinkler::attrib_igs2[MAX_NUM_BOARDS]; unsigned char OpenSprinkler::attrib_igrd[MAX_NUM_BOARDS]; unsigned char OpenSprinkler::attrib_dis[MAX_NUM_BOARDS]; @@ -79,12 +83,12 @@ extern ProgramData pd; extern const char* user_agent_string; extern unsigned char curr_alert_sid; -#if defined(USE_SSD1306) - SSD1306Display OpenSprinkler::lcd(0x3c, SDA, SCL); -#elif defined(USE_LCD) - LiquidCrystal OpenSprinkler::lcd; +#if defined(USE_DISPLAY) +SSD1306Display OpenSprinkler::lcd(0x3c, SDA, SCL); #endif +ADS1115 *OpenSprinkler::ads1115_devices[4] = {nullptr}; + #if defined(ESP8266) unsigned char OpenSprinkler::state = OS_STATE_INITIAL; unsigned char OpenSprinkler::prev_station_bits[MAX_NUM_BOARDS]; @@ -98,17 +102,13 @@ extern unsigned char curr_alert_sid; unsigned char OpenSprinkler::wifi_testmode = 0; CH224 OpenSprinkler::usbpd; uint8_t OpenSprinkler::actual_pd_voltage = 0; -#elif defined(ARDUINO) - extern SdFat sd; #else #if defined(OSPI) unsigned char OpenSprinkler::pin_sr_data = PIN_SR_DATA; #endif #endif -#if defined(USE_OTF) - OTCConfig OpenSprinkler::otc; -#endif +OTCConfig OpenSprinkler::otc; /** Option json names (stored in PROGMEM to reduce RAM usage) */ // IMPORTANT: each json name is strictly 5 characters @@ -189,6 +189,12 @@ const char iopt_json_names[] PROGMEM = "resv8" "wimod" "reset" + "mas3\0" + "mton3" + "mtof3" + "mas4\0" + "mton4" + "mtof4" ; /** Option prompts (stored in PROGMEM to reduce RAM usage) */ @@ -267,7 +273,13 @@ const char iopt_prompts[] PROGMEM = "Reserved 7 " "Reserved 8 " "WiFi mode? " - "Factory reset? "; + "Factory reset? " + "Master 3 (Mas3):" + "Mas3 on adjust:" + "Mas3 off adjust:" + "Master 4 (Mas4):" + "Mas4 on adjust:" + "Mas4 off adjust:"; // string options do not have prompts @@ -345,7 +357,13 @@ const unsigned char iopt_max[] PROGMEM = { 255, 255, 255, - 1 + 1, + MAX_NUM_STATIONS, + 255, + 255, + MAX_NUM_STATIONS, + 255, + 255 }; // string options do not have maximum values @@ -364,7 +382,7 @@ unsigned char OpenSprinkler::iopts[] = { 0, 0, 0, -#if defined(ARDUINO) // on AVR, the default HTTP port is 80 +#if defined(ESP8266) // on Arduino, the default HTTP port is 80 80, // this and next byte define http port number 0, #else // on RPI/LINUX, the default HTTP port is 8080 @@ -429,7 +447,13 @@ unsigned char OpenSprinkler::iopts[] = { 0, // reserved 7 0, // reserved 8 WIFI_MODE_AP, // wifi mode - 0 // reset + 0, // reset + 0, // index of master3. 0: no master3 station + 120,// master3 on adjusted time + 120,// master3 off adjusted time + 0, // index of master4. 0: no master4 station + 120,// master4 on adjusted time + 120,// master4 off adjusted time }; /** String option values (stored in RAM) */ @@ -474,7 +498,7 @@ static const char months_str[] PROGMEM = "Nov\0" "Dec\0"; -#if !defined(ARDUINO) +#if !defined(ESP8266) static inline uint32_t now() { time_t rawtime; time(&rawtime); @@ -486,7 +510,7 @@ time_os_t OpenSprinkler::now_tz() { return now()+(int32_t)3600/4*(int32_t)(iopts[IOPT_TIMEZONE]-48); } -#if defined(ARDUINO) +#if defined(ESP8266) bool detect_i2c(int addr) { Wire.beginTransmission(addr); @@ -494,44 +518,19 @@ bool detect_i2c(int addr) { } /** read hardware MAC into tmp_buffer */ -#define MAC_CTRL_ID 0x50 bool OpenSprinkler::load_hardware_mac(unsigned char* buffer, bool wired) { -#if defined(ESP8266) WiFi.macAddress((unsigned char*)buffer); // if requesting wired Ethernet MAC, flip the last byte to create a modified MAC if(wired) buffer[5] = ~buffer[5]; return true; -#else - // initialize the buffer by assigning software mac - buffer[0] = 0x00; - buffer[1] = 0x69; - buffer[2] = 0x69; - buffer[3] = 0x2D; - buffer[4] = 0x31; - buffer[5] = iopts[IOPT_DEVICE_ID]; - if (detect_i2c(MAC_CTRL_ID)==false) return false; - - Wire.beginTransmission(MAC_CTRL_ID); - Wire.write(0xFA); // The address of the register we want - Wire.endTransmission(); // Send the data - if(Wire.requestFrom(MAC_CTRL_ID, 6) != 6) return false; // if not enough data, return false - for(unsigned char ret=0;ret<6;ret++) { - buffer[ret] = Wire.read(); - } - return true; -#endif } -void(* resetFunc) (void) = 0; // AVR software reset function - /** Initialize network with the given mac address and http port */ unsigned char OpenSprinkler::start_network() { lcd_print_line_clear_pgm(PSTR("Starting..."), 1); uint16_t httpport = (uint16_t)(iopts[IOPT_HTTPPORT_1]<<8) + (uint16_t)iopts[IOPT_HTTPPORT_0]; -#if defined(ESP8266) - if (start_ether()) { useEth = true; WiFi.mode(WIFI_OFF); @@ -553,24 +552,9 @@ unsigned char OpenSprinkler::start_network() { DEBUG_PRINT(F("Started update server")); return 1; -#else - - if (start_ether()) { - if(m_server) { delete m_server; m_server = NULL; } - m_server = new EthernetServer(httpport); - m_server->begin(); - useEth = true; - return 1; - } else { - useEth = false; - return 0; - } - -#endif } unsigned char OpenSprinkler::start_ether() { -#if defined(ESP8266) if(hw_rev<2) return 0; // ethernet capability is only available when hw_rev>=2 eth.isW5500 = (hw_rev==2)?false:true; // os 3.2 uses enc28j60 and 3.3 uses w5500 @@ -651,9 +635,9 @@ unsigned char OpenSprinkler::start_ether() { lcd_print_line_clear_pgm(PSTR("Start wired link"), 1); lcd_print_line_clear_pgm(eth.isW5500 ? PSTR(" [w5500] ") : PSTR(" [enc28j60] "), 2); - ulong timeout = millis()+60000; // 60 seconds time out + uint32_t timeout = millis()+60000; // 60 seconds time out unsigned char timecount = 1; - while (!eth.connected() && (long)(millis()-timeout)<0) { // overflow proof + while (!eth.connected() && (int32_t)((uint32_t)millis()-timeout)<0) { // overflow proof DEBUG_PRINT("."); lcd.setCursor(13, 2); lcd.print(timecount); @@ -675,42 +659,13 @@ unsigned char OpenSprinkler::start_ether() { // if wired connection has failed at this point, return depending on whether the user wants to force wired return (iopts[IOPT_FORCE_WIRED] ? 1 : 0); } - -#else - Ethernet.init(PIN_ETHER_CS); // make sure to call this before any Ethernet calls - if(Ethernet.hardwareStatus()==EthernetNoHardware) return 0; - load_hardware_mac((uint8_t*)tmp_buffer, true); - - lcd_print_line_clear_pgm(PSTR("Start wired link"), 1); - - if (iopts[IOPT_USE_DHCP]) { - if(!Ethernet.begin((uint8_t*)tmp_buffer)) return 0; - memcpy(iopts+IOPT_STATIC_IP1, &(Ethernet.localIP()[0]), 4); - memcpy(iopts+IOPT_GATEWAY_IP1, &(Ethernet.gatewayIP()[0]),4); - memcpy(iopts+IOPT_DNS_IP1, &(Ethernet.dnsServerIP()[0]), 4); - memcpy(iopts+IOPT_SUBNET_MASK1, &(Ethernet.subnetMask()[0]), 4); - iopts_save(); - } else { - IPAddress staticip(iopts+IOPT_STATIC_IP1); - IPAddress gateway(iopts+IOPT_GATEWAY_IP1); - IPAddress dns(iopts+IOPT_DNS_IP1); - IPAddress subn(iopts+IOPT_SUBNET_MASK1); - Ethernet.begin((uint8_t*)tmp_buffer, staticip, dns, gateway, subn); - } - - return 1; -#endif } bool OpenSprinkler::network_connected(void) { -#if defined (ESP8266) if(useEth) return eth.connected(); else return (get_wifi_mode()==WIFI_MODE_STA && WiFi.status()==WL_CONNECTED && state==OS_STATE_CONNECTED); -#else - return (Ethernet.linkStatus()==LinkON); -#endif } /** Reboot controller */ @@ -720,11 +675,7 @@ void OpenSprinkler::reboot_dev(uint8_t cause) { nvdata.reboot_cause = cause; nvdata_save(); } -#if defined(ESP8266) ESP.restart(); -#else - resetFunc(); -#endif } #else // RPI/LINUX network init functions @@ -763,6 +714,12 @@ bool OpenSprinkler::network_connected(void) { return true; } +#if defined(OSPI) +bool detect_i2c(int addr) { + return Bus.detect(addr); +} +#endif + // Return mac of first recognised interface and fallback to software mac // Note: on OSPi, operating system handles interface allocation so 'wired' ignored bool OpenSprinkler::load_hardware_mac(unsigned char* mac, bool wired) { @@ -812,32 +769,13 @@ void OpenSprinkler::update_dev() { } #endif // end network init functions -#if defined(USE_DISPLAY) /** Initialize LCD */ +#if defined(USE_DISPLAY) void OpenSprinkler::lcd_start() { - -#if defined(USE_SSD1306) // initialize SSD1306 lcd.init(); lcd.begin(); flash_screen(); -#elif defined(USE_LCD) - // initialize 16x2 character LCD - // turn on lcd - lcd.init(1, PIN_LCD_RS, 255, PIN_LCD_EN, PIN_LCD_D4, PIN_LCD_D5, PIN_LCD_D6, PIN_LCD_D7, 0,0,0,0); - lcd.begin(); - - if (lcd.type() == LCD_STD) { - // this is standard 16x2 LCD - // set PWM frequency for adjustable LCD backlight and contrast - TCCR1B = 0x02; // increase division factor for faster clock - // turn on LCD backlight and contrast - lcd_set_brightness(); - lcd_set_contrast(); - } else { - // for I2C LCD, we don't need to do anything - } -#endif } #endif @@ -845,16 +783,11 @@ void OpenSprinkler::lcd_start() { /** Initialize pins, controller variables, LCD */ void OpenSprinkler::begin() { - -#if defined(ARDUINO) - Wire.begin(); // init I2C -#endif - hw_type = HW_TYPE_UNKNOWN; hw_rev = 0; -#if defined(ESP8266) // ESP8266 specific initializations - +#if defined(ESP8266) + Wire.begin(); // init I2C /* detect hardware revision type */ if(detect_i2c(MAIN_I2CADDR)) { // check if main PCF8574 exists /* assign revision 0 pins */ @@ -988,35 +921,13 @@ void OpenSprinkler::begin() { expanders[i] = NULL; detect_expanders(); -#else - - // shift register setup - pinMode(PIN_SR_OE, OUTPUT); - // pull shift register OE high to disable output - digitalWrite(PIN_SR_OE, HIGH); - pinMode(PIN_SR_LATCH, OUTPUT); - digitalWrite(PIN_SR_LATCH, HIGH); - - pinMode(PIN_SR_CLOCK, OUTPUT); - - #if defined(OSPI) - pin_sr_data = PIN_SR_DATA; - // detect RPi revision - unsigned int rev = detect_rpi_rev(); - if (rev==0x0002 || rev==0x0003) - pin_sr_data = PIN_SR_DATA_ALT; - // if this is revision 1, use PIN_SR_DATA_ALT - pinMode(pin_sr_data, OUTPUT); - #else - pinMode(PIN_SR_DATA, OUTPUT); - #endif - #endif #if defined(OSPI) -pinModeExt(PIN_BUTTON_1, INPUT_PULLUP); -pinModeExt(PIN_BUTTON_2, INPUT_PULLUP); -pinModeExt(PIN_BUTTON_3, INPUT_PULLUP); + Bus.begin(); // init I2C for OSPI + pinModeExt(PIN_BUTTON_1, INPUT_PULLUP); + pinModeExt(PIN_BUTTON_2, INPUT_PULLUP); + pinModeExt(PIN_BUTTON_3, INPUT_PULLUP); #endif // init masters_last_on array @@ -1035,9 +946,7 @@ pinModeExt(PIN_BUTTON_3, INPUT_PULLUP); digitalWrite(PIN_SR_OE, LOW); // Rain sensor port set up pinMode(PIN_SENSOR1, INPUT_PULLUP); - #if defined(PIN_SENSOR2) pinMode(PIN_SENSOR2, INPUT_PULLUP); - #endif #endif // Default controller status variables @@ -1061,94 +970,36 @@ pinModeExt(PIN_BUTTON_3, INPUT_PULLUP); digitalWriteExt(PIN_RFTX, LOW); } -#if defined(ARDUINO) // AVR SD and LCD functions - - #if defined(ESP8266) // OS3.0 specific detections - +#if defined(ESP8266) status.has_curr_sense = 1; // OS3.0 has current sensing capacility // measure baseline current baseline_current = 80; - - #else // OS 2.3 specific detections - - // detect hardware type - if (detect_i2c(MAC_CTRL_ID)) { - Wire.beginTransmission(MAC_CTRL_ID); - Wire.write(0x00); - Wire.endTransmission(); - Wire.requestFrom(MAC_CTRL_ID, 1); - unsigned char ret = Wire.read(); - if (ret == HW_TYPE_AC || ret == HW_TYPE_DC || ret == HW_TYPE_LATCH) { - hw_type = ret; - } else { - hw_type = HW_TYPE_AC; // if type not supported, make it AC - } - } - - if (hw_type == HW_TYPE_DC) { - pinMode(PIN_BOOST, OUTPUT); - digitalWrite(PIN_BOOST, LOW); - - pinMode(PIN_BOOST_EN, OUTPUT); - digitalWrite(PIN_BOOST_EN, LOW); - } - - // detect if current sensing pin is present - pinMode(PIN_CURR_DIGITAL, INPUT); - digitalWrite(PIN_CURR_DIGITAL, HIGH); // enable internal pullup - status.has_curr_sense = digitalRead(PIN_CURR_DIGITAL) ? 0 : 1; - digitalWrite(PIN_CURR_DIGITAL, LOW); - baseline_current = 0; - - #endif #endif + #if defined(USE_DISPLAY) lcd_start(); - - #if defined(USE_SSD1306) - lcd.createChar(ICON_ETHER_CONNECTED, _iconimage_ether_connected); - lcd.createChar(ICON_ETHER_DISCONNECTED, _iconimage_ether_disconnected); - lcd.createChar(ICON_WIFI_CONNECTED, _iconimage_wifi_connected); - lcd.createChar(ICON_WIFI_DISCONNECTED, _iconimage_wifi_disconnected); - #elif defined(USE_LCD) - lcd.createChar(ICON_ETHER_CONNECTED, _iconimage_connected); - lcd.createChar(ICON_ETHER_DISCONNECTED, _iconimage_disconnected); - #endif - + lcd.createChar(ICON_ETHER_CONNECTED, _iconimage_ether_connected); + lcd.createChar(ICON_ETHER_DISCONNECTED, _iconimage_ether_disconnected); + lcd.createChar(ICON_WIFI_CONNECTED, _iconimage_wifi_connected); + lcd.createChar(ICON_WIFI_DISCONNECTED, _iconimage_wifi_disconnected); lcd.createChar(ICON_REMOTEXT, _iconimage_remotext); lcd.createChar(ICON_RAINDELAY, _iconimage_raindelay); lcd.createChar(ICON_RAIN, _iconimage_rain); lcd.createChar(ICON_SOIL, _iconimage_soil); #endif -#if defined(ARDUINO) - #if defined(ESP8266) - lcd.setCursor(0,0); - lcd.print(F("Init file system")); - lcd.setCursor(0,1); - if(!LittleFS.begin()) { - // !!! flash init failed, stall as we cannot proceed - lcd.setCursor(0, 0); - lcd_print_pgm(PSTR("Error Code: 0x2D")); - delay(5000); - } - - state = OS_STATE_INITIAL; - - #else - - // set sd cs pin high to release SD - pinMode(PIN_SD_CS, OUTPUT); - digitalWrite(PIN_SD_CS, HIGH); - - if(!sd.begin(PIN_SD_CS, SPI_HALF_SPEED)) { - // !!! sd card not detected, stall as we cannot proceed - lcd.setCursor(0, 0); - lcd_print_pgm(PSTR("Error Code: 0x2D")); - while(1){} - } +#if defined(ESP8266) + lcd.setCursor(0,0); + lcd.print(F("Init file system")); + lcd.setCursor(0,1); + if(!LittleFS.begin()) { + // !!! flash init failed, stall as we cannot proceed + lcd.setCursor(0, 0); + lcd_print_pgm(PSTR("Error Code: 0x2D")); + delay(5000); + } - #endif + state = OS_STATE_INITIAL; // set button pins // enable internal pullup @@ -1162,6 +1013,18 @@ pinModeExt(PIN_BUTTON_3, INPUT_PULLUP); #else //DEBUG_PRINTLN(get_runtime_path()); #endif + + for (size_t i = 0; i < 4; i++) { + uint8_t address = 0x48 + i; +#if defined(ADS1115_HARDWARE) + if (detect_i2c(address)) { + ads1115_devices[i] = new ADS1115(address); + } +#else + // DEMO/SIM: instantiate all four mock chips so all 16 channels are available + ads1115_devices[i] = new ADS1115(address); +#endif + } } #if defined(ESP8266) @@ -1199,7 +1062,7 @@ void OpenSprinkler::latch_boost(int8_t volt) { uint32_t boost_timeout = millis() + (iopts[IOPT_BOOST_TIME]<<2); digitalWriteExt(PIN_BOOST, HIGH); // boost until either top voltage is reached or boost timeout is reached - while((long)(millis()-boost_timeout)<0 && analogRead(PIN_CURR_SENSE)5?delay_time:5); sensor1_off_timer = 0; } else { @@ -1495,7 +1341,7 @@ void OpenSprinkler::detect_binarysensor_status(time_os_t curr_time) { } } else { if(!sensor1_off_timer) { - ulong delay_time = (ulong)iopts[IOPT_SENSOR1_OFF_DELAY]*60; + uint32_t delay_time = (uint32_t)iopts[IOPT_SENSOR1_OFF_DELAY]*60; sensor1_off_timer = curr_time + (delay_time>5?delay_time:5); sensor1_on_timer = 0; } else { @@ -1506,8 +1352,6 @@ void OpenSprinkler::detect_binarysensor_status(time_os_t curr_time) { } } -// ESP8266 is guaranteed to have sensor 2 -#if defined(ESP8266) || defined(PIN_SENSOR2) if(iopts[IOPT_SENSOR2_TYPE]==SENSOR_TYPE_RAIN || iopts[IOPT_SENSOR2_TYPE]==SENSOR_TYPE_SOIL) { if(hw_rev>=2) pinMode(PIN_SENSOR2, INPUT_PULLUP); // this seems necessary for OS 3.2 unsigned char val = digitalReadExt(PIN_SENSOR2); @@ -1515,7 +1359,7 @@ void OpenSprinkler::detect_binarysensor_status(time_os_t curr_time) { if(status.sensor2) { if(!sensor2_on_timer) { // add minimum of 5 seconds on delay - ulong delay_time = (ulong)iopts[IOPT_SENSOR2_ON_DELAY]*60; + uint32_t delay_time = (uint32_t)iopts[IOPT_SENSOR2_ON_DELAY]*60; sensor2_on_timer = curr_time + (delay_time>5?delay_time:5); sensor2_off_timer = 0; } else { @@ -1525,7 +1369,7 @@ void OpenSprinkler::detect_binarysensor_status(time_os_t curr_time) { } } else { if(!sensor2_off_timer) { - ulong delay_time = (ulong)iopts[IOPT_SENSOR2_OFF_DELAY]*60; + uint32_t delay_time = (uint32_t)iopts[IOPT_SENSOR2_OFF_DELAY]*60; sensor2_off_timer = curr_time + (delay_time>5?delay_time:5); sensor2_on_timer = 0; } else { @@ -1535,8 +1379,6 @@ void OpenSprinkler::detect_binarysensor_status(time_os_t curr_time) { } } } - -#endif } /** Return program switch status */ @@ -1553,7 +1395,7 @@ unsigned char OpenSprinkler::detect_programswitch_status(time_os_t curr_time) { ret |= 0x01; } } -#if defined(ESP8266) || defined(PIN_SENSOR2) + if(iopts[IOPT_SENSOR2_TYPE]==SENSOR_TYPE_PSWITCH) { static unsigned char sensor2_hist = 0; if(hw_rev>=2) pinMode(PIN_SENSOR2, INPUT_PULLUP); // this seems necessary for OS 3.2 @@ -1563,7 +1405,7 @@ unsigned char OpenSprinkler::detect_programswitch_status(time_os_t curr_time) { ret |= 0x02; } } -#endif + return ret; } @@ -1587,7 +1429,7 @@ void OpenSprinkler::sensor_resetall() { * ESP8266's analog reference voltage is 1.0 instead of 3.3, therefore * it's further discounted by 1/3.3 */ -#if defined(ARDUINO) +#if defined(ESP8266) uint16_t OpenSprinkler::read_current(bool use_ema) { static uint16_t ema = 0; // exponential moving average static float scale = -1; @@ -1615,36 +1457,24 @@ uint16_t OpenSprinkler::read_current(bool use_ema) { #endif /** Read the number of 8-station expansion boards */ -// AVR has capability to detect number of expansion boards +// Arduino has capability to detect number of expansion boards int OpenSprinkler::detect_exp() { -#if defined(ARDUINO) - #if defined(ESP8266) +#if defined(ESP8266) // detect the highest expansion board index int n; for(n=4;n>=0;n--) { if(detect_i2c(EXP_I2CADDR_BASE+n)) break; } return (n+1)*2; - #else - // OpenSprinkler uses voltage divider to detect expansion boards - // Master controller has a 1.6K pull-up; - // each expansion board (8 stations) has 2x 4.7K pull-down connected in parallel; - // so the exact ADC value for n expansion boards is: - // ADC = 1024 * 9.4 / (10 + 9.4 * n) - // Reverse this fomular we have: - // n = (1024 * 9.4 / ADC - 9.4) / 1.6 - int n = (int)((1024 * 9.4 / analogRead(PIN_EXP_SENSE) - 9.4) / 1.6 + 0.33); - return n; - #endif #else return -1; #endif } -/** Convert hex code to ulong integer */ -static ulong hex2ulong(unsigned char *code, unsigned char len) { +/** Convert hex code to uint32_t integer */ +static uint32_t hex2uint32_t(unsigned char *code, unsigned char len) { char c; - ulong v = 0; + uint32_t v = 0; for(unsigned char i=0;itiming = 0; // temporarily set it to 0 if(data->version=='H') { // this is version G rf code data (25 bytes long including version signature at the beginning) - code->on = hex2ulong(data->on, sizeof(data->on)); - code->off = hex2ulong(data->off, sizeof(data->off)); - code->timing = hex2ulong(data->timing, sizeof(data->timing)); - code->protocol = hex2ulong(data->protocol, sizeof(data->protocol)); - code->bitlength = hex2ulong(data->bitlength, sizeof(data->bitlength)); + code->on = hex2uint32_t(data->on, sizeof(data->on)); + code->off = hex2uint32_t(data->off, sizeof(data->off)); + code->timing = hex2uint32_t(data->timing, sizeof(data->timing)); + code->protocol = hex2uint32_t(data->protocol, sizeof(data->protocol)); + code->bitlength = hex2uint32_t(data->bitlength, sizeof(data->bitlength)); } else { // this is classic rf code data (16 bytes long, assuming protocol=1 and bitlength=24) RFStationDataClassic *classic = (RFStationDataClassic*)data; - code->on = hex2ulong(classic->on, sizeof(classic->on)); - code->off = hex2ulong(classic->off, sizeof(classic->off)); - code->timing = hex2ulong(classic->timing, sizeof(classic->timing)); + code->on = hex2uint32_t(classic->on, sizeof(classic->on)); + code->off = hex2uint32_t(classic->off, sizeof(classic->off)); + code->timing = hex2uint32_t(classic->timing, sizeof(classic->timing)); code->protocol = 1; code->bitlength = 24; } @@ -1769,11 +1599,17 @@ unsigned char OpenSprinkler::bound_to_master(unsigned char sid, unsigned char ma switch (mas) { case MASTER_1: - attributes= attrib_mas[bid]; + attributes = attrib_mas[bid]; break; case MASTER_2: attributes = attrib_mas2[bid]; break; + case MASTER_3: + attributes = attrib_mas3[bid]; + break; + case MASTER_4: + attributes = attrib_mas4[bid]; + break; default: break; } @@ -1804,6 +1640,8 @@ void OpenSprinkler::attribs_save() { at.igs2= (attrib_igs2[bid]>>s) & 1; at.igrd= (attrib_igrd[bid]>>s) & 1; at.dis = (attrib_dis[bid]>>s) & 1; + at.mas3= (attrib_mas3[bid]>>s) & 1; + at.mas4= (attrib_mas4[bid]>>s) & 1; at.gid = get_station_gid(sid); set_station_gid(sid, at.gid); @@ -1833,6 +1671,8 @@ void OpenSprinkler::attribs_load() { memset(attrib_mas, 0, nboards); memset(attrib_igs, 0, nboards); memset(attrib_mas2, 0, nboards); + memset(attrib_mas3, 0, nboards); + memset(attrib_mas4, 0, nboards); memset(attrib_igs2, 0, nboards); memset(attrib_igrd, 0, nboards); memset(attrib_dis, 0, nboards); @@ -1845,6 +1685,8 @@ void OpenSprinkler::attribs_load() { attrib_mas[bid] |= (at.mas<setInsecure(); - bool mfln = _c->probeMaxFragmentLength(server, port, 512); - DEBUG_PRINTF("MFLN supported: %s\n", mfln ? "yes" : "no"); - if (mfln) { - _c->setBufferSizes(512, 512); - } else { - _c->setBufferSizes(2048, 2048); - } - client = _c; + if(usessl) { + WiFiClientSecure *_c = new WiFiClientSecure(); + _c->setInsecure(); + bool mfln = _c->probeMaxFragmentLength(server, port, 512); + DEBUG_PRINTF("MFLN supported: %s\n", mfln ? "yes" : "no"); + if (mfln) { + _c->setBufferSizes(512, 512); } else { - client = new WiFiClient(); + _c->setBufferSizes(2048, 2048); } - #else - client = new EthernetClient(); - #endif + client = _c; + } else { + client = new WiFiClient(); + } #define HTTP_CONNECT_NTRIES 3 unsigned char tries = 0; @@ -2084,7 +1922,7 @@ int8_t OpenSprinkler::send_http_request(const char* server, uint16_t port, char* uint32_t stoptime = millis()+timeout; int pos = 0; -#if defined(ARDUINO) +#if defined(ESP8266) // with ESP8266 core 3.0.2, client->connected() is not always true even if there is more data // so this loop is going to take longer than it should be // todo: can consider using HTTPClient for ESP8266 @@ -2095,7 +1933,7 @@ int8_t OpenSprinkler::send_http_request(const char* server, uint16_t port, char* client->read((uint8_t*)ether_buffer+pos, nbytes); pos+=nbytes; } - if((long)(millis()-stoptime)>0) { // overflow proof + if((int32_t)((uint32_t)millis()-stoptime)>0) { // overflow proof DEBUG_PRINTLN(F("host timeout occured")); //return HTTP_RQT_TIMEOUT; // instead of returning with timeout, we'll work with data received so far break; @@ -2146,8 +1984,8 @@ void OpenSprinkler::switch_remotestation(RemoteIPStationData *data, bool turnon, RemoteIPStationData copy; memcpy((char*)©, (char*)data, sizeof(RemoteIPStationData)); - uint32_t ip4 = hex2ulong(copy.ip, sizeof(copy.ip)); - uint16_t port = (uint16_t)hex2ulong(copy.port, sizeof(copy.port)); + uint32_t ip4 = hex2uint32_t(copy.ip, sizeof(copy.ip)); + uint16_t port = (uint16_t)hex2uint32_t(copy.port, sizeof(copy.port)); unsigned char ip[4]; ip[0] = ip4>>24; @@ -2156,7 +1994,7 @@ void OpenSprinkler::switch_remotestation(RemoteIPStationData *data, bool turnon, ip[3] = ip4&0xff; char *p = tmp_buffer; - BufferFiller bf = BufferFiller(p, TMP_BUFFER_SIZE*2); + BufferFiller bf = BufferFiller(p, TMP_BUFFER_ALLOC_SIZE); // if turning on the zone and duration is defined, give duration as the timer value // otherwise: // if autorefresh is defined, we give a fixed duration each time, and auto refresh will renew it periodically @@ -2171,7 +2009,7 @@ void OpenSprinkler::switch_remotestation(RemoteIPStationData *data, bool turnon, } bf.emit_p(PSTR("GET /cm?pw=$O&sid=$D&en=$D&t=$D"), SOPT_PASSWORD, - (int)hex2ulong(copy.sid, sizeof(copy.sid)), + (int)hex2uint32_t(copy.sid, sizeof(copy.sid)), turnon, timer); bf.emit_p(PSTR(" HTTP/1.0\r\nHOST: $D.$D.$D.$D\r\n"), ip[0],ip[1],ip[2],ip[3]); @@ -2195,7 +2033,7 @@ void OpenSprinkler::switch_remotestation(RemoteOTCStationData *data, bool turnon memcpy((char*)©, (char*)data, sizeof(RemoteOTCStationData)); copy.token[sizeof(copy.token)-1] = 0; // ensure the string ends properly char *p = tmp_buffer; - BufferFiller bf = BufferFiller(p, TMP_BUFFER_SIZE*2); + BufferFiller bf = BufferFiller(p, TMP_BUFFER_ALLOC_SIZE); // if turning on the zone and duration is defined, give duration as the timer value // otherwise: // if autorefresh is defined, we give a fixed duration each time, and auto refresh will renew it periodically @@ -2211,7 +2049,7 @@ void OpenSprinkler::switch_remotestation(RemoteOTCStationData *data, bool turnon bf.emit_p(PSTR("GET /forward/v1/$S/cm?pw=$O&sid=$D&en=$D&t=$D"), copy.token, SOPT_PASSWORD, - (int)hex2ulong(copy.sid, sizeof(copy.sid)), + (int)hex2uint32_t(copy.sid, sizeof(copy.sid)), turnon, timer); bf.emit_p(PSTR(" HTTP/1.0\r\nHOST: $S\r\nConnection:close\r\n"), DEFAULT_OTC_SERVER_APP); @@ -2236,7 +2074,7 @@ void OpenSprinkler::switch_httpstation(HTTPStationData *data, bool turnon, bool char * cmd = turnon ? on_cmd : off_cmd; char *p = tmp_buffer; - BufferFiller bf = BufferFiller(p, TMP_BUFFER_SIZE*2); + BufferFiller bf = BufferFiller(p, TMP_BUFFER_ALLOC_SIZE); if(cmd==NULL || server==NULL) return; // proceed only if cmd and server are valid @@ -2262,7 +2100,7 @@ void OpenSprinkler::pre_factory_reset() { /** Factory reset */ void OpenSprinkler::factory_reset() { -#if defined(ARDUINO) +#if defined(ESP8266) lcd_print_line_clear_pgm(PSTR("Factory reset"), 0); lcd_print_line_clear_pgm(PSTR("Please Wait..."), 1); #else @@ -2274,7 +2112,7 @@ void OpenSprinkler::factory_reset() { // reset string options by first wiping the file clean then write default values memset(tmp_buffer, 0, MAX_SOPTS_SIZE); for(int i=0; i NUM_IOPTS) load_count = NUM_IOPTS; + file_read_block(IOPTS_FILENAME, iopts, 0, load_count); nboards = iopts[IOPT_EXT_BOARDS]+1; nstations = nboards * 8; status.enabled = iopts[IOPT_DEVICE_ENABLE]; @@ -2547,6 +2390,14 @@ void OpenSprinkler::populate_master() { masters[MASTER_2][MASOPT_SID] = iopts[IOPT_MASTER_STATION_2]; masters[MASTER_2][MASOPT_ON_ADJ] = iopts[IOPT_MASTER_ON_ADJ_2]; masters[MASTER_2][MASOPT_OFF_ADJ] = iopts[IOPT_MASTER_OFF_ADJ_2]; + + masters[MASTER_3][MASOPT_SID] = iopts[IOPT_MASTER_STATION_3]; + masters[MASTER_3][MASOPT_ON_ADJ] = iopts[IOPT_MASTER_ON_ADJ_3]; + masters[MASTER_3][MASOPT_OFF_ADJ] = iopts[IOPT_MASTER_OFF_ADJ_3]; + + masters[MASTER_4][MASOPT_SID] = iopts[IOPT_MASTER_STATION_4]; + masters[MASTER_4][MASOPT_ON_ADJ] = iopts[IOPT_MASTER_ON_ADJ_4]; + masters[MASTER_4][MASOPT_OFF_ADJ] = iopts[IOPT_MASTER_OFF_ADJ_4]; } /** Save integer options to file */ @@ -2574,13 +2425,13 @@ String OpenSprinkler::sopt_load(unsigned char oid) { /** Save a string option to file */ bool OpenSprinkler::sopt_save(unsigned char oid, const char *buf) { // smart save: if value hasn't changed, don't write - if(file_cmp_block(SOPTS_FILENAME, buf, (ulong)MAX_SOPTS_SIZE*oid)==0) return false; + if(file_cmp_block(SOPTS_FILENAME, buf, (uint32_t)MAX_SOPTS_SIZE*oid)==0) return false; int len = strlen(buf); if(len>=MAX_SOPTS_SIZE) { - file_write_block(SOPTS_FILENAME, buf, (ulong)MAX_SOPTS_SIZE*oid, MAX_SOPTS_SIZE); + file_write_block(SOPTS_FILENAME, buf, (uint32_t)MAX_SOPTS_SIZE*oid, MAX_SOPTS_SIZE); } else { // copy ending 0 too - file_write_block(SOPTS_FILENAME, buf, (ulong)MAX_SOPTS_SIZE*oid, len+1); + file_write_block(SOPTS_FILENAME, buf, (uint32_t)MAX_SOPTS_SIZE*oid, len+1); } return true; } @@ -2616,15 +2467,167 @@ void OpenSprinkler::raindelay_stop() { nvdata_save(); } +/** Sensor functions */ + +void list_all_files() { +#if defined(ESP8266) + Serial.println(PSTR("\n--- Flash File System Map ---")); + Serial.printf("%-30s %10s\n", "Filename", "Size (B)"); + Serial.println(PSTR("------------------------------------------")); + + uint32_t totalUsed = 0; + uint32_t fileCount = 0; + + // Root directory (skip directory entries — they appear as dot-less names with size 0) + Dir dir = LittleFS.openDir("/"); + while (dir.next()) { + if (dir.fileName().indexOf('.') < 0) continue; + Serial.printf("%-30s %10u\n", ("/" + dir.fileName()).c_str(), dir.fileSize()); + totalUsed += dir.fileSize(); + fileCount++; + } + + // /logs/ subdirectory + uint32_t logUsed = 0; + uint32_t logCount = 0; + Dir logdir = LittleFS.openDir("/logs/"); + while (logdir.next()) { + String fullname = "/logs/" + logdir.fileName(); + Serial.printf("%-30s %10u\n", fullname.c_str(), logdir.fileSize()); + logUsed += logdir.fileSize(); + logCount++; + } + totalUsed += logUsed; + fileCount += logCount; + + // Filesystem totals + FSInfo fs_info; + LittleFS.info(fs_info); + + Serial.println(PSTR("------------------------------------------")); + Serial.printf("Total Files: %u (logs/: %u)\n", fileCount, logCount); + Serial.printf("Total Size: %u bytes (logs/: %u bytes)\n", totalUsed, logUsed); + Serial.printf("FS Total: %u bytes\n", fs_info.totalBytes); + Serial.printf("FS Used: %u bytes\n", fs_info.usedBytes); + Serial.printf("FS Free: %u bytes\n", fs_info.totalBytes - fs_info.usedBytes); + Serial.printf("Block Size: %u bytes\n", fs_info.blockSize); + Serial.println(PSTR("------------------------------------------\n")); +#endif +} + + +void OpenSprinkler::log_sensor(uint8_t sid, float value) { + // Read central header; create/recreate if missing or version mismatch + SensorLogHeader hdr = {}; + bool hdr_valid = false; + os_file_type hfile = open_sensor_log_header(FileOpenMode::Read); + if (hfile) { + int n = file_read(hfile, &hdr, sizeof(hdr)); + file_close(hfile); + hdr_valid = (n == (int)sizeof(hdr) && + hdr.magic == SENSOR_LOG_MAGIC && + hdr.version == SENSOR_LOG_VERSION); + } + + if (!hdr_valid) { + // First use or firmware upgrade: ensure directory exists, wipe stale data files, write fresh header + ensure_log_dir(); + char fname[24]; + for (uint16_t i = 0; i < SENSOR_LOG_MAX_FILES; i++) { + get_sensor_log_filename(fname, i); + remove_file(fname); + } + hdr = {}; + hdr.magic = SENSOR_LOG_MAGIC; + hdr.version = SENSOR_LOG_VERSION; + hdr.max_files = SENSOR_LOG_MAX_FILES; + hdr.records_per_file = SENSOR_LOG_RECORDS_PER_FILE; + hfile = open_sensor_log_header(FileOpenMode::WriteTruncate); + if (!hfile) { + DEBUG_PRINTLN("Failed to create sensor log header"); + return; + } + file_write(hfile, &hdr, sizeof(hdr)); + file_close(hfile); + } + + // Open current data file for appending; infer record count from file size + os_file_type dfile = open_sensor_log(hdr.cur_file, FileOpenMode::Append); + if (!dfile) { + DEBUG_PRINTLN("Failed to open sensor log data file"); + return; + } + uint32_t count = file_size(dfile) / sizeof(SensorLogRecord); + + if (count >= hdr.records_per_file) { + // Current file is full — rotate to next slot + file_close(dfile); + hdr.cur_file = (uint16_t)((hdr.cur_file + 1) % hdr.max_files); + if (hdr.cur_file == 0 && !hdr.wrapped) hdr.wrapped = 1; + + // Evict the file at the new slot (oldest data) + remove_sensor_log(hdr.cur_file); + + // Persist updated header (only written on rotation, not on every record) + hfile = open_sensor_log_header(FileOpenMode::WriteTruncate); + if (hfile) { file_write(hfile, &hdr, sizeof(hdr)); file_close(hfile); } + + dfile = open_sensor_log(hdr.cur_file, FileOpenMode::Append); + if (!dfile) { + DEBUG_PRINTLN("Failed to open new sensor log data file"); + return; + } + } + + SensorLogRecord rec = {}; + rec.timestamp = now(); + rec.value = value; + rec.uuid = sensors[sid].uuid; + file_write(dfile, &rec, sizeof(rec)); + file_close(dfile); +} + +void OpenSprinkler::poll_sensors() { + for (uint8_t i = 0; i < nsensors; i++) { + sensor_memory_t &mem = sensors[i]; + if (!mem.interval || !(mem.flag & (1 << SENSOR_FLAG_ENABLE))) continue; + + if ((int32_t)((uint32_t)millis() - mem.next_update) <= 0) continue; + + Sensor *sensor = Sensor::get(i); + if (!sensor) { + // Can't load sensor from disk — value is now stale + mem.status |= SENSOR_STATUS_STALE; + continue; + } + + uint8_t new_status = 0; + float new_value = sensor->get_new_value(&new_status); + + if (new_status & SENSOR_STATUS_ERROR) { + // Hardware fault — preserve last good value but flag error; clear stale + mem.status = (mem.status & SENSOR_STATUS_VALID) | SENSOR_STATUS_ERROR; + } else { + mem.value = new_value; + mem.status = new_status; // VALID + CLAMPED_* as appropriate; clears ERROR/STALE + } + mem.next_update = millis() + (mem.interval * 1000 * 60); + + if (mem.flag & (1 << SENSOR_FLAG_LOG)) { + os.log_sensor(i, mem.value); + } + } +} + +float OpenSprinkler::get_sensor_weather_data(WeatherAction action) { + return NAN; // TODO make function for WeatherSensor +} + /** LCD and button functions */ #if defined(USE_DISPLAY) -#if defined(ARDUINO) // AVR LCD and button functions +#if defined(ESP8266) // Arduino LCD and button functions /** print a program memory string */ -#if defined(ESP8266) void OpenSprinkler::lcd_print_pgm(PGM_P str) { -#else -void OpenSprinkler::lcd_print_pgm(PGM_P PROGMEM str) { -#endif uint8_t c; while((c=pgm_read_byte(str++))!= '\0') { lcd.print((char)c); @@ -2632,11 +2635,7 @@ void OpenSprinkler::lcd_print_pgm(PGM_P PROGMEM str) { } /** print a program memory string to a given line with clearing */ -#if defined(ESP8266) void OpenSprinkler::lcd_print_line_clear_pgm(PGM_P str, unsigned char line) { -#else -void OpenSprinkler::lcd_print_line_clear_pgm(PGM_P PROGMEM str, unsigned char line) { -#endif lcd.setCursor(0, line); uint8_t c; int8_t cnt = 0; @@ -2678,42 +2677,26 @@ void OpenSprinkler::lcd_print_2digit(int v) /** print time to a given line */ void OpenSprinkler::lcd_print_time(time_os_t t) { -#if defined(USE_SSD1306) lcd.setAutoDisplay(false); -#endif lcd.setCursor(0, 0); lcd_print_2digit(hour(t)); - lcd_print_pgm(PSTR(":")); - lcd_print_2digit(minute(t)); - lcd_print_pgm(PSTR(" ")); - // each weekday string has 3 characters + ending 0 lcd_print_pgm(days_str+4*weekday_today()); - lcd_print_pgm(PSTR(" ")); - lcd_print_pgm(months_str+4*(month(t)-1)); - lcd_print_pgm(PSTR("-")); - lcd_print_2digit(day(t)); -#if defined(USE_SSD1306) lcd.display(); lcd.setAutoDisplay(true); -#endif } /** print ip address */ void OpenSprinkler::lcd_print_ip(const unsigned char *ip, unsigned char endian) { -#if defined(USE_SSD1306) lcd.clear(0, 1); lcd.setAutoDisplay(false); -#elif defined(USE_LCD) - lcd.clear(); -#endif lcd.setCursor(0, 0); for (unsigned char i=0; i<4; i++) { lcd.print(endian ? (int)ip[3-i] : (int)ip[i]); @@ -2722,18 +2705,13 @@ void OpenSprinkler::lcd_print_ip(const unsigned char *ip, unsigned char endian) lcd_print_pgm(PSTR(".")); } } - - #if defined(USE_SSD1306) - lcd.display(); - lcd.setAutoDisplay(true); - #endif + lcd.display(); + lcd.setAutoDisplay(true); } /** print mac address */ void OpenSprinkler::lcd_print_mac(const unsigned char *mac) { - #if defined(USE_SSD1306) - lcd.setAutoDisplay(false); // reduce screen drawing time by turning off display() when drawing individual characters - #endif + lcd.setAutoDisplay(false); // reduce screen drawing time by turning off display() when drawing individual characters lcd.setCursor(0, 0); for(unsigned char i=0; i<6; i++) { if(i) { @@ -2744,7 +2722,7 @@ void OpenSprinkler::lcd_print_mac(const unsigned char *mac) { lcd.print((mac[i]&0x0F), HEX); if(i==4) lcd.setCursor(0, 1); } - #if defined(ARDUINO) + #if defined(ESP8266) if(useEth) { lcd_print_pgm(PSTR(" (Ether MAC)")); } else { @@ -2754,17 +2732,13 @@ void OpenSprinkler::lcd_print_mac(const unsigned char *mac) { lcd_print_pgm(PSTR(" (MAC)")); #endif - #if defined(USE_SSD1306) - lcd.display(); - lcd.setAutoDisplay(true); - #endif + lcd.display(); + lcd.setAutoDisplay(true); } /** print station bits */ void OpenSprinkler::lcd_print_screen(char c) { -#if defined(USE_SSD1306) lcd.setAutoDisplay(false); // reduce screen drawing time by turning off display() when drawing individual characters -#endif lcd.setCursor(0, 1); if (status.display_board == 0) { lcd.print(F("MC:")); // Master controller is display as 'MC' @@ -2784,6 +2758,10 @@ void OpenSprinkler::lcd_print_screen(char c) { lcd.print((bitvalue&1) ? c : 'M'); // print master station } else if (sid == iopts[IOPT_MASTER_STATION_2]) { lcd.print((bitvalue&1) ? c : 'N'); // print master2 station + } else if (sid == iopts[IOPT_MASTER_STATION_3]) { + lcd.print((bitvalue&1) ? c : 'U'); // print master3 station + } else if (sid == iopts[IOPT_MASTER_STATION_4]) { + lcd.print((bitvalue&1) ? c : 'V'); // print master4 station } else { lcd.print((bitvalue&1) ? c : '_'); } @@ -2851,7 +2829,6 @@ void OpenSprinkler::lcd_print_screen(char c) { lcd.write(status.network_fails>2?ICON_ETHER_DISCONNECTED:ICON_ETHER_CONNECTED); // if network failure detection is more than 2, display disconnect icon #endif -#if defined(USE_SSD1306) #if defined(ESP8266) if(useEth || (get_wifi_mode()==WIFI_MODE_STA && WiFi.status()==WL_CONNECTED && WiFi.localIP())) { #else @@ -2883,11 +2860,9 @@ void OpenSprinkler::lcd_print_screen(char c) { lcd.clear(2, 2); } } - - lcd.display(); lcd.setAutoDisplay(true); -#endif + } /** print a version number */ @@ -2932,8 +2907,12 @@ void OpenSprinkler::lcd_print_option(int i) { break; case IOPT_MASTER_ON_ADJ: case IOPT_MASTER_ON_ADJ_2: + case IOPT_MASTER_ON_ADJ_3: + case IOPT_MASTER_ON_ADJ_4: case IOPT_MASTER_OFF_ADJ: case IOPT_MASTER_OFF_ADJ_2: + case IOPT_MASTER_OFF_ADJ_3: + case IOPT_MASTER_OFF_ADJ_4: case IOPT_STATION_DELAY_TIME: { int16_t t=water_time_decode_signed(iopts[i]); @@ -2962,7 +2941,7 @@ void OpenSprinkler::lcd_print_option(int i) { lcd.print((int)iopts[i]); break; case IOPT_BOOST_TIME: - #if defined(ARDUINO) + #if defined(ESP8266) if(hw_type==HW_TYPE_AC) { lcd.print('-'); } else { @@ -2975,7 +2954,7 @@ void OpenSprinkler::lcd_print_option(int i) { break; case IOPT_I_MIN_THRESHOLD: case IOPT_I_MAX_LIMIT: - #if defined(ARDUINO) + #if defined(ESP8266) lcd.print((int)iopts[i]*10); lcd_print_pgm(PSTR(" mA")); #else @@ -2984,7 +2963,7 @@ void OpenSprinkler::lcd_print_option(int i) { break; case IOPT_LATCH_ON_VOLTAGE: case IOPT_LATCH_OFF_VOLTAGE: - #if defined(ARDUINO) + #if defined(ESP8266) if(hw_type==HW_TYPE_LATCH) { lcd.print((int)iopts[i]); lcd.print('V'); @@ -3018,7 +2997,10 @@ void OpenSprinkler::lcd_print_option(int i) { break; } if (i==IOPT_WATER_PERCENTAGE) lcd_print_pgm(PSTR("%")); - else if (i==IOPT_MASTER_ON_ADJ || i==IOPT_MASTER_OFF_ADJ || i==IOPT_MASTER_ON_ADJ_2 || i==IOPT_MASTER_OFF_ADJ_2) + else if (i==IOPT_MASTER_ON_ADJ || i==IOPT_MASTER_OFF_ADJ || + i==IOPT_MASTER_ON_ADJ_2 || i==IOPT_MASTER_OFF_ADJ_2 || + i==IOPT_MASTER_ON_ADJ_3 || i==IOPT_MASTER_OFF_ADJ_3 || + i==IOPT_MASTER_ON_ADJ_4 || i==IOPT_MASTER_OFF_ADJ_4) lcd_print_pgm(PSTR(" sec")); } @@ -3075,7 +3057,7 @@ unsigned char OpenSprinkler::button_read(unsigned char waitmode) return ret; } -#if defined(ARDUINO) +#if defined(ESP8266) /** user interface for setting options during startup */ void OpenSprinkler::ui_set_options(int oid) @@ -3116,8 +3098,10 @@ void OpenSprinkler::ui_set_options(int oid) if (i==IOPT_USE_DHCP && iopts[i]) i += 9; // if use DHCP, skip static ip set else if (i==IOPT_HTTPPORT_0) i+=2; // skip IOPT_HTTPPORT_1 else if (i==IOPT_PULSE_RATE_0) i+=2; // skip IOPT_PULSE_RATE_1 - else if (i==IOPT_MASTER_STATION && iopts[i]==0) i+=3; // if not using master station, skip master on/off adjust including two retired options - else if (i==IOPT_MASTER_STATION_2&& iopts[i]==0) i+=3; // if not using master2, skip master2 on/off adjust + else if (i==IOPT_MASTER_STATION && iopts[i]==0) i+=3; // if not using master, skip on/off adjust + else if (i==IOPT_MASTER_STATION_2 && iopts[i]==0) i+=3; // if not using master2, skip on/off adjust + else if (i==IOPT_MASTER_STATION_3 && iopts[i]==0) i+=3; // if not using master3, skip on/off adjust + else if (i==IOPT_MASTER_STATION_4 && iopts[i]==0) i+=3; // if not using master4, skip on/off adjust else { i = (i+1) % NUM_IOPTS; } @@ -3127,11 +3111,7 @@ void OpenSprinkler::ui_set_options(int oid) if (hw_type==HW_TYPE_AC && i==IOPT_BOOST_TIME) i++; // skip boost time for non-DC controller if (i==IOPT_LATCH_ON_VOLTAGE && hw_type!=HW_TYPE_LATCH) i+= 2; // skip latch voltage defs for non-latch controllers if (i==IOPT_TARGET_PD_VOLTAGE && !(hw_rev==4 && hw_type==HW_TYPE_DC)) i++; // skip target pd voltage if not 3.4 or not DC type - #if defined(ESP8266) else if (lcd.type()==LCD_I2C && i==IOPT_LCD_CONTRAST) i+=3; - #else - else if (lcd.type()==LCD_I2C && i==IOPT_LCD_CONTRAST) i+=2; - #endif // string options are not editable } break; @@ -3143,56 +3123,22 @@ void OpenSprinkler::ui_set_options(int oid) } lcd.noBlink(); } - -#else #endif // end of LCD and button functions /** Set LCD contrast (using PWM) */ void OpenSprinkler::lcd_set_contrast() { -#ifdef PIN_LCD_CONTRAST - // set contrast is only valid for standard LCD - if (lcd.type()==LCD_STD) { - pinMode(PIN_LCD_CONTRAST, OUTPUT); - analogWrite(PIN_LCD_CONTRAST, iopts[IOPT_LCD_CONTRAST]); - } -#endif } /** Set LCD brightness (using PWM) */ void OpenSprinkler::lcd_set_brightness(unsigned char value) { -#if defined(PIN_LCD_BACKLIGHT) - #if defined(OS_AVR) - if (lcd.type()==LCD_I2C) { - if (value) lcd.backlight(); - else { - // turn off LCD backlight - // only if dimming value is set to 0 - if(iopts[IOPT_LCD_DIMMING]==0) lcd.noBacklight(); - else lcd.backlight(); - } - } - #endif - if (lcd.type()==LCD_STD) { - pinMode(PIN_LCD_BACKLIGHT, OUTPUT); - if (value) { - analogWrite(PIN_LCD_BACKLIGHT, 255-iopts[IOPT_LCD_BACKLIGHT]); - } else { - analogWrite(PIN_LCD_BACKLIGHT, 255-iopts[IOPT_LCD_DIMMING]); - } - } - -#elif defined(USE_SSD1306) if (value) {lcd.displayOn();lcd.setBrightness(255); } else { if(iopts[IOPT_LCD_DIMMING]==0) lcd.displayOff(); else { lcd.displayOn();lcd.setBrightness(iopts[IOPT_LCD_DIMMING]); } } -#endif } - - -#if defined(USE_SSD1306) +#if defined(USE_DISPLAY) #include "images.h" void OpenSprinkler::flash_screen() { lcd.setCursor(0, -1); diff --git a/OpenSprinkler.h b/OpenSprinkler.h index 15bac6d3b..8b0505482 100644 --- a/OpenSprinkler.h +++ b/OpenSprinkler.h @@ -21,9 +21,7 @@ * . */ - -#ifndef _OPENSPRINKLER_H -#define _OPENSPRINKLER_H +#pragma once #include "types.h" #include "defines.h" @@ -32,30 +30,26 @@ #include "images.h" #include "mqtt.h" #include "RCSwitch.h" +#include +#include -#if defined(ARDUINO) // headers for Arduino +#if defined(ESP8266) // headers for Arduino #include #include #include #include #include "I2CRTC.h" - #if defined(ESP8266) // for ESP8266 - #include - #include - #include - #include - #include - #include - #include - #include "espconnect.h" - #include "EMailSender.h" - #include "ch224.h" - #else // for AVR - #include - #include - #include "LiquidCrystal.h" - #endif + #include + #include + #include + #include + #include + #include + #include + #include "espconnect.h" + #include "EMailSender.h" + #include "ch224.h" #else // headers for RPI/LINUX #include @@ -69,16 +63,17 @@ #include "smtp.h" #endif // end of headers -#if defined(USE_LCD) - #include "LiquidCrystal.h" -#endif - -#if defined(USE_SSD1306) +#if defined(USE_DISPLAY) #include "SSD1306Display.h" #endif -#if defined(ARDUINO) - #if defined(ESP8266) +#include "sensors/sensor.h" +#include "sensors/aggregate_sensor.h" +#include "sensors/weather_sensor.h" +#include "ads1115.h" +#include "sensors/ads1115_sensor.h" + +#if defined(ESP8266) extern ESP8266WebServer *update_server; extern ENC28J60lwIP enc28j60; extern Wiznet5500lwIP w5500; @@ -110,28 +105,21 @@ } }; extern lwipEth eth; - #else - // AVR specific - #endif extern bool useEth; #else // OSPI/Linux specific #endif -#if defined(USE_OTF) - extern OTF::OpenThingsFramework *otf; -#else - extern EthernetServer *m_server; - extern bool useEth; -#endif +extern OTF::OpenThingsFramework *otf; /** Non-volatile data structure */ struct NVConData { - uint16_t sunrise_time; // sunrise time (in minutes) - uint16_t sunset_time; // sunset time (in minutes) - uint32_t rd_stop_time; // rain delay stop time - uint32_t external_ip; // external ip - uint8_t reboot_cause; // reboot cause + uint16_t sunrise_time; // sunrise time (in minutes) + uint16_t sunset_time; // sunset time (in minutes) + uint32_t rd_stop_time; // rain delay stop time + uint32_t external_ip; // external ip + uint8_t reboot_cause; // reboot cause + uint16_t last_sensor_uuid; // counter for sensor UUID generation; next sensor gets ++this }; struct StationAttrib { // station attributes @@ -145,8 +133,11 @@ struct StationAttrib { // station attributes unsigned char igpu:1; // todo: ignore pause unsigned char gid; // sequential group id - unsigned char reserved[2]; // reserved bytes for the future -}; // total is 4 bytes so far + unsigned char mas3:1; // master 3 binding bit (was reserved[0]) + unsigned char mas4:1; // master 4 binding bit + unsigned char :6; // remaining bits of this byte, reserved + unsigned char reserved; // reserved for future use (was reserved[1]) +}; // total is 4 bytes /** Station data structure */ struct StationData { @@ -218,6 +209,8 @@ struct ConStatus { unsigned char network_fails:3; // number of network fails unsigned char mas:8; // master station index unsigned char mas2:8; // master2 station index + unsigned char mas3:8; // master3 station index + unsigned char mas4:8; // master4 station index unsigned char sensor2:1; // sensor2 status bit (when set, sensor2 on is detected) unsigned char sensor1_active:1; // sensor1 active bit (when set, sensor1 is activated) unsigned char sensor2_active:1; // sensor2 active bit (when set, sensor2 is activated) @@ -241,12 +234,19 @@ class OpenSprinkler { public: // data members -#if defined(USE_SSD1306) +#if defined(USE_DISPLAY) static SSD1306Display lcd; // 128x64 OLED display -#elif defined(USE_LCD) - static LiquidCrystal lcd; // 16x2 character LCD #endif + static ADS1115 *ads1115_devices[4]; + + union SensorUnion { + ADS1115Sensor ads1115; + AggregateSensor aggregate; + WeatherSensor weather; + }; + static sensor_memory_t sensors[MAX_SENSORS]; + #if defined(OSPI) static unsigned char pin_sr_data; // RPi shift register data pin to handle RPi rev. 1 #endif @@ -256,7 +256,7 @@ class OpenSprinkler { static NVConData nvdata; static ConStatus status; static ConStatus old_status; - static unsigned char nboards, nstations; + static unsigned char nboards, nstations, nsensors; static unsigned char hw_type; // hardware type static unsigned char hw_rev; // hardware minor @@ -268,6 +268,8 @@ class OpenSprinkler { static unsigned char attrib_mas[]; static unsigned char attrib_igs[]; static unsigned char attrib_mas2[]; + static unsigned char attrib_mas3[]; + static unsigned char attrib_mas4[]; static unsigned char attrib_igs2[]; static unsigned char attrib_igrd[]; static unsigned char attrib_dis[]; @@ -284,9 +286,9 @@ class OpenSprinkler { static time_os_t sensor2_off_timer; // time when sensor2 is detected off last time static time_os_t sensor2_active_lasttime; // most recent time sensor1 is activated static time_os_t raindelay_on_lasttime; // time when the most recent rain delay started - static ulong pause_timer; // count down timer in paused state - static ulong flowcount_rt; // flow count (for computing real-time flow rate) - static ulong flowcount_log_start; // starting flow count (for logging) + static uint32_t pause_timer; // count down timer in paused state + static uint32_t flowcount_rt; // flow count (for computing real-time flow rate) + static uint32_t flowcount_log_start; // starting flow count (for logging) static unsigned char button_timeout; // button timeout static time_os_t checkwt_lasttime; // time when weather was checked @@ -372,10 +374,12 @@ class OpenSprinkler { static int8_t send_http_request(const char* server, uint16_t port, char* p, void(*callback)(char*)=NULL, bool usessl=false, uint16_t timeout=5000); static int8_t send_http_request(char* server_with_port, char* p, void(*callback)(char*)=NULL, bool usessl=false, uint16_t timeout=5000); - #if defined(USE_OTF) static OTCConfig otc; - #endif + // -- Sensor functions + void log_sensor(uint8_t sid, float value); + static void poll_sensors(); + static float get_sensor_weather_data(WeatherAction action); // -- LCD functions #if defined(USE_DISPLAY) static void lcd_print_time(time_os_t t); // print current time @@ -385,12 +389,9 @@ class OpenSprinkler { static void lcd_print_version(unsigned char v); // print version number static void lcd_set_brightness(unsigned char value=1); static void lcd_set_contrast(); - - #if defined(USE_SSD1306) static void flash_screen(); static void toggle_screen_led(); static void set_screen_led(unsigned char status); - #endif static String time2str(uint32_t t) { uint16_t h = hour(t); @@ -417,16 +418,10 @@ class OpenSprinkler { static void ui_set_options(int oid); // ui for setting options (oid-> starting option index) #endif -#if defined(ARDUINO) // LCD functions for Arduino - #if defined(ESP8266) +#if defined(ESP8266) // LCD functions for Arduino static void lcd_print_pgm(PGM_P str); // ESP8266 does not allow PGM_P followed by PROGMEM static void lcd_print_line_clear_pgm(PGM_P str, unsigned char line); - #else - static void lcd_print_pgm(PGM_P PROGMEM str); // print a program memory string - static void lcd_print_line_clear_pgm(PGM_P PROGMEM str, unsigned char line); - #endif - #if defined(ESP8266) static IOEXP *mainio, *drio; static IOEXP *expanders[]; static CH224 usbpd; @@ -442,11 +437,10 @@ class OpenSprinkler { static void reset_to_ap(); static unsigned char state; static void setup_pd_voltage(); - #endif #else -static void lcd_print_pgm(const char *str); -static void lcd_print_line_clear_pgm(const char *str, unsigned char line); + static void lcd_print_pgm(const char *str); + static void lcd_print_line_clear_pgm(const char *str, unsigned char line); #endif // LCD functions for Arduino private: @@ -471,9 +465,6 @@ static void lcd_print_line_clear_pgm(const char *str, unsigned char line); static unsigned char engage_booster; static RCSwitch rfswitch; - #if defined(USE_OTF) static void parse_otc_config(); - #endif }; -#endif // _OPENSPRINKLER_H diff --git a/RCSwitch.cpp b/RCSwitch.cpp index 2f4ae7f64..1aaf8fadd 100644 --- a/RCSwitch.cpp +++ b/RCSwitch.cpp @@ -32,7 +32,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -#if defined(ARDUINO) +#if defined(ESP8266) #include #endif @@ -173,7 +173,7 @@ void RCSwitch::transmit(HighLow pulses) { uint8_t firstLogicLevel = (this->protocol.invertedSignal) ? LOW : HIGH; uint8_t secondLogicLevel = (this->protocol.invertedSignal) ? HIGH : LOW; - ulong timeout = micros() + this->protocol.pulseLength * pulses.high; + uint32_t timeout = micros() + this->protocol.pulseLength * pulses.high; digitalWrite(this->nTransmitterPin, firstLogicLevel); while(micros() < timeout) { delayMicroseconds(5); diff --git a/RCSwitch.h b/RCSwitch.h index bb81aeedd..f429ca287 100644 --- a/RCSwitch.h +++ b/RCSwitch.h @@ -27,8 +27,7 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef _RCSwitch_h -#define _RCSwitch_h +#pragma once #include @@ -99,4 +98,3 @@ class RCSwitch { Protocol protocol; }; -#endif diff --git a/SSD1306Display.h b/SSD1306Display.h index f97683a5f..bb8369001 100644 --- a/SSD1306Display.h +++ b/SSD1306Display.h @@ -1,5 +1,4 @@ -#ifndef SSD1306_DISPLAY_H -#define SSD1306_DISPLAY_H +#pragma once #if defined(ESP8266) @@ -13,81 +12,81 @@ class SSD1306Display : public SSD1306 { public: - SSD1306Display(uint8_t _addr, uint8_t _sda, uint8_t _scl) - : SSD1306(_addr, _sda, _scl) { - cx = 0; - cy = 0; - for (unsigned char i = 0; i < NUM_CUSTOM_ICONS; i++) - custom_chars[i] = NULL; - } - void begin() { - Wire.setClock(400000L); // lower clock to 400kHz - flipScreenVertically(); - setFont(Monospaced_plain_13); - fontWidth = 8; - fontHeight = 16; - } - void clear() { SSD1306::clear(); } - void clear(int start, int end) { - setColor(BLACK); - fillRect(0, (start + 1) * fontHeight, 128, (end - start + 1) * fontHeight); - setColor(WHITE); - } - - uint8_t type() { return LCD_I2C; } - void noBlink() { /*no support*/ } - void blink() { /*no support*/ } - void setCursor(uint8_t col, int8_t row) { - /* assume 4 lines, the middle two lines - are row 0 and 1 */ - cy = (row + 1) * fontHeight; - cx = col * fontWidth; - } - void noBacklight() { /*no support*/ } - void backlight() { /*no support*/ } - size_t write(uint8_t c) { - setColor(BLACK); - fillRect(cx, cy, fontWidth, fontHeight); - setColor(WHITE); - - if (c < NUM_CUSTOM_ICONS && custom_chars[c] != NULL) { - drawXbm(cx, cy, fontWidth, fontHeight, - (const unsigned char *)custom_chars[c]); - } else { - drawString(cx, cy, String((char)c)); - } - cx += fontWidth; - if (auto_display) - display(); // todo: not very efficient - return 1; - } - size_t write(const char *s) { - uint8_t nc = strlen(s); - setColor(BLACK); - fillRect(cx, cy, fontWidth * nc, fontHeight); - setColor(WHITE); - drawString(cx, cy, String(s)); - cx += fontWidth * nc; - if (auto_display) - display(); // todo: not very efficient - return nc; - } - void createChar(unsigned char idx, PGM_P ptr) { - if (idx >= 0 && idx < NUM_CUSTOM_ICONS) - custom_chars[idx] = ptr; - } - - void createChar(unsigned char idx, const unsigned char *ptr) { - createChar(idx, (const char *)ptr); - } - - void setAutoDisplay(bool v) { auto_display = v; } + SSD1306Display(uint8_t _addr, uint8_t _sda, uint8_t _scl) + : SSD1306(_addr, _sda, _scl) { + cx = 0; + cy = 0; + for (unsigned char i = 0; i < NUM_CUSTOM_ICONS; i++) + custom_chars[i] = NULL; + } + void begin() { + Wire.setClock(400000L); // lower clock to 400kHz + flipScreenVertically(); + setFont(Monospaced_plain_13); + fontWidth = 8; + fontHeight = 16; + } + void clear() { SSD1306::clear(); } + void clear(int start, int end) { + setColor(BLACK); + fillRect(0, (start + 1) * fontHeight, 128, (end - start + 1) * fontHeight); + setColor(WHITE); + } + + uint8_t type() { return LCD_I2C; } + void noBlink() { /*no support*/ } + void blink() { /*no support*/ } + void setCursor(uint8_t col, int8_t row) { + /* assume 4 lines, the middle two lines + are row 0 and 1 */ + cy = (row + 1) * fontHeight; + cx = col * fontWidth; + } + void noBacklight() { /*no support*/ } + void backlight() { /*no support*/ } + size_t write(uint8_t c) { + setColor(BLACK); + fillRect(cx, cy, fontWidth, fontHeight); + setColor(WHITE); + + if (c < NUM_CUSTOM_ICONS && custom_chars[c] != NULL) { + drawXbm(cx, cy, fontWidth, fontHeight, + (const unsigned char *)custom_chars[c]); + } else { + drawString(cx, cy, String((char)c)); + } + cx += fontWidth; + if (auto_display) + display(); + return 1; + } + size_t write(const char *s) { + uint8_t nc = strlen(s); + setColor(BLACK); + fillRect(cx, cy, fontWidth * nc, fontHeight); + setColor(WHITE); + drawString(cx, cy, String(s)); + cx += fontWidth * nc; + if (auto_display) + display(); + return nc; + } + void createChar(unsigned char idx, PGM_P ptr) { + if (idx >= 0 && idx < NUM_CUSTOM_ICONS) + custom_chars[idx] = ptr; + } + + void createChar(unsigned char idx, const unsigned char *ptr) { + createChar(idx, (const char *)ptr); + } + + void setAutoDisplay(bool v) { auto_display = v; } private: - bool auto_display = true; - uint8_t cx, cy; - uint8_t fontWidth, fontHeight; - PGM_P custom_chars[NUM_CUSTOM_ICONS]; + bool auto_display = true; + uint8_t cx, cy; + uint8_t fontWidth, fontHeight; + PGM_P custom_chars[NUM_CUSTOM_ICONS]; }; #else @@ -171,387 +170,379 @@ class SSD1306Display : public SSD1306 { class SSD1306Display { public: - SSD1306Display(uint8_t addr, uint8_t _sda, uint8_t _scl) { - cx = 0; - cy = 0; - _addr = addr; - for (uint8_t i = 0; i < NUM_CUSTOM_ICONS; i++) - custom_chars[i] = 0; - - clear_buffer(); - - height = 64; - width = 128; - - i2c = I2CDevice(); - } - - ~SSD1306Display() { - displayOff(); - close(file); - } - - void init() {} // Dummy function to match ESP8266 - - int begin() { - i2c.begin(_addr); - - setFont(Monospaced_plain_13); - fontWidth = 8; - fontHeight = 16; - - i2c.begin_transaction(SSD1306_COMMAND_ADDRESS); - ssd1306_command(SSD1306_DISPLAY_OFF); - ssd1306_command(SSD1306_SET_DISPLAY_CLOCK_DIV_RATIO); - ssd1306_command(0x80); - ssd1306_command(SSD1306_SET_MULTIPLEX_RATIO); - ssd1306_command(height - 1); - ssd1306_command(SSD1306_SET_DISPLAY_OFFSET); - ssd1306_command(0x00); - ssd1306_command(SSD1306_SET_START_LINE); - ssd1306_command(SSD1306_CHARGE_PUMP); - ssd1306_command(0x14); - ssd1306_command(SSD1306_MEMORY_ADDR_MODE); - ssd1306_command(0x00); - ssd1306_command(SSD1306_SET_SEGMENT_REMAP | 0x01); - ssd1306_command(SSD1306_COM_SCAN_DIR_DEC); - - switch (height) { - case 64: - ssd1306_command(SSD1306_SET_COM_PINS); - ssd1306_command(0x12); - ssd1306_command(SSD1306_SET_CONTRAST_CONTROL); - ssd1306_command(0xCF); - break; - case 32: - ssd1306_command(SSD1306_SET_COM_PINS); - ssd1306_command(0x02); - ssd1306_command(SSD1306_SET_CONTRAST_CONTROL); - ssd1306_command(0x8F); - break; - case 16: // NOTE: not tested, lacking part. - ssd1306_command(SSD1306_SET_COM_PINS); - ssd1306_command(0x2); - ssd1306_command(SSD1306_SET_CONTRAST_CONTROL); - ssd1306_command(0xAF); - break; - } - - ssd1306_command(SSD1306_SET_PRECHARGE_PERIOD); - ssd1306_command(0xF1); - - ssd1306_command(SSD1306_SET_VCOM_DESELECT); - ssd1306_command(0x40); - - ssd1306_command(SSD1306_DISPLAY_ALL_ON_RESUME); - ssd1306_command(SSD1306_NORMAL_DISPLAY); - ssd1306_command(SSD1306_DEACTIVATE_SCROLL); - ssd1306_command(SSD1306_DISPLAY_ON); - - i2c.end_transaction(); - - return 0; - } - - void setFont(const uint8_t *f) { font = (uint8_t *)f; } - - void display() { - i2c.begin_transaction(SSD1306_COMMAND_ADDRESS); - ssd1306_command(SSD1306_SET_PAGE_ADDR); - ssd1306_command(0x00); // Page start address (0 = reset) - switch (height) { - case 64: - ssd1306_command(7); - break; - case 32: - ssd1306_command(3); - break; - case 16: - ssd1306_command(1); - break; - } - - ssd1306_command(SSD1306_SET_COLUMN_ADDR); - ssd1306_command(0x00); // Column start address (0 = reset) + SSD1306Display(uint8_t addr, uint8_t _sda, uint8_t _scl) : i2c(Bus, addr) { + cx = 0; + cy = 0; + for (uint8_t i = 0; i < NUM_CUSTOM_ICONS; i++) + custom_chars[i] = 0; + + clear_buffer(); + + height = 64; + width = 128; + } + + ~SSD1306Display() { + displayOff(); + close(file); + } + + void init() {} // Dummy function to match ESP8266 + + int begin() { + setFont(Monospaced_plain_13); + fontWidth = 8; + fontHeight = 16; + + i2c.begin_transaction(SSD1306_COMMAND_ADDRESS); + ssd1306_command(SSD1306_DISPLAY_OFF); + ssd1306_command(SSD1306_SET_DISPLAY_CLOCK_DIV_RATIO); + ssd1306_command(0x80); + ssd1306_command(SSD1306_SET_MULTIPLEX_RATIO); + ssd1306_command(height - 1); + ssd1306_command(SSD1306_SET_DISPLAY_OFFSET); + ssd1306_command(0x00); + ssd1306_command(SSD1306_SET_START_LINE); + ssd1306_command(SSD1306_CHARGE_PUMP); + ssd1306_command(0x14); + ssd1306_command(SSD1306_MEMORY_ADDR_MODE); + ssd1306_command(0x00); + ssd1306_command(SSD1306_SET_SEGMENT_REMAP | 0x01); + ssd1306_command(SSD1306_COM_SCAN_DIR_DEC); + + switch (height) { + case 64: + ssd1306_command(SSD1306_SET_COM_PINS); + ssd1306_command(0x12); + ssd1306_command(SSD1306_SET_CONTRAST_CONTROL); + ssd1306_command(0xCF); + break; + case 32: + ssd1306_command(SSD1306_SET_COM_PINS); + ssd1306_command(0x02); + ssd1306_command(SSD1306_SET_CONTRAST_CONTROL); + ssd1306_command(0x8F); + break; + case 16: // NOTE: not tested, lacking part. + ssd1306_command(SSD1306_SET_COM_PINS); + ssd1306_command(0x2); + ssd1306_command(SSD1306_SET_CONTRAST_CONTROL); + ssd1306_command(0xAF); + break; + } + + ssd1306_command(SSD1306_SET_PRECHARGE_PERIOD); + ssd1306_command(0xF1); + + ssd1306_command(SSD1306_SET_VCOM_DESELECT); + ssd1306_command(0x40); + + ssd1306_command(SSD1306_DISPLAY_ALL_ON_RESUME); + ssd1306_command(SSD1306_NORMAL_DISPLAY); + ssd1306_command(SSD1306_DEACTIVATE_SCROLL); + ssd1306_command(SSD1306_DISPLAY_ON); + + i2c.end_transaction(); + + return 0; + } + + void setFont(const uint8_t *f) { font = (uint8_t *)f; } + + void display() { + i2c.begin_transaction(SSD1306_COMMAND_ADDRESS); + ssd1306_command(SSD1306_SET_PAGE_ADDR); + ssd1306_command(0x00); // Page start address (0 = reset) + switch (height) { + case 64: + ssd1306_command(7); + break; + case 32: + ssd1306_command(3); + break; + case 16: + ssd1306_command(1); + break; + } + + ssd1306_command(SSD1306_SET_COLUMN_ADDR); + ssd1306_command(0x00); // Column start address (0 = reset) ssd1306_command(width - 1); // Column end address (127 = reset) - i2c.end_transaction(); + i2c.end_transaction(); - i2c.begin_transaction(SSD1306_DATA_CONTINUE_ADDRESS); + i2c.begin_transaction(SSD1306_DATA_CONTINUE_ADDRESS); - int b; - for (b = 0; b < 1024; b++) { - ssd1306_data(frame[b]); - } + int b; + for (b = 0; b < 1024; b++) { + ssd1306_data(frame[b]); + } - i2c.end_transaction(); - } + i2c.end_transaction(); + } - void clear() { - clear_buffer(); - display(); - } + void clear() { + clear_buffer(); + display(); + } - void setBrightness(uint8_t brightness) { - ssd1306_command(SSD1306_SET_CONTRAST_CONTROL); - ssd1306_command(brightness); - } + void setBrightness(uint8_t brightness) { + ssd1306_command(SSD1306_SET_CONTRAST_CONTROL); + ssd1306_command(brightness); + } - void displayOn() { ssd1306_command(SSD1306_DISPLAY_ON); } + void displayOn() { ssd1306_command(SSD1306_DISPLAY_ON); } - void displayOff() { ssd1306_command(SSD1306_DISPLAY_OFF); } + void displayOff() { ssd1306_command(SSD1306_DISPLAY_OFF); } - void setColor(uint8_t color) { this->color = color; } + void setColor(uint8_t color) { this->color = color; } - void drawPixel(uint8_t x, uint8_t y) { - if (x >= 128 || y >= 64) - return; + void drawPixel(uint8_t x, uint8_t y) { + if (x >= 128 || y >= 64) + return; - if (color == WHITE) { - frame[x + (y / 8) * 128] |= 1 << (y % 8); - } else { - frame[x + (y / 8) * 128] &= ~(1 << (y % 8)); - } - } + if (color == WHITE) { + frame[x + (y / 8) * 128] |= 1 << (y % 8); + } else { + frame[x + (y / 8) * 128] &= ~(1 << (y % 8)); + } + } - void fillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h) { - for (int _x = x; _x < x + w; _x++) { - for (int _y = y; _y < y + h; _y++) { - drawPixel(_x, _y); - } - } - } + void fillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h) { + for (int _x = x; _x < x + w; _x++) { + for (int _y = y; _y < y + h; _y++) { + drawPixel(_x, _y); + } + } + } - void clear(int start, int end) { - setColor(BLACK); - fillRect(0, (start + 1) * fontHeight, 128, (end - start + 1) * fontHeight); - setColor(WHITE); - } + void clear(int start, int end) { + setColor(BLACK); + fillRect(0, (start + 1) * fontHeight, 128, (end - start + 1) * fontHeight); + setColor(WHITE); + } - void print(const char *s) { write(s); } + void print(const char *s) { write(s); } - void print(char s) { write(s); } + void print(char s) { write(s); } - void print(int i) { - char buf[100]; - snprintf(buf, 100, "%d", i); - print((const char *)buf); - } + void print(int i) { + char buf[100]; + snprintf(buf, 100, "%d", i); + print((const char *)buf); + } - void print(unsigned int i) { - char buf[100]; - snprintf(buf, 100, "%u", i); - print((const char *)buf); - } - void print(float f) { - char buf[100]; - snprintf(buf, 100, "%f", f); - print((const char *)buf); - } + void print(unsigned int i) { + char buf[100]; + snprintf(buf, 100, "%u", i); + print((const char *)buf); + } + void print(float f) { + char buf[100]; + snprintf(buf, 100, "%f", f); + print((const char *)buf); + } #define DEC 10 #define HEX 16 #define OCT 8 #define BIN 2 - void print(int i, int base) { - char buf[100]; - switch (base) { - case DEC: - snprintf(buf, 100, "%d", i); - break; - case HEX: - snprintf(buf, 100, "%x", i); - break; - case OCT: - snprintf(buf, 100, "%o", i); - break; - case BIN: - snprintf(buf, 100, "%b", i); - break; - default: - snprintf(buf, 100, "%d", i); - break; - } - print((const char *)buf); - } - - uint8_t type() { return LCD_I2C; } - void noBlink() { /*no support*/ } - void blink() { /*no support*/ } - void setCursor(uint8_t col, int8_t row) { - /* assume 4 lines, the middle two lines - are row 0 and 1 */ - cy = (row + 1) * fontHeight; - cx = col * fontWidth; - } - void noBacklight() { /*no support*/ } - void backlight() { /*no support*/ } - void drawXbm(int x, int y, int w, int h, const char *xbm) { - int xbmWidth = (w + 7) / 8; - uint8_t data = 0; - - for (int i = 0; i < h; i++) { - for (int j = 0; j < w; j++) { - if (j & 7) { - data >>= 1; - } else { - data = xbm[(i * xbmWidth) + (j / 8)]; - } - - if (data & 0x01) { - drawPixel(x + j, y + i); - } - } - } - } - - void drawXbm(int x, int y, int w, int h, const uint8_t *data) { - drawXbm(x, y, w, h, (const char *)data); - } - - void fillCircle(int x0, int y0, int r) { - for (int y = -r; y <= r; y++) { - for (int x = -r; x <= r; x++) { - if (x * x + y * y <= r * r) { - drawPixel(x0 + x, y0 + y); - } - } - } - } - - void drawChar(int x, int y, char c) { - uint8_t textHeight = font[HEIGHT_POS]; - uint8_t firstChar = font[FIRST_CHAR_POS]; - uint8_t numChars = font[CHAR_NUM_POS]; - uint16_t sizeOfJumpTable = numChars * JUMPTABLE_BYTES; - - if (c < firstChar || c >= firstChar + numChars) - return; - - // 4 Bytes per char code - uint8_t charCode = c - firstChar; - - uint8_t msbJumpToChar = - font[JUMPTABLE_START + charCode * JUMPTABLE_BYTES]; // MSB \ JumpAddress - uint8_t lsbJumpToChar = font[JUMPTABLE_START + charCode * JUMPTABLE_BYTES + - JUMPTABLE_LSB]; // LSB / - uint8_t charByteSize = font[JUMPTABLE_START + charCode * JUMPTABLE_BYTES + - JUMPTABLE_SIZE]; // Size - uint8_t currentCharWidth = - font[JUMPTABLE_START + (c - firstChar) * JUMPTABLE_BYTES + - JUMPTABLE_WIDTH]; // Width - - // Test if the char is drawable - if (!(msbJumpToChar == 255 && lsbJumpToChar == 255)) { - // Get the position of the char data - uint16_t charDataPosition = JUMPTABLE_START + sizeOfJumpTable + - ((msbJumpToChar << 8) + lsbJumpToChar); - int _y = y; - int _x = x; - - setColor(WHITE); - - for (int b = 0; b < charByteSize; b++) { - for (int i = 0; i < 8; i++) { - if (font[charDataPosition + b] & (1 << i)) { - drawPixel(_x, _y); - } - - _y++; - if (_y >= y + textHeight) { - _y = y; - _x++; - break; - } - } - } - } - } - - void drawString(int x, int y, const char *text) { - int _x = x; - int _y = y; - - while (*text) { - if (*text == '\n') { - _x = x; - _y += fontHeight; - } else { - drawChar(_x, _y, *text); - _x += fontWidth; - } - - text++; - } - } - - size_t write(uint8_t c) { - setColor(BLACK); - fillRect(cx, cy, fontWidth, fontHeight); - setColor(WHITE); - char cc[2] = {(char)c, 0}; - - if (c < NUM_CUSTOM_ICONS && custom_chars[c] != 0) { - drawXbm(cx, cy, fontWidth, fontHeight, (const char *)custom_chars[c]); - } else { - drawString(cx, cy, cc); - } - cx += fontWidth; - if (auto_display) - display(); // todo: not very efficient - return 1; - } - - uint8_t write(const char *s) { - uint8_t nc = strlen(s); - bool temp_auto_display = auto_display; - auto_display = false; - setColor(BLACK); - fillRect(cx, cy, fontWidth * nc, fontHeight); - setColor(WHITE); - drawString(cx, cy, s); - auto_display = temp_auto_display; - cx += fontWidth * nc; - if (auto_display) - display(); // todo: not very efficient - return nc; - } - - void createChar(uint8_t idx, const char *ptr) { - if (idx >= 0 && idx < NUM_CUSTOM_ICONS) - custom_chars[idx] = ptr; - } - - void createChar(unsigned char idx, const unsigned char *ptr) { - createChar(idx, (const char *)ptr); - } - - void setAutoDisplay(bool v) { auto_display = v; } + void print(int i, int base) { + char buf[100]; + switch (base) { + case DEC: + snprintf(buf, 100, "%d", i); + break; + case HEX: + snprintf(buf, 100, "%x", i); + break; + case OCT: + snprintf(buf, 100, "%o", i); + break; + case BIN: + snprintf(buf, 100, "%b", i); + break; + default: + snprintf(buf, 100, "%d", i); + break; + } + print((const char *)buf); + } + + uint8_t type() { return LCD_I2C; } + void noBlink() { /*no support*/ } + void blink() { /*no support*/ } + void setCursor(uint8_t col, int8_t row) { + /* assume 4 lines, the middle two lines + are row 0 and 1 */ + cy = (row + 1) * fontHeight; + cx = col * fontWidth; + } + void noBacklight() { /*no support*/ } + void backlight() { /*no support*/ } + void drawXbm(int x, int y, int w, int h, const char *xbm) { + int xbmWidth = (w + 7) / 8; + uint8_t data = 0; + + for (int i = 0; i < h; i++) { + for (int j = 0; j < w; j++) { + if (j & 7) { + data >>= 1; + } else { + data = xbm[(i * xbmWidth) + (j / 8)]; + } + + if (data & 0x01) { + drawPixel(x + j, y + i); + } + } + } + } + + void drawXbm(int x, int y, int w, int h, const uint8_t *data) { + drawXbm(x, y, w, h, (const char *)data); + } + + void fillCircle(int x0, int y0, int r) { + for (int y = -r; y <= r; y++) { + for (int x = -r; x <= r; x++) { + if (x * x + y * y <= r * r) { + drawPixel(x0 + x, y0 + y); + } + } + } + } + + void drawChar(int x, int y, char c) { + uint8_t textHeight = font[HEIGHT_POS]; + uint8_t firstChar = font[FIRST_CHAR_POS]; + uint8_t numChars = font[CHAR_NUM_POS]; + uint16_t sizeOfJumpTable = numChars * JUMPTABLE_BYTES; + + if (c < firstChar || c >= firstChar + numChars) + return; + + // 4 Bytes per char code + uint8_t charCode = c - firstChar; + + uint8_t msbJumpToChar = + font[JUMPTABLE_START + charCode * JUMPTABLE_BYTES]; // MSB \ JumpAddress + uint8_t lsbJumpToChar = font[JUMPTABLE_START + charCode * JUMPTABLE_BYTES + + JUMPTABLE_LSB]; // LSB / + uint8_t charByteSize = font[JUMPTABLE_START + charCode * JUMPTABLE_BYTES + + JUMPTABLE_SIZE]; // Size + uint8_t currentCharWidth = + font[JUMPTABLE_START + (c - firstChar) * JUMPTABLE_BYTES + + JUMPTABLE_WIDTH]; // Width + + // Test if the char is drawable + if (!(msbJumpToChar == 255 && lsbJumpToChar == 255)) { + // Get the position of the char data + uint16_t charDataPosition = JUMPTABLE_START + sizeOfJumpTable + + ((msbJumpToChar << 8) + lsbJumpToChar); + int _y = y; + int _x = x; + + setColor(WHITE); + + for (int b = 0; b < charByteSize; b++) { + for (int i = 0; i < 8; i++) { + if (font[charDataPosition + b] & (1 << i)) { + drawPixel(_x, _y); + } + + _y++; + if (_y >= y + textHeight) { + _y = y; + _x++; + break; + } + } + } + } + } + + void drawString(int x, int y, const char *text) { + int _x = x; + int _y = y; + + while (*text) { + if (*text == '\n') { + _x = x; + _y += fontHeight; + } else { + drawChar(_x, _y, *text); + _x += fontWidth; + } + + text++; + } + } + + size_t write(uint8_t c) { + setColor(BLACK); + fillRect(cx, cy, fontWidth, fontHeight); + setColor(WHITE); + char cc[2] = {(char)c, 0}; + + if (c < NUM_CUSTOM_ICONS && custom_chars[c] != 0) { + drawXbm(cx, cy, fontWidth, fontHeight, (const char *)custom_chars[c]); + } else { + drawString(cx, cy, cc); + } + cx += fontWidth; + if (auto_display) + display(); + return 1; + } + + uint8_t write(const char *s) { + uint8_t nc = strlen(s); + bool temp_auto_display = auto_display; + auto_display = false; + setColor(BLACK); + fillRect(cx, cy, fontWidth * nc, fontHeight); + setColor(WHITE); + drawString(cx, cy, s); + auto_display = temp_auto_display; + cx += fontWidth * nc; + if (auto_display) + display(); + return nc; + } + + void createChar(uint8_t idx, const char *ptr) { + if (idx >= 0 && idx < NUM_CUSTOM_ICONS) + custom_chars[idx] = ptr; + } + + void createChar(unsigned char idx, const unsigned char *ptr) { + createChar(idx, (const char *)ptr); + } + + void setAutoDisplay(bool v) { auto_display = v; } private: - int file = -1; - bool auto_display = false; - uint8_t cx, cy = 0; - uint8_t fontWidth, fontHeight; - const char *custom_chars[NUM_CUSTOM_ICONS]; - uint8_t frame[1024]; - int i2cd; - bool color; - uint8_t *font; + int file = -1; + bool auto_display = false; + uint8_t cx, cy = 0; + uint8_t fontWidth, fontHeight; + const char *custom_chars[NUM_CUSTOM_ICONS]; + uint8_t frame[1024]; + int i2cd; + bool color; + uint8_t *font; - I2CDevice i2c; - unsigned char _addr; + I2CDevice i2c; - unsigned char height; - unsigned char width; + unsigned char height; + unsigned char width; - void clear_buffer() { memset(frame, 0x00, sizeof(frame)); } + void clear_buffer() { memset(frame, 0x00, sizeof(frame)); } - int ssd1306_command(unsigned char command) { return i2c.send(0x00, command); } + int ssd1306_command(unsigned char command) { return i2c.send(0x00, command); } - int ssd1306_data(unsigned char value) { return i2c.send(0x40, value); } + int ssd1306_data(unsigned char value) { return i2c.send(0x40, value); } }; #endif - -#endif // SSD1306_DISPLAY_H diff --git a/TimeLib.cpp b/TimeLib.cpp index b985a0242..5d727e16f 100644 --- a/TimeLib.cpp +++ b/TimeLib.cpp @@ -157,7 +157,7 @@ void breakTime(time_os_t timeInput, tmElements_t &tm){ uint8_t year; uint8_t month, monthLength; uint32_t time; - unsigned long days; + uint32_t days; time = (uint32_t)timeInput; tm.Second = time % 60; diff --git a/ads1115.cpp b/ads1115.cpp new file mode 100644 index 000000000..3d3fc519d --- /dev/null +++ b/ads1115.cpp @@ -0,0 +1,113 @@ +#include "ads1115.h" +#include "utils.h" // millis() on Linux/DEMO + +#include + +#if defined(ESP8266) +ADS1115::ADS1115(uint8_t address, TwoWire& wire) : _address(address), _wire(&wire) {} +ADS1115::ADS1115(uint8_t address) : ADS1115(address, Wire) {} + +bool ADS1115::begin() { + if ((this->_address < 0x48) || (this->_address > 0x4B)) { + return false; + } + this->_wire->beginTransmission(_address); + return (this->_wire->endTransmission() == 0); +} + + +void ADS1115::_write_register(uint8_t reg, uint16_t value) { + this->_wire->beginTransmission(this->_address); + this->_wire->write(reg); + this->_wire->write((uint8_t)(value >> 8)); + this->_wire->write((uint8_t)(value & 0xFF)); + this->_wire->endTransmission(); +} + +uint16_t ADS1115::_read_register(uint8_t reg) { + this->_wire->beginTransmission(this->_address); + this->_wire->write(reg); + if (!this->_wire->endTransmission(false)) { + if (this->_wire->requestFrom((int)_address, (int)2) == 2) { + uint16_t val = ((uint16_t)this->_wire->read()) << 8; + val += (uint16_t)this->_wire->read(); + return val; + } + } + + return 0; +} + +#elif defined(OSPI) +ADS1115::ADS1115(uint8_t address, I2CBus& bus) : _address(address), _i2c(bus, address) {} +ADS1115::ADS1115(uint8_t address) : ADS1115(address, Bus) {} + +bool ADS1115::begin() { + if ((this->_address < 0x48) || (this->_address > 0x4B)) { + return false; + } + if (this->_i2c.detect() < 0) { + return false; + } + return true; +} + +void ADS1115::_write_register(uint8_t reg, uint16_t value) { + this->_i2c.send_word(reg, this->swap_reg(value)); +} + +uint16_t ADS1115::_read_register(uint8_t reg) { + return this->swap_reg((uint16_t)(this->_i2c.read_word(reg) & 0xFFFF)); +} + +#else +// Simulator/mock backend: no I2C hardware, methods that touch the chip are stubs. +// get_pin_value() below produces synthetic counts based on time + channel index. +ADS1115::ADS1115(uint8_t address) : _address(address) {} + +bool ADS1115::begin() { + return (this->_address >= 0x48) && (this->_address <= 0x4B); +} + +void ADS1115::_write_register(uint8_t reg, uint16_t value) { (void)reg; (void)value; } +uint16_t ADS1115::_read_register(uint8_t reg) { (void)reg; return 0; } +#endif + +int16_t ADS1115::get_pin_value(uint8_t pin) { +#if defined(ADS1115_HARDWARE) + this->request_pin(pin); + uint32_t start = millis(); + while (this->is_busy()) { + // if ((millis() - start) > 11) { + if ((millis() - start) > 18) { + return -1; + } + +#if defined(ESP8266) + yield(); +#else + delay(1); +#endif + } + + return this->get_value(); +#else + // Mock: per-channel slow sine, ~0.5..2.5 V, mapped to single-ended counts [0, 32767]. + float t = millis() * 0.001f; + float phase = ((this->_address - 0x48) * 4 + pin) * 0.7f; + float volts = 1.5f + 1.0f * sinf(t * 0.1f + phase); + int32_t counts = (int32_t)(volts * 1000.0f / ADS1115_SCALE_FACTOR); + if (counts < 0) counts = 0; + if (counts > 32767) counts = 32767; + return (int16_t)counts; +#endif +} + +void ADS1115::request_pin(uint8_t pin) { +#if defined(ADS1115_HARDWARE) + uint16_t config = 0x8000 | ((4 + ((uint16_t)pin)) << 12) | 0x0100 | (4 << 5); + this->_write_register(0x01, config); +#else + (void)pin; +#endif +} diff --git a/ads1115.h b/ads1115.h new file mode 100644 index 000000000..5702091b4 --- /dev/null +++ b/ads1115.h @@ -0,0 +1,58 @@ +#pragma once + +#include "defines.h" + +#include + +#define ADS1115_SCALE_FACTOR (6144.0 / 32768.0) + +// Hardware ADS1115 chip is wired up on ESP8266 (Arduino I2C via Wire) and OSPi +// (Linux I2C via libi2c). Other targets (DEMO/SIM) use a mock backend that +// produces synthetic counts so the sensor pipeline is exercisable end-to-end +// without real hardware. +#if defined(ESP8266) || defined(OSPI) +#define ADS1115_HARDWARE +#endif + +#if defined(ESP8266) +#include +#include +#elif defined(OSPI) +#include "i2cd.h" +#endif + +class ADS1115 { +public: +#if defined(ESP8266) + ADS1115(uint8_t address, TwoWire& wire); +#elif defined(OSPI) + ADS1115(uint8_t address, I2CBus& bus); +#endif + ADS1115(uint8_t address); + int16_t get_pin_value(uint8_t pin); + bool begin(); + + int16_t get_value() { + return (int16_t) this->_read_register(0x00); + } + + void request_pin(uint8_t pin); + + bool is_busy() { + return (this->_read_register(0x01) & 0x8000) == 0; + } + + private: + uint8_t _address; +#if defined(ESP8266) + TwoWire *_wire; +#elif defined(OSPI) + I2CDevice _i2c; + uint16_t swap_reg(uint16_t val) { + return (val << 8) | (val >> 8); + } +#endif + + void _write_register(uint8_t reg, uint16_t value); + uint16_t _read_register(uint8_t reg); +}; diff --git a/bfiller.h b/bfiller.h new file mode 100644 index 000000000..e83c8ad5d --- /dev/null +++ b/bfiller.h @@ -0,0 +1,116 @@ +#pragma once + +#include "utils.h" + +#if defined(ESP8266) +#include +#else +#include +#include +#include +#include +#endif + +typedef void (*bfill_flush_fn)(const char *buf, size_t len); + +class BufferFiller { + char *start; //!< Pointer to start of buffer + char *ptr; //!< Pointer to cursor position + size_t len; + bfill_flush_fn flush_fn = nullptr; + + void mid_flush() { + if (flush_fn && ptr > start) { + flush_fn(start, (size_t)(ptr - start)); + ptr = start; + *ptr = 0; + } + } +public: + BufferFiller () {} + BufferFiller (char *buf, size_t buffer_len) { + start = buf; + ptr = buf; + len = buffer_len; + flush_fn = nullptr; + } + + char* buffer () const { return start; } + size_t length () const { return len; } + unsigned int position () const { return ptr - start; } + void set_flush(bfill_flush_fn fn) { flush_fn = fn; } + + void emit_p(PGM_P fmt, ...) { + va_list ap; + va_start(ap, fmt); + for (;;) { + char c = pgm_read_byte(fmt++); + if (c == 0) + break; + if (c != '$') { + if ((size_t)(ptr - start) >= len - 1) mid_flush(); + *ptr++ = c; + continue; + } + c = pgm_read_byte(fmt++); + switch (c) { + case 'D': + // itoa(va_arg(ap, int), (char*) ptr, 10); // ray + snprintf((char*) ptr, len - position(), "%d", va_arg(ap, int)); + break; + case 'E': //Double + snprintf((char*) ptr, len - position(), "%g", va_arg(ap, double)); + break; + case 'L': + // ultoa(va_arg(ap, uint32_t), (char*) ptr, 10); + // TODO: check if there is a way to print uint32_t + snprintf((char*) ptr, len - position(), "%" PRIu32, va_arg(ap, uint32_t)); + break; + case 'S': { + const char *s = va_arg(ap, const char*); + size_t slen = strlen(s); + if (flush_fn && (size_t)(ptr - start) + slen + 1 > len) { + mid_flush(); + if (slen >= len) { + // string larger than entire buffer: stream directly + flush_fn(s, slen); + *ptr = 0; + break; + } + } + strcpy((char*) ptr, s); + } + break; + case 'X': { + char d = va_arg(ap, int); + *ptr++ = dec2hexchar((d >> 4) & 0x0F); + *ptr++ = dec2hexchar(d & 0x0F); + } + continue; + case 'F': { + PGM_P s = va_arg(ap, PGM_P); + char d; + while ((d = pgm_read_byte(s++)) != 0) { + if ((size_t)(ptr - start) >= len - 1) mid_flush(); + *ptr++ = d; + } + continue; + } + case 'O': { + uint16_t oid = va_arg(ap, int); + if (flush_fn && (size_t)(ptr - start) + MAX_SOPTS_SIZE + 1 > len) + mid_flush(); + file_read_block(SOPTS_FILENAME, (char*) ptr, oid*MAX_SOPTS_SIZE, MAX_SOPTS_SIZE); + } + break; + default: + *ptr++ = c; + continue; + } + ptr += strlen((char*) ptr); + } + *(ptr)=0; + va_end(ap); + mid_flush(); + } +}; diff --git a/build.sh b/build.sh index 5a75868dc..57a3ef730 100755 --- a/build.sh +++ b/build.sh @@ -53,7 +53,8 @@ if [ "$1" == "demo" ]; then ws=$(ls external/TinyWebsockets/tiny_websockets_lib/src/*.cpp) otf=$(ls external/OpenThings-Framework-Firmware-Library/*.cpp) - g++ -o OpenSprinkler -DDEMO -DSMTP_OPENSSL $DEBUG -std=c++14 -include string.h -include cstdint main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp mqtt.cpp notifier.cpp smtp.c RCSwitch.cpp -Iexternal/TinyWebsockets/tiny_websockets_lib/include $ws -Iexternal/OpenThings-Framework-Firmware-Library/ $otf -lpthread -lmosquitto -lssl -lcrypto + sens=$(ls sensors/*.cpp) + g++ -o OpenSprinkler -DDEMO -DSMTP_OPENSSL $DEBUG -std=c++14 -include string.h -include cstdint main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp mqtt.cpp notifier.cpp smtp.c RCSwitch.cpp ads1115.cpp $sens -Iexternal/TinyWebsockets/tiny_websockets_lib/include $ws -Iexternal/OpenThings-Framework-Firmware-Library/ $otf -lpthread -lmosquitto -lssl -lcrypto else echo "Installing required libraries..." apt-get update @@ -68,7 +69,8 @@ else ws=$(ls external/TinyWebsockets/tiny_websockets_lib/src/*.cpp) otf=$(ls external/OpenThings-Framework-Firmware-Library/*.cpp) - g++ -o OpenSprinkler -DOSPI -DSMTP_OPENSSL $DEBUG -std=c++14 -include string.h -include cstdint main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp mqtt.cpp notifier.cpp smtp.c RCSwitch.cpp -Iexternal/TinyWebsockets/tiny_websockets_lib/include $ws -Iexternal/OpenThings-Framework-Firmware-Library/ $otf -lpthread -lmosquitto -lssl -lcrypto -li2c $GPIOLIB + sens=$(ls sensors/*.cpp) + g++ -o OpenSprinkler -DOSPI -DSMTP_OPENSSL $DEBUG -std=c++14 -include string.h -include cstdint main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp mqtt.cpp notifier.cpp smtp.c RCSwitch.cpp i2cd.cpp ads1115.cpp $sens -Iexternal/TinyWebsockets/tiny_websockets_lib/include $ws -Iexternal/OpenThings-Framework-Firmware-Library/ $otf -lpthread -lmosquitto -lssl -lcrypto -li2c $GPIOLIB fi diff --git a/ch224.h b/ch224.h index 9dba85bb9..d741a2338 100644 --- a/ch224.h +++ b/ch224.h @@ -1,7 +1,5 @@ #pragma once -#if defined(ESP8266) - #include #include @@ -232,5 +230,3 @@ class CH224 { uint8_t _pps_apdo_count; uint8_t _avs_apdo_count; }; - -#endif \ No newline at end of file diff --git a/defines.h b/defines.h index e92b4eab2..e8010370e 100755 --- a/defines.h +++ b/defines.h @@ -20,22 +20,19 @@ * along with this program. If not, see * . */ +#pragma once -#ifndef _DEFINES_H -#define _DEFINES_H +#define ENABLE_DEBUG // enable serial debug -//#define ENABLE_DEBUG // enable serial debug - -typedef unsigned long ulong; - -#define TMP_BUFFER_SIZE 320 // scratch buffer size +#define TMP_BUFFER_SIZE 320 // scratch buffer size +#define TMP_BUFFER_ALLOC_SIZE TMP_BUFFER_SIZE+(TMP_BUFFER_SIZE/2) // allocate extra space to allow overflow when needed /** Firmware version, hardware version, and maximal values */ #define OS_FW_VERSION 221 // Firmware version: 221 means 2.2.1 // if this number is different from the one stored in non-volatile memory // a device reset will be automatically triggered -#define OS_FW_MINOR 4 // Firmware minor version +#define OS_FW_MINOR 5 // Firmware minor version /** Hardware version base numbers */ #define OS_HW_VERSION_BASE 0x00 // OpenSprinkler @@ -55,6 +52,17 @@ typedef unsigned long ulong; #define NVCON_FILENAME "nvcon.dat" // non-volatile controller data file, see OpenSprinkler.h --> struct NVConData #define PROG_FILENAME "prog.dat" // program data file #define DONE_FILENAME "done.dat" // used to indicate the completion of all files +// External sensor board (ADS1115-based analog inputs, see sensor.h). +// Unrelated to the onboard SENSOR1/SENSOR2 GPIO inputs. +#define SENSORS_FILENAME "sens.dat" // external sensor definitions +#if defined(ESP8266) +#define LOG_DIR "/logs/" // absolute path on LittleFS; parent dir created implicitly +#else +#define LOG_DIR "logs/" // relative to data dir on Linux; get_filename_fullpath prepends it +#endif +#define SENSORS_LOG_FILENAME LOG_DIR "sens.log" // external sensor log data (…sens.log000 … sens.logNNN) +#define SENSORS_LOG_HEADER_FILENAME LOG_DIR "sens.hdr" // external sensor log header +#define SENADJ_FILENAME "senadj.dat" // external sensor adjustment data for programs /** Station macro defines */ #define STN_TYPE_STANDARD 0x00 // standard solenoid station @@ -139,7 +147,7 @@ enum { #define LED_SLOW_BLINK 500 /** Storage / zone expander defines */ -#if defined(ARDUINO) +#if defined(ESP8266) #define MAX_EXT_BOARDS 8 // maximum number of 8-zone expanders (each 16-zone expander counts as 2) #else #define MAX_EXT_BOARDS 24 // allow more zones for linux-based firmwares @@ -150,6 +158,22 @@ enum { #define STATION_NAME_SIZE 32 // maximum number of characters in each station name #define MAX_SOPTS_SIZE 320 // maximum string option size +#if defined(ESP8266) +#define LOG_SPRINKLER_MAX_KB 1200 // max total size of sprinkler .txt log files in KB (~1.2 MB) +#endif + +#define MAX_SENSORS 64 +#define SENSOR_LOG_MAGIC 0x55 +#define SENSOR_LOG_VERSION 0x01 +#define SENSOR_LOG_MAX_FILES 50 // number of data files in the rotation +#if defined(ESP8266) + #define SENSOR_LOG_RECORDS_PER_FILE 819 // records per file; 819×10 B = 8 190 B fits in one 8 KB LittleFS block +#else + // Linux/OSPi/DEMO: filesystem doesn't penalize large appends, so a much bigger + // per-file count is cheap. 50 × 65 535 ≈ 3.3 M records ≈ 33 MB. + #define SENSOR_LOG_RECORDS_PER_FILE 65535 +#endif + #define STATION_SPECIAL_DATA_SIZE (TMP_BUFFER_SIZE - STATION_NAME_SIZE - 12) /** Default string option values */ @@ -172,14 +196,6 @@ enum { #define DEFAULT_LATCH_BOOST_VOLTAGE 9 // default latch boost voltage in volt #define DEFAULT_TARGET_PD_VOLTAGE 75 // default target voltage (unit: 100mV, so 75 means 7500mV ot 7.5V) -#if (defined(__AVR_ATmega1284P__) || defined(__AVR_ATmega1284__)) - #define OS_AVR -#else // all non-AVR platforms support OTF, EMAIL and HTTPS - #define USE_OTF - #define SUPPORT_EMAIL - #define SUPPORT_HTTPS -#endif - /* Weather Adjustment Methods */ enum { WEATHER_METHOD_MANUAL = 0, @@ -194,6 +210,8 @@ enum { enum { MASTER_1 = 0, MASTER_2, + MASTER_3, + MASTER_4, NUM_MASTER_ZONES, }; @@ -285,6 +303,20 @@ enum { IOPT_RESERVE_8, IOPT_WIFI_MODE, //ro IOPT_RESET, //ro + IOPT_MASTER_STATION_3, + IOPT_MASTER_ON_ADJ_3, + IOPT_MASTER_OFF_ADJ_3, + IOPT_MASTER_STATION_4, + IOPT_MASTER_ON_ADJ_4, + IOPT_MASTER_OFF_ADJ_4, + /*IOPT_SENSOR3_TYPE, + IOPT_SENSOR3_OPTION, + IOPT_SENSOR3_ON_DELAY, + IOPT_SENSOR3_OFF_DELAY, + IOPT_SENSOR4_TYPE, + IOPT_SENSOR4_OPTION, + IOPT_SENSOR4_ON_DELAY, + IOPT_SENSOR4_OFF_DELAY,*/ NUM_IOPTS // total number of integer options }; @@ -317,56 +349,7 @@ enum { #undef OS_HW_VERSION /** Hardware defines */ -#if defined(OS_AVR) // for OS 2.3 - - #define OS_HW_VERSION (OS_HW_VERSION_BASE+23) - #define PIN_FREE_LIST {2,10,12,13,14,15,18,19} // Free GPIO pins - - // hardware pins - #define PIN_BUTTON_1 31 // button 1 - #define PIN_BUTTON_2 30 // button 2 - #define PIN_BUTTON_3 29 // button 3 - #define PIN_RFTX 28 // RF data pin - #define PORT_RF PORTA - #define PINX_RF PINA3 - #define PIN_SR_LATCH 3 // shift register latch pin - #define PIN_SR_DATA 21 // shift register data pin - #define PIN_SR_CLOCK 22 // shift register clock pin - #define PIN_SR_OE 1 // shift register output enable pin - - // regular 16x2 LCD pin defines - #define PIN_LCD_RS 19 // LCD rs pin - #define PIN_LCD_EN 18 // LCD enable pin - #define PIN_LCD_D4 20 // LCD d4 pin - #define PIN_LCD_D5 21 // LCD d5 pin - #define PIN_LCD_D6 22 // LCD d6 pin - #define PIN_LCD_D7 23 // LCD d7 pin - #define PIN_LCD_BACKLIGHT 12 // LCD backlight pin - #define PIN_LCD_CONTRAST 13 // LCD contrast pin - - // DC controller pin defines - #define PIN_BOOST 20 // booster pin - #define PIN_BOOST_EN 23 // boost voltage enable pin - - #define PIN_ETHER_CS 4 // Ethernet controller chip select pin - #define PIN_SENSOR1 11 // - #define PIN_SD_CS 0 // SD card chip select pin - #define PIN_FLOWSENSOR_INT 1 // flow sensor interrupt pin (INT1) - #define PIN_EXP_SENSE 4 // expansion board sensing pin (A4) - #define PIN_CURR_SENSE 7 // current sensing pin (A7) - #define PIN_CURR_DIGITAL 24 // digital pin index for A7 - - #define ETHER_BUFFER_SIZE 2048 - - #define wdt_reset() __asm__ __volatile__ ("wdr") // watchdog timer reset - - #define pinModeExt pinMode - #define digitalReadExt digitalRead - #define digitalWriteExt digitalWrite - - #define USE_DISPLAY - #define USE_LCD -#elif defined(ESP8266) // for ESP8266 +#if defined(ESP8266) // for ESP8266 #define OS_HW_VERSION (OS_HW_VERSION_BASE+30) #define IOEXP_PIN 0x80 // base for pins on main IO expander @@ -383,6 +366,7 @@ enum { #define PIN_LATCH_VOLT_SENSE A0 // latch voltage sensing pin #define PIN_FREE_LIST {} // no free GPIO pin at the moment #define ETHER_BUFFER_SIZE 2048 + #define ETHER_BUFFER_ALLOC_SIZE ETHER_BUFFER_SIZE #define PIN_ETHER_CS 16 // Ethernet CS (chip select pin) is 16 on OS 3.2 and above #define ETHER_SPI_CLOCK 10000000L // SPI clock for Ethernet (e.g. 10MHz) @@ -453,7 +437,6 @@ enum { #define V2_PIN_BOOST_SEL IOEXP_PIN+8 #define USE_DISPLAY - #define USE_SSD1306 #elif defined(OSPI) // for OSPi @@ -471,13 +454,13 @@ enum { #define PIN_BUTTON_3 10 // button 3 #define PIN_FREE_LIST {5,6,7,8,9,11,12,13,16,19,20,21,23,25,26} // free GPIO pins - #define ETHER_BUFFER_SIZE 16384 + #define ETHER_BUFFER_SIZE 8192 + #define ETHER_BUFFER_ALLOC_SIZE ETHER_BUFFER_SIZE #define SDA 0 #define SCL 0 #define USE_DISPLAY - #define USE_SSD1306 #else // for demo / simulation // use fake hardware pins @@ -494,13 +477,14 @@ enum { #define PIN_SENSOR2 0 #define PIN_RFTX 0 #define PIN_FREE_LIST {} - #define ETHER_BUFFER_SIZE 16384 + #define ETHER_BUFFER_SIZE 8192 // HTTP client send/receive (weather, notifier, remote station) + #define ETHER_BUFFER_ALLOC_SIZE ETHER_BUFFER_SIZE #endif #if defined(ENABLE_DEBUG) /** Serial debug functions */ - #if defined(ARDUINO) + #if defined(ESP8266) #define DEBUG_BEGIN(x) {Serial.begin(x);} #define DEBUG_PRINT(x) {Serial.print(x);} #define DEBUG_PRINTLN(x) {Serial.println(x);} @@ -516,7 +500,7 @@ enum { #else - #if defined(ARDUINO) + #if defined(ESP8266) // work-around for PIN_SENSOR1 on OS3.2 and above #define DEBUG_BEGIN(x) {Serial.begin(115200); Serial.end();} #else @@ -528,8 +512,8 @@ enum { #endif -/** Re-define avr-specific (e.g. PGM) types to use standard types */ -#if !defined(ARDUINO) +/** Re-define arduino-specific (e.g. PGM) types to use standard types */ +#if !defined(ESP8266) #include #include #include @@ -579,5 +563,3 @@ enum { #define BUTTON_WAIT_HOLD 2 // wait until button hold time expires #define DISPLAY_MSG_MS 2000 // message display time (milliseconds) - -#endif // _DEFINES_H diff --git a/docs/docs/index.md b/docs/docs/index.md index f3fdf3674..a270b7331 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -72,6 +72,6 @@ The firmware compilation instructions below are for OpenSprinkler **v3.x and v2. 1. In VS Code, click `File -> Open Folder` and select the `OpenSprinkler-Firmware` folder. 2. PlatformIO will recognize the `platformio.ini` file in that folder, which contains all the libraries and settings needed to compile the firmware. 3. Click the **PlatformIO: Build** button (with the checkmark icon ✓) in the blue status bar at the bottom of the screen to build the firmware. -4. The default build environment is for OpenSprinkler v3.x (`env:os3x_esp8266`). To build for OpenSprinkler v2.3, switch to `env:os23_atmega1284p`. +4. The default build environment is for OpenSprinkler v3.x (`env:os3x_esp8266`).
diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index 0c24cceb3..2464806fb 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -123,9 +123,10 @@ First confirm the expander is detected (see the question above). Note that detec **The expander is recognized, but zones on the expander don't work.** 1. **Confirm detection:** Follow the questions above to verify the expander is detected. -2. **Check the COM (common) wire:** All zones including expanded must have a common wire that goes to the COM terminal on the main controller. A missing/broken COM wire will cause zones to stop working. -3. **Perform a [solenoid resistance test](#wiring-and-solenoids)** to rule out solenoid and wiring issues. -4. **Check zones on the main controller:** If the first eight zones exhibit the same issue, this is likely due to a broken fuse/COM wire; otherwise, it's more likely an expander-specific issue. +2. **Reboot the controller** to allow it to re-detect expanders. Any expander change (connecting, disconnecting, changing DIP) should be done **while the main controller is powered off**. +3. **Check the COM (common) wire:** All zones including expanded must have a common wire that goes to the COM terminal on the main controller. A missing/broken COM wire will cause zones to stop working. +4. **Perform a [solenoid resistance test](#wiring-and-solenoids)** to rule out solenoid and wiring issues. +5. **Check zones on the main controller:** If the first eight zones exhibit the same issue, this is likely due to a broken fuse/COM wire; otherwise, it's more likely an expander-specific issue. --- diff --git a/espconnect.h b/espconnect.h index edf595c1d..e67613d79 100644 --- a/espconnect.h +++ b/espconnect.h @@ -17,11 +17,7 @@ * along with this program. If not, see * . */ - -#if defined(ESP8266) - -#ifndef _ESP_CONNECT_H -#define _ESP_CONNECT_H +#pragma once #include #include @@ -34,6 +30,3 @@ String scan_network(); void start_network_ap(const char *ssid, const char *pass); void start_network_sta(const char *ssid, const char *pass, int32_t channel=0, const unsigned char *mac=NULL); void start_network_sta_with_ap(const char *ssid, const char *pass, int32_t channel=0, const unsigned char *mac=NULL); -#endif - -#endif diff --git a/external/influxdb-cpp b/external/influxdb-cpp new file mode 160000 index 000000000..75a4ec9bb --- /dev/null +++ b/external/influxdb-cpp @@ -0,0 +1 @@ +Subproject commit 75a4ec9bb216e4d4a073c363e5ba10f95900e3ec diff --git a/font.h b/font.h index dc1ad7f10..0a2b37320 100644 --- a/font.h +++ b/font.h @@ -1,7 +1,6 @@ // Created by http://oleddisplay.squix.ch/ Consider a donation // In case of problems make sure that you are using the font file with the correct version! -#ifndef FONT_H -#define FONT_H +#pragma once const unsigned char Monospaced_plain_13[] PROGMEM = { 0x08, // Width: 8 @@ -459,5 +458,3 @@ const unsigned char Monospaced_plain_13[] PROGMEM = { 0x00,0x00,0x00,0xF8,0xFF,0x00,0x80,0x18,0x00,0x40,0x10,0x00,0x40,0x10,0x00,0xC0,0x18,0x00,0x80,0x0F, // 254 0x00,0x00,0x00,0xC0,0x80,0x00,0x08,0x87,0x00,0x00,0xFC,0x00,0x00,0x38,0x00,0x08,0x07,0x00,0xC0,0x01 // 255 }; - -#endif \ No newline at end of file diff --git a/gpio.cpp b/gpio.cpp index 0b0df7514..8656d24b0 100644 --- a/gpio.cpp +++ b/gpio.cpp @@ -25,8 +25,6 @@ #if defined(ARDUINO) -#if defined(ESP8266) - #include #include "defines.h" @@ -173,7 +171,6 @@ unsigned char digitalReadExt(unsigned char pin) { return digitalRead(pin); } } -#endif #elif defined(OSPI) diff --git a/gpio.h b/gpio.h index d9b028b84..45548a206 100644 --- a/gpio.h +++ b/gpio.h @@ -21,13 +21,10 @@ * . */ -#ifndef GPIO_H -#define GPIO_H +#pragma once #if defined(ARDUINO) -#if defined(ESP8266) - #include "Arduino.h" // PCA9555 register defines @@ -45,6 +42,7 @@ class IOEXP { public: IOEXP(uint8_t addr=255) { address = addr; type = IOEXP_TYPE_NONEXIST; } + virtual ~IOEXP() {} virtual void pinMode(uint8_t pin, uint8_t IOMode) { } virtual uint16_t i2c_read(uint8_t reg) { return 0xFFFF; } @@ -113,8 +111,6 @@ void pinModeExt(unsigned char pin, unsigned char mode); void digitalWriteExt(unsigned char pin, unsigned char value); unsigned char digitalReadExt(unsigned char pin); -#endif // ESP8266 - #else #include @@ -145,4 +141,3 @@ void attachInterrupt(int pin, const char* mode, void (*isr)(void)); #endif -#endif // GPIO_H diff --git a/i2cd.cpp b/i2cd.cpp new file mode 100644 index 000000000..3bc67916f --- /dev/null +++ b/i2cd.cpp @@ -0,0 +1,5 @@ +#if defined(OSPI) +#include "i2cd.h" + +I2CBus Bus; +#endif \ No newline at end of file diff --git a/i2cd.h b/i2cd.h index 53d46f9a0..9aba8dc8b 100644 --- a/i2cd.h +++ b/i2cd.h @@ -1,5 +1,6 @@ -#ifndef I2CD_H -#define I2CD_H +#pragma once + +#if defined(OSPI) #include #include @@ -7,92 +8,172 @@ extern "C" { #include #include +} #include #include "utils.h" -} + +class I2CBus { +public: + I2CBus() {} + /*~I2CBus() { + if (_file >= 0) { + close(_file); + } + }*/ + int begin(const char *bus) { + _file = open(bus, O_RDWR); + if (_file < 0) { + return _file; + } + + return 0; + } + + int begin() { return begin(getDefaultBus()); } + + int send(unsigned char addr, unsigned char reg, unsigned char data) { + if (_file < 0) return -1; + int res = ioctl(_file, I2C_SLAVE, addr); + if (res < 0) return -1; + return i2c_smbus_write_byte_data(_file, reg, data); + } + + int send_transaction(unsigned char addr, unsigned char transaction_id, unsigned char transaction_buffer_length, unsigned char *transaction_buffer) { + if (_file < 0) return -1; + int res = ioctl(_file, I2C_SLAVE, addr); + if (res < 0) return -1; + return i2c_smbus_write_i2c_block_data( + _file, transaction_id, transaction_buffer_length, transaction_buffer); + } + + int read(unsigned char addr, unsigned char reg, unsigned char length, unsigned char *values) { + if (_file < 0) return -1; + int res = ioctl(_file, I2C_SLAVE, addr); + if (res < 0) return -1; + return i2c_smbus_read_i2c_block_data(_file, reg, length, values); + } + + int send_word(unsigned char addr, unsigned char reg, unsigned short data) { + if (_file < 0) return -1; + int res = ioctl(_file, I2C_SLAVE, addr); + if (res < 0) return -1; + return i2c_smbus_write_word_data(_file, reg, data); + } + + int read_word(unsigned char addr, unsigned char reg) { + if (_file < 0) return -1; + int res = ioctl(_file, I2C_SLAVE, addr); + if (res < 0) return -1; + return i2c_smbus_read_word_data(_file, reg); + } + + int detect(unsigned char addr) { + if (_file < 0) return -1; + int res = ioctl(_file, I2C_SLAVE, addr); + if (res < 0) return -1; + + res = i2c_smbus_read_byte(_file); + if (res < 0) { + return res; + } else { + return 0; + } + } + +private: + int _file = -1; + + const char *getDefaultBus() { + switch (get_board_type()) { + case BoardType::RaspberryPi_bcm2712: + case BoardType::RaspberryPi_bcm2711: + case BoardType::RaspberryPi_bcm2837: + case BoardType::RaspberryPi_bcm2836: + case BoardType::RaspberryPi_bcm2835: + return "/dev/i2c-1"; + case BoardType::Unknown: + case BoardType::RaspberryPi_Unknown: + default: + return "/dev/i2c-0"; + } + } +}; class I2CDevice { public: - I2CDevice() {} - - int begin(const char *bus, unsigned char addr) { - _file = open(bus, O_RDWR); - if (_file < 0) { - return _file; - } - - return ioctl(_file, I2C_SLAVE, addr); - } - - int begin(unsigned char addr) { return begin(getDefaultBus(), addr); } - - int begin_transaction(unsigned char id) { - if (transaction) { - return -1; - } else { - transaction_id = id; - transaction = true; - memset(transaction_buffer, 0x00, sizeof(transaction_buffer)); - transaction_buffer_length = 0; - return 0; - } - } - - int end_transaction() { - if (transaction) { - transaction = false; - return send_transaction(); - } else { - return -1; - } - } - - int send(unsigned char reg, unsigned char data) { - if (transaction) { - if (reg != transaction_id) { - return -1; - } - - int res = 0; - if (transaction_buffer_length >= sizeof(transaction_buffer)) { - res = send_transaction(); - transaction_buffer_length = 0; - } - - transaction_buffer[transaction_buffer_length] = data; - transaction_buffer_length++; - return res; - } else { - return i2c_smbus_write_byte_data(_file, reg, data); - } - } + I2CDevice(I2CBus &bus, unsigned char addr) : _addr(addr), _bus(&bus) {} + + bool detect() { + return _bus->detect(_addr)==0; + } + + int begin_transaction(unsigned char id) { + if (transaction) { + return -1; + } else { + transaction_id = id; + transaction = true; + memset(transaction_buffer, 0x00, sizeof(transaction_buffer)); + transaction_buffer_length = 0; + return 0; + } + } + + int end_transaction() { + if (transaction) { + transaction = false; + return send_transaction(); + } else { + return -1; + } + } + + int send(unsigned char reg, unsigned char data) { + if (transaction) { + if (reg != transaction_id) { + return -1; + } + + int res = 0; + if (transaction_buffer_length >= sizeof(transaction_buffer)) { + res = send_transaction(); + transaction_buffer_length = 0; + } + + transaction_buffer[transaction_buffer_length] = data; + transaction_buffer_length++; + return res; + } else { + return _bus->send(_addr, reg, data); + } + } + + int read(unsigned char reg, unsigned char length, unsigned char *values) { + return _bus->read(_addr, reg, length, values); + } + + int send_word(unsigned char reg, unsigned short data) { + return _bus->send_word(_addr, reg, data); + } + + int read_word(unsigned char reg) { + return _bus->read_word(_addr, reg); + } private: - int _file = -1; - bool transaction = false; - unsigned char transaction_id = 0; - unsigned char transaction_buffer[32]; - unsigned char transaction_buffer_length = 0; - - const char *getDefaultBus() { - switch (get_board_type()) { - case BoardType::RaspberryPi_bcm2712: - case BoardType::RaspberryPi_bcm2711: - case BoardType::RaspberryPi_bcm2837: - case BoardType::RaspberryPi_bcm2836: - case BoardType::RaspberryPi_bcm2835: - return "/dev/i2c-1"; - case BoardType::Unknown: - case BoardType::RaspberryPi_Unknown: - default: - return "/dev/i2c-0"; - } - } - - int send_transaction() { - return i2c_smbus_write_i2c_block_data( - _file, transaction_id, transaction_buffer_length, transaction_buffer); - } + I2CBus *_bus; + unsigned char _addr; + + bool transaction = false; + unsigned char transaction_id = 0; + unsigned char transaction_buffer[32]; + unsigned char transaction_buffer_length = 0; + + int send_transaction() { + return _bus->send_transaction(_addr, transaction_id, transaction_buffer_length, transaction_buffer); + } }; -#endif // I2CD_H \ No newline at end of file +extern I2CBus Bus; + +#endif // I2CD_H diff --git a/images.h b/images.h index adaf517e4..02c3ed259 100644 --- a/images.h +++ b/images.h @@ -1,5 +1,4 @@ -#ifndef IMAGES_H -#define IMAGES_H +#pragma once enum { ICON_ETHER_CONNECTED = 0, @@ -13,7 +12,6 @@ enum { NUM_CUSTOM_ICONS }; -#if defined(ESP8266) || defined(PIN_SENSOR2) enum { LCD_CURSOR_REMOTEXT = 11, LCD_CURSOR_RAINDELAY,// 12 @@ -21,22 +19,8 @@ enum { LCD_CURSOR_SENSOR2, // 14 LCD_CURSOR_NETWORK // 15 }; -#else -enum { - LCD_CURSOR_SENSOR2 = 11, - LCD_CURSOR_REMOTEXT, // 12 - LCD_CURSOR_RAINDELAY,// 13 - LCD_CURSOR_SENSOR1, // 14 - LCD_CURSOR_NETWORK // 15 -}; -#endif - -#if defined(USE_SSD1306) - -#if not defined(ARDUINO) -#define PROGMEM -#endif +#if defined(USE_DISPLAY) #define WiFi_Logo_width 60 #define WiFi_Logo_height 36 @@ -141,102 +125,4 @@ const unsigned char _iconimage_pswitch[] PROGMEM = { */ -#elif defined(USE_LCD) - -const char _iconimage_connected[] PROGMEM = { - B00000, - B00000, - B00000, - B00001, - B00001, - B00101, - B00101, - B10101 -}; - -const char _iconimage_disconnected[] PROGMEM = { - B00000, - B10100, - B01000, - B10101, - B00001, - B00101, - B00101, - B10101 -}; - -const char _iconimage_remotext[] PROGMEM = { - B00000, - B00000, - B00000, - B10001, - B01011, - B00101, - B01001, - B11110 -}; - -const char _iconimage_raindelay[] PROGMEM = { - B00000, - B01110, - B10101, - B10101, - B10111, - B10001, - B10001, - B01110 -}; - -const char _iconimage_rain[] PROGMEM = { - B00000, - B00000, - B00110, - B01001, - B11111, - B00000, - B10101, - B10101 -}; - -const char _iconimage_soil[] PROGMEM = { - B00100, - B00100, - B01010, - B01010, - B10001, - B10001, - B10001, - B01110 -}; - -/* -const unsigned char _iconimage_flow[] PROGMEM = { - B00000, - B00000, - B00000, - B11010, - B10010, - B11010, - B10011, - B00000 -}; - -const unsigned char _iconimage_pswitch[] PROGMEM = { - B00000, - B11000, - B10100, - B11000, - B10010, - B10110, - B00010, - B00111 -}; - -// todo - -*/ - #endif - - -#endif \ No newline at end of file diff --git a/main.cpp b/main.cpp index bcc87495a..50be303ac 100644 --- a/main.cpp +++ b/main.cpp @@ -32,39 +32,26 @@ #include "main.h" #include "notifier.h" -#if defined(ARDUINO) -#include -#endif - -#if defined(ARDUINO) - #if defined(ESP8266) - ESP8266WebServer *update_server = NULL; - DNSServer *dns = NULL; - ENC28J60lwIP enc28j60(PIN_ETHER_CS); // ENC28J60 lwip for wired Ether - Wiznet5500lwIP w5500(PIN_ETHER_CS); // W5500 lwip for wired Ether - lwipEth eth; - bool useEth = false; // tracks whether we are using WiFi or wired Ether connection - #else - EthernetServer *m_server = NULL; - EthernetClient *m_client = NULL; - SdFat sd; // SD card object - bool useEth = true; - #endif - unsigned long getNtpTime(); +#if defined(ESP8266) + #include + ESP8266WebServer *update_server = NULL; + DNSServer *dns = NULL; + ENC28J60lwIP enc28j60(PIN_ETHER_CS); // ENC28J60 lwip for wired Ether + Wiznet5500lwIP w5500(PIN_ETHER_CS); // W5500 lwip for wired Ether + lwipEth eth; + bool useEth = false; // tracks whether we are using WiFi or wired Ether connection + uint32_t getNtpTime(); #else // header and defs for RPI/Linux + #include bool useEth = false; #endif -#if defined(USE_OTF) - OTF::OpenThingsFramework *otf = NULL; -#endif +OTF::OpenThingsFramework *otf = NULL; -#if defined(USE_SSD1306) - #if defined(ESP8266) - static uint16_t led_blink_ms = LED_FAST_BLINK; - #else - static uint16_t led_blink_ms = 0; - #endif +#if defined(ESP8266) +static uint16_t led_blink_ms = LED_FAST_BLINK; +#else +static uint16_t led_blink_ms = 0; #endif #define STRINGIFY(x) #x @@ -72,7 +59,7 @@ const char *user_agent_string = "OpenSprinkler/" TOSTRING(OS_FW_VERSION) "#" TOSTRING(OS_FW_MINOR); -void manual_start_program(unsigned char, unsigned char, unsigned char); +void manual_start_program(unsigned char, unsigned char, unsigned char, unsigned char usa=0); // Small variations have been added to the timing values below // to minimize conflicting events @@ -87,9 +74,10 @@ void manual_start_program(unsigned char, unsigned char, unsigned char); #define DHCP_CHECKLEASE_INTERVAL 3600L // DHCP check lease interval (in seconds) #define FLOWPOLL_INTERVAL 5 // flow poll interval (in milli-seconds) #define CURRPOLL_INTERVAL 20 // current poll interval (in milli-seconds) +#define SENSORPOLL_INTERVAL 5000 // sensor poll interval (in milli-seconds) // Define buffers: need them to be sufficiently large to cover string option reading -char ether_buffer[ETHER_BUFFER_SIZE*2]; // ethernet buffer, make it twice as large to allow overflow -char tmp_buffer[TMP_BUFFER_SIZE*2]; // scratch buffer, make it twice as large to allow overflow +char ether_buffer[ETHER_BUFFER_ALLOC_SIZE]; // HTTP client send/receive buffer +char tmp_buffer[TMP_BUFFER_ALLOC_SIZE]; // scratch buffer // ====== Object defines ====== OpenSprinkler os; // OpenSprinkler object @@ -102,8 +90,8 @@ NotifQueue notif; // NotifQueue object * flow_stop - time when valve turns off (last rising edge pulse detected before off) * flow_gallons - total # of gallons+1 from flow_start to flow_stop * flow_last_gpm - last flow rate measured (averaged over flow_gallons) from last valve stopped (used to write to log file). */ -ulong flow_begin, flow_start, flow_stop, flow_gallons, flow_rt_reset, last_flow_rt; -ulong flow_count = 0; +uint32_t flow_begin, flow_start, flow_stop, flow_gallons, flow_rt_reset, last_flow_rt; +uint32_t flow_count = 0; unsigned char prev_flow_state = HIGH; float flow_last_gpm = 0; int32_t flow_rt_period = -1; @@ -111,7 +99,7 @@ uint32_t reboot_timer = 0; unsigned char curr_alert_sid = 0; void flow_poll() { - ulong curr = millis(); + uint32_t curr = millis(); // Resets counter if timeout occurs if (flow_rt_reset && curr > flow_rt_reset) { @@ -156,7 +144,7 @@ void flow_poll() { } // Use exponential moving average (alpha=0.2) if flow has been previosuly calculated, otherwise just set the value - ulong curr_period = curr - last_flow_rt; + uint32_t curr_period = curr - last_flow_rt; if (flow_rt_period > 0) { flow_rt_period = (curr_period / 5 + flow_rt_period * 4 / 5); } else { @@ -165,7 +153,7 @@ void flow_poll() { // calculates the flow rate scaled by the window size to simulated a fixed point number if (flow_rt_period > 0) { - os.flowcount_rt = (ulong) (FLOWCOUNT_RT_WINDOW * 1000L / flow_rt_period); + os.flowcount_rt = (uint32_t) (FLOWCOUNT_RT_WINDOW * 1000L / flow_rt_period); // Sets the timeout to be 10x the last period flow_rt_reset = curr + (curr - last_flow_rt) * 10; } else { @@ -196,7 +184,7 @@ bool ui_confirm(PGM_P str) { os.lcd_print_line_clear_pgm(str, 0); os.lcd_print_line_clear_pgm(PSTR("(B1:No, B3:Yes)"), 1); unsigned char button; - ulong start = millis(); + uint32_t start = millis(); do { button = os.button_read(BUTTON_WAIT_NONE); if((button&BUTTON_MASK)==BUTTON_3 && (button&BUTTON_FLAG_DOWN)) return true; @@ -213,17 +201,15 @@ void ui_state_machine() { if(millis() - last_usm <= UI_STATE_MACHINE_INTERVAL) { return; } last_usm = millis(); -#if defined(USE_SSD1306) // process screen led - static ulong led_toggle_prev = 0; + static uint32_t led_toggle_prev = 0; if(led_blink_ms) { - ulong tm = millis(); + uint32_t tm = millis(); if(tm - led_toggle_prev > led_blink_ms) { // overflow proof timeout os.toggle_screen_led(); led_toggle_prev = tm; } } -#endif if (!os.button_timeout) { os.lcd_set_brightness(0); @@ -249,86 +235,68 @@ void ui_state_machine() { if(!ui_confirm(PSTR("Start 2s test?"))) {ui_state = UI_STATE_DEFAULT; break;} manual_start_program(255, 0, QUEUE_OPTION_REPLACE); } else if (digitalReadExt(PIN_BUTTON_2)==0) { // if B2 is pressed while holding B1, display gateway IP - #if defined(USE_SSD1306) - os.lcd.setAutoDisplay(false); - #endif + os.lcd.setAutoDisplay(false); os.lcd.clear(0, 1); os.lcd.setCursor(0, 0); - #if defined(ARDUINO) #if defined(ESP8266) - if (useEth) { os.lcd.print(eth.gatewayIP()); } - else { os.lcd.print(WiFi.gatewayIP()); } + if (useEth) { os.lcd.print(eth.gatewayIP()); } + else { os.lcd.print(WiFi.gatewayIP()); } #else - { os.lcd.print(Ethernet.gatewayIP()); } - #endif - #else - route_t route = get_route(); - char str[INET_ADDRSTRLEN]; + route_t route = get_route(); + char str[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &(route.gateway), str, INET_ADDRSTRLEN); - os.lcd.print(str); + inet_ntop(AF_INET, &(route.gateway), str, INET_ADDRSTRLEN); + os.lcd.print(str); #endif os.lcd.setCursor(0, 1); os.lcd_print_pgm(PSTR("(gwip)")); ui_state = UI_STATE_DISP_IP; - #if defined(USE_SSD1306) - os.lcd.display(); - os.lcd.setAutoDisplay(true); - #endif + os.lcd.display(); + os.lcd.setAutoDisplay(true); } else { // if no other button is clicked, stop all zones if(!ui_confirm(PSTR("Stop all zones?"))) {ui_state = UI_STATE_DEFAULT; break;} reset_all_stations(); } } else { // clicking B1: display device IP and port - #if defined(USE_SSD1306) - os.lcd.setAutoDisplay(false); - #endif + os.lcd.setAutoDisplay(false); os.lcd.clear(0, 1); os.lcd.setCursor(0, 0); - #if defined(ARDUINO) #if defined(ESP8266) - if (useEth) { os.lcd.print(eth.localIP()); } - else { os.lcd.print(WiFi.localIP()); } + if (useEth) { os.lcd.print(eth.localIP()); } + else { os.lcd.print(WiFi.localIP()); } #else - { os.lcd.print(Ethernet.localIP()); } - #endif - #else - route_t route = get_route(); - char str[INET_ADDRSTRLEN]; - in_addr_t ip = get_ip_address(route.iface); + route_t route = get_route(); + char str[INET_ADDRSTRLEN]; + in_addr_t ip = get_ip_address(route.iface); - inet_ntop(AF_INET, &ip, str, INET_ADDRSTRLEN); - os.lcd.print(str); + inet_ntop(AF_INET, &ip, str, INET_ADDRSTRLEN); + os.lcd.print(str); #endif os.lcd.setCursor(0, 1); os.lcd_print_pgm(PSTR(":")); uint16_t httpport = (uint16_t)(os.iopts[IOPT_HTTPPORT_1]<<8) + (uint16_t)os.iopts[IOPT_HTTPPORT_0]; os.lcd.print(httpport); os.lcd_print_pgm(PSTR(" (ip:port)")); - #if defined(USE_OTF) - os.lcd.setCursor(0, 2); - os.lcd_print_pgm(PSTR("OTC:")); - switch(otf->getCloudStatus()) { - case OTF::NOT_ENABLED: - os.lcd_print_pgm(PSTR(" not enabled")); - break; - case OTF::UNABLE_TO_CONNECT: - os.lcd_print_pgm(PSTR("connecting..")); - break; - case OTF::DISCONNECTED: - os.lcd_print_pgm(PSTR("disconnected")); - break; - case OTF::CONNECTED: - os.lcd_print_pgm(PSTR(" Connected")); - break; - } - #endif + os.lcd.setCursor(0, 2); + os.lcd_print_pgm(PSTR("OTC:")); + switch(otf->getCloudStatus()) { + case OTF::NOT_ENABLED: + os.lcd_print_pgm(PSTR(" not enabled")); + break; + case OTF::UNABLE_TO_CONNECT: + os.lcd_print_pgm(PSTR("connecting..")); + break; + case OTF::DISCONNECTED: + os.lcd_print_pgm(PSTR("disconnected")); + break; + case OTF::CONNECTED: + os.lcd_print_pgm(PSTR(" Connected")); + break; + } ui_state = UI_STATE_DISP_IP; - #if defined(USE_SSD1306) - os.lcd.display(); - os.lcd.setAutoDisplay(true); - #endif + os.lcd.display(); + os.lcd.setAutoDisplay(true); } break; case BUTTON_2: @@ -415,15 +383,11 @@ void ui_state_machine() { // ====================== // Setup Function // ====================== -#if defined(ARDUINO) +#if defined(ESP8266) void do_setup() { /* Clear WDT reset flag. */ -#if defined(ESP8266) WiFi.persistent(false); led_blink_ms = LED_FAST_BLINK; -#else - MCUSR &= ~(1< 15) { - // reset after 120 seconds of timeout - sysReset(); - } -} -#endif - #else void initialize_otf(); @@ -525,7 +468,7 @@ void do_setup() { #endif -void turn_on_station(unsigned char sid, ulong duration); +void turn_on_station(unsigned char sid, uint32_t duration); static void check_network(); void check_weather(); static bool process_special_program_command(const char*, uint32_t curr_time); @@ -533,6 +476,7 @@ static void perform_ntp_sync(); #if defined(ESP8266) bool delete_log_oldest(); +uint32_t get_sprinkler_log_size(); void start_server_ap(); void start_server_client(); static Ticker reboot_ticker; @@ -548,9 +492,9 @@ void reboot_in(uint32_t ms) { void handle_web_request(char *p); #endif -ulong currpoll_timeout = 0; +uint32_t currpoll_timeout = 0; void overcurrent_monitor() { -#if defined(ARDUINO) +#if defined(ESP8266) // If a zone is turning on, do immediate overcurrent monitoring here for ~50ms if (curr_alert_sid) { int16_t imax = os.get_imax(); @@ -579,21 +523,30 @@ void overcurrent_monitor() { /** Main Loop */ void do_loop() { - static ulong flowpoll_timeout = 0; + static uint32_t flowpoll_timeout = 0; if(os.iopts[IOPT_SENSOR1_TYPE]==SENSOR_TYPE_FLOW) { // handle flow sensor using polling. Maximum freq is 1/(2*FLOWPOLL_INTERVAL) // e.g. if FLOWPOLL_INTERVAL is 3ms, maximum freq is 166Hz - ulong tm = millis(); - if((long)(tm-flowpoll_timeout) > 0) { // overflow proof timeout + uint32_t tm = millis(); + if((int32_t)(tm-flowpoll_timeout) > 0) { // overflow proof timeout flowpoll_timeout = tm+FLOWPOLL_INTERVAL; flow_poll(); } } -#if defined(ARDUINO) { - ulong tn = millis(); - if((long)(tn-currpoll_timeout) > 0) { // overflow proof timeout + static uint32_t sensorpoll_timeout = 0; + uint32_t tm = millis(); + if((int32_t)(tm-sensorpoll_timeout) > 0) { + sensorpoll_timeout = tm + SENSORPOLL_INTERVAL; + os.poll_sensors(); + } + } + +#if defined(ESP8266) + { + uint32_t tn = millis(); + if((int32_t)(tn-currpoll_timeout) > 0) { // overflow proof timeout int16_t curr = (int16_t)os.read_current(); int16_t imax = os.get_imax(); if((imax > 0) && (curr > imax)) { @@ -609,19 +562,20 @@ void do_loop() #endif static time_os_t last_time = 0; - static ulong last_minute = 0; + static uint32_t last_minute = 0; unsigned char bid, sid, s, pid, qid, gid, bitvalue; ProgramStruct prog; os.status.mas = os.iopts[IOPT_MASTER_STATION]; os.status.mas2= os.iopts[IOPT_MASTER_STATION_2]; + os.status.mas3= os.iopts[IOPT_MASTER_STATION_3]; + os.status.mas4= os.iopts[IOPT_MASTER_STATION_4]; time_os_t curr_time = os.now_tz(); // ====== Process Ethernet packets ====== -#if defined(ARDUINO) // Process Ethernet packets for Arduino - #if defined(ESP8266) - static ulong connecting_timeout; +#if defined(ESP8266) // Process Ethernet packets for Arduino + static uint32_t connecting_timeout; switch(os.state) { case OS_STATE_INITIAL: if(useEth) { @@ -676,7 +630,7 @@ void do_loop() os.state = OS_STATE_CONNECTED; connecting_timeout = 0; } else { - if((long)(millis()-connecting_timeout)>0) { + if((int32_t)((uint32_t)millis()-connecting_timeout)>0) { os.state = OS_STATE_INITIAL; WiFi.disconnect(true); DEBUG_PRINTLN(F("timeout")); @@ -717,45 +671,6 @@ void do_loop() break; } - #else // AVR - - static unsigned long dhcp_timeout = 0; - if(curr_time > dhcp_timeout) { - Ethernet.maintain(); - dhcp_timeout = curr_time + DHCP_CHECKLEASE_INTERVAL; - } - EthernetClient client = m_server->available(); - if (client) { - ulong cli_timeout = now() + CLIENT_READ_TIMEOUT; - size_t size = 0; - while(client.connected() && now() < cli_timeout) { - size = client.available(); // wait till we have client data available - if(size>0) break; - } - if(size>0) { - size_t len = 0; - while (client.available() && now()0) { - m_client = &client; - ether_buffer[len] = 0; // properly end the buffer - handle_web_request(ether_buffer); - m_client = NULL; - } - } - client.stop(); - } - - wdt_reset(); // reset watchdog timer - wdt_timeout = 0; - - #endif - ui_state_machine(); #else // Process Ethernet packets for RPI/LINUX @@ -858,7 +773,7 @@ void do_loop() } // ====== Schedule program data ====== - ulong curr_minute = curr_time / 60; + uint32_t curr_minute = curr_time / 60; boolean match_found = false; RuntimeQueueStruct *q; // since the granularity of start time is minute @@ -875,6 +790,9 @@ void do_loop() unsigned char runcount = prog.check_match(curr_time, &will_delete); if(runcount>0) { // program match found + unsigned char wl = get_program_water_percent(prog); + float sensor_adj = get_program_sensor_adj(pid); + // check and process special program command if(process_special_program_command(prog.name, curr_time)) continue; @@ -882,39 +800,24 @@ void do_loop() unsigned char order[os.nstations]; prog.gen_station_runorder(runcount, order); - // prepare watering level - unsigned char wl = 100; // default 100% - if (prog.use_weather) { // if program is set to use weather scaling - if (wt_restricted > 0) wl = 0; // if watering restriction is active - else { - wl = os.iopts[IOPT_WATER_PERCENTAGE]; - // If historical data is enabled and interval program, overwrite watering percentage with historical one. - if (mda == 100 && prog.type == PROGRAM_TYPE_INTERVAL && md_N > 0) { - // Use interval length unless longer than available data - if ((unsigned int)prog.days[1]-1 < md_N){ - wl = md_scales[prog.days[1]-1]; - } else { - wl = md_scales[md_N-1]; - } - } - } - } - // process all selected stations for(unsigned char oi=0;oi>3; s=sid&0x07; // skip if the station is a master station (because master cannot be scheduled independently - if ((os.status.mas==sid+1) || (os.status.mas2==sid+1)) + if ((os.status.mas==sid+1) || (os.status.mas2==sid+1) || + (os.status.mas3==sid+1) || (os.status.mas4==sid+1)) continue; + // TODO: compare with old code + uint32_t dur = prog.durations[sid]; // if station has non-zero water time and the station is not disabled - if (prog.durations[sid] && !(os.attrib_dis[bid]&(1< os.checkwt_lasttime + CHECK_WEATHER_TIMEOUT)) { os.checkwt_lasttime = ntz; - #if defined(ARDUINO) + #if defined(USE_DISPLAY) if (!ui_state) { os.lcd_print_line_clear_pgm(PSTR("Check Weather..."),1); } @@ -1242,7 +1149,7 @@ void check_weather() { /** Turn on a station * This function turns on a scheduled station */ -void turn_on_station(unsigned char sid, ulong duration) { +void turn_on_station(unsigned char sid, uint32_t duration) { // RAH implementation of flow sensor flow_start=0; //Added flow_gallons reset to station turn on. @@ -1257,7 +1164,7 @@ void turn_on_station(unsigned char sid, ulong duration) { void handle_shift_remaining_stations(RuntimeQueueStruct* q, unsigned char gid, time_os_t curr_time) { RuntimeQueueStruct *s = pd.queue; time_os_t q_end_time = q->st + q->dur; - ulong remainder = 0; + uint32_t remainder = 0; if (q_end_time > curr_time) { // remainder is non-zero remainder = (q->st < curr_time) ? q_end_time - curr_time : q->dur; @@ -1337,7 +1244,7 @@ void turn_off_station(unsigned char sid, time_os_t curr_time, unsigned char shif if (!station_bit) { return; } } //else { return; } - #if defined(ARDUINO) + #if defined(ESP8266) int16_t current = (int16_t)os.read_current(true); // use ema value int16_t imin = os.get_imin(); // if current is less than imin threshold and hardware type is AC or DC @@ -1360,7 +1267,8 @@ void turn_off_station(unsigned char sid, time_os_t curr_time, unsigned char shif // because we may be turning off a station that hasn't started yet if (curr_time >= q->st) { // record lastrun log (only for non-master stations) - if (os.status.mas != (sid + 1) && os.status.mas2 != (sid + 1)) { + if (os.status.mas != (sid + 1) && os.status.mas2 != (sid + 1) && + os.status.mas3 != (sid + 1) && os.status.mas4 != (sid + 1)) { pd.lastrun.station = sid; pd.lastrun.program = q->pid; pd.lastrun.duration = curr_time - q->st; @@ -1416,6 +1324,8 @@ void process_dynamic_events(time_os_t curr_time) { // ignore master stations because they are handled separately if (os.status.mas == sid+1) continue; if (os.status.mas2== sid+1) continue; + if (os.status.mas3== sid+1) continue; + if (os.status.mas4== sid+1) continue; // If this is a normal program (not a run-once or test program) // and either the controller is disabled, or // if raining and ignore rain bit is cleared @@ -1437,7 +1347,7 @@ void process_dynamic_events(time_os_t curr_time) { * this function determines the appropriate start and dequeue times * of stations bound to master stations with on and off adjustments */ -void handle_master_adjustments(time_os_t curr_time, RuntimeQueueStruct *q, unsigned char gid, ulong *seq_start_times) { +void handle_master_adjustments(time_os_t curr_time, RuntimeQueueStruct *q, unsigned char gid, uint32_t *seq_start_times) { int16_t start_adj = 0; int16_t dequeue_adj = 0; @@ -1473,7 +1383,7 @@ void handle_master_adjustments(time_os_t curr_time, RuntimeQueueStruct *q, unsig * preemptively, before existing queued stations */ void schedule_all_stations(time_os_t curr_time, unsigned char qo) { - ulong con_start_time = curr_time; // concurrent start time + uint32_t con_start_time = curr_time; // concurrent start time // if the queue is paused, make sure the start time is after the scheduled pause ends if (os.status.pause_state) { con_start_time += os.pause_timer; @@ -1494,8 +1404,8 @@ void schedule_all_stations(time_os_t curr_time, unsigned char qo) { stagger[i] += stagger[i-1]; // accumulate stagger time } - ulong seq_start_times[NUM_SEQ_GROUPS]; // sequential start times - ulong seq_adjustments[NUM_SEQ_GROUPS]; // adjustment amounts for insert-to-front + uint32_t seq_start_times[NUM_SEQ_GROUPS]; // sequential start times + uint32_t seq_adjustments[NUM_SEQ_GROUPS]; // adjustment amounts for insert-to-front memset(seq_adjustments, 0, sizeof(seq_adjustments)); unsigned char re = os.iopts[IOPT_REMOTE_EXT_MODE]; @@ -1523,14 +1433,14 @@ void schedule_all_stations(time_os_t curr_time, unsigned char qo) { if (!os.is_sequential_station(q->sid) || re) continue; gid = os.get_station_gid(q->sid); - ulong adjustment = seq_adjustments[gid] + stagger[gid]; + uint32_t adjustment = seq_adjustments[gid] + stagger[gid]; if (adjustment == 0) continue; // no adjustment needed for this group // Only adjust sequential stations in the same group // If station is currently running if (curr_time >= q->st && curr_time < q->st + q->dur) { - turn_off_station(q->sid, curr_time); // TODO: double check the logic - ulong remaining = q->dur - (curr_time - q->st); + turn_off_station(q->sid, curr_time); // TODO: re-check the logic + uint32_t remaining = q->dur - (curr_time - q->st); q->st = curr_time + adjustment; q->dur = remaining; q->deque_time += adjustment; @@ -1681,10 +1591,28 @@ void reset_all_stations(bool running_ones_only) { * If pid==255, this is a short test program (2 second per station) * If pid > 0. run program pid-1 */ -void manual_start_program(unsigned char pid, unsigned char uwt, unsigned char qo) { + +uint8_t get_program_water_percent(const ProgramStruct &prog) { + if (!prog.use_weather) return 100; + if (wt_restricted > 0) return 0; + uint8_t wl = os.iopts[IOPT_WATER_PERCENTAGE]; + if (mda == 100 && prog.type == PROGRAM_TYPE_INTERVAL && md_N > 0) { + wl = ((unsigned int)prog.days[1] - 1 < md_N) ? md_scales[prog.days[1] - 1] : md_scales[md_N - 1]; + } + return wl; +} + +float get_program_sensor_adj(uint8_t pid) { + SensorAdjustment *adj = SensorAdjustment::read(pid, pd.nprograms); + if (adj) return adj->get_adjustment_factor(os.sensors); + return 1.f; +} + +void manual_start_program(unsigned char pid, unsigned char uwt, unsigned char qo, unsigned char usa) { boolean match_found = false; ProgramStruct prog; - ulong dur; + uint32_t dur; + float sensor_adj = 1.f; unsigned char sid, bid, s; unsigned char ns = os.nstations; unsigned char order[ns]; @@ -1696,8 +1624,9 @@ void manual_start_program(unsigned char pid, unsigned char uwt, unsigned char qo unsigned char wl = 100; if ((pid>0)&&(pid<255)) { pd.read(pid-1, &prog); - if(uwt) wl = os.iopts[IOPT_WATER_PERCENTAGE]; - notif.add(NOTIFY_PROGRAM_SCHED, pid-1, wl, 1); + if(usa) sensor_adj = get_program_sensor_adj(pid-1); + if(uwt) wl = get_program_water_percent(prog); + notif.add(NOTIFY_PROGRAM_SCHED, pid-1, wl, 1, sensor_adj); // get station ordering from program name prog.gen_station_runorder(1, order); } @@ -1707,13 +1636,17 @@ void manual_start_program(unsigned char pid, unsigned char uwt, unsigned char qo bid=sid>>3; s=sid&0x07; // skip if the station is a master station (because master cannot be scheduled independently - if ((os.status.mas==sid+1) || (os.status.mas2==sid+1)) + if ((os.status.mas==sid+1) || (os.status.mas2==sid+1) || + (os.status.mas3==sid+1) || (os.status.mas4==sid+1)) continue; dur = 60; - if(pid==255) dur=2; - else if(pid>0) + if(pid==255) { + dur=2; + } else if (pid>0) { dur = water_time_resolve(prog.durations[sid]); - dur = dur * wl / 100; + } + + dur = (uint32_t)(dur * wl / 100 * sensor_adj); if(dur>0 && !(os.attrib_dis[bid]&(1<= limit) { + if (!delete_log_oldest()) break; } file = LittleFS.open(tmp_buffer, "w"); if(!file) return; } file.seek(0, SeekEnd); - #else - sd.chdir("/"); - if (sd.chdir(LOG_PREFIX) == false) { - // create dir if it doesn't exist yet - if (sd.mkdir(LOG_PREFIX) == false) { - return; - } - } - SdFile file; - int ret = file.open(tmp_buffer, O_CREAT | O_WRITE ); - file.seekEnd(); - if(!ret) { - return; - } - #endif - #else // prepare log folder for RPI/LINUX struct stat st; - if(stat(get_filename_fullpath(LOG_PREFIX), &st)) { - if(mkdir(get_filename_fullpath(LOG_PREFIX), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IWOTH | S_IXOTH)) { + if(stat(get_filename_fullpath(LOG_DIR), &st)) { + if(mkdir(get_filename_fullpath(LOG_DIR), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IWOTH | S_IXOTH)) { return; } } @@ -1840,16 +1743,16 @@ void write_log(unsigned char type, time_os_t curr_time) { strcat_P(tmp_buffer, PSTR(",")); // duration is unsigned integer size = strlen(tmp_buffer); - snprintf(tmp_buffer + size, TMP_BUFFER_SIZE - size , "%lu", (ulong)pd.lastrun.duration); + snprintf(tmp_buffer + size, TMP_BUFFER_SIZE - size , "%" PRIu32, (uint32_t)pd.lastrun.duration); } else { - ulong lvalue=0; + uint32_t lvalue=0; if(type==LOGDATA_FLOWSENSE) { lvalue = (flow_count>os.flowcount_log_start)?(flow_count-os.flowcount_log_start):0; } size_t size = strlen(tmp_buffer); - snprintf(tmp_buffer + size, TMP_BUFFER_SIZE - size , "%lu", lvalue); + snprintf(tmp_buffer + size, TMP_BUFFER_SIZE - size , "%" PRIu32, lvalue); strcat_P(tmp_buffer, PSTR(",\"")); strcat_P(tmp_buffer, log_type_names+type*3); strcat_P(tmp_buffer, PSTR("\",")); @@ -1872,15 +1775,15 @@ void write_log(unsigned char type, time_os_t curr_time) { break; } size = strlen(tmp_buffer); - snprintf(tmp_buffer + size, TMP_BUFFER_SIZE - size , "%lu", lvalue); + snprintf(tmp_buffer + size, TMP_BUFFER_SIZE - size , "%" PRIu32, lvalue); } strcat_P(tmp_buffer, PSTR(",")); size_t size = strlen(tmp_buffer); - snprintf(tmp_buffer + size, TMP_BUFFER_SIZE - size , "%lu", curr_time); + snprintf(tmp_buffer + size, TMP_BUFFER_SIZE - size , "%" PRIu32, (uint32_t)curr_time); if((os.iopts[IOPT_SENSOR1_TYPE]==SENSOR_TYPE_FLOW) && (type==LOGDATA_STATION)) { // RAH implementation of flow sensor strcat_P(tmp_buffer, PSTR(",")); - #if defined(ARDUINO) + #if defined(ESP8266) dtostrf(flow_last_gpm,5,2,tmp_buffer+strlen(tmp_buffer)); #else snprintf(tmp_buffer+strlen(tmp_buffer), TMP_BUFFER_SIZE, "%5.2f", flow_last_gpm); @@ -1888,12 +1791,8 @@ void write_log(unsigned char type, time_os_t curr_time) { } strcat_P(tmp_buffer, PSTR("]\r\n")); -#if defined(ARDUINO) - #if defined(ESP8266) +#if defined(ESP8266) file.write((const uint8_t*)tmp_buffer, strlen(tmp_buffer)); - #else - file.write(tmp_buffer); - #endif file.close(); #else fwrite(tmp_buffer, 1, strlen(tmp_buffer), file); @@ -1902,25 +1801,35 @@ void write_log(unsigned char type, time_os_t curr_time) { } #if defined(ESP8266) +uint32_t get_sprinkler_log_size() { + Dir dir = LittleFS.openDir(LOG_DIR); + uint32_t total = 0; + while (dir.next()) { + if (dir.fileName().endsWith(".txt")) + total += dir.fileSize(); + } + return total; +} + bool delete_log_oldest() { - Dir dir = LittleFS.openDir(LOG_PREFIX); - time_os_t oldest_t = ULONG_MAX; + Dir dir = LittleFS.openDir(LOG_DIR); + uint32_t oldest_day = UINT32_MAX; String oldest_fn; while (dir.next()) { - time_os_t t = dir.fileCreationTime(); - if(t0) { + if (oldest_fn.length() > 0) { DEBUG_PRINT(F("deleting ")) - DEBUG_PRINTLN(LOG_PREFIX+oldest_fn); - LittleFS.remove(LOG_PREFIX+oldest_fn); + DEBUG_PRINTLN(LOG_DIR + oldest_fn); + LittleFS.remove(LOG_DIR + oldest_fn); return true; - } else { - return false; } + return false; } #endif @@ -1929,14 +1838,13 @@ bool delete_log_oldest() { */ void delete_log(char *name) { if (!os.iopts[IOPT_ENABLE_LOGGING]) return; -#if defined(ARDUINO) - - #if defined(ESP8266) +#if defined(ESP8266) if (strncmp(name, "all", 3) == 0) { - // delete all log files - Dir dir = LittleFS.openDir(LOG_PREFIX); + // delete all sprinkler log files (.txt), leaving sensor logs intact + Dir dir = LittleFS.openDir(LOG_DIR); while (dir.next()) { - LittleFS.remove(LOG_PREFIX+dir.fileName()); + if (dir.fileName().endsWith(".txt")) + LittleFS.remove(LOG_DIR+dir.fileName()); } } else { // delete a single log file @@ -1944,28 +1852,23 @@ void delete_log(char *name) { if(!LittleFS.exists(tmp_buffer)) return; LittleFS.remove(tmp_buffer); } - #else - if (strncmp(name, "all", 3) == 0) { - // delete the log folder - SdFile file; - - if (sd.chdir(LOG_PREFIX)) { - // delete the whole log folder - sd.vwd()->rmRfStar(); - } - } else { - // delete a single log file - make_logfile_name(name); - if (!sd.exists(tmp_buffer)) return; - sd.remove(tmp_buffer); - } - #endif - #else // delete_log implementation for RPI/LINUX if (strncmp(name, "all", 3) == 0) { - // delete the log folder - rmdir(get_filename_fullpath(LOG_PREFIX)); - return; + // delete all sprinkler log files (.txt), leaving sensor logs intact + char log_dir[PATH_MAX]; + strcpy(log_dir, get_filename_fullpath(LOG_DIR)); + DIR *d = opendir(log_dir); + if (d) { + struct dirent *ent; + while ((ent = readdir(d)) != NULL) { + size_t len = strlen(ent->d_name); + if (len > 4 && strcmp(ent->d_name + len - 4, ".txt") == 0) { + snprintf(tmp_buffer, TMP_BUFFER_SIZE, "%s%s", log_dir, ent->d_name); + remove(tmp_buffer); + } + } + closedir(d); + } } else { make_logfile_name(name); remove(get_filename_fullpath(tmp_buffer)); @@ -1979,57 +1882,13 @@ void delete_log(char *name) { * If not, it re-initializes Ethernet controller. */ static void check_network() { -#if defined(OS_AVR) - // do not perform network checking if the controller has just started, or if a program is running - if (os.status.program_busy) {return;} - - // check network condition periodically - if (os.status.req_network) { - os.status.req_network = 0; - // change LCD icon to indicate it's checking network - if (!ui_state) { - os.lcd.setCursor(LCD_CURSOR_NETWORK, 1); - os.lcd.write('>'); - } - - - boolean failed = false; - // todo: ping gateway ip - /*ether.clientIcmpRequest(ether.gwip); - ulong start = millis(); - // wait at most PING_TIMEOUT milliseconds for ping result - do { - ether.packetLoop(ether.packetReceive()); - if (ether.packetLoopIcmpCheckReply(ether.gwip)) { - failed = false; - break; - } - } while((long)(millis() - start) < PING_TIMEOUT);*/ - if (failed) { - if(os.status.network_fails<3) os.status.network_fails++; - // clamp it to 6 - //if (os.status.network_fails > 6) os.status.network_fails = 6; - } - else os.status.network_fails=0; - // if failed more than 3 times, restart - if (os.status.network_fails==3) { - // mark for safe restart - os.nvdata.reboot_cause = REBOOT_CAUSE_NETWORK_FAIL; - os.status.safe_reboot = 1; - } else if (os.status.network_fails>2) { - // if failed more than twice, try to reconnect - if (os.start_network()) - os.status.network_fails=0; - } - } -#else + // TODO: // nothing to do for other platforms -#endif } /** Perform NTP sync */ static void perform_ntp_sync() { -#if defined(ARDUINO) +#if defined(ESP8266) // do not perform ntp if this option is disabled, or if a program is currently running if (!os.iopts[IOPT_USE_NTP] || os.status.program_busy) return; // do not perform ntp if network is not connected @@ -2041,8 +1900,8 @@ static void perform_ntp_sync() { os.lcd_print_line_clear_pgm(PSTR("NTP Syncing..."),1); } DEBUG_PRINTLN(F("NTP Syncing...")); - static ulong last_ntp_result = 0; - ulong t = getNtpTime(); + static uint32_t last_ntp_result = 0; + uint32_t t = getNtpTime(); if(last_ntp_result>3 && t>last_ntp_result-3 && t. */ -#if defined(ARDUINO) +#if defined(ESP8266) #include - #if defined(ESP8266) - #include - #else - #include - #endif + #include #define MQTT_SOCKET_TIMEOUT 5 #include @@ -52,17 +48,17 @@ // Debug routines to help identify any blocking of the event loop for an extended period #if defined(ENABLE_DEBUG) - #if defined(ARDUINO) + #if defined(ESP8266) #include "TimeLib.h" #define DEBUG_TIMESTAMP(msg, ...) {time_os_t t = os.now_tz(); Serial.printf("%02d-%02d-%02d %02d:%02d:%02d - ", year(t), month(t), day(t), hour(t), minute(t), second(t));} #else #include - #define DEBUG_TIMESTAMP() {char tstr[21]; time_os_t t = time(NULL); struct tm *tm = localtime(&t); strftime(tstr, 21, "%y-%m-%d %H:%M:%S - ", tm);printf("%s", tstr);} + #define DEBUG_TIMESTAMP() {char tstr[21]; time_t t = time(NULL); struct tm *tm = localtime(&t); strftime(tstr, 21, "%y-%m-%d %H:%M:%S - ", tm);printf("%s", tstr);} #endif #define DEBUG_LOGF(msg, ...) {DEBUG_TIMESTAMP(); DEBUG_PRINTF(msg, ##__VA_ARGS__);} - static unsigned long _lastMillis = 0; // Holds the timestamp associated with the last call to DEBUG_DURATION() - inline unsigned long DEBUG_DURATION() {unsigned long dur = millis() - _lastMillis; _lastMillis = millis(); return dur;} + static uint32_t _lastMillis = 0; // Holds the timestamp associated with the last call to DEBUG_DURATION() + inline uint32_t DEBUG_DURATION() {uint32_t dur = millis() - _lastMillis; _lastMillis = millis(); return dur;} #else #define DEBUG_LOGF(msg, ...) {} #define DEBUG_DURATION() {} @@ -150,7 +146,7 @@ void changeValues(char *message){ if(findKeyVal(message, tmp_buffer, TMP_BUFFER_SIZE, PSTR("rd"), true)){ int rd = atoi(tmp_buffer); if(rd>0){ - os.nvdata.rd_stop_time = os.now_tz() + (unsigned long) rd * 3600; + os.nvdata.rd_stop_time = os.now_tz() + (uint32_t) rd * 3600; os.raindelay_start(); }else if (rd==0){ os.raindelay_stop(); @@ -181,7 +177,7 @@ void manualRun(char *message){ } uint16_t timer = 0; - unsigned long curr_time = os.now_tz(); + uint32_t curr_time = os.now_tz(); if(en){ if(findKeyVal(message, tmp_buffer, TMP_BUFFER_SIZE, PSTR("t"), true)){ timer = (uint16_t)atol(tmp_buffer); @@ -229,7 +225,7 @@ void manualRun(char *message){ } //handles /mp command -void manual_start_program(unsigned char, unsigned char, unsigned char); +void manual_start_program(unsigned char, unsigned char, unsigned char, unsigned char usa=0); void programStart(char *message){ if(!findKeyVal(message, tmp_buffer, TMP_BUFFER_SIZE, PSTR("pid"), true)){ DEBUG_LOGF("Program ID missing.\r\n") @@ -442,7 +438,7 @@ void OSMqtt::subscribe(void){ // Regularly call the loop function to ensure "keep alive" messages are sent to the broker and to reconnect if needed. void OSMqtt::loop(void) { - static unsigned long last_reconnect_attempt = 0; + static uint32_t last_reconnect_attempt = 0; if (mqtt_client == NULL || !_enabled || os.status.network_fails > 0) return; @@ -480,19 +476,20 @@ void OSMqtt::loop(void) { #endif } -/**************************** ARDUINO ********************************************/ -#if defined(ARDUINO) - - #if defined(ESP8266) - WiFiClient wifiClient; - #else - EthernetClient ethClient; - #endif +/**************************** ESP8266 ********************************************/ +#if defined(ESP8266) +WiFiClient wifiClient; int OSMqtt::_init(void) { Client * client = NULL; - if (mqtt_client) { delete mqtt_client; mqtt_client = 0; } + if (mqtt_client) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" + delete mqtt_client; +#pragma GCC diagnostic pop + mqtt_client = 0; + } #if defined(ESP8266) client = &wifiClient; diff --git a/mqtt.h b/mqtt.h index 65c2fd0a3..463a19d23 100644 --- a/mqtt.h +++ b/mqtt.h @@ -21,40 +21,38 @@ * . */ -#ifndef _MQTT_H -#define _MQTT_H +#pragma once class OSMqtt { private: - static char _id[]; - static char _host[]; - static int _port; - static char _username[]; - static char _password[]; - static bool _enabled; - static char _pub_topic[]; - static char _sub_topic[]; - static bool _done_subscribed; + static char _id[]; + static char _host[]; + static int _port; + static char _username[]; + static char _password[]; + static bool _enabled; + static char _pub_topic[]; + static char _sub_topic[]; + static bool _done_subscribed; - // Following routines are platform specific versions of the public interface - static int _init(void); - static int _connect(void); - static int _disconnect(void); - static bool _connected(void); - static int _publish(const char *topic, const char *payload); - static int _subscribe(void); - static int _loop(void); - static const char * _state_string(int state); - public: - static void init(void); - static void init(const char * id); - static void begin(void); - static bool enabled(void) { return _enabled; }; - static void publish(const char *topic, const char *payload); - static void subscribe(); - static void loop(void); - static char* get_pub_topic() { return _pub_topic; } - static char* get_sub_topic() { return _sub_topic; } + // Following routines are platform specific versions of the public interface + static int _init(void); + static int _connect(void); + static int _disconnect(void); + static bool _connected(void); + static int _publish(const char *topic, const char *payload); + static int _subscribe(void); + static int _loop(void); + static const char * _state_string(int state); + public: + static void init(void); + static void init(const char * id); + static void begin(void); + static bool enabled(void) { return _enabled; }; + static void publish(const char *topic, const char *payload); + static void subscribe(); + static void loop(void); + static char* get_pub_topic() { return _pub_topic; } + static char* get_sub_topic() { return _sub_topic; } }; -#endif // _MQTT_H diff --git a/notifier.cpp b/notifier.cpp index 7fefbb751..3c5bea6d5 100644 --- a/notifier.cpp +++ b/notifier.cpp @@ -26,9 +26,10 @@ #include "ArduinoJson.hpp" #include "opensprinkler_server.h" -NotifNodeStruct* NotifQueue::head = NULL; -NotifNodeStruct* NotifQueue::tail = NULL; -unsigned char NotifQueue::nqueue = 0; +uint8_t NotifQueue::nqueue = 0; +uint8_t NotifQueue::head = 0; +uint8_t NotifQueue::tail = 0; +NotifNodeStruct NotifQueue::queue[NOTIF_QUEUE_MAXSIZE]; extern OpenSprinkler os; extern ProgramData pd; @@ -58,20 +59,15 @@ void ip2string(char* str, size_t str_len, unsigned char ip[4]) { snprintf_P(str+strlen(str), str_len, PSTR("%d.%d.%d.%d"), ip[0], ip[1], ip[2], ip[3]); } -bool NotifQueue::add(uint16_t t, uint32_t l, float f, uint8_t b) { +bool NotifQueue::add(uint16_t t, uint32_t l, float f, uint8_t b, float f2) { if (!is_notif_enabled(t)) { // if not subscribed to this type, return return false; } if(nqueuenext = node; - } - tail = node; + queue[tail] = NotifNodeStruct(t, l, f, b, f2); + tail = (tail + 1 )% NOTIF_QUEUE_MAXSIZE; nqueue++; - DEBUG_PRINTF("NotifQueue::add (type %d) [%d]\n", t, nqueue); + DEBUG_PRINTF("NotifQueue::add (type %d) [%d|%d|%d]\n", t, nqueue, head, tail); return true; } DEBUG_PRINTLN(F("NotifQueue::add queue is full!")); @@ -79,33 +75,23 @@ bool NotifQueue::add(uint16_t t, uint32_t l, float f, uint8_t b) { } void NotifQueue::clear() { - while(nqueue!=0) { - NotifNodeStruct* node = head; - head = head->next; - if(head==NULL) { - tail = NULL; - } - delete node; - nqueue--; - } + nqueue = 0; + head = 0; + tail = 0; } -void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval); +void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval, float fval2=0.f); bool NotifQueue::run(int n) { if(nqueue == 0) return false; // queue is empty if(n<=0 || n>nqueue) n=nqueue; while(nqueue!=0 && n!=0) { - NotifNodeStruct* node = head; - head = head->next; - if(head==NULL) { - tail = NULL; - } - push_message(node->type, node->lval, node->fval, node->bval); - DEBUG_PRINTF("NotifQueue::run (type %d) [%d]\n", node->type, nqueue); - delete node; + NotifNodeStruct* node = &queue[head]; + head = (head + 1) % NOTIF_QUEUE_MAXSIZE; + push_message(node->type, node->lval, node->fval, node->bval, node->fval2); nqueue--; n--; + DEBUG_PRINTF("NotifQueue::run (type %d) [%d|%d|%d]\n", node->type, nqueue, head, tail); } return true; } @@ -113,7 +99,7 @@ bool NotifQueue::run(int n) { #define PUSH_TOPIC_LEN 120 #define PUSH_PAYLOAD_LEN TMP_BUFFER_SIZE -void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { +void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval, float fval2) { if (!is_notif_enabled(type)) { return; } @@ -130,8 +116,6 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { #define DEFAULT_EMAIL_PORT 465 // parse email variables - #if defined(SUPPORT_EMAIL) - // define email variables ArduinoJson::JsonDocument doc; // make sure this has the same scope of email_x variables to prevent use after free const char *email_host = NULL; const char *email_username = NULL; @@ -163,7 +147,6 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { email_recipient= doc["recipient"]; } } - #endif #if defined(ESP8266) EMailSender::EMailMessage email_message; @@ -175,13 +158,11 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { #endif bool email_enabled = false; -#if defined(SUPPORT_EMAIL) - if(!email_en){ // todo: this should be simplified + if(!email_en){ email_enabled = false; }else{ email_enabled = true; } -#endif // if none if enabled, return here if ((!ifttt_enabled) && (!email_enabled) && (!os.mqtt.enabled())) @@ -193,7 +174,7 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { topic[PUSH_TOPIC_LEN]=0; strcat(postval+strlen(postval), topic); strcat_P(postval, PSTR("], ")); - if(email_enabled) { + if(email_enabled) { strcat(topic, " "); email_message.subject = topic; // prefix the email subject with device name } @@ -236,11 +217,7 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { snprintf_P(payload+strlen(payload), PUSH_PAYLOAD_LEN, PSTR(",\"duration\":%d"), (int)fval); if (os.iopts[IOPT_SENSOR1_TYPE]==SENSOR_TYPE_FLOW) { float gpm = flow_last_gpm * flowrate100 / 100.f; - #if defined(OS_AVR) - snprintf_P(payload+strlen(payload), PUSH_PAYLOAD_LEN, PSTR(",\"flow\":%d.%02d"), (int)gpm, (int)(gpm*100)%100); - #else snprintf_P(payload+strlen(payload), PUSH_PAYLOAD_LEN, PSTR(",\"flow\":%.2f"), gpm); - #endif } } strcat_P(payload, PSTR("}")); @@ -256,11 +233,7 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { if(os.iopts[IOPT_SENSOR1_TYPE]==SENSOR_TYPE_FLOW) { float gpm = flow_last_gpm * flowrate100 / 100.f; - #if defined(OS_AVR) - snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR(" Flow rate: %d.%02d"), (int)gpm, (int)(gpm*100)%100); - #else snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR(" Flow rate: %.2f"), gpm); - #endif } if(email_enabled) { email_message.subject += PSTR("station event"); } } @@ -291,7 +264,7 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { char *endptr; flow_gpm_alert_setpoint = strtod(station_name_last_five_chars, &endptr); if (endptr != station_name_last_five_chars) { - //station_name_last_five_chars was successfully converted to a number + //station_name_last_five_chars was successfully converted to a number //flow_last_gpm is actually collected and stored as pulses per minute, not gallons per minute // Alert Check - Compare flow_gpm_alert_setpoint with flow_last_gpm and enable flow_alert_flag if flow is above setpoint if ((flow_last_gpm*flowrate100/100.f) > flow_gpm_alert_setpoint) { @@ -313,12 +286,7 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { //Format mqtt message snprintf_P(topic, PUSH_TOPIC_LEN, PSTR("station/%d/alert/flow"), lval); float gpm = flow_last_gpm * flowrate100 / 100.f; - #if defined(OS_AVR) - snprintf_P(payload, PUSH_PAYLOAD_LEN, PSTR("{\"flow_rate\":%d.%02d,\"duration\":%d,\"alert_setpoint\":%d.%02d}"), (int)gpm, (int)(gpm*100)%100, - (int)fval, (int)flow_gpm_alert_setpoint, (int)(flow_gpm_alert_setpoint*100)%100); - #else snprintf_P(payload, PUSH_PAYLOAD_LEN, PSTR("{\"flow_rate\":%.2f,\"duration\":%d,\"alert_setpoint\":%.4f}"), gpm, (int)fval, flow_gpm_alert_setpoint); - #endif } @@ -328,13 +296,14 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { // Get and format current local time as "YYYY-MM-DD hh:mm:ss AM/PM" strcat_P(postval, PSTR("at ")); time_os_t curr_time = os.now_tz(); - #if defined(ARDUINO) + #if defined(ESP8266) tmElements_t tm; breakTime(curr_time, tm); snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR("%04d-%02d-%02d %02d:%02d:%02d"), 1970+tm.Year, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second); #else - struct tm *ti = gmtime(&curr_time); + time_t _ct = curr_time; + struct tm *ti = gmtime(&_ct); snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR("%04d-%02d-%02d %02d:%02d:%02d"), ti->tm_year+1900, ti->tm_mon+1, ti->tm_mday, ti->tm_hour, ti->tm_min, ti->tm_sec); #endif @@ -351,19 +320,14 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { strcat_P(postval, PSTR(" FLOW ALERT!")); float gpm = flow_last_gpm * flowrate100 / 100.f; - #if defined(OS_AVR) - snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR(" | Flow rate: %d.%02d > Flow alert setpoint: %d.%02d"), - (int)gpm, (int)(gpm*100)%100, (int)flow_gpm_alert_setpoint, (int)(flow_gpm_alert_setpoint*100)%100); - #else snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR(" | Flow rate: %.2f > Flow alert setpoint: %.4f"), gpm, flow_gpm_alert_setpoint); - #endif if(email_enabled) { email_message.subject += PSTR("- FLOW ALERT"); } } } else { - //Do not send an alert. Flow was not above setpoint or setpoint not valid. + //Do not send an alert. Flow was not above setpoint or setpoint not valid. //Must force ifftt_enabled and email_enabled to false to prevent sending //Can not force os.mqtt.enabled() off, but it will not publish an mqtt message as topic\payload will be empty. ifttt_enabled=false; @@ -371,7 +335,7 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { } break; } - + case NOTIFY_PROGRAM_SCHED: if (os.mqtt.enabled()) { snprintf_P(topic, PUSH_TOPIC_LEN, PSTR("program/%d"), lval); @@ -379,8 +343,8 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { strcat_P(payload, PSTR("{\"state\":\"skipped\",\"wtrestr\":")); snprintf_P(payload+strlen(payload), PUSH_PAYLOAD_LEN, PSTR("%d"), (int)bval); // if a program is skipped, also output the wt_restricted variable } else { - strcat_P(payload, PSTR("{\"state\":1,\"wl\":")); - snprintf_P(payload+strlen(payload), PUSH_PAYLOAD_LEN, PSTR("%d"), (int)fval); + snprintf_P(payload+strlen(payload), PUSH_PAYLOAD_LEN, PSTR("{\"state\":1,\"wl\":%d,\"wa\":%.4g,\"sa\":%.4g,\"ta\":%.4g"), + (int)fval, fval/100.f, fval2, fval/100.f*fval2); } strcat_P(payload, PSTR("}")); } @@ -400,7 +364,7 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { if(lval0) { - snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR(" with %d%% water level."), (int)fval); + snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR(". Adjustments: Weather->%d%%, Sensor->%.2f%%, Total->%.2f%%."), (int)fval, fval2*100.f, fval*fval2); } if(email_enabled) { email_message.subject += PSTR("program event"); } @@ -451,18 +415,10 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { float vol = lval*flowrate100/100.f; if (os.mqtt.enabled()) { strcpy_P(topic, PSTR("sensor/flow")); - #if defined(OS_AVR) - snprintf_P(payload, PUSH_PAYLOAD_LEN, PSTR("{\"count\":%d,\"volume\":%d.%02d}"), (int)lval, (int)vol, (int)(vol*100)%100); - #else snprintf_P(payload, PUSH_PAYLOAD_LEN, PSTR("{\"count\":%d,\"volume\":%.2f}"), (int)lval, vol); - #endif } if (ifttt_enabled || email_enabled) { - #if defined(OS_AVR) - snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR("Flow count: %d, volume: %d.%02d"), (int)lval, (int)vol, (int)(vol*100)%100); - #else snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR("Flow count: %d, volume: %.2f"), (int)lval, vol); - #endif if(email_enabled) { email_message.subject += PSTR("flow sensor event"); } } } @@ -496,13 +452,14 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { // Get and format current local time as "YYYY-MM-DD hh:mm:ss AM/PM" strcat_P(postval, PSTR("at ")); time_os_t curr_time = os.now_tz(); - #if defined(ARDUINO) + #if defined(ESP8266) tmElements_t tm; breakTime(curr_time, tm); snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR("%04d-%02d-%02d %02d:%02d:%02d"), 1970+tm.Year, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second); #else - struct tm *ti = gmtime(&curr_time); + time_t _ct = curr_time; + struct tm *ti = gmtime(&_ct); snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR("%04d-%02d-%02d %02d:%02d:%02d"), ti->tm_year+1900, ti->tm_mon+1, ti->tm_mday, ti->tm_hour, ti->tm_min, ti->tm_sec); #endif @@ -565,9 +522,8 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { snprintf_P(payload, PUSH_PAYLOAD_LEN, PSTR("{\"state\":\"started\",\"cause\":%d}"), (int)os.last_reboot_cause); } if (ifttt_enabled || email_enabled) { - #if defined(ARDUINO) + #if defined(ESP8266) snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR("rebooted. Cause: %d. Device IP: "), os.last_reboot_cause); - #if defined(ESP8266) { IPAddress _ip; if (useEth) { @@ -579,9 +535,6 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { unsigned char ip[4] = {_ip[0], _ip[1], _ip[2], _ip[3]}; ip2string(postval, TMP_BUFFER_SIZE, ip); } - #else - ip2string(postval, TMP_BUFFER_SIZE, &(Ethernet.localIP()[0])); - #endif #else strcat_P(postval, PSTR("controller process restarted.")); #endif @@ -610,22 +563,40 @@ void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval) { if(email_enabled){ email_message.message = strchr(postval, 'O'); // ad-hoc: remove the value1 part from the ifttt message - #if defined(ARDUINO) - #if defined(ESP8266) - if(email_host && email_username && email_password && email_recipient) { // make sure all are valid - EMailSender emailSend(email_username, email_password); - emailSend.setSMTPServer(email_host); - emailSend.setSMTPPort(email_port); - EMailSender::Response resp = emailSend.send(email_recipient, email_message); + #if defined(ESP8266) + if(email_host && email_username && email_password && email_recipient) { // make sure all are valid + EMailSender emailSend(email_username, email_password); + emailSend.setSMTPServer(email_host); + emailSend.setSMTPPort(email_port); + if (email_username == NULL || strlen(email_username) == 0) { + emailSend.setUseAuth(false); } - #endif + EMailSender::Response resp = emailSend.send(email_recipient, email_message); + } #else struct smtp *smtp = NULL; + enum smtp_connection_security security_flag; + if (email_port == 25) { + security_flag = SMTP_SECURITY_NONE; + } else if (email_port == 587) { + security_flag = SMTP_SECURITY_STARTTLS; + } else { + // Default to Implicit SSL for 465 (or legacy configurations) + security_flag = SMTP_SECURITY_TLS; + } + enum smtp_authentication_method auth_flag; + // Check if the user left the SMTP Username field blank in the UI + if (email_username == NULL || strlen(email_username) == 0) { + auth_flag = SMTP_AUTH_NONE; + } else { + // Retain the OpenSprinkler default for populated credentials + auth_flag = SMTP_AUTH_PLAIN; + } String email_port_str = to_string(email_port); smtp_status_code rc; if(email_host && email_username && email_password && email_recipient) { // make sure all are valid - rc = smtp_open(email_host, email_port_str.c_str(), SMTP_SECURITY_TLS, SMTP_NO_CERT_VERIFY, NULL, &smtp); - rc = smtp_auth(smtp, SMTP_AUTH_PLAIN, email_username, email_password); + rc = smtp_open(email_host, email_port_str.c_str(), security_flag, SMTP_NO_CERT_VERIFY, NULL, &smtp); + rc = smtp_auth(smtp, auth_flag, email_username, email_password); rc = smtp_address_add(smtp, SMTP_ADDRESS_FROM, email_username, "OpenSprinkler"); rc = smtp_address_add(smtp, SMTP_ADDRESS_TO, email_recipient, "User"); rc = smtp_header_add(smtp, "Subject", email_message.subject.c_str()); diff --git a/notifier.h b/notifier.h index 2572bd846..93c5fcb45 100644 --- a/notifier.h +++ b/notifier.h @@ -21,11 +21,9 @@ * . */ +#pragma once -#ifndef _NOTIFIER_H -#define _NOTIFIER_H - -#define NOTIF_QUEUE_MAXSIZE 32 +#define NOTIF_QUEUE_MAXSIZE 16 #include "OpenSprinkler.h" #include "types.h" @@ -35,9 +33,9 @@ struct NotifNodeStruct { uint16_t type; uint32_t lval; float fval; + float fval2; uint8_t bval; - NotifNodeStruct *next; - NotifNodeStruct(uint16_t t, uint32_t l=0, float f=0.f, uint8_t b=0) : type(t), lval(l), fval(f), bval(b), next(NULL) + NotifNodeStruct(uint16_t t=0, uint32_t l=0, float f=0.f, uint8_t b=0, float f2=0.f) : type(t), lval(l), fval(f), fval2(f2), bval(b) { } }; @@ -45,15 +43,12 @@ struct NotifNodeStruct { class NotifQueue { public: // Insert a new notification element - static bool add(uint16_t t, uint32_t l=0, float f=0.f, uint8_t b=0); + static bool add(uint16_t t, uint32_t l=0, float f=0.f, uint8_t b=0, float f2=0.f); // Clear all elements (i.e. empty the queue) static void clear(); // Run/Process elements. By default process 1 at a time. If n<=0, process all. static bool run(int n=1); protected: - static NotifNodeStruct* head; - static NotifNodeStruct* tail; - static unsigned char nqueue; + static NotifNodeStruct queue[NOTIF_QUEUE_MAXSIZE]; + static uint8_t head, tail, nqueue; }; - -#endif // _NOTIFIER_H \ No newline at end of file diff --git a/opensprinkler_server.cpp b/opensprinkler_server.cpp index 0314337e6..f67395a17 100644 --- a/opensprinkler_server.cpp +++ b/opensprinkler_server.cpp @@ -21,64 +21,54 @@ * . */ +#include "opensprinkler_server.h" #include "types.h" #include "OpenSprinkler.h" #include "program.h" -#include "opensprinkler_server.h" +#include "bfiller.h" #include "weather.h" #include "mqtt.h" #include "main.h" // External variables defined in main ion file -#if defined(USE_OTF) - extern OTF::OpenThingsFramework *otf; - #define OTF_PARAMS_DEF const OTF::Request &req,OTF::Response &res - #define OTF_PARAMS req,res - #define FKV_SOURCE req - #define handle_return(x) {if(x==HTML_OK) res.writeBodyData(ether_buffer, strlen(ether_buffer)); else otf_send_result(req,res,x); return;} -#else - extern EthernetClient *m_client; - #define OTF_PARAMS_DEF - #define OTF_PARAMS - #define FKV_SOURCE p - #define handle_return(x) {return_code=x; return;} -#endif +extern OTF::OpenThingsFramework *otf; +#define OTF_PARAMS_DEF const OTF::Request &req,OTF::Response &res +#define OTF_PARAMS req,res +#define FKV_SOURCE req +#define handle_return(x) {if(x!=HTML_OK) otf_send_result(req,res,x); return;} -#if defined(ARDUINO) - #if defined(ESP8266) - #include - #include - #include "espconnect.h" - extern ESP8266WebServer *update_server; - extern ENC28J60lwIP enc28j60; - extern Wiznet5500lwIP w5500; - extern lwipEth eth; - #else - #include "SdFat.h" - extern SdFat sd; - #endif +#if defined(ESP8266) + #include + #include + #include "espconnect.h" + extern ESP8266WebServer *update_server; + extern ENC28J60lwIP enc28j60; + extern Wiznet5500lwIP w5500; + extern lwipEth eth; #else #include #include #include "etherport.h" #endif -extern char ether_buffer[]; extern char tmp_buffer[]; +extern char ether_buffer[]; extern OpenSprinkler os; extern ProgramData pd; -extern ulong flow_count; - -#if !defined(USE_OTF) -static unsigned char return_code; -static char* get_buffer = NULL; -#endif +extern uint32_t flow_count; +static OTF::Response *current_res = nullptr; BufferFiller bfill; -/* Check available space (number of bytes) in the Ethernet buffer */ -int available_ether_buffer() { - return ETHER_BUFFER_SIZE - (int)bfill.position(); +static void bfill_flush(const char *buf, size_t len) { + if (current_res && len > 0) + current_res->writeBodyData(buf, len); +} + +void begin_response(OTF::Response &res) { + current_res = &res; + bfill = BufferFiller(ether_buffer, ETHER_BUFFER_SIZE); + bfill.set_flush(bfill_flush); } // Define return error code @@ -93,32 +83,9 @@ int available_ether_buffer() { #define HTML_PAGE_NOT_FOUND 0x20 #define HTML_NOT_PERMITTED 0x30 #define HTML_UPLOAD_FAILED 0x40 +#define HTML_INTERNAL_ERROR 0x50 #define HTML_REDIRECT_HOME 0xFF -#if !defined(USE_OTF) -static const char html200OK[] PROGMEM = - "HTTP/1.1 200 OK\r\n" -; - -static const char htmlNoCache[] PROGMEM = - "Cache-Control: max-age=0, no-cache, no-store, must-revalidate\r\n" -; - -static const char htmlContentJSON[] PROGMEM = - "Content-Type: application/json\r\n" - "Connection: close\r\n" -; - -static const char htmlContentHTML[] PROGMEM = - "Content-Type: text/html\r\n" - "Connection: close\r\n" -; - -static const char htmlAccessControl[] PROGMEM = - "Access-Control-Allow-Origin: *\r\n" -; -#endif - static const char htmlMobileHeader[] PROGMEM = "" ; @@ -127,9 +94,8 @@ static const char htmlReturnHome[] PROGMEM = "\n" ; -#if defined(USE_OTF) unsigned char findKeyVal (const OTF::Request &req,char *strbuf, uint16_t maxlen,const char *key,bool key_in_pgm=false,uint8_t *keyfound=NULL) { -#if defined(ARDUINO) +#if defined(ESP8266) char* result = key_in_pgm ? req.getQueryParameter((const __FlashStringHelper *)key) : req.getQueryParameter(key); #else char* result = req.getQueryParameter(key); @@ -144,7 +110,7 @@ unsigned char findKeyVal (const OTF::Request &req,char *strbuf, uint16_t maxlen, } return 0; } -#endif + unsigned char findKeyVal (const char *str,char *strbuf, uint16_t maxlen,const char *key,bool key_in_pgm=false,uint8_t *keyfound=NULL) { uint8_t found=0; uint16_t i=0; @@ -205,29 +171,17 @@ unsigned char findKeyVal (const char *str,char *strbuf, uint16_t maxlen,const ch return(i); } -void rewind_ether_buffer() { - bfill = BufferFiller(ether_buffer, ETHER_BUFFER_SIZE*2); - ether_buffer[0] = 0; -} -void send_packet(OTF_PARAMS_DEF) { -#if defined(USE_OTF) - res.writeBodyData(ether_buffer, strlen(ether_buffer)); -#else - m_client->write((const uint8_t *)ether_buffer, strlen(ether_buffer)); -#endif - rewind_ether_buffer(); -} +enum ContentType { CT_JSON, CT_HTML, CT_CSV, CT_BINARY }; -char dec2hexchar(unsigned char dec) { - if(dec<10) return '0'+dec; - else return 'A'+(dec-10); -} - -#if defined(USE_OTF) -void print_header(OTF_PARAMS_DEF, bool isJson=true, int len=0) { +void print_header(OTF_PARAMS_DEF, ContentType ct=CT_JSON, int len=0) { res.writeStatus(200, F("OK")); - res.writeHeader(F("Content-Type"), isJson?F("application/json"):F("text/html")); + switch (ct) { + case CT_JSON: res.writeHeader(F("Content-Type"), F("application/json")); break; + case CT_HTML: res.writeHeader(F("Content-Type"), F("text/html")); break; + case CT_CSV: res.writeHeader(F("Content-Type"), F("text/csv")); break; + case CT_BINARY: res.writeHeader(F("Content-Type"), F("application/octet-stream")); break; + } if(len>0) res.writeHeader(F("Content-Length"), len); res.writeHeader(F("Access-Control-Allow-Origin"), F("*")); @@ -244,14 +198,8 @@ void print_header_compressed_html(OTF_PARAMS_DEF, int len) { res.writeHeader(F("Content-Encoding"), F("gzip")); res.writeHeader(F("Connection"), F("close")); } -#else -void print_header(bool isJson=true) { - bfill.emit_p(PSTR("$F$F$F$F\r\n"), html200OK, isJson?htmlContentJSON:htmlContentHTML, htmlAccessControl, htmlNoCache); -} -#endif -#if defined(USE_OTF) -#if !defined(ARDUINO) +#if !defined(ESP8266) string two_digits(uint8_t x) { return std::to_string(x); } @@ -261,13 +209,13 @@ String two_digits(uint8_t x) { } #endif -String toHMS(ulong t) { +String toHMS(uint32_t t) { return two_digits(t/3600)+":"+two_digits((t/60)%60)+":"+two_digits(t%60); } void otf_send_result(OTF_PARAMS_DEF, unsigned char code, const char *item = NULL) { String json = F("{\"result\":"); -#if defined(ARDUINO) +#if defined(ESP8266) json += code; #else json += std::to_string(code); @@ -277,7 +225,7 @@ void otf_send_result(OTF_PARAMS_DEF, unsigned char code, const char *item = NULL json += item; json += F("\""); json += F("}"); - print_header(OTF_PARAMS, true, json.length()); + print_header(OTF_PARAMS, CT_JSON, json.length()); res.writeBodyChunk((char *)"%s",json.c_str()); } @@ -319,7 +267,7 @@ void on_ap_home(OTF_PARAMS_DEF) { void on_ap_scan(OTF_PARAMS_DEF) { if(os.get_wifi_mode()!=WIFI_MODE_AP) return; - print_header(OTF_PARAMS, true, scanned_ssids.length()); + print_header(OTF_PARAMS, CT_JSON, scanned_ssids.length()); res.writeBodyChunk((char *)"%s",scanned_ssids.c_str()); } @@ -367,7 +315,7 @@ void on_ap_try_connect(OTF_PARAMS_DEF) { json += F("\"ip\":"); json += (WiFi.status() == WL_CONNECTED) ? (uint32_t)WiFi.localIP() : 0; json += F("}"); - print_header(OTF_PARAMS,true,json.length()); + print_header(OTF_PARAMS, CT_JSON, json.length()); res.writeBodyChunk((char *)"%s",json.c_str()); if(WiFi.status() == WL_CONNECTED && WiFi.localIP()) { os.iopts[IOPT_WIFI_MODE] = WIFI_MODE_STA; @@ -377,32 +325,18 @@ void on_ap_try_connect(OTF_PARAMS_DEF) { } } #endif -#endif /** Check and verify password */ -#if defined(USE_OTF) boolean check_password(char *p) { return true; } -boolean process_password(OTF_PARAMS_DEF, boolean fwv_on_fail=false) -#else -boolean check_password(char *p) -#endif -{ +boolean process_password(OTF_PARAMS_DEF, boolean fwv_on_fail=false) { #if defined(DEMO) return true; #endif if (os.iopts[IOPT_IGNORE_PASSWORD]) return true; -#if !defined(USE_OTF) - if (m_client && !p) { - p = get_buffer; - } - if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("pw"), true)) { - if (os.password_verify(tmp_buffer)) return true; - } -#else /*if(req.isCloudRequest()){ // password is not required if this is coming from cloud connection return true; }*/ @@ -411,14 +345,12 @@ boolean check_password(char *p) /* if fwv_on_fail is true, output fwv if password check has failed */ if(fwv_on_fail) { - rewind_ether_buffer(); + print_header(OTF_PARAMS); + begin_response(res); bfill.emit_p(PSTR("{\"$F\":$D}"), iopt_json_names+0, os.iopts[0]); - print_header(OTF_PARAMS,true,strlen(ether_buffer)); - res.writeBodyChunk((char *)"%s",ether_buffer); } else { otf_send_result(OTF_PARAMS, HTML_UNAUTHORIZED); } -#endif return false; } @@ -450,6 +382,8 @@ void server_json_stations_attrib(const char* name, unsigned char *attrib) void server_json_stations_main(OTF_PARAMS_DEF) { server_json_board_attrib(PSTR("masop"), os.attrib_mas); server_json_board_attrib(PSTR("masop2"), os.attrib_mas2); + server_json_board_attrib(PSTR("masop3"), os.attrib_mas3); + server_json_board_attrib(PSTR("masop4"), os.attrib_mas4); server_json_board_attrib(PSTR("ignore_rain"), os.attrib_igrd); server_json_board_attrib(PSTR("ignore_sn1"), os.attrib_igs); server_json_board_attrib(PSTR("ignore_sn2"), os.attrib_igs2); @@ -464,22 +398,15 @@ void server_json_stations_main(OTF_PARAMS_DEF) { bfill.emit_p(PSTR("\"$S\""), tmp_buffer); if(sid!=os.nstations-1) bfill.emit_p(PSTR(",")); - if (available_ether_buffer() <=0 ) { - send_packet(OTF_PARAMS); - } } bfill.emit_p(PSTR("],\"maxlen\":$D}"), STATION_NAME_SIZE); } /** Output stations data */ void server_json_stations(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; - rewind_ether_buffer(); + begin_response(res); print_header(OTF_PARAMS); -#else - print_header(); -#endif bfill.emit_p(PSTR("{")); server_json_stations_main(OTF_PARAMS); @@ -488,13 +415,9 @@ void server_json_stations(OTF_PARAMS_DEF) { /** Output station special attribute */ void server_json_station_special(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; - rewind_ether_buffer(); + begin_response(res); print_header(OTF_PARAMS); -#else - print_header(); -#endif unsigned char sid; unsigned char comma=0; @@ -509,20 +432,12 @@ void server_json_station_special(OTF_PARAMS_DEF) { else {comma=1;} bfill.emit_p(PSTR("\"$D\":{\"st\":$D,\"sd\":\"$S\"}"), sid, data->type, data->sped); } - if (available_ether_buffer() <=0 ) { - send_packet(OTF_PARAMS); - } } bfill.emit_p(PSTR("}")); handle_return(HTML_OK); } -#if defined(USE_OTF) -void server_change_board_attrib(const OTF::Request &req, char header, unsigned char *attrib) -#else -void server_change_board_attrib(char *p, char header, unsigned char *attrib) -#endif -{ +void server_change_board_attrib(const OTF::Request &req, char header, unsigned char *attrib) { char tbuf2[6] = {0}; unsigned char bid; tbuf2[0]=header; @@ -534,12 +449,7 @@ void server_change_board_attrib(char *p, char header, unsigned char *attrib) } } -#if defined(USE_OTF) -void server_change_stations_attrib(const OTF::Request &req, char header, unsigned char *attrib) -#else -void server_change_stations_attrib(char *p, char header, unsigned char *attrib) -#endif -{ +void server_change_stations_attrib(const OTF::Request &req, char header, unsigned char *attrib) { char tbuf2[6] = {0}; unsigned char bid, s, sid; tbuf2[0]=header; @@ -566,13 +476,11 @@ void server_change_stations_attrib(char *p, char header, unsigned char *attrib) * q?: station sequential bit field * p?: station special flag bit field * g?: sequential group id + * u?: master3 operation bit field + * v?: master4 operation bit field */ void server_change_stations(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; -#else - char* p = get_buffer; -#endif unsigned char sid; char tbuf2[5] = {'s', 0, 0, 0, 0}; @@ -580,9 +488,6 @@ void server_change_stations(OTF_PARAMS_DEF) { for(sid=0;sid sizeof(HTTPStationData)) { handle_return(HTML_DATA_OUTOFBOUND); } @@ -660,7 +564,7 @@ uint16_t parse_listdata(char **p) { return (uint16_t)atol(tmp_buffer); } -void manual_start_program(unsigned char, unsigned char, unsigned char); +void manual_start_program(unsigned char, unsigned char, unsigned char, unsigned char usa=0); /** Manual start program * Command: /mp?pw=xxx&pid=xx&uwt=x&qo=x * @@ -670,11 +574,7 @@ void manual_start_program(unsigned char, unsigned char, unsigned char); * qo: queue option (0: append; 1: insert at front; 2: replace (default) ) */ void server_manual_program(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; -#else - char *p = get_buffer; -#endif if (!findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("pid"), true)) handle_return(HTML_DATA_MISSING); @@ -689,6 +589,11 @@ void server_manual_program(OTF_PARAMS_DEF) { if(tmp_buffer[0]=='1') uwt = 1; } + unsigned char usa = 0; + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("usa"), true)) { + if(tmp_buffer[0]=='1') usa = 1; + } + unsigned char qo = QUEUE_OPTION_REPLACE; if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("qo"), true)) { qo=(unsigned char)atoi(tmp_buffer); @@ -698,7 +603,7 @@ void server_manual_program(OTF_PARAMS_DEF) { reset_all_stations_immediate(); } - manual_start_program(pid+1, uwt, qo); + manual_start_program(pid+1, uwt, qo, usa); handle_return(HTML_SUCCESS); } @@ -715,27 +620,9 @@ void server_manual_program(OTF_PARAMS_DEF) { * anno?: annotation for station ordering (refer to program name annotation) */ void server_change_runonce(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; if(!findKeyVal(FKV_SOURCE,tmp_buffer,TMP_BUFFER_SIZE, "t", false)) handle_return(HTML_DATA_MISSING); char *pv = tmp_buffer+1; -#else - char *p = get_buffer; - - // decode url first - if(p) urlDecode(p); - // search for the start of t=[ - char *pv; - boolean found=false; - for(pv=p;(*pv)!=0 && pv 0xFF) handle_return(HTML_DATA_FORMATERROR); + adj_flag = (uint8_t)v; + + if (*end == ',') { + ptr = end + 1; + v = strtoul(ptr, &end, 10); + if (end == ptr || (*end != ',' && *end != '\0') || v > 0xFFFF) handle_return(HTML_DATA_FORMATERROR); + adj_uuid = (uint16_t)v; + + if (*end == ',') { + ptr = end + 1; + float last_x = -std::numeric_limits::infinity(); + while (*ptr != '\0') { + if (point_count >= SENSOR_ADJUSTMENT_POINTS) handle_return(HTML_DATA_FORMATERROR); + float x = strtof(ptr, &end); + if (end == ptr || *end != ',') handle_return(HTML_DATA_FORMATERROR); + ptr = end + 1; + float y = strtof(ptr, &end); + if (end == ptr || (*end != ',' && *end != '\0')) handle_return(HTML_DATA_FORMATERROR); + if (!isfinite(x) || !isfinite(y) || y < 0 || x < last_x) handle_return(HTML_DATA_FORMATERROR); + points[point_count++] = {x, y}; + last_x = x; + ptr = (*end == ',') ? end + 1 : end; + } + } + } -#if !defined(USE_OTF) - if(p) urlDecode(p); -#endif - + snadj = SensorAdjustment(adj_uuid, point_count, adj_flag, points); + snadj_ptr = &snadj; + } -#if defined(USE_OTF) if(!findKeyVal(FKV_SOURCE,tmp_buffer,TMP_BUFFER_SIZE, "v",false)) handle_return(HTML_DATA_MISSING); char *pv = tmp_buffer+1; -#else - // parse ad-hoc v=[... - // search for the start of v=[ - char *pv; - boolean found=false; - - for(pv=p;(*pv)!=0 && pv=IOPT_STATIC_IP1 && oid<=IOPT_STATIC_IP4) || (oid>=IOPT_GATEWAY_IP1 && oid<=IOPT_GATEWAY_IP4) || @@ -1054,20 +948,16 @@ void server_json_options_main() { continue; #endif - #if !(defined(ESP8266) || defined(PIN_SENSOR2)) - // only OS 3.x or controllers that have PIN_SENSOR2 defined support sensor 2 options - if (oid==IOPT_SENSOR2_TYPE || oid==IOPT_SENSOR2_OPTION || oid==IOPT_SENSOR2_ON_DELAY || oid==IOPT_SENSOR2_OFF_DELAY) - continue; - #endif - int32_t v=os.iopts[oid]; - if (oid==IOPT_MASTER_OFF_ADJ || oid==IOPT_MASTER_OFF_ADJ_2 || - oid==IOPT_MASTER_ON_ADJ || oid==IOPT_MASTER_ON_ADJ_2 || + if (oid==IOPT_MASTER_OFF_ADJ || oid==IOPT_MASTER_OFF_ADJ_2 || + oid==IOPT_MASTER_OFF_ADJ_3 || oid==IOPT_MASTER_OFF_ADJ_4 || + oid==IOPT_MASTER_ON_ADJ || oid==IOPT_MASTER_ON_ADJ_2 || + oid==IOPT_MASTER_ON_ADJ_3 || oid==IOPT_MASTER_ON_ADJ_4 || oid==IOPT_STATION_DELAY_TIME) { v=water_time_decode_signed(v); } - #if defined(ARDUINO) + #if defined(ESP8266) if (oid==IOPT_BOOST_TIME) { if (os.hw_type==HW_TYPE_AC || os.hw_type==HW_TYPE_UNKNOWN) continue; else v<<=2; @@ -1095,22 +985,9 @@ void server_json_options_main() { } #endif - if (oid==IOPT_SEQUENTIAL_RETIRED || oid==IOPT_URS_RETIRED || oid==IOPT_RSO_RETIRED || oid==IOPT_RESERVE_7 || oid==IOPT_RESERVE_8) continue; - -#if defined(ARDUINO) - #if defined(ESP8266) - // for SSD1306, we can't adjust contrast or backlight - if(oid==IOPT_LCD_CONTRAST || oid==IOPT_LCD_BACKLIGHT) continue; - #else - if (os.lcd.type() == LCD_I2C) { - // for I2C type LCD, we can't adjust contrast or backlight - if(oid==IOPT_LCD_CONTRAST || oid==IOPT_LCD_BACKLIGHT) continue; - } - #endif -#else - // for Linux-based platforms, we can't adjust contrast or backlight - if(oid==IOPT_LCD_CONTRAST || oid==IOPT_LCD_BACKLIGHT) continue; -#endif + if (oid==IOPT_LCD_CONTRAST || oid==IOPT_LCD_BACKLIGHT || oid==IOPT_SEQUENTIAL_RETIRED || oid==IOPT_URS_RETIRED || + oid==IOPT_RSO_RETIRED || oid==IOPT_RESERVE_7 || oid==IOPT_RESERVE_8) + continue; // each json name takes 5 characters strncpy_P0(tmp_buffer, iopt_json_names+oid*5, 5); @@ -1134,13 +1011,9 @@ void server_json_options_main() { /** Output Options */ void server_json_options(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS,true)) return; - rewind_ether_buffer(); + begin_response(res); print_header(OTF_PARAMS); -#else - print_header(); -#endif bfill.emit_p(PSTR("{")); server_json_options_main(); handle_return(HTML_OK); @@ -1173,41 +1046,61 @@ void server_json_programs_main(OTF_PARAMS_DEF) { // program name strncpy(tmp_buffer, prog.name, PROGRAM_NAME_SIZE); tmp_buffer[PROGRAM_NAME_SIZE] = 0; // make sure the string ends - bfill.emit_p(PSTR("$S\",[$D,$D,$D]]"), tmp_buffer,prog.en_daterange,prog.daterange[0],prog.daterange[1]); + bfill.emit_p(PSTR("$S\",[$D,$D,$D],"), tmp_buffer,prog.en_daterange,prog.daterange[0],prog.daterange[1]); + // sensor adjustment embedded in each program entry + { + SensorAdjustment *adj = SensorAdjustment::read(pid, pd.nprograms); + if (adj) { + bfill.emit_p(PSTR("{\"flag\":$D,\"uuid\":$D,\"splits\":["), adj->flag, adj->uuid); + for (int j = 0; j < adj->point_count; j++) { + if (j) bfill.emit_p(PSTR(",")); + bfill.emit_p(PSTR("{\"x\":$E,\"y\":$E}"), adj->points[j].x, adj->points[j].y); + } + bfill.emit_p(PSTR("]}")); + } else { + bfill.emit_p(PSTR("{}")); + } + } + bfill.emit_p(PSTR("]")); if(pid!=pd.nprograms-1) { bfill.emit_p(PSTR(",")); } - // push out a packet if available - // buffer size is getting small - if (available_ether_buffer() <= 0) { - send_packet(OTF_PARAMS); - } } bfill.emit_p(PSTR("]}")); } /** Output program data */ void server_json_programs(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; - rewind_ether_buffer(); + begin_response(res); print_header(OTF_PARAMS); -#else - print_header(); -#endif bfill.emit_p(PSTR("{")); server_json_programs_main(OTF_PARAMS); handle_return(HTML_OK); } +/** Output per-program adjustment factors: water percent (wa), sensor adj (sa), total adj (ta) */ +void server_json_program_adj(OTF_PARAMS_DEF) { + if(!process_password(OTF_PARAMS)) return; + begin_response(res); + print_header(OTF_PARAMS); + bfill.emit_p(PSTR("{\"jpa\":[")); + ProgramStruct prog; + for(uint8_t pid=0; pid
JavaScript:
Default:$S
Weather:
Default:$S
Password:
"), bfill.emit_p(PSTR(R"(
@@ -1257,12 +1150,10 @@ void server_json_controller_main(OTF_PARAMS_DEF) { bfill.emit_p(PSTR("\"apdv\":$D,"), os.actual_pd_voltage); #endif -#if defined(USE_OTF) bfill.emit_p(PSTR("\"otc\":{$O},\"otcs\":$D,"), SOPT_OTC_OPTS, otf->getCloudStatus()); -#endif unsigned char mac[6] = {0}; -#if defined(ARDUINO) +#if defined(ESP8266) os.load_hardware_mac(mac, useEth); #else os.load_hardware_mac(mac, true); @@ -1282,9 +1173,7 @@ void server_json_controller_main(OTF_PARAMS_DEF) { wt_restricted, SOPT_DEVICE_NAME); -#if defined(SUPPORT_EMAIL) bfill.emit_p(PSTR("\"email\":{$O},"), SOPT_EMAIL_OPTS); -#endif bfill.emit_p(PSTR("\"wls\":[")); if (md_N == 0) { @@ -1295,7 +1184,7 @@ void server_json_controller_main(OTF_PARAMS_DEF) { bfill.emit_p((idx == md_N-1) ? PSTR("],") : PSTR(",")); } -#if defined(ARDUINO) +#if defined(ESP8266) uint16_t current = os.read_current(true); if((!os.status.program_busy) && (current$F"), @@ -1394,12 +1270,8 @@ void server_home(OTF_PARAMS_DEF) */ void server_change_values(OTF_PARAMS_DEF) { -#if defined(USE_OTF) extern uint32_t reboot_timer; if(!process_password(OTF_PARAMS)) return; -#else - char *p = get_buffer; -#endif if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("rsn"), true) && atoi(tmp_buffer) > 0) { reset_all_stations(); } @@ -1412,24 +1284,16 @@ void server_change_values(OTF_PARAMS_DEF) os.status.overcurrent_sid = 0; // clear overcurrent status } - #if !defined(ARDUINO) + #if !defined(ESP8266) if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("update"), true) && atoi(tmp_buffer) > 0) { os.update_dev(); } #endif if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("rbt"), true) && atoi(tmp_buffer) > 0) { - #if defined(USE_OTF) - os.status.safe_reboot = 0; - reboot_timer = os.now_tz() + 1; - handle_return(HTML_SUCCESS); - #else - print_header(false); - //bfill.emit_p(PSTR("Rebooting...")); - send_packet(); - m_client->stop(); - os.reboot_dev(REBOOT_CAUSE_WEB); - #endif + os.status.safe_reboot = 0; + reboot_timer = os.now_tz() + 1; + handle_return(HTML_SUCCESS); } if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("en"), true)) { @@ -1440,7 +1304,7 @@ void server_change_values(OTF_PARAMS_DEF) if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("rd"), true)) { int rd = atoi(tmp_buffer); if (rd>0) { - os.nvdata.rd_stop_time = os.now_tz() + (unsigned long) rd * 3600; + os.nvdata.rd_stop_time = os.now_tz() + (uint32_t) rd * 3600; os.raindelay_start(); } else if (rd==0){ os.raindelay_stop(); @@ -1485,40 +1349,26 @@ void string_remove_space(char *src) { * jsp: Javascript path */ void server_change_scripturl(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; -#else - char *p = get_buffer; -#endif #if defined(DEMO) handle_return(HTML_REDIRECT_HOME); #endif if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("jsp"), true)) { tmp_buffer[TMP_BUFFER_SIZE-1]=0; // make sure we don't exceed the maximum size - #if !defined(USE_OTF) - urlDecode(tmp_buffer); - #endif // trim unwanted space characters string_remove_space(tmp_buffer); os.sopt_save(SOPT_JAVASCRIPTURL, tmp_buffer); } if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("wsp"), true)) { tmp_buffer[TMP_BUFFER_SIZE-1]=0; - #if !defined(USE_OTF) - urlDecode(tmp_buffer); - #endif string_remove_space(tmp_buffer); os.sopt_save(SOPT_WEATHERURL, tmp_buffer); } -#if defined(USE_OTF) - rewind_ether_buffer(); - print_header(OTF_PARAMS,false,strlen(ether_buffer)); + begin_response(res); + print_header(OTF_PARAMS, CT_HTML); bfill.emit_p(PSTR("$F"), htmlReturnHome); handle_return(HTML_OK); -#else - handle_return(HTML_REDIRECT_HOME); -#endif } /** @@ -1532,12 +1382,7 @@ void server_change_scripturl(OTF_PARAMS_DEF) { */ void server_change_options(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; -#else - char *p = get_buffer; -#endif - // temporarily save some old options values bool time_change = false; bool weather_change = false; @@ -1566,8 +1411,10 @@ void server_change_options(OTF_PARAMS_DEF) strncpy_P0(tbuf2, iopt_json_names+oid*5, 5); if(findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, tbuf2)) { int32_t v = atol(tmp_buffer); - if (oid==IOPT_MASTER_OFF_ADJ || oid==IOPT_MASTER_OFF_ADJ_2 || - oid==IOPT_MASTER_ON_ADJ || oid==IOPT_MASTER_ON_ADJ_2 || + if (oid==IOPT_MASTER_OFF_ADJ || oid==IOPT_MASTER_OFF_ADJ_2 || + oid==IOPT_MASTER_OFF_ADJ_3 || oid==IOPT_MASTER_OFF_ADJ_4 || + oid==IOPT_MASTER_ON_ADJ || oid==IOPT_MASTER_ON_ADJ_2 || + oid==IOPT_MASTER_ON_ADJ_3 || oid==IOPT_MASTER_ON_ADJ_4 || oid==IOPT_STATION_DELAY_TIME) { v=water_time_encode_signed(v); } // encode station delay time @@ -1598,9 +1445,6 @@ void server_change_options(OTF_PARAMS_DEF) } if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("loc"), true)) { - #if !defined(USE_OTF) - urlDecode(tmp_buffer); - #endif strReplaceQuoteBackslash(tmp_buffer); if (os.sopt_save(SOPT_LOCATION, tmp_buffer)) { // if location string has changed weather_change = true; @@ -1608,9 +1452,6 @@ void server_change_options(OTF_PARAMS_DEF) } uint8_t keyfound = 0; if(findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("wto"), true)) { - #if !defined(USE_OTF) - urlDecode(tmp_buffer); - #endif if (os.sopt_save(SOPT_WEATHER_OPTS, tmp_buffer)) { os.sopt_load(SOPT_WEATHER_OPTS, tmp_buffer+1); // make room for the leading '{' parse_wto(tmp_buffer); // parse wto @@ -1621,9 +1462,6 @@ void server_change_options(OTF_PARAMS_DEF) keyfound = 0; if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("ifkey"), true, &keyfound)) { - #if !defined(USE_OTF) - urlDecode(tmp_buffer); - #endif os.sopt_save(SOPT_IFTTT_KEY, tmp_buffer); } else if (keyfound) { tmp_buffer[0]=0; @@ -1632,9 +1470,6 @@ void server_change_options(OTF_PARAMS_DEF) keyfound = 0; if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("otc"), true, &keyfound)) { - #if !defined(USE_OTF) - urlDecode(tmp_buffer); - #endif os.sopt_save(SOPT_OTC_OPTS, tmp_buffer); } else if (keyfound) { tmp_buffer[0]=0; @@ -1643,9 +1478,6 @@ void server_change_options(OTF_PARAMS_DEF) keyfound = 0; if(findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("mqtt"), true, &keyfound)) { - #if !defined(USE_OTF) - urlDecode(tmp_buffer); - #endif os.sopt_save(SOPT_MQTT_OPTS, tmp_buffer); os.status.req_mqtt_restart = true; } else if (keyfound) { @@ -1656,9 +1488,6 @@ void server_change_options(OTF_PARAMS_DEF) keyfound = 0; if(findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("email"), true, &keyfound)) { - #if !defined(USE_OTF) - urlDecode(tmp_buffer); - #endif os.sopt_save(SOPT_EMAIL_OPTS, tmp_buffer); } else if (keyfound) { tmp_buffer[0]=0; @@ -1666,22 +1495,19 @@ void server_change_options(OTF_PARAMS_DEF) } if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("dname"), true)) { - #if !defined(USE_OTF) - urlDecode(tmp_buffer); - #endif strReplaceQuoteBackslash(tmp_buffer); os.sopt_save(SOPT_DEVICE_NAME, tmp_buffer); } // if not using NTP and manually setting time if (!os.iopts[IOPT_USE_NTP] && findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("ttt"), true)) { -#if defined(ARDUINO) - unsigned long t; +#if defined(ESP8266) + uint32_t t; t = strtoul(tmp_buffer, NULL, 0); #endif // before chaging time, reset all stations to avoid messing up with timing reset_all_stations_immediate(); -#if defined(ARDUINO) +#if defined(ESP8266) setTime(t); RTC.set(t); #endif @@ -1731,11 +1557,6 @@ void server_change_password(OTF_PARAMS_DEF) { return; #endif -#if defined(USE_OTF) - if(!process_password(OTF_PARAMS)) return; -#else - char* p = get_buffer; -#endif if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("npw"), true)) { const int pwBufferSize = TMP_BUFFER_SIZE/2; char *tbuf2 = tmp_buffer + pwBufferSize; // use the second half of tmp_buffer @@ -1763,13 +1584,9 @@ void server_json_status_main() { /** Output station status */ void server_json_status(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; - rewind_ether_buffer(); + begin_response(res); print_header(OTF_PARAMS); -#else - print_header(); -#endif bfill.emit_p(PSTR("{")); server_json_status_main(); @@ -1788,11 +1605,7 @@ void server_json_status(OTF_PARAMS_DEF) * qo: queuing option (0: append after others; 1: run now and pause others) */ void server_change_manual(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; -#else - char *p = get_buffer; -#endif int sid=-1; if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("sid"), true)) { @@ -1810,7 +1623,7 @@ void server_change_manual(OTF_PARAMS_DEF) { } uint16_t timer=0; - unsigned long curr_time = os.now_tz(); + uint32_t curr_time = os.now_tz(); if (en) { // if turning on a station, must provide timer if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("t"), true)) { timer=(uint16_t)atol(tmp_buffer); @@ -1825,7 +1638,8 @@ void server_change_manual(OTF_PARAMS_DEF) { // schedule manual station // skip if the station is a master station // (because master cannot be scheduled independently) - if ((os.status.mas==sid+1) || (os.status.mas2==sid+1)) + if ((os.status.mas==sid+1) || (os.status.mas2==sid+1) || + (os.status.mas3==sid+1) || (os.status.mas4==sid+1)) handle_return(HTML_NOT_PERMITTED); RuntimeQueueStruct *q = NULL; @@ -1896,12 +1710,7 @@ int file_fgets(File file, char* buf, int maxsize) { * if unspecified, output all records */ void server_json_log(OTF_PARAMS_DEF) { - -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; -#else - char *p = get_buffer; -#endif unsigned int start, end; @@ -1932,29 +1741,21 @@ void server_json_log(OTF_PARAMS_DEF) { if (findKeyVal(FKV_SOURCE, type, 4, PSTR("type"), true)) type_specified = true; -#if defined(USE_OTF) // as the log data can be large, we will use ESP8266's sendContent function to // send multiple packets of data, instead of the standard way of using send(). - rewind_ether_buffer(); + begin_response(res); print_header(OTF_PARAMS); -#else - print_header(); -#endif bfill.emit_p(PSTR("[")); bool comma = 0; for(unsigned int i=start;i<=end;i++) { - snprintf(tmp_buffer, TMP_BUFFER_SIZE*2 , "%d", i); + snprintf(tmp_buffer, TMP_BUFFER_ALLOC_SIZE , "%d", i); make_logfile_name(tmp_buffer); #if defined(ESP8266) File file = LittleFS.open(tmp_buffer, "r"); if(!file) continue; -#elif defined(ARDUINO) - if (!sd.exists(tmp_buffer)) continue; - SdFile file; - file.open(tmp_buffer, O_READ); #else // prepare to open log file for Linux FILE *file = fopen(get_filename_fullpath(tmp_buffer), "rb"); if(!file) continue; @@ -1969,12 +1770,6 @@ void server_json_log(OTF_PARAMS_DEF) { break; } tmp_buffer[result]=0; - #elif defined(ARDUINO) - result = file.fgets(tmp_buffer, TMP_BUFFER_SIZE); - if (result <= 0) { - file.close(); - break; - } #else if(fgets(tmp_buffer, TMP_BUFFER_SIZE, file)) { result = strlen(tmp_buffer); @@ -2007,11 +1802,6 @@ void server_json_log(OTF_PARAMS_DEF) { if (comma) bfill.emit_p(PSTR(",")); else {comma=1;} bfill.emit_p(PSTR("$S"), tmp_buffer); - // if the available ether buffer size is getting small - // push out a packet - if (available_ether_buffer() <= 0) { - send_packet(OTF_PARAMS); - } } } @@ -2028,12 +1818,7 @@ void server_json_log(OTF_PARAMS_DEF) { * if day=all: delete all log files) */ void server_delete_log(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; -#else - char *p = get_buffer; -#endif - if (!findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("day"), true)) handle_return(HTML_DATA_MISSING); @@ -2048,13 +1833,9 @@ void server_delete_log(OTF_PARAMS_DEF) { * repl: replace (in units of seconds) (New UI allows for replace, extend, and pause using this) */ void server_pause_queue(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS)) return; -#else - char *p = get_buffer; -#endif - ulong duration = 0; + uint32_t duration = 0; if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("repl"), true)) { duration = strtoul(tmp_buffer, NULL, 0); pd.resume_stations(); @@ -2077,45 +1858,703 @@ void server_pause_queue(OTF_PARAMS_DEF) { handle_return(HTML_SUCCESS); } +void server_json_sensors_main(OTF_PARAMS_DEF) { + bfill.emit_p(PSTR("\"sn\":[")); + uint8_t sensor_count = 0; + + Sensor *sensor; + for (size_t i = 0; i < os.nsensors; i++) { + if (os.sensors[i].interval && (sensor = Sensor::get(i))) { + if (sensor_count) bfill.emit_p(PSTR(",")); + bfill.emit_p(PSTR("{\"uuid\":$D,\"name\":\"$S\",\"unit\":$D,\"flag\":$D,\"status\":$D,\"interval\":$L,\"min\":$E,\"max\":$E,\"value\":$E,\"type\":$D,\"extra\":"), sensor->uuid, sensor->name, static_cast(sensor->unit), sensor->flag, os.sensors[i].status, sensor->interval, sensor->min, sensor->max, os.sensors[i].value, static_cast(sensor->get_sensor_type())); + sensor->emit_extra_json(&bfill); + bfill.emit_p(PSTR("}")); + sensor_count += 1; + } + } + + + bfill.emit_p(PSTR("],\"count\":$D}"), sensor_count); +} + +/** Sensor status */ +void server_json_sensors(OTF_PARAMS_DEF) +{ + if(!process_password(OTF_PARAMS)) return; + begin_response(res); + print_header(OTF_PARAMS); + + bfill.emit_p(PSTR("{")); + server_json_sensors_main(OTF_PARAMS); + handle_return(HTML_OK); +} + +/** + * Add or change a sensor + * Command: /csn?pw=xxx&[uuid=xxx|sid=xxx]&type=xxx&... + * + * pw: password + * uuid: sensor stable ID (1-65535; -1 to add new) + * sid: sensor positional index (0-based; -1 to add new) + * (uuid takes precedence if both are provided) + * type: sensor type (0: Aggregate, 1: ADS1115, 2: Weather) + * name: sensor name + * min/max: output clamping range + * interval: sampling interval in minutes + * unit: sensor unit index + * flag: bitmask (bit 0: enable, bit 1: log) + * [Aggregate] children: semicolon separated list of "uuid,scale,offset;" + * [Aggregate] action: aggregate action index (0: Min, 1: Max, 2: Average, 3: Sum, 4: Median, 5: Range) + * [ADS1115] pin: pin number (1-16) + * [Weather] action: weather information index + */ +void server_change_sensor(OTF_PARAMS_DEF) { + if(!process_password(OTF_PARAMS)) return; + + char *end; + long sid = -1; + bool is_new = false; + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("uuid"), true)) { + long uuid_param = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (uuid_param == -1) { + is_new = true; + sid = os.nsensors; + } else { + if (uuid_param < 1 || uuid_param > 0xFFFF) handle_return(HTML_DATA_OUTOFBOUND); + sid = Sensor::find_index((uint16_t)uuid_param); + if (sid >= os.nsensors) handle_return(HTML_DATA_OUTOFBOUND); + } + } else if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("sid"), true)) { + long sid_param = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (sid_param == -1) { + is_new = true; + sid = os.nsensors; + } else { + if (sid_param < 0 || sid_param >= os.nsensors) handle_return(HTML_DATA_OUTOFBOUND); + sid = sid_param; + } + } else { + handle_return(HTML_DATA_MISSING); + } + + if (is_new && sid >= MAX_SENSORS) handle_return(HTML_DATA_OUTOFBOUND); + + if (!findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("type"), true)) handle_return(HTML_DATA_MISSING); + + uint32_t type_raw = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (type_raw >= (uint32_t)SensorType::MAX_VALUE) handle_return(HTML_DATA_OUTOFBOUND); + + SensorType sensor_type = static_cast(type_raw); + + Sensor *sensor = nullptr; + float min = SENSOR_DEFAULT_MIN; + float max = SENSOR_DEFAULT_MAX; + uint32_t interval = SENSOR_DEFAULT_INTERVAL; + SensorUnit unit = SENSOR_DEFAULT_UNIT; + uint8_t flag = SENSOR_DEFAULT_FLAG; + + char name[SENSOR_NAME_LEN]; + strncpy(name, SENSOR_DEFAULT_NAME, SENSOR_NAME_LEN); + + SensorType original_sensor_type = SensorType::MAX_VALUE; + if (os.sensors[sid].interval) { + if ((sensor = Sensor::get(sid))) { + original_sensor_type = sensor->get_sensor_type(); + strncpy(name, sensor->name, SENSOR_NAME_LEN); + min = sensor->min; + max = sensor->max; + interval = sensor->interval; + unit = sensor->unit; + flag = sensor->flag; + } + } + + // parse sensor name + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("name"), true)) { + strReplaceQuoteBackslash(tmp_buffer); + strncpy(name, tmp_buffer, SENSOR_NAME_LEN); + } + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("min"), true)) { + min=strtod(tmp_buffer, &end); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); +} + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("max"), true)) { + max=strtod(tmp_buffer, &end); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + } + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("interval"), true)) { + interval=strtoul(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + } + + if (interval < 1) handle_return(HTML_DATA_OUTOFBOUND); + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("unit"), true)) { + uint32_t unit_raw = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (unit_raw >= (uint32_t)SensorUnit::MAX_VALUE) handle_return(HTML_DATA_OUTOFBOUND); + unit = static_cast(unit_raw); + } + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("flag"), true)) { + flag = (uint8_t)strtoul(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + } + + Sensor *result_sensor; + switch (sensor_type) { + case SensorType::Aggregate: { + uint8_t children_count = 0; + aggregate_children_t children[AGGREGATE_SENSOR_CHILDREN_COUNT]; + for (size_t i = 0; i < AGGREGATE_SENSOR_CHILDREN_COUNT; i++) { + children[i].uuid = SENSOR_UUID_NONE; + } + + AggregateAction action = AggregateAction::Min; + + if (sensor_type == original_sensor_type) { + if ((sensor = Sensor::get(sid))) { + AggregateSensor* e = static_cast(sensor); + for (size_t i = 0; i < AGGREGATE_SENSOR_CHILDREN_COUNT; i++) { + children[i] = e->children[i]; + } + + action = e->action; + } + } + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("children"), true)) { + unsigned int i = 0; + int d; + float d1, d2; + const char *ptr = tmp_buffer; + int result; + + while (*ptr != '\0') { + if (i >= AGGREGATE_SENSOR_CHILDREN_COUNT) handle_return(HTML_DATA_FORMATERROR); + + result = sscanf(ptr, "%d,%f,%f;", &d, &d1, &d2); + + if (result != 3) { + handle_return(HTML_DATA_FORMATERROR); + } + + // d is the child sensor's UUID; out-of-range values map to disabled + uint16_t child_uuid = (d >= 1 && d <= 0xFFFF) ? (uint16_t)d : SENSOR_UUID_NONE; + + children[i++] = aggregate_children_t {d1, d2, child_uuid}; + + while (*ptr != '\0' && *(ptr++) != ';') {} + } + + children_count = i; + } + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("action"), true)) { + uint32_t action_raw = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (action_raw >= (uint32_t)AggregateAction::MAX_VALUE) handle_return(HTML_DATA_OUTOFBOUND); + action = static_cast(action_raw); + } + + result_sensor = new AggregateSensor(interval, min, max, (const char*)&name, unit, flag, os.sensors, children, children_count, action); + break; + } + case SensorType::ADS1115: { + uint32_t sensor_index = 0; + uint32_t sensor_pin = 0; + float scale = ADS1115_DEFAULT_SCALE; + float offset = ADS1115_DEFAULT_OFFSET; + + if (sensor_type == original_sensor_type) { + if ((sensor = Sensor::get(sid))) { + ADS1115Sensor* e = static_cast(sensor); + sensor_index = e->sensor_index; + sensor_pin = e->pin; + scale = e->scale; + offset = e->offset; + } + } + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("pin"), true)) { + uint32_t raw_sensor_pin = strtoul(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (raw_sensor_pin == 0 || raw_sensor_pin > 16) handle_return(HTML_DATA_OUTOFBOUND); + raw_sensor_pin -= 1; + sensor_index = raw_sensor_pin >> 2; + sensor_pin = raw_sensor_pin & 0b11; + } + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("scale"), true)) { + scale = strtod(tmp_buffer, &end); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + } + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("offset"), true)) { + offset = strtod(tmp_buffer, &end); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + } + + result_sensor = new ADS1115Sensor(interval, min, max, (const char*)&name, unit, flag, os.ads1115_devices, sensor_index, sensor_pin, scale, offset); + break; + } + case SensorType::Weather: { + WeatherAction action = WeatherAction::MAX_VALUE; + + if (sensor_type == original_sensor_type) { + if ((sensor = Sensor::get(sid))) { + WeatherSensor* e = static_cast(sensor); + action = e->action; + } + } + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("action"), true)) { + uint32_t action_raw = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (action_raw >= (uint32_t)WeatherAction::MAX_VALUE) handle_return(HTML_DATA_OUTOFBOUND); + action = static_cast(action_raw); + } + + result_sensor = new WeatherSensor(interval, min, max, (const char*)&name, unit, flag, os.get_sensor_weather_data, action); + + break; + } + default: { + handle_return(HTML_DATA_OUTOFBOUND) + break; + } + } + + os.sensors[sid].interval = interval; + os.sensors[sid].flag = static_cast(flag); + os.sensors[sid].next_update = 0; + os.sensors[sid].value = 0.f; + + if (is_new) { + // Assign a new UUID for this sensor + uint16_t new_uuid = os.nvdata.last_sensor_uuid + 1; + if (new_uuid == SENSOR_UUID_NONE) new_uuid = 1; + os.nvdata.last_sensor_uuid = new_uuid; + os.nvdata_save(); + result_sensor->uuid = new_uuid; + + if (!Sensor::add(result_sensor)) { + delete result_sensor; + handle_return(HTML_DATA_OUTOFBOUND); + } + } else { + result_sensor->uuid = os.sensors[sid].uuid; + if (!Sensor::modify(sid, result_sensor)) { + delete result_sensor; + handle_return(HTML_DATA_OUTOFBOUND); + } + } + + delete result_sensor; + + handle_return(HTML_SUCCESS); +} + +/** + * Delete a sensor + * Command: /dsn?pw=xxx&[uuid=xxx|sid=xxx] + * + * pw: password + * uuid: sensor stable ID (1-65535; -1 to delete all sensors) + * sid: sensor positional index (0-based; -1 to delete all sensors) + * (uuid takes precedence if both are provided) + */ +void server_delete_sensor(OTF_PARAMS_DEF) { + if(!process_password(OTF_PARAMS)) return; + + long idx = -1; + bool delete_all = false; + char *end; + + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("uuid"), true)) { + long uuid_param = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (uuid_param == -1) { + delete_all = true; + } else { + if (uuid_param < 1 || uuid_param > 0xFFFF) handle_return(HTML_DATA_OUTOFBOUND); + idx = Sensor::find_index((uint16_t)uuid_param); + if (idx >= os.nsensors) handle_return(HTML_DATA_OUTOFBOUND); + } + } else if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("sid"), true)) { + long sid_param = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (sid_param == -1) { + delete_all = true; + } else { + if (sid_param < 0 || sid_param >= os.nsensors) handle_return(HTML_DATA_OUTOFBOUND); + idx = sid_param; + } + } else { + handle_return(HTML_DATA_MISSING); + } + + if (delete_all) { + // Delete all sensors + os.nsensors = 0; + Sensor::save_count(); + for (uint8_t i = 0; i < MAX_SENSORS; i++) { + os.sensors[i].interval = 0; + os.sensors[i].uuid = 0; + } + } else { + if (!Sensor::del((uint8_t)idx)) handle_return(HTML_INTERNAL_ERROR); + } + + handle_return(HTML_SUCCESS); +} + +uint8_t write_buf_log(uint32_t num, char *buf) { + if (num) { + uint8_t index = 0; + while (num > 0) { + buf[index++] = (num%10) + '0'; + num /= 10; + } + + return index; + } else { + buf[0] = '0'; + return 1; + } +} + +/** + * Get sensor logs + * Command: /jsl?pw=xxx&[uuid=xxx|sid=xxx]&count=xxx&before=xxx&after=xxx&cursor=xxx&fmt=xxx + * + * pw: password + * uuid: sensor stable ID (1-65535; -1 for all) + * sid: sensor positional index (0-based; -1 for all) + * (uuid takes precedence if both are provided) + * count: max records to return + * before: timestamp before which records are returned + * after: timestamp after which records are returned + * cursor: number of records to skip + * fmt: output format: json (default), csv, binary + * json: [[uuid,ts,value],...] — JSON array of arrays + * csv: uuid,timestamp,value\n with header row; downloads as sensor_log.csv + * binary: packed SensorLogRecord structs (uint32 ts, float val, uint16 uuid) + */ +void server_json_sensor_log(OTF_PARAMS_DEF) { + if(!process_password(OTF_PARAMS)) return; + begin_response(res); + + char *end; + + // Read central header + os_file_type hfile = open_sensor_log_header(FileOpenMode::Read); + if (!hfile) handle_return(HTML_INTERNAL_ERROR); + SensorLogHeader hdr; + file_read(hfile, &hdr, sizeof(hdr)); + file_close(hfile); + if (hdr.magic != SENSOR_LOG_MAGIC || hdr.version != SENSOR_LOG_VERSION) + handle_return(HTML_INTERNAL_ERROR); + + uint32_t total_capacity = (uint32_t)hdr.max_files * hdr.records_per_file; + + uint32_t max_count = 100; + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("count"), true)) { + if (strcmp(tmp_buffer, "max") == 0 || strcmp(tmp_buffer, "all") == 0) { + max_count = total_capacity; + } else { + max_count = strtoul(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (max_count == 0) handle_return(HTML_DATA_OUTOFBOUND); + if (max_count > total_capacity) max_count = total_capacity; + } + } + + // cursor = flat sequential index from oldest record to skip before emitting + uint32_t cursor = 0; + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("cursor"), true)) { + cursor = strtoul(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (cursor > total_capacity) handle_return(HTML_DATA_OUTOFBOUND); + } + + using std::numeric_limits; + time_os_t before = numeric_limits::max(); + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("before"), true)) { + before = (time_os_t)strtoul(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (before == 0) handle_return(HTML_DATA_OUTOFBOUND); + } + + time_os_t after = 0; + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("after"), true)) { + after = (time_os_t)strtoul(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (after >= before) handle_return(HTML_DATA_OUTOFBOUND); + } + + long target_uuid = -1; + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("uuid"), true)) { + target_uuid = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (target_uuid != -1 && (target_uuid < 1 || target_uuid > 0xFFFF)) handle_return(HTML_DATA_OUTOFBOUND); + } else if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("sid"), true)) { + long sid_param = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (sid_param == -1) { + target_uuid = -1; + } else { + if (sid_param < 0 || sid_param >= os.nsensors) handle_return(HTML_DATA_OUTOFBOUND); + target_uuid = os.sensors[sid_param].uuid; + } + } + + enum LogFmt { FMT_JSON, FMT_CSV, FMT_BINARY } logfmt = FMT_JSON; + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("fmt"), true)) { + if (strcmp(tmp_buffer, "csv") == 0) logfmt = FMT_CSV; + else if (strcmp(tmp_buffer, "binary") == 0) logfmt = FMT_BINARY; + else if (strcmp(tmp_buffer, "json") != 0) handle_return(HTML_DATA_FORMATERROR); + } + + ContentType ct = (logfmt == FMT_BINARY) ? CT_BINARY : (logfmt == FMT_CSV) ? CT_CSV : CT_JSON; + print_header(OTF_PARAMS, ct); + if (logfmt == FMT_CSV) + res.writeHeader(F("Content-Disposition"), F("attachment; filename=\"sensor_log.csv\"")); + res.writeBodyData("", 0); + + if (logfmt == FMT_JSON) res.write("[", 1); + if (logfmt == FMT_CSV) res.write("uuid,timestamp,value\n", 21); + + // Iterate files from oldest to newest + uint16_t first_file = hdr.wrapped ? (uint16_t)((hdr.cur_file + 1) % hdr.max_files) : 0; + uint16_t total_files = hdr.wrapped ? hdr.max_files : (uint16_t)(hdr.cur_file + 1); + + SensorLogRecord rec; + uint32_t flat_idx = 0; + uint32_t count = 0; + + for (uint16_t fi = 0; fi < total_files && count < max_count; fi++) { + uint16_t file_no = (first_file + fi) % hdr.max_files; + os_file_type dfile = open_sensor_log(file_no, FileOpenMode::Read); + if (!dfile) continue; + + while (count < max_count) { + if (file_read(dfile, &rec, sizeof(rec)) != (int)sizeof(rec)) break; + + flat_idx++; + if (flat_idx <= cursor) continue; + if (rec.timestamp == 0) continue; + if (target_uuid > -1 && rec.uuid != (uint16_t)target_uuid) continue; + if (rec.timestamp > before || rec.timestamp < after) continue; + + char rec_buf[40]; + int rec_len; + switch (logfmt) { + case FMT_JSON: + rec_len = snprintf(rec_buf, sizeof(rec_buf), "%s[%u,%u,%g]", + count == 0 ? "" : ",", rec.uuid, rec.timestamp, rec.value); + res.write(rec_buf, rec_len); + break; + case FMT_CSV: + rec_len = snprintf(rec_buf, sizeof(rec_buf), "%u,%u,%g\n", + rec.uuid, rec.timestamp, rec.value); + res.write(rec_buf, rec_len); + break; + case FMT_BINARY: + res.write((const char*)&rec, sizeof(rec)); + break; + } + count++; + } + file_close(dfile); + } + + if (logfmt == FMT_JSON) res.write("]", 1); + + handle_return(HTML_OK); +} + + +void server_delete_sensor_log(OTF_PARAMS_DEF) { + if(!process_password(OTF_PARAMS)) return; + + long uuid = -1; + char *end; + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("uuid"), true)) { + uuid = strtol(tmp_buffer, &end, 10); + if (*end != '\0') handle_return(HTML_DATA_FORMATERROR); + if (uuid != -1 && (uuid < 1 || uuid > 0xFFFF)) handle_return(HTML_DATA_OUTOFBOUND); + } else { + handle_return(HTML_DATA_MISSING); + } + + if (uuid == -1) { + // Remove all log files — frees flash immediately; header recreated on next log_sensor call + remove_sensor_log(); + handle_return(HTML_SUCCESS); + } + + // Per-sensor clear: read central header to know file layout + os_file_type hfile = open_sensor_log_header(FileOpenMode::Read); + if (!hfile) handle_return(HTML_INTERNAL_ERROR); + SensorLogHeader hdr; + file_read(hfile, &hdr, sizeof(hdr)); + file_close(hfile); + if (hdr.magic != SENSOR_LOG_MAGIC || hdr.version != SENSOR_LOG_VERSION) + handle_return(HTML_INTERNAL_ERROR); + + uint16_t first_file = hdr.wrapped ? (uint16_t)((hdr.cur_file + 1) % hdr.max_files) : 0; + uint16_t total_files = hdr.wrapped ? hdr.max_files : (uint16_t)(hdr.cur_file + 1); + + SensorLogRecord rec; + for (uint16_t fi = 0; fi < total_files; fi++) { + uint16_t file_no = (first_file + fi) % hdr.max_files; + os_file_type dfile = open_sensor_log(file_no, FileOpenMode::ReadWrite); + if (!dfile) continue; + + uint32_t pos = 0; + while (true) { + if (file_read(dfile, &rec, sizeof(rec)) != (int)sizeof(rec)) break; + if (rec.timestamp != 0 && rec.uuid == (uint16_t)uuid) { + memset(&rec, 0, sizeof(rec)); + file_seek(dfile, pos); + file_write(dfile, &rec, sizeof(rec)); + } + pos += sizeof(rec); + } + file_close(dfile); + } + + handle_return(HTML_SUCCESS); +} + +template +void bfill_enum_values(const char *name) { + static_assert(std::is_enum::value, "T must be an enum type"); + + bool needs_comma = false; + + bfill.emit_p(PSTR("\"$S\":["), name); + + for (size_t i = 0; i < static_cast(T::MAX_VALUE); ++i) { + if (needs_comma) { + bfill.emit_p(PSTR(",")); + needs_comma = false; + } + + const char* str = enum_string(static_cast(i)); + if (str) { + bfill.emit_p(PSTR("\"$S\""), str); + needs_comma = true; + } + } + + bfill.emit_p(PSTR("]")); +} + +void server_json_sensor_description_main(OTF_PARAMS_DEF) { + bfill.emit_p(PSTR("\"sensors\":[")); + for (uint8_t i = 0; i < static_cast(SensorType::MAX_VALUE); i++) { + if (i) bfill.emit_p(PSTR(",")); + switch (static_cast(i)) { + case SensorType::Aggregate: + AggregateSensor::emit_description_json(&bfill); + break; + case SensorType::ADS1115: + ADS1115Sensor::emit_description_json(&bfill); + break; + case SensorType::Weather: + WeatherSensor::emit_description_json(&bfill); + break; + case SensorType::MAX_VALUE: + break; + } + } + + bfill.emit_p(PSTR("],\"units\":[")); + for (uint8_t i = 0; i < static_cast(SensorUnit::MAX_VALUE); i++) { + if (i) bfill.emit_p(PSTR(",")); + SensorUnit unit = static_cast(i); + bfill.emit_p(PSTR("{\"name\":\"$S\",\"short\":\"$S\",\"group\":$D,\"index\":$D,\"value\":$D}"), get_sensor_unit_name(unit), get_sensor_unit_short(unit), get_sensor_unit_group(unit), get_sensor_unit_index(unit), i); + } + + bfill.emit_p(PSTR("],\"enums\":{")); + bfill_enum_values(PSTR("SensorUnitGroup")); + bfill.emit_p(PSTR(",")); + bfill_enum_values(PSTR("AggregateAction")); + bfill.emit_p(PSTR(",")); + bfill_enum_values(PSTR("WeatherAction")); + bfill.emit_p(PSTR("}")); + + bfill.emit_p(PSTR( + ",\"args\":[" + "{\"name\":\"Name\",\"arg\":\"name\",\"type\":\"string::[1,32]\",\"default\":\"" SENSOR_DEFAULT_NAME "\"}," + "{\"name\":\"Read Interval\",\"arg\":\"interval\",\"type\":\"int::[1,any]\",\"default\":\"" SENSOR_DEFAULT_STR(SENSOR_DEFAULT_INTERVAL) "\"}," + )); + bfill.emit_p(PSTR("{\"name\":\"Unit\",\"arg\":\"unit\",\"type\":\"unit\",\"default\":\"$D\"},"), static_cast(SENSOR_DEFAULT_UNIT)); + bfill.emit_p(PSTR( + "{\"name\":\"Min. Value\",\"arg\":\"min\",\"type\":\"float\",\"default\":\"" SENSOR_DEFAULT_STR(SENSOR_DEFAULT_MIN) "\"}," + "{\"name\":\"Max. Value\",\"arg\":\"max\",\"type\":\"float\",\"default\":\"" SENSOR_DEFAULT_STR(SENSOR_DEFAULT_MAX) "\"}," + "{\"name\":\"Type\",\"arg\":\"type\",\"type\":\"type\",\"default\":\"" SENSOR_DEFAULT_STR(SENSOR_DEFAULT_TYPE) "\"}" + "]" + )); + + static_assert(SENSOR_FLAG_COUNT == 3); // If this fails, update the flags array below + bfill.emit_p(PSTR(",\"flags\":[{\"name\":\"Enabled\",\"default\":$D},{\"name\":\"Logging\",\"default\":$D},{\"name\":\"Show on Home\",\"default\":$D}]"), + (SENSOR_DEFAULT_FLAG >> SENSOR_FLAG_ENABLE) & 1, + (SENSOR_DEFAULT_FLAG >> SENSOR_FLAG_LOG) & 1, + (SENSOR_DEFAULT_FLAG >> SENSOR_FLAG_SHOW) & 1); + + bfill.emit_p(PSTR("}")); +} + +void server_json_sensor_desc(OTF_PARAMS_DEF) +{ + if(!process_password(OTF_PARAMS)) return; + begin_response(res); + print_header(OTF_PARAMS); + + bfill.emit_p(PSTR("{")); + server_json_sensor_description_main(OTF_PARAMS); + handle_return(HTML_OK); +} + /** Output all JSON data, including jc, jp, jo, js, jn */ void server_json_all(OTF_PARAMS_DEF) { -#if defined(USE_OTF) if(!process_password(OTF_PARAMS,true)) return; - rewind_ether_buffer(); + begin_response(res); print_header(OTF_PARAMS); -#else - print_header(); -#endif + bfill.emit_p(PSTR("{\"settings\":{")); server_json_controller_main(OTF_PARAMS); - send_packet(OTF_PARAMS); bfill.emit_p(PSTR(",\"programs\":{")); server_json_programs_main(OTF_PARAMS); - send_packet(OTF_PARAMS); bfill.emit_p(PSTR(",\"options\":{")); server_json_options_main(); - send_packet(OTF_PARAMS); bfill.emit_p(PSTR(",\"status\":{")); server_json_status_main(); - send_packet(OTF_PARAMS); bfill.emit_p(PSTR(",\"stations\":{")); server_json_stations_main(OTF_PARAMS); + bfill.emit_p(PSTR(",\"sensors\":{")); + server_json_sensors_main(OTF_PARAMS); + //bfill.emit_p(PSTR(",\"sensor_desc\":{")); + //server_json_sensor_description_main(OTF_PARAMS); bfill.emit_p(PSTR("}")); handle_return(HTML_OK); } -#if defined(ARDUINO) +#if defined(ESP8266) -#if defined(OS_AVR) -static int freeHeap () { - extern int __heap_start, *__brkval; - int v; - return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); -} -#endif #else #include -static unsigned long freeHeap() { +static uint32_t freeHeap() { //return sysconf(_SC_AVPHYS_PAGES) * sysconf(_SC_PAGESIZE); struct sysinfo info; if (sysinfo(&info) == 0) { @@ -2127,12 +2566,9 @@ static unsigned long freeHeap() { #endif void server_json_debug(OTF_PARAMS_DEF) { -#if defined(USE_OTF) - rewind_ether_buffer(); + begin_response(res); print_header(OTF_PARAMS); -#else - print_header(); -#endif + bfill.emit_p(PSTR("{\"date\":\"$S\",\"time\":\"$S\",\"heap\":$L"), __DATE__, __TIME__, #if defined(ESP8266) ESP.getFreeHeap()); @@ -2168,6 +2604,62 @@ void server_json_debug(OTF_PARAMS_DEF) { handle_return(HTML_OK); } +/** + * List all files + * Command: /lf?pw=xxx + * + * pw: password + * Returns a JSON array of [filename, size] + */ +#if defined(ESP8266) +void server_list_files(OTF_PARAMS_DEF) { + if(!process_password(OTF_PARAMS)) return; + begin_response(res); + print_header(OTF_PARAMS); + + bfill.emit_p(PSTR("{\"files\":[")); + bool first = true; + + // root + Dir dir = LittleFS.openDir("/"); + while (dir.next()) { + if (dir.fileName().indexOf('.') < 0) continue; + if (!first) bfill.emit_p(PSTR(",")); + bfill.emit_p(PSTR("[\"/$S\",$D]"), dir.fileName().c_str(), dir.fileSize()); + first = false; + } + // logs + dir = LittleFS.openDir("/logs/"); + while (dir.next()) { + if (!first) bfill.emit_p(PSTR(",")); + bfill.emit_p(PSTR("[\"/logs/$S\",$D]"), dir.fileName().c_str(), dir.fileSize()); + first = false; + } + + bfill.emit_p(PSTR("]}")); + handle_return(HTML_OK); +} +#endif + +/** + * Delete a file + * Command: /df?pw=xxx&fn=filename + * + * pw: password + * fn: filename to delete + */ +#if defined(ESP8266) +void server_delete_file(OTF_PARAMS_DEF) { + if(!process_password(OTF_PARAMS)) return; + + if (!findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("fn"), true)) + handle_return(HTML_DATA_MISSING); + + remove_file(tmp_buffer); + handle_return(HTML_SUCCESS); +} +#endif + /* // fill ESP8266 flash with some dummy files void server_fill_files(OTF_PARAMS_DEF) { @@ -2175,7 +2667,7 @@ void server_fill_files(OTF_PARAMS_DEF) { ether_buffer[75] = 0; FSInfo fs_info; for(int index=1;index<64;index++) { - snprintf(tmp_buffer, TMP_BUFFER_SIZE*2 , "%d", index); + snprintf(tmp_buffer, TMP_BUFFER_ALLOC_SIZE , "%d", index); make_logfile_name(tmp_buffer); DEBUG_PRINT(F("creating ")); DEBUG_PRINT(tmp_buffer); @@ -2193,39 +2685,46 @@ void server_fill_files(OTF_PARAMS_DEF) { typedef void (*URLHandler)(OTF_PARAMS_DEF); /* Server function urls - * To save RAM space, each GET command keyword is exactly - * 2 characters long, with no ending 0 * The order must exactly match the order of the * handler functions below */ -const char _url_keys[] PROGMEM = - "cv" - "jc" - "dp" - "cp" - "cr" - "mp" - "up" - "jp" - "co" - "jo" - "sp" - "js" - "cm" - "cs" - "jn" - "je" - "jl" - "dl" - "su" - "cu" - "ja" - "pq" - "db" -#if defined(ARDUINO) - //"ff" -#endif - ; + +const char *uris[] PROGMEM = { + "cv", + "jc", + "dp", + "cp", + "cr", + "mp", + "up", + "jp", + "jpa", + "co", + "jo", + "sp", + "js", + "cm", + "cs", + "jn", + "je", + "jl", + "dl", + "su", + "cu", + "ja", + "pq", + "db", +#if defined(ESP8266) + "lf", + "df", +#endif + "jsn", + "csn", + "dsn", + "jsl", + "dsl", + "jsd", +}; // Server function handlers URLHandler urls[] = { @@ -2237,6 +2736,7 @@ URLHandler urls[] = { server_manual_program, // mp server_moveup_program, // up server_json_programs, // jp + server_json_program_adj,// jpa server_change_options, // co server_json_options, // jo server_change_password, // sp @@ -2252,9 +2752,16 @@ URLHandler urls[] = { server_json_all, // ja server_pause_queue, // pq server_json_debug, // db -#if defined(ARDUINO) - //server_fill_files, -#endif +#if defined(ESP8266) + server_list_files, // lf + server_delete_file, // df +#endif + server_json_sensors, // jsn + server_change_sensor, // csn + server_delete_sensor, // dsn + server_json_sensor_log, // jsl + server_delete_sensor_log, // dsl + server_json_sensor_desc, // jsd }; // handle Ethernet request @@ -2337,14 +2844,14 @@ void start_server_client() { update_server->on("/update", HTTP_POST, on_firmware_upload_fin, on_firmware_upload); update_server->on("/update", HTTP_OPTIONS, on_update_options); + char uri_buf[10] = {0}; + uri_buf[0] = '/'; + // set up all other handlers - char uri[4]; - uri[0]='/'; - uri[3]=0; for(unsigned char i=0;ion(uri, urls[i]); + strncpy_P(uri_buf+1, uris[i], 9); + uri_buf[9] = 0; + otf->on(uri_buf, urls[i]); } callback_initialized = true; } @@ -2368,14 +2875,14 @@ void start_server_ap() { otf->onMissingPage(on_ap_home); update_server->begin(); + char uri_buf[10] = {0}; + uri_buf[0] = '/'; + // set up all other handlers - char uri[4]; - uri[0]='/'; - uri[3]=0; for(unsigned char i=0;ion(uri, urls[i]); + strncpy(uri_buf+1, uris[i], 9); + uri_buf[9] = 0; + otf->on(uri_buf, urls[i]); } os.lcd.setCursor(0, -1); @@ -2387,7 +2894,7 @@ void start_server_ap() { #endif -#if defined(USE_OTF) && !defined(ARDUINO) +#if !defined(ESP8266) void initialize_otf() { if(!otf) return; static bool callback_initialized = false; @@ -2396,112 +2903,26 @@ void initialize_otf() { otf->on("/", server_home); // handle home page otf->on("/index.html", server_home); + char uri_buf[10] = {0}; + uri_buf[0] = '/'; + // set up all other handlers - char uri[4]; - uri[0]='/'; - uri[3]=0; for(unsigned char i=0;ion(uri, urls[i]); + strncpy(uri_buf+1, uris[i], 9); + uri_buf[9] = 0; + otf->on(uri_buf, urls[i]); } callback_initialized = true; } } #endif -#if !defined(USE_OTF) -// This funtion is only used for non-OTF platforms -void handle_web_request(char *p) { - rewind_ether_buffer(); - - // assume this is a GET request - // GET /xx?xxxx - char *com = p+5; - char *dat = com+3; - - if(com[0]==' ') { - server_home(); // home page handler - send_packet(); - m_client->stop(); - } else { - // server funtion handlers - unsigned char i; - for(i=0;istop(); - return; - } - switch(ret) { - case HTML_OK: - break; - case HTML_REDIRECT_HOME: - print_header(false); - bfill.emit_p(PSTR("$F"), htmlReturnHome); - break; - default: - print_header(); - bfill.emit_p(PSTR("{\"result\":$D}"), ret); - } - break; - } - } - - if(i==sizeof(urls)/sizeof(URLHandler)) { - // no server funtion found - print_header(); - bfill.emit_p(PSTR("{\"result\":$D}"), HTML_PAGE_NOT_FOUND); - } - send_packet(); - m_client->stop(); - } -} -#endif - -#if defined(ARDUINO) +#if defined(ESP8266) #define NTP_NTRIES 10 /** NTP sync request */ -#if defined(ESP8266) // due to lwip not supporting UDP, we have to use configTime and time() functions // othewise, using UDP is much faster for NTP sync -ulong getNtpTime() { +uint32_t getNtpTime() { static bool configured = false; static char customAddress[16]; if(!configured) { @@ -2523,7 +2944,7 @@ ulong getNtpTime() { configured = true; } unsigned char tries = 0; - ulong gt = 0; + uint32_t gt = 0; while(tries1577836800UL) break; @@ -2533,104 +2954,4 @@ ulong getNtpTime() { } return gt; } -#else // AVR -ulong getNtpTime() { - - // only proceed if we are connected - if(!os.network_connected()) return 0; - - uint16_t port = (uint16_t)(os.iopts[IOPT_HTTPPORT_1]<<8) + (uint16_t)os.iopts[IOPT_HTTPPORT_0]; - port = (port==8000) ? 8888:8000; // use a different port than http port - EthernetUDP udp; - - #define NTP_PACKET_SIZE 48 - #define NTP_PORT 123 - #define N_PUBLIC_SERVERS 5 - - static const char* public_ntp_servers[] = { - "time.google.com", - "time.nist.gov", - "time.windows.com", - "time.cloudflare.com", - "pool.ntp.org" }; - static uint8_t sidx = 0; - - static unsigned char packetBuffer[NTP_PACKET_SIZE]; - unsigned char ntpip[4] = { - os.iopts[IOPT_NTP_IP1], - os.iopts[IOPT_NTP_IP2], - os.iopts[IOPT_NTP_IP3], - os.iopts[IOPT_NTP_IP4]}; - unsigned char tries=0; - ulong startt = millis(); - while(tries1577836800UL) { - udp.stop(); - DEBUG_PRINT(F("took ")); - DEBUG_PRINT(millis()-startt); - DEBUG_PRINTLN(F("ms")); - return gt; - } - } - } - tries++; - udp.stop(); - sidx=(sidx+1)%N_PUBLIC_SERVERS; - } - if(tries==NTP_NTRIES) {DEBUG_PRINTLN(F("NTP failed!!"));} - udp.stop(); - return 0; -} -#endif #endif diff --git a/opensprinkler_server.h b/opensprinkler_server.h index 1635236c4..930abe415 100644 --- a/opensprinkler_server.h +++ b/opensprinkler_server.h @@ -21,84 +21,13 @@ * . */ -#ifndef _OPENSPRINKLER_SERVER_H -#define _OPENSPRINKLER_SERVER_H +#pragma once -#if !defined(ARDUINO) +#include "types.h" +#include +#include "bfiller.h" + +#if !defined(ESP8266) #include #include #endif - -char dec2hexchar(unsigned char dec); - -class BufferFiller { - char *start; //!< Pointer to start of buffer - char *ptr; //!< Pointer to cursor position - size_t len; -public: - BufferFiller () {} - BufferFiller (char *buf, size_t buffer_len) { - start = buf; - ptr = buf; - len = buffer_len; - } - - char* buffer () const { return start; } - size_t length () const { return len; } - unsigned int position () const { return ptr - start; } - - void emit_p(PGM_P fmt, ...) { - va_list ap; - va_start(ap, fmt); - for (;;) { - char c = pgm_read_byte(fmt++); - if (c == 0) - break; - if (c != '$') { - *ptr++ = c; - continue; - } - c = pgm_read_byte(fmt++); - switch (c) { - case 'D': - // itoa(va_arg(ap, int), (char*) ptr, 10); // ray - snprintf((char*) ptr, len - position(), "%d", va_arg(ap, int)); - break; - case 'L': - // ultoa(va_arg(ap, uint32_t), (char*) ptr, 10); - snprintf((char*) ptr, len - position(), "%lu", (unsigned long) va_arg(ap, uint32_t)); - break; - case 'S': - strcpy((char*) ptr, va_arg(ap, const char*)); - break; - case 'X': { - char d = va_arg(ap, int); - *ptr++ = dec2hexchar((d >> 4) & 0x0F); - *ptr++ = dec2hexchar(d & 0x0F); - } - continue; - case 'F': { - PGM_P s = va_arg(ap, PGM_P); - char d; - while ((d = pgm_read_byte(s++)) != 0) - *ptr++ = d; - continue; - } - case 'O': { - uint16_t oid = va_arg(ap, int); - file_read_block(SOPTS_FILENAME, (char*) ptr, oid*MAX_SOPTS_SIZE, MAX_SOPTS_SIZE); - } - break; - default: - *ptr++ = c; - continue; - } - ptr += strlen((char*) ptr); - } - *(ptr)=0; - va_end(ap); - } -}; - - -#endif // _OPENSPRINKLER_SERVER_H diff --git a/platformio.ini b/platformio.ini index a14fcd484..02fbd0598 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,11 +19,12 @@ framework = arduino extra_scripts = pre:run_prebuild.py lib_ldf_mode = deep lib_deps = - https://github.com/ThingPulse/esp8266-oled-ssd1306/archive/4.2.0.zip - knolleary/PubSubClient @ ^2.8 - https://github.com/OpenThingsIO/OpenThings-Framework-Firmware-Library @ ^0.2.0 + https://github.com/ThingPulse/esp8266-oled-ssd1306/archive/4.2.0.zip + knolleary/PubSubClient @ ^2.8 + ;./external/OpenThings-Framework-Firmware-Library + https://github.com/OpenThingsIO/OpenThings-Framework-Firmware-Library @ ^0.2.0 ; ignore html2raw.cpp source file for firmware compilation (external helper program) -build_src_filter = +<*> - -- +build_src_filter = +<*> + - -- upload_speed = 460800 monitor_speed = 115200 board_build.flash_mode = dio @@ -33,23 +34,6 @@ board_build.f_flash = 80000000L build_flags = -DARP_TABLE_SIZE=40 ;build_flags = -DENABLE_DEBUG -[env:os23_atmega1284p] -platform = atmelavr -board = ATmega1284P -board_build.f_cpu = 16000000L -board_build.variant = sanguino -framework = arduino -lib_ldf_mode = deep -lib_deps = - https://github.com/UIPEthernet/UIPEthernet/archive/refs/tags/v2.0.12.zip - knolleary/PubSubClient @ ^2.8 - https://github.com/greiman/SdFat/archive/refs/tags/1.0.7.zip - Wire -build_src_filter = +<*> - -- -monitor_speed=115200 -upload_protocol = arduino -upload_speed = 115200 - ; The following env is for syntax highlighting only, ; it is NOT for building the firmware for Linux. ; To build the firmware for Linux, please follow: diff --git a/program.cpp b/program.cpp index fce3e1d7b..8868eff0e 100644 --- a/program.cpp +++ b/program.cpp @@ -101,14 +101,15 @@ void ProgramData::eraseall() { void ProgramData::read(unsigned char pid, ProgramStruct *buf) { if (pid >= nprograms) return; // first unsigned char is program counter, so 1+ - file_read_block(PROG_FILENAME, buf, 1+(ulong)pid*PROGRAMSTRUCT_SIZE, PROGRAMSTRUCT_SIZE); + file_read_block(PROG_FILENAME, buf, 1+(uint32_t)pid*PROGRAMSTRUCT_SIZE, PROGRAMSTRUCT_SIZE); } /** Add a program */ -unsigned char ProgramData::add(ProgramStruct *buf) { +unsigned char ProgramData::add(ProgramStruct *buf, SensorAdjustment *adj) { if (nprograms >= MAX_NUM_PROGRAMS) return 0; - file_write_block(PROG_FILENAME, buf, 1+(ulong)nprograms*PROGRAMSTRUCT_SIZE, PROGRAMSTRUCT_SIZE); - nprograms ++; + file_write_block(PROG_FILENAME, buf, 1+(uint32_t)nprograms*PROGRAMSTRUCT_SIZE, PROGRAMSTRUCT_SIZE); + if (adj) SensorAdjustment::write(adj, nprograms); + nprograms++; save_count(); return 1; } @@ -117,16 +118,22 @@ unsigned char ProgramData::add(ProgramStruct *buf) { void ProgramData::moveup(unsigned char pid) { if(pid >= nprograms || pid == 0) return; // swap program pid-1 and pid - ulong pos = 1+(ulong)(pid-1)*PROGRAMSTRUCT_SIZE; - ulong next = pos+PROGRAMSTRUCT_SIZE; + uint32_t pos = 1+(uint32_t)(pid-1)*PROGRAMSTRUCT_SIZE; + uint32_t next = pos+PROGRAMSTRUCT_SIZE; char buf2[PROGRAMSTRUCT_SIZE]; file_read_block(PROG_FILENAME, tmp_buffer, pos, PROGRAMSTRUCT_SIZE); file_read_block(PROG_FILENAME, buf2, next, PROGRAMSTRUCT_SIZE); file_write_block(PROG_FILENAME, tmp_buffer, next, PROGRAMSTRUCT_SIZE); file_write_block(PROG_FILENAME, buf2, pos, PROGRAMSTRUCT_SIZE); + // swap senadj entries for pid-1 and pid to match the program swap + char buf3[SENSOR_ADJUSTMENT_SIZE]; + file_read_block(SENADJ_FILENAME, tmp_buffer, SENSOR_ADJUSTMENT_SIZE * (pid-1), SENSOR_ADJUSTMENT_SIZE); + file_read_block(SENADJ_FILENAME, buf3, SENSOR_ADJUSTMENT_SIZE * pid, SENSOR_ADJUSTMENT_SIZE); + file_write_block(SENADJ_FILENAME, buf3, SENSOR_ADJUSTMENT_SIZE * (pid-1), SENSOR_ADJUSTMENT_SIZE); + file_write_block(SENADJ_FILENAME, tmp_buffer, SENSOR_ADJUSTMENT_SIZE * pid, SENSOR_ADJUSTMENT_SIZE); } -void ProgramData::toggle_pause(ulong delay) { +void ProgramData::toggle_pause(uint32_t delay) { if (os.status.pause_state) { // was paused resume_stations(); } else { @@ -178,10 +185,11 @@ void ProgramData::clear_pause() { } /** Modify a program */ -unsigned char ProgramData::modify(unsigned char pid, ProgramStruct *buf) { +unsigned char ProgramData::modify(unsigned char pid, ProgramStruct *buf, SensorAdjustment *adj) { if (pid >= nprograms) return 0; - ulong pos = 1+(ulong)pid*PROGRAMSTRUCT_SIZE; + uint32_t pos = 1+(uint32_t)pid*PROGRAMSTRUCT_SIZE; file_write_block(PROG_FILENAME, buf, pos, PROGRAMSTRUCT_SIZE); + if (adj) SensorAdjustment::write(adj, pid); return 1; } @@ -189,11 +197,14 @@ unsigned char ProgramData::modify(unsigned char pid, ProgramStruct *buf) { unsigned char ProgramData::del(unsigned char pid) { if (pid >= nprograms) return 0; if (nprograms == 0) return 0; - ulong pos = 1+(ulong)(pid+1)*PROGRAMSTRUCT_SIZE; + uint32_t pos = 1+(uint32_t)(pid+1)*PROGRAMSTRUCT_SIZE; // erase by copying backward - for (; pos < 1+(ulong)nprograms*PROGRAMSTRUCT_SIZE; pos+=PROGRAMSTRUCT_SIZE) { + for (; pos < 1+(uint32_t)nprograms*PROGRAMSTRUCT_SIZE; pos+=PROGRAMSTRUCT_SIZE) { file_copy_block(PROG_FILENAME, pos, pos-PROGRAMSTRUCT_SIZE, PROGRAMSTRUCT_SIZE, tmp_buffer); } + for (int i = pid; i < nprograms-1; i++) { + file_copy_block(SENADJ_FILENAME, SENSOR_ADJUSTMENT_SIZE * (i+1), SENSOR_ADJUSTMENT_SIZE * i, SENSOR_ADJUSTMENT_SIZE, tmp_buffer); + } nprograms --; save_count(); return 1; @@ -202,10 +213,10 @@ unsigned char ProgramData::del(unsigned char pid) { // set the enable bit unsigned char ProgramData::set_flagbit(unsigned char pid, unsigned char bid, unsigned char value) { if (pid >= nprograms) return 0; - unsigned char flag = file_read_byte(PROG_FILENAME, 1+(ulong)pid*PROGRAMSTRUCT_SIZE); + unsigned char flag = file_read_byte(PROG_FILENAME, 1+(uint32_t)pid*PROGRAMSTRUCT_SIZE); if(value) flag|=(1<tm_wday+1)%7; // tm_wday ranges from [0,6] with Sunday being 0 unsigned char day_t = ti->tm_mday; unsigned char month_t = ti->tm_mon+1; // tm_mon ranges from [0,11] @@ -490,14 +501,12 @@ void ProgramStruct::gen_station_runorder(uint16_t runcount, unsigned char *order void ProgramData::drem_to_relative(unsigned char days[2]) { unsigned char rem_abs=days[0]; unsigned char inv=days[1]; - // todo future: use now_tz()? - days[0] = (unsigned char)((rem_abs + inv - (os.now_tz()/SECS_PER_DAY) % inv) % inv); +days[0] = (unsigned char)((rem_abs + inv - (os.now_tz()/SECS_PER_DAY) % inv) % inv); } // relative remainder -> absolute remainder void ProgramData::drem_to_absolute(unsigned char days[2]) { unsigned char rem_rel=days[0]; unsigned char inv=days[1]; - // todo future: use now_tz()? days[0] = (unsigned char)(((os.now_tz()/SECS_PER_DAY) + rem_rel) % inv); } diff --git a/program.h b/program.h index 1da1d6a53..3e46716a7 100644 --- a/program.h +++ b/program.h @@ -21,9 +21,7 @@ * . */ - -#ifndef _PROGRAM_H -#define _PROGRAM_H +#pragma once #define MAX_NUM_PROGRAMS 40 // maximum number of programs #define MAX_NUM_STARTTIMES 4 @@ -33,6 +31,8 @@ #include "OpenSprinkler.h" #include "types.h" +class SensorAdjustment; // forward declaration to avoid pulling sensors/sensor.h into this header + /** Log data structure */ struct LogStruct { unsigned char station; @@ -79,7 +79,7 @@ class ProgramStruct { unsigned char en_daterange:1; // weekly: days[0][0..6] correspond to Monday till Sunday - // single-run:days[0] and [1] store the epoch time in days of the start + // single-run:days[0] and [1] store the epoch time in days of the start // monthly: days[0][0..5] stores the day of the month (32 means last day of month) // interval: days[1] stores the interval (0 to 255), days[0] stores the starting day remainder (0 to 254) unsigned char days[2]; @@ -134,7 +134,7 @@ class ProgramData { static LogStruct lastrun; static time_os_t last_seq_stop_times[]; // the last stop time of a sequential station (for each sequential group respectively) - static void toggle_pause(ulong delay); + static void toggle_pause(uint32_t delay); static void set_pause(); static void resume_stations(); static void clear_pause(); @@ -146,8 +146,8 @@ class ProgramData { static void init(); static void eraseall(); static void read(unsigned char pid, ProgramStruct *buf); - static unsigned char add(ProgramStruct *buf); - static unsigned char modify(unsigned char pid, ProgramStruct *buf); + static unsigned char add(ProgramStruct *buf, SensorAdjustment *adj = nullptr); + static unsigned char modify(unsigned char pid, ProgramStruct *buf, SensorAdjustment *adj = nullptr); static unsigned char set_flagbit(unsigned char pid, unsigned char bid, unsigned char value); static void moveup(unsigned char pid); static unsigned char del(unsigned char pid); @@ -157,5 +157,3 @@ class ProgramData { static void load_count(); static void save_count(); }; - -#endif // _PROGRAM_H diff --git a/rpitime.h b/rpitime.h index 49ff07e8f..ae6178949 100644 --- a/rpitime.h +++ b/rpitime.h @@ -1,5 +1,5 @@ -#ifndef RPI_TIME_H -#define RPI_TIME_H +#pragma once +#if !defined(ARDUINO) #include diff --git a/sensors/ads1115_sensor.cpp b/sensors/ads1115_sensor.cpp new file mode 100644 index 000000000..22ea0c11b --- /dev/null +++ b/sensors/ads1115_sensor.cpp @@ -0,0 +1,78 @@ +#include "ads1115_sensor.h" + +ADS1115Sensor::ADS1115Sensor(uint32_t interval, float min, float max, const char* name, SensorUnit unit, uint8_t flag, ADS1115** sensors, uint8_t sensor_index, uint8_t pin, float scale, float offset) : + Sensor(interval, min, max, name, unit, flag), + sensor_index(sensor_index), + pin(pin), + scale(scale), + offset(offset), + sensors(sensors) {} + +void ADS1115Sensor::emit_extra_json(BufferFiller *bfill) { + bfill->emit_p(PSTR("{\"pin\":$D,\"scale\":$E,\"offset\":$E}"), + ((this->sensor_index << 2) + this->pin + 1), + this->scale, + this->offset); +} + +void ADS1115Sensor::emit_description_json(BufferFiller* bfill) { + bfill->emit_p(PSTR( +#if defined(ESP8266) || defined(OSPI) + "{\"name\":\"ADS1115 Sensor\"," +#else + "{\"name\":\"ADS1115 Sensor (simulated)\"," +#endif + "\"args\":[" + "{\"name\":\"Pin Number\"," + "\"arg\":\"pin\"," + "\"type\":\"int::[1,16]\"," + "\"default\":\"1\"}," + "{\"name\":\"Linear Scale\"," + "\"arg\":\"scale\"," + "\"type\":\"float\"," + "\"default\":\"" SENSOR_DEFAULT_STR(ADS1115_DEFAULT_SCALE) "\"}," + "{\"name\":\"Value Offset\"," + "\"arg\":\"offset\"," + "\"type\":\"float\"," + "\"default\":\"" SENSOR_DEFAULT_STR(ADS1115_DEFAULT_OFFSET) "\"}" + "]}" + )); +} + + +float ADS1115Sensor::_get_raw_value() { + if (this->sensors[sensor_index] == nullptr) { + return NAN; + } + int16_t counts = this->sensors[sensor_index]->get_pin_value(this->pin); + if (counts < 0) return NAN; + return (float)counts * ADS1115_VOLTS_PER_COUNT * this->scale + this->offset; +} + +uint32_t ADS1115Sensor::_serialize_internal(char *buf) { + uint32_t i = 0; + buf[i++] = static_cast(this->sensor_index); + buf[i++] = static_cast(this->pin); + i += write_buf(buf + i, this->scale); + i += write_buf(buf + i, this->offset); + return i; +} + +ADS1115Sensor::ADS1115Sensor(ADS1115 **sensors, char *buf, uint32_t len) { + uint8_t subclass_len = 0; + uint32_t i = Sensor::_deserialize(buf, len, &subclass_len); + uint32_t end = i + subclass_len; + + this->sensor_index = 0; + this->pin = 0; + this->scale = ADS1115_DEFAULT_SCALE; + this->offset = ADS1115_DEFAULT_OFFSET; + + if (i + 1 <= end) this->sensor_index = static_cast(buf[i]); + i++; + if (i + 1 <= end) this->pin = static_cast(buf[i]); + i++; + read_buf(buf, &i, end, this->scale); + read_buf(buf, &i, end, this->offset); + this->sensors = sensors; +} diff --git a/sensors/ads1115_sensor.h b/sensors/ads1115_sensor.h new file mode 100644 index 000000000..c6992eaee --- /dev/null +++ b/sensors/ads1115_sensor.h @@ -0,0 +1,33 @@ +#pragma once + +#include "../defines.h" +#include "sensor.h" +#include "../ads1115.h" + +#define ADS1115_DEFAULT_SCALE 1 +#define ADS1115_DEFAULT_OFFSET 0 +#define ADS1115_VOLTS_PER_COUNT (ADS1115_SCALE_FACTOR / 1000.0f) + +class ADS1115Sensor : public Sensor { + public: + ADS1115Sensor(uint32_t interval, float min, float max, const char *name, SensorUnit unit, uint8_t flag, ADS1115 **sensors, uint8_t sensor_index, uint8_t pin, float scale, float offset); + ADS1115Sensor(ADS1115 **sensors, char *buf, uint32_t len); + + void emit_extra_json(BufferFiller *bfill); + static void emit_description_json(BufferFiller *bfill); + + SensorType get_sensor_type() { + return SensorType::ADS1115; + } + + uint8_t sensor_index; + uint8_t pin; + float scale; + float offset; + + private: + float _get_raw_value(); + uint32_t _serialize_internal(char *buf); + + ADS1115 **sensors; +}; diff --git a/sensors/aggregate_sensor.cpp b/sensors/aggregate_sensor.cpp new file mode 100644 index 000000000..ac67fe9de --- /dev/null +++ b/sensors/aggregate_sensor.cpp @@ -0,0 +1,135 @@ +#include "aggregate_sensor.h" +#include "../OpenSprinkler.h" + +extern OpenSprinkler os; + +AggregateSensor::AggregateSensor(uint32_t interval, float min, float max, const char* name, SensorUnit unit, uint8_t flag, sensor_memory_t* sensors, aggregate_children_t* children, uint8_t children_count, AggregateAction action) : + Sensor(interval, min, max, name, unit, flag), + action(action), + sensors(sensors) { + for (size_t i = 0; i < AGGREGATE_SENSOR_CHILDREN_COUNT; i++) { + if (i < children_count) { + this->children[i] = children[i]; + } else { + this->children[i] = aggregate_children_t{ AGGREGATE_CHILD_DEFAULT_SCALE, AGGREGATE_CHILD_DEFAULT_OFFSET, SENSOR_UUID_NONE }; + } + } +} + +void AggregateSensor::emit_extra_json(BufferFiller* bfill) { + bfill->emit_p(PSTR("{\"action\":$D,\"children\":["), this->action); + for (size_t i = 0; i < AGGREGATE_SENSOR_CHILDREN_COUNT; i++) { + if (i) bfill->emit_p(PSTR(",")); + aggregate_children_t* child = &this->children[i]; + bfill->emit_p(PSTR("{\"uuid\":$D,\"scale\":$E,\"offset\":$E}"), child->uuid, child->scale, child->offset); + } + bfill->emit_p(PSTR("]}")); +} + +void AggregateSensor::emit_description_json(BufferFiller* bfill) { + bfill->emit_p(PSTR( + "{\"name\":\"Aggregate Sensor\"," + "\"args\":[" + "{\"name\":\"Child Sensors\"," + "\"arg\":\"children\"," + "\"type\":\"array::" SENSOR_DEFAULT_STR(AGGREGATE_SENSOR_CHILDREN_COUNT) "\"," + "\"extra\":[" + "{\"name\":\"Sensor UUID\",\"arg\":\"uuid\",\"type\":\"sensor\",\"default\":\"" SENSOR_DEFAULT_STR(SENSOR_UUID_NONE) "\",\"indicator\":true}," + "{\"name\":\"Linear Scale\",\"arg\":\"scale\",\"type\":\"float\",\"default\":\"" SENSOR_DEFAULT_STR(AGGREGATE_CHILD_DEFAULT_SCALE) "\"}," + "{\"name\":\"Value Offset\",\"arg\":\"offset\",\"type\":\"float\",\"default\":\"" SENSOR_DEFAULT_STR(AGGREGATE_CHILD_DEFAULT_OFFSET) "\"}" + "]}," + "{\"name\":\"Aggregate Action\"," + "\"arg\":\"action\"," + "\"type\":\"enum::AggregateAction\"," + "\"default\":\"" SENSOR_DEFAULT_STR(AGGREGATE_DEFAULT_ACTION) "\"}" + "]}" + )); +} + + +float AggregateSensor::_get_raw_value() { + float values[AGGREGATE_SENSOR_CHILDREN_COUNT]; + uint8_t count = 0; + + for (size_t i = 0; i < AGGREGATE_SENSOR_CHILDREN_COUNT; i++) { + if (this->children[i].uuid == SENSOR_UUID_NONE) continue; + uint8_t idx = Sensor::find_index(this->children[i].uuid); + if (idx >= OpenSprinkler::nsensors || !sensors[idx].interval) continue; + if (!(sensors[idx].status & SENSOR_STATUS_VALID)) continue; + + values[count++] = sensors[idx].value * this->children[i].scale + this->children[i].offset; + } + + if (count == 0) return NAN; + + switch (this->action) { + case AggregateAction::Min: { + float result = values[0]; + for (uint8_t i = 1; i < count; i++) if (values[i] < result) result = values[i]; + return result; + } + case AggregateAction::Max: { + float result = values[0]; + for (uint8_t i = 1; i < count; i++) if (values[i] > result) result = values[i]; + return result; + } + case AggregateAction::Average: { + float sum = 0; + for (uint8_t i = 0; i < count; i++) sum += values[i]; + return sum / count; + } + case AggregateAction::Sum: { + float sum = 0; + for (uint8_t i = 0; i < count; i++) sum += values[i]; + return sum; + } + case AggregateAction::Median: { + // Insertion sort (up to AGGREGATE_SENSOR_CHILDREN_COUNT elements) + for (uint8_t i = 1; i < count; i++) { + float key = values[i]; + int8_t j = i - 1; + while (j >= 0 && values[j] > key) { values[j + 1] = values[j]; j--; } + values[j + 1] = key; + } + if (count % 2 == 1) return values[count / 2]; + return (values[count / 2 - 1] + values[count / 2]) / 2.0f; + } + case AggregateAction::Range: { + float lo = values[0], hi = values[0]; + for (uint8_t i = 1; i < count; i++) { + if (values[i] < lo) lo = values[i]; + if (values[i] > hi) hi = values[i]; + } + return hi - lo; + } + default: + return NAN; + } +} + +uint32_t AggregateSensor::_serialize_internal(char* buf) { + uint32_t i = 0; + for (size_t j = 0; j < AGGREGATE_SENSOR_CHILDREN_COUNT; j++) { + i += write_buf(buf + i, this->children[j].scale); + i += write_buf(buf + i, this->children[j].offset); + i += write_buf(buf + i, this->children[j].uuid); + } + buf[i++] = static_cast(this->action); + return i; +} + +AggregateSensor::AggregateSensor(sensor_memory_t* sensors, char* buf, uint32_t len) { + uint8_t subclass_len = 0; + uint32_t i = Sensor::_deserialize(buf, len, &subclass_len); + uint32_t end = i + subclass_len; + + for (size_t j = 0; j < AGGREGATE_SENSOR_CHILDREN_COUNT; j++) { + this->children[j] = aggregate_children_t{ AGGREGATE_CHILD_DEFAULT_SCALE, AGGREGATE_CHILD_DEFAULT_OFFSET, SENSOR_UUID_NONE }; + read_buf(buf, &i, end, this->children[j].scale); + read_buf(buf, &i, end, this->children[j].offset); + read_buf(buf, &i, end, this->children[j].uuid); + } + this->action = static_cast(AGGREGATE_DEFAULT_ACTION); + if (i + 1 <= end) this->action = static_cast(buf[i]); + this->sensors = sensors; +} diff --git a/sensors/aggregate_sensor.h b/sensors/aggregate_sensor.h new file mode 100644 index 000000000..8f4dbfc97 --- /dev/null +++ b/sensors/aggregate_sensor.h @@ -0,0 +1,37 @@ +#pragma once + +#include "sensor.h" + +typedef struct { + float scale; // multiplier applied to child value + float offset; // added after scale + uint16_t uuid; // UUID of child sensor (SENSOR_UUID_NONE = unused slot) +} aggregate_children_t; + +#define AGGREGATE_SENSOR_CHILDREN_COUNT 8 + +#define AGGREGATE_CHILD_DEFAULT_SCALE 1 +#define AGGREGATE_CHILD_DEFAULT_OFFSET 0 +#define AGGREGATE_DEFAULT_ACTION 2 // AggregateAction::Average + +class AggregateSensor : public Sensor { + public: + AggregateSensor(uint32_t interval, float min, float max, const char *name, SensorUnit unit, uint8_t flag, sensor_memory_t *sensors, aggregate_children_t *children, uint8_t children_count, AggregateAction action); + AggregateSensor(sensor_memory_t *sensors, char *buf, uint32_t len); + + void emit_extra_json(BufferFiller *bfill); + static void emit_description_json(BufferFiller *bfill); + + SensorType get_sensor_type() { + return SensorType::Aggregate; + } + + aggregate_children_t children[AGGREGATE_SENSOR_CHILDREN_COUNT]; + AggregateAction action; + + private: + float _get_raw_value(); + uint32_t _serialize_internal(char *buf); + + sensor_memory_t *sensors; +}; diff --git a/sensors/sensor.cpp b/sensors/sensor.cpp new file mode 100644 index 000000000..685a2a86f --- /dev/null +++ b/sensors/sensor.cpp @@ -0,0 +1,474 @@ +#include "sensor.h" +#include "../OpenSprinkler.h" + +extern OpenSprinkler os; +extern char tmp_buffer[]; + +const char *enum_string(SensorUnitGroup group) { + switch (group) { + #define X(id, name) case SensorUnitGroup::id: return PSTR(name); + SENSOR_UNIT_GROUP_LIST(X) + #undef X + case SensorUnitGroup::MAX_VALUE: return nullptr; + } + return nullptr; +} + +const char *enum_string(AggregateAction action) { + switch (action) { + #define X(id, name) case AggregateAction::id: return PSTR(name); + AGGREGATE_ACTION_LIST(X) + #undef X + case AggregateAction::MAX_VALUE: return nullptr; + } + return nullptr; +} + +const char *enum_string(WeatherAction action) { + switch (action) { + case WeatherAction::MAX_VALUE: return nullptr; + } + return nullptr; +} + +const char* get_sensor_unit_name(SensorUnit unit) { + switch (unit) { + #define X(id, name, sym, group) case SensorUnit::id: return PSTR(name); + SENSOR_UNIT_LIST(X) + #undef X + case SensorUnit::MAX_VALUE: return nullptr; + } + return nullptr; +} + +const char* get_sensor_unit_short(SensorUnit unit) { + switch (unit) { + #define X(id, name, sym, group) case SensorUnit::id: return PSTR(sym); + SENSOR_UNIT_LIST(X) + #undef X + case SensorUnit::MAX_VALUE: return nullptr; + } + return nullptr; +} + +const SensorUnitGroup get_sensor_unit_group(SensorUnit unit) { + switch (unit) { + #define X(id, name, sym, group) case SensorUnit::id: return SensorUnitGroup::group; + SENSOR_UNIT_LIST(X) + #undef X + case SensorUnit::MAX_VALUE: return SensorUnitGroup::MAX_VALUE; + } + return SensorUnitGroup::MAX_VALUE; +} + +const uint32_t get_sensor_unit_index(SensorUnit unit) { + return static_cast(unit); +} + +static bool sensor_has_valid_layout(char *buf, uint32_t slot_size, uint32_t *len_out) { + if ((uint8_t)buf[0] >= (uint8_t)SensorType::MAX_VALUE) return false; + uint32_t len = SENSOR_RECORD_HEADER_LEN + (uint8_t)buf[1] + (uint8_t)buf[2]; + if (len < SENSOR_RECORD_HEADER_LEN || len > slot_size) return false; + if (len_out) *len_out = len; + return true; +} + +Sensor::Sensor(uint32_t interval, float min, float max, const char* name, SensorUnit unit, uint8_t flag) : + interval(interval), min(min), max(max), flag(flag), unit(unit) { + strncpy(this->name, name, SENSOR_NAME_LEN); + this->name[SENSOR_NAME_LEN - 1] = 0; +} + +Sensor::Sensor() {} + +float Sensor::get_new_value(uint8_t *status_out) { + float value = this->_get_raw_value(); + + if (isnan(value)) { + if (status_out) *status_out = SENSOR_STATUS_ERROR; + return value; + } + + uint8_t status = SENSOR_STATUS_VALID; + if (value < this->min) { value = this->min; status |= SENSOR_STATUS_CLAMPED_LOW; } + if (value > this->max) { value = this->max; status |= SENSOR_STATUS_CLAMPED_HIGH; } + if (status_out) *status_out = status; + + return value; +} + +uint32_t Sensor::serialize(char* buf) { + uint32_t i = 0; + + buf[i++] = static_cast(this->get_sensor_type()); + uint32_t common_len_pos = i++; + uint32_t subclass_len_pos = i++; + + uint32_t common_start = i; + memcpy(buf + i, this->name, SENSOR_NAME_LEN); + i += SENSOR_NAME_LEN; + buf[i++] = static_cast(this->unit); + i += write_buf(buf + i, this->interval); + i += write_buf(buf + i, this->flag); + i += write_buf(buf + i, this->min); + i += write_buf(buf + i, this->max); + i += write_buf(buf + i, this->uuid); + buf[common_len_pos] = static_cast(i - common_start); + + uint32_t subclass_start = i; + i += this->_serialize_internal(buf + i); + buf[subclass_len_pos] = static_cast(i - subclass_start); + return i; +} + +uint32_t Sensor::_deserialize(char* buf, uint32_t len, uint8_t *subclass_len) { + uint8_t common_len = static_cast(buf[1]); + *subclass_len = static_cast(buf[2]); + + uint32_t i = SENSOR_RECORD_HEADER_LEN; + uint32_t common_end = i + common_len; + + if (i + SENSOR_NAME_LEN <= common_end) { + memcpy(this->name, buf + i, SENSOR_NAME_LEN); + this->name[SENSOR_NAME_LEN - 1] = 0; + } + i += SENSOR_NAME_LEN; + + if (i + 1 <= common_end) this->unit = static_cast(buf[i]); + i++; + + read_buf(buf, &i, common_end, this->interval); + read_buf(buf, &i, common_end, this->flag); + read_buf(buf, &i, common_end, this->min); + read_buf(buf, &i, common_end, this->max); + read_buf(buf, &i, common_end, this->uuid); + + return common_end; +} + +SensorAdjustment::SensorAdjustment(uint16_t uuid, uint8_t point_count, uint8_t flag, sensor_adjustment_point_t* points) { + this->uuid = uuid; + this->flag = flag; + memset(this->points, 0, sizeof(this->points)); + if (point_count > SENSOR_ADJUSTMENT_POINTS) point_count = SENSOR_ADJUSTMENT_POINTS; + this->point_count = point_count; + for (size_t i = 0; i < point_count; i++) { + this->points[i] = points[i]; + } +} + +float SensorAdjustment::get_adjustment_factor(sensor_memory_t* sensors) { + if (this->uuid != SENSOR_UUID_NONE && (this->flag & (1 << SENADJ_FLAG_ENABLE))) { + uint8_t idx = Sensor::find_index(this->uuid); + if (idx < MAX_SENSORS && sensors[idx].interval) { + if (this->point_count == 0) return 1.f; + float value = sensors[idx].value; + // duplicate x values form a step: x == T maps to the rightmost point at T + if (value < this->points[0].x) return this->points[0].y; + if (value >= this->points[this->point_count - 1].x) return this->points[this->point_count - 1].y; + + uint8_t i = 0; + while (i + 1 < this->point_count - 1 && value >= this->points[i + 1].x) i++; + + sensor_adjustment_point_t left = this->points[i]; + sensor_adjustment_point_t right = this->points[i + 1]; + + if (right.x == left.x) return right.y; + + value = (value - left.x) / (right.x - left.x) * (right.y - left.y) + left.y; + + if (value < 0) value = 0; + return value; + } + } + return 1.f; +} + +SensorAdjustment *SensorAdjustment::read(uint8_t index, uint8_t nprograms) { + static SensorAdjustment result(SENSOR_UUID_NONE, 0, 0, nullptr); + + if (index >= nprograms) return nullptr; + os_file_type file = file_open(SENADJ_FILENAME, FileOpenMode::Read); + if (file) { + uint32_t pos = (uint32_t)index * SENSOR_ADJUSTMENT_SIZE; + file_seek(file, pos, FileSeekMode::Set); + bool ok = (file_read(file, &result, SENSOR_ADJUSTMENT_SIZE) == (int)SENSOR_ADJUSTMENT_SIZE); + file_close(file); + if (ok && result.uuid != SENSOR_UUID_NONE) { + return &result; + } + } + return nullptr; +} + +void SensorAdjustment::write(SensorAdjustment *adj, uint8_t index) { + uint32_t pos = (uint32_t)SENSOR_ADJUSTMENT_SIZE * index; + + os_file_type file = file_open(SENADJ_FILENAME, FileOpenMode::ReadWrite); + if (file) { + SensorAdjustment disabled(SENSOR_UUID_NONE, 0, 0, nullptr); + + uint32_t cur_size = file_size(file); + if (cur_size < pos) { + file_seek(file, 0, FileSeekMode::End); + while (cur_size < pos) { + file_write(file, &disabled, SENSOR_ADJUSTMENT_SIZE); + cur_size += SENSOR_ADJUSTMENT_SIZE; + } + } + + file_seek(file, pos, FileSeekMode::Set); + SensorAdjustment *to_write = adj ? adj : &disabled; + file_write(file, to_write, SENSOR_ADJUSTMENT_SIZE); + + file_close(file); + } else { + DEBUG_PRINT("Failed to open file: "); + DEBUG_PRINTLN(SENADJ_FILENAME); + } +} + +// --------------------------------------------------------------------------- +// Sensor file I/O +// --------------------------------------------------------------------------- + +#include "aggregate_sensor.h" +#include "weather_sensor.h" +#include "ads1115_sensor.h" + +static void sensor_memory_init(sensor_memory_t &m, Sensor *sensor) { + m.interval = sensor->interval; + m.flag = static_cast(sensor->flag); + m.uuid = sensor->uuid; + m.next_update = 0; + m.value = 0.f; + m.status = 0; +} + +Sensor *Sensor::parse(os_file_type file) { + static uint8_t sensor_scratchpad[sizeof(OpenSprinkler::SensorUnion)] __attribute__((aligned(4))); + static Sensor *active_sensor = nullptr; + + if (active_sensor != nullptr) { + active_sensor->~Sensor(); + active_sensor = nullptr; + } + const uint32_t slot_size = TMP_BUFFER_SIZE; + file_read(file, tmp_buffer, slot_size); + + uint32_t len = 0; + if (!sensor_has_valid_layout((char*)tmp_buffer, slot_size, &len)) return nullptr; + + SensorType sensor_type = static_cast(tmp_buffer[0]); + + switch (sensor_type) { + case SensorType::Aggregate: + active_sensor = new (sensor_scratchpad) AggregateSensor(os.sensors, (char*)tmp_buffer, len); + break; + case SensorType::ADS1115: + active_sensor = new (sensor_scratchpad) ADS1115Sensor(os.ads1115_devices, (char*)tmp_buffer, len); + break; + case SensorType::Weather: + active_sensor = new (sensor_scratchpad) WeatherSensor(os.get_sensor_weather_data, (char*)tmp_buffer, len); + break; + default: + return nullptr; + } + return active_sensor; +} + +Sensor *Sensor::get(uint8_t index) { + const uint32_t slot_size = TMP_BUFFER_SIZE; + uint32_t pos = 1 + slot_size * index; + + os_file_type file = file_open(SENSORS_FILENAME, FileOpenMode::Read); + if (file) { + file_seek(file, pos, FileSeekMode::Set); + Sensor *result = Sensor::parse(file); + file_close(file); + return result; + } else { + DEBUG_PRINT("Failed to open file: "); + DEBUG_PRINTLN(SENSORS_FILENAME); + return nullptr; + } +} + +void Sensor::write(Sensor *sensor, uint8_t index) { + const uint32_t slot_size = TMP_BUFFER_SIZE; + uint32_t pos = 1 + slot_size * index; + + memset(tmp_buffer, 0, slot_size); + if (sensor) sensor->serialize(tmp_buffer); + + os_file_type file = file_open(SENSORS_FILENAME, FileOpenMode::ReadWrite); + if (file) { + file_seek(file, pos, FileSeekMode::Set); + file_write(file, tmp_buffer, slot_size); + file_close(file); + } else { + DEBUG_PRINT("Failed to open file: "); + DEBUG_PRINTLN(SENSORS_FILENAME); + } +} + +void Sensor::load_count() { + OpenSprinkler::nsensors = file_read_byte(SENSORS_FILENAME, 0); +} + +void Sensor::save_count() { + file_write_byte(SENSORS_FILENAME, 0, OpenSprinkler::nsensors); +} + +unsigned char Sensor::add(Sensor *sensor) { + if (OpenSprinkler::nsensors >= MAX_SENSORS) return 0; + + Sensor::write(sensor, OpenSprinkler::nsensors); + sensor_memory_init(OpenSprinkler::sensors[OpenSprinkler::nsensors], sensor); + + OpenSprinkler::nsensors++; + Sensor::save_count(); + return 1; +} + +unsigned char Sensor::modify(uint8_t index, Sensor *sensor) { + if (index >= OpenSprinkler::nsensors) return 0; + Sensor::write(sensor, index); + sensor_memory_init(OpenSprinkler::sensors[index], sensor); + return 1; +} + +unsigned char Sensor::del(uint8_t index) { + if (index >= OpenSprinkler::nsensors) return 0; + if (OpenSprinkler::nsensors == 0) return 0; + + const uint32_t slot_size = TMP_BUFFER_SIZE; + // erase by copying backward + for (uint8_t i = index; i < OpenSprinkler::nsensors - 1; i++) { + file_copy_block(SENSORS_FILENAME, 1 + (uint32_t)(i + 1) * slot_size, 1 + (uint32_t)i * slot_size, slot_size, tmp_buffer); + // also shift in-memory state + OpenSprinkler::sensors[i] = OpenSprinkler::sensors[i + 1]; + } + + OpenSprinkler::nsensors--; + OpenSprinkler::sensors[OpenSprinkler::nsensors].interval = 0; + OpenSprinkler::sensors[OpenSprinkler::nsensors].uuid = 0; + + Sensor::save_count(); + return 1; +} + +void Sensor::load_all() { + if (!file_exists(SENSORS_FILENAME)) { + DEBUG_PRINTLN(F("Sensor files missing. Initializing...")); + OpenSprinkler::nsensors = 0; + Sensor::save_count(); + } else { + Sensor::load_count(); + } + + Sensor *sensor; + os_file_type file = file_open(SENSORS_FILENAME, FileOpenMode::Read); + if (file) { + file_seek(file, 1, FileSeekMode::Set); + for (size_t i = 0; i < OpenSprinkler::nsensors; i++) { + if ((sensor = Sensor::parse(file))) + sensor_memory_init(OpenSprinkler::sensors[i], sensor); + } + + file_close(file); + } else { + DEBUG_PRINT("Failed to open file: "); + DEBUG_PRINTLN(SENSORS_FILENAME); + } +} + +uint8_t Sensor::find_index(uint16_t uuid) { + if (uuid == SENSOR_UUID_NONE) return OpenSprinkler::nsensors; + for (uint8_t i = 0; i < OpenSprinkler::nsensors; i++) { + if (OpenSprinkler::sensors[i].uuid == uuid) return i; + } + return MAX_SENSORS; +} + +void Sensor::test_log(uint32_t n_records) { + remove_sensor_log(); + + DEBUG_PRINTF("sensor log test: writing %lu records\n", (unsigned long)n_records); + uint32_t t0 = millis(); + + for (uint32_t i = 0; i < n_records; i++) { + if (OpenSprinkler::nsensors > 0) os.log_sensor((uint8_t)((i + 1) % OpenSprinkler::nsensors), (float)i / 1000.f); + } + + uint32_t write_ms = millis() - t0; + DEBUG_PRINTF("sensor log write: %lu ms total, %.2f ms/record\n", + (unsigned long)write_ms, + n_records ? (float)write_ms / n_records : 0.f); + + os_file_type hfile = open_sensor_log_header(FileOpenMode::Read); + if (!hfile) { + DEBUG_PRINTLN("sensor log test: cannot open header"); + return; + } + SensorLogHeader hdr = {}; + file_read(hfile, &hdr, sizeof(hdr)); + file_close(hfile); + if (hdr.magic != SENSOR_LOG_MAGIC || hdr.version != SENSOR_LOG_VERSION) { + DEBUG_PRINTLN("sensor log test: bad header"); + return; + } + + uint16_t first_file = hdr.wrapped ? (uint16_t)((hdr.cur_file + 1) % hdr.max_files) : 0; + uint16_t total_files = hdr.wrapped ? hdr.max_files : (uint16_t)(hdr.cur_file + 1); + DEBUG_PRINTF("sensor log state: max_files=%u records_per_file=%u cur_file=%u wrapped=%u total_files=%u\n", + hdr.max_files, hdr.records_per_file, hdr.cur_file, hdr.wrapped, total_files); + + uint32_t tr = millis(); + uint32_t count = 0; + for (uint16_t fi = 0; fi < total_files; fi++) { + uint16_t file_no = (first_file + fi) % hdr.max_files; + os_file_type dfile = open_sensor_log(file_no, FileOpenMode::Read); + if (!dfile) continue; + SensorLogRecord rec; + while (file_read(dfile, &rec, sizeof(rec)) == (int)sizeof(rec)) count++; + file_close(dfile); + } + + uint32_t read_ms = millis() - tr; + DEBUG_PRINTF("sensor log read: %lu records in %lu ms (%.2f ms/record)\n", + (unsigned long)count, (unsigned long)read_ms, + count ? (float)read_ms / count : 0.f); +} + +// --------------------------------------------------------------------------- +// Sensor log file helpers +// --------------------------------------------------------------------------- + +void get_sensor_log_filename(char *buf, uint16_t file_no) { + snprintf(buf, 24, "%s%03u", SENSORS_LOG_FILENAME, file_no % 1000); +} + +os_file_type open_sensor_log(uint16_t file_no, FileOpenMode mode) { + char fname[24]; + get_sensor_log_filename(fname, file_no); + return file_open(fname, mode); +} + +os_file_type open_sensor_log_header(FileOpenMode mode) { + return file_open(SENSORS_LOG_HEADER_FILENAME, mode); +} + +void remove_sensor_log(int16_t file_no) { + char fname[24]; + if (file_no < 0) { + remove_file(SENSORS_LOG_HEADER_FILENAME); + for (uint16_t i = 0; i < SENSOR_LOG_MAX_FILES; i++) { + get_sensor_log_filename(fname, i); + remove_file(fname); + } + } else { + get_sensor_log_filename(fname, (uint16_t)file_no); + remove_file(fname); + } +} diff --git a/sensors/sensor.h b/sensors/sensor.h new file mode 100644 index 000000000..9eb5acb09 --- /dev/null +++ b/sensors/sensor.h @@ -0,0 +1,297 @@ +#pragma once + +// Sensor subsystem. +// +// Sensor is the abstract base for any periodic data source surfaced through +// the firmware's sensor list and adjustment pipeline. Concrete subclasses +// today include analog probes via ADS1115, weather-service inputs, and +// aggregates over other sensors; future ones may wrap GPIO inputs or +// system-internal signals (board temperature, RAM, etc.). + +#include +#include +#include "../utils.h" +#include "../defines.h" +#include "../bfiller.h" + +#define SENSOR_NAME_LEN 33 +#define SENSOR_CUSTOM_UNIT_LEN 9 + +// Serialized sensor records are: type, common length, subclass length, +// common payload, subclass payload. Payloads are append-only. +#define SENSOR_RECORD_HEADER_LEN 3 + +// Current common payload length: +// name[33] + unit[1] + interval[4] + flag[1] + min[4] + max[4] + uuid[2]. +// Future common fields should be appended; new records carry their own common_len byte. +#define SENSOR_COMMON_PAYLOAD_LEN 49 + +#define SENSOR_UUID_NONE 0 // sentinel: "no sensor assigned" (0 = uninitialized/disabled) + +// New-sensor defaults — single source of truth for both server_change_sensor and /jsd +#define SENSOR_DEFAULT_NAME "New Sensor" +#define SENSOR_DEFAULT_INTERVAL 15 +#define SENSOR_DEFAULT_UNIT SensorUnit::Volt +#define SENSOR_DEFAULT_MIN 0 +#define SENSOR_DEFAULT_MAX 5 +#define SENSOR_DEFAULT_TYPE 1 // SensorType::ADS1115 +#define SENSOR_DEFAULT_FLAG (1 << SENSOR_FLAG_ENABLE) + +// Two-level stringify so macro values expand before quoting (for use inside PSTR()) +#define _SENSOR_DEFAULT_STR(x) #x +#define SENSOR_DEFAULT_STR(x) _SENSOR_DEFAULT_STR(x) + +typedef struct { + uint32_t interval; + uint32_t next_update; + float value; + uint16_t uuid; // stable sensor identifier (0 = empty slot) + uint8_t flag; // persistent config bits — synced from sens.dat on load + uint8_t status; // runtime state bits — never persisted, cleared on load +} sensor_memory_t; // 16 bytes + +// Runtime status bits (sensor_memory_t::status) — never persisted +#define SENSOR_STATUS_VALID (1 << 0) // has had at least one successful read +#define SENSOR_STATUS_ERROR (1 << 1) // last read attempt failed (hardware fault) +#define SENSOR_STATUS_STALE (1 << 2) // update window passed but read could not complete +#define SENSOR_STATUS_CLAMPED_HIGH (1 << 3) // last value was clamped to max +#define SENSOR_STATUS_CLAMPED_LOW (1 << 4) // last value was clamped to min + +// Sensor log file format — both structs are tightly packed (no padding) so +// sizeof() gives the exact on-disk byte count and offsetof() gives exact field offsets. +struct __attribute__((packed)) SensorLogHeader { + uint8_t magic; // SENSOR_LOG_MAGIC + uint8_t version; // SENSOR_LOG_VERSION + uint16_t max_files; // number of data files in rotation + uint16_t records_per_file; // max records per data file + uint16_t cur_file; // index of the data file currently being written + uint8_t wrapped; // 1 once all max_files slots have been used at least once + uint8_t reserved[7]; +}; // 16 bytes + +struct __attribute__((packed)) SensorLogRecord { + uint32_t timestamp; // unix epoch seconds + float value; // sensor reading + uint16_t uuid; // sensor UUID (replaces sid+reserved, same 10-byte size) +}; + +enum class SensorType : uint8_t { + Aggregate = 0, + ADS1115, + Weather, + MAX_VALUE, +}; + +// X(id, display_name) +#define SENSOR_UNIT_GROUP_LIST(X) \ + X(None, "No Unit") \ + X(Energy, "Energy") \ + X(Flow, "Flow") \ + X(Pressure, "Pressure") \ + X(Temperature, "Temperature") \ + X(Light, "Light") \ + X(Length, "Length") \ + X(Velocity, "Velocity") \ + X(Volume, "Volume") \ + X(Salinity, "Salinity") \ + X(Angle, "Angle") \ + X(Precipitation, "Precipitation") + +// X(id, display_name, short_symbol, group_id) +#define SENSOR_UNIT_LIST(X) \ + X(None, "None", " ", None) \ + X(Percent, "Percent", "%", None) \ + X(PartsPerMillion, "Parts Per Million", "ppm", None) \ + X(Millivolt, "Millivolt", "mV", Energy) \ + X(Volt, "Volt", "V", Energy) \ + X(Milliampere, "Milliampere", "mA", Energy) \ + X(Ampere, "Ampere", "A", Energy) \ + X(Ohm, "Ohm", "Ω", Energy) \ + X(Milliohm, "Milliohm", "mΩ", Energy) \ + X(Kiloohm, "Kiloohm", "kΩ", Energy) \ + X(DielectricConstant,"Dielectric Constant"," ", Energy) \ + X(LitersPerSecond, "Liters Per Second", "L/s", Flow) \ + X(GallonsPerSecond, "Gallons Per Second", "gal/s", Flow) \ + X(Kilopascal, "Kilopascal", "kPa", Pressure) \ + X(Bar, "Bar", "bar", Pressure) \ + X(Pascal, "Pascal", "Pa", Pressure) \ + X(Torr, "Torr", "torr", Pressure) \ + X(Celsius, "Celsius", "°C", Temperature) \ + X(Fahrenheit, "Fahrenheit", "°F", Temperature) \ + X(Kelvin, "Kelvin", "K", Temperature) \ + X(Lux, "Lux", "lx", Light) \ + X(Lumen, "Lumen", "lm", Light) \ + X(Millimeter, "Millimeter", "mm", Length) \ + X(Centimeter, "Centimeter", "cm", Length) \ + X(Meter, "Meter", "m", Length) \ + X(Kilometer, "Kilometer", "km", Length) \ + X(Inch, "Inch", "in", Length) \ + X(Foot, "Foot", "ft", Length) \ + X(Mile, "Mile", "mi", Length) \ + X(MetersPerSecond, "Meters Per Second", "m/s", Velocity) \ + X(KilometersPerHour, "Kilometers Per Hour","km/h", Velocity) \ + X(MilesPerHour, "Miles Per Hour", "mph", Velocity) \ + X(Milliliter, "Milliliter", "mL", Volume) \ + X(Liter, "Liter", "L", Volume) \ + X(CubicMeter, "Cubic Meter", "m³", Volume) \ + X(Gallon, "Gallon", "gal", Volume) \ + X(CubicFoot, "Cubic Foot", "ft³", Volume) \ + X(LitersPerMinute, "Liters Per Minute", "L/min", Flow) \ + X(GallonsPerMinute, "Gallons Per Minute", "gpm", Flow) \ + X(PoundsPerSquareInch, "Pounds Per Square Inch", "psi", Pressure) \ + X(VolumetricMoistureContent, "Volumetric Moisture Content", "%VMC", None) \ + X(Watt, "Watt", "W", Energy) \ + X(Milliwatt, "Milliwatt", "mW", Energy) \ + X(WattHour, "Watt Hour", "Wh", Energy) \ + X(KilowattHour, "Kilowatt Hour", "kWh", Energy) \ + X(MicrosiemensPerCentimeter, "Microsiemens Per Centimeter", "uS/cm", Salinity) \ + X(MillisiemensPerCentimeter, "Millisiemens Per Centimeter", "mS/cm", Salinity) \ + X(Ph, "pH", "pH", Salinity) \ + X(Degree, "Degree", "deg", Angle) \ + X(Radian, "Radian", "rad", Angle) \ + X(MillimetersPerHour, "Millimeters Per Hour", "mm/h", Precipitation) \ + X(InchesPerHour, "Inches Per Hour", "in/h", Precipitation) + +enum class SensorUnitGroup : uint8_t { +#define X(id, name) id, + SENSOR_UNIT_GROUP_LIST(X) +#undef X + MAX_VALUE, +}; + +enum class SensorUnit : uint8_t { +#define X(id, name, sym, group) id, + SENSOR_UNIT_LIST(X) +#undef X + MAX_VALUE, +}; + +typedef enum { + SENSOR_FLAG_ENABLE = 0, // sensor is active + SENSOR_FLAG_LOG, // write readings to log file + SENSOR_FLAG_SHOW, // show on homepage + SENSOR_FLAG_COUNT +} sensor_flag; + +class Sensor { +public: + Sensor(uint32_t interval, float min, float max, const char *name, SensorUnit unit, uint8_t flag); + Sensor(); + virtual ~Sensor() {} + + float get_new_value(uint8_t *status_out = nullptr); + uint32_t serialize(char *buf); + + static Sensor *parse(os_file_type file); // statically allocated, do not delete + static Sensor *get(uint8_t index); // statically allocated, do not delete + static void write(Sensor *sensor, uint8_t index); + static void load_count(); + static void save_count(); + static unsigned char add(Sensor *sensor); + static unsigned char modify(uint8_t index, Sensor *sensor); // index is positional index + static unsigned char del(uint8_t index); // index is positional index + static void load_all(); + static uint8_t find_index(uint16_t uuid); + static void test_log(uint32_t n_records); + + void virtual emit_extra_json(BufferFiller *bfill) = 0; + + uint32_t interval = 1; + float min = 0.f; + float max = 0.f; + uint8_t flag = 0; + uint16_t uuid = 0; // assigned by write_sensor on creation; 0 = not yet assigned + SensorUnit unit = SensorUnit::None; + char name[SENSOR_NAME_LEN] = {0}; + + SensorType virtual get_sensor_type() = 0; + + private: + float virtual _get_raw_value() = 0; + protected: + uint32_t _deserialize(char *buf, uint32_t len, uint8_t *subclass_len); + uint32_t virtual _serialize_internal(char *buf) = 0; +}; + +// X(id, display_name) +#define AGGREGATE_ACTION_LIST(X) \ + X(Min, "Min") \ + X(Max, "Max") \ + X(Average, "Average") \ + X(Sum, "Sum") \ + X(Median, "Median") \ + X(Range, "Range") + +enum class AggregateAction : uint8_t { +#define X(id, name) id, + AGGREGATE_ACTION_LIST(X) +#undef X + MAX_VALUE, +}; + +typedef Sensor* (*SensorGetter)(uint8_t); + +enum class WeatherAction : uint8_t { + MAX_VALUE, +}; + +typedef float (*WeatherGetter)(WeatherAction); + +typedef struct { + float x; + float y; +} sensor_adjustment_point_t; + +#define SENSOR_ADJUSTMENT_POINTS 8 + +typedef enum { + SENADJ_FLAG_ENABLE = 0, + SENADJ_FLAG_COUNT +} senadj_flag; + +class SensorAdjustment { +public: + SensorAdjustment(uint16_t uuid, uint8_t point_count, uint8_t flag, sensor_adjustment_point_t *points); + + static SensorAdjustment *read(uint8_t index, uint8_t nprograms); // returns statically allocated object, do not delete + static void write(SensorAdjustment *adj, uint8_t index); + + float get_adjustment_factor(sensor_memory_t *sensors); + + uint16_t uuid; // sensor UUID (SENSOR_UUID_NONE = adjustment disabled) + uint8_t point_count; + uint8_t flag; // bit 0 (SENADJ_FLAG_ENABLE): enable sensor adjustment + sensor_adjustment_point_t points[SENSOR_ADJUSTMENT_POINTS]; +}; + +#define SENSOR_ADJUSTMENT_SIZE sizeof(SensorAdjustment) + +// Serialization helpers used by all sensor types +template +inline uint32_t write_buf(char* buf, T val) { + memcpy(buf, &val, sizeof(val)); + return sizeof(val); +} + +template +inline bool read_buf(char* buf, uint32_t* i, uint32_t end, T& out) { + bool ok = (*i + sizeof(T) <= end); + if (ok) memcpy(&out, buf + *i, sizeof(T)); + *i += sizeof(T); + return ok; +} + +const char *enum_string(SensorUnitGroup group); +const char *enum_string(AggregateAction action); +const char *enum_string(WeatherAction action); + +const char* get_sensor_unit_name(SensorUnit unit); +const char* get_sensor_unit_short(SensorUnit unit); +const SensorUnitGroup get_sensor_unit_group(SensorUnit unit); +const uint32_t get_sensor_unit_index(SensorUnit unit); + +// Sensor log file helpers +void get_sensor_log_filename(char *buf, uint16_t file_no); +os_file_type open_sensor_log(uint16_t file_no, FileOpenMode mode); +os_file_type open_sensor_log_header(FileOpenMode mode); +void remove_sensor_log(int16_t file_no = -1); // -1 removes header + all data files diff --git a/sensors/weather_sensor.cpp b/sensors/weather_sensor.cpp new file mode 100644 index 000000000..8f0d9abe9 --- /dev/null +++ b/sensors/weather_sensor.cpp @@ -0,0 +1,44 @@ +#include "weather_sensor.h" + +WeatherSensor::WeatherSensor(uint32_t interval, float min, float max, const char* name, SensorUnit unit, uint8_t flag, WeatherGetter weather_getter, WeatherAction action) : + Sensor(interval, min, max, name, unit, flag), + action(action), + weather_getter(weather_getter) { +} + +void WeatherSensor::emit_extra_json(BufferFiller* bfill) { + bfill->emit_p(PSTR("{\"action\":$D}"), this->action); +} + +void WeatherSensor::emit_description_json(BufferFiller* bfill) { + bfill->emit_p(PSTR( + "{\"name\":\"Weather Sensor\"," + "\"args\":[" + "{\"name\":\"Weather Information\"," + "\"arg\":\"action\"," + "\"type\":\"enum::WeatherAction\"," + "\"default\":\"0\"}" + "]}" + )); +} + + +float WeatherSensor::_get_raw_value() { + return this->weather_getter(this->action); +} + +uint32_t WeatherSensor::_serialize_internal(char* buf) { + uint32_t i = 0; + buf[i++] = static_cast(this->action); + return i; +} + +WeatherSensor::WeatherSensor(WeatherGetter weather_getter, char* buf, uint32_t len) { + uint8_t subclass_len = 0; + uint32_t i = Sensor::_deserialize(buf, len, &subclass_len); + uint32_t end = i + subclass_len; + + this->action = WeatherAction::MAX_VALUE; + if (i + 1 <= end) this->action = static_cast(buf[i]); + this->weather_getter = weather_getter; +} diff --git a/sensors/weather_sensor.h b/sensors/weather_sensor.h new file mode 100644 index 000000000..d39cbc26e --- /dev/null +++ b/sensors/weather_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "sensor.h" + +class WeatherSensor : public Sensor { + public: + WeatherSensor(uint32_t interval, float min, float max, const char *name, SensorUnit unit, uint8_t flag, WeatherGetter weather_getter, WeatherAction action); + WeatherSensor(WeatherGetter weather_getter, char *buf, uint32_t len); + + void emit_extra_json(BufferFiller *bfill); + static void emit_description_json(BufferFiller *bfill); + + SensorType get_sensor_type() { + return SensorType::Weather; + } + + WeatherAction action; + + private: + float _get_raw_value(); + uint32_t _serialize_internal(char *buf); + + WeatherGetter weather_getter; +}; diff --git a/types.h b/types.h index d03aee036..f77651727 100644 --- a/types.h +++ b/types.h @@ -1,12 +1,10 @@ -#ifndef TYPES_H -#define TYPES_H +#pragma once + #include -#if defined(ARDUINO) -typedef unsigned long time_os_t; +#if defined(ESP8266) +typedef uint32_t time_os_t; #else #include -typedef time_t time_os_t; +typedef uint32_t time_os_t; #endif - -#endif \ No newline at end of file diff --git a/utils.cpp b/utils.cpp index 18de656b4..2d0fc0958 100644 --- a/utils.cpp +++ b/utils.cpp @@ -26,20 +26,15 @@ #include "OpenSprinkler.h" extern OpenSprinkler os; -#if defined(ARDUINO) // Arduino - - #if defined(ESP8266) - #include - #include - #else - #include - #include "SdFat.h" - extern SdFat sd; - #endif +#if defined(ESP8266) // Arduino + #include + #include #else // RPI/LINUX #include +#include +#include char* get_runtime_path() { static char path[PATH_MAX]; @@ -89,7 +84,7 @@ char* get_filename_fullpath(const char *filename) { return fullpath; } -void delay(ulong howLong) +void delay(uint32_t howLong) { struct timespec sleeper, dummy ; @@ -99,7 +94,7 @@ void delay(ulong howLong) nanosleep (&sleeper, &dummy) ; } -void delayMicroseconds (ulong howLong) +void delayMicroseconds (uint32_t howLong) { struct timespec sleeper ; unsigned int uSecs = howLong % 1000000 ; @@ -117,7 +112,7 @@ void delayMicroseconds (ulong howLong) } } -void delayMicrosecondsHard (ulong howLong) +void delayMicrosecondsHard (uint32_t howLong) { struct timeval tNow, tLong, tEnd ; @@ -141,18 +136,24 @@ void initialiseEpoch() epochMicro = (uint64_t)tv.tv_sec * (uint64_t)1000000 + (uint64_t)(tv.tv_usec) ; } -// ulong millis (void) +// millis() lives in external/OpenThings-Framework-Firmware-Library/Websocket.cpp +// so OTF stays self-contained when used as a standalone library. Reference +// implementation kept here for clarity: +// uint32_t millis (void) // { // struct timeval tv ; // uint64_t now ; - +// // gettimeofday (&tv, NULL) ; // now = (uint64_t)tv.tv_sec * (uint64_t)1000 + (uint64_t)(tv.tv_usec / 1000) ; - -// return (ulong)(now - epochMilli) ; +// +// return (uint32_t)(now - epochMilli) ; // } -ulong micros (void) +// Arduino-compatible: us since initialiseEpoch(), wraps every ~71 minutes. +// Returns uint32_t explicitly so the 32-bit semantics are part of the API on +// every target (matches Arduino, where unsigned long is also 32-bit). +uint32_t micros (void) { struct timeval tv ; uint64_t now ; @@ -160,7 +161,7 @@ ulong micros (void) gettimeofday (&tv, NULL) ; now = (uint64_t)tv.tv_sec * (uint64_t)1000000 + (uint64_t)tv.tv_usec ; - return (ulong)(now - epochMicro) ; + return (uint32_t)(now - epochMicro) ; } #if defined(OSPI) @@ -239,52 +240,52 @@ in_addr_t get_ip_address(char *iface) { #endif bool prefix(const char *pre, const char *str) { - return strncmp(pre, str, strlen(pre)) == 0; + return strncmp(pre, str, strlen(pre)) == 0; } BoardType get_board_type() { - FILE *file = fopen("/proc/device-tree/compatible", "rb"); - if (file == NULL) { - return BoardType::Unknown; - } - - char buffer[100]; - - BoardType res = BoardType::Unknown; - - int total = fread(buffer, 1, sizeof(buffer), file); - - if (prefix("raspberrypi", buffer)) { - res = BoardType::RaspberryPi_Unknown; - const char *cpu_buf = buffer; - size_t index = 0; - - // model and cpu is seperated by a null byte - while (index < (total - 1) && cpu_buf[index]) { - index += 1; - } - - cpu_buf += index + 1; - - if (!strcmp("brcm,bcm2712", cpu_buf)) { - // Pi 5 - res = BoardType::RaspberryPi_bcm2712; - } else if (!strcmp("brcm,bcm2711", cpu_buf)) { - // Pi 4 - res = BoardType::RaspberryPi_bcm2711; - } else if (!strcmp("brcm,bcm2837", cpu_buf)) { - // Pi 3 / Pi Zero 2 - res = BoardType::RaspberryPi_bcm2837; - } else if (!strcmp("brcm,bcm2836", cpu_buf)) { - // Pi 2 - res = BoardType::RaspberryPi_bcm2836; - } else if (!strcmp("brcm,bcm2835", cpu_buf)) { - // Pi / Pi Zero - res = BoardType::RaspberryPi_bcm2835; - } - } - - return res; + FILE *file = fopen("/proc/device-tree/compatible", "rb"); + if (file == NULL) { + return BoardType::Unknown; + } + + char buffer[100]; + + BoardType res = BoardType::Unknown; + + int total = fread(buffer, 1, sizeof(buffer), file); + + if (prefix("raspberrypi", buffer)) { + res = BoardType::RaspberryPi_Unknown; + const char *cpu_buf = buffer; + size_t index = 0; + + // model and cpu is seperated by a null byte + while (index < (total - 1) && cpu_buf[index]) { + index += 1; + } + + cpu_buf += index + 1; + + if (!strcmp("brcm,bcm2712", cpu_buf)) { + // Pi 5 + res = BoardType::RaspberryPi_bcm2712; + } else if (!strcmp("brcm,bcm2711", cpu_buf)) { + // Pi 4 + res = BoardType::RaspberryPi_bcm2711; + } else if (!strcmp("brcm,bcm2837", cpu_buf)) { + // Pi 3 / Pi Zero 2 + res = BoardType::RaspberryPi_bcm2837; + } else if (!strcmp("brcm,bcm2836", cpu_buf)) { + // Pi 2 + res = BoardType::RaspberryPi_bcm2836; + } else if (!strcmp("brcm,bcm2835", cpu_buf)) { + // Pi / Pi Zero + res = BoardType::RaspberryPi_bcm2835; + } + } + + return res; } #endif @@ -296,12 +297,6 @@ void remove_file(const char *fn) { if(!LittleFS.exists(fn)) return; LittleFS.remove(fn); -#elif defined(ARDUINO) - - sd.chdir("/"); - if (!sd.exists(fn)) return; - sd.remove(fn); - #else remove(get_filename_fullpath(fn)); @@ -309,16 +304,21 @@ void remove_file(const char *fn) { #endif } +void ensure_log_dir() { +#if !defined(ESP8266) + const char *dir = get_filename_fullpath(LOG_DIR); + struct stat st; + if (stat(dir, &st) != 0) { + mkdir(dir, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IWGRP|S_IXGRP|S_IROTH|S_IWOTH|S_IXOTH); + } +#endif +} + bool file_exists(const char *fn) { #if defined(ESP8266) return LittleFS.exists(fn); -#elif defined(ARDUINO) - - sd.chdir("/"); - return sd.exists(fn); - #else FILE *file; @@ -329,8 +329,118 @@ bool file_exists(const char *fn) { #endif } +os_file_type file_open(const char *fn, FileOpenMode mode) { + #if defined(ESP8266) + switch (mode) { + default: + case FileOpenMode::Read: + return LittleFS.open(fn, "r"); + case FileOpenMode::ReadWrite: + if (!LittleFS.exists(fn)) { + File f = LittleFS.open(fn, "w"); + if (!f) return f; + f.close(); + } + return LittleFS.open(fn, "r+"); + case FileOpenMode::WriteTruncate: + return LittleFS.open(fn, "w"); + case FileOpenMode::ReadWriteTruncate: + return LittleFS.open(fn, "w+"); + case FileOpenMode::Append: + return LittleFS.open(fn, "a"); + case FileOpenMode::ReadAppend: + return LittleFS.open(fn, "a+"); + } + #else + char *full_file = get_filename_fullpath(fn); + switch (mode) { + default: + case FileOpenMode::Read: + return fopen(full_file, "rb"); + case FileOpenMode::ReadWrite: { + int fd = open(full_file, O_RDWR | O_CREAT, 0644); + if (fd == -1) return nullptr; + return fdopen(fd, "rb+"); + } + case FileOpenMode::WriteTruncate: + return fopen(full_file, "wb"); + case FileOpenMode::ReadWriteTruncate: + return fopen(full_file, "wb+"); + case FileOpenMode::Append: + return fopen(full_file, "ab"); + case FileOpenMode::ReadAppend: + return fopen(full_file, "ab+"); + } + + #endif +} + +void file_close(os_file_type f) { + #if defined(ESP8266) + f.close(); + #else + fclose(f); + #endif +} + +bool file_seek(os_file_type f, uint32_t position, FileSeekMode mode) { + #if defined(ESP8266) + switch (mode) { + case FileSeekMode::Set: + return f.seek(position, fs::SeekMode::SeekSet); + case FileSeekMode::Current: + return f.seek(position, fs::SeekMode::SeekCur); + case FileSeekMode::End: + return f.seek(position, fs::SeekMode::SeekEnd); + } + #else + switch (mode) { + case FileSeekMode::Set: + return fseek(f, position, SEEK_SET); + case FileSeekMode::Current: + return fseek(f, position, SEEK_CUR); + case FileSeekMode::End: + return fseek(f, position, SEEK_END); + } + #endif + + return false; +} + +bool file_seek(os_file_type f, uint32_t position) { + return file_seek(f, position, FileSeekMode::Set); +} + +int file_read(os_file_type f, void *target, uint32_t len) { + #if defined(ESP8266) + return f.read((uint8_t*)target, len); + #else + return fread(target, 1, len, f); + #endif +} + +int file_write(os_file_type f, const void *source, uint32_t len) { + #if defined(ESP8266) + return f.write((const uint8_t*)source, len); + #else + return fwrite(source, 1, len, f); + #endif +} + +uint32_t file_size(os_file_type f) { + #if defined(ESP8266) + return f.size(); + #else + long cur = ftell(f); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, cur, SEEK_SET); + return (uint32_t)(sz >= 0 ? sz : 0); + #endif +} + // file functions -void file_read_block(const char *fn, void *dst, ulong pos, ulong len) { +void file_read_block(const char *fn, void *dst, uint32_t pos, uint32_t len) { #if defined(ESP8266) // do not use File.read_byte or read_byteUntil because it's very slow @@ -341,16 +451,6 @@ void file_read_block(const char *fn, void *dst, ulong pos, ulong len) { f.close(); } -#elif defined(ARDUINO) - - sd.chdir("/"); - SdFile file; - if(file.open(fn, O_READ)) { - file.seekSet(pos); - file.read(dst, len); - file.close(); - } - #else FILE *fp = fopen(get_filename_fullpath(fn), "rb"); @@ -363,7 +463,7 @@ void file_read_block(const char *fn, void *dst, ulong pos, ulong len) { #endif } -void file_write_block(const char *fn, const void *src, ulong pos, ulong len) { +void file_write_block(const char *fn, const void *src, uint32_t pos, uint32_t len) { #if defined(ESP8266) File f = LittleFS.open(fn, "r+"); @@ -374,16 +474,6 @@ void file_write_block(const char *fn, const void *src, ulong pos, ulong len) { f.close(); } -#elif defined(ARDUINO) - - sd.chdir("/"); - SdFile file; - int ret = file.open(fn, O_CREAT | O_RDWR); - if(!ret) return; - file.seekSet(pos); - file.write(src, len); - file.close(); - #else FILE *fp = fopen(get_filename_fullpath(fn), "rb+"); @@ -400,7 +490,7 @@ void file_write_block(const char *fn, const void *src, ulong pos, ulong len) { } -void file_copy_block(const char *fn, ulong from, ulong to, ulong len, void *tmp) { +void file_copy_block(const char *fn, uint32_t from, uint32_t to, uint32_t len, void *tmp) { // assume tmp buffer is provided and is larger than len // todo future: if tmp buffer is not provided, do unsigned char-to-unsigned char copy if(tmp==NULL) { return; } @@ -414,18 +504,6 @@ void file_copy_block(const char *fn, ulong from, ulong to, ulong len, void *tmp) f.write((unsigned char*)tmp, len); f.close(); -#elif defined(ARDUINO) - - sd.chdir("/"); - SdFile file; - int ret = file.open(fn, O_RDWR); - if(!ret) return; - file.seekSet(from); - file.read(tmp, len); - file.seekSet(to); - file.write(tmp, len); - file.close(); - #else FILE *fp = fopen(get_filename_fullpath(fn), "rb+"); @@ -441,7 +519,7 @@ void file_copy_block(const char *fn, ulong from, ulong to, ulong len, void *tmp) } // compare a block of content -unsigned char file_cmp_block(const char *fn, const char *buf, ulong pos) { +unsigned char file_cmp_block(const char *fn, const char *buf, uint32_t pos) { #if defined(ESP8266) File f = LittleFS.open(fn, "r"); @@ -456,21 +534,6 @@ unsigned char file_cmp_block(const char *fn, const char *buf, ulong pos) { return (*buf==c)?0:1; } -#elif defined(ARDUINO) - - sd.chdir("/"); - SdFile file; - if(file.open(fn, O_READ)) { - file.seekSet(pos); - char c = file.read(); - while(*buf && (c==*buf)) { - buf++; - c=file.read(); - } - file.close(); - return (*buf==c)?0:1; - } - #else FILE *fp = fopen(get_filename_fullpath(fn), "rb"); @@ -489,13 +552,13 @@ unsigned char file_cmp_block(const char *fn, const char *buf, ulong pos) { return 1; } -unsigned char file_read_byte(const char *fn, ulong pos) { +unsigned char file_read_byte(const char *fn, uint32_t pos) { unsigned char v = 0; file_read_block(fn, &v, pos, 1); return v; } -void file_write_byte(const char *fn, ulong pos, unsigned char v) { +void file_write_byte(const char *fn, uint32_t pos, unsigned char v) { file_write_block(fn, &v, pos, 1); } @@ -514,7 +577,7 @@ void strncpy_P0(char* dest, const char* src, int n) { * 65534: sunrise to sunset duration * 65535: sunset to sunrise duration */ -ulong water_time_resolve(uint16_t v) { +uint32_t water_time_resolve(uint16_t v) { if(v==65534) { return (os.nvdata.sunset_time-os.nvdata.sunrise_time) * 60L; } else if(v==65535) { @@ -718,4 +781,9 @@ void str2mac(const char *_str, unsigned char mac[]) { yield(); } } -#endif \ No newline at end of file +#endif + +char dec2hexchar(unsigned char dec) { + if(dec<10) return '0'+dec; + else return 'A'+(dec-10); +} \ No newline at end of file diff --git a/utils.h b/utils.h index 24790bed9..aa9411491 100644 --- a/utils.h +++ b/utils.h @@ -21,11 +21,12 @@ * . */ -#ifndef _UTILS_H -#define _UTILS_H + #pragma once -#if defined(ARDUINO) +#if defined(ESP8266) #include + #include + #include #else // headers for RPI/LINUX #include #include @@ -37,22 +38,54 @@ #endif #include "defines.h" + +#if defined(ESP8266) +typedef File os_file_type; +#else +typedef FILE* os_file_type; +#endif + +enum class FileOpenMode { + Read, + ReadWrite, + WriteTruncate, + ReadWriteTruncate, + Append, + ReadAppend, +}; + +enum class FileSeekMode { + Set, + Current, + End +}; + + // File reading/writing functions -//remove unused functions: void write_to_file(const char *fname, const char *data, ulong size, ulong pos=0, bool trunc=true); -//remove unused functions: void read_from_file(const char *fname, char *data, ulong maxsize=TMP_BUFFER_SIZE, int pos=0); +//remove unused functions: void write_to_file(const char *fname, const char *data, uint32_t size, uint32_t pos=0, bool trunc=true); +//remove unused functions: void read_from_file(const char *fname, char *data, uint32_t maxsize=TMP_BUFFER_SIZE, int pos=0); void remove_file(const char *fname); bool file_exists(const char *fname); +void ensure_log_dir(); -void file_read_block (const char *fname, void *dst, ulong pos, ulong len); -void file_write_block(const char *fname, const void *src, ulong pos, ulong len); -void file_copy_block (const char *fname, ulong from, ulong to, ulong len, void *tmp=0); -unsigned char file_read_byte (const char *fname, ulong pos); -void file_write_byte(const char *fname, ulong pos, unsigned char v); -unsigned char file_cmp_block(const char *fname, const char *buf, ulong pos); +os_file_type file_open(const char *fn, FileOpenMode mode); +void file_close(os_file_type f); +bool file_seek(os_file_type f, uint32_t position, FileSeekMode mode); +bool file_seek(os_file_type f, uint32_t position); +int file_read(os_file_type f, void *target, uint32_t len); +int file_write(os_file_type f, const void *source, uint32_t len); +uint32_t file_size(os_file_type f); + +void file_read_block (const char *fname, void *dst, uint32_t pos, uint32_t len); +void file_write_block(const char *fname, const void *src, uint32_t pos, uint32_t len); +void file_copy_block (const char *fname, uint32_t from, uint32_t to, uint32_t len, void *tmp=0); +unsigned char file_read_byte (const char *fname, uint32_t pos); +void file_write_byte(const char *fname, uint32_t pos, unsigned char v); +unsigned char file_cmp_block(const char *fname, const char *buf, uint32_t pos); // misc. string and time converstion functions void strncpy_P0(char* dest, const char* src, int n); -ulong water_time_resolve(uint16_t v); +uint32_t water_time_resolve(uint16_t v); unsigned char water_time_encode_signed(int16_t i); int16_t water_time_decode_signed(unsigned char i); void urlDecode(char *); @@ -74,17 +107,17 @@ bool isValidMAC(const char *_mac); void str2mac(const char *_str, unsigned char mac[]); #endif -#if defined(ARDUINO) +#if defined(ESP8266) #else // Arduino compatible functions for RPI/LINUX const char* get_data_dir(); void set_data_dir(const char *new_data_dir); char* get_filename_fullpath(const char *filename); - void delay(ulong ms); - void delayMicroseconds(ulong us); - void delayMicrosecondsHard(ulong us); - ulong millis(); - ulong micros(); + void delay(uint32_t ms); + void delayMicroseconds(uint32_t us); + void delayMicrosecondsHard(uint32_t us); + unsigned long millis(); // TODO: change to uint32_t once upstream OTF is updated + uint32_t micros(); void initialiseEpoch(); #if defined(OSPI) unsigned int detect_rpi_rev(); @@ -113,4 +146,4 @@ void str2mac(const char *_str, unsigned char mac[]); BoardType get_board_type(); #endif -#endif // _UTILS_H +char dec2hexchar(unsigned char dec); \ No newline at end of file diff --git a/weather.cpp b/weather.cpp index 628b40de7..2a7569d01 100644 --- a/weather.cpp +++ b/weather.cpp @@ -127,7 +127,7 @@ static void getweather_callback(char* buffer) { if (findKeyVal(p, tmp_buffer, TMP_BUFFER_SIZE, PSTR("rd"), true)) { v = atoi(tmp_buffer); if (v>0) { - os.nvdata.rd_stop_time = tnow + (unsigned long) v * 3600; + os.nvdata.rd_stop_time = tnow + (uint32_t) v * 3600; os.raindelay_start(); } else if (v==0) { os.raindelay_stop(); @@ -160,7 +160,7 @@ static void getweather_callback_with_peel_header(char* buffer) { void GetWeather() { if(!os.network_connected()) return; // use temp buffer to construct get command - BufferFiller bf = BufferFiller(tmp_buffer, TMP_BUFFER_SIZE*2); + BufferFiller bf = BufferFiller(tmp_buffer, TMP_BUFFER_ALLOC_SIZE); int method = os.iopts[IOPT_USE_WEATHER]; // use manual adjustment call for monthly adjustment -- a bit ugly, but does not involve weather server changes if(method==WEATHER_METHOD_MONTHLY) method=WEATHER_METHOD_MANUAL; @@ -186,13 +186,6 @@ void GetWeather() { // Parse protocol and extract host/port char *host_start = host; -#if defined(OS_AVR) - if (strncmp_P(host, PSTR("http://"), 7) == 0) { - host_start = host + 7; - } else if (strncmp_P(host, PSTR("https://"), 8) == 0) { // note that avr does not support https - host_start = host + 8; - } -#else bool use_ssl = true; // default to https int port = 443; // default to https port @@ -214,8 +207,6 @@ void GetWeather() { port = atoi(colon + 1); } -#endif - strcat(ether_buffer, " HTTP/1.0\r\nHOST: "); strcat(ether_buffer, host_start); strcat(ether_buffer, "\r\nUser-Agent: "); @@ -224,11 +215,7 @@ void GetWeather() { wt_errCode = HTTP_RQT_NOT_RECEIVED; DEBUG_PRINT(ether_buffer); -#if defined(OS_AVR) - int ret = os.send_http_request(host_start, ether_buffer, getweather_callback_with_peel_header); -#else int ret = os.send_http_request(host_start, port, ether_buffer, getweather_callback_with_peel_header, use_ssl); -#endif if(ret!=HTTP_RQT_SUCCESS) { if(wt_errCode < 0) wt_errCode = ret; // if wt_errCode > 0, the call is successful but weather script may return error @@ -270,11 +257,11 @@ void parse_wto(char* wto) { void apply_monthly_adjustment(time_os_t curr_time) { // ====== Check monthly water percentage ====== if(os.iopts[IOPT_USE_WEATHER]==WEATHER_METHOD_MONTHLY) { -#if defined(ARDUINO) +#if defined(ESP8266) unsigned char m = month(curr_time)-1; #else - time_os_t ct = curr_time; - struct tm *ti = gmtime(&ct); + time_t _ct = curr_time; + struct tm *ti = gmtime(&_ct); unsigned char m = ti->tm_mon; // tm_mon ranges from [0,11] #endif if(os.iopts[IOPT_WATER_PERCENTAGE]!=wt_monthly[m]) { diff --git a/weather.h b/weather.h index 1ac7a7458..a8ad3621c 100644 --- a/weather.h +++ b/weather.h @@ -21,9 +21,7 @@ * */ - -#ifndef _WEATHER_H -#define _WEATHER_H +#pragma once #define WEATHER_UPDATE_SUNRISE 0x01 #define WEATHER_UPDATE_SUNSET 0x02 @@ -45,4 +43,3 @@ extern unsigned char wt_monthly[]; extern unsigned char wt_restricted; void parse_wto(char* wto); void apply_monthly_adjustment(time_os_t curr_time); -#endif // _WEATHER_H
UI Source: