diff --git a/Dockerfile b/Dockerfile index 3bc27516..4c86c872 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,26 +3,11 @@ FROM debian:bookworm-slim AS base ARG BUILD_VERSION="OSPI" ######################################## -## 1st stage compiles OpenSprinkler runtime dependency raspi-gpio -FROM base AS raspi-gpio-build - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update -RUN apt-get install -y git gcc make automake -RUN rm -rf /var/lib/apt/lists/* -RUN mkdir /raspi-gpio -WORKDIR /raspi-gpio -RUN git clone --depth 1 https://github.com/RPi-Distro/raspi-gpio.git . -RUN autoreconf -f -i -RUN (./configure || cat config.log) -RUN make - -######################################## -## 2nd stage compiles OpenSprinkler code +## 1st stage compiles OpenSprinkler code FROM base AS os-build ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y bash g++ make libmosquittopp-dev libssl-dev +RUN apt-get update && apt-get install -y bash g++ make libmosquittopp-dev libssl-dev libi2c-dev libgpiod-dev libgpiod2 gpiod RUN rm -rf /var/lib/apt/lists/* COPY . /OpenSprinkler WORKDIR /OpenSprinkler @@ -30,17 +15,16 @@ RUN make clean RUN make VERSION=${BUILD_VERSION} ######################################## -## 3rd stage is minimal runtime + executable +## 2nd stage is minimal runtime + executable FROM base ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y libstdc++6 libmosquittopp1 +RUN apt-get update && apt-get install -y libstdc++6 libmosquittopp1 libi2c0 libgpiod2 RUN rm -rf /var/lib/apt/lists/* RUN mkdir /OpenSprinkler RUN mkdir -p /data/logs COPY --from=os-build /OpenSprinkler/OpenSprinkler /OpenSprinkler/OpenSprinkler -COPY --from=raspi-gpio-build /raspi-gpio/raspi-gpio /usr/bin/raspi-gpio WORKDIR /OpenSprinkler #-- Logs and config information go into the volume on /data diff --git a/Makefile b/Makefile index fe9c06fc..c11e2ae9 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ CXX=g++ # -std=gnu++17 -VERSION=OSPI +VERSION?=OSPI CXXFLAGS=-std=gnu++14 -D$(VERSION) -DSMTP_OPENSSL -Wall -include string.h -include cstdint -Iexternal/TinyWebsockets/tiny_websockets_lib/include -Iexternal/OpenThings-Framework-Firmware-Library/ LD=$(CXX) -LIBS=pthread mosquitto ssl crypto +LIBS=pthread mosquitto ssl crypto i2c gpiod LDFLAGS=$(addprefix -l,$(LIBS)) BINARY=OpenSprinkler -SOURCES=main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp mqtt.cpp smtp.c $(wildcard external/TinyWebsockets/tiny_websockets_lib/src/*.cpp) $(wildcard external/OpenThings-Framework-Firmware-Library/*.cpp) +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) OBJECTS=$(addsuffix .o,$(basename $(SOURCES))) diff --git a/OpenSprinkler.cpp b/OpenSprinkler.cpp index cdf25ec9..71f85a1c 100644 --- a/OpenSprinkler.cpp +++ b/OpenSprinkler.cpp @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX/ESP8266) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * OpenSprinkler library @@ -70,33 +70,36 @@ unsigned char OpenSprinkler::attrib_dis[MAX_NUM_BOARDS]; unsigned char OpenSprinkler::attrib_spe[MAX_NUM_BOARDS]; unsigned char OpenSprinkler::attrib_grp[MAX_NUM_STATIONS]; unsigned char OpenSprinkler::masters[NUM_MASTER_ZONES][NUM_MASTER_OPTS]; +time_os_t OpenSprinkler::masters_last_on[NUM_MASTER_ZONES]; +RCSwitch OpenSprinkler::rfswitch; extern char tmp_buffer[]; extern char ether_buffer[]; extern ProgramData pd; -#if defined(ESP8266) +#if defined(USE_SSD1306) SSD1306Display OpenSprinkler::lcd(0x3c, SDA, SCL); +#elif defined(USE_LCD) + LiquidCrystal OpenSprinkler::lcd; +#endif + +#if defined(ESP8266) unsigned char OpenSprinkler::state = OS_STATE_INITIAL; unsigned char OpenSprinkler::prev_station_bits[MAX_NUM_BOARDS]; IOEXP* OpenSprinkler::expanders[MAX_NUM_BOARDS/2]; IOEXP* OpenSprinkler::mainio; // main controller IO expander object IOEXP* OpenSprinkler::drio; // driver board IO expander object - RCSwitch OpenSprinkler::rfswitch; - String OpenSprinkler::wifi_ssid=""; String OpenSprinkler::wifi_pass=""; unsigned char OpenSprinkler::wifi_bssid[6]={0}; unsigned char OpenSprinkler::wifi_channel=255; unsigned char OpenSprinkler::wifi_testmode = 0; #elif defined(ARDUINO) - LiquidCrystal OpenSprinkler::lcd; extern SdFat sd; #else #if defined(OSPI) unsigned char OpenSprinkler::pin_sr_data = PIN_SR_DATA; #endif - // todo future: LCD define for Linux-based systems #endif #if defined(USE_OTF) @@ -172,9 +175,9 @@ const char iopt_json_names[] PROGMEM = "subn3" "subn4" "fwire" - "resv1" - "resv2" - "resv3" + "laton" + "latof" + "ife2\0" "resv4" "resv5" "resv6" @@ -251,9 +254,9 @@ const char iopt_prompts[] PROGMEM = "Subnet mask3: " "Subnet mask4: " "Force wired? " - "Reserved 1 " - "Reserved 2 " - "Reserved 3 " + "Latch On Volt. " + "Latch Off Volt. " + "Notif 2 Enable " "Reserved 4 " "Reserved 5 " "Reserved 6 " @@ -329,8 +332,8 @@ const unsigned char iopt_max[] PROGMEM = { 255, 255, 1, - 255, - 255, + 24, + 24, 255, 255, 255, @@ -360,7 +363,7 @@ unsigned char OpenSprinkler::iopts[] = { #if defined(ARDUINO) // on AVR, the default HTTP port is 80 80, // this and next byte define http port number 0, -#else // on RPI/BBB/LINUX, the default HTTP port is 8080 +#else // on RPI/LINUX, the default HTTP port is 8080 144,// this and next byte define http port number 31, #endif @@ -413,9 +416,9 @@ unsigned char OpenSprinkler::iopts[] = { 255,// subnet mask 3 0, 1, // force wired connection - 0, // reserved 1 - 0, // reserved 2 - 0, // reserved 3 + 0, // latch on volt + 0, // latch off volt + 0, // notif enable bits 2 0, // reserved 4 0, // reserved 5 0, // reserved 6 @@ -712,7 +715,7 @@ void OpenSprinkler::reboot_dev(uint8_t cause) { #endif } -#else // RPI/BBB/LINUX network init functions +#else // RPI/LINUX network init functions #include "etherport.h" #include @@ -734,11 +737,12 @@ unsigned char OpenSprinkler::start_network() { #endif if(otc.en>0 && otc.token.length()>=DEFAULT_OTC_TOKEN_LENGTH) { otf = new OTF::OpenThingsFramework(port, otc.server.c_str(), otc.port, otc.token.c_str(), false, ether_buffer, ETHER_BUFFER_SIZE); - DEBUG_PRINTLN(F("Started OTF with remote connection")); + DEBUG_PRINT(F("Started OTF with remote connection. Local port is: ")); } else { otf = new OTF::OpenThingsFramework(port, ether_buffer, ETHER_BUFFER_SIZE); - DEBUG_PRINTLN(F("Started OTF with just local connection")); + DEBUG_PRINT(F("Started OTF with just local connection. Local port is: ")); } + DEBUG_PRINTLN(port); return 1; } @@ -796,16 +800,16 @@ void OpenSprinkler::update_dev() { } #endif // end network init functions -#if defined(ARDUINO) +#if defined(USE_DISPLAY) /** Initialize LCD */ void OpenSprinkler::lcd_start() { -#if defined(ESP8266) +#if defined(USE_SSD1306) // initialize SSD1306 lcd.init(); lcd.begin(); flash_screen(); -#else +#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); @@ -958,6 +962,14 @@ void OpenSprinkler::begin() { #endif +#if defined(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 + memset(masters_last_on, 0, sizeof(masters_last_on)); // Reset all stations clear_all_station_bits(); apply_all_station_bits(); @@ -1038,27 +1050,28 @@ void OpenSprinkler::begin() { baseline_current = 0; #endif - +#endif +#if defined(USE_DISPLAY) lcd_start(); - #if defined(ESP8266) + #if defined(USE_SSD1306) lcd.createChar(ICON_ETHER_CONNECTED, _iconimage_ether_connected); lcd.createChar(ICON_ETHER_DISCONNECTED, _iconimage_ether_disconnected); - #else + 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_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) - - /* create custom characters */ - lcd.createChar(ICON_WIFI_CONNECTED, _iconimage_wifi_connected); - lcd.createChar(ICON_WIFI_DISCONNECTED, _iconimage_wifi_disconnected); - lcd.setCursor(0,0); lcd.print(F("Init file system")); lcd.setCursor(0,1); @@ -1104,10 +1117,24 @@ void OpenSprinkler::begin() { /** LATCH boost voltage * */ -void OpenSprinkler::latch_boost() { - digitalWriteExt(PIN_BOOST, HIGH); // enable boost converter - delay((int)iopts[IOPT_BOOST_TIME]<<2); // wait for booster to charge - digitalWriteExt(PIN_BOOST, LOW); // disable boost converter +void OpenSprinkler::latch_boost(unsigned char volt) { + // if volt is 0 or larger than max volt, ignore it and boost according to BOOST_TIME only + if(volt==0 || volt>iopt_max[IOPT_LATCH_ON_VOLTAGE]) { + digitalWriteExt(PIN_BOOST, HIGH); // enable boost converter + delay((int)iopts[IOPT_BOOST_TIME]<<2); // wait for booster to charge + digitalWriteExt(PIN_BOOST, LOW); // disable boost converter + } else { + // boost to specified volt, up to time specified by BOOST_TIME + uint16_t top = (uint16_t)(volt * 19.25f); // ADC = 1024 * volt * 1.5k / 79.8k + if(analogRead(PIN_CURR_SENSE)>=top) return; // if the voltage has already reached top, return right away + 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(millis()=2) { DEBUG_PRINTLN(F("latch_open_v2")); latch_disable_alloutputs_v2(); // disable all output pins - latch_boost(); // generate boost voltage + DEBUG_PRINTLN(F("boost on voltage: ")); + DEBUG_PRINTLN(iopts[IOPT_LATCH_ON_VOLTAGE]); + latch_boost(iopts[IOPT_LATCH_ON_VOLTAGE]); // generate boost voltage digitalWriteExt(PIN_LATCH_COMA, HIGH); // enable COM+ latch_setzoneoutput_v2(sid, LOW, HIGH); // enable sid- digitalWriteExt(PIN_BOOST_EN, HIGH); // enable output path @@ -1213,7 +1242,9 @@ void OpenSprinkler::latch_close(unsigned char sid) { if(hw_rev>=2) { DEBUG_PRINTLN(F("latch_close_v2")); latch_disable_alloutputs_v2(); // disable all output pins - latch_boost(); // generate boost voltage + DEBUG_PRINTLN(F("boost off voltage: ")); + DEBUG_PRINTLN(iopts[IOPT_LATCH_OFF_VOLTAGE]); + latch_boost(iopts[IOPT_LATCH_OFF_VOLTAGE]); // generate boost voltage latch_setzoneoutput_v2(sid, HIGH, LOW); // enable sid+ digitalWriteExt(PIN_LATCH_COMK, HIGH); // enable COM- digitalWriteExt(PIN_BOOST_EN, HIGH); // enable output path @@ -1564,18 +1595,28 @@ static ulong hex2ulong(unsigned char *code, unsigned char len) { return v; } -/** Parse RF code into on/off/timeing sections */ -uint16_t OpenSprinkler::parse_rfstation_code(RFStationData *data, ulong* on, ulong *off) { - ulong v; - v = hex2ulong(data->on, sizeof(data->on)); - if (!v) return 0; - if (on) *on = v; - v = hex2ulong(data->off, sizeof(data->off)); - if (!v) return 0; - if (off) *off = v; - v = hex2ulong(data->timing, sizeof(data->timing)); - if (!v) return 0; - return v; +/** Parse RF station data into code */ +bool OpenSprinkler::parse_rfstation_code(RFStationData *data, RFStationCode *code) { + if(!code) return false; + code->timing = 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)); + } 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->protocol = 1; + code->bitlength = 24; + } + if(!code->timing) return false; + return true; } /** Get station data */ @@ -1598,7 +1639,6 @@ void OpenSprinkler::get_station_name(unsigned char sid, char tmp[]) { /** Set station name */ void OpenSprinkler::set_station_name(unsigned char sid, char tmp[]) { - // todo: store the right size tmp[STATION_NAME_SIZE]=0; char n0[STATION_NAME_SIZE+1]; get_station_name(sid, n0); @@ -1753,8 +1793,9 @@ unsigned char OpenSprinkler::weekday_today() { ulong wd = now_tz() / 86400L; return (wd+3) % 7; // Jan 1, 1970 is a Thursday #else - return 0; - // todo future: is this function needed for RPI/BBB? + time_t t = time(NULL); + struct tm *tm = localtime(&t); + return (tm->tm_wday+6) % 7; #endif } @@ -1838,85 +1879,26 @@ unsigned char OpenSprinkler::get_station_bit(unsigned char sid) { /** Clear all station bits */ void OpenSprinkler::clear_all_station_bits() { unsigned char sid; - for(sid=0;sid<=MAX_NUM_STATIONS;sid++) { + for(sid=0;sid=0) { - if ((code>>i) & 1) { - transmit_rfbit(len3, len); - } else { - transmit_rfbit(len, len3); - } - i--; - }; - // send sync - transmit_rfbit(len, len31); - } -} - /** Switch RF station * This function takes a RF code, * parses it into signals and timing, * and sends it out through RF transmitter. */ void OpenSprinkler::switch_rfstation(RFStationData *data, bool turnon) { - ulong on, off; - uint16_t length = parse_rfstation_code(data, &on, &off); + RFStationCode code; + if(!parse_rfstation_code(data, &code)) return; // return if the timing parameter is 0 if(PIN_RFTX == 255) return; // ignore RF station if RF pin disabled -#if defined(ARDUINO) - #if defined(ESP8266) rfswitch.enableTransmit(PIN_RFTX); - rfswitch.setProtocol(1); - rfswitch.setPulseLength(length); - rfswitch.send(turnon ? on : off, 24); - #else - send_rfsignal(turnon ? on : off, length); - #endif -#else - // pre-open gpio file to minimize overhead - rf_gpio_fd = gpio_fd_open(PIN_RFTX); - send_rfsignal(turnon ? on : off, length); - gpio_fd_close(rf_gpio_fd); - rf_gpio_fd = -1; -#endif - + rfswitch.setProtocol(code.protocol); + rfswitch.setPulseLength(code.timing); + rfswitch.send(turnon ? code.on : code.off, code.bitlength); } /** Switch GPIO station @@ -2550,6 +2532,7 @@ void OpenSprinkler::raindelay_stop() { } /** LCD and button functions */ +#if defined(USE_DISPLAY) #if defined(ARDUINO) // AVR LCD and button functions /** print a program memory string */ #if defined(ESP8266) @@ -2579,6 +2562,28 @@ void OpenSprinkler::lcd_print_line_clear_pgm(PGM_P PROGMEM str, unsigned char li for(; (16-cnt) >= 0; cnt ++) lcd_print_pgm(PSTR(" ")); } +#else +void OpenSprinkler::lcd_print_pgm(const char *str) { + lcd.print(str); +} +void OpenSprinkler::lcd_print_line_clear_pgm(const char *str, uint8_t line) { + char buf[16]; + uint8_t c; + int8_t cnt = 0; + while((c=*str++)!= '\0' && cnt<16) { + buf[cnt] = c; + cnt++; + } + + for(int i=cnt; i<16; i++) buf[i] = ' '; + + lcd.setCursor(0, line); + lcd.print(buf); +} + +#define PGSTR(s) s +#endif + void OpenSprinkler::lcd_print_2digit(int v) { lcd.print((int)(v/10)); @@ -2588,21 +2593,29 @@ void OpenSprinkler::lcd_print_2digit(int v) /** print time to a given line */ void OpenSprinkler::lcd_print_time(time_os_t t) { -#if defined(ESP8266) +#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_2digit(month(t)); + lcd_print_pgm(PSTR("-")); + lcd_print_2digit(day(t)); -#if defined(ESP8266) +#if defined(USE_SSD1306) lcd.display(); lcd.setAutoDisplay(true); #endif @@ -2610,37 +2623,61 @@ void OpenSprinkler::lcd_print_time(time_os_t t) /** print ip address */ void OpenSprinkler::lcd_print_ip(const unsigned char *ip, unsigned char endian) { -#if defined(ESP8266) +#if defined(USE_SSD1306) lcd.clear(0, 1); -#else + 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]); - if(i<3) lcd_print_pgm(PSTR(".")); + + if(i<3) { + lcd_print_pgm(PSTR(".")); + } } + + #if defined(USE_SSD1306) + lcd.display(); + lcd.setAutoDisplay(true); + #endif } /** 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.setCursor(0, 0); for(unsigned char i=0; i<6; i++) { - if(i) lcd_print_pgm(PSTR("-")); + if(i) { + lcd_print_pgm(PSTR("-")); + } + lcd.print((mac[i]>>4), HEX); lcd.print((mac[i]&0x0F), HEX); if(i==4) lcd.setCursor(0, 1); } + #if defined(ARDUINO) if(useEth) { lcd_print_pgm(PSTR(" (Ether MAC)")); } else { lcd_print_pgm(PSTR(" (WiFi MAC)")); } + #else + lcd_print_pgm(PSTR(" (MAC)")); + #endif + + #if defined(USE_SSD1306) + lcd.display(); + lcd.setAutoDisplay(true); + #endif } /** print station bits */ void OpenSprinkler::lcd_print_screen(char c) { -#if defined(ESP8266) +#if defined(USE_SSD1306) lcd.setAutoDisplay(false); // reduce screen drawing time by turning off display() when drawing individual characters #endif lcd.setCursor(0, 1); @@ -2720,7 +2757,7 @@ void OpenSprinkler::lcd_print_screen(char c) { lcd.setCursor(LCD_CURSOR_NETWORK, 1); #if defined(ESP8266) if(useEth) { - lcd.write(eth.connected()?ICON_ETHER_CONNECTED:ICON_ETHER_DISCONNECTED); // todo: need to detect ether status + lcd.write(eth.connected()?ICON_ETHER_CONNECTED:ICON_ETHER_DISCONNECTED); } else lcd.write(WiFi.status()==WL_CONNECTED?ICON_WIFI_CONNECTED:ICON_WIFI_DISCONNECTED); @@ -2728,9 +2765,12 @@ 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(ESP8266) - - if(useEth || (get_wifi_mode()==WIFI_MODE_STA && WiFi.status()==WL_CONNECTED && WiFi.localIP())) { +#if defined(USE_SSD1306) + #if defined(ESP8266) + if(useEth || (get_wifi_mode()==WIFI_MODE_STA && WiFi.status()==WL_CONNECTED && WiFi.localIP())) { + #else + { + #endif lcd.setCursor(0, -1); if(status.rain_delayed) { lcd.print(F(" ")); @@ -2742,17 +2782,21 @@ void OpenSprinkler::lcd_print_screen(char c) { lcd.print(F(" (System Idle) ")); } + #if defined(ESP8266) lcd.setCursor(2, 2); if(status.program_busy && !status.pause_state) { //lcd.print(F("Curr: ")); lcd.print(read_current()); lcd.print(F(" mA ")); } else { + #else + { + #endif lcd.clear(2, 2); } } -#endif -#if defined(ESP8266) + + lcd.display(); lcd.setAutoDisplay(true); #endif @@ -2841,6 +2885,19 @@ void OpenSprinkler::lcd_print_option(int i) { lcd.print('-'); #endif break; + case IOPT_LATCH_ON_VOLTAGE: + case IOPT_LATCH_OFF_VOLTAGE: + #if defined(ARDUINO) + if(hw_type==HW_TYPE_LATCH) { + lcd.print((int)iopts[i]); + lcd.print('V'); + } else { + lcd.print('-'); + } + #else + lcd.print('-'); + #endif + break; default: // if this is a boolean option if (pgm_read_byte(iopt_max+i)==1) @@ -2855,6 +2912,7 @@ void OpenSprinkler::lcd_print_option(int i) { } + /** Button functions */ /** wait for button */ unsigned char OpenSprinkler::button_read_busy(unsigned char pin_butt, unsigned char waitmode, unsigned char butt, unsigned char is_holding) { @@ -2906,6 +2964,8 @@ unsigned char OpenSprinkler::button_read(unsigned char waitmode) return ret; } +#if defined(ARDUINO) + /** user interface for setting options during startup */ void OpenSprinkler::ui_set_options(int oid) { @@ -2954,6 +3014,7 @@ void OpenSprinkler::ui_set_options(int oid) if(i==IOPT_URS_RETIRED) i++; if(i==IOPT_RSO_RETIRED) i++; 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 defined(ESP8266) else if (lcd.type()==LCD_I2C && i==IOPT_LCD_CONTRAST) i+=3; #else @@ -2971,6 +3032,9 @@ 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 @@ -3005,7 +3069,7 @@ void OpenSprinkler::lcd_set_brightness(unsigned char value) { } } -#elif defined(ESP8266) +#elif defined(USE_SSD1306) if (value) {lcd.displayOn();lcd.setBrightness(255); } else { if(iopts[IOPT_LCD_DIMMING]==0) lcd.displayOff(); @@ -3013,9 +3077,10 @@ void OpenSprinkler::lcd_set_brightness(unsigned char value) { } #endif } -#endif // end of LCD and button functions -#if defined(ESP8266) + + +#if defined(USE_SSD1306) #include "images.h" void OpenSprinkler::flash_screen() { lcd.setCursor(0, -1); @@ -3041,6 +3106,12 @@ void OpenSprinkler::set_screen_led(unsigned char status) { lcd.setColor(WHITE); } +#endif + +#endif + +#if defined(ESP8266) + void OpenSprinkler::reset_to_ap() { iopts[IOPT_WIFI_MODE] = WIFI_MODE_AP; iopts_save(); diff --git a/OpenSprinkler.h b/OpenSprinkler.h index 050937bc..8010d232 100644 --- a/OpenSprinkler.h +++ b/OpenSprinkler.h @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX/ESP8266) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * OpenSprinkler library header file @@ -31,11 +31,13 @@ #include "gpio.h" #include "images.h" #include "mqtt.h" +#include "RCSwitch.h" #if defined(ARDUINO) // headers for Arduino #include #include #include + #include #include "I2CRTC.h" #if defined(ESP8266) // for ESP8266 @@ -43,11 +45,9 @@ #include #include #include - #include #include #include #include - #include "SSD1306Display.h" #include "espconnect.h" #include "EMailSender.h" #else // for AVR @@ -56,7 +56,7 @@ #include "LiquidCrystal.h" #endif -#else // headers for RPI/BBB/LINUX +#else // headers for RPI/LINUX #include #include #include @@ -64,9 +64,18 @@ #include #include "OpenThingsFramework.h" #include "etherport.h" + #include "rpitime.h" #include "smtp.h" #endif // end of headers +#if defined(USE_LCD) + #include "LiquidCrystal.h" +#endif + +#if defined(USE_SSD1306) + #include "SSD1306Display.h" +#endif + #if defined(ARDUINO) #if defined(ESP8266) extern ESP8266WebServer *update_server; @@ -112,6 +121,7 @@ extern OTF::OpenThingsFramework *otf; #else extern EthernetServer *m_server; + extern bool useEth; #endif /** Non-volatile data structure */ @@ -147,6 +157,23 @@ struct StationData { /** RF station data structures - Must fit in STATION_SPECIAL_DATA_SIZE */ struct RFStationData { + unsigned char version; + unsigned char on[8]; + unsigned char off[8]; + unsigned char timing[4]; + unsigned char protocol[2]; + unsigned char bitlength[2]; +}; + +struct RFStationCode { + uint32_t on; + uint32_t off; + uint16_t timing; + uint8_t protocol; + uint8_t bitlength; +}; + +struct RFStationDataClassic { unsigned char on[6]; unsigned char off[6]; unsigned char timing[4]; @@ -212,12 +239,10 @@ class OpenSprinkler { public: // data members -#if defined(ESP8266) +#if defined(USE_SSD1306) static SSD1306Display lcd; // 128x64 OLED display -#elif defined(ARDUINO) +#elif defined(USE_LCD) static LiquidCrystal lcd; // 16x2 character LCD -#else - // todo: LCD define for RPI/BBB #endif #if defined(OSPI) @@ -247,6 +272,7 @@ class OpenSprinkler { static unsigned char attrib_spe[]; static unsigned char attrib_grp[]; static unsigned char masters[NUM_MASTER_ZONES][NUM_MASTER_OPTS]; + static time_os_t masters_last_on[NUM_MASTER_ZONES]; // variables for time keeping static time_os_t sensor1_on_timer; // time when sensor1 is detected on last time @@ -295,7 +321,7 @@ class OpenSprinkler { //static StationAttrib get_station_attrib(unsigned char sid); // get station attribute static void attribs_save(); // repackage attrib bits and save (backward compatibility) static void attribs_load(); // load and repackage attrib bits (backward compatibility) - static uint16_t parse_rfstation_code(RFStationData *data, ulong *on, ulong *off); // parse rf code into on/off/time sections + static bool parse_rfstation_code(RFStationData *data, RFStationCode *code); // parse rf code into on/off/time sections static void switch_rfstation(RFStationData *data, bool turnon); // switch rf station static void switch_remotestation(RemoteIPStationData *data, bool turnon, uint16_t dur=0); // switch remote IP station static void switch_remotestation(RemoteOTCStationData *data, bool turnon, uint16_t dur=0); // switch remote OTC station @@ -347,19 +373,20 @@ class OpenSprinkler { #endif // -- LCD functions -#if defined(ARDUINO) // LCD functions for Arduino - #if defined(ESP8266) - 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(USE_DISPLAY) static void lcd_print_time(time_os_t t); // print current time static void lcd_print_ip(const unsigned char *ip, unsigned char endian); // print ip static void lcd_print_mac(const unsigned char *mac); // print mac static void lcd_print_screen(char c); // print station bits of the board selected by display_board 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); @@ -384,17 +411,22 @@ class OpenSprinkler { // -- UI functions -- static void ui_set_options(int oid); // ui for setting options (oid-> starting option index) - static void lcd_set_brightness(unsigned char value=1); - static void lcd_set_contrast(); +#endif + +#if defined(ARDUINO) // LCD functions for Arduino + #if defined(ESP8266) + 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 RCSwitch rfswitch; + static void detect_expanders(); - static void flash_screen(); - static void toggle_screen_led(); - static void set_screen_led(unsigned char status); static unsigned char get_wifi_mode() { if (useEth) return WIFI_MODE_STA; else return wifi_testmode ? WIFI_MODE_STA : iopts[IOPT_WIFI_MODE];} static unsigned char wifi_testmode; static String wifi_ssid, wifi_pass; @@ -405,14 +437,21 @@ class OpenSprinkler { static unsigned char state; #endif +#else +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: +#if defined(USE_DISPLAY) // LCD functions static void lcd_print_option(int i); // print an option to the lcd static void lcd_print_2digit(int v); // print a integer in 2 digits static void lcd_start(); static unsigned char button_read_busy(unsigned char pin_butt, unsigned char waitmode, unsigned char butt, unsigned char is_holding); +#endif // LCD functions - #if defined(ESP8266) - static void latch_boost(); +#if defined(ESP8266) + static void latch_boost(unsigned char volt=0); static void latch_open(unsigned char sid); static void latch_close(unsigned char sid); static void latch_setzonepin(unsigned char sid, unsigned char value); @@ -421,9 +460,9 @@ class OpenSprinkler { static void latch_setzoneoutput_v2(unsigned char sid, unsigned char A, unsigned char K); static void latch_apply_all_station_bits(); static unsigned char prev_station_bits[]; - #endif #endif // LCD functions static unsigned char engage_booster; + static RCSwitch rfswitch; #if defined(USE_OTF) static void parse_otc_config(); diff --git a/RCSwitch.cpp b/RCSwitch.cpp new file mode 100644 index 00000000..2f4ae7f6 --- /dev/null +++ b/RCSwitch.cpp @@ -0,0 +1,187 @@ +/* + RCSwitch - Arduino libary for remote control outlet switches + Copyright (c) 2011 Suat Özgür. All right reserved. + + Contributors: + - Andre Koehler / info(at)tomate-online(dot)de + - Gordeev Andrey Vladimirovich / gordeev(at)openpyro(dot)com + - Skineffect / http://forum.ardumote.com/viewtopic.php?f=2&t=46 + - Dominik Fischer / dom_fischer(at)web(dot)de + - Frank Oltmanns / .(at)gmail(dot)com + - Andreas Steinel / A.(at)gmail(dot)com + - Max Horn / max(at)quendi(dot)de + - Robert ter Vehn / .(at)gmail(dot)com + - Johann Richard / .(at)gmail(dot)com + - Vlad Gheorghe / .(at)gmail(dot)com https://github.com/vgheo + - Matias Cuenca-Acuna + + Project home: https://github.com/sui77/rc-switch/ + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#if defined(ARDUINO) +#include +#endif + +#include "gpio.h" +#include "utils.h" +#include "RCSwitch.h" + +/* Format for protocol definitions: + * {pulselength, Sync bit, "0" bit, "1" bit, invertedSignal} + * + * pulselength: pulse length in microseconds, e.g. 350 + * Sync bit: {1, 31} means 1 high pulse and 31 low pulses + * (perceived as a 31*pulselength long pulse, total length of sync bit is + * 32*pulselength microseconds), i.e: + * _ + * | |_______________________________ (don't count the vertical bars) + * "0" bit: waveform for a data bit of value "0", {1, 3} means 1 high pulse + * and 3 low pulses, total length (1+3)*pulselength, i.e: + * _ + * | |___ + * "1" bit: waveform for a data bit of value "1", e.g. {3,1}: + * ___ + * | |_ + * + * These are combined to form Tri-State bits when sending or receiving codes. + */ +static const RCSwitch::Protocol PROGMEM proto[] = { + { 350, { 1, 31 }, { 1, 3 }, { 3, 1 }, false }, // protocol 1 + { 650, { 1, 10 }, { 1, 2 }, { 2, 1 }, false }, // protocol 2 + { 100, { 30, 71 }, { 4, 11 }, { 9, 6 }, false }, // protocol 3 + { 380, { 1, 6 }, { 1, 3 }, { 3, 1 }, false }, // protocol 4 + { 500, { 6, 14 }, { 1, 2 }, { 2, 1 }, false }, // protocol 5 + { 450, { 23, 1 }, { 1, 2 }, { 2, 1 }, true }, // protocol 6 (HT6P20B) + { 150, { 2, 62 }, { 1, 6 }, { 6, 1 }, false }, // protocol 7 (HS2303-PT, i. e. used in AUKEY Remote) + { 200, { 3, 130}, { 7, 16 }, { 3, 16}, false}, // protocol 8 Conrad RS-200 RX + { 200, { 130, 7 }, { 16, 7 }, { 16, 3 }, true}, // protocol 9 Conrad RS-200 TX + { 365, { 18, 1 }, { 3, 1 }, { 1, 3 }, true }, // protocol 10 (1ByOne Doorbell) + { 270, { 36, 1 }, { 1, 2 }, { 2, 1 }, true }, // protocol 11 (HT12E) + { 320, { 36, 1 }, { 1, 2 }, { 2, 1 }, true } // protocol 12 (SM5212) +}; + +enum { + numProto = sizeof(proto) / sizeof(proto[0]) +}; + +RCSwitch::RCSwitch() { + this->nTransmitterPin = 255; + this->setRepeatTransmit(10); + this->setProtocol(1); +} + +/** + * Sets the protocol to send. + */ +void RCSwitch::setProtocol(Protocol protocol) { + this->protocol = protocol; +} + +/** + * Sets the protocol to send, from a list of predefined protocols + */ +void RCSwitch::setProtocol(int nProtocol) { + if (nProtocol < 1 || nProtocol > numProto) { + nProtocol = 1; // TODO: trigger an error, e.g. "bad protocol" ??? + } + //this->protocol = proto[nProtocol-1]; + memcpy_P(&this->protocol, &proto[nProtocol-1], sizeof(Protocol)); +} + +/** + * Sets the protocol to send with pulse length in microseconds. + */ +void RCSwitch::setProtocol(int nProtocol, int nPulseLength) { + setProtocol(nProtocol); + this->setPulseLength(nPulseLength); +} + + +/** + * Sets pulse length in microseconds + */ +void RCSwitch::setPulseLength(int nPulseLength) { + this->protocol.pulseLength = nPulseLength; +} + +/** + * Sets Repeat Transmits + */ +void RCSwitch::setRepeatTransmit(int nRepeatTransmit) { + this->nRepeatTransmit = nRepeatTransmit; +} + +/** + * Enable transmissions + * + * @param nTransmitterPin Arduino Pin to which the sender is connected to + */ +void RCSwitch::enableTransmit(int nTransmitterPin) { + this->nTransmitterPin = nTransmitterPin; + pinMode(this->nTransmitterPin, OUTPUT); +} + +/** + * Disable transmissions + */ +void RCSwitch::disableTransmit() { + this->nTransmitterPin = 255; +} + +/** + * Transmit the first 'length' bits of the integer 'code'. The + * bits are sent from MSB to LSB, i.e., first the bit at position length-1, + * then the bit at position length-2, and so on, till finally the bit at position 0. + */ +void RCSwitch::send(uint32_t code, uint16_t length) { + if (this->nTransmitterPin == -1) + return; + + for (int nRepeat = 0; nRepeat < nRepeatTransmit; nRepeat++) { + for (int i = length-1; i >= 0; i--) { + if (code & (1L << i)) + this->transmit(protocol.one); + else + this->transmit(protocol.zero); + } + this->transmit(protocol.syncFactor); + } + + // Disable transmit after sending (i.e., for inverted protocols) + digitalWrite(this->nTransmitterPin, LOW); + +} + +/** + * Transmit a single high-low pulse. + */ +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; + digitalWrite(this->nTransmitterPin, firstLogicLevel); + while(micros() < timeout) { + delayMicroseconds(5); + } + + timeout = micros() + this->protocol.pulseLength * pulses.low; + digitalWrite(this->nTransmitterPin, secondLogicLevel); + while(micros() < timeout) { + delayMicroseconds(5); + } +} diff --git a/RCSwitch.h b/RCSwitch.h new file mode 100644 index 00000000..bb81aeed --- /dev/null +++ b/RCSwitch.h @@ -0,0 +1,102 @@ +/* + RCSwitch - Arduino libary for remote control outlet switches + Copyright (c) 2011 Suat Özgür. All right reserved. + + Contributors: + - Andre Koehler / info(at)tomate-online(dot)de + - Gordeev Andrey Vladimirovich / gordeev(at)openpyro(dot)com + - Skineffect / http://forum.ardumote.com/viewtopic.php?f=2&t=46 + - Dominik Fischer / dom_fischer(at)web(dot)de + - Frank Oltmanns / .(at)gmail(dot)com + - Max Horn / max(at)quendi(dot)de + - Robert ter Vehn / .(at)gmail(dot)com + + Project home: https://github.com/sui77/rc-switch/ + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + 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 + +#include + +class RCSwitch { + + public: + RCSwitch(); + + void send(uint32_t code, uint16_t length); + + void enableTransmit(int nTransmitterPin); + void disableTransmit(); + void setPulseLength(int nPulseLength); + void setRepeatTransmit(int nRepeatTransmit); + + /** + * Description of a single pule, which consists of a high signal + * whose duration is "high" times the base pulse length, followed + * by a low signal lasting "low" times the base pulse length. + * Thus, the pulse overall lasts (high+low)*pulseLength + */ + struct HighLow { + uint8_t high; + uint8_t low; + }; + + /** + * A "protocol" describes how zero and one bits are encoded into high/low + * pulses. + */ + struct Protocol { + /** base pulse length in microseconds, e.g. 350 */ + uint16_t pulseLength; + + HighLow syncFactor; + HighLow zero; + HighLow one; + + /** + * If true, interchange high and low logic levels in all transmissions. + * + * By default, RCSwitch assumes that any signals it sends or receives + * can be broken down into pulses which start with a high signal level, + * followed by a a low signal level. This is e.g. the case for the + * popular PT 2260 encoder chip, and thus many switches out there. + * + * But some devices do it the other way around, and start with a low + * signal level, followed by a high signal level, e.g. the HT6P20B. To + * accommodate this, one can set invertedSignal to true, which causes + * RCSwitch to change how it interprets any HighLow struct FOO: It will + * then assume transmissions start with a low signal lasting + * FOO.high*pulseLength microseconds, followed by a high signal lasting + * FOO.low*pulseLength microseconds. + */ + bool invertedSignal; + }; + + void setProtocol(Protocol protocol); + void setProtocol(int nProtocol); + void setProtocol(int nProtocol, int nPulseLength); + + private: + void transmit(HighLow pulses); + + int nTransmitterPin; + int nRepeatTransmit; + + Protocol protocol; +}; + +#endif diff --git a/README.txt b/README.txt index e379f91d..9c15abb8 100755 --- a/README.txt +++ b/README.txt @@ -1,6 +1,6 @@ -============================================ -==== OpenSprinkler AVR/RPI/BBB Firmware ==== -============================================ +======================================== +==== OpenSprinkler Unified Firmware ==== +======================================== This is a unified OpenSprinkler firmware for Arduino, and Linux-based OpenSprinklers such as OpenSprinkler Pi. @@ -11,7 +11,7 @@ Compilation instructions for OS (Arduino-based OpenSprinkler) 2.3 and 3.x: Additional details: https://openthings.freshdesk.com/support/solutions/articles/5000165132 -For OSPi/OSBO or other Linux-based OpenSprinkler: +For OSPi or other Linux-based OpenSprinkler: https://openthings.freshdesk.com/support/solutions/articles/5000631599 ============================================ diff --git a/SSD1306Display.h b/SSD1306Display.h index e93e7a15..f97683a5 100644 --- a/SSD1306Display.h +++ b/SSD1306Display.h @@ -4,84 +4,554 @@ #if defined(ESP8266) #include + #include "font.h" #include "images.h" -#define LCD_STD 0 // Standard LCD +#define LCD_STD 0 // Standard LCD #define LCD_I2C 1 -class SSD1306Display : public SSD1306{ +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=0&&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]; }; -#endif +#else +#include +#include -#endif // SSD1306_DISPLAY_H +#include + +#include "i2cd.h" + +#include "font.h" +#include "images.h" +#include +#include +#include + +#define LCD_STD 0 // Standard LCD +#define LCD_I2C 1 + +#define BLACK 0 +#define WHITE 1 + +// Header Values +#define JUMPTABLE_BYTES 4 + +#define JUMPTABLE_LSB 1 +#define JUMPTABLE_SIZE 2 +#define JUMPTABLE_WIDTH 3 +#define JUMPTABLE_START 4 + +#define WIDTH_POS 0 +#define HEIGHT_POS 1 +#define FIRST_CHAR_POS 2 +#define CHAR_NUM_POS 3 + +// SSD1306 +// Codes +#define SSD1306_COMMAND_ADDRESS 0x00 +#define SSD1306_DATA_CONTINUE_ADDRESS 0x40 + +// Fundamental Commands +#define SSD1306_SET_CONTRAST_CONTROL 0x81 +#define SSD1306_DISPLAY_ALL_ON_RESUME 0xA4 +#define SSD1306_DISPLAY_ALL_ON 0xA5 +#define SSD1306_NORMAL_DISPLAY 0xA6 +#define SSD1306_INVERT_DISPLAY 0xA7 +#define SSD1306_DISPLAY_OFF 0xAE +#define SSD1306_DISPLAY_ON 0xAF +#define SSD1306_NOP 0xE3 + +// Scrolling Commands +#define SSD1306_RIGHT_HORIZONTAL_SCROLL 0x26 +#define SSD1306_LEFT_HORIZONTAL_SCROLL 0x27 +#define SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL 0x29 +#define SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL 0x2A +#define SSD1306_DEACTIVATE_SCROLL 0x2E +#define SSD1306_ACTIVATE_SCROLL 0x2F +#define SSD1306_SET_VERTICAL_SCROLL_AREA 0xA3 + +// Addressing Setting Commands +#define SSD1306_SET_LOWER_COLUMN 0x00 +#define SSD1306_SET_HIGHER_COLUMN 0x10 +#define SSD1306_MEMORY_ADDR_MODE 0x20 +#define SSD1306_SET_COLUMN_ADDR 0x21 +#define SSD1306_SET_PAGE_ADDR 0x22 + +// Hardware Configuration Commands +#define SSD1306_SET_START_LINE 0x40 +#define SSD1306_SET_SEGMENT_REMAP 0xA0 +#define SSD1306_SET_MULTIPLEX_RATIO 0xA8 +#define SSD1306_COM_SCAN_DIR_INC 0xC0 +#define SSD1306_COM_SCAN_DIR_DEC 0xC8 +#define SSD1306_SET_DISPLAY_OFFSET 0xD3 +#define SSD1306_SET_COM_PINS 0xDA +#define SSD1306_CHARGE_PUMP 0x8D + +// Timing & Driving Scheme Setting Commands +#define SSD1306_SET_DISPLAY_CLOCK_DIV_RATIO 0xD5 +#define SSD1306_SET_PRECHARGE_PERIOD 0xD9 +#define SSD1306_SET_VCOM_DESELECT 0xDB + +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) + ssd1306_command(width - 1); // Column end address (127 = reset) + + i2c.end_transaction(); + + i2c.begin_transaction(SSD1306_DATA_CONTINUE_ADDRESS); + + int b; + for (b = 0; b < 1024; b++) { + ssd1306_data(frame[b]); + } + i2c.end_transaction(); + } + void clear() { + clear_buffer(); + display(); + } + void setBrightness(uint8_t brightness) { + ssd1306_command(SSD1306_SET_CONTRAST_CONTROL); + ssd1306_command(brightness); + } + + void displayOn() { ssd1306_command(SSD1306_DISPLAY_ON); } + + void displayOff() { ssd1306_command(SSD1306_DISPLAY_OFF); } + + void setColor(uint8_t color) { this->color = color; } + + 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)); + } + } + + 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 print(const 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(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; } + +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; + + I2CDevice i2c; + unsigned char _addr; + + unsigned char height; + unsigned char width; + + void clear_buffer() { memset(frame, 0x00, sizeof(frame)); } + + int ssd1306_command(unsigned char command) { return i2c.send(0x00, command); } + + int ssd1306_data(unsigned char value) { return i2c.send(0x40, value); } +}; +#endif + +#endif // SSD1306_DISPLAY_H diff --git a/build.sh b/build.sh index f384628e..740b6188 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,25 @@ #!/bin/bash set -e +function enable_i2c { + if command -v raspi-config &> /dev/null; then + if [[ $(sudo raspi-config nonint get_i2c) -eq 1 ]] ; then + echo "Enabling i2c" + sudo modprobe i2c-dev + sudo raspi-config nonint do_i2c 0 + fi + if [[ $(grep -c '^dtparam=i2c_arm=on$' /boot/config.txt) -ge 1 ]] ; then + echo "Setting the i2c clock speed to 400 kHz, you will have to reboot for this to take effect." + sudo sed -i -e 's/dtparam=i2c_arm=on$/dtparam=i2c_arm=on,i2c_arm_baudrate=400000/g' /boot/config.txt + elif [[ $(grep -c '^dtparam=i2c_arm=on$' /boot/firmware/config.txt) -ge 1 ]] ; then + echo "Setting the i2c clock speed to 400 kHz, you will have to reboot for this to take effect." + sudo sed -i -e 's/dtparam=i2c_arm=on$/dtparam=i2c_arm=on,i2c_arm_baudrate=400000/g' /boot/firmware/config.txt + fi + else + echo "Can not automatically enable i2c you might have to do this manually" + fi +} + DEBUG="" while getopts ":s:d" opt; do @@ -34,39 +53,22 @@ 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 smtp.c -Iexternal/TinyWebsockets/tiny_websockets_lib/include $ws -Iexternal/OpenThings-Framework-Firmware-Library/ $otf -lpthread -lmosquitto -lssl -lcrypto -elif [ "$1" == "osbo" ]; then - echo "Installing required libraries..." - apt-get install -y libmosquitto-dev libssl-dev - echo "Compiling osbo firmware..." - - ws=$(ls external/TinyWebsockets/tiny_websockets_lib/src/*.cpp) - otf=$(ls external/OpenThings-Framework-Firmware-Library/*.cpp) - g++ -o OpenSprinkler -DOSBO -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 smtp.c -Iexternal/TinyWebsockets/tiny_websockets_lib/include $ws -Iexternal/OpenThings-Framework-Firmware-Library/ $otf -lpthread -lmosquitto -lssl -lcrypto + 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 else echo "Installing required libraries..." apt-get update - apt-get install -y libmosquitto-dev raspi-gpio libi2c-dev libssl-dev libgpiod-dev - if ! command -v raspi-gpio &> /dev/null - then - echo "Command raspi-gpio is required and is not installed" - exit 0 - fi + apt-get install -y libmosquitto-dev libi2c-dev libssl-dev libgpiod-dev gpiod + enable_i2c - USEGPIO="" - GPIOLIB="" - - - if [ -h "/sys/class/gpio/gpiochip512" ]; then - echo "using libgpiod" - USEGPIO="-DLIBGPIOD" - GPIOLIB="-lgpiod" - fi + USEGPIO="-DLIBGPIOD" + GPIOLIB="-lgpiod" echo "Compiling ospi firmware..." + ws=$(ls external/TinyWebsockets/tiny_websockets_lib/src/*.cpp) otf=$(ls external/OpenThings-Framework-Firmware-Library/*.cpp) - g++ -o OpenSprinkler -DOSPI $USEGPIO -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 smtp.c -Iexternal/TinyWebsockets/tiny_websockets_lib/include $ws -Iexternal/OpenThings-Framework-Firmware-Library/ $otf -lpthread -lmosquitto -lssl -lcrypto $GPIOLIB + g++ -o OpenSprinkler -DOSPI $USEGPIO -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 + fi if [ -f /etc/init.d/OpenSprinkler.sh ]; then diff --git a/defines.h b/defines.h index 8eb4519f..6fad1734 100755 --- a/defines.h +++ b/defines.h @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX/ESP8266) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * OpenSprinkler macro defines and hardware pin assignments @@ -24,7 +24,7 @@ #ifndef _DEFINES_H #define _DEFINES_H -// #define ENABLE_DEBUG // enable serial debug +//#define ENABLE_DEBUG // enable serial debug typedef unsigned long ulong; @@ -35,12 +35,11 @@ typedef unsigned long ulong; // if this number is different from the one stored in non-volatile memory // a device reset will be automatically triggered -#define OS_FW_MINOR 0 // Firmware minor version +#define OS_FW_MINOR 1 // Firmware minor version /** Hardware version base numbers */ #define OS_HW_VERSION_BASE 0x00 // OpenSprinkler #define OSPI_HW_VERSION_BASE 0x40 // OpenSprinkler Pi -#define OSBO_HW_VERSION_BASE 0x80 // OpenSprinkler Beagle #define SIM_HW_VERSION_BASE 0xC0 // simulation hardware /** Hardware type macro defines */ @@ -77,6 +76,7 @@ typedef unsigned long ulong; #define NOTIFY_SENSOR2 0x0040 #define NOTIFY_RAINDELAY 0x0080 #define NOTIFY_STATION_ON 0x0100 +#define NOTIFY_FLOW_ALERT 0x0200 /** HTTP request macro defines */ #define HTTP_RQT_SUCCESS 0 @@ -255,9 +255,9 @@ enum { IOPT_SUBNET_MASK3, IOPT_SUBNET_MASK4, IOPT_FORCE_WIRED, - IOPT_RESERVE_1, - IOPT_RESERVE_2, - IOPT_RESERVE_3, + IOPT_LATCH_ON_VOLTAGE, + IOPT_LATCH_OFF_VOLTAGE, + IOPT_NOTIF2_ENABLE, IOPT_RESERVE_4, IOPT_RESERVE_5, IOPT_RESERVE_6, @@ -344,6 +344,8 @@ enum { #define digitalReadExt digitalRead #define digitalWriteExt digitalWrite + #define USE_DISPLAY + #define USE_LCD #elif defined(ESP8266) // for ESP8266 #define OS_HW_VERSION (OS_HW_VERSION_BASE+30) @@ -356,7 +358,8 @@ enum { #define LCD_I2CADDR 0x3C // 128x64 OLED display I2C address #define EEPROM_I2CADDR 0x50 // 24C02 EEPROM I2C address - #define PIN_CURR_SENSE A0 + #define PIN_CURR_SENSE A0 // current sensing pin + #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 @@ -426,6 +429,9 @@ enum { #define V2_PIN_SENSOR1 3 // sensor 1 #define V2_PIN_SENSOR2 10 // sensor 2 + #define USE_DISPLAY + #define USE_SSD1306 + #elif defined(OSPI) // for OSPi #define OS_HW_VERSION OSPI_HW_VERSION_BASE @@ -437,27 +443,18 @@ enum { #define PIN_SENSOR1 14 #define PIN_SENSOR2 23 #define PIN_RFTX 15 // RF transmitter pin - //#define PIN_BUTTON_1 23 // button 1 - //#define PIN_BUTTON_2 24 // button 2 - //#define PIN_BUTTON_3 25 // button 3 + #define PIN_BUTTON_1 24 // button 1 + #define PIN_BUTTON_2 18 // button 2 + #define PIN_BUTTON_3 10 // button 3 - #define PIN_FREE_LIST {5,6,7,8,9,10,11,12,13,16,18,19,20,21,23,24,25,26} // free GPIO pins + #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 -#elif defined(OSBO) // for OSBo - - #define OS_HW_VERSION OSBO_HW_VERSION_BASE - // these are gpio pin numbers, refer to - // https://github.com/mkaczanowski/BeagleBoneBlack-GPIO/blob/master/GPIO/GPIOConst.cpp - #define PIN_SR_LATCH 60 // P9_12, shift register latch pin - #define PIN_SR_DATA 30 // P9_11, shift register data pin - #define PIN_SR_CLOCK 31 // P9_13, shift register clock pin - #define PIN_SR_OE 50 // P9_14, shift register output enable pin - #define PIN_SENSOR1 48 - #define PIN_RFTX 51 // RF transmitter pin + #define SDA 0 + #define SCL 0 - #define PIN_FREE_LIST {38,39,34,35,45,44,26,47,27,65,63,62,37,36,33,32,61,86,88,87,89,76,77,74,72,73,70,71} - #define ETHER_BUFFER_SIZE 16384 + #define USE_DISPLAY + #define USE_SSD1306 #else // for demo / simulation // use fake hardware pins @@ -513,8 +510,9 @@ enum { #define PSTR(x) x #define F(x) x #define strcat_P strcat - #define strncat_P strncat + #define strncat_P strncat #define strcpy_P strcpy + #define memcpy_P memcpy #define snprintf_P snprintf #include #define String string diff --git a/font.h b/font.h index a1649938..dc1ad7f1 100644 --- a/font.h +++ b/font.h @@ -1,6 +1,8 @@ -#if defined(ESP8266) // 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 + const unsigned char Monospaced_plain_13[] PROGMEM = { 0x08, // Width: 8 0x11, // Height: 17 @@ -458,4 +460,4 @@ const unsigned char Monospaced_plain_13[] PROGMEM = { 0x00,0x00,0x00,0xC0,0x80,0x00,0x08,0x87,0x00,0x00,0xFC,0x00,0x00,0x38,0x00,0x08,0x07,0x00,0xC0,0x01 // 255 }; -#endif +#endif \ No newline at end of file diff --git a/gpio.cpp b/gpio.cpp index 8e773121..4c06aeeb 100644 --- a/gpio.cpp +++ b/gpio.cpp @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2014 by Ray Wang (ray@opensprinkler.com) * * GPIO functions @@ -175,276 +175,7 @@ unsigned char digitalReadExt(unsigned char pin) { } #endif -#elif defined(OSPI) || defined(OSBO) - -#if !defined(LIBGPIOD) // use classic sysfs - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define BUFFER_MAX 64 -#define GPIO_MAX 64 - -// GPIO file descriptors -static int sysFds[GPIO_MAX] = { - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -} ; - -// Interrupt service routine functions -static void (*isrFunctions [GPIO_MAX])(void); - -static volatile int pinPass = -1 ; -static pthread_mutex_t pinMutex ; - -/** Export gpio pin */ -static unsigned char GPIOExport(int pin) { - char buffer[BUFFER_MAX]; - int fd, len; - - fd = open("/sys/class/gpio/export", O_WRONLY); - if (fd < 0) { - DEBUG_PRINTLN("failed to open export for writing"); - return 0; - } - - len = snprintf(buffer, sizeof(buffer), "%d", pin); - write(fd, buffer, len); - close(fd); - return 1; -} - -#if 0 -/** Unexport gpio pin */ -static unsigned char GPIOUnexport(int pin) { - char buffer[BUFFER_MAX]; - int fd, len; - - fd = open("/sys/class/gpio/unexport", O_WRONLY); - if (fd < 0) { - DEBUG_PRINTLN("failed to open unexport for writing"); - return 0; - } - - len = snprintf(buffer, sizeof(buffer), "%d", pin); - write(fd, buffer, len); - close(fd); - return 1; -} -#endif - -/** Set interrupt edge mode */ -static unsigned char GPIOSetEdge(int pin, const char *edge) { - char path[BUFFER_MAX]; - int fd; - - snprintf(path, BUFFER_MAX, "/sys/class/gpio/gpio%d/edge", pin); - - fd = open(path, O_WRONLY); - if (fd < 0) { - DEBUG_PRINTLN("failed to open gpio edge for writing"); - return 0; - } - write(fd, edge, strlen(edge)+1); - close(fd); - return 1; -} - -/** Set pin mode, in or out */ -void pinMode(int pin, unsigned char mode) { - static const char dir_str[] = "in\0out"; - - char path[BUFFER_MAX]; - int fd; - - snprintf(path, BUFFER_MAX, "/sys/class/gpio/gpio%d/direction", pin); - - struct stat st; - if(stat(path, &st)) { - if (!GPIOExport(pin)) return; - } - - fd = open(path, O_WRONLY); - if (fd < 0) { - DEBUG_PRINTLN("failed to open gpio direction for writing"); - return; - } - - if (-1 == write(fd, &dir_str[(INPUT==mode)||(INPUT_PULLUP==mode)?0:3], (INPUT==mode)||(INPUT_PULLUP==mode)?2:3)) { - DEBUG_PRINTLN("failed to set direction"); - return; - } - - close(fd); -#if defined(OSPI) - if(mode==INPUT_PULLUP) { - char cmd[BUFFER_MAX]; - //snprintf(cmd, BUFFER_MAX, "gpio -g mode %d up", pin); - snprintf(cmd, BUFFER_MAX, "raspi-gpio set %d pu", pin); - system(cmd); - } -#endif - return; -} - -/** Open file for digital pin */ -int gpio_fd_open(int pin, int mode) { - char path[BUFFER_MAX]; - int fd; - - snprintf(path, BUFFER_MAX, "/sys/class/gpio/gpio%d/value", pin); - fd = open(path, mode); - if (fd < 0) { - DEBUG_PRINTLN("failed to open gpio"); - return -1; - } - return fd; -} - -/** Close file */ -void gpio_fd_close(int fd) { - close(fd); -} - -/** Read digital value */ -unsigned char digitalRead(int pin) { - char value_str[3]; - - int fd = gpio_fd_open(pin, O_RDONLY); - if (fd < 0) { - return 0; - } - - if (read(fd, value_str, 3) < 0) { - DEBUG_PRINTLN("failed to read value"); - return 0; - } - - close(fd); - return atoi(value_str); -} - -/** Write digital value given file descriptor */ -void gpio_write(int fd, unsigned char value) { - static const char value_str[] = "01"; - - if (1 != write(fd, &value_str[LOW==value?0:1], 1)) { - DEBUG_PRINT("failed to write value on pin "); - } -} - -/** Write digital value */ -void digitalWrite(int pin, unsigned char value) { - int fd = gpio_fd_open(pin); - if (fd < 0) { - return; - } - gpio_write(fd, value); - close(fd); -} - -static int HiPri (const int pri) { - struct sched_param sched ; - - memset (&sched, 0, sizeof(sched)) ; - - if (pri > sched_get_priority_max (SCHED_RR)) - sched.sched_priority = sched_get_priority_max (SCHED_RR) ; - else - sched.sched_priority = pri ; - - return sched_setscheduler (0, SCHED_RR, &sched) ; -} - -static int waitForInterrupt (int pin, int mS) -{ - int fd, x ; - uint8_t c ; - struct pollfd polls ; - - if((fd=sysFds[pin]) < 0) - return -2; - - polls.fd = fd ; - polls.events = POLLPRI ; // Urgent data! - - x = poll (&polls, 1, mS) ; -// Do a dummy read to clear the interrupt -// A one character read appars to be enough. -// Followed by a seek to reset it. - - (void)read (fd, &c, 1); - lseek (fd, 0, SEEK_SET); - - return x ; -} - -static void *interruptHandler (void *arg) { - int myPin ; - - (void) HiPri (55) ; // Only effective if we run as root - - myPin = pinPass ; - pinPass = -1 ; - - for (;;) - if (waitForInterrupt (myPin, -1) > 0) - isrFunctions[myPin]() ; - - return NULL ; -} - -#include "utils.h" -/** Attach an interrupt function to pin */ -void attachInterrupt(int pin, const char* mode, void (*isr)(void)) { - if((pin<0)||(pin>GPIO_MAX)) { - DEBUG_PRINTLN("pin out of range"); - return; - } - - // set pin to INPUT mode and set interrupt edge mode - pinMode(pin, INPUT); - GPIOSetEdge(pin, mode); - - char path[BUFFER_MAX]; - snprintf(path, BUFFER_MAX, "/sys/class/gpio/gpio%d/value", pin); - - // open gpio file - if(sysFds[pin]==-1) { - if((sysFds[pin]=open(path, O_RDWR))<0) { - DEBUG_PRINTLN("failed to open gpio value for reading"); - return; - } - } - - int count, i; - char c; - // clear any pending interrupts - ioctl (sysFds[pin], FIONREAD, &count) ; - for (i=0; i +#include + +extern "C" { +#include +#include +#include +#include "utils.h" +} + +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); + } + } + +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); + } +}; + +#endif // I2CD_H \ No newline at end of file diff --git a/images.h b/images.h index e9f87106..67e376c5 100644 --- a/images.h +++ b/images.h @@ -32,11 +32,15 @@ enum { #endif -#if defined(ESP8266) +#if defined(USE_SSD1306) + +#if not defined(ARDUINO) +#define PROGMEM +#endif #define WiFi_Logo_width 60 #define WiFi_Logo_height 36 -const char WiFi_Logo_image[] PROGMEM = { +const unsigned char WiFi_Logo_image[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, @@ -63,56 +67,56 @@ const char WiFi_Logo_image[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; -const char _iconimage_wifi_connected[] PROGMEM = { +const unsigned char _iconimage_wifi_connected[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0xA0, 0xA0, 0xA8, 0xA8, 0xAA, 0xAA, 0x00, 0x00, }; -const char _iconimage_wifi_disconnected[] PROGMEM = { +const unsigned char _iconimage_wifi_disconnected[] PROGMEM = { 0x00, 0x00, 0x00, 0x11, 0x0A, 0x04, 0x8A, 0x91, 0xA0, 0xA0, 0xA8, 0xA8, 0xAA, 0xAA, 0x00, 0x00, }; -const char _iconimage_remotext[] PROGMEM = { +const unsigned char _iconimage_remotext[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x62, 0x54, 0x48, 0x44, 0x22, 0x1F, 0x00, 0x00, }; -const char _iconimage_raindelay[] PROGMEM = { +const unsigned char _iconimage_raindelay[] PROGMEM = { 0x00, 0x00, 0x00, 0x1C, 0x22, 0x49, 0x49, 0x49, 0x59, 0x41, 0x41, 0x41, 0x22, 0x1C, 0x00, 0x00, }; -const char _iconimage_rain[] PROGMEM = { +const unsigned char _iconimage_rain[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x18, 0x24, 0x3E, 0x00, 0x2A, 0x2A, 0x00, 0x2A, 0x2A, 0x00, 0x00, 0x00, }; -const char _iconimage_soil[] PROGMEM = { +const unsigned char _iconimage_soil[] PROGMEM = { 0x00, 0x00, 0x10, 0x10, 0x28, 0x28, 0x44, 0x44, 0x8A, 0x8A, 0x92, 0x44, 0x38, 0x00, 0xC6, 0x38, }; -const char _iconimage_ether_connected[] PROGMEM = { +const unsigned char _iconimage_ether_connected[] PROGMEM = { 0x00, 0x00, 0x00, 0x38, 0x28, 0x38, 0x10, 0x10, 0xFE, 0x44, 0x44, 0xEE, 0xAA, 0xEE, 0x00, 0x00, }; -const char _iconimage_ether_disconnected[] PROGMEM = { +const unsigned char _iconimage_ether_disconnected[] PROGMEM = { 0x00, 0x00, 0x11, 0x0A, 0x04, 0xEA, 0xB1, 0xE0, 0x40, 0xFE, 0x44, 0xEE, @@ -121,14 +125,14 @@ const char _iconimage_ether_disconnected[] PROGMEM = { /* -const char _iconimage_flow[] PROGMEM = { +const unsigned char _iconimage_flow[] PROGMEM = { 0x00, 0x00, 0x0F, 0x0F, 0x03, 0x0F, 0x0F, 0x03, 0x1B, 0x18, 0x18, 0x18, 0x78, 0x78, 0x00, 0x00, }; -const char _iconimage_pswitch[] PROGMEM = { +const unsigned char _iconimage_pswitch[] PROGMEM = { 0x00, 0x00, 0x1E, 0x12, 0x12, 0x12, 0x1E, 0x02, 0x22, 0x32, 0x22, 0x20, @@ -137,9 +141,9 @@ const char _iconimage_pswitch[] PROGMEM = { */ -#elif defined(ARDUINO) +#elif defined(USE_LCD) -const char _iconimage_connected[] PROGMEM = { +const unsigned char _iconimage_connected[] PROGMEM = { B00000, B00000, B00000, @@ -150,7 +154,7 @@ const char _iconimage_connected[] PROGMEM = { B10101 }; -const char _iconimage_disconnected[] PROGMEM = { +const unsigned char _iconimage_disconnected[] PROGMEM = { B00000, B10100, B01000, @@ -161,7 +165,7 @@ const char _iconimage_disconnected[] PROGMEM = { B10101 }; -const char _iconimage_remotext[] PROGMEM = { +const unsigned char _iconimage_remotext[] PROGMEM = { B00000, B00000, B00000, @@ -172,7 +176,7 @@ const char _iconimage_remotext[] PROGMEM = { B11110 }; -const char _iconimage_raindelay[] PROGMEM = { +const unsigned char _iconimage_raindelay[] PROGMEM = { B00000, B01110, B10101, @@ -183,7 +187,7 @@ const char _iconimage_raindelay[] PROGMEM = { B01110 }; -const char _iconimage_rain[] PROGMEM = { +const unsigned char _iconimage_rain[] PROGMEM = { B00000, B00000, B00110, @@ -194,7 +198,7 @@ const char _iconimage_rain[] PROGMEM = { B10101 }; -const char _iconimage_soil[] PROGMEM = { +const unsigned char _iconimage_soil[] PROGMEM = { B00100, B00100, B01010, @@ -206,7 +210,7 @@ const char _iconimage_soil[] PROGMEM = { }; /* -const char _iconimage_flow[] PROGMEM = { +const unsigned char _iconimage_flow[] PROGMEM = { B00000, B00000, B00000, @@ -217,7 +221,7 @@ const char _iconimage_flow[] PROGMEM = { B00000 }; -const char _iconimage_pswitch[] PROGMEM = { +const unsigned char _iconimage_pswitch[] PROGMEM = { B00000, B11000, B10100, @@ -232,9 +236,7 @@ const char _iconimage_pswitch[] PROGMEM = { */ -#else - #endif -#endif +#endif \ No newline at end of file diff --git a/main.cpp b/main.cpp index 31166462..047ce807 100644 --- a/main.cpp +++ b/main.cpp @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * Main loop @@ -30,13 +30,12 @@ #include "opensprinkler_server.h" #include "mqtt.h" #include "main.h" +#include "notifier.h" #if defined(ARDUINO) #include #endif -#include "ArduinoJson.hpp" - #if defined(ARDUINO) #if defined(ESP8266) ESP8266WebServer *update_server = NULL; @@ -45,7 +44,6 @@ 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 - static uint16_t led_blink_ms = LED_FAST_BLINK; #else EthernetServer *m_server = NULL; EthernetClient *m_client = NULL; @@ -53,17 +51,23 @@ bool useEth = true; #endif unsigned long getNtpTime(); -#else // header and defs for RPI/BBB - +#else // header and defs for RPI/Linux + bool useEth = false; #endif #if defined(USE_OTF) OTF::OpenThingsFramework *otf = NULL; #endif -void push_message(int type, uint32_t lval=0, float fval=0.f, const char* sval=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 +#endif + void manual_start_program(unsigned char, unsigned char); -void remote_http_callback(char*); // Small variations have been added to the timing values below // to minimize conflicting events @@ -83,6 +87,7 @@ char tmp_buffer[TMP_BUFFER_SIZE*2]; // scratch buffer, make it twice as large to // ====== Object defines ====== OpenSprinkler os; // OpenSprinkler object ProgramData pd; // ProgramdData object +NotifQueue notif; // NotifQueue object /* ====== Robert Hillman (RAH)'s implementation of flow sensor ====== * flow_begin - time when valve turns on @@ -119,7 +124,7 @@ void flow_poll() { /* End of RAH implementation of flow sensor */ } -#if defined(ARDUINO) +#if defined(USE_DISPLAY) // ====== UI defines ====== static char ui_anim_chars[3] = {'.', 'o', 'O'}; @@ -146,14 +151,13 @@ bool ui_confirm(PGM_P str) { } void ui_state_machine() { - // to avoid ui_state_machine taking too much computation time // we run it only every UI_STATE_MACHINE_INTERVAL ms static uint32_t last_usm = 0; if(millis() - last_usm <= UI_STATE_MACHINE_INTERVAL) { return; } last_usm = millis(); -#if defined(ESP8266) +#if defined(USE_SSD1306) // process screen led static ulong led_toggle_timeout = 0; if(led_blink_ms) { @@ -188,36 +192,86 @@ void ui_state_machine() { if(!ui_confirm(PSTR("Start 2s test?"))) {ui_state = UI_STATE_DEFAULT; break;} manual_start_program(255, 0); } 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.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()); } #else { os.lcd.print(Ethernet.gatewayIP()); } #endif + #else + route_t route = get_route(); + char str[INET_ADDRSTRLEN]; + + 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 } 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.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()); } #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); + + 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 + ui_state = UI_STATE_DISP_IP; + #if defined(USE_SSD1306) + os.lcd.display(); + os.lcd.setAutoDisplay(true); + #endif } break; case BUTTON_2: @@ -298,10 +352,13 @@ void ui_state_machine() { break; } } +#endif + // ====================== // Setup Function // ====================== +#if defined(ARDUINO) void do_setup() { /* Clear WDT reset flag. */ #if defined(ESP8266) @@ -376,7 +433,7 @@ ISR(WDT_vect) #endif #else -void initalize_otf(); +void initialize_otf(); void do_setup() { initialiseEpoch(); // initialize time reference for millis() and micros() @@ -403,8 +460,9 @@ void do_setup() { os.mqtt.init(); os.status.req_mqtt_restart = true; - initalize_otf(); + initialize_otf(); } + #endif void turn_on_station(unsigned char sid, ulong duration); @@ -429,11 +487,12 @@ void reboot_in(uint32_t ms) { void handle_web_request(char *p); #endif + /** Main Loop */ void do_loop() { - // handle flow sensor using polling every 1ms (maximum freq 1/(2*1ms)=500Hz) static ulong flowpoll_timeout=0; + // handle flow sensor using polling every 1ms (maximum freq 1/(2*1ms)=500Hz) if(os.iopts[IOPT_SENSOR1_TYPE]==SENSOR_TYPE_FLOW) { ulong curr = millis(); if(curr!=flowpoll_timeout) { @@ -569,6 +628,14 @@ void do_loop() if(size>0) { if(size>ETHER_BUFFER_SIZE) size=ETHER_BUFFER_SIZE; int len = client.read((uint8_t*) ether_buffer, size); + // Hack: see if we may have another packet, in case there is an overly large packet + // This really should be implemented more gracefully + size = client.available(); + if(size>0) { + // There is still more data to read + if(size+len > ETHER_BUFFER_SIZE) size = ETHER_BUFFER_SIZE - len; // cap read size + len += client.read((uint8_t*) ether_buffer+len, size); + } if(len>0) { m_client = &client; ether_buffer[len] = 0; // properly end the buffer @@ -588,8 +655,11 @@ void do_loop() ui_state_machine(); -#else // Process Ethernet packets for RPI/BBB +#else // Process Ethernet packets for RPI/LINUX if(otf) otf->loop(); +#if defined(USE_DISPLAY) + ui_state_machine(); +#endif #endif // Process Ethernet packets // Start up MQTT when we have a network connection @@ -614,7 +684,7 @@ void do_loop() last_time = curr_time; if (os.button_timeout) os.button_timeout--; -#if defined(ARDUINO) +#if defined(USE_DISPLAY) if (!ui_state) os.lcd_print_time(curr_time); // print time #endif @@ -635,12 +705,12 @@ void do_loop() if (os.status.rain_delayed) { // rain delay started, record time os.raindelay_on_lasttime = curr_time; - push_message(NOTIFY_RAINDELAY, LOGDATA_RAINDELAY, 1); + notif.add(NOTIFY_RAINDELAY, LOGDATA_RAINDELAY, 1); } else { // rain delay stopped, write log write_log(LOGDATA_RAINDELAY, curr_time); - push_message(NOTIFY_RAINDELAY, LOGDATA_RAINDELAY, 0); + notif.add(NOTIFY_RAINDELAY, LOGDATA_RAINDELAY, 0); } os.old_status.rain_delayed = os.status.rain_delayed; } @@ -652,10 +722,10 @@ void do_loop() // send notification when sensor1 becomes active if(os.status.sensor1_active) { os.sensor1_active_lasttime = curr_time; - push_message(NOTIFY_SENSOR1, LOGDATA_SENSOR1, 1); + notif.add(NOTIFY_SENSOR1, LOGDATA_SENSOR1, 1); } else { write_log(LOGDATA_SENSOR1, curr_time); - push_message(NOTIFY_SENSOR1, LOGDATA_SENSOR1, 0); + notif.add(NOTIFY_SENSOR1, LOGDATA_SENSOR1, 0); } } os.old_status.sensor1_active = os.status.sensor1_active; @@ -664,10 +734,10 @@ void do_loop() // send notification when sensor1 becomes active if(os.status.sensor2_active) { os.sensor2_active_lasttime = curr_time; - push_message(NOTIFY_SENSOR2, LOGDATA_SENSOR2, 1); + notif.add(NOTIFY_SENSOR2, LOGDATA_SENSOR2, 1); } else { write_log(LOGDATA_SENSOR2, curr_time); - push_message(NOTIFY_SENSOR2, LOGDATA_SENSOR2, 0); + notif.add(NOTIFY_SENSOR2, LOGDATA_SENSOR2, 0); } } os.old_status.sensor2_active = os.status.sensor2_active; @@ -698,13 +768,19 @@ void do_loop() // check through all programs for(pid=0; pid0) { // program match found // check and process special program command if(process_special_program_command(prog.name, curr_time)) continue; + // get station ordering + unsigned char order[os.nstations]; + prog.gen_station_runorder(runcount, order); // process all selected stations - for(sid=0;sid>3; s=sid&0x07; // skip if the station is a master station (because master cannot be scheduled independently @@ -741,7 +817,11 @@ void do_loop() }// if prog.durations[sid] }// for sid if(match_found) { - push_message(NOTIFY_PROGRAM_SCHED, pid, prog.use_weather?os.iopts[IOPT_WATER_PERCENTAGE]:100); + notif.add(NOTIFY_PROGRAM_SCHED, pid, prog.use_weather?os.iopts[IOPT_WATER_PERCENTAGE]:100); + } + //delete run-once if on final runtime (stations have already been queued) + if(will_delete){ + pd.del(pid); } }// if check_match }// for pid @@ -859,7 +939,7 @@ void do_loop() // log flow sensor reading if flow sensor is used if(os.iopts[IOPT_SENSOR1_TYPE]==SENSOR_TYPE_FLOW) { write_log(LOGDATA_FLOWSENSE, curr_time); - push_message(NOTIFY_FLOWSENSOR, (flow_count>os.flowcount_log_start)?(flow_count-os.flowcount_log_start):0); + notif.add(NOTIFY_FLOWSENSOR, (flow_count>os.flowcount_log_start)?(flow_count-os.flowcount_log_start):0); } // in case some options have changed while executing the program @@ -896,11 +976,7 @@ void do_loop() } } } - - if(os.get_station_bit(mas_id - 1) == 0 && masbit == 1){ // notify master on event - push_message(NOTIFY_STATION_ON, mas_id - 1, 0); - } - + os.set_station_bit(mas_id - 1, masbit); } } @@ -916,13 +992,29 @@ void do_loop() // process dynamic events process_dynamic_events(curr_time); + // handle master on / off notif events + for (unsigned char mas = MASTER_1; mas < NUM_MASTER_ZONES; mas++) { + unsigned char mas_id = os.masters[mas][MASOPT_SID]; + if (mas_id) { // if this master station is defined + time_os_t laston = os.masters_last_on[mas]; + unsigned char masbit = os.get_station_bit(mas_id - 1); + if(!laston && masbit) { // master is about to turn on + notif.add(NOTIFY_STATION_ON, mas_id - 1, 0); + os.masters_last_on[mas] = curr_time; + } + if(laston > 0 && !masbit) { // master is about to turn off + notif.add(NOTIFY_STATION_OFF, mas_id - 1, (curr_time>laston) ? (curr_time-laston) : 0); + os.masters_last_on[mas] = 0; + } + } + } + // activate/deactivate valves os.apply_all_station_bits(); -#if defined(ARDUINO) +#if defined(USE_DISPLAY) // process LCD display if (!ui_state) { os.lcd_print_screen(ui_anim_chars[(unsigned long)curr_time%3]); } - #endif // handle reboot request @@ -932,9 +1024,10 @@ void do_loop() if (!os.status.program_busy) { // and if no program is scheduled to run in the next minute bool willrun = false; + bool will_delete = false; for(pid=0; pid0 || os.iopts[IOPT_REMOTE_EXT_MODE]) return; if (os.status.program_busy) return; -#if defined(ESP8266) - if (!useEth) { // todo: what about useEth==true? - if (os.get_wifi_mode()!=WIFI_MODE_STA || WiFi.status()!=WL_CONNECTED || os.state!=OS_STATE_CONNECTED) return; - } -#endif + if (!os.network_connected()) return; time_os_t ntz = os.now_tz(); if (os.checkwt_success_lasttime && (ntz > os.checkwt_success_lasttime + CHECK_WEATHER_SUCCESS_TIMEOUT)) { @@ -1055,9 +1144,11 @@ void check_weather() { void turn_on_station(unsigned char sid, ulong duration) { // RAH implementation of flow sensor flow_start=0; + //Added flow_gallons reset to station turn on. + flow_gallons=0; if (os.set_station_bit(sid, 1, duration)) { - push_message(NOTIFY_STATION_ON, sid, duration); + notif.add(NOTIFY_STATION_ON, sid, duration); } } @@ -1141,7 +1232,8 @@ void turn_off_station(unsigned char sid, time_os_t curr_time, unsigned char shif // log station run write_log(LOGDATA_STATION, curr_time); // LOG_TODO - push_message(NOTIFY_STATION_OFF, sid, pd.lastrun.duration); + notif.add(NOTIFY_STATION_OFF, sid, pd.lastrun.duration); + notif.add(NOTIFY_FLOW_ALERT, sid, pd.lastrun.duration); } } @@ -1176,7 +1268,6 @@ void process_dynamic_events(time_os_t curr_time) { && os.status.sensor2_active) sn2 = true; - // todo: handle sensor 2 unsigned char sid, s, bid, qid, igs, igs2, igrd; for(bid=0;bidst - curr_time < abs(start_adj)) { q->st += abs(start_adj); + seq_start_times[gid] += abs(start_adj); } q->deque_time = q->st + q->dur + dequeue_adj; @@ -1280,7 +1372,7 @@ void schedule_all_stations(time_os_t curr_time) { con_start_time++; } - handle_master_adjustments(curr_time, q); + handle_master_adjustments(curr_time, q, gid, seq_start_times); if (!os.status.program_busy) { os.status.program_busy = 1; // set program busy bit @@ -1331,7 +1423,7 @@ void manual_start_program(unsigned char pid, unsigned char uwt) { unsigned char sid, bid, s; if ((pid>0)&&(pid<255)) { pd.read(pid-1, &prog); - push_message(NOTIFY_PROGRAM_SCHED, pid-1, uwt?os.iopts[IOPT_WATER_PERCENTAGE]:100, ""); + notif.add(NOTIFY_PROGRAM_SCHED, pid-1, uwt?os.iopts[IOPT_WATER_PERCENTAGE]:100, 1); } for(sid=0;sid>3; @@ -1362,326 +1454,6 @@ void manual_start_program(unsigned char pid, unsigned char uwt) { } } -// ========================================== -// ====== PUSH NOTIFICATION FUNCTIONS ======= -// ========================================== -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]); -} - -#define PUSH_TOPIC_LEN 120 -#define PUSH_PAYLOAD_LEN TMP_BUFFER_SIZE - -void push_message(int type, uint32_t lval, float fval, const char* sval) { - static char topic[PUSH_TOPIC_LEN+1]; - static char payload[PUSH_PAYLOAD_LEN+1]; - char* postval = tmp_buffer+1; // +1 so we can fit a opening { before the loaded config - uint32_t volume; - - // check if ifttt key exists and also if the enable bit is set - os.sopt_load(SOPT_IFTTT_KEY, tmp_buffer); - bool ifttt_enabled = ((os.iopts[IOPT_NOTIF_ENABLE]&type)!=0) && (strlen(tmp_buffer)!=0); - -#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; - const char *email_password = NULL; - const char *email_recipient = NULL; - int email_port = DEFAULT_EMAIL_PORT; - int email_en = 0; - - os.sopt_load(SOPT_EMAIL_OPTS, postval); - if (*postval != 0) { - // Add the wrapping curly braces to the string - postval = tmp_buffer; - postval[0] = '{'; - int len = strlen(postval); - postval[len] = '}'; - postval[len+1] = 0; - - ArduinoJson::DeserializationError error = ArduinoJson::deserializeJson(doc, postval); - // Test the parsing otherwise parse - if (error) { - DEBUG_PRINT(F("mqtt: deserializeJson() failed: ")); - DEBUG_PRINTLN(error.c_str()); - } else { - email_en = doc["en"]; - email_host = doc["host"]; - email_port = doc["port"]; - email_username = doc["user"]; - email_password = doc["pass"]; - email_recipient= doc["recipient"]; - } - } - #endif - - #if defined(ESP8266) - EMailSender::EMailMessage email_message; - #else - struct { - String subject; - String message; - } email_message; - #endif - - bool email_enabled = false; -#if defined(SUPPORT_EMAIL) - if(!email_en){ - email_enabled = false; - }else{ - email_enabled = os.iopts[IOPT_NOTIF_ENABLE]&type; - } -#endif - - // if none if enabled, return here - if ((!ifttt_enabled) && (!email_enabled) && (!os.mqtt.enabled())) - return; - - if (ifttt_enabled || email_enabled) { - strcpy_P(postval, PSTR("{\"value1\":\"On site [")); - os.sopt_load(SOPT_DEVICE_NAME, topic, PUSH_TOPIC_LEN); - topic[PUSH_TOPIC_LEN]=0; - strcat(postval+strlen(postval), topic); - strcat_P(postval, PSTR("], ")); - if(email_enabled) { - strcat(topic, " "); - email_message.subject = topic; // prefix the email subject with device name - } - } - - if (os.mqtt.enabled()) { - topic[0] = 0; - payload[0] = 0; - } - - switch(type) { - case NOTIFY_STATION_ON: - - if (os.mqtt.enabled()) { - snprintf_P(topic, PUSH_TOPIC_LEN, PSTR("station/%d"), lval); - if((int)fval == 0){ - snprintf_P(payload, PUSH_PAYLOAD_LEN, PSTR("{\"state\":1}")); // master on event does not have duration attached to it - }else{ - snprintf_P(payload, PUSH_PAYLOAD_LEN, PSTR("{\"state\":1,\"duration\":%d}"), (int)fval); - } - } - - // todo: add IFTTT and email support for this event as well. - // currently no support due to the number of events exceeds 8 so need to use more than 1 byte - break; - - case NOTIFY_STATION_OFF: - - if (os.mqtt.enabled()) { - snprintf_P(topic, PUSH_TOPIC_LEN, PSTR("station/%d"), lval); - if (os.iopts[IOPT_SENSOR1_TYPE]==SENSOR_TYPE_FLOW) { - snprintf_P(payload, PUSH_PAYLOAD_LEN, PSTR("{\"state\":0,\"duration\":%d,\"flow\":%d.%02d}"), (int)fval, (int)flow_last_gpm, (int)(flow_last_gpm*100)%100); - } else { - snprintf_P(payload, PUSH_PAYLOAD_LEN, PSTR("{\"state\":0,\"duration\":%d}"), (int)fval); - } - } - if (ifttt_enabled || email_enabled) { - strcat_P(postval, PSTR("Station [")); - os.get_station_name(lval, postval+strlen(postval)); - if((int)fval == 0){ - strcat_P(postval, PSTR("] closed.")); - }else{ - strcat_P(postval, PSTR("] closed. It ran for ")); - size_t len = strlen(postval); - snprintf_P(postval + len, TMP_BUFFER_SIZE, PSTR(" %d minutes %d seconds."), (int)fval/60, (int)fval%60); - } - - if(os.iopts[IOPT_SENSOR1_TYPE]==SENSOR_TYPE_FLOW) { - size_t len = strlen(postval); - snprintf_P(postval + len, TMP_BUFFER_SIZE, PSTR(" Flow rate: %d.%02d"), (int)flow_last_gpm, (int)(flow_last_gpm*100)%100); - } - if(email_enabled) { email_message.subject += PSTR("station event"); } - } - break; - - case NOTIFY_PROGRAM_SCHED: - - if (ifttt_enabled || email_enabled) { - if (sval) strcat_P(postval, PSTR("manually scheduled ")); - else strcat_P(postval, PSTR("automatically scheduled ")); - strcat_P(postval, PSTR("Program ")); - { - ProgramStruct prog; - pd.read(lval, &prog); - if(lval0) { - strcat_P(postval, PSTR("external IP updated: ")); - unsigned char ip[4] = {(unsigned char)((lval>>24)&0xFF), - (unsigned char)((lval>>16)&0xFF), - (unsigned char)((lval>>8)&0xFF), - (unsigned char)(lval&0xFF)}; - ip2string(postval, TMP_BUFFER_SIZE, ip); - } - if(fval>=0) { - size_t len = strlen(postval); - snprintf_P(postval + len, TMP_BUFFER_SIZE, PSTR("water level updated: %d%%."), (int)fval); - } - if(email_enabled) { email_message.subject += PSTR("weather update event"); } - } - break; - - case NOTIFY_REBOOT: - if (os.mqtt.enabled()) { - strcpy_P(topic, PSTR("system")); - strcpy_P(payload, PSTR("{\"state\":\"started\"}")); - } - if (ifttt_enabled || email_enabled) { - #if defined(ARDUINO) - strcat_P(postval, PSTR("rebooted. Device IP: ")); - #if defined(ESP8266) - { - IPAddress _ip; - if (useEth) { - //_ip = Ethernet.localIP(); - _ip = eth.localIP(); - } else { - _ip = WiFi.localIP(); - } - 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 - if(email_enabled) { email_message.subject += PSTR("reboot event"); } - } - break; - } - - if (os.mqtt.enabled() && strlen(topic) && strlen(payload)) - os.mqtt.publish(topic, payload); - - if (ifttt_enabled) { - strcat_P(postval, PSTR("\"}")); - - BufferFiller bf = BufferFiller(ether_buffer, TMP_BUFFER_SIZE); - bf.emit_p(PSTR("POST /trigger/sprinkler/with/key/$O HTTP/1.0\r\n" - "Host: $S\r\n" - "Accept: */*\r\n" - "Content-Length: $D\r\n" - "Content-Type: application/json\r\n\r\n$S"), - SOPT_IFTTT_KEY, DEFAULT_IFTTT_URL, strlen(postval), postval); - - os.send_http_request(DEFAULT_IFTTT_URL, 80, ether_buffer, remote_http_callback); - } - - 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); // TODO: double check removing strdup - emailSend.setSMTPPort(email_port); - EMailSender::Response resp = emailSend.send(email_recipient, email_message); - // DEBUG_PRINTLN(F("Sending Status:")); - // DEBUG_PRINTLN(resp.status); - // DEBUG_PRINTLN(resp.code); - // DEBUG_PRINTLN(resp.desc); - } - #endif - #else - struct smtp *smtp = NULL; - String email_port_str = to_string(email_port); - // todo: check error? - 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_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()); - rc = smtp_mail(smtp, email_message.message.c_str()); - rc = smtp_close(smtp); - if (rc!=SMTP_STATUS_OK) { - DEBUG_PRINTF("SMTP: Error %s\n", smtp_status_code_errstr(rc)); - } - } - #endif - } -} // ================================ // ====== LOGGING FUNCTIONS ======= @@ -1764,7 +1536,7 @@ void write_log(unsigned char type, time_os_t curr_time) { } #endif -#else // prepare log folder for RPI/BBB +#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)) { @@ -1835,8 +1607,7 @@ void write_log(unsigned char type, time_os_t curr_time) { #if defined(ARDUINO) dtostrf(flow_last_gpm,5,2,tmp_buffer+strlen(tmp_buffer)); #else - size_t len = strlen(tmp_buffer); - snprintf(tmp_buffer + len, TMP_BUFFER_SIZE - len, "%5.2f", flow_last_gpm); + snprintf(tmp_buffer+strlen(tmp_buffer), TMP_BUFFER_SIZE, "%5.2f", flow_last_gpm); #endif } strcat_P(tmp_buffer, PSTR("]\r\n")); @@ -1914,7 +1685,7 @@ void delete_log(char *name) { } #endif -#else // delete_log implementation for RPI/BBB +#else // delete_log implementation for RPI/LINUX if (strncmp(name, "all", 3) == 0) { // delete the log folder rmdir(get_filename_fullpath(LOG_PREFIX)); @@ -2014,7 +1785,7 @@ static void perform_ntp_sync() { #endif } -#if !defined(ARDUINO) // main function for RPI/BBB +#if !defined(ARDUINO) // main function for RPI/LINUX int main(int argc, char *argv[]) { // Disable buffering to work with systemctl journal setvbuf(stdout, NULL, _IOLBF, 0); @@ -2032,7 +1803,7 @@ int main(int argc, char *argv[]) { } } - do_setup(); + do_setup(); while(true) { do_loop(); diff --git a/main.h b/main.h index b49793b0..717693b6 100644 --- a/main.h +++ b/main.h @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * Header file containing declarations of functions defined in main.cpp, diff --git a/mqtt.cpp b/mqtt.cpp index fa17b670..ea521622 100644 --- a/mqtt.cpp +++ b/mqtt.cpp @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX/ESP8266) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * OpenSprinkler library @@ -110,7 +110,6 @@ boolean checkPassword(char* pw) { if (os.iopts[IOPT_IGNORE_PASSWORD]) return true; if(findKeyVal(pw, tmp_buffer, TMP_BUFFER_SIZE, PSTR("pw"), true)){ - urlDecode(tmp_buffer); if (os.password_verify(tmp_buffer)) return true; }else{ DEBUG_LOGF("Device password not found.\r\n"); @@ -376,7 +375,6 @@ void OSMqtt::begin(void) { if(_sub_topic[0] == 0) { // subscribe topic is empty DEBUG_LOGF("No sub_topic found\r\n"); - // TODO: do not subscribe then } DEBUG_LOGF("MQTT Begin: Config (%s:%d %s) %s\r\n", _host, _port, _username, _enabled ? "Enabled" : "Disabled"); @@ -430,6 +428,7 @@ void OSMqtt::loop(void) { // Only attemp to reconnect every MQTT_RECONNECT_DELAY seconds to avoid blocking the main loop if (!_connected() && (millis() - last_reconnect_attempt >= MQTT_RECONNECT_DELAY * 1000UL)) { DEBUG_LOGF("MQTT Loop: Reconnecting\r\n"); + _done_subscribed = false; _connect(); last_reconnect_attempt = millis(); } @@ -592,7 +591,7 @@ const char * OSMqtt::_state_string(int rc) { } #else -/************************** RASPBERRY PI / BBB / DEMO ****************************************/ +/************************** RASPBERRY PI / Linux ****************************************/ static bool _connected = false; diff --git a/mqtt.h b/mqtt.h index 452d6d5c..65c2fd0a 100644 --- a/mqtt.h +++ b/mqtt.h @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX/ESP8266) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * OpenSprinkler library header file diff --git a/notifier.cpp b/notifier.cpp new file mode 100644 index 00000000..532e524a --- /dev/null +++ b/notifier.cpp @@ -0,0 +1,547 @@ +/* OpenSprinkler Unified Firmware + * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) + * + * Notifier data structures and functions + * Feb 2015 @ OpenSprinkler.com + * + * This file is part of the OpenSprinkler library + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * . + */ + +#include "notifier.h" +#include "program.h" +#include "ArduinoJson.hpp" +#include "opensprinkler_server.h" + +NotifNodeStruct* NotifQueue::head = NULL; +NotifNodeStruct* NotifQueue::tail = NULL; +unsigned char NotifQueue::nqueue = 0; + +extern OpenSprinkler os; +extern ProgramData pd; +extern char tmp_buffer[]; +extern char ether_buffer[]; +extern float flow_last_gpm; +void remote_http_callback(char*); + +bool is_notif_enabled(uint16_t type) { + uint16_t notif = (uint16_t)os.iopts[IOPT_NOTIF_ENABLE] | ((uint16_t)os.iopts[IOPT_NOTIF2_ENABLE] << 8); + return (notif&type) != 0; +} + +uint16_t get_notif_enabled() { + return (uint16_t)os.iopts[IOPT_NOTIF_ENABLE]|((uint16_t)os.iopts[IOPT_NOTIF2_ENABLE]<<8); +} + +void set_notif_enabled(uint16_t notif) { + os.iopts[IOPT_NOTIF_ENABLE] = notif&0xFF; + os.iopts[IOPT_NOTIF2_ENABLE] = notif >> 8; +} + +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) { + if (!is_notif_enabled(t)) { // if not subscribed to this type, return + return false; + } + if(nqueuenext = node; + } + tail = node; + nqueue++; + DEBUG_PRINTF("NotifQueue::add (type %d) [%d]\n", t, nqueue); + return true; + } + DEBUG_PRINTLN(F("NotifQueue::add queue is full!")); + return false; +} + +void NotifQueue::clear() { + while(nqueue!=0) { + NotifNodeStruct* node = head; + head = head->next; + if(head==NULL) { + tail = NULL; + } + delete node; + nqueue--; + } +} + +void push_message(uint16_t type, uint32_t lval, float fval, uint8_t bval); + +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; + nqueue--; + n--; + } + return true; +} + +#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) { + if (!is_notif_enabled(type)) { + return; + } + static char topic[PUSH_TOPIC_LEN+1]; + static char payload[PUSH_PAYLOAD_LEN+1]; + char* postval = tmp_buffer+1; // +1 so we can fit a opening { before the loaded config + + // check if ifttt key exists and also if the enable bit is set + os.sopt_load(SOPT_IFTTT_KEY, tmp_buffer); + bool ifttt_enabled = (strlen(tmp_buffer)!=0); + // flow rate + uint32_t flowrate100 = (((uint32_t)os.iopts[IOPT_PULSE_RATE_1])<<8) + os.iopts[IOPT_PULSE_RATE_0]; + +#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; + const char *email_password = NULL; + const char *email_recipient = NULL; + int email_port = DEFAULT_EMAIL_PORT; + int email_en = 0; + + os.sopt_load(SOPT_EMAIL_OPTS, postval); + if (*postval != 0) { + // Add the wrapping curly braces to the string + postval = tmp_buffer; + postval[0] = '{'; + int len = strlen(postval); + postval[len] = '}'; + postval[len+1] = 0; + + ArduinoJson::DeserializationError error = ArduinoJson::deserializeJson(doc, postval); + // Test the parsing otherwise parse + if (error) { + DEBUG_PRINT(F("mqtt: deserializeJson() failed: ")); + DEBUG_PRINTLN(error.c_str()); + } else { + email_en = doc["en"]; + email_host = doc["host"]; + email_port = doc["port"]; + email_username = doc["user"]; + email_password = doc["pass"]; + email_recipient= doc["recipient"]; + } + } + #endif + + #if defined(ESP8266) + EMailSender::EMailMessage email_message; + #else + struct { + String subject; + String message; + } email_message; + #endif + + bool email_enabled = false; +#if defined(SUPPORT_EMAIL) + if(!email_en){ // todo: this should be simplified + email_enabled = false; + }else{ + email_enabled = true; + } +#endif + + // if none if enabled, return here + if ((!ifttt_enabled) && (!email_enabled) && (!os.mqtt.enabled())) + return; + + if (ifttt_enabled || email_enabled) { + strcpy_P(postval, PSTR("{\"value1\":\"On site [")); + os.sopt_load(SOPT_DEVICE_NAME, topic, PUSH_TOPIC_LEN); + topic[PUSH_TOPIC_LEN]=0; + strcat(postval+strlen(postval), topic); + strcat_P(postval, PSTR("], ")); + if(email_enabled) { + strcat(topic, " "); + email_message.subject = topic; // prefix the email subject with device name + } + } + + if (os.mqtt.enabled()) { + topic[0] = 0; + payload[0] = 0; + } + + switch(type) { + case NOTIFY_STATION_ON: + + if (os.mqtt.enabled()) { + snprintf_P(topic, PUSH_TOPIC_LEN, PSTR("station/%d"), lval); + strcat_P(payload, PSTR("{\"state\":1")); + if((int)fval > 0){ + snprintf_P(payload+strlen(payload), PUSH_PAYLOAD_LEN, PSTR(",\"duration\":%d"), (int)fval); + } + strcat_P(payload, PSTR("}")); + } + if (ifttt_enabled || email_enabled) { + strcat_P(postval, PSTR("Station [")); + os.get_station_name(lval, postval+strlen(postval)); + strcat_P(postval, PSTR("] just turned on.")); + if((int)fval > 0){ + strcat_P(postval, PSTR(" It's scheduled to run for ")); + snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR(" %d minutes %d seconds."), (int)fval/60, (int)fval%60); + } + if(email_enabled) { email_message.subject += PSTR("station event"); } + } break; + + case NOTIFY_STATION_OFF: + + if (os.mqtt.enabled()) { + snprintf_P(topic, PUSH_TOPIC_LEN, PSTR("station/%d"), lval); + strcat_P(payload, PSTR("{\"state\":0")); + if((int)fval > 0) { + 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("}")); + } + if (ifttt_enabled || email_enabled) { + strcat_P(postval, PSTR("Station [")); + os.get_station_name(lval, postval+strlen(postval)); + strcat_P(postval, PSTR("] closed.")); + if((int)fval > 0) { + strcat_P(postval, PSTR(" It ran for ")); + snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR(" %d minutes %d seconds."), (int)fval/60, (int)fval%60); + } + + 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"); } + } + break; + + case NOTIFY_FLOW_ALERT:{ + //First determine if a Flow Alert should be sent based on flow amount and setpoint + + //Added variable to track flow alert status + bool flow_alert_flag = false; + + //Added variable for flow_gpm_alert_setpoint and set default value to max + float flow_gpm_alert_setpoint = 999.9f; + + //Added variable for tmp station name + char tmp_station_name[STATION_NAME_SIZE]; + + //Get satation name + os.get_station_name(lval, tmp_station_name); + + // only proceed if flow rate is positive, and the station name has at least 5 characters + if (flow_last_gpm > 0 && strlen(tmp_station_name) > 5) { + const char *station_name_last_five_chars = tmp_station_name; + // extract the last 5 characters + station_name_last_five_chars = tmp_station_name + strlen(tmp_station_name) - 5; + // Convert last five characters to number and check if valid + // Had to switch to use strtod because sscanf in AVR doesn't work with float :( + 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 + //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) { + flow_alert_flag = true; + } + } else { + //Could not convert to a valid number. If a number is not detected as a station name suffix, never send an alert + flow_alert_flag = false; + } + } else { + //Station name was not long enough to include 5 character flow setpoint. + flow_alert_flag = false; + } + + // If flow_alert_flag is true, format the appropriate messages, else don't send alert + if (flow_alert_flag == true) { + + if (os.mqtt.enabled()) { + //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 + } + + + if (ifttt_enabled || email_enabled) { + //Format ifttt\email message + + // 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) + 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); + 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 + + strcat_P(postval, PSTR(", Station [")); + //Truncate flow setpoint value off station name to shorten ifttt\email message + tmp_station_name[(strlen(tmp_station_name) - 5)] = '\0'; + strcat_P(postval, tmp_station_name); + strcat_P(postval, PSTR("]")); + if(fval > 0){ // if there is a valid duration + strcat_P(postval, PSTR(" ran for ")); + snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR("%d minutes %d seconds."), (int)fval/60, ((int)fval%60)); + } + + 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. + //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; + email_enabled=false; + } + break; + } + + case NOTIFY_PROGRAM_SCHED: + + if (ifttt_enabled || email_enabled) { + if (bval) strcat_P(postval, PSTR("manually")); + else strcat_P(postval, PSTR("automatically")); + strcat_P(postval, PSTR(" scheduled Program ")); + { + ProgramStruct prog; + pd.read(lval, &prog); + if(lval0) { + strcat_P(postval, PSTR("external IP updated: ")); + unsigned char ip[4] = {(unsigned char)((lval>>24)&0xFF), + (unsigned char)((lval>>16)&0xFF), + (unsigned char)((lval>>8)&0xFF), + (unsigned char)(lval&0xFF)}; + ip2string(postval, TMP_BUFFER_SIZE, ip); + } + if(fval>=0) { + snprintf_P(postval+strlen(postval), TMP_BUFFER_SIZE, PSTR("water level updated: %d%%."), (int)fval); + } + if(email_enabled) { email_message.subject += PSTR("weather update event"); } + } + break; + + case NOTIFY_REBOOT: + if (os.mqtt.enabled()) { + strcpy_P(topic, PSTR("system")); + snprintf_P(payload, PUSH_PAYLOAD_LEN, PSTR("{\"state\":\"started\",\"cause\":%d}"), (int)os.last_reboot_cause); + } + if (ifttt_enabled || email_enabled) { + #if defined(ARDUINO) + 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) { + //_ip = Ethernet.localIP(); + _ip = eth.localIP(); + } else { + _ip = WiFi.localIP(); + } + 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 + if(email_enabled) { email_message.subject += PSTR("reboot event"); } + } + break; + } + + if (os.mqtt.enabled() && strlen(topic) && strlen(payload)) + os.mqtt.publish(topic, payload); + + if (ifttt_enabled) { + strcat_P(postval, PSTR("\"}")); + + BufferFiller bf = BufferFiller(ether_buffer, TMP_BUFFER_SIZE); + bf.emit_p(PSTR("POST /trigger/sprinkler/with/key/$O HTTP/1.0\r\n" + "Host: $S\r\n" + "Accept: */*\r\n" + "Content-Length: $D\r\n" + "Content-Type: application/json\r\n\r\n$S"), + SOPT_IFTTT_KEY, DEFAULT_IFTTT_URL, strlen(postval), postval); + + os.send_http_request(DEFAULT_IFTTT_URL, 80, ether_buffer, remote_http_callback); + } + + 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); + } + #endif + #else + struct smtp *smtp = NULL; + 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_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()); + rc = smtp_mail(smtp, email_message.message.c_str()); + rc = smtp_close(smtp); + if (rc!=SMTP_STATUS_OK) { + DEBUG_PRINTF("SMTP: Error %s\n", smtp_status_code_errstr(rc)); + } + } + #endif + } +} diff --git a/notifier.h b/notifier.h new file mode 100644 index 00000000..2572bd84 --- /dev/null +++ b/notifier.h @@ -0,0 +1,59 @@ +/* OpenSprinkler Unified Firmware + * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) + * + * Notifier data structures and functions header file + * Feb 2015 @ OpenSprinkler.com + * + * This file is part of the OpenSprinkler library + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * . + */ + + +#ifndef _NOTIFIER_H +#define _NOTIFIER_H + +#define NOTIF_QUEUE_MAXSIZE 32 + +#include "OpenSprinkler.h" +#include "types.h" + +/** Notifier Node data structure */ +struct NotifNodeStruct { + uint16_t type; + uint32_t lval; + float fval; + 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) + { } +}; + +/** Notifier Queue data structure */ +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); + // 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; +}; + +#endif // _NOTIFIER_H \ No newline at end of file diff --git a/opensprinkler_server.cpp b/opensprinkler_server.cpp index f2acfc5a..8d073d30 100644 --- a/opensprinkler_server.cpp +++ b/opensprinkler_server.cpp @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * Server functions @@ -206,7 +206,7 @@ unsigned char findKeyVal (const char *str,char *strbuf, uint16_t maxlen,const ch } void rewind_ether_buffer() { - bfill = BufferFiller(ether_buffer, ETHER_BUFFER_SIZE*2); + bfill = BufferFiller(ether_buffer, ETHER_BUFFER_SIZE*2); ether_buffer[0] = 0; } @@ -389,7 +389,6 @@ boolean check_password(char *p) p = get_buffer; } if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("pw"), true)) { - urlDecode(tmp_buffer); if (os.password_verify(tmp_buffer)) return true; } #else @@ -570,9 +569,10 @@ void server_change_stations(OTF_PARAMS_DEF) { for(sid=0;sid sizeof(HTTPStationData)) { handle_return(HTML_DATA_OUTOFBOUND); } @@ -687,10 +684,13 @@ void server_manual_program(OTF_PARAMS_DEF) { /** * Change run-once program - * Command: /cr?pw=xxx&t=[x,x,x...] + * Command: /cr?pw=xxx&t=[x,x,x...]&cnt?=xxx&int?=xxx&uwt?=xxx * * pw: password * t: station water time + * cnt?: repeat count + * int?: repeat interval + * uwt?: use weather adjustment */ void server_change_runonce(OTF_PARAMS_DEF) { #if defined(USE_OTF) @@ -701,7 +701,9 @@ void server_change_runonce(OTF_PARAMS_DEF) { char *p = get_buffer; // decode url first + #if !defined(USE_OTF) if(p) urlDecode(p); + #endif // search for the start of t=[ char *pv; boolean found=false; @@ -718,11 +720,65 @@ void server_change_runonce(OTF_PARAMS_DEF) { // reset all stations and prepare to run one-time program reset_all_stations_immediate(); - unsigned char sid, bid, s; + ProgramStruct prog; + uint16_t dur; + for(int i=0;i 0 ? dur : 0; + } + + //check if repeat count is defined and create program to perform the repetitions + if(findKeyVal(FKV_SOURCE,tmp_buffer,TMP_BUFFER_SIZE,PSTR("cnt"),true)){ + prog.starttimes[1] = (uint16_t)atol(tmp_buffer) - 1; + if(prog.starttimes[1] >= 0){ + if(findKeyVal(FKV_SOURCE,tmp_buffer,TMP_BUFFER_SIZE,PSTR("int"),true)){ + prog.starttimes[2] = (uint16_t)atol(tmp_buffer); + }else{ + handle_return(HTML_DATA_MISSING); + } + //check for positive interval length + if(prog.starttimes[2] < 1){ + handle_return(HTML_DATA_OUTOFBOUND); + } + unsigned long curr_time = os.now_tz(); + + curr_time = (curr_time / 60) + prog.starttimes[2] + 1; //time in minutes for one interval past current time + uint16_t epoch_t = curr_time / 1440; + + //if repeat count and interval are defined --> complete program + prog.enabled = 1; + prog.use_weather = 0; + if(findKeyVal(FKV_SOURCE,tmp_buffer,TMP_BUFFER_SIZE,PSTR("uwt"),true)){ + if((uint16_t)atol(tmp_buffer)){ + prog.use_weather = 1; + } + } + prog.oddeven = 0; + prog.type = 1; + prog.starttime_type = 0; + prog.en_daterange = 0; + prog.days[0] = (epoch_t >> 8) & 0b11111111; //one interval past current day in epoch time + prog.days[1] = epoch_t & 0b11111111; //one interval past current day in epoch time + prog.starttimes[0] = curr_time % 1440; //one interval past current time + strcpy_P(prog.name, PSTR("Run-Once with repeat")); + + //if no more repeats, remove interval to flag for deletion + if(prog.starttimes[1] == 0){ + prog.starttimes[2] = 0; + } + + if(!pd.add(&prog)){ + handle_return(HTML_DATA_OUTOFBOUND); + } + } + } + + //No repeat count defined or first repeat --> use old API + unsigned char sid, bid, s; boolean match_found = false; for(sid=0;sid>3; s=sid&0x07; // if non-zero duration is given @@ -849,9 +905,10 @@ void server_change_program(OTF_PARAMS_DEF) { // parse program name if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("name"), true)) { + #if !defined(USE_OTF) urlDecode(tmp_buffer); - strReplace(tmp_buffer, '\"', '\''); - strReplace(tmp_buffer, '\\', '/'); + #endif + strReplaceQuoteBackslash(tmp_buffer); strncpy(prog.name, tmp_buffer, PROGRAM_NAME_SIZE); } else { strcpy_P(prog.name, _str_program); @@ -881,7 +938,6 @@ void server_change_program(OTF_PARAMS_DEF) { #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; - // TODO: may need a urlDecode for non-ESP platforms #else // parse ad-hoc v=[... // search for the start of v=[ @@ -971,8 +1027,11 @@ void server_json_options_main() { if (os.hw_type==HW_TYPE_AC || os.hw_type==HW_TYPE_UNKNOWN) continue; else v<<=2; } + if (oid==IOPT_LATCH_ON_VOLTAGE || oid==IOPT_LATCH_OFF_VOLTAGE) { + if (os.hw_type!=HW_TYPE_LATCH) continue; + } #else - if (oid==IOPT_BOOST_TIME) continue; + if (oid==IOPT_BOOST_TIME || oid==IOPT_LATCH_ON_VOLTAGE || oid==IOPT_LATCH_OFF_VOLTAGE) continue; #endif #if defined(ESP8266) @@ -994,8 +1053,8 @@ void server_json_options_main() { } #endif #else - // for Linux-based platforms, there is no LCD currently - if(oid==IOPT_LCD_CONTRAST || oid==IOPT_LCD_BACKLIGHT || oid==IOPT_LCD_DIMMING) continue; + // for Linux-based platforms, we can't adjust contrast or backlight + if(oid==IOPT_LCD_CONTRAST || oid==IOPT_LCD_BACKLIGHT) continue; #endif // each json name takes 5 characters @@ -1263,7 +1322,7 @@ void server_home(OTF_PARAMS_DEF) * rd: rain delay hours (0 turns off rain delay) * re: remote extension mode * ap: reset to ap (ESP8266 only) - * update: launch update script (for OSPi/OSBo/Linux only) + * update: launch update script (for OSPi/Linux only) */ void server_change_values(OTF_PARAMS_DEF) { @@ -1361,14 +1420,18 @@ void server_change_scripturl(OTF_PARAMS_DEF) { #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); } @@ -1450,14 +1513,19 @@ 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; } } 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)) { if(os.iopts[IOPT_USE_WEATHER]==WEATHER_METHOD_MONTHLY) { load_wt_monthly(tmp_buffer); @@ -1466,12 +1534,13 @@ void server_change_options(OTF_PARAMS_DEF) weather_change = true; // if wto has changed } } - //DEBUG_PRINTLN(os.sopt_load(SOPT_WEATHER_OPTS)); } 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; @@ -1480,7 +1549,9 @@ 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; @@ -1489,7 +1560,9 @@ 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) { @@ -1500,7 +1573,9 @@ 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; @@ -1508,9 +1583,10 @@ 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); - strReplace(tmp_buffer, '\"', '\''); - strReplace(tmp_buffer, '\\', '/'); + #endif + strReplaceQuoteBackslash(tmp_buffer); os.sopt_save(SOPT_DEVICE_NAME, tmp_buffer); } @@ -1573,7 +1649,6 @@ void server_change_password(OTF_PARAMS_DEF) { const int pwBufferSize = TMP_BUFFER_SIZE/2; char *tbuf2 = tmp_buffer + pwBufferSize; // use the second half of tmp_buffer if (findKeyVal(FKV_SOURCE, tbuf2, pwBufferSize, PSTR("cpw"), true) && strncmp(tmp_buffer, tbuf2, pwBufferSize) == 0) { - urlDecode(tmp_buffer); os.sopt_save(SOPT_PASSWORD, tmp_buffer); handle_return(HTML_SUCCESS); } else { @@ -1659,12 +1734,12 @@ void server_change_manual(OTF_PARAMS_DEF) { RuntimeQueueStruct *q = NULL; unsigned char sqi = pd.station_qid[sid]; // check if the station already has a schedule - if (sqi!=0xFF) { // if so, we will overwrite the schedule - q = pd.queue+sqi; + if (sqi!=0xFF) { // if so, do nothing + } else { // otherwise create a new queue element q = pd.enqueue(); } - // if the queue is not full + // if the queue is not full (and the station doesn't already have a schedule if (q) { q->st = 0; q->dur = timer; @@ -1778,7 +1853,7 @@ void server_json_log(OTF_PARAMS_DEF) { if (!sd.exists(tmp_buffer)) continue; SdFile file; file.open(tmp_buffer, O_READ); -#else // prepare to open log file for RPI/BBB +#else // prepare to open log file for Linux FILE *file = fopen(get_filename_fullpath(tmp_buffer), "rb"); if(!file) continue; #endif // prepare to open log file @@ -1866,8 +1941,9 @@ void server_delete_log(OTF_PARAMS_DEF) { } /** - * Command: "/pq?pw=x&dur=x" + * Command: "/pq?pw=x&dur=x&repl=x" * dur: duration (in units of seconds) + * 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) @@ -1877,6 +1953,19 @@ void server_pause_queue(OTF_PARAMS_DEF) { #endif ulong duration = 0; + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("repl"), true)) { + duration = strtoul(tmp_buffer, NULL, 0); + pd.resume_stations(); + os.status.pause_state = 0; + if(duration != 0){ + os.pause_timer = duration; + pd.set_pause(); + os.status.pause_state = 1; + } + + handle_return(HTML_SUCCESS); + } + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("dur"), true)) { duration = strtoul(tmp_buffer, NULL, 0); } @@ -2083,7 +2172,9 @@ void on_sta_update(OTF_PARAMS_DEF) { } void on_sta_upload_fin() { - if(!(update_server->hasArg("pw") && os.password_verify(update_server->arg("pw").c_str()))) { + if (os.iopts[IOPT_IGNORE_PASSWORD]) { + // don't check password + } else if(!(update_server->hasArg("pw") && os.password_verify(update_server->arg("pw").c_str()))) { update_server_send_result(HTML_UNAUTHORIZED); Update.end(false); return; @@ -2193,7 +2284,7 @@ void start_server_ap() { #endif #if defined(USE_OTF) && !defined(ARDUINO) -void initalize_otf() { +void initialize_otf() { if(!otf) return; static bool callback_initialized = false; diff --git a/opensprinkler_server.h b/opensprinkler_server.h index 329c5a82..1635236c 100644 --- a/opensprinkler_server.h +++ b/opensprinkler_server.h @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * Server functions diff --git a/platformio.ini b/platformio.ini index 18c82ae1..cdd227a0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,7 +18,6 @@ board = d1_mini framework = arduino lib_ldf_mode = deep lib_deps = - sui77/rc-switch @ ^2.6.3 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 @@ -30,7 +29,7 @@ board_build.flash_mode = dio board_build.ldscript = eagle.flash.4m2m.ld board_build.f_cpu = 160000000L board_build.f_flash = 80000000L - +;build_flags = -DENABLE_DEBUG [env:sanguino_atmega1284p] platform = atmelavr @@ -42,7 +41,7 @@ lib_ldf_mode = deep lib_deps = https://github.com/UIPEthernet/UIPEthernet/archive/refs/tags/v2.0.12.zip knolleary/PubSubClient @ ^2.8 - greiman/SdFat @ 1.0.7 + https://github.com/greiman/SdFat/archive/refs/tags/1.0.7.zip Wire build_src_filter = +<*> - -- monitor_speed=115200 diff --git a/program.cpp b/program.cpp index 88dfdfce..73fff7d1 100644 --- a/program.cpp +++ b/program.cpp @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * Program data structures and functions @@ -231,14 +231,17 @@ unsigned char ProgramStruct::check_day_match(time_os_t t) { unsigned char weekday_t = weekday(t); // weekday ranges from [0,6] within Sunday being 1 unsigned char day_t = day(t); unsigned char month_t = month(t); -#else // get current time from RPI/BBB + unsigned char year_t = year(t); +#else // get current time from RPI/LINUX time_os_t ct = t; struct tm *ti = gmtime(&ct); unsigned char weekday_t = (ti->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] + unsigned char year_t = ti->tm_year+1900; // tm_year is years since 1900 #endif // get current time + int epoch_t = (t / 86400); unsigned char wd = (weekday_t+5)%7; unsigned char dt = day_t; @@ -261,13 +264,26 @@ unsigned char ProgramStruct::check_day_match(time_os_t t) { return 0; break; - case PROGRAM_TYPE_BIWEEKLY: - // todo future + case PROGRAM_TYPE_SINGLERUN: + // check match of exact day + if(((days[0] << 8) + days[1]) != epoch_t) + return 0; break; case PROGRAM_TYPE_MONTHLY: - if (dt != (days[0]&0b11111)) + if ((days[0]&0b11111) == 0) { + if(month_t == 2){ + if((isLeapYear(year_t) && dt != 29) || (!isLeapYear(year_t) && dt != 28)){ + return 0; + } + }else{ + if(!isLastDayofMonth(month_t, dt)) + return 0; + } + } else if (dt != (days[0]&0b11111)){ return 0; + } + break; case PROGRAM_TYPE_INTERVAL: @@ -294,7 +310,9 @@ unsigned char ProgramStruct::check_day_match(time_os_t t) { // Check if a given time matches program's start time // this also checks for programs that started the previous // day and ran over night -unsigned char ProgramStruct::check_match(time_os_t t) { +// Return value: 0 if no match; otherwise return the n-th count of the match. +// For example, if this is the first-run of the day, return 1 etc. +unsigned char ProgramStruct::check_match(time_os_t t, bool *to_delete) { // check program enable status if (!enabled) return 0; @@ -310,21 +328,49 @@ unsigned char ProgramStruct::check_match(time_os_t t) { if (starttime_type) { // given start time type + unsigned char maxStartTime = -1; + for(unsigned char i=0;i maxStartTime){ + maxStartTime = starttime_decode(starttimes[i]); + } + } for(unsigned char i=0;i delete + if (current_minute == starttime_decode(starttimes[i])){ + if(maxStartTime == current_minute && type == PROGRAM_TYPE_SINGLERUN){ + *to_delete = true; + }else{ + *to_delete = false; + } + return (i+1); // if curren_minute matches any of the given start time, return matched index + 1 + } } return 0; // otherwise return 0 } else { // repeating type // if current_minute matches start time, return 1 - if (current_minute == start) return 1; + // if also no interval and run once --> delete + if (current_minute == start){ + if(!interval && type == PROGRAM_TYPE_SINGLERUN){ + *to_delete = true; + }else{ + *to_delete = false; + } + return 1; + } // otherwise, current_minute must be larger than start time, and interval must be non-zero if (current_minute > start && interval) { // check if we are on any interval match int16_t c = (current_minute - start) / interval; if ((c * interval == (current_minute - start)) && c <= repeat) { - return 1; + //if c == repeat (final repeat) and program is run-once --> delete + if(c == repeat && type == PROGRAM_TYPE_SINGLERUN){ + *to_delete = true; + }else{ + *to_delete = false; + } + return (c+1); // return match count n } } } @@ -337,12 +383,106 @@ unsigned char ProgramStruct::check_match(time_os_t t) { // t-86400L matches the program's start day int16_t c = (current_minute - start + 1440) / interval; if ((c * interval == (current_minute - start + 1440)) && c <= repeat) { - return 1; + //if c == repeat (final repeat) and program is run-once --> delete + if(c == repeat && type == PROGRAM_TYPE_SINGLERUN){ + *to_delete = true; + }else{ + *to_delete = false; + } + return (c+1); // return the match count n } } return 0; } +struct StationNameSortElem { + unsigned char idx; + char *name; +}; + +int StationNameSortAscendCmp(const void *a, const void *b) { + return strcmp(((StationNameSortElem*)a)->name,((StationNameSortElem*)b)->name); +} + +int StationNameSortDescendCmp(const void *a, const void *b) { + return StationNameSortAscendCmp(b, a); +} + +// generate station runorder based on the annotation in program names +// alternating means on the odd numbered runs of the program, it uses one order; on the even runs, it uses the opposite order +void ProgramStruct::gen_station_runorder(uint16_t runcount, unsigned char *order) { + unsigned char len = strlen(name); + unsigned char ns = os.nstations; + int16_t i; + unsigned char temp; + + // default order: ascending by index + for(i=0;i=2 && name[len-2]=='>') { + char anno = name[len-1]; + switch(anno) { + case 'I': // descending by index + case 'a': // alternating: odd-numbered runs ascending by index, even-numbered runs descending. + case 'A': // odd-numbered runs descending by index, even-numbered runs ascending + { + if((anno=='I') || ((anno=='a') && (runcount%2==0)) || ((anno=='A') && (runcount%2==1))) { + // reverse the order + for(i=0;ibiweekly, 2->monthly, 3->interval + // 0: weekly, 1->single-run, 2->monthly, 3->interval unsigned char type:2; // starttime type: @@ -78,10 +78,10 @@ class ProgramStruct { // enable date range unsigned char en_daterange:1; - // weekly: days[0][0..6] correspond to Monday till Sunday - // bi-weekly:days[0][0..6] and [1][0..6] store two weeks - // 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) + // 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 + // 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]; // When the program is a fixed start time type: @@ -104,7 +104,8 @@ class ProgramStruct { char name[PROGRAM_NAME_SIZE]; int16_t daterange[2] = {MIN_ENCODED_DATE, MAX_ENCODED_DATE}; // date range: start date, end date - unsigned char check_match(time_os_t t); + unsigned char check_match(time_os_t t, bool *to_delete); + void gen_station_runorder(uint16_t runcount, unsigned char *order); int16_t starttime_decode(int16_t t); protected: diff --git a/rpitime.h b/rpitime.h new file mode 100644 index 00000000..49ff07e8 --- /dev/null +++ b/rpitime.h @@ -0,0 +1,31 @@ +#ifndef RPI_TIME_H +#define RPI_TIME_H + +#include + +static int hour(time_t ct) { + struct tm *ti = gmtime(&ct); + return ti->tm_hour; +} + +static int minute(time_t ct) { + struct tm *ti = gmtime(&ct); + return ti->tm_min; +} + +static int second(time_t ct) { + struct tm *ti = gmtime(&ct); + return ti->tm_sec; +} + +static int day(time_t ct) { + struct tm *ti = gmtime(&ct); + return ti->tm_mday; +} + +static int month(time_t ct) { + struct tm *ti = gmtime(&ct); + return ti->tm_mon+1; +} + +#endif \ No newline at end of file diff --git a/utils.cpp b/utils.cpp index b9209ec1..bfcc3121 100644 --- a/utils.cpp +++ b/utils.cpp @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * Utility functions @@ -37,9 +37,11 @@ extern OpenSprinkler os; extern SdFat sd; #endif -#else // RPI/BBB +#else // RPI/LINUX -static char* get_runtime_path() { +#include + +char* get_runtime_path() { static char path[PATH_MAX]; static unsigned char query = 1; @@ -97,19 +99,6 @@ void delay(ulong howLong) nanosleep (&sleeper, &dummy) ; } -void delayMicrosecondsHard (ulong howLong) -{ - struct timeval tNow, tLong, tEnd ; - - gettimeofday (&tNow, NULL) ; - tLong.tv_sec = howLong / 1000000 ; - tLong.tv_usec = howLong % 1000000 ; - timeradd (&tNow, &tLong, &tEnd) ; - - while (timercmp (&tNow, &tEnd, <)) - gettimeofday (&tNow, NULL) ; -} - void delayMicroseconds (ulong howLong) { struct timespec sleeper ; @@ -128,6 +117,19 @@ void delayMicroseconds (ulong howLong) } } +void delayMicrosecondsHard (ulong howLong) +{ + struct timeval tNow, tLong, tEnd ; + + gettimeofday (&tNow, NULL) ; + tLong.tv_sec = howLong / 1000000 ; + tLong.tv_usec = howLong % 1000000 ; + timeradd (&tNow, &tLong, &tEnd) ; + + while (timercmp (&tNow, &tEnd, <)) + gettimeofday (&tNow, NULL) ; +} + static uint64_t epochMilli, epochMicro ; void initialiseEpoch() @@ -184,8 +186,107 @@ unsigned int detect_rpi_rev() { } return rev; } + +route_t get_route() { + route_t route; + char iface[16]; + unsigned long dst, gw; + unsigned int flags, refcnt, use, metric, mask, mtu, window, irtt; + + FILE *filp; + char buf[512]; + char term; + filp = fopen("/proc/net/route", "r"); + if(filp) { + while(fgets(buf, sizeof(buf), filp) != NULL) { + if(sscanf(buf, "%s %lx %lx %X %d %d %d %lx %d %d %d", iface, &dst, &gw, &flags, &refcnt, &use, &metric, &mask, &mtu, &window, &irtt) == 11) { + if(flags & RTF_UP) { + if(dst==0) { + strcpy(route.iface, iface); + route.gateway = gw; + route.destination = dst; + } + } + } + } + fclose(filp); + } + return route; +} + +in_addr_t get_ip_address(char *iface) { + struct ifaddrs *ifaddr; + struct ifaddrs *ifa; + in_addr_t ip = 0; + if(getifaddrs(&ifaddr) == -1) { + return 0; + } + + ifa = ifaddr; + + while(ifa) { + if(ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET) { + if(strcmp(ifa->ifa_name, iface)==0) { + ip = ((struct sockaddr_in *)ifa->ifa_addr)->sin_addr.s_addr; + break; + } + } + ifa = ifa->ifa_next; + } + freeifaddrs(ifaddr); + return ip; +} #endif +bool prefix(const char *pre, const char *str) { + 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; +} + #endif @@ -503,8 +604,17 @@ void strReplace(char *str, char c, char r) { } } +void strReplaceQuoteBackslash(char *buf) { + strReplace(buf, '\"', '\''); + strReplace(buf, '\\', '/'); +} + static const unsigned char month_days[] = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; +bool isLastDayofMonth(unsigned char month, unsigned char day) { + return day == month_days[month-1]; +} + bool isValidDate(unsigned char m, unsigned char d) { if(m<1 || m>12) return false; if(d<1 || d>month_days[m-1]) return false; @@ -520,6 +630,10 @@ bool isValidDate(uint16_t date) { return isValidDate(month, day); } +bool isLeapYear(uint16_t y){ // Accepts 4 digit year and returns if leap year + return (y%400==0) || ((y%4==0) && (y%100!=0)); +} + #if defined(ESP8266) unsigned char hex2dec(const char *hex) { return strtol(hex, NULL, 16); diff --git a/utils.h b/utils.h index 6a740753..14140aa7 100644 --- a/utils.h +++ b/utils.h @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * Utility functions header file @@ -26,11 +26,14 @@ #if defined(ARDUINO) #include -#else // headers for RPI/BBB +#else // headers for RPI/LINUX #include #include #include - + #include + #include + #include + #include #endif #include "defines.h" @@ -53,13 +56,16 @@ ulong 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 *); +void strReplaceQuoteBackslash(char *); void peel_http_header(char*); void strReplace(char *, char c, char r); #define date_encode(m,d) ((m<<5)+d) #define MIN_ENCODED_DATE date_encode(1,1) #define MAX_ENCODED_DATE date_encode(12, 31) +bool isLastDayofMonth(unsigned char month, unsigned char day); bool isValidDate(uint16_t date); +bool isLeapYear(uint16_t year); // whether a 4 digit year is a leap year #if defined(ESP8266) unsigned char hex2dec(const char *hex); bool isHex(char c); @@ -69,20 +75,41 @@ void str2mac(const char *_str, unsigned char mac[]); #if defined(ARDUINO) -#else // Arduino compatible functions for RPI/BBB +#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 delay(ulong ms); + void delayMicroseconds(ulong us); void delayMicrosecondsHard(ulong us); ulong millis(); ulong micros(); void initialiseEpoch(); #if defined(OSPI) unsigned int detect_rpi_rev(); + char* get_runtime_path(); + + struct route_t { + char iface[16]; + in_addr_t gateway; + in_addr_t destination; + }; + + route_t get_route(); + in_addr_t get_ip_address(char *iface); #endif + enum BoardType { + Unknown, + RaspberryPi_Unknown, + RaspberryPi_bcm2712, + RaspberryPi_bcm2711, + RaspberryPi_bcm2837, + RaspberryPi_bcm2836, + RaspberryPi_bcm2835, + }; + + BoardType get_board_type(); #endif #endif // _UTILS_H diff --git a/weather.cpp b/weather.cpp index d8289fa8..a36d7eab 100644 --- a/weather.cpp +++ b/weather.cpp @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * Weather functions @@ -132,10 +132,7 @@ static void getweather_callback_with_peel_header(char* buffer) { } void GetWeather() { -#if defined(ESP8266) - if (!useEth) - if (os.state!=OS_STATE_CONNECTED || WiFi.status()!=WL_CONNECTED) return; -#endif + if(!os.network_connected()) return; // use temp buffer to construct get command BufferFiller bf = BufferFiller(tmp_buffer, TMP_BUFFER_SIZE*2); int method = os.iopts[IOPT_USE_WEATHER]; diff --git a/weather.h b/weather.h index 27fb9a1f..8caa8932 100644 --- a/weather.h +++ b/weather.h @@ -1,4 +1,4 @@ -/* OpenSprinkler Unified (AVR/RPI/BBB/LINUX) Firmware +/* OpenSprinkler Unified Firmware * Copyright (C) 2015 by Ray Wang (ray@opensprinkler.com) * * Weather functions header file