├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── atmega328 ├── .gitignore ├── .travis.yml ├── high.bin ├── lib │ └── readme.txt ├── platformio.ini └── sonoffsc │ └── sonoffsc.ino ├── docs ├── FWTRX-TAMCUSC-SONOFFSC-ATMEGA328P.zip ├── Sonoff-SC-Schematic.pdf └── Sonoff-SC-User-Guide.pdf └── esp8266 ├── .gitignore ├── .travis.yml ├── debug ├── gulpfile.js ├── html ├── checkboxes.css ├── checkboxes.js ├── custom.css ├── custom.js ├── favicon.ico ├── grids-responsive-min.css ├── images │ ├── border-off.png │ ├── border-on.png │ ├── handle-center.png │ ├── handle-left.png │ ├── handle-right.png │ ├── label-off.png │ └── label-on.png ├── index.html ├── jquery-1.12.3.min.js ├── pure-min.css └── side-menu.css ├── package.json ├── platformio.ini └── sonoffsc ├── alexa.ino ├── button.ino ├── comms.ino ├── config ├── all.h ├── arduino.h ├── general.h ├── prototypes.h └── version.h ├── data └── index.html.gz ├── debug.ino ├── domoticz.ino ├── lights.ino ├── mqtt.ino ├── nofuss.ino ├── ntp.ino ├── ota.ino ├── settings.h ├── settings.ino ├── sonoffsc.ino ├── static └── index.html.gz.h ├── telnet.ino ├── utils.ino ├── web.ino └── wifi.ino /.gitignore: -------------------------------------------------------------------------------- 1 | *.s#? 2 | *.b#? 3 | .modgit 4 | firmware* 5 | *.gch 6 | .pio* 7 | .clang_complete 8 | .gcc-flags.json 9 | .sconsign.dblite 10 | credentials.h 11 | .build 12 | node_modules 13 | code/utils 14 | *.suo 15 | atmega328/sonoffsc/Debug/board.buildinfo 16 | *.a 17 | atmega328/sonoffsc/Debug/sonoffsc.cpp.d 18 | *.elf 19 | atmega328/sonoffsc/Debug/sonoffsc.hex 20 | *.hex 21 | atmega328/sonoffsc/Release/board.buildinfo 22 | *.d 23 | atmega328/sonoffsc/Release/sonoffsc.cpp.o 24 | *.sln 25 | atmega328/sonoffsc/sonoffsc.vcxproj 26 | *.filters 27 | atmega328/sonoffsc/__vm/.sonoffsc.vsarduino.h 28 | *.xml 29 | atmega328/sonoffsc/Debug/sonoffsc.cpp.o 30 | esp8266/sonoffsc/all.h 31 | esp8266/sonoffsc/debug.h 32 | *.vcxproj 33 | esp8266/sonoffsc/__vm/.sonoffsc.vsarduino.h 34 | esp8266/sonoffsc/Debug/board.buildinfo 35 | esp8266/sonoffsc/Debug/sonoffsc.bin 36 | esp8266/sonoffsc/Debug/sonoffsc.cpp.o 37 | esp8266/sonoffsc/Debug/sonoffsc.ino.bin 38 | esp8266/sonoffsc/Release/board.buildinfo 39 | esp8266/sonoffsc/Release/sonoffsc.bin 40 | esp8266/sonoffsc/Release/sonoffsc.cpp.o 41 | esp8266/sonoffsc/Release/sonoffsc.ino.bin 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | sudo: false 5 | cache: 6 | directories: 7 | - "~/.platformio" 8 | install: 9 | - pip install -U platformio 10 | - cd esp8266 ; npm install --only=dev ; cd .. 11 | script: 12 | - cd atmega328 && platformio run && cd ../esp8266 && node node_modules/gulp/bin/gulp.js && platformio run 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # SonoffSC change log 2 | 3 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 4 | and this project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [1.1.1] 2017-10-06 7 | ### Changed 8 | - Reduced comm speed to 9600 bauds to get more reliable results 9 | 10 | ### Fixed 11 | - Fixed color translation from RGB to long 12 | 13 | ## [1.1.0] 2017-10-01 14 | Lots of changes from ESPurna 15 | 16 | ## [1.0.0] 2017-06-27 17 | ### Added 18 | - Support for microwave presence sensor 19 | - Support for small fan to move air and get better dust measurements (testing) 20 | - Reset via MQTT 21 | - Debug messages via UDP 22 | - Debug messages in program memory (still migrating) 23 | - Force user to change default password 24 | - New favicon 25 | 26 | ### Changed 27 | - Support for RGB LED ring based on the WS2812FX library 28 | - Embedded web code inside firmware (fast access, more space for code) 29 | - Do not scan networks if just one defined 30 | - Using "restart" instead of "reset" to reset board (cleaner sessions) 31 | - Flash memory layout (will probably require erasing and reflash ESP8266) 32 | - Using long values for communication between microcontrollers 33 | 34 | ### Fixed 35 | - Wifi persistance bug in ESP8266 36 | 37 | ## [0.3.0] 2017-02-13 38 | ### Added 39 | - Support for RGB LED ring (thanks to Blair Thompson) 40 | 41 | ## [0.2.0] 2017-01-12 42 | ### Added 43 | - Enable/disable clap mode with voice commands using Alexa 44 | 45 | ### Fixed 46 | - Ported MQTT connectivity fixes from ESPurna firmware 47 | 48 | ### Removed 49 | - NTP support removed 50 | 51 | ## [0.1.0] 2017-01-10 52 | - Initial stable version 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SonoffSC 2 | 3 | Custom firmware for the Sonoff SC (both for the ATMega328P and the ESP8266). 4 | 5 | [![version](https://badge.fury.io/gh/xoseperez%2Fsonoffsc.svg)](CHANGELOG.md) 6 | [![travis](https://travis-ci.org/xoseperez/sonoffsc.svg?branch=master)](https://travis-ci.org/xoseperez/sonoffsc) 7 | [![license](https://img.shields.io/github/license/xoseperez/sonoffsc.svg)](LICENSE) 8 | [![donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=xose%2eperez%40gmail%2ecom&lc=US&no_note=0¤cy_code=EUR&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHostedGuest) 9 | [![twitter](https://img.shields.io/twitter/follow/xoseperez.svg?style=social)](https://twitter.com/intent/follow?screen_name=xoseperez) 10 | 11 | ## Features 12 | 13 | * Up to 5 different **configurable WiFi networks** with static IP support 14 | * **MQTT** enabled, send sensor data to your local/cloud broker 15 | * Support for all the sensors in the board 16 | * **DHT11** but also for the **DHT22** (upgrading hardware) 17 | * **Sharp GP2Y1010AU0F** 18 | * **GM55 LDR** 19 | * **Electret microphone** 20 | * Support for extra sensors and actuators 21 | * **WS2812 RGB LED ring** for notifications 22 | * Small **fan** to move air inside the enclosure and get better dust readings 23 | * **Microwave based presence detector 24 | * **Clap monitoring** (switch light on/off clapping your hands) 25 | * **Support for RGB LED ring** for notifications you can drive via MQTT messages (thanks to Blair Thompson) 26 | * Fast asynchronous **HTTP Server** 27 | * Basic authentication 28 | * Web-based configuration 29 | * Websockets-based communication between the device and the browser 30 | * Web server code cleaned, merged and embedded into firmware for fastest access 31 | * **NTP** synchronisation 32 | * **Over-The-Air** (OTA) updates (only for the ESP8266) 33 | * **Remote OTA** updates using the [NoFuss library](https://bitbucket.org/xoseperez/nofuss) (only for the ESP8266) 34 | * [**Domoticz**](https://domoticz.com/) integration via MQTT 35 | * Enable/disable clap mode from **Alexa** 36 | * **Debug** messages via UDP & Telnet 37 | 38 | ## Documentation 39 | 40 | *TODO* 41 | 42 | Both projects (for the atmega328p and the esp8266) are ready to be built with [PlatformIO](http://platformio.org/). 43 | If you do not use PlatformIO check the platformio.ini files for dependencies (in the "lib_deps" parameter). 44 | 45 | Know more here: 46 | * [http://tinkerman.cat/sonoff-sc-with-mqtt-and-domoticz-support](http://tinkerman.cat/sonoff-sc-with-mqtt-and-domoticz-support) 47 | * [http://tinkerman.cat/itead-studio-sonoff-sc-revisited/](http://tinkerman.cat/itead-studio-sonoff-sc-revisited/) 48 | 49 | ## License 50 | 51 | Copyright (C) 2017-2018 by Xose Pérez (@xoseperez) 52 | 53 | This program is free software: you can redistribute it and/or modify 54 | it under the terms of the GNU General Public License as published by 55 | the Free Software Foundation, either version 3 of the License, or 56 | (at your option) any later version. 57 | 58 | This program is distributed in the hope that it will be useful, 59 | but WITHOUT ANY WARRANTY; without even the implied warranty of 60 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 61 | GNU General Public License for more details. 62 | 63 | You should have received a copy of the GNU General Public License 64 | along with this program. If not, see . 65 | -------------------------------------------------------------------------------- /atmega328/.gitignore: -------------------------------------------------------------------------------- 1 | .pioenvs 2 | .clang_complete 3 | .gcc-flags.json 4 | .piolibdeps 5 | -------------------------------------------------------------------------------- /atmega328/.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < http://docs.platformio.org/en/stable/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < http://docs.platformio.org/en/stable/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < http://docs.platformio.org/en/stable/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choice one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | # language: python 28 | # python: 29 | # - "2.7" 30 | # 31 | # sudo: false 32 | # cache: 33 | # directories: 34 | # - "~/.platformio" 35 | # 36 | # install: 37 | # - pip install -U platformio 38 | # 39 | # script: 40 | # - platformio run 41 | 42 | 43 | # 44 | # Template #2: The project is intended to by used as a library with examples 45 | # 46 | 47 | # language: python 48 | # python: 49 | # - "2.7" 50 | # 51 | # sudo: false 52 | # cache: 53 | # directories: 54 | # - "~/.platformio" 55 | # 56 | # env: 57 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 58 | # - PLATFORMIO_CI_SRC=examples/file.ino 59 | # - PLATFORMIO_CI_SRC=path/to/test/directory 60 | # 61 | # install: 62 | # - pip install -U platformio 63 | # 64 | # script: 65 | # - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N 66 | -------------------------------------------------------------------------------- /atmega328/high.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/atmega328/high.bin -------------------------------------------------------------------------------- /atmega328/lib/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for the project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link to executable file. 4 | 5 | The source code of each library should be placed in separate directory, like 6 | "lib/private_lib/[here are source files]". 7 | 8 | For example, see how can be organized `Foo` and `Bar` libraries: 9 | 10 | |--lib 11 | | |--Bar 12 | | | |--docs 13 | | | |--examples 14 | | | |--src 15 | | | |- Bar.c 16 | | | |- Bar.h 17 | | |--Foo 18 | | | |- Foo.c 19 | | | |- Foo.h 20 | | |- readme.txt --> THIS FILE 21 | |- platformio.ini 22 | |--src 23 | |- main.c 24 | 25 | Then in `src/main.c` you should use: 26 | 27 | #include 28 | #include 29 | 30 | // rest H/C/CPP code 31 | 32 | PlatformIO will find your libraries automatically, configure preprocessor's 33 | include paths and build them. 34 | 35 | More information about PlatformIO Library Dependency Finder 36 | - http://docs.platformio.org/en/stable/librarymanager/ldf.html 37 | -------------------------------------------------------------------------------- /atmega328/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter, extra scripting 4 | ; Upload options: custom port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; 7 | ; Please visit documentation for the other options and examples 8 | ; http://docs.platformio.org/en/stable/projectconf.html 9 | 10 | [platformio] 11 | env_default = sonoffsc 12 | src_dir = sonoffsc 13 | 14 | [env:sonoffsc] 15 | platform = atmelavr 16 | board = uno 17 | framework = arduino 18 | build_flags = -DDHT_TYPE=DHT22 19 | lib_deps = 20 | Adafruit Unified Sensor 21 | DHT sensor library 22 | Adafruit NeoPixel 23 | WS2812FX 24 | Ticker 25 | https://github.com/xoseperez/seriallink#0.1.0 26 | -------------------------------------------------------------------------------- /atmega328/sonoffsc/sonoffsc.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Sonoff SC 4 | Copyright (C) 2017 by Xose Pérez 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | */ 20 | 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | // ----------------------------------------------------------------------------- 28 | // 29 | // ----------------------------------------------------------------------------- 30 | 31 | #define SERIAL_BAUDRATE 9600 32 | 33 | #define FAN_PIN 7 34 | #define FAN_OFF_DELAY 0 35 | 36 | #define LDR_PIN A3 37 | 38 | #define SHARP_LED_PIN 9 39 | #define SHARP_READ_PIN A1 40 | #define SHARP_SAMPLING_TIME 280 41 | #define SHARP_DELTA_TIME 40 42 | #define SHARP_SLEEP_TIME 9680 43 | 44 | #define DHT_PIN 6 45 | #ifndef DHT_TYPE 46 | // Uncomment the sensor type that you have (comment the other if applicable) 47 | //#define DHT_TYPE DHT11 48 | #define DHT_TYPE DHT22 49 | #endif 50 | #define DHT_EXPIRES 60000 51 | 52 | #define RGB_PIN 12 53 | #define RGB_COUNT 24 54 | #define RGB_TIMEOUT 0 55 | #define RGB_SPEED 255 56 | #define RGB_BRIGHTNESS 255 57 | #define RGB_COLOR 0x000000 58 | #define RGB_EFFECT FX_MODE_STATIC 59 | 60 | #define ADC_COUNTS 1024 61 | #define MICROPHONE_PIN A2 62 | 63 | #define MW_PIN 13 64 | 65 | //#define NOISE_READING_DELAY 100 66 | #define NOISE_READING_WINDOW 20 67 | #define NOISE_BUFFER_SIZE 20 68 | 69 | #define CLAP_DEBOUNCE_DELAY 150 70 | #define CLAP_TIMEOUT_DELAY 1000 71 | #define CLAP_SENSIBILITY 80 72 | #define CLAP_COUNT_TRIGGER 4 73 | #define CLAP_BUFFER_SIZE 7 74 | #define CLAP_TOLERANCE 1.50 75 | 76 | #define MAX_SERIAL_BUFFER 20 77 | 78 | #define DEFAULT_EVERY 60 79 | #define DEFAULT_PUSH 0 80 | #define DEFAULT_CLAP 0 81 | #define DEFAULT_THRESHOLD 0 82 | 83 | #define NULL_VALUE -999 84 | 85 | // ----------------------------------------------------------------------------- 86 | // Keywords 87 | // ----------------------------------------------------------------------------- 88 | 89 | const PROGMEM char at_hello[] = "AT+HELLO"; 90 | const PROGMEM char at_push[] = "AT+PUSH"; 91 | const PROGMEM char at_every[] = "AT+EVERY"; 92 | const PROGMEM char at_temp[] = "AT+TEMP"; 93 | const PROGMEM char at_hum[] = "AT+HUM"; 94 | const PROGMEM char at_dust[] = "AT+DUST"; 95 | const PROGMEM char at_noise[] = "AT+NOISE"; 96 | const PROGMEM char at_light[] = "AT+LIGHT"; 97 | const PROGMEM char at_clap[] = "AT+CLAP"; 98 | const PROGMEM char at_code[] = "AT+CODE"; 99 | const PROGMEM char at_thld[] = "AT+THLD"; 100 | const PROGMEM char at_fan[] = "AT+FAN"; 101 | const PROGMEM char at_fanoff[] = "AT+FANOFF"; 102 | const PROGMEM char at_timeout[] = "AT+TIMEOUT"; 103 | const PROGMEM char at_effect[] = "AT+EFFECT"; 104 | const PROGMEM char at_color[] = "AT+COLOR"; 105 | const PROGMEM char at_bright[] = "AT+BRIGHT"; 106 | const PROGMEM char at_speed[] = "AT+SPEED"; 107 | const PROGMEM char at_move[] = "AT+MOVE"; 108 | 109 | // ----------------------------------------------------------------------------- 110 | // Globals 111 | // ----------------------------------------------------------------------------- 112 | 113 | // Parameter 1 = number of pixels in strip 114 | // Parameter 2 = pin number (most are valid) 115 | // Parameter 3 = pixel type flags, add together as needed: 116 | // NEO_KHZ800 800 KHz bitstream (most NeoPixel products w/WS2812 LEDs) 117 | // NEO_KHZ400 400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers) 118 | // NEO_GRB Pixels are wired for GRB bitstream (most NeoPixel products) 119 | // NEO_RGB Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2) 120 | WS2812FX ws2812fx = WS2812FX(RGB_COUNT, RGB_PIN, NEO_GRB + NEO_KHZ800); 121 | bool rgbRunning = false; 122 | unsigned long rgbTimeout = RGB_TIMEOUT; 123 | unsigned long rgbStart = 0; 124 | 125 | SerialLink link(Serial); 126 | DHT dht(DHT_PIN, DHT_TYPE); 127 | int clapTimings[CLAP_BUFFER_SIZE]; 128 | byte clapPointer = 0; 129 | 130 | Ticker fanTicker; 131 | bool dustPush = false; 132 | 133 | // If push == false the slave waits for the master to request for values 134 | // If push == true the slaves sends messages 135 | // 136 | // If every == 0 values are retrieved upon request 137 | // If every > 0 values are retrieved every seconds and cached/sent 138 | // 139 | // If push == true and every == 0 messages are never sent. 140 | 141 | bool push = DEFAULT_PUSH; 142 | bool clap = DEFAULT_CLAP; 143 | unsigned long every = 1000L * DEFAULT_EVERY; 144 | unsigned int threshold = DEFAULT_THRESHOLD; 145 | unsigned long fanoff = FAN_OFF_DELAY; 146 | 147 | float temperature = NULL_VALUE; 148 | int humidity = NULL_VALUE; 149 | float dust = NULL_VALUE; 150 | int light = NULL_VALUE; 151 | int noise = NULL_VALUE; 152 | bool movement; 153 | 154 | //unsigned int noise_count = 0; 155 | //unsigned long noise_sum = 0; 156 | //unsigned int noise_peak = 0; 157 | //unsigned int noise_min = 1024; 158 | //unsigned int noise_max = 0; 159 | 160 | unsigned int noise_buffer[NOISE_BUFFER_SIZE] = {0}; 161 | unsigned int noise_buffer_pointer = 0; 162 | unsigned int noise_buffer_sum = 0; 163 | 164 | // ----------------------------------------------------------------------------- 165 | // FAN 166 | // ----------------------------------------------------------------------------- 167 | 168 | void fanStatus(bool status) { 169 | digitalWrite(FAN_PIN, status ? HIGH : LOW); 170 | } 171 | 172 | bool fanStatus() { 173 | return digitalRead(FAN_PIN) == HIGH; 174 | } 175 | 176 | // ----------------------------------------------------------------------------- 177 | // RGB LED 178 | // ----------------------------------------------------------------------------- 179 | 180 | void rgbLoop() { 181 | 182 | if (rgbRunning && (rgbTimeout > 0)) { 183 | if (millis() - rgbStart > rgbTimeout) { 184 | ws2812fx.setMode(FX_MODE_FADE); 185 | rgbRunning = false; 186 | } 187 | } 188 | 189 | ws2812fx.service(); 190 | 191 | } 192 | 193 | void rgbOff() { 194 | ws2812fx.setColor(0); 195 | ws2812fx.setMode(FX_MODE_STATIC); 196 | } 197 | 198 | void rgbEffect(unsigned int effect) { 199 | ws2812fx.setMode(effect); 200 | rgbStart = millis(); 201 | rgbRunning = true; 202 | } 203 | 204 | void rgbColor(unsigned long color) { 205 | ws2812fx.setColor(color); 206 | rgbStart = millis(); 207 | rgbRunning = true; 208 | } 209 | 210 | // ----------------------------------------------------------------------------- 211 | // SENSORS 212 | // ----------------------------------------------------------------------------- 213 | 214 | int getLight() { 215 | return map(analogRead(LDR_PIN), 0, ADC_COUNTS, 100, 0); 216 | } 217 | 218 | // 0.5V ==> 100ug/m3 219 | float getDust() { 220 | 221 | digitalWrite(SHARP_LED_PIN, LOW); 222 | delayMicroseconds(SHARP_SAMPLING_TIME); 223 | 224 | float reading = analogRead(SHARP_READ_PIN); 225 | 226 | delayMicroseconds(SHARP_DELTA_TIME); 227 | digitalWrite(SHARP_LED_PIN, HIGH); 228 | 229 | // mg/m3 230 | float dust = 170.0 * reading * (5.0 / 1024.0) - 100.0; 231 | if (dust < 0) dust = 0; 232 | return dust; 233 | 234 | } 235 | 236 | void getDustDefer(bool push = false) { 237 | if (fanoff > 0) { 238 | fanStatus(true); 239 | dustPush = push; 240 | fanTicker.setInterval(fanoff); 241 | fanTicker.setCallback([](){ 242 | fanTicker.stop(); 243 | dust = getDust(); 244 | fanStatus(false); 245 | if (dustPush) link.send_P(at_dust, dust, false); 246 | }); 247 | fanTicker.start(); 248 | } else { 249 | dust = getDust(); 250 | if (push) link.send_P(at_dust, dust, false); 251 | } 252 | } 253 | 254 | void loadTempAndHum() { 255 | 256 | // Check at most once every minute 257 | static unsigned long last = 0; 258 | if (millis() - last < DHT_EXPIRES) return; 259 | 260 | // Retrieve data 261 | double h = dht.readHumidity(); 262 | double t = dht.readTemperature(); 263 | 264 | // Check values 265 | if (isnan(h) || isnan(t)) return; 266 | temperature = t; 267 | humidity = h; 268 | 269 | // Only set new expiration time if good reading 270 | last = millis(); 271 | 272 | } 273 | 274 | float getTemperature() { 275 | loadTempAndHum(); 276 | return temperature; 277 | } 278 | 279 | int getHumidity() { 280 | loadTempAndHum(); 281 | return humidity; 282 | } 283 | 284 | int getNoise() { 285 | 286 | int value = 0; 287 | 288 | //if (noise_count > 0) { 289 | 290 | value = noise_buffer_sum / NOISE_BUFFER_SIZE; 291 | 292 | //Serial.print("CNT : "); Serial.println(noise_count); 293 | //Serial.print("SUM : "); Serial.println(noise_sum / noise_count); 294 | //Serial.print("PEAK: "); Serial.println(noise_peak / noise_count); 295 | //Serial.print("MAX : "); Serial.println(noise_max); 296 | //Serial.print("MIN : "); Serial.println(noise_min); 297 | //Serial.print("VAL : "); Serial.println(value); 298 | 299 | //noise_count = 0; 300 | //noise_sum = 0; 301 | //noise_peak = 0; 302 | //noise_min = ADC_COUNTS; 303 | //noise_max = 0; 304 | 305 | //} 306 | 307 | return value; 308 | 309 | } 310 | 311 | // ----------------------------------------------------------------------------- 312 | // MICROWAVE 313 | // ----------------------------------------------------------------------------- 314 | 315 | bool getMovement() { 316 | return digitalRead(MW_PIN) == HIGH; 317 | } 318 | 319 | void moveLoop(bool force = false) { 320 | bool value = getMovement(); 321 | if (force || (movement != value)) { 322 | link.send_P(at_move, value ? 1 : 0, false); 323 | } 324 | movement = value; 325 | } 326 | 327 | // ----------------------------------------------------------------------------- 328 | // MIC 329 | // ----------------------------------------------------------------------------- 330 | 331 | void clapDecode() { 332 | 333 | // at least 2 claps 334 | if (clapPointer > 0) { 335 | 336 | byte code = 2; 337 | if (clapPointer > 1) { 338 | int length = clapTimings[0] * CLAP_TOLERANCE; 339 | for(byte i=1; i length) code += 1; 342 | } 343 | } 344 | 345 | link.send_P(at_code, code); 346 | 347 | } 348 | 349 | // reset 350 | clapPointer = 0; 351 | 352 | } 353 | 354 | void clapRecord(int value) { 355 | 356 | static bool reading = false; 357 | static unsigned long last_clap; 358 | static int counts = 0; 359 | unsigned long current = millis(); 360 | unsigned long span = current - last_clap; 361 | 362 | if (value > CLAP_SENSIBILITY) { 363 | ++counts; 364 | } else { 365 | counts = 0; 366 | } 367 | 368 | if (counts == CLAP_COUNT_TRIGGER) { 369 | 370 | //Serial.print("Value: "); Serial.println(value); 371 | 372 | // Is it the first clap? 373 | if (!reading) { 374 | 375 | last_clap = current; 376 | reading = true; 377 | 378 | // or not 379 | } else { 380 | 381 | //Serial.print("Span : "); Serial.println(span); 382 | 383 | // timed out 384 | if (span > CLAP_TIMEOUT_DELAY) { 385 | 386 | clapDecode(); 387 | 388 | // reset 389 | reading = false; 390 | 391 | } else if (span < CLAP_DEBOUNCE_DELAY) { 392 | 393 | // do nothing 394 | 395 | // new clap! 396 | } else if (clapPointer < CLAP_BUFFER_SIZE) { 397 | clapTimings[clapPointer] = span; 398 | last_clap = current; 399 | clapPointer++; 400 | 401 | // buffer overrun 402 | } else { 403 | clapPointer = 0; 404 | reading = false; 405 | } 406 | 407 | } 408 | 409 | // check if we have to process it 410 | } else if (reading) { 411 | 412 | if (span > CLAP_TIMEOUT_DELAY) { 413 | 414 | clapDecode(); 415 | 416 | // back to idle 417 | reading = false; 418 | 419 | } 420 | 421 | } 422 | 423 | } 424 | 425 | void noiseLoop() { 426 | 427 | static unsigned long last_reading = 0; 428 | static unsigned int triggered = 0; 429 | 430 | unsigned int sample; 431 | //unsigned int count = 0; 432 | //unsigned long sum; 433 | unsigned int min = ADC_COUNTS; 434 | unsigned int max = 0; 435 | 436 | // Check MIC every NOISE_READING_DELAY 437 | // if (millis() - last_reading < NOISE_READING_DELAY) return; 438 | last_reading = millis(); 439 | 440 | while (millis() - last_reading < NOISE_READING_WINDOW) { 441 | sample = analogRead(MICROPHONE_PIN); 442 | //++count; 443 | //sum += sample; 444 | if (sample < min) min = sample; 445 | if (sample > max) max = sample; 446 | } 447 | 448 | //++noise_count; 449 | //unsigned int average = 100 * (sum / count) / ADC_COUNTS; 450 | //noise_sum += average; 451 | 452 | unsigned int peak = map(max - min, 0, ADC_COUNTS, 0, 100); 453 | //Serial.println(peak); 454 | if (clap) clapRecord(peak); 455 | 456 | noise_buffer_sum = noise_buffer_sum + peak - noise_buffer[noise_buffer_pointer]; 457 | noise_buffer[noise_buffer_pointer] = peak; 458 | noise_buffer_pointer = (noise_buffer_pointer + 1) % NOISE_BUFFER_SIZE; 459 | 460 | //noise_peak += peak; 461 | //if (max > noise_max) noise_max = max; 462 | //if (min < noise_min) noise_min = min; 463 | 464 | if (threshold > 0) { 465 | unsigned int value = noise_buffer_sum / NOISE_BUFFER_SIZE; 466 | if (value > threshold) { 467 | if (value > triggered) { 468 | link.send_P(at_noise, value); 469 | triggered = value; 470 | } 471 | } else if (triggered > 0) { 472 | link.send_P(at_noise, value); 473 | triggered = 0; 474 | } 475 | } 476 | 477 | } 478 | 479 | // ----------------------------------------------------------------------------- 480 | // COMMUNICATION 481 | // ----------------------------------------------------------------------------- 482 | 483 | // How to respond to AT+...=? requests 484 | bool linkGet(char * key) { 485 | 486 | if (strcmp_P(key, at_push) == 0) { 487 | link.send(key, push ? 1 : 0, false); 488 | return true; 489 | } 490 | 491 | if (strcmp_P(key, at_fan) == 0) { 492 | link.send(key, fanStatus() ? 1 : 0, false); 493 | return true; 494 | } 495 | 496 | if (strcmp_P(key, at_fanoff) == 0) { 497 | link.send(key, fanoff, false); 498 | return true; 499 | } 500 | 501 | if (strcmp_P(key, at_clap) == 0) { 502 | link.send(key, clap ? 1 : 0, false); 503 | return true; 504 | } 505 | 506 | if (strcmp_P(key, at_thld) == 0) { 507 | link.send(key, threshold, false); 508 | return true; 509 | } 510 | 511 | if (strcmp_P(key, at_every) == 0) { 512 | link.send(key, every / 1000, false); 513 | return true; 514 | } 515 | 516 | if (strcmp_P(key, at_temp) == 0) { 517 | if (every == 0) temperature = getTemperature(); 518 | if (temperature == NULL_VALUE) return false; 519 | link.send(key, 10 * temperature, false); 520 | return true; 521 | } 522 | 523 | if (strcmp_P(key, at_hum) == 0) { 524 | if (every == 0) humidity = getHumidity(); 525 | if (humidity == NULL_VALUE) return false; 526 | link.send(key, humidity, false); 527 | return true; 528 | } 529 | 530 | if (strcmp_P(key, at_noise) == 0) { 531 | if (every == 0) noise = getNoise(); 532 | if (noise == NULL_VALUE) return false; 533 | link.send(key, noise, false); 534 | return true; 535 | } 536 | 537 | if (strcmp_P(key, at_dust) == 0) { 538 | if (every == 0) { 539 | getDustDefer(true); 540 | } else { 541 | if (dust == NULL_VALUE) return false; 542 | link.send(key, dust, false); 543 | } 544 | return true; 545 | } 546 | 547 | if (strcmp_P(key, at_light) == 0) { 548 | if (every == 0) light = getLight(); 549 | if (light == NULL_VALUE) return false; 550 | link.send(key, light, false); 551 | return true; 552 | } 553 | 554 | if (strcmp_P(key, at_move) == 0) { 555 | link.send(key, getMovement() ? 1 : 0, false); 556 | return true; 557 | } 558 | 559 | if (strcmp_P(key, at_timeout) == 0) { 560 | link.send(key, rgbTimeout, false); 561 | return true; 562 | } 563 | 564 | if (strcmp_P(key, at_effect) == 0) { 565 | link.send(key, ws2812fx.getMode(), false); 566 | return true; 567 | } 568 | 569 | if (strcmp_P(key, at_color) == 0) { 570 | link.send(key, ws2812fx.getColor(), false); 571 | return true; 572 | } 573 | 574 | if (strcmp_P(key, at_speed) == 0) { 575 | link.send(key, ws2812fx.getSpeed(), false); 576 | return true; 577 | } 578 | 579 | if (strcmp_P(key, at_bright) == 0) { 580 | link.send(key, ws2812fx.getBrightness(), false); 581 | return true; 582 | } 583 | 584 | return false; 585 | 586 | } 587 | 588 | // Functions for responding to AT+...= commands that set values and functions 589 | bool linkSet(char * key, long value) { 590 | 591 | if (strcmp_P(key, at_push) == 0) { 592 | if (0 <= value && value <= 1) { 593 | push = value == 1; 594 | return true; 595 | } 596 | } 597 | 598 | if (strcmp_P(key, at_clap) == 0) { 599 | if (0 <= value && value <= 1) { 600 | clap = value == 1; 601 | return true; 602 | } 603 | } 604 | 605 | if (strcmp_P(key, at_every) == 0) { 606 | if (5 <= value && value <= 300) { 607 | every = 1000L * value; 608 | return true; 609 | } 610 | } 611 | 612 | if (strcmp_P(key, at_thld) == 0) { 613 | if (0 <= value && value <= 100) { 614 | threshold = value; 615 | return true; 616 | } 617 | } 618 | 619 | if (strcmp_P(key, at_fan) == 0) { 620 | if (0 <= value && value <= 1) { 621 | fanStatus(value == 1); 622 | return true; 623 | } 624 | } 625 | 626 | if (strcmp_P(key, at_fanoff) == 0) { 627 | fanoff = value; 628 | return true; 629 | } 630 | 631 | if (strcmp_P(key, at_timeout) == 0) { 632 | if (0 <= value) { 633 | rgbTimeout = value; 634 | return true; 635 | } 636 | } 637 | 638 | if (strcmp_P(key, at_color) == 0) { 639 | if (0 <= value && value <= 0xFFFFFF) { 640 | rgbColor(value); 641 | return true; 642 | } 643 | } 644 | 645 | if (strcmp_P(key, at_effect) == 0) { 646 | if (0 <= value && value < MODE_COUNT) { 647 | rgbEffect(value); 648 | return true; 649 | } 650 | } 651 | 652 | if (strcmp_P(key, at_speed) == 0) { 653 | if (SPEED_MIN <= value && value <= SPEED_MAX) { 654 | ws2812fx.setSpeed(value); 655 | return true; 656 | } 657 | } 658 | 659 | if (strcmp_P(key, at_bright) == 0) { 660 | if (BRIGHTNESS_MIN <= value && value <= BRIGHTNESS_MAX) { 661 | ws2812fx.setBrightness(value); 662 | return true; 663 | } 664 | } 665 | 666 | return false; 667 | 668 | } 669 | 670 | //Setup callbacks when AT commands are recieved 671 | void linkSetup() { 672 | link.onGet(linkGet); 673 | link.onSet(linkSet); 674 | } 675 | 676 | // Check for incoming AT+ commands 677 | void linkLoop() { 678 | link.handle(); 679 | } 680 | 681 | // ----------------------------------------------------------------------------- 682 | // MAIN 683 | // ----------------------------------------------------------------------------- 684 | 685 | void setup() { 686 | 687 | // Setup Serial port 688 | Serial.begin(SERIAL_BAUDRATE); 689 | link.send_P(at_hello, 1); 690 | 691 | linkSetup(); 692 | 693 | // Setup physical pins on the ATMega328 694 | pinMode(FAN_PIN, OUTPUT); 695 | pinMode(LDR_PIN, INPUT); 696 | pinMode(DHT_PIN, INPUT); 697 | pinMode(SHARP_LED_PIN, OUTPUT); 698 | pinMode(SHARP_READ_PIN, INPUT); 699 | pinMode(MICROPHONE_PIN, INPUT_PULLUP); 700 | pinMode(MW_PIN, INPUT); 701 | 702 | // Switch FAN off 703 | fanStatus(false); 704 | 705 | // Setup the DHT Thermometer/Humidity Sensor 706 | dht.begin(); 707 | 708 | // Neopixel setup and start animation 709 | ws2812fx.init(); 710 | ws2812fx.setBrightness(RGB_BRIGHTNESS); 711 | ws2812fx.setSpeed(RGB_SPEED); 712 | ws2812fx.setColor(RGB_COLOR); 713 | ws2812fx.setMode(RGB_EFFECT); 714 | ws2812fx.start(); 715 | 716 | rgbStart = millis(); 717 | rgbRunning = true; 718 | 719 | } 720 | 721 | void loop() { 722 | 723 | static unsigned long last = 0; 724 | 725 | linkLoop(); 726 | 727 | // If AT+EVERY>0 then we are sending a signal every so many seconds 728 | if ((every > 0) && ((millis() - last > every) || (last == 0))) { 729 | 730 | last = millis(); 731 | 732 | getDustDefer(push); 733 | 734 | temperature = getTemperature(); 735 | if (push) link.send_P(at_temp, 10 * temperature, false); 736 | 737 | noiseLoop(); 738 | 739 | humidity = getHumidity(); 740 | if (push) link.send_P(at_hum, humidity, false); 741 | 742 | noiseLoop(); 743 | 744 | light = getLight(); 745 | if (push) link.send_P(at_light, light, false); 746 | 747 | noiseLoop(); 748 | 749 | noise = getNoise(); 750 | if (push) link.send_P(at_noise, noise, false); 751 | 752 | moveLoop(true); 753 | 754 | } 755 | 756 | fanTicker.update(); 757 | noiseLoop(); 758 | moveLoop(); 759 | rgbLoop(); 760 | 761 | } 762 | -------------------------------------------------------------------------------- /docs/FWTRX-TAMCUSC-SONOFFSC-ATMEGA328P.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/docs/FWTRX-TAMCUSC-SONOFFSC-ATMEGA328P.zip -------------------------------------------------------------------------------- /docs/Sonoff-SC-Schematic.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/docs/Sonoff-SC-Schematic.pdf -------------------------------------------------------------------------------- /docs/Sonoff-SC-User-Guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/docs/Sonoff-SC-User-Guide.pdf -------------------------------------------------------------------------------- /esp8266/.gitignore: -------------------------------------------------------------------------------- 1 | .pioenvs 2 | .piolibdeps 3 | .clang_complete 4 | .gcc-flags.json 5 | utils 6 | -------------------------------------------------------------------------------- /esp8266/.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < http://docs.platformio.org/en/stable/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < http://docs.platformio.org/en/stable/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < http://docs.platformio.org/en/stable/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choice one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | # language: python 28 | # python: 29 | # - "2.7" 30 | # 31 | # sudo: false 32 | # cache: 33 | # directories: 34 | # - "~/.platformio" 35 | # 36 | # install: 37 | # - pip install -U platformio 38 | # 39 | # script: 40 | # - platformio run 41 | 42 | 43 | # 44 | # Template #2: The project is intended to by used as a library with examples 45 | # 46 | 47 | # language: python 48 | # python: 49 | # - "2.7" 50 | # 51 | # sudo: false 52 | # cache: 53 | # directories: 54 | # - "~/.platformio" 55 | # 56 | # env: 57 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 58 | # - PLATFORMIO_CI_SRC=examples/file.ino 59 | # - PLATFORMIO_CI_SRC=path/to/test/directory 60 | # 61 | # install: 62 | # - pip install -U platformio 63 | # 64 | # script: 65 | # - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N 66 | -------------------------------------------------------------------------------- /esp8266/debug: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ------------------------------------------------------------------------------ 4 | # CONFIGURATION 5 | # ------------------------------------------------------------------------------ 6 | 7 | ENVIRONMENT="sonoffsc" 8 | ADDR2LINE=$HOME/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-addr2line 9 | DECODER=utils/EspStackTraceDecoder.jar 10 | DECODER_ORIGIN=https://github.com/littleyoda/EspStackTraceDecoder/releases/download/untagged-83b6db3208da17a0f1fd/EspStackTraceDecoder.jar 11 | FILE="/tmp/.trace" 12 | 13 | # ------------------------------------------------------------------------------ 14 | # END CONFIGURATION - DO NOT EDIT FURTHER 15 | # ------------------------------------------------------------------------------ 16 | 17 | # remove default trace file 18 | rm -rf $FILE 19 | 20 | function help { 21 | echo 22 | echo "Syntax: $0 [-e ] [-d ]" 23 | echo 24 | } 25 | 26 | # get environment from command line 27 | while [[ $# -gt 1 ]]; do 28 | 29 | key="$1" 30 | 31 | case $key in 32 | -e) 33 | ENVIRONMENT="$2" 34 | shift 35 | ;; 36 | -d) 37 | FILE="$2" 38 | shift 39 | ;; 40 | esac 41 | 42 | shift # past argument or value 43 | 44 | done 45 | 46 | # check environment folder 47 | if [ $ENVIRONMENT == "" ]; then 48 | echo "No environment defined" 49 | help 50 | exit 1 51 | fi 52 | ELF=.pioenvs/$ENVIRONMENT/firmware.elf 53 | if [ ! -f $ELF ]; then 54 | echo "Could not find ELF file for the selected environment: $ELF" 55 | exit 2 56 | fi 57 | 58 | # get decode 59 | if [ ! -f $DECODER ]; then 60 | folder=$(dirname "$DECODER") 61 | if [ $folder != "." ]; then 62 | mkdir -p $folder 63 | fi 64 | echo "Downloading decoder..." 65 | wget -q $DECODER_ORIGIN -O "$DECODER" 66 | fi 67 | 68 | # get trace interactively 69 | if [ ! -f $FILE ]; then 70 | echo "Paste stack trace and end with a blank line:" 71 | trace=$(sed '/^$/q') 72 | echo $trace > $FILE 73 | fi 74 | 75 | java -jar $DECODER $ADDR2LINE $ELF $FILE 76 | -------------------------------------------------------------------------------- /esp8266/gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | ESP8266 file system builder 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | */ 21 | 22 | // ----------------------------------------------------------------------------- 23 | // File system builder 24 | // ----------------------------------------------------------------------------- 25 | 26 | const fs = require('fs'); 27 | const gulp = require('gulp'); 28 | const htmlmin = require('gulp-htmlmin'); 29 | const cleancss = require('gulp-clean-css'); 30 | const uglify = require('gulp-uglify'); 31 | const gzip = require('gulp-gzip'); 32 | const inline = require('gulp-inline'); 33 | const inlineImages = require('gulp-css-base64'); 34 | const favicon = require('gulp-base64-favicon'); 35 | 36 | const dataFolder = 'sonoffsc/data/'; 37 | const staticFolder = 'sonoffsc/static/'; 38 | 39 | String.prototype.replaceAll = function(search, replacement) { 40 | var target = this; 41 | return target.split(search).join(replacement); 42 | }; 43 | 44 | var toHeader = function(filename) { 45 | 46 | var source = dataFolder + filename; 47 | var destination = staticFolder + filename + '.h'; 48 | var safename = filename.replaceAll('.', '_'); 49 | 50 | var wstream = fs.createWriteStream(destination); 51 | wstream.on('error', function (err) { 52 | console.log(err); 53 | }); 54 | 55 | var data = fs.readFileSync(source); 56 | 57 | wstream.write('#define ' + safename + '_len ' + data.length + '\n'); 58 | wstream.write('const uint8_t ' + safename + '[] PROGMEM = {') 59 | 60 | for (i=0; i"); 62 | this.container = this.elem.parent(); 63 | this.offLabel = $("").appendTo(this.container); 64 | this.offSpan = this.offLabel.children('span'); 65 | this.onLabel = $("").appendTo(this.container); 66 | this.onBorder = $("
").appendTo(this.container); 67 | this.offBorder = $("
").appendTo(this.container); 68 | this.onSpan = this.onLabel.children('span'); 69 | this.handle = $("
").appendTo(this.container); 70 | this.handleCenter = $("
").appendTo(this.handle); 71 | this.handleRight = $("
").appendTo(this.handle); 72 | return true; 73 | }; 74 | 75 | iOSCheckbox.prototype.disableTextSelection = function() { 76 | if ($.browser.msie) { 77 | return $([this.handle, this.offLabel, this.onLabel, this.container]).attr("unselectable", "on"); 78 | } 79 | }; 80 | 81 | iOSCheckbox.prototype._getDimension = function(elem, dimension) { 82 | if ($.fn.actual != null) { 83 | return elem.actual(dimension); 84 | } else { 85 | return elem[dimension](); 86 | } 87 | }; 88 | 89 | iOSCheckbox.prototype.optionallyResize = function(mode) { 90 | var newWidth, offLabelWidth, offSpan, onLabelWidth, onSpan; 91 | 92 | onSpan = this.onLabel.find('span'); 93 | onLabelWidth = this._getDimension(onSpan, "width"); 94 | onLabelWidth += parseInt(onSpan.css('padding-left'), 10); 95 | offSpan = this.offLabel.find('span'); 96 | offLabelWidth = this._getDimension(offSpan, "width"); 97 | offLabelWidth += parseInt(offSpan.css('padding-right'), 10); 98 | if (mode === "container") { 99 | newWidth = onLabelWidth > offLabelWidth ? onLabelWidth : offLabelWidth; 100 | newWidth += this._getDimension(this.handle, "width") + this.handleMargin; 101 | return this.container.css({ 102 | width: newWidth 103 | }); 104 | } else { 105 | newWidth = onLabelWidth > offLabelWidth ? onLabelWidth : offLabelWidth; 106 | this.handleCenter.css({ 107 | width: newWidth + 4 108 | }); 109 | return this.handle.css({ 110 | width: newWidth + 7 111 | }); 112 | } 113 | }; 114 | 115 | iOSCheckbox.prototype.onMouseDown = function(event) { 116 | var x; 117 | 118 | event.preventDefault(); 119 | if (this.isDisabled()) { 120 | return; 121 | } 122 | x = event.pageX || event.originalEvent.changedTouches[0].pageX; 123 | iOSCheckbox.currentlyClicking = this.handle; 124 | iOSCheckbox.dragStartPosition = x; 125 | return iOSCheckbox.handleLeftOffset = parseInt(this.handle.css('left'), 10) || 0; 126 | }; 127 | 128 | iOSCheckbox.prototype.onDragMove = function(event, x) { 129 | var newWidth, p; 130 | 131 | if (iOSCheckbox.currentlyClicking !== this.handle) { 132 | return; 133 | } 134 | p = (x + iOSCheckbox.handleLeftOffset - iOSCheckbox.dragStartPosition) / this.rightSide; 135 | if (p < 0) { 136 | p = 0; 137 | } 138 | if (p > 1) { 139 | p = 1; 140 | } 141 | newWidth = p * this.rightSide; 142 | this.handle.css({ 143 | left: newWidth 144 | }); 145 | this.onLabel.css({ 146 | width: newWidth + this.handleRadius 147 | }); 148 | this.offSpan.css({ 149 | marginRight: -newWidth 150 | }); 151 | return this.onSpan.css({ 152 | marginLeft: -(1 - p) * this.rightSide 153 | }); 154 | }; 155 | 156 | iOSCheckbox.prototype.onDragEnd = function(event, x) { 157 | var p; 158 | 159 | if (iOSCheckbox.currentlyClicking !== this.handle) { 160 | return; 161 | } 162 | if (this.isDisabled()) { 163 | return; 164 | } 165 | if (iOSCheckbox.dragging) { 166 | p = (x - iOSCheckbox.dragStartPosition) / this.rightSide; 167 | this.elem.prop('checked', p >= 0.5).change(); 168 | } else { 169 | this.elem.prop('checked', !this.elem.prop('checked')).change(); 170 | } 171 | iOSCheckbox.currentlyClicking = null; 172 | iOSCheckbox.dragging = null; 173 | if (typeof this.onChange === "function") { 174 | this.onChange(this.elem, this.elem.prop('checked')); 175 | } 176 | return this.didChange(); 177 | }; 178 | 179 | iOSCheckbox.prototype.refresh = function() { 180 | return this.didChange(); 181 | }; 182 | 183 | iOSCheckbox.prototype.didChange = function() { 184 | var new_left; 185 | 186 | if (this.isDisabled()) { 187 | this.container.addClass(this.disabledClass); 188 | return false; 189 | } else { 190 | this.container.removeClass(this.disabledClass); 191 | } 192 | new_left = this.elem.prop('checked') ? this.rightSide + 2 : 0; 193 | this.handle.animate({ 194 | left: new_left 195 | }, this.duration); 196 | this.onLabel.animate({ 197 | width: new_left + this.handleRadius 198 | }, this.duration); 199 | this.offSpan.animate({ 200 | marginRight: -new_left 201 | }, this.duration); 202 | return this.onSpan.animate({ 203 | marginLeft: new_left - this.rightSide 204 | }, this.duration); 205 | }; 206 | 207 | iOSCheckbox.prototype.attachEvents = function() { 208 | var localMouseMove, localMouseUp, self; 209 | 210 | self = this; 211 | localMouseMove = function(event) { 212 | return self.onGlobalMove.apply(self, arguments); 213 | }; 214 | localMouseUp = function(event) { 215 | self.onGlobalUp.apply(self, arguments); 216 | $(document).unbind('mousemove touchmove', localMouseMove); 217 | return $(document).unbind('mouseup touchend', localMouseUp); 218 | }; 219 | this.elem.change(function() { 220 | return self.refresh(); 221 | }); 222 | return this.container.bind('mousedown touchstart', function(event) { 223 | self.onMouseDown.apply(self, arguments); 224 | $(document).bind('mousemove touchmove', localMouseMove); 225 | return $(document).bind('mouseup touchend', localMouseUp); 226 | }); 227 | }; 228 | 229 | iOSCheckbox.prototype.initialPosition = function() { 230 | var containerWidth, offset; 231 | 232 | containerWidth = this._getDimension(this.container, "width"); 233 | this.offLabel.css({ 234 | width: containerWidth - this.containerRadius - 4 235 | }); 236 | this.offBorder.css({ 237 | left: containerWidth - 4 238 | }); 239 | offset = this.containerRadius + 1; 240 | if ($.browser.msie && $.browser.version < 7) { 241 | offset -= 3; 242 | } 243 | this.rightSide = containerWidth - this._getDimension(this.handle, "width") - offset; 244 | if (this.elem.is(':checked')) { 245 | this.handle.css({ 246 | left: this.rightSide 247 | }); 248 | this.onLabel.css({ 249 | width: this.rightSide + this.handleRadius 250 | }); 251 | this.offSpan.css({ 252 | marginRight: -this.rightSide, 253 | }); 254 | } else { 255 | this.onLabel.css({ 256 | width: 0 257 | }); 258 | this.onSpan.css({ 259 | marginLeft: -this.rightSide 260 | }); 261 | } 262 | if (this.isDisabled()) { 263 | return this.container.addClass(this.disabledClass); 264 | } 265 | }; 266 | 267 | iOSCheckbox.prototype.onGlobalMove = function(event) { 268 | var x; 269 | 270 | if (!(!this.isDisabled() && iOSCheckbox.currentlyClicking)) { 271 | return; 272 | } 273 | event.preventDefault(); 274 | x = event.pageX || event.originalEvent.changedTouches[0].pageX; 275 | if (!iOSCheckbox.dragging && (Math.abs(iOSCheckbox.dragStartPosition - x) > this.dragThreshold)) { 276 | iOSCheckbox.dragging = true; 277 | } 278 | return this.onDragMove(event, x); 279 | }; 280 | 281 | iOSCheckbox.prototype.onGlobalUp = function(event) { 282 | var x; 283 | 284 | if (!iOSCheckbox.currentlyClicking) { 285 | return; 286 | } 287 | event.preventDefault(); 288 | x = event.pageX || event.originalEvent.changedTouches[0].pageX; 289 | this.onDragEnd(event, x); 290 | return false; 291 | }; 292 | 293 | iOSCheckbox.defaults = { 294 | duration: 200, 295 | checkedLabel: 'ON', 296 | uncheckedLabel: 'OFF', 297 | resizeHandle: true, 298 | resizeContainer: true, 299 | disabledClass: 'iPhoneCheckDisabled', 300 | containerClass: 'iPhoneCheckContainer', 301 | labelOnClass: 'iPhoneCheckLabelOn', 302 | labelOffClass: 'iPhoneCheckLabelOff', 303 | handleClass: 'iPhoneCheckHandle', 304 | handleCenterClass: 'iPhoneCheckHandleCenter', 305 | handleRightClass: 'iPhoneCheckHandleRight', 306 | dragThreshold: 5, 307 | handleMargin: 15, 308 | handleRadius: 4, 309 | containerRadius: 5, 310 | dataName: "iphoneStyle", 311 | onChange: function() {} 312 | }; 313 | 314 | return iOSCheckbox; 315 | 316 | })(); 317 | 318 | $.iphoneStyle = this.iOSCheckbox = iOSCheckbox; 319 | 320 | $.fn.iphoneStyle = function() { 321 | var args, checkbox, dataName, existingControl, method, params, _i, _len, _ref, _ref1, _ref2, _ref3; 322 | 323 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 324 | dataName = (_ref = (_ref1 = args[0]) != null ? _ref1.dataName : void 0) != null ? _ref : iOSCheckbox.defaults.dataName; 325 | _ref2 = this.filter(':checkbox'); 326 | for (_i = 0, _len = _ref2.length; _i < _len; _i++) { 327 | checkbox = _ref2[_i]; 328 | existingControl = $(checkbox).data(dataName); 329 | if (existingControl != null) { 330 | method = args[0], params = 2 <= args.length ? __slice.call(args, 1) : []; 331 | if ((_ref3 = existingControl[method]) != null) { 332 | _ref3.apply(existingControl, params); 333 | } 334 | } else { 335 | new iOSCheckbox(checkbox, args[0]); 336 | } 337 | } 338 | return this; 339 | }; 340 | 341 | $.fn.iOSCheckbox = function(options) { 342 | var opts; 343 | 344 | if (options == null) { 345 | options = {}; 346 | } 347 | opts = $.extend({}, options, { 348 | resizeHandle: false, 349 | disabledClass: 'iOSCheckDisabled', 350 | containerClass: 'iOSCheckContainer', 351 | labelOnClass: 'iOSCheckLabelOn', 352 | labelOffClass: 'iOSCheckLabelOff', 353 | handleClass: 'iOSCheckHandle', 354 | handleCenterClass: 'iOSCheckHandleCenter', 355 | handleRightClass: 'iOSCheckHandleRight', 356 | dataName: 'iOSCheckbox' 357 | }); 358 | return this.iphoneStyle(opts); 359 | }; 360 | 361 | }).call(this); 362 | -------------------------------------------------------------------------------- /esp8266/html/custom.css: -------------------------------------------------------------------------------- 1 | #menu .pure-menu-heading { 2 | font-size: 100%; 3 | padding: .5em .5em; 4 | } 5 | .header h2 { 6 | font-size: 1em; 7 | } 8 | .panel { 9 | display: none; 10 | } 11 | .footer { 12 | position: absolute; 13 | bottom: 0; 14 | left: 0; 15 | right: 0; 16 | padding: 10px; 17 | font-size: 80%; 18 | color: #999; 19 | } 20 | #menu .footer a { 21 | text-decoration: none; 22 | padding: 0px; 23 | } 24 | .content { 25 | margin: 0px; 26 | } 27 | .page { 28 | margin-top: 40px; 29 | } 30 | .pure-button { 31 | color: white; 32 | padding: 8px 12px; 33 | border-radius: 4px; 34 | text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); 35 | } 36 | .main-buttons { 37 | margin: 50px auto; 38 | text-align: center; 39 | } 40 | .main-buttons button { 41 | width: 100px; 42 | margin: 5px auto; 43 | } 44 | .button-update-password, 45 | .button-update { 46 | background: #1f8dd6; 47 | } 48 | .button-reset, 49 | .button-reconnect { 50 | background: rgb(202, 60, 60); 51 | } 52 | .button-upgrade { 53 | background: rgb(202, 60, 60); 54 | margin-left: 5px; 55 | } 56 | .button-upgrade-browse, 57 | .button-apikey { 58 | background: rgb(0, 202, 0); 59 | margin-left: 5px; 60 | } 61 | .button-add-network { 62 | background: rgb(28, 184, 65); 63 | } 64 | .button-del-network { 65 | background: rgb(202, 60, 60); 66 | } 67 | .button-more-network { 68 | background: rgb(223, 117, 20); 69 | } 70 | .button-settings-backup, 71 | .button-settings-restore { 72 | background: rgb(0, 202, 0); 73 | } 74 | .pure-g { 75 | margin-bottom: 20px; 76 | } 77 | legend { 78 | font-weight: bold; 79 | } 80 | .l-box { 81 | padding-right: 1px; 82 | } 83 | .pure-form input[type=text][disabled] { 84 | color: #777777; 85 | } 86 | div.hint { 87 | font-size: 80%; 88 | color: #ccc; 89 | } 90 | .break { 91 | margin-top: 5px; 92 | } 93 | #networks .pure-g { 94 | padding-bottom: 10px; 95 | margin-bottom: 5px; 96 | border-bottom: 2px dashed #e5e5e5; 97 | } 98 | #networks div.more { 99 | display: none; 100 | } 101 | .module { 102 | display: none; 103 | } 104 | .template { 105 | display: none; 106 | } 107 | input[name=upgrade] { 108 | display: none; 109 | } 110 | #upgrade-progress { 111 | display: none; 112 | width: 100%; 113 | height: 20px; 114 | margin-top: 10px; 115 | } 116 | .pure-form .center { 117 | margin: .5em 0 .2em; 118 | } 119 | .webmode { 120 | display: none; 121 | } 122 | #credentials { 123 | font-size: 200%; 124 | text-align: center; 125 | height: 100px; 126 | width: 400px; 127 | position: fixed; 128 | top: 50%; 129 | left: 50%; 130 | margin-top: -50px; 131 | margin-left: -200px; 132 | } 133 | 134 | div.state { 135 | border-top: 1px solid #eee; 136 | margin-top: 20px; 137 | padding-top: 30px; 138 | } 139 | 140 | .state div { 141 | font-size: 80%; 142 | } 143 | 144 | .state span { 145 | font-size: 80%; 146 | font-weight: bold; 147 | } 148 | 149 | .right { 150 | text-align: right; 151 | } 152 | -------------------------------------------------------------------------------- /esp8266/html/custom.js: -------------------------------------------------------------------------------- 1 | var websock; 2 | var password = false; 3 | var maxNetworks; 4 | var messages = []; 5 | var webhost; 6 | 7 | // http://www.the-art-of-web.com/javascript/validate-password/ 8 | function checkPassword(str) { 9 | // at least one number, one lowercase and one uppercase letter 10 | // at least eight characters that are letters, numbers or the underscore 11 | var re = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])\w{8,}$/; 12 | return re.test(str); 13 | } 14 | 15 | function validateForm(form) { 16 | 17 | // password 18 | var adminPass1 = $("input[name='adminPass1']", form).val(); 19 | if (adminPass1.length > 0 && !checkPassword(adminPass1)) { 20 | alert("The password you have entered is not valid, it must have at least 8 characters, 1 lower and 1 uppercase and 1 number!"); 21 | return false; 22 | } 23 | 24 | var adminPass2 = $("input[name='adminPass2']", form).val(); 25 | if (adminPass1 != adminPass2) { 26 | alert("Passwords are different!"); 27 | return false; 28 | } 29 | 30 | return true; 31 | 32 | } 33 | 34 | function valueSet(data, name, value) { 35 | for (var i in data) { 36 | if (data[i]['name'] == name) { 37 | data[i]['value'] = value; 38 | return; 39 | } 40 | } 41 | data.push({'name': name, 'value': value}); 42 | } 43 | 44 | function zeroPad(number, positions) { 45 | return ("0".repeat(positions) + number).slice(-positions); 46 | } 47 | 48 | function doUpdate() { 49 | 50 | var form = $("#formSave"); 51 | 52 | if (validateForm(form)) { 53 | 54 | // Get data 55 | var data = form.serializeArray(); 56 | 57 | // Post-process 58 | delete(data['filename']); 59 | $("input[type='checkbox']").each(function() { 60 | var name = $(this).attr("name"); 61 | if (name) { 62 | valueSet(data, name, $(this).is(':checked') ? 1 : 0); 63 | } 64 | }); 65 | 66 | websock.send(JSON.stringify({'config': data})); 67 | } 68 | 69 | return false; 70 | 71 | } 72 | 73 | function doUpgrade() { 74 | 75 | var contents = $("input[name='upgrade']")[0].files[0]; 76 | if (typeof contents == 'undefined') { 77 | alert("First you have to select a file from your computer."); 78 | return false; 79 | } 80 | var filename = $("input[name='upgrade']").val().split('\\').pop(); 81 | 82 | var data = new FormData(); 83 | data.append('upgrade', contents, filename); 84 | 85 | $.ajax({ 86 | 87 | // Your server script to process the upload 88 | url: webhost + 'upgrade', 89 | type: 'POST', 90 | 91 | // Form data 92 | data: data, 93 | 94 | // Tell jQuery not to process data or worry about content-type 95 | // You *must* include these options! 96 | cache: false, 97 | contentType: false, 98 | processData: false, 99 | 100 | success: function(data, text) { 101 | $("#upgrade-progress").hide(); 102 | if (data == 'OK') { 103 | alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds."); 104 | setTimeout(function() { 105 | window.location.reload(); 106 | }, 5000); 107 | } else { 108 | alert("There was an error trying to upload the new image, please try again (" + data + ")."); 109 | } 110 | }, 111 | 112 | // Custom XMLHttpRequest 113 | xhr: function() { 114 | $("#upgrade-progress").show(); 115 | var myXhr = $.ajaxSettings.xhr(); 116 | if (myXhr.upload) { 117 | // For handling the progress of the upload 118 | myXhr.upload.addEventListener('progress', function(e) { 119 | if (e.lengthComputable) { 120 | $('progress').attr({ value: e.loaded, max: e.total }); 121 | } 122 | } , false); 123 | } 124 | return myXhr; 125 | }, 126 | 127 | }); 128 | 129 | return false; 130 | 131 | } 132 | 133 | function doUpdatePassword() { 134 | var form = $("#formPassword"); 135 | if (validateForm(form)) { 136 | var data = form.serializeArray(); 137 | websock.send(JSON.stringify({'config': data})); 138 | } 139 | return false; 140 | } 141 | 142 | function doReset() { 143 | var response = window.confirm("Are you sure you want to reset the device?"); 144 | if (response == false) return false; 145 | websock.send(JSON.stringify({'action': 'reset'})); 146 | return false; 147 | } 148 | 149 | function doReconnect() { 150 | var response = window.confirm("Are you sure you want to disconnect from the current WIFI network?"); 151 | if (response == false) return false; 152 | websock.send(JSON.stringify({'action': 'reconnect'})); 153 | return false; 154 | } 155 | 156 | function backupSettings() { 157 | document.getElementById('downloader').src = webhost + 'config'; 158 | return false; 159 | } 160 | 161 | function onFileUpload(event) { 162 | 163 | var inputFiles = this.files; 164 | if (inputFiles == undefined || inputFiles.length == 0) return false; 165 | var inputFile = inputFiles[0]; 166 | this.value = ""; 167 | 168 | var response = window.confirm("Previous settings will be overwritten. Are you sure you want to restore this settings?"); 169 | if (response == false) return false; 170 | 171 | var reader = new FileReader(); 172 | reader.onload = function(e) { 173 | var data = getJson(e.target.result); 174 | if (data) { 175 | websock.send(JSON.stringify({'action': 'restore', 'data': data})); 176 | } else { 177 | alert(messages[4]); 178 | } 179 | }; 180 | reader.readAsText(inputFile); 181 | 182 | return false; 183 | 184 | } 185 | 186 | function restoreSettings() { 187 | if (typeof window.FileReader !== 'function') { 188 | alert("The file API isn't supported on this browser yet."); 189 | } else { 190 | $("#uploader").click(); 191 | } 192 | return false; 193 | } 194 | 195 | function randomString(length, chars) { 196 | var mask = ''; 197 | if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz'; 198 | if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 199 | if (chars.indexOf('#') > -1) mask += '0123456789'; 200 | if (chars.indexOf('@') > -1) mask += 'ABCDEF'; 201 | if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; 202 | var result = ''; 203 | for (var i = length; i > 0; --i) result += mask[Math.round(Math.random() * (mask.length - 1))]; 204 | return result; 205 | } 206 | 207 | function doGenerateAPIKey() { 208 | var apikey = randomString(16, '@#'); 209 | $("input[name=\"apiKey\"]").val(apikey); 210 | return false; 211 | } 212 | 213 | function showPanel() { 214 | $(".panel").hide(); 215 | $("#" + $(this).attr("data")).show(); 216 | if ($("#layout").hasClass('active')) toggleMenu(); 217 | $("input[type='checkbox']").iphoneStyle("calculateDimensions").iphoneStyle("refresh"); 218 | }; 219 | 220 | function toggleMenu() { 221 | $("#layout").toggleClass('active'); 222 | $("#menu").toggleClass('active'); 223 | $("#menuLink").toggleClass('active'); 224 | } 225 | 226 | function delNetwork() { 227 | var parent = $(this).parents(".pure-g"); 228 | $(parent).remove(); 229 | } 230 | 231 | function moreNetwork() { 232 | var parent = $(this).parents(".pure-g"); 233 | $("div.more", parent).toggle(); 234 | } 235 | 236 | function addNetwork() { 237 | 238 | var numNetworks = $("#networks > div").length; 239 | if (numNetworks >= maxNetworks) { 240 | alert("Max number of networks reached"); 241 | return; 242 | } 243 | 244 | var tabindex = 200 + numNetworks * 10; 245 | var template = $("#networkTemplate").children(); 246 | var line = $(template).clone(); 247 | $(line).find("input").each(function() { 248 | $(this).attr("tabindex", tabindex++); 249 | }); 250 | $(line).find(".button-del-network").on('click', delNetwork); 251 | $(line).find(".button-more-network").on('click', moreNetwork); 252 | line.appendTo("#networks"); 253 | 254 | return line; 255 | 256 | } 257 | 258 | function forgetCredentials() { 259 | $.ajax({ 260 | 'method': 'GET', 261 | 'url': '/', 262 | 'async': false, 263 | 'username': "logmeout", 264 | 'password': "123456", 265 | 'headers': { "Authorization": "Basic xxx" } 266 | }).done(function(data) { 267 | return false; 268 | // If we don't get an error, we actually got an error as we expect an 401! 269 | }).fail(function(){ 270 | // We expect to get an 401 Unauthorized error! In this case we are successfully 271 | // logged out and we redirect the user. 272 | return true; 273 | }); 274 | } 275 | 276 | function processData(data) { 277 | 278 | // title 279 | if ("app" in data) { 280 | var title = data.app; 281 | if ("version" in data) { 282 | title = title + " " + data.version; 283 | } 284 | $(".pure-menu-heading").html(title); 285 | if ("hostname" in data) { 286 | title = data.hostname + " - " + title; 287 | } 288 | document.title = title; 289 | } 290 | 291 | Object.keys(data).forEach(function(key) { 292 | 293 | // Web Modes 294 | if (key == "webMode") { 295 | password = data.webMode == 1; 296 | $("#layout").toggle(data.webMode == 0); 297 | $("#password").toggle(data.webMode == 1); 298 | $("#credentials").hide(); 299 | } 300 | 301 | // Actions 302 | if (key == "action") { 303 | 304 | if (data.action == "reload") { 305 | if (password) forgetCredentials(); 306 | setTimeout(function() { 307 | window.location.reload(); 308 | }, 1000); 309 | } 310 | 311 | return; 312 | 313 | } 314 | 315 | if (key == "uptime") { 316 | var uptime = parseInt(data[key]); 317 | var seconds = uptime % 60; uptime = parseInt(uptime / 60); 318 | var minutes = uptime % 60; uptime = parseInt(uptime / 60); 319 | var hours = uptime % 24; uptime = parseInt(uptime / 24); 320 | var days = uptime; 321 | data[key] = days + 'd ' + zeroPad(hours, 2) + 'h ' + zeroPad(minutes, 2) + 'm ' + zeroPad(seconds, 2) + 's'; 322 | } 323 | 324 | if (key == "maxNetworks") { 325 | maxNetworks = parseInt(data.maxNetworks); 326 | return; 327 | } 328 | 329 | // Wifi 330 | if (key == "wifi") { 331 | 332 | var networks = data.wifi; 333 | 334 | for (var i in networks) { 335 | 336 | // add a new row 337 | var line = addNetwork(); 338 | 339 | // fill in the blanks 340 | var wifi = data.wifi[i]; 341 | Object.keys(wifi).forEach(function(key) { 342 | var element = $("input[name=" + key + "]", line); 343 | if (element.length) element.val(wifi[key]); 344 | }); 345 | 346 | } 347 | 348 | return; 349 | 350 | } 351 | 352 | // Messages 353 | if (key == "message") { 354 | window.alert(messages[data.message]); 355 | return; 356 | } 357 | 358 | // Enable options 359 | if (key.endsWith("Visible")) { 360 | var module = key.slice(0,-7); 361 | $(".module-" + module).show(); 362 | return; 363 | } 364 | 365 | // Pre-process 366 | if (key == "network") { 367 | data.network = data.network.toUpperCase(); 368 | } 369 | if (key == "mqttStatus") { 370 | data.mqttStatus = data.mqttStatus ? "CONNECTED" : "NOT CONNECTED"; 371 | } 372 | if (key == "ntpStatus") { 373 | data.ntpStatus = data.ntpStatus ? "SYNC'D" : "NOT SYNC'D"; 374 | } 375 | 376 | // Look for INPUTs 377 | var element = $("input[name=" + key + "]"); 378 | if (element.length > 0) { 379 | if (element.attr('type') == 'checkbox') { 380 | element 381 | .prop("checked", data[key]) 382 | .iphoneStyle("refresh"); 383 | } else if (element.attr('type') == 'radio') { 384 | element.val([data[key]]); 385 | } else { 386 | var pre = element.attr("pre") || ""; 387 | var post = element.attr("post") || ""; 388 | element.val(pre + data[key] + post); 389 | } 390 | return; 391 | } 392 | 393 | // Look for SPANs 394 | var element = $("span[name=" + key + "]"); 395 | if (element.length > 0) { 396 | var pre = element.attr("pre") || ""; 397 | var post = element.attr("post") || ""; 398 | element.html(pre + data[key] + post); 399 | return; 400 | } 401 | 402 | // Look for SELECTs 403 | var element = $("select[name=" + key + "]"); 404 | if (element.length > 0) { 405 | element.val(data[key]); 406 | return; 407 | } 408 | 409 | }); 410 | 411 | // Auto generate an APIKey if none defined yet 412 | if ($("input[name='apiKey']").val() == "") { 413 | doGenerateAPIKey(); 414 | } 415 | 416 | } 417 | 418 | function getJson(str) { 419 | try { 420 | return JSON.parse(str); 421 | } catch (e) { 422 | return false; 423 | } 424 | } 425 | 426 | function connect(host) { 427 | 428 | if (typeof host === 'undefined') { 429 | host = window.location.href.replace('#', ''); 430 | } else { 431 | if (!host.startsWith("http")) { 432 | host = 'http://' + host + '/'; 433 | } 434 | } 435 | webhost = host; 436 | wshost = host.replace('http', 'ws') + 'ws'; 437 | 438 | if (websock) websock.close(); 439 | websock = new WebSocket(wshost); 440 | websock.onopen = function(evt) { 441 | console.log("Connected"); 442 | }; 443 | websock.onclose = function(evt) { 444 | console.log("Disconnected"); 445 | }; 446 | websock.onerror = function(evt) { 447 | console.log("Error: ", evt); 448 | }; 449 | websock.onmessage = function(evt) { 450 | var data = getJson(evt.data); 451 | if (data) processData(data); 452 | }; 453 | } 454 | 455 | function initMessages() { 456 | messages[01] = "Remote update started"; 457 | messages[02] = "OTA update started"; 458 | messages[03] = "Error parsing data!"; 459 | messages[04] = "The file does not look like a valid configuration backup or is corrupted"; 460 | messages[05] = "Changes saved. You should reboot your board now"; 461 | messages[06] = "Home Assistant auto-discovery message sent"; 462 | messages[07] = "Passwords do not match!"; 463 | messages[08] = "Changes saved"; 464 | messages[09] = "No changes detected"; 465 | messages[10] = "Session expired, please reload page..."; 466 | } 467 | 468 | function init() { 469 | 470 | initMessages(); 471 | 472 | $("#menuLink").on('click', toggleMenu); 473 | $(".button-update").on('click', doUpdate); 474 | $(".button-update-password").on('click', doUpdatePassword); 475 | $(".button-reset").on('click', doReset); 476 | $(".button-reconnect").on('click', doReconnect); 477 | $(".button-settings-backup").on('click', backupSettings); 478 | $(".button-settings-restore").on('click', restoreSettings); 479 | $('#uploader').on('change', onFileUpload); 480 | $(".button-apikey").on('click', doGenerateAPIKey); 481 | $(".button-upgrade").on('click', doUpgrade); 482 | $(".button-upgrade-browse").on('click', function() { 483 | $("input[name='upgrade']")[0].click(); 484 | return false; 485 | }); 486 | $("input[name='upgrade']").change(function (){ 487 | var fileName = $(this).val(); 488 | $("input[name='filename']").val(fileName.replace(/^.*[\\\/]/, '')); 489 | }); 490 | $('progress').attr({ value: 0, max: 100 }); 491 | $(".pure-menu-link").on('click', showPanel); 492 | $(".button-add-network").on('click', function() { 493 | $("div.more", addNetwork()).toggle(); 494 | }); 495 | 496 | var host = window.location.hostname; 497 | var port = location.port; 498 | 499 | $.ajax({ 500 | 'method': 'GET', 501 | 'url': window.location.href + 'auth' 502 | }).done(function(data) { 503 | connect(); 504 | }).fail(function(){ 505 | $("#credentials").show(); 506 | }); 507 | 508 | } 509 | 510 | $(init); 511 | -------------------------------------------------------------------------------- /esp8266/html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/esp8266/html/favicon.ico -------------------------------------------------------------------------------- /esp8266/html/grids-responsive-min.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-2,.pure-u-sm-1-3,.pure-u-sm-2-3,.pure-u-sm-1-4,.pure-u-sm-3-4,.pure-u-sm-1-5,.pure-u-sm-2-5,.pure-u-sm-3-5,.pure-u-sm-4-5,.pure-u-sm-5-5,.pure-u-sm-1-6,.pure-u-sm-5-6,.pure-u-sm-1-8,.pure-u-sm-3-8,.pure-u-sm-5-8,.pure-u-sm-7-8,.pure-u-sm-1-12,.pure-u-sm-5-12,.pure-u-sm-7-12,.pure-u-sm-11-12,.pure-u-sm-1-24,.pure-u-sm-2-24,.pure-u-sm-3-24,.pure-u-sm-4-24,.pure-u-sm-5-24,.pure-u-sm-6-24,.pure-u-sm-7-24,.pure-u-sm-8-24,.pure-u-sm-9-24,.pure-u-sm-10-24,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-sm-1-24{width:4.1667%;*width:4.1357%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%;*width:8.3023%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%;*width:12.469%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%;*width:16.6357%}.pure-u-sm-1-5{width:20%;*width:19.969%}.pure-u-sm-5-24{width:20.8333%;*width:20.8023%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%;*width:24.969%}.pure-u-sm-7-24{width:29.1667%;*width:29.1357%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%;*width:33.3023%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%;*width:37.469%}.pure-u-sm-2-5{width:40%;*width:39.969%}.pure-u-sm-5-12,.pure-u-sm-10-24{width:41.6667%;*width:41.6357%}.pure-u-sm-11-24{width:45.8333%;*width:45.8023%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%;*width:49.969%}.pure-u-sm-13-24{width:54.1667%;*width:54.1357%}.pure-u-sm-7-12,.pure-u-sm-14-24{width:58.3333%;*width:58.3023%}.pure-u-sm-3-5{width:60%;*width:59.969%}.pure-u-sm-5-8,.pure-u-sm-15-24{width:62.5%;*width:62.469%}.pure-u-sm-2-3,.pure-u-sm-16-24{width:66.6667%;*width:66.6357%}.pure-u-sm-17-24{width:70.8333%;*width:70.8023%}.pure-u-sm-3-4,.pure-u-sm-18-24{width:75%;*width:74.969%}.pure-u-sm-19-24{width:79.1667%;*width:79.1357%}.pure-u-sm-4-5{width:80%;*width:79.969%}.pure-u-sm-5-6,.pure-u-sm-20-24{width:83.3333%;*width:83.3023%}.pure-u-sm-7-8,.pure-u-sm-21-24{width:87.5%;*width:87.469%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%;*width:91.6357%}.pure-u-sm-23-24{width:95.8333%;*width:95.8023%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-5-5,.pure-u-sm-24-24{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-2,.pure-u-md-1-3,.pure-u-md-2-3,.pure-u-md-1-4,.pure-u-md-3-4,.pure-u-md-1-5,.pure-u-md-2-5,.pure-u-md-3-5,.pure-u-md-4-5,.pure-u-md-5-5,.pure-u-md-1-6,.pure-u-md-5-6,.pure-u-md-1-8,.pure-u-md-3-8,.pure-u-md-5-8,.pure-u-md-7-8,.pure-u-md-1-12,.pure-u-md-5-12,.pure-u-md-7-12,.pure-u-md-11-12,.pure-u-md-1-24,.pure-u-md-2-24,.pure-u-md-3-24,.pure-u-md-4-24,.pure-u-md-5-24,.pure-u-md-6-24,.pure-u-md-7-24,.pure-u-md-8-24,.pure-u-md-9-24,.pure-u-md-10-24,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-24{width:4.1667%;*width:4.1357%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%;*width:8.3023%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%;*width:12.469%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%;*width:16.6357%}.pure-u-md-1-5{width:20%;*width:19.969%}.pure-u-md-5-24{width:20.8333%;*width:20.8023%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%;*width:24.969%}.pure-u-md-7-24{width:29.1667%;*width:29.1357%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%;*width:33.3023%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%;*width:37.469%}.pure-u-md-2-5{width:40%;*width:39.969%}.pure-u-md-5-12,.pure-u-md-10-24{width:41.6667%;*width:41.6357%}.pure-u-md-11-24{width:45.8333%;*width:45.8023%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%;*width:49.969%}.pure-u-md-13-24{width:54.1667%;*width:54.1357%}.pure-u-md-7-12,.pure-u-md-14-24{width:58.3333%;*width:58.3023%}.pure-u-md-3-5{width:60%;*width:59.969%}.pure-u-md-5-8,.pure-u-md-15-24{width:62.5%;*width:62.469%}.pure-u-md-2-3,.pure-u-md-16-24{width:66.6667%;*width:66.6357%}.pure-u-md-17-24{width:70.8333%;*width:70.8023%}.pure-u-md-3-4,.pure-u-md-18-24{width:75%;*width:74.969%}.pure-u-md-19-24{width:79.1667%;*width:79.1357%}.pure-u-md-4-5{width:80%;*width:79.969%}.pure-u-md-5-6,.pure-u-md-20-24{width:83.3333%;*width:83.3023%}.pure-u-md-7-8,.pure-u-md-21-24{width:87.5%;*width:87.469%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%;*width:91.6357%}.pure-u-md-23-24{width:95.8333%;*width:95.8023%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-5-5,.pure-u-md-24-24{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-2,.pure-u-lg-1-3,.pure-u-lg-2-3,.pure-u-lg-1-4,.pure-u-lg-3-4,.pure-u-lg-1-5,.pure-u-lg-2-5,.pure-u-lg-3-5,.pure-u-lg-4-5,.pure-u-lg-5-5,.pure-u-lg-1-6,.pure-u-lg-5-6,.pure-u-lg-1-8,.pure-u-lg-3-8,.pure-u-lg-5-8,.pure-u-lg-7-8,.pure-u-lg-1-12,.pure-u-lg-5-12,.pure-u-lg-7-12,.pure-u-lg-11-12,.pure-u-lg-1-24,.pure-u-lg-2-24,.pure-u-lg-3-24,.pure-u-lg-4-24,.pure-u-lg-5-24,.pure-u-lg-6-24,.pure-u-lg-7-24,.pure-u-lg-8-24,.pure-u-lg-9-24,.pure-u-lg-10-24,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-lg-1-24{width:4.1667%;*width:4.1357%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%;*width:8.3023%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%;*width:12.469%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%;*width:16.6357%}.pure-u-lg-1-5{width:20%;*width:19.969%}.pure-u-lg-5-24{width:20.8333%;*width:20.8023%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%;*width:24.969%}.pure-u-lg-7-24{width:29.1667%;*width:29.1357%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%;*width:33.3023%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%;*width:37.469%}.pure-u-lg-2-5{width:40%;*width:39.969%}.pure-u-lg-5-12,.pure-u-lg-10-24{width:41.6667%;*width:41.6357%}.pure-u-lg-11-24{width:45.8333%;*width:45.8023%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%;*width:49.969%}.pure-u-lg-13-24{width:54.1667%;*width:54.1357%}.pure-u-lg-7-12,.pure-u-lg-14-24{width:58.3333%;*width:58.3023%}.pure-u-lg-3-5{width:60%;*width:59.969%}.pure-u-lg-5-8,.pure-u-lg-15-24{width:62.5%;*width:62.469%}.pure-u-lg-2-3,.pure-u-lg-16-24{width:66.6667%;*width:66.6357%}.pure-u-lg-17-24{width:70.8333%;*width:70.8023%}.pure-u-lg-3-4,.pure-u-lg-18-24{width:75%;*width:74.969%}.pure-u-lg-19-24{width:79.1667%;*width:79.1357%}.pure-u-lg-4-5{width:80%;*width:79.969%}.pure-u-lg-5-6,.pure-u-lg-20-24{width:83.3333%;*width:83.3023%}.pure-u-lg-7-8,.pure-u-lg-21-24{width:87.5%;*width:87.469%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%;*width:91.6357%}.pure-u-lg-23-24{width:95.8333%;*width:95.8023%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-5-5,.pure-u-lg-24-24{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-2,.pure-u-xl-1-3,.pure-u-xl-2-3,.pure-u-xl-1-4,.pure-u-xl-3-4,.pure-u-xl-1-5,.pure-u-xl-2-5,.pure-u-xl-3-5,.pure-u-xl-4-5,.pure-u-xl-5-5,.pure-u-xl-1-6,.pure-u-xl-5-6,.pure-u-xl-1-8,.pure-u-xl-3-8,.pure-u-xl-5-8,.pure-u-xl-7-8,.pure-u-xl-1-12,.pure-u-xl-5-12,.pure-u-xl-7-12,.pure-u-xl-11-12,.pure-u-xl-1-24,.pure-u-xl-2-24,.pure-u-xl-3-24,.pure-u-xl-4-24,.pure-u-xl-5-24,.pure-u-xl-6-24,.pure-u-xl-7-24,.pure-u-xl-8-24,.pure-u-xl-9-24,.pure-u-xl-10-24,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xl-1-24{width:4.1667%;*width:4.1357%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%;*width:8.3023%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%;*width:12.469%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%;*width:16.6357%}.pure-u-xl-1-5{width:20%;*width:19.969%}.pure-u-xl-5-24{width:20.8333%;*width:20.8023%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%;*width:24.969%}.pure-u-xl-7-24{width:29.1667%;*width:29.1357%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%;*width:33.3023%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%;*width:37.469%}.pure-u-xl-2-5{width:40%;*width:39.969%}.pure-u-xl-5-12,.pure-u-xl-10-24{width:41.6667%;*width:41.6357%}.pure-u-xl-11-24{width:45.8333%;*width:45.8023%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%;*width:49.969%}.pure-u-xl-13-24{width:54.1667%;*width:54.1357%}.pure-u-xl-7-12,.pure-u-xl-14-24{width:58.3333%;*width:58.3023%}.pure-u-xl-3-5{width:60%;*width:59.969%}.pure-u-xl-5-8,.pure-u-xl-15-24{width:62.5%;*width:62.469%}.pure-u-xl-2-3,.pure-u-xl-16-24{width:66.6667%;*width:66.6357%}.pure-u-xl-17-24{width:70.8333%;*width:70.8023%}.pure-u-xl-3-4,.pure-u-xl-18-24{width:75%;*width:74.969%}.pure-u-xl-19-24{width:79.1667%;*width:79.1357%}.pure-u-xl-4-5{width:80%;*width:79.969%}.pure-u-xl-5-6,.pure-u-xl-20-24{width:83.3333%;*width:83.3023%}.pure-u-xl-7-8,.pure-u-xl-21-24{width:87.5%;*width:87.469%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%;*width:91.6357%}.pure-u-xl-23-24{width:95.8333%;*width:95.8023%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-5-5,.pure-u-xl-24-24{width:100%}} 2 | -------------------------------------------------------------------------------- /esp8266/html/images/border-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/esp8266/html/images/border-off.png -------------------------------------------------------------------------------- /esp8266/html/images/border-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/esp8266/html/images/border-on.png -------------------------------------------------------------------------------- /esp8266/html/images/handle-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/esp8266/html/images/handle-center.png -------------------------------------------------------------------------------- /esp8266/html/images/handle-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/esp8266/html/images/handle-left.png -------------------------------------------------------------------------------- /esp8266/html/images/handle-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/esp8266/html/images/handle-right.png -------------------------------------------------------------------------------- /esp8266/html/images/label-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/esp8266/html/images/label-off.png -------------------------------------------------------------------------------- /esp8266/html/images/label-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/esp8266/html/images/label-on.png -------------------------------------------------------------------------------- /esp8266/html/pure-min.css: -------------------------------------------------------------------------------- 1 | html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap;-ms-align-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000\9}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129FEA}.pure-form input:not([type]):focus{outline:0;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-list,.pure-menu-item{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-link,.pure-menu-heading{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-separator{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-allow-hover:hover>.pure-menu-children,.pure-menu-active>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-link,.pure-menu-disabled,.pure-menu-heading{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:hover,.pure-menu-link:focus{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} 2 | -------------------------------------------------------------------------------- /esp8266/html/side-menu.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #777; 3 | } 4 | 5 | .pure-img-responsive { 6 | max-width: 100%; 7 | height: auto; 8 | } 9 | 10 | /* 11 | Add transition to containers so they can push in and out. 12 | */ 13 | #layout, 14 | #menu, 15 | .menu-link { 16 | -webkit-transition: all 0.2s ease-out; 17 | -moz-transition: all 0.2s ease-out; 18 | -ms-transition: all 0.2s ease-out; 19 | -o-transition: all 0.2s ease-out; 20 | transition: all 0.2s ease-out; 21 | } 22 | 23 | /* 24 | This is the parent `
` that contains the menu and the content area. 25 | */ 26 | #layout { 27 | position: relative; 28 | padding-left: 0; 29 | } 30 | #layout.active #menu { 31 | left: 150px; 32 | width: 150px; 33 | } 34 | 35 | #layout.active .menu-link { 36 | left: 150px; 37 | } 38 | /* 39 | The content `
` is where all your content goes. 40 | */ 41 | .content { 42 | margin: 0 auto; 43 | padding: 0 2em; 44 | max-width: 800px; 45 | margin-bottom: 50px; 46 | line-height: 1.6em; 47 | } 48 | 49 | .header { 50 | margin: 0; 51 | color: #333; 52 | text-align: center; 53 | padding: 2.5em 2em 0; 54 | border-bottom: 1px solid #eee; 55 | } 56 | .header h1 { 57 | margin: 0.2em 0; 58 | font-size: 3em; 59 | font-weight: 300; 60 | } 61 | .header h2 { 62 | font-weight: 300; 63 | color: #ccc; 64 | padding: 0; 65 | margin-top: 0; 66 | } 67 | 68 | .content-subhead { 69 | margin: 50px 0 20px 0; 70 | font-weight: 300; 71 | color: #888; 72 | } 73 | 74 | 75 | 76 | /* 77 | The `#menu` `
` is the parent `
` that contains the `.pure-menu` that 78 | appears on the left side of the page. 79 | */ 80 | 81 | #menu { 82 | margin-left: -150px; /* "#menu" width */ 83 | width: 150px; 84 | position: fixed; 85 | top: 0; 86 | left: 0; 87 | bottom: 0; 88 | z-index: 1000; /* so the menu or its navicon stays above all content */ 89 | background: #191818; 90 | overflow-y: auto; 91 | -webkit-overflow-scrolling: touch; 92 | } 93 | /* 94 | All anchors inside the menu should be styled like this. 95 | */ 96 | #menu a { 97 | color: #999; 98 | border: none; 99 | padding: 0.6em 0 0.6em 0.6em; 100 | } 101 | 102 | /* 103 | Remove all background/borders, since we are applying them to #menu. 104 | */ 105 | #menu .pure-menu, 106 | #menu .pure-menu ul { 107 | border: none; 108 | background: transparent; 109 | } 110 | 111 | /* 112 | Add that light border to separate items into groups. 113 | */ 114 | #menu .pure-menu ul, 115 | #menu .pure-menu .menu-item-divided { 116 | border-top: 1px solid #333; 117 | } 118 | /* 119 | Change color of the anchor links on hover/focus. 120 | */ 121 | #menu .pure-menu li a:hover, 122 | #menu .pure-menu li a:focus { 123 | background: #333; 124 | } 125 | 126 | /* 127 | This styles the selected menu item `
  • `. 128 | */ 129 | #menu .pure-menu-selected, 130 | #menu .pure-menu-heading { 131 | background: #1f8dd6; 132 | } 133 | /* 134 | This styles a link within a selected menu item `
  • `. 135 | */ 136 | #menu .pure-menu-selected a { 137 | color: #fff; 138 | } 139 | 140 | /* 141 | This styles the menu heading. 142 | */ 143 | #menu .pure-menu-heading { 144 | font-size: 110%; 145 | color: #fff; 146 | margin: 0; 147 | } 148 | 149 | /* -- Dynamic Button For Responsive Menu -------------------------------------*/ 150 | 151 | /* 152 | The button to open/close the Menu is custom-made and not part of Pure. Here's 153 | how it works: 154 | */ 155 | 156 | /* 157 | `.menu-link` represents the responsive menu toggle that shows/hides on 158 | small screens. 159 | */ 160 | .menu-link { 161 | position: fixed; 162 | display: block; /* show this only on small screens */ 163 | top: 0; 164 | left: 0; /* "#menu width" */ 165 | background: #000; 166 | background: rgba(0,0,0,0.7); 167 | font-size: 10px; /* change this value to increase/decrease button size */ 168 | z-index: 10; 169 | width: 2em; 170 | height: auto; 171 | padding: 2.1em 1.6em; 172 | } 173 | 174 | .menu-link:hover, 175 | .menu-link:focus { 176 | background: #000; 177 | } 178 | 179 | .menu-link span { 180 | position: relative; 181 | display: block; 182 | } 183 | 184 | .menu-link span, 185 | .menu-link span:before, 186 | .menu-link span:after { 187 | background-color: #fff; 188 | width: 100%; 189 | height: 0.2em; 190 | } 191 | 192 | .menu-link span:before, 193 | .menu-link span:after { 194 | position: absolute; 195 | margin-top: -0.6em; 196 | content: " "; 197 | } 198 | 199 | .menu-link span:after { 200 | margin-top: 0.6em; 201 | } 202 | 203 | 204 | /* -- Responsive Styles (Media Queries) ------------------------------------- */ 205 | 206 | /* 207 | Hides the menu at `48em`, but modify this based on your app's needs. 208 | */ 209 | @media (min-width: 48em) { 210 | 211 | .header, 212 | .content { 213 | padding-left: 2em; 214 | padding-right: 2em; 215 | } 216 | 217 | #layout { 218 | padding-left: 150px; /* left col width "#menu" */ 219 | left: 0; 220 | } 221 | #menu { 222 | left: 150px; 223 | } 224 | 225 | .menu-link { 226 | position: fixed; 227 | left: 150px; 228 | display: none; 229 | } 230 | 231 | #layout.active .menu-link { 232 | left: 150px; 233 | } 234 | } 235 | 236 | @media (max-width: 48em) { 237 | /* Only apply this when the window is small. Otherwise, the following 238 | case results in extra padding on the left: 239 | * Make the window small. 240 | * Tap the menu to trigger the active state. 241 | * Make the window large again. 242 | */ 243 | #layout.active { 244 | position: relative; 245 | left: 150px; 246 | } 247 | } 248 | 249 | -------------------------------------------------------------------------------- /esp8266/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp8266-filesystem-builder", 3 | "version": "0.1.0", 4 | "description": "Gulp based build system for ESP8266 file system files", 5 | "main": "gulpfile.js", 6 | "author": "Xose Pérez ", 7 | "license": "GPL-3.0", 8 | "devDependencies": { 9 | "del": "^2.2.1", 10 | "gulp": "^3.9.1", 11 | "gulp-base64-favicon": "^1.0.2", 12 | "gulp-clean-css": "^3.4.2", 13 | "gulp-css-base64": "^1.3.4", 14 | "gulp-gzip": "^1.4.0", 15 | "gulp-htmlmin": "^2.0.0", 16 | "gulp-inline": "^0.1.1", 17 | "gulp-uglify": "^1.5.3" 18 | }, 19 | "dependencies": {} 20 | } 21 | -------------------------------------------------------------------------------- /esp8266/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter, extra scripting 4 | ; Upload options: custom port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; 7 | ; Please visit documentation for the other options and examples 8 | ; http://docs.platformio.org/en/stable/projectconf.html 9 | 10 | [platformio] 11 | env_default = sonoffsc 12 | src_dir = sonoffsc 13 | data_dir = sonoffsc/data 14 | 15 | [common] 16 | lib_deps = 17 | ArduinoJson 18 | https://github.com/xoseperez/Time 19 | https://github.com/me-no-dev/ESPAsyncTCP#991f855 20 | https://github.com/me-no-dev/ESPAsyncWebServer#a94265d 21 | https://github.com/marvinroger/async-mqtt-client#v0.8.1 22 | PubSubClient 23 | Embedis 24 | NtpClientLib 25 | https://github.com/xoseperez/seriallink#0.1.0 26 | https://bitbucket.org/xoseperez/justwifi.git#1.1.4 27 | https://bitbucket.org/xoseperez/fauxmoesp.git#2.2.0 28 | https://bitbucket.org/xoseperez/nofuss.git#0.2.5 29 | https://bitbucket.org/xoseperez/debounceevent.git#2.0.1 30 | build_flags = -g -Wl,-Tesp8266.flash.1m64.ld 31 | 32 | [env:sonoffsc] 33 | platform = https://github.com/platformio/platform-espressif8266.git#v1.5.0 34 | framework = arduino 35 | board = esp01_1m 36 | flash_mode = dio 37 | lib_deps = ${common.lib_deps} 38 | build_flags = ${common.build_flags} 39 | 40 | [env:ota] 41 | platform = https://github.com/platformio/platform-espressif8266.git#v1.5.0 42 | framework = arduino 43 | board = esp01_1m 44 | flash_mode = dio 45 | lib_deps = ${common.lib_deps} 46 | build_flags = ${common.build_flags} 47 | upload_speed = 115200 48 | upload_port = "sonoffsc.local" 49 | upload_flags = --auth=Algernon1 --port 8266 50 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/alexa.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | ALEXA MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if ALEXA_SUPPORT 10 | 11 | #include 12 | 13 | fauxmoESP alexa; 14 | 15 | // ----------------------------------------------------------------------------- 16 | // ALEXA 17 | // ----------------------------------------------------------------------------- 18 | 19 | void alexaConfigure() { 20 | alexa.enable(getSetting("alexaEnabled", ALEXA_ENABLED).toInt() == 1); 21 | } 22 | 23 | void alexaSetup() { 24 | 25 | alexa.addDevice("clap"); 26 | 27 | alexa.onMessage([](unsigned char device_id, const char * name, bool state) { 28 | 29 | DEBUG_MSG("[FAUXMO] %s state: %s\n", name, state ? "ON" : "OFF"); 30 | 31 | if (strcmp(name, "clap") == 0) { 32 | setSetting("clapEnabled", state); 33 | if (state) { 34 | wsSend((char *) "{\"clapEnabled\": true}"); 35 | } else { 36 | wsSend((char *) "{\"clapEnabled\": false}"); 37 | } 38 | commsConfigure(); 39 | } 40 | 41 | }); 42 | 43 | } 44 | 45 | void alexaLoop() { 46 | alexa.handle(); 47 | } 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/button.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | BUTTON MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | // ----------------------------------------------------------------------------- 10 | // BUTTON 11 | // ----------------------------------------------------------------------------- 12 | 13 | #include 14 | 15 | DebounceEvent _button = DebounceEvent(BUTTON_PIN, BUTTON_PUSHBUTTON, BUTTON_DEBOUNCE_DELAY, BUTTON_DBLCLICK_DELAY); 16 | 17 | void buttonSetup() { 18 | } 19 | 20 | void buttonLoop() { 21 | 22 | if (uint8_t event = _button.loop()) { 23 | 24 | if (event == EVENT_RELEASED) { 25 | 26 | DEBUG_MSG("[BUTTON] Button pressed. Event: %d Length:%d\n", _button.getEventCount(), _button.getEventLength()); 27 | 28 | if (_button.getEventCount() == 1) { 29 | 30 | if(_button.getEventLength() >= BUTTON_LNGLNGCLICK_DELAY) { 31 | customReset(CUSTOM_RESET_HARDWARE); 32 | ESP.restart(); 33 | } 34 | 35 | if(_button.getEventLength() >= BUTTON_LNGCLICK_DELAY) { 36 | DEBUG_MSG_P(PSTR("\n\nFACTORY RESET\n\n")); 37 | settingsFactoryReset(); 38 | customReset(CUSTOM_RESET_FACTORY); 39 | ESP.restart(); 40 | } 41 | 42 | } 43 | 44 | if (_button.getEventCount() >= 2) { 45 | createAP(); 46 | } 47 | 48 | } 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/comms.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | COMMUNICATIONS MODULE 4 | 5 | Copyright (C) 2016 by Xose Pérez 6 | 7 | */ 8 | 9 | #include "SerialLink.h" 10 | 11 | SerialLink link(Serial, false); 12 | 13 | const PROGMEM char at_hello[] = "AT+HELLO"; 14 | const PROGMEM char at_push[] = "AT+PUSH"; 15 | const PROGMEM char at_every[] = "AT+EVERY"; 16 | const PROGMEM char at_temp[] = "AT+TEMP"; 17 | const PROGMEM char at_hum[] = "AT+HUM"; 18 | const PROGMEM char at_dust[] = "AT+DUST"; 19 | const PROGMEM char at_noise[] = "AT+NOISE"; 20 | const PROGMEM char at_light[] = "AT+LIGHT"; 21 | const PROGMEM char at_clap[] = "AT+CLAP"; 22 | const PROGMEM char at_code[] = "AT+CODE"; 23 | const PROGMEM char at_thld[] = "AT+THLD"; 24 | const PROGMEM char at_fan[] = "AT+FAN"; 25 | const PROGMEM char at_fanoff[] = "AT+FANOFF"; 26 | const PROGMEM char at_timeout[] = "AT+TIMEOUT"; 27 | const PROGMEM char at_effect[] = "AT+EFFECT"; 28 | const PROGMEM char at_color[] = "AT+COLOR"; 29 | const PROGMEM char at_bright[] = "AT+BRIGHT"; 30 | const PROGMEM char at_speed[] = "AT+SPEED"; 31 | const PROGMEM char at_move[] = "AT+MOVE"; 32 | 33 | // ----------------------------------------------------------------------------- 34 | // VALUES 35 | // ----------------------------------------------------------------------------- 36 | 37 | float temperature; 38 | int humidity; 39 | int light; 40 | float dust; 41 | int noise; 42 | bool movement; 43 | 44 | bool gotResponse = false; 45 | long response; 46 | 47 | float getTemperature() { return temperature; } 48 | float getHumidity() { return humidity; } 49 | float getLight() { return light; } 50 | float getDust() { return dust; } 51 | float getNoise() { return noise; } 52 | float getMovement() { return movement; } 53 | 54 | // ----------------------------------------------------------------------------- 55 | // COMMUNICATIONS 56 | // ----------------------------------------------------------------------------- 57 | 58 | bool commsGet(char * key) { 59 | return false; 60 | } 61 | 62 | bool commsSet(char * key, long value) { 63 | 64 | char buffer[50]; 65 | 66 | if (strcmp_P(key, at_code) == 0) { 67 | mqttSend(getSetting("mqttTopicClap", MQTT_TOPIC_CLAP).c_str(), String(value).c_str()); 68 | return true; 69 | } 70 | 71 | if (strcmp_P(key, at_temp) == 0) { 72 | temperature = (float) value / 10; 73 | if (temperature < SENSOR_TEMPERATURE_MIN || SENSOR_TEMPERATURE_MAX < temperature) return false; 74 | mqttSend(getSetting("mqttTopicTemp", MQTT_TOPIC_TEMPERATURE).c_str(), String(temperature).c_str()); 75 | domoticzSend("dczIdxTemp", temperature); 76 | sprintf(buffer, "{\"sensorTemp\": %s}", String(temperature).c_str()); 77 | wsSend(buffer); 78 | return true; 79 | } 80 | 81 | if (strcmp_P(key, at_hum) == 0) { 82 | humidity = value; 83 | if (humidity < SENSOR_HUMIDITY_MIN || SENSOR_HUMIDITY_MAX < humidity) return false; 84 | mqttSend(getSetting("mqttTopicHum", MQTT_TOPIC_HUMIDITY).c_str(), String(humidity).c_str()); 85 | domoticzSend("dczIdxHum", humidity); 86 | sprintf(buffer, "{\"sensorHum\": %d}", humidity); 87 | wsSend(buffer); 88 | return true; 89 | } 90 | 91 | if (strcmp_P(key, at_light) == 0) { 92 | light = value; 93 | if (light < 0 || 100 < light) return false; 94 | mqttSend(getSetting("mqttTopicLight", MQTT_TOPIC_LIGHT).c_str(), String(light).c_str()); 95 | domoticzSend("dczIdxLight", light); 96 | sprintf(buffer, "{\"sensorLight\": %d}", light); 97 | wsSend(buffer); 98 | return true; 99 | } 100 | 101 | if (strcmp_P(key, at_dust) == 0) { 102 | dust = (float) value / 100; 103 | if (dust < SENSOR_DUST_MIN || SENSOR_DUST_MAX < dust) return false; 104 | mqttSend(getSetting("mqttTopicDust", MQTT_TOPIC_DUST).c_str(), String(dust).c_str()); 105 | domoticzSend("dczIdxDust", dust); 106 | sprintf(buffer, "{\"sensorDust\": %s}", String(dust).c_str()); 107 | wsSend(buffer); 108 | return true; 109 | } 110 | 111 | if (strcmp_P(key, at_noise) == 0) { 112 | noise = value; 113 | if (noise < 0 || 100 < noise) return false; 114 | mqttSend(getSetting("mqttTopicNoise", MQTT_TOPIC_NOISE).c_str(), String(noise).c_str()); 115 | domoticzSend("dczIdxNoise", noise); 116 | sprintf(buffer, "{\"sensorNoise\": %d}", noise); 117 | wsSend(buffer); 118 | return true; 119 | } 120 | 121 | if (strcmp_P(key, at_move) == 0) { 122 | movement = value; 123 | mqttSend(getSetting("mqttTopicMovement", MQTT_TOPIC_MOVE).c_str(), movement ? "1" : "0"); 124 | domoticzSend("dczIdxMovement", movement); 125 | sprintf(buffer, "{\"sensorMove\": %d}", movement ? 1 : 0); 126 | wsSend(buffer); 127 | #if LOCAL_NOTIFICATION 128 | sendNotification(movement); 129 | #endif 130 | return true; 131 | } 132 | 133 | gotResponse = true; 134 | response = value; 135 | 136 | return true; 137 | 138 | } 139 | 140 | bool send_P_repeat(const char * command, long payload, unsigned char tries = COMMS_DEFAULT_TRIES) { 141 | link.clear(); 142 | while (tries--) { 143 | delay(50); 144 | link.send_P(command, payload); 145 | } 146 | } 147 | 148 | void commsConfigure() { 149 | link.clear(); 150 | delay(200); 151 | send_P_repeat(at_every, getSetting("sensorEvery", SENSOR_EVERY).toInt()); 152 | send_P_repeat(at_clap, getSetting("clapEnabled", SENSOR_CLAP_ENABLED).toInt() == 1 ? 1 : 0); 153 | send_P_repeat(at_push,1); 154 | } 155 | 156 | void commsSetup() { 157 | 158 | link.onGet(commsGet); 159 | link.onSet(commsSet); 160 | link.clear(); 161 | delay(200); 162 | 163 | // Set FAN mode depending on delay 164 | send_P_repeat(at_fanoff, FAN_DELAY); 165 | send_P_repeat(at_fan, FAN_DELAY == 0); 166 | 167 | } 168 | 169 | void commsLoop() { 170 | link.handle(); 171 | } 172 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/config/all.h: -------------------------------------------------------------------------------- 1 | #include "version.h" 2 | #include "arduino.h" 3 | #include "general.h" 4 | #include "prototypes.h" 5 | 6 | /* 7 | If you want to modify the stock configuration but you don't want to touch 8 | the repo files you can either define USE_CUSTOM_H or remove the 9 | "#ifdef USE_CUSTOM_H" & "#endif" lines and add a "custom.h" 10 | file to this same folder. 11 | Check https://bitbucket.org/xoseperez/espurna/issues/104/general_customh 12 | for an example on how to use this file. 13 | (Define USE_CUSTOM_H on commandline for platformio: 14 | export PLATFORMIO_BUILD_FLAGS="'-DUSE_CUSTOM_H'" ) 15 | */ 16 | #ifdef USE_CUSTOM_H 17 | #include "custom.h" 18 | #endif 19 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/config/arduino.h: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------- 2 | // These settings are normally provided by PlatformIO 3 | // Uncomment the appropiate line(s) to build from the Arduino IDE 4 | //-------------------------------------------------------------------------------- 5 | 6 | //-------------------------------------------------------------------------------- 7 | // Features (values below are non-default values) 8 | //-------------------------------------------------------------------------------- 9 | 10 | //#define ALEXA_SUPPORT 0 11 | //#define DEBUG_SERIAL_SUPPORT 0 12 | //#define DEBUG_TELNET_SUPPORT 0 13 | //#define DEBUG_UDP_SUPPORT 1 14 | //#define DOMOTICZ_SUPPORT 0 15 | //#define MDNS_SUPPORT 0 16 | //#define NOFUSS_SUPPORT 1 17 | //#define NTP_SUPPORT 0 18 | //#define SPIFFS_SUPPORT 1 19 | //#define TELNET_SUPPORT 0 20 | //#define TERMINAL_SUPPORT 0 21 | //#define WEB_SUPPORT 0 22 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/config/general.h: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // GENERAL 3 | //------------------------------------------------------------------------------ 4 | 5 | #define ADMIN_PASS "fibonacci" // Default password (WEB, OTA, WIFI) 6 | #define SERIAL_BAUDRATE 9600 7 | #define DEVICE_NAME APP_NAME 8 | #define MANUFACTURER "ITEAD STUDIO" 9 | 10 | //------------------------------------------------------------------------------ 11 | // TELNET 12 | //------------------------------------------------------------------------------ 13 | 14 | #ifndef TELNET_SUPPORT 15 | #define TELNET_SUPPORT 1 // Enable telnet support by default 16 | #endif 17 | 18 | #ifndef TELNET_ONLY_AP 19 | #define TELNET_ONLY_AP 0 // By default, allow only connections via AP interface 20 | #endif 21 | 22 | #define TELNET_PORT 23 // Port to listen to telnet clients 23 | #define TELNET_MAX_CLIENTS 1 // Max number of concurrent telnet clients 24 | 25 | //------------------------------------------------------------------------------ 26 | // DEBUG 27 | //------------------------------------------------------------------------------ 28 | 29 | // Serial debug log 30 | 31 | #ifndef DEBUG_SERIAL_SUPPORT 32 | #define DEBUG_SERIAL_SUPPORT 0 // Do not enable serial debug log 33 | #endif 34 | #ifndef DEBUG_PORT 35 | #define DEBUG_PORT Serial // Default debugging port 36 | #endif 37 | 38 | //------------------------------------------------------------------------------ 39 | 40 | // UDP debug log 41 | // To receive the message son the destination computer use nc: 42 | // nc -ul 8113 43 | 44 | #ifndef DEBUG_UDP_SUPPORT 45 | #define DEBUG_UDP_SUPPORT 0 // Enable UDP debug log 46 | #endif 47 | #define DEBUG_UDP_IP IPAddress(192, 168, 1, 100) 48 | #define DEBUG_UDP_PORT 8113 49 | 50 | //------------------------------------------------------------------------------ 51 | 52 | #ifndef DEBUG_TELNET_SUPPORT 53 | #define DEBUG_TELNET_SUPPORT TELNET_SUPPORT // Enable telnet debug log if telnet is enabled too 54 | #endif 55 | 56 | //------------------------------------------------------------------------------ 57 | 58 | // General debug options and macros 59 | #define DEBUG_MESSAGE_MAX_LENGTH 80 60 | #define DEBUG_SUPPORT DEBUG_SERIAL_SUPPORT || DEBUG_UDP_SUPPORT || DEBUG_TELNET_SUPPORT 61 | 62 | 63 | #if DEBUG_SUPPORT 64 | #define DEBUG_MSG(...) debugSend(__VA_ARGS__) 65 | #define DEBUG_MSG_P(...) debugSend_P(__VA_ARGS__) 66 | #endif 67 | 68 | #ifndef DEBUG_MSG 69 | #define DEBUG_MSG(...) 70 | #define DEBUG_MSG_P(...) 71 | #endif 72 | 73 | //------------------------------------------------------------------------------ 74 | // TERMINAL 75 | //------------------------------------------------------------------------------ 76 | 77 | #ifndef TERMINAL_SUPPORT 78 | #define TERMINAL_SUPPORT 0 // Terminal support conflicts comms 79 | #endif 80 | 81 | //------------------------------------------------------------------------------ 82 | // CRASH 83 | //------------------------------------------------------------------------------ 84 | 85 | #define CRASH_SAFE_TIME 60000 // The system is considered stable after these many millis 86 | #define CRASH_COUNT_MAX 5 // After this many crashes on boot 87 | // the system is flagged as unstable 88 | 89 | //------------------------------------------------------------------------------ 90 | // EEPROM 91 | //------------------------------------------------------------------------------ 92 | 93 | #define EEPROM_SIZE 4096 // EEPROM size in bytes 94 | #define EEPROM_CUSTOM_RESET 0 // Address for the reset reason (1 byte) 95 | #define EEPROM_CRASH_COUNTER 1 // Address for the crash counter (1 byte) 96 | #define EEPROM_DATA_END 2 // End of custom EEPROM data block 97 | 98 | //------------------------------------------------------------------------------ 99 | // HEARTBEAT 100 | //------------------------------------------------------------------------------ 101 | 102 | #define HEARTBEAT_INTERVAL 300000 // Interval between heartbeat messages (in ms) 103 | #define UPTIME_OVERFLOW 4294967295 // Uptime overflow value 104 | 105 | //------------------------------------------------------------------------------ 106 | // RESET 107 | //------------------------------------------------------------------------------ 108 | 109 | #define CUSTOM_RESET_HARDWARE 1 // Reset from hardware button 110 | #define CUSTOM_RESET_WEB 2 // Reset from web interface 111 | #define CUSTOM_RESET_TERMINAL 3 // Reset from terminal 112 | #define CUSTOM_RESET_MQTT 4 // Reset via MQTT 113 | #define CUSTOM_RESET_RPC 5 // Reset via RPC (HTTP) 114 | #define CUSTOM_RESET_OTA 6 // Reset after successful OTA update 115 | #define CUSTOM_RESET_NOFUSS 8 // Reset after successful NOFUSS update 116 | #define CUSTOM_RESET_UPGRADE 9 // Reset after update from web interface 117 | #define CUSTOM_RESET_FACTORY 10 // Factory reset from terminal 118 | 119 | #define CUSTOM_RESET_MAX 10 120 | 121 | #include 122 | 123 | PROGMEM const char custom_reset_hardware[] = "Hardware button"; 124 | PROGMEM const char custom_reset_web[] = "Reset from web interface"; 125 | PROGMEM const char custom_reset_terminal[] = "Reset from terminal"; 126 | PROGMEM const char custom_reset_mqtt[] = "Reset from MQTT"; 127 | PROGMEM const char custom_reset_rpc[] = "Reset from RPC"; 128 | PROGMEM const char custom_reset_ota[] = "Reset after successful OTA update"; 129 | PROGMEM const char custom_reset_nofuss[] = "Reset after successful NoFUSS update"; 130 | PROGMEM const char custom_reset_upgrade[] = "Reset after successful web update"; 131 | PROGMEM const char custom_reset_factory[] = "Factory reset"; 132 | PROGMEM const char* const custom_reset_string[] = { 133 | custom_reset_hardware, custom_reset_web, custom_reset_terminal, 134 | custom_reset_mqtt, custom_reset_rpc, custom_reset_ota, 135 | custom_reset_nofuss, custom_reset_upgrade, custom_reset_factory 136 | }; 137 | 138 | //------------------------------------------------------------------------------ 139 | // BUTTON 140 | //------------------------------------------------------------------------------ 141 | 142 | #define BUTTON_DEBOUNCE_DELAY 50 // Debounce delay (ms) 143 | #define BUTTON_DBLCLICK_DELAY 500 // Time in ms to wait for a second (or third...) click 144 | #define BUTTON_LNGCLICK_DELAY 1000 // Time in ms holding the button down to get a long click 145 | #define BUTTON_LNGLNGCLICK_DELAY 10000 // Time in ms holding the button down to get a long-long click 146 | 147 | #define BUTTON_PIN 0 148 | 149 | // ----------------------------------------------------------------------------- 150 | // WIFI 151 | // ----------------------------------------------------------------------------- 152 | 153 | #define WIFI_CONNECT_TIMEOUT 30000 // Connecting timeout for WIFI in ms 154 | #define WIFI_RECONNECT_INTERVAL 300000 155 | #define WIFI_MAX_NETWORKS 5 156 | #define WIFI_AP_MODE AP_MODE_ALONE 157 | 158 | // Optional hardcoded configuration (up to 2 different networks) 159 | //#define WIFI1_SSID "..." 160 | //#define WIFI1_PASS "..." 161 | //#define WIFI1_IP "192.168.1.201" 162 | //#define WIFI1_GW "192.168.1.1" 163 | //#define WIFI1_MASK "255.255.255.0" 164 | //#define WIFI1_DNS "8.8.8.8" 165 | //#define WIFI2_SSID "..." 166 | //#define WIFI2_PASS "..." 167 | 168 | // ----------------------------------------------------------------------------- 169 | // WEB 170 | // ----------------------------------------------------------------------------- 171 | 172 | #ifndef WEB_SUPPORT 173 | #define WEB_SUPPORT 1 // Enable web support (http, api) 174 | #endif 175 | 176 | #ifndef WEB_EMBEDDED 177 | #define WEB_EMBEDDED 1 // Build the firmware with the web interface embedded in 178 | #endif 179 | 180 | // This is not working at the moment!! 181 | // Requires ASYNC_TCP_SSL_ENABLED to 1 and ESP8266 Arduino Core staging version. 182 | #define WEB_SSL_ENABLED 0 // Use HTTPS web interface 183 | 184 | #define WEB_MODE_NORMAL 0 185 | #define WEB_MODE_PASSWORD 1 186 | 187 | #define WEB_USERNAME "admin" // HTTP username 188 | #define WEB_FORCE_PASS_CHANGE 1 // Force the user to change the password if default one 189 | #define WEB_PORT 80 // HTTP port 190 | 191 | // ----------------------------------------------------------------------------- 192 | // WEBSOCKETS 193 | // ----------------------------------------------------------------------------- 194 | 195 | // This will only be enabled if WEB_SUPPORT is 1 (this is the default value) 196 | 197 | #define WS_BUFFER_SIZE 5 // Max number of secured websocket connections 198 | #define WS_TIMEOUT 1800000 // Timeout for secured websocket 199 | 200 | // ----------------------------------------------------------------------------- 201 | // API 202 | // ----------------------------------------------------------------------------- 203 | 204 | // This will only be enabled if WEB_SUPPORT is 1 (this is the default value) 205 | 206 | #define API_ENABLED 0 // Do not enable API by default 207 | #define API_BUFFER_SIZE 10 // Size of the buffer for HTTP GET API responses 208 | 209 | // ----------------------------------------------------------------------------- 210 | // MDNS 211 | // ----------------------------------------------------------------------------- 212 | 213 | #ifndef MDNS_SUPPORT 214 | #define MDNS_SUPPORT 1 // Publish services using mDNS by default 215 | #endif 216 | 217 | // ----------------------------------------------------------------------------- 218 | // SPIFFS 219 | // ----------------------------------------------------------------------------- 220 | 221 | #ifndef SPIFFS_SUPPORT 222 | #define SPIFFS_SUPPORT 0 // Do not add support for SPIFFS by default 223 | #endif 224 | 225 | // ----------------------------------------------------------------------------- 226 | // OTA 227 | // ----------------------------------------------------------------------------- 228 | 229 | #define OTA_PORT 8266 // OTA port 230 | 231 | // ----------------------------------------------------------------------------- 232 | // NOFUSS 233 | // ----------------------------------------------------------------------------- 234 | 235 | #ifndef NOFUSS_SUPPORT 236 | #define NOFUSS_SUPPORT 0 // Do not enable support for NoFuss by default 237 | #endif 238 | 239 | #define NOFUSS_ENABLED 0 // Do not perform NoFUSS updates by default 240 | #define NOFUSS_SERVER "" // Default NoFuss Server 241 | #define NOFUSS_INTERVAL 3600000 // Check for updates every hour 242 | 243 | // ----------------------------------------------------------------------------- 244 | // MQTT 245 | // ----------------------------------------------------------------------------- 246 | 247 | #ifndef MQTT_USE_ASYNC 248 | #define MQTT_USE_ASYNC 1 // Use AysncMQTTClient (1) or PubSubClient (0) 249 | #endif 250 | 251 | // MQTT OVER SSL 252 | // Using MQTT over SSL works pretty well but generates problems with the web interface. 253 | // It could be a good idea to use it in conjuntion with WEB_SUPPORT=0. 254 | // Requires ASYNC_TCP_SSL_ENABLED to 1 and ESP8266 Arduino Core staging version. 255 | // 256 | // You can use it with MQTT_USE_ASYNC=1 (AsyncMqttClient library) 257 | // but you might experience hiccups on the web interface, so my recommendation is: 258 | // WEB_SUPPORT=0 259 | // 260 | // If you use it with MQTT_USE_ASYNC=0 (PubSubClient library) 261 | // you will have to disable all the modules that use ESPAsyncTCP, that is: 262 | // ALEXA_SUPPORT=0, INFLUXDB_SUPPORT=0, TELNET_SUPPORT=0 and WEB_SUPPORT=0 263 | // 264 | // You will need the fingerprint for your MQTT server, example for CloudMQTT: 265 | // $ echo -n | openssl s_client -connect m11.cloudmqtt.com:24055 > cloudmqtt.pem 266 | // $ openssl x509 -noout -in cloudmqtt.pem -fingerprint -sha1 267 | 268 | #define MQTT_SSL_ENABLED 0 // By default MQTT over SSL will not be enabled 269 | #define MQTT_SSL_FINGERPRINT "" // SSL fingerprint of the server 270 | 271 | #define MQTT_ENABLED 0 // Do not enable MQTT connection by default 272 | #define MQTT_AUTOCONNECT 1 // If enabled and MDNS_SUPPORT=1 will perform an autodiscover and 273 | 274 | #define MQTT_SERVER "" // Default MQTT broker address 275 | #define MQTT_USER "" // Default MQTT broker usename 276 | #define MQTT_PASS "" // Default MQTT broker password 277 | #define MQTT_PORT 1883 // MQTT broker port 278 | #define MQTT_TOPIC "/test/sonoffsc" // Default MQTT base topic 279 | #define MQTT_RETAIN true // MQTT retain flag 280 | #define MQTT_QOS 0 // MQTT QoS value for all messages 281 | #define MQTT_KEEPALIVE 30 // MQTT keepalive value 282 | 283 | #define MQTT_RECONNECT_DELAY_MIN 5000 // Try to reconnect in 5 seconds upon disconnection 284 | #define MQTT_RECONNECT_DELAY_STEP 5000 // Increase the reconnect delay in 5 seconds after each failed attempt 285 | #define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most 286 | 287 | #define MQTT_SKIP_RETAINED 1 // Skip retained messages on connection 288 | #define MQTT_SKIP_TIME 1000 // Skip messages for 1 second anter connection 289 | 290 | #define MQTT_USE_JSON 0 // Group messages in a JSON body 291 | #define MQTT_USE_JSON_DELAY 100 // Wait this many ms before grouping messages 292 | 293 | // Internal MQTT events (do not change) 294 | #define MQTT_CONNECT_EVENT 0 295 | #define MQTT_DISCONNECT_EVENT 1 296 | #define MQTT_MESSAGE_EVENT 2 297 | 298 | // Topics 299 | #define MQTT_TOPIC_STATUS "status" 300 | #define MQTT_TOPIC_IP "ip" 301 | #define MQTT_TOPIC_VERSION "version" 302 | #define MQTT_TOPIC_HEARTBEAT "heartbeat" 303 | #define MQTT_TOPIC_MODE "mode" 304 | #define MQTT_TOPIC_INTERVAL "interval" 305 | #define MQTT_TOPIC_TEMPERATURE "temperature" 306 | #define MQTT_TOPIC_HUMIDITY "humidity" 307 | #define MQTT_TOPIC_NOISE "noise" 308 | #define MQTT_TOPIC_CLAP "clap" 309 | #define MQTT_TOPIC_DUST "dust" 310 | #define MQTT_TOPIC_LIGHT "light" 311 | #define MQTT_TOPIC_RGB "color" 312 | #define MQTT_TOPIC_BRIGHTNESS "brightness" 313 | #define MQTT_TOPIC_SPEED "speed" 314 | #define MQTT_TOPIC_EFFECT "effect" 315 | #define MQTT_TOPIC_MOVE "movement" 316 | 317 | #define MQTT_TOPIC_TIME "time" 318 | #define MQTT_TOPIC_HOSTNAME "host" 319 | 320 | #define MQTT_TOPIC_JSON "data" 321 | #define MQTT_TOPIC_ACTION "action" 322 | #define MQTT_ACTION_RESET "reset" 323 | 324 | // Custom get and set postfixes 325 | // Use something like "/status" or "/set", with leading slash 326 | // Since 1.9.0 the default value is "" for getter and "/set" for setter 327 | #define MQTT_USE_GETTER "" 328 | #define MQTT_USE_SETTER "/set" 329 | 330 | // ----------------------------------------------------------------------------- 331 | // SETTINGS 332 | // ----------------------------------------------------------------------------- 333 | 334 | #ifndef SETTINGS_AUTOSAVE 335 | #define SETTINGS_AUTOSAVE 1 // Autosave settings o force manual commit 336 | #endif 337 | 338 | // ----------------------------------------------------------------------------- 339 | // DOMOTICZ 340 | // ----------------------------------------------------------------------------- 341 | 342 | #ifndef DOMOTICZ_SUPPORT 343 | #define DOMOTICZ_SUPPORT 1 // Build with domoticz support 344 | #endif 345 | 346 | #define DOMOTICZ_ENABLED 0 // Disable domoticz by default 347 | #define DOMOTICZ_IN_TOPIC "domoticz/in" // Default subscription topic 348 | #define DOMOTICZ_OUT_TOPIC "domoticz/out" // Default publication topic 349 | 350 | // ----------------------------------------------------------------------------- 351 | // NTP 352 | // ----------------------------------------------------------------------------- 353 | 354 | #ifndef NTP_SUPPORT 355 | #define NTP_SUPPORT 1 // Build with NTP support by default 356 | #endif 357 | 358 | #define NTP_SERVER "pool.ntp.org" // Default NTP server 359 | #define NTP_TIME_OFFSET 1 // Default timezone offset (GMT+1) 360 | #define NTP_DAY_LIGHT true // Enable daylight time saving by default 361 | #define NTP_UPDATE_INTERVAL 1800 // NTP check every 30 minutes 362 | 363 | // ----------------------------------------------------------------------------- 364 | // COMMS 365 | // ----------------------------------------------------------------------------- 366 | 367 | #define COMMS_DEFAULT_TRIES 1 368 | 369 | // ----------------------------------------------------------------------------- 370 | // NOTIFICATIONS 371 | // ----------------------------------------------------------------------------- 372 | 373 | #define LOCAL_NOTIFICATION 0 374 | #define NOTIFICATION_EFFECT 12 375 | #define NOTIFICATION_SPEED 128 376 | #define NOTIFICATION_COLOR 255UL // Blue 377 | #define NOTIFICATION_BRIGHTNESS 255 378 | #define NOTIFICATION_TIME 30 // Seconds 379 | 380 | // ----------------------------------------------------------------------------- 381 | // SENSORS 382 | // ----------------------------------------------------------------------------- 383 | 384 | #define SENSOR_EVERY 60 385 | #define SENSOR_CLAP_ENABLED 1 386 | 387 | #define SENSOR_TEMPERATURE_MIN -10 388 | #define SENSOR_TEMPERATURE_MAX 50 389 | #define SENSOR_PRESSURE_MIN 870 390 | #define SENSOR_PRESSURE_MAX 1100 391 | #define SENSOR_DUST_MIN 0 392 | #define SENSOR_DUST_MAX 1 393 | #define SENSOR_HUMIDITY_MIN 10 394 | #define SENSOR_HUMIDITY_MAX 100 395 | 396 | #define FAN_DELAY 0 397 | 398 | // ----------------------------------------------------------------------------- 399 | // LED 400 | // ----------------------------------------------------------------------------- 401 | 402 | #define LED_PIN 13 403 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/config/prototypes.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | typedef std::function apiGetCallbackFunction; 8 | typedef std::function apiPutCallbackFunction; 9 | void apiRegister(const char * url, const char * key, apiGetCallbackFunction getFn, apiPutCallbackFunction putFn = NULL); 10 | 11 | void mqttRegister(void (*callback)(unsigned int, const char *, const char *)); 12 | String mqttSubtopic(char * topic); 13 | 14 | template bool setSetting(const String& key, T value); 15 | template bool setSetting(const String& key, unsigned int index, T value); 16 | template String getSetting(const String& key, T defaultValue); 17 | template String getSetting(const String& key, unsigned int index, T defaultValue); 18 | template void domoticzSend(const char * key, T value); 19 | template void domoticzSend(const char * key, T nvalue, const char * svalue); 20 | 21 | 22 | char * ltrim(char * s); 23 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/config/version.h: -------------------------------------------------------------------------------- 1 | #define APP_NAME "SONOFFSC" 2 | #define APP_VERSION "1.1.1" 3 | #define APP_AUTHOR "xose.perez@gmail.com" 4 | #define APP_WEBSITE "http://tinkerman.cat" 5 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/data/index.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoseperez/sonoffsc/b8c438245e61029ce07e9a8f18b2618cd97500b5/esp8266/sonoffsc/data/index.html.gz -------------------------------------------------------------------------------- /esp8266/sonoffsc/debug.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | DEBUG MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if DEBUG_SUPPORT 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | extern "C" { 16 | #include "user_interface.h" 17 | } 18 | 19 | #if DEBUG_UDP_SUPPORT 20 | #include 21 | WiFiUDP udpDebug; 22 | #endif 23 | 24 | void debugSend(const char * format, ...) { 25 | 26 | char buffer[DEBUG_MESSAGE_MAX_LENGTH+1]; 27 | 28 | va_list args; 29 | va_start(args, format); 30 | int len = ets_vsnprintf(buffer, DEBUG_MESSAGE_MAX_LENGTH, format, args); 31 | va_end(args); 32 | 33 | #if DEBUG_SERIAL_SUPPORT 34 | DEBUG_PORT.printf(buffer); 35 | if (len > DEBUG_MESSAGE_MAX_LENGTH) { 36 | DEBUG_PORT.printf(" (...)\n"); 37 | } 38 | #endif 39 | 40 | #if DEBUG_UDP_SUPPORT 41 | if (systemCheck()) { 42 | udpDebug.beginPacket(DEBUG_UDP_IP, DEBUG_UDP_PORT); 43 | udpDebug.write(buffer); 44 | if (len > DEBUG_MESSAGE_MAX_LENGTH) { 45 | udpDebug.write(" (...)\n"); 46 | } 47 | udpDebug.endPacket(); 48 | delay(1); 49 | } 50 | #endif 51 | 52 | #if DEBUG_TELNET_SUPPORT 53 | _telnetWrite(buffer, strlen(buffer)); 54 | #endif 55 | 56 | } 57 | 58 | void debugSend_P(PGM_P format, ...) { 59 | 60 | char f[DEBUG_MESSAGE_MAX_LENGTH+1]; 61 | memcpy_P(f, format, DEBUG_MESSAGE_MAX_LENGTH); 62 | 63 | char buffer[DEBUG_MESSAGE_MAX_LENGTH+1]; 64 | 65 | va_list args; 66 | va_start(args, format); 67 | int len = ets_vsnprintf(buffer, DEBUG_MESSAGE_MAX_LENGTH, f, args); 68 | va_end(args); 69 | 70 | #if DEBUG_SERIAL_SUPPORT 71 | DEBUG_PORT.printf(buffer); 72 | if (len > DEBUG_MESSAGE_MAX_LENGTH) { 73 | DEBUG_PORT.printf(" (...)\n"); 74 | } 75 | #endif 76 | 77 | #if DEBUG_UDP_SUPPORT 78 | if (systemCheck()) { 79 | udpDebug.beginPacket(DEBUG_UDP_IP, DEBUG_UDP_PORT); 80 | udpDebug.write(buffer); 81 | if (len > DEBUG_MESSAGE_MAX_LENGTH) { 82 | udpDebug.write(" (...)\n"); 83 | } 84 | udpDebug.endPacket(); 85 | delay(1); 86 | } 87 | #endif 88 | 89 | #if DEBUG_TELNET_SUPPORT 90 | _telnetWrite(buffer, strlen(buffer)); 91 | #endif 92 | 93 | } 94 | 95 | // ----------------------------------------------------------------------------- 96 | // Save crash info 97 | // Taken from krzychb EspSaveCrash 98 | // https://github.com/krzychb/EspSaveCrash 99 | // ----------------------------------------------------------------------------- 100 | 101 | #define SAVE_CRASH_EEPROM_OFFSET 0x0100 // initial address for crash data 102 | 103 | /** 104 | * Structure of the single crash data set 105 | * 106 | * 1. Crash time 107 | * 2. Restart reason 108 | * 3. Exception cause 109 | * 4. epc1 110 | * 5. epc2 111 | * 6. epc3 112 | * 7. excvaddr 113 | * 8. depc 114 | * 9. adress of stack start 115 | * 10. adress of stack end 116 | * 11. stack trace bytes 117 | * ... 118 | */ 119 | #define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes 120 | #define SAVE_CRASH_RESTART_REASON 0x04 // 1 byte 121 | #define SAVE_CRASH_EXCEPTION_CAUSE 0x05 // 1 byte 122 | #define SAVE_CRASH_EPC1 0x06 // 4 bytes 123 | #define SAVE_CRASH_EPC2 0x0A // 4 bytes 124 | #define SAVE_CRASH_EPC3 0x0E // 4 bytes 125 | #define SAVE_CRASH_EXCVADDR 0x12 // 4 bytes 126 | #define SAVE_CRASH_DEPC 0x16 // 4 bytes 127 | #define SAVE_CRASH_STACK_START 0x1A // 4 bytes 128 | #define SAVE_CRASH_STACK_END 0x1E // 4 bytes 129 | #define SAVE_CRASH_STACK_TRACE 0x22 // variable 130 | 131 | /** 132 | * Save crash information in EEPROM 133 | * This function is called automatically if ESP8266 suffers an exception 134 | * It should be kept quick / consise to be able to execute before hardware wdt may kick in 135 | */ 136 | extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end ) { 137 | 138 | // This method assumes EEPROM has already been initialized 139 | // which is the first thing ESPurna does 140 | 141 | // write crash time to EEPROM 142 | uint32_t crash_time = millis(); 143 | EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); 144 | 145 | // write reset info to EEPROM 146 | EEPROM.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason); 147 | EEPROM.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause); 148 | 149 | // write epc1, epc2, epc3, excvaddr and depc to EEPROM 150 | EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1); 151 | EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2); 152 | EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3); 153 | EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr); 154 | EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc); 155 | 156 | // write stack start and end address to EEPROM 157 | EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); 158 | EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); 159 | 160 | // write stack trace to EEPROM 161 | int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; 162 | for (uint32_t i = stack_start; i < stack_end; i++) { 163 | byte* byteValue = (byte*) i; 164 | EEPROM.write(current_address++, *byteValue); 165 | } 166 | 167 | EEPROM.commit(); 168 | 169 | } 170 | 171 | /** 172 | * Clears crash info 173 | */ 174 | void debugClearCrashInfo() { 175 | uint32_t crash_time = 0xFFFFFFFF; 176 | EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); 177 | EEPROM.commit(); 178 | } 179 | 180 | /** 181 | * Print out crash information that has been previusly saved in EEPROM 182 | */ 183 | void debugDumpCrashInfo() { 184 | 185 | uint32_t crash_time; 186 | EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); 187 | if ((crash_time == 0) || (crash_time == 0xFFFFFFFF)) { 188 | DEBUG_MSG_P(PSTR("[DEBUG] No crash info\n")); 189 | return; 190 | } 191 | 192 | DEBUG_MSG_P(PSTR("[DEBUG] Crash at %ld ms\n"), crash_time); 193 | DEBUG_MSG_P(PSTR("[DEBUG] Reason of restart: %d\n"), EEPROM.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON)); 194 | DEBUG_MSG_P(PSTR("[DEBUG] Exception cause: %d\n"), EEPROM.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE)); 195 | 196 | uint32_t epc1, epc2, epc3, excvaddr, depc; 197 | EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1); 198 | EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2); 199 | EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3); 200 | EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr); 201 | EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc); 202 | DEBUG_MSG_P(PSTR("[DEBUG] epc1=0x%08x epc2=0x%08x epc3=0x%08x\n"), epc1, epc2, epc3); 203 | DEBUG_MSG_P(PSTR("[DEBUG] excvaddr=0x%08x depc=0x%08x\n"), excvaddr, depc); 204 | 205 | uint32_t stack_start, stack_end; 206 | EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); 207 | EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); 208 | DEBUG_MSG_P(PSTR("[DEBUG] >>>stack>>>\n[DEBUG] ")); 209 | int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; 210 | int16_t stack_len = stack_end - stack_start; 211 | uint32_t stack_trace; 212 | for (int16_t i = 0; i < stack_len; i += 0x10) { 213 | DEBUG_MSG_P(PSTR("%08x: "), stack_start + i); 214 | for (byte j = 0; j < 4; j++) { 215 | EEPROM.get(current_address, stack_trace); 216 | DEBUG_MSG_P(PSTR("%08x "), stack_trace); 217 | current_address += 4; 218 | } 219 | DEBUG_MSG_P(PSTR("\n[DEBUG] ")); 220 | } 221 | DEBUG_MSG_P(PSTR("<< 6 | 7 | */ 8 | 9 | #if DOMOTICZ_SUPPORT 10 | 11 | #include 12 | 13 | bool _dcz_enabled = false; 14 | 15 | //------------------------------------------------------------------------------ 16 | // Private methods 17 | //------------------------------------------------------------------------------ 18 | 19 | //------------------------------------------------------------------------------ 20 | // Public API 21 | //------------------------------------------------------------------------------ 22 | 23 | template void domoticzSend(const char * key, T nvalue, const char * svalue) { 24 | if (!_dcz_enabled) return; 25 | unsigned int idx = getSetting(key).toInt(); 26 | if (idx > 0) { 27 | char payload[128]; 28 | snprintf(payload, sizeof(payload), "{\"idx\": %d, \"nvalue\": %s, \"svalue\": \"%s\"}", idx, String(nvalue).c_str(), svalue); 29 | mqttSendRaw(getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC).c_str(), payload); 30 | } 31 | } 32 | 33 | template void domoticzSend(const char * key, T nvalue) { 34 | domoticzSend(key, nvalue, ""); 35 | } 36 | 37 | void domoticzConfigure() { 38 | _dcz_enabled = getSetting("dczEnabled", DOMOTICZ_ENABLED).toInt() == 1; 39 | } 40 | 41 | void domoticzSetup() { 42 | domoticzConfigure(); 43 | } 44 | 45 | bool domoticzEnabled() { 46 | return _dcz_enabled; 47 | } 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/lights.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | LIGHTS MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include 10 | 11 | Ticker _lights_defer; 12 | unsigned char _channels[3] = {0}; 13 | 14 | // ----------------------------------------------------------------------------- 15 | // LIGHTS 16 | // ----------------------------------------------------------------------------- 17 | 18 | void _fromRGB(const char * rgb) { 19 | 20 | char * p = (char *) rgb; 21 | if (strlen(p) == 0) return; 22 | 23 | // if color begins with a # then assume HEX RGB 24 | if (p[0] == '#') { 25 | 26 | ++p; 27 | unsigned long value = strtoul(p, NULL, 16); 28 | _channels[0] = (value >> 16) & 0xFF; 29 | _channels[1] = (value >> 8) & 0xFF; 30 | _channels[2] = (value) & 0xFF; 31 | 32 | // it's a temperature in mireds 33 | } else if (p[0] == 'M') { 34 | 35 | unsigned long mireds = atol(p + 1); 36 | _fromMireds(mireds); 37 | 38 | // it's a temperature in kelvin 39 | } else if (p[0] == 'K') { 40 | 41 | unsigned long kelvin = atol(p + 1); 42 | _fromKelvin(kelvin); 43 | 44 | // otherwise assume decimal values separated by commas 45 | } else { 46 | 47 | char * tok; 48 | unsigned char count = 0; 49 | 50 | tok = strtok(p, ","); 51 | while (tok != NULL) { 52 | _channels[count] = atoi(tok); 53 | if (++count == 3) break; 54 | tok = strtok(NULL, ","); 55 | } 56 | 57 | // RGB but less than 3 values received 58 | if (count < 3) { 59 | _channels[1] = _channels[0]; 60 | _channels[2] = _channels[0]; 61 | } 62 | 63 | } 64 | 65 | } 66 | 67 | // Thanks to Sacha Telgenhof for sharing this code in his AiLight library 68 | // https://github.com/stelgenhof/AiLight 69 | void _fromKelvin(unsigned long kelvin) { 70 | 71 | // Calculate colors 72 | unsigned int red = (kelvin <= 66) 73 | ? 255 74 | : 329.698727446 * pow((kelvin - 60), -0.1332047592); 75 | unsigned int green = (kelvin <= 66) 76 | ? 99.4708025861 * log(kelvin) - 161.1195681661 77 | : 288.1221695283 * pow(kelvin, -0.0755148492); 78 | unsigned int blue = (kelvin >= 66) 79 | ? 255 80 | : ((kelvin <= 19) 81 | ? 0 82 | : 138.5177312231 * log(kelvin - 10) - 305.0447927307); 83 | 84 | // Save values 85 | _channels[0] = constrain(red, 0, 255); 86 | _channels[1] = constrain(green, 0, 255); 87 | _channels[2] = constrain(blue, 0, 255); 88 | 89 | } 90 | 91 | // Color temperature is measured in mireds (kelvin = 1e6/mired) 92 | void _fromMireds(unsigned long mireds) { 93 | if (mireds == 0) mireds = 1; 94 | unsigned long kelvin = constrain(1000000UL / mireds, 1000, 40000) / 100; 95 | _fromKelvin(kelvin); 96 | } 97 | 98 | void _lightsMqttCallback(unsigned int type, const char * topic, const char * payload) { 99 | 100 | // When connected, subscribe to the topic 101 | if (type == MQTT_CONNECT_EVENT) { 102 | mqttSubscribe(MQTT_TOPIC_RGB); 103 | mqttSubscribe(MQTT_TOPIC_EFFECT); 104 | mqttSubscribe(MQTT_TOPIC_BRIGHTNESS); 105 | mqttSubscribe(MQTT_TOPIC_SPEED); 106 | } 107 | 108 | // Messages 109 | if (type == MQTT_MESSAGE_EVENT) { 110 | 111 | String t = mqttSubtopic((char*) topic); 112 | 113 | if (t.equals(MQTT_TOPIC_RGB)) { 114 | sendColor((char*) payload); 115 | } 116 | 117 | if (t.equals(MQTT_TOPIC_BRIGHTNESS)) { 118 | unsigned char brightness = constrain(atoi(payload), 0, 255); 119 | sendBrightness(brightness); 120 | } 121 | 122 | if (t.equals(MQTT_TOPIC_SPEED)) { 123 | unsigned char speed = constrain(atoi(payload), 0, 255); 124 | sendSpeed(speed); 125 | } 126 | 127 | if (t.equals(MQTT_TOPIC_EFFECT)) { 128 | unsigned char effect = constrain(atoi(payload), 0, 53); 129 | sendEffect(effect); 130 | } 131 | 132 | } 133 | 134 | } 135 | 136 | //------------------------------------------------------------------------------ 137 | 138 | void sendEffect(long effect) { 139 | DEBUG_MSG_P(PSTR("[LIGHTS] Effect to %d\n"), effect); 140 | send_P_repeat(at_effect, effect); 141 | } 142 | 143 | void sendColor(unsigned long color) { 144 | DEBUG_MSG_P(PSTR("[LIGHTS] Color to %lu\n"), color); 145 | send_P_repeat(at_color, color); 146 | } 147 | 148 | void sendBrightness(unsigned char brightness) { 149 | DEBUG_MSG_P(PSTR("[LIGHTS] Brightness to %d\n"), brightness); 150 | send_P_repeat(at_bright, brightness); 151 | } 152 | 153 | void sendColor(const char * rgb) { 154 | _fromRGB(rgb); 155 | unsigned long color = _channels[0] << 16 | _channels[1] << 8 | _channels[2]; 156 | sendColor(color); 157 | } 158 | 159 | void sendSpeed(unsigned char speed) { 160 | DEBUG_MSG_P(PSTR("[LIGHTS] Speed to %d\n"), speed); 161 | send_P_repeat(at_speed, speed); 162 | } 163 | 164 | void sendNotification(bool state, unsigned long time) { 165 | 166 | if (state) { 167 | sendBrightness(NOTIFICATION_BRIGHTNESS); 168 | sendEffect(NOTIFICATION_EFFECT); 169 | sendSpeed(NOTIFICATION_SPEED); 170 | sendColor(NOTIFICATION_COLOR); 171 | } else { 172 | sendColor(0UL); 173 | } 174 | 175 | if (time > 0) _lights_defer.once(time, sendNotification, !state); 176 | 177 | } 178 | 179 | void sendNotification(bool state) { 180 | sendNotification(state, 0); 181 | } 182 | 183 | void lightsSetup() { 184 | mqttRegister(_lightsMqttCallback); 185 | send_P_repeat(at_timeout, 0); 186 | } 187 | 188 | void lightsLoop() { 189 | } 190 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/mqtt.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | MQTT MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #if MQTT_USE_ASYNC // Using AsyncMqttClient 16 | 17 | #include 18 | AsyncMqttClient _mqtt; 19 | 20 | #else // Using PubSubClient 21 | 22 | #include 23 | PubSubClient _mqtt; 24 | bool _mqtt_connected = false; 25 | 26 | WiFiClient _mqtt_client; 27 | #if ASYNC_TCP_SSL_ENABLED 28 | WiFiClientSecure _mqtt_client_secure; 29 | #endif // ASYNC_TCP_SSL_ENABLED 30 | 31 | #endif // MQTT_USE_ASYNC 32 | 33 | bool _mqtt_enabled = MQTT_ENABLED; 34 | bool _mqtt_use_json = false; 35 | unsigned long _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; 36 | String _mqtt_topic; 37 | String _mqtt_setter; 38 | String _mqtt_getter; 39 | bool _mqtt_forward; 40 | char *_mqtt_user = 0; 41 | char *_mqtt_pass = 0; 42 | char *_mqtt_will; 43 | #if MQTT_SKIP_RETAINED 44 | unsigned long _mqtt_connected_at = 0; 45 | #endif 46 | 47 | std::vector _mqtt_callbacks; 48 | 49 | typedef struct { 50 | char * topic; 51 | char * message; 52 | } mqtt_message_t; 53 | std::vector _mqtt_queue; 54 | Ticker _mqtt_flush_ticker; 55 | 56 | // ----------------------------------------------------------------------------- 57 | // Public API 58 | // ----------------------------------------------------------------------------- 59 | 60 | bool mqttConnected() { 61 | return _mqtt.connected(); 62 | } 63 | 64 | void mqttDisconnect() { 65 | if (_mqtt.connected()) { 66 | DEBUG_MSG_P("[MQTT] Disconnecting\n"); 67 | _mqtt.disconnect(); 68 | } 69 | } 70 | 71 | bool mqttForward() { 72 | return _mqtt_forward; 73 | } 74 | 75 | String mqttSubtopic(char * topic) { 76 | String response; 77 | String t = String(topic); 78 | if (t.startsWith(_mqtt_topic) && t.endsWith(_mqtt_setter)) { 79 | response = t.substring(_mqtt_topic.length(), t.length() - _mqtt_setter.length()); 80 | } 81 | return response; 82 | } 83 | 84 | void mqttSendRaw(const char * topic, const char * message) { 85 | if (_mqtt.connected()) { 86 | #if MQTT_USE_ASYNC 87 | unsigned int packetId = _mqtt.publish(topic, MQTT_QOS, MQTT_RETAIN, message); 88 | DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s (PID %d)\n"), topic, message, packetId); 89 | #else 90 | _mqtt.publish(topic, message, MQTT_RETAIN); 91 | DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s\n"), topic, message); 92 | #endif 93 | } 94 | } 95 | 96 | String getTopic(const char * topic, bool set) { 97 | String output = _mqtt_topic + String(topic); 98 | if (set) output += _mqtt_setter; 99 | return output; 100 | } 101 | 102 | String getTopic(const char * topic, unsigned int index, bool set) { 103 | char buffer[strlen(topic)+5]; 104 | snprintf_P(buffer, sizeof(buffer), PSTR("%s/%d"), topic, index); 105 | return getTopic(buffer, set); 106 | } 107 | 108 | void _mqttFlush() { 109 | 110 | if (_mqtt_queue.size() == 0) return; 111 | 112 | DynamicJsonBuffer jsonBuffer; 113 | JsonObject& root = jsonBuffer.createObject(); 114 | for (unsigned char i=0; i<_mqtt_queue.size(); i++) { 115 | mqtt_message_t element = _mqtt_queue[i]; 116 | root[element.topic] = element.message; 117 | } 118 | #if NTP_SUPPORT 119 | if (ntpConnected()) root[MQTT_TOPIC_TIME] = ntpDateTime(); 120 | #endif 121 | root[MQTT_TOPIC_HOSTNAME] = getSetting("hostname"); 122 | root[MQTT_TOPIC_IP] = getIP(); 123 | 124 | String output; 125 | root.printTo(output); 126 | String path = _mqtt_topic + String(MQTT_TOPIC_JSON); 127 | mqttSendRaw(path.c_str(), output.c_str()); 128 | 129 | for (unsigned char i = 0; i < _mqtt_queue.size(); i++) { 130 | mqtt_message_t element = _mqtt_queue[i]; 131 | free(element.topic); 132 | free(element.message); 133 | } 134 | _mqtt_queue.clear(); 135 | 136 | } 137 | 138 | void mqttSend(const char * topic, const char * message, bool force) { 139 | bool useJson = force ? false : _mqtt_use_json; 140 | if (useJson) { 141 | mqtt_message_t element; 142 | element.topic = strdup(topic); 143 | element.message = strdup(message); 144 | _mqtt_queue.push_back(element); 145 | _mqtt_flush_ticker.once_ms(MQTT_USE_JSON_DELAY, _mqttFlush); 146 | } else { 147 | String path = _mqtt_topic + String(topic) + _mqtt_getter; 148 | mqttSendRaw(path.c_str(), message); 149 | } 150 | } 151 | 152 | void mqttSend(const char * topic, const char * message) { 153 | mqttSend(topic, message, false); 154 | } 155 | 156 | void mqttSend(const char * topic, unsigned int index, const char * message, bool force) { 157 | char buffer[strlen(topic)+5]; 158 | snprintf_P(buffer, sizeof(buffer), PSTR("%s/%d"), topic, index); 159 | mqttSend(buffer, message, force); 160 | } 161 | 162 | void mqttSend(const char * topic, unsigned int index, const char * message) { 163 | mqttSend(topic, index, message, false); 164 | } 165 | 166 | void mqttSubscribeRaw(const char * topic) { 167 | if (_mqtt.connected() && (strlen(topic) > 0)) { 168 | #if MQTT_USE_ASYNC 169 | unsigned int packetId = _mqtt.subscribe(topic, MQTT_QOS); 170 | DEBUG_MSG_P(PSTR("[MQTT] Subscribing to %s (PID %d)\n"), topic, packetId); 171 | #else 172 | _mqtt.subscribe(topic, MQTT_QOS); 173 | DEBUG_MSG_P(PSTR("[MQTT] Subscribing to %s\n"), topic); 174 | #endif 175 | } 176 | } 177 | 178 | void mqttSubscribe(const char * topic) { 179 | String path = _mqtt_topic + String(topic) + _mqtt_setter; 180 | mqttSubscribeRaw(path.c_str()); 181 | } 182 | 183 | void mqttRegister(void (*callback)(unsigned int, const char *, const char *)) { 184 | _mqtt_callbacks.push_back(callback); 185 | } 186 | 187 | // ----------------------------------------------------------------------------- 188 | // Callbacks 189 | // ----------------------------------------------------------------------------- 190 | 191 | void _mqttCallback(unsigned int type, const char * topic, const char * payload) { 192 | 193 | 194 | if (type == MQTT_CONNECT_EVENT) { 195 | 196 | mqttSubscribe(MQTT_TOPIC_ACTION); 197 | 198 | } 199 | 200 | if (type == MQTT_MESSAGE_EVENT) { 201 | 202 | // Match topic 203 | String t = mqttSubtopic((char *) topic); 204 | 205 | // Actions 206 | if (t.equals(MQTT_TOPIC_ACTION)) { 207 | if (strcmp(payload, MQTT_ACTION_RESET) == 0) { 208 | customReset(CUSTOM_RESET_MQTT); 209 | ESP.restart(); 210 | } 211 | } 212 | 213 | } 214 | 215 | } 216 | 217 | void _mqttOnConnect() { 218 | 219 | DEBUG_MSG_P(PSTR("[MQTT] Connected!\n")); 220 | _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; 221 | 222 | #if MQTT_SKIP_RETAINED 223 | _mqtt_connected_at = millis(); 224 | #endif 225 | 226 | // Send first Heartbeat 227 | heartbeat(); 228 | 229 | // Send connect event to subscribers 230 | for (unsigned char i = 0; i < _mqtt_callbacks.size(); i++) { 231 | (*_mqtt_callbacks[i])(MQTT_CONNECT_EVENT, NULL, NULL); 232 | } 233 | 234 | } 235 | 236 | void _mqttOnDisconnect() { 237 | 238 | DEBUG_MSG_P(PSTR("[MQTT] Disconnected!\n")); 239 | 240 | // Send disconnect event to subscribers 241 | for (unsigned char i = 0; i < _mqtt_callbacks.size(); i++) { 242 | (*_mqtt_callbacks[i])(MQTT_DISCONNECT_EVENT, NULL, NULL); 243 | } 244 | 245 | } 246 | 247 | void _mqttOnMessage(char* topic, char* payload, unsigned int len) { 248 | 249 | if (len == 0) return; 250 | 251 | char message[len + 1]; 252 | strlcpy(message, (char *) payload, len + 1); 253 | 254 | #if MQTT_SKIP_RETAINED 255 | if (millis() - _mqtt_connected_at < MQTT_SKIP_TIME) { 256 | DEBUG_MSG_P(PSTR("[MQTT] Received %s => %s - SKIPPED\n"), topic, message); 257 | return; 258 | } 259 | #endif 260 | DEBUG_MSG_P(PSTR("[MQTT] Received %s => %s\n"), topic, message); 261 | 262 | // Send message event to subscribers 263 | for (unsigned char i = 0; i < _mqtt_callbacks.size(); i++) { 264 | (*_mqtt_callbacks[i])(MQTT_MESSAGE_EVENT, topic, message); 265 | } 266 | 267 | } 268 | 269 | #if MQTT_USE_ASYNC 270 | 271 | bool mqttFormatFP(const char * fingerprint, unsigned char * bytearray) { 272 | 273 | // check length (20 2-character digits ':' or ' ' separated => 20*2+19 = 59) 274 | if (strlen(fingerprint) != 59) return false; 275 | 276 | DEBUG_MSG_P(PSTR("[MQTT] Fingerprint %s\n"), fingerprint); 277 | 278 | // walk the fingerprint 279 | for (unsigned int i=0; i<20; i++) { 280 | bytearray[i] = strtol(fingerprint + 3*i, NULL, 16); 281 | } 282 | 283 | return true; 284 | 285 | } 286 | 287 | #else 288 | 289 | bool mqttFormatFP(const char * fingerprint, char * destination) { 290 | 291 | // check length (20 2-character digits ':' or ' ' separated => 20*2+19 = 59) 292 | if (strlen(fingerprint) != 59) return false; 293 | 294 | DEBUG_MSG_P(PSTR("[MQTT] Fingerprint %s\n"), fingerprint); 295 | 296 | // copy it 297 | strncpy(destination, fingerprint, 59); 298 | 299 | // walk the fingerprint replacing ':' for ' ' 300 | for (unsigned char i = 0; i<59; i++) { 301 | if (destination[i] == ':') destination[i] = ' '; 302 | } 303 | 304 | return true; 305 | 306 | } 307 | 308 | #endif 309 | 310 | void mqttEnabled(bool status) { 311 | _mqtt_enabled = status; 312 | setSetting("mqttEnabled", status ? 1 : 0); 313 | } 314 | 315 | bool mqttEnabled() { 316 | return _mqtt_enabled; 317 | } 318 | 319 | void mqttConnect() { 320 | 321 | // Do not connect if disabled 322 | if (!_mqtt_enabled) return; 323 | 324 | // Do not connect if already connected 325 | if (_mqtt.connected()) return; 326 | 327 | // Check reconnect interval 328 | static unsigned long last = 0; 329 | if (millis() - last < _mqtt_reconnect_delay) return; 330 | last = millis(); 331 | 332 | // Increase the reconnect delay 333 | _mqtt_reconnect_delay += MQTT_RECONNECT_DELAY_STEP; 334 | if (_mqtt_reconnect_delay > MQTT_RECONNECT_DELAY_MAX) { 335 | _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX; 336 | } 337 | 338 | char * host = strdup(getSetting("mqttServer", MQTT_SERVER).c_str()); 339 | if (strlen(host) == 0) return; 340 | unsigned int port = getSetting("mqttPort", MQTT_PORT).toInt(); 341 | 342 | if (_mqtt_user) free(_mqtt_user); 343 | if (_mqtt_pass) free(_mqtt_pass); 344 | if (_mqtt_will) free(_mqtt_will); 345 | 346 | _mqtt_user = strdup(getSetting("mqttUser", MQTT_USER).c_str()); 347 | _mqtt_pass = strdup(getSetting("mqttPassword", MQTT_PASS).c_str()); 348 | _mqtt_will = strdup((_mqtt_topic + MQTT_TOPIC_STATUS).c_str()); 349 | 350 | DEBUG_MSG_P(PSTR("[MQTT] Connecting to broker at %s:%d\n"), host, port); 351 | 352 | #if MQTT_USE_ASYNC 353 | 354 | _mqtt.setServer(host, port); 355 | _mqtt.setKeepAlive(MQTT_KEEPALIVE).setCleanSession(false); 356 | _mqtt.setWill(_mqtt_will, MQTT_QOS, MQTT_RETAIN, "0"); 357 | if ((strlen(_mqtt_user) > 0) && (strlen(_mqtt_pass) > 0)) { 358 | DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_user); 359 | _mqtt.setCredentials(_mqtt_user, _mqtt_pass); 360 | } 361 | 362 | #if ASYNC_TCP_SSL_ENABLED 363 | 364 | bool secure = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1; 365 | _mqtt.setSecure(secure); 366 | if (secure) { 367 | DEBUG_MSG_P(PSTR("[MQTT] Using SSL\n")); 368 | unsigned char fp[20] = {0}; 369 | if (mqttFormatFP(getSetting("mqttFP", MQTT_SSL_FINGERPRINT).c_str(), fp)) { 370 | _mqtt.addServerFingerprint(fp); 371 | } else { 372 | DEBUG_MSG_P(PSTR("[MQTT] Wrong fingerprint\n")); 373 | } 374 | } 375 | 376 | #endif // ASYNC_TCP_SSL_ENABLED 377 | 378 | DEBUG_MSG_P(PSTR("[MQTT] Will topic: %s\n"), _mqtt_will); 379 | DEBUG_MSG_P(PSTR("[MQTT] QoS: %d\n"), MQTT_QOS); 380 | DEBUG_MSG_P(PSTR("[MQTT] Retain flag: %d\n"), MQTT_RETAIN); 381 | 382 | _mqtt.connect(); 383 | 384 | #else // not MQTT_USE_ASYNC 385 | 386 | bool response = true; 387 | 388 | #if ASYNC_TCP_SSL_ENABLED 389 | 390 | bool secure = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1; 391 | if (secure) { 392 | DEBUG_MSG_P(PSTR("[MQTT] Using SSL\n")); 393 | if (_mqtt_client_secure.connect(host, port)) { 394 | char fp[60] = {0}; 395 | if (mqttFormatFP(getSetting("mqttFP", MQTT_SSL_FINGERPRINT).c_str(), fp)) { 396 | if (_mqtt_client_secure.verify(fp, host)) { 397 | _mqtt.setClient(_mqtt_client_secure); 398 | } else { 399 | DEBUG_MSG_P(PSTR("[MQTT] Invalid fingerprint\n")); 400 | response = false; 401 | } 402 | _mqtt_client_secure.stop(); 403 | yield(); 404 | } else { 405 | DEBUG_MSG_P(PSTR("[MQTT] Wrong fingerprint\n")); 406 | response = false; 407 | } 408 | } else { 409 | DEBUG_MSG_P(PSTR("[MQTT] Client connection failed\n")); 410 | response = false; 411 | } 412 | 413 | } else { 414 | _mqtt.setClient(_mqtt_client); 415 | } 416 | 417 | #else // not ASYNC_TCP_SSL_ENABLED 418 | 419 | _mqtt.setClient(_mqtt_client); 420 | 421 | #endif // ASYNC_TCP_SSL_ENABLED 422 | 423 | if (response) { 424 | 425 | _mqtt.setServer(host, port); 426 | 427 | if ((strlen(_mqtt_user) > 0) && (strlen(_mqtt_pass) > 0)) { 428 | DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_user); 429 | response = _mqtt.connect(getIdentifier().c_str(), _mqtt_user, _mqtt_pass, _mqtt_will, MQTT_QOS, MQTT_RETAIN, "0"); 430 | } else { 431 | response = _mqtt.connect(getIdentifier().c_str(), _mqtt_will, MQTT_QOS, MQTT_RETAIN, "0"); 432 | } 433 | 434 | DEBUG_MSG_P(PSTR("[MQTT] Will topic: %s\n"), _mqtt_will); 435 | DEBUG_MSG_P(PSTR("[MQTT] QoS: %d\n"), MQTT_QOS); 436 | DEBUG_MSG_P(PSTR("[MQTT] Retain flag: %d\n"), MQTT_RETAIN); 437 | 438 | } 439 | 440 | if (response) { 441 | _mqttOnConnect(); 442 | } else { 443 | DEBUG_MSG_P(PSTR("[MQTT] Connection failed\n")); 444 | } 445 | 446 | #endif // MQTT_USE_ASYNC 447 | 448 | free(host); 449 | 450 | } 451 | 452 | void mqttConfigure() { 453 | 454 | // Replace identifier 455 | _mqtt_topic = getSetting("mqttTopic", MQTT_TOPIC); 456 | _mqtt_topic.replace("{identifier}", getSetting("hostname")); 457 | if (!_mqtt_topic.endsWith("/")) _mqtt_topic = _mqtt_topic + "/"; 458 | 459 | // Getters and setters 460 | _mqtt_setter = getSetting("mqttSetter", MQTT_USE_SETTER); 461 | _mqtt_getter = getSetting("mqttGetter", MQTT_USE_GETTER); 462 | _mqtt_forward = !_mqtt_getter.equals(_mqtt_setter); 463 | 464 | // Enable 465 | if (getSetting("mqttServer", MQTT_SERVER).length() == 0) { 466 | mqttEnabled(false); 467 | } else { 468 | _mqtt_enabled = getSetting("mqttEnabled", MQTT_ENABLED).toInt() == 1; 469 | } 470 | _mqtt_use_json = (getSetting("mqttUseJson", MQTT_USE_JSON).toInt() == 1); 471 | 472 | _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; 473 | 474 | } 475 | 476 | #if MDNS_SUPPORT 477 | boolean mqttDiscover() { 478 | 479 | int count = MDNS.queryService("mqtt", "tcp"); 480 | DEBUG_MSG_P("[MQTT] MQTT brokers found: %d\n", count); 481 | 482 | for (int i=0; i 6 | 7 | */ 8 | 9 | #if NOFUSS_SUPPORT 10 | 11 | #include "NoFUSSClient.h" 12 | 13 | unsigned long _nofussLastCheck = 0; 14 | unsigned long _nofussInterval = 0; 15 | bool _nofussEnabled = false; 16 | 17 | // ----------------------------------------------------------------------------- 18 | // NOFUSS 19 | // ----------------------------------------------------------------------------- 20 | 21 | void nofussRun() { 22 | NoFUSSClient.handle(); 23 | _nofussLastCheck = millis(); 24 | } 25 | 26 | void nofussConfigure() { 27 | 28 | String nofussServer = getSetting("nofussServer", NOFUSS_SERVER); 29 | if (nofussServer.length() == 0) { 30 | setSetting("nofussEnabled", 0); 31 | _nofussEnabled = false; 32 | } else { 33 | _nofussEnabled = getSetting("nofussEnabled", NOFUSS_ENABLED).toInt() == 1; 34 | } 35 | _nofussInterval = getSetting("nofussInterval", NOFUSS_INTERVAL).toInt(); 36 | _nofussLastCheck = 0; 37 | 38 | if (!_nofussEnabled) { 39 | 40 | DEBUG_MSG_P(PSTR("[NOFUSS] Disabled\n")); 41 | 42 | } else { 43 | 44 | char buffer[20]; 45 | snprintf_P(buffer, sizeof(buffer), PSTR("%s-%s"), APP_NAME, DEVICE); 46 | 47 | NoFUSSClient.setServer(nofussServer); 48 | NoFUSSClient.setDevice(buffer); 49 | NoFUSSClient.setVersion(APP_VERSION); 50 | 51 | DEBUG_MSG_P(PSTR("[NOFUSS] Server : %s\n"), nofussServer.c_str()); 52 | DEBUG_MSG_P(PSTR("[NOFUSS] Dervice: %s\n"), buffer); 53 | DEBUG_MSG_P(PSTR("[NOFUSS] Version: %s\n"), APP_VERSION); 54 | DEBUG_MSG_P(PSTR("[NOFUSS] Enabled\n")); 55 | 56 | } 57 | 58 | } 59 | 60 | void nofussSetup() { 61 | 62 | nofussConfigure(); 63 | 64 | NoFUSSClient.onMessage([](nofuss_t code) { 65 | 66 | if (code == NOFUSS_START) { 67 | DEBUG_MSG_P(PSTR("[NoFUSS] Start\n")); 68 | } 69 | 70 | if (code == NOFUSS_UPTODATE) { 71 | DEBUG_MSG_P(PSTR("[NoFUSS] Already in the last version\n")); 72 | } 73 | 74 | if (code == NOFUSS_NO_RESPONSE_ERROR) { 75 | DEBUG_MSG_P(PSTR("[NoFUSS] Wrong server response: %d %s\n"), NoFUSSClient.getErrorNumber(), (char *) NoFUSSClient.getErrorString().c_str()); 76 | } 77 | 78 | if (code == NOFUSS_PARSE_ERROR) { 79 | DEBUG_MSG_P(PSTR("[NoFUSS] Error parsing server response\n")); 80 | } 81 | 82 | if (code == NOFUSS_UPDATING) { 83 | DEBUG_MSG_P(PSTR("[NoFUSS] Updating\n")); 84 | DEBUG_MSG_P(PSTR(" New version: %s\n"), (char *) NoFUSSClient.getNewVersion().c_str()); 85 | DEBUG_MSG_P(PSTR(" Firmware: %s\n"), (char *) NoFUSSClient.getNewFirmware().c_str()); 86 | DEBUG_MSG_P(PSTR(" File System: %s\n"), (char *) NoFUSSClient.getNewFileSystem().c_str()); 87 | #if WEB_SUPPORT 88 | wsSend_P(PSTR("{\"message\": 1}")); 89 | #endif 90 | } 91 | 92 | if (code == NOFUSS_FILESYSTEM_UPDATE_ERROR) { 93 | DEBUG_MSG_P(PSTR("[NoFUSS] File System Update Error: %s\n"), (char *) NoFUSSClient.getErrorString().c_str()); 94 | } 95 | 96 | if (code == NOFUSS_FILESYSTEM_UPDATED) { 97 | DEBUG_MSG_P(PSTR("[NoFUSS] File System Updated\n")); 98 | } 99 | 100 | if (code == NOFUSS_FIRMWARE_UPDATE_ERROR) { 101 | DEBUG_MSG_P(PSTR("[NoFUSS] Firmware Update Error: %s\n"), (char *) NoFUSSClient.getErrorString().c_str()); 102 | } 103 | 104 | if (code == NOFUSS_FIRMWARE_UPDATED) { 105 | DEBUG_MSG_P(PSTR("[NoFUSS] Firmware Updated\n")); 106 | } 107 | 108 | if (code == NOFUSS_RESET) { 109 | DEBUG_MSG_P(PSTR("[NoFUSS] Resetting board\n")); 110 | #if WEB_SUPPORT 111 | wsSend_P(PSTR("{\"action\": \"reload\"}")); 112 | #endif 113 | delay(100); 114 | } 115 | 116 | if (code == NOFUSS_END) { 117 | DEBUG_MSG_P(PSTR("[NoFUSS] End\n")); 118 | } 119 | 120 | }); 121 | 122 | } 123 | 124 | void nofussLoop() { 125 | 126 | if (!_nofussEnabled) return; 127 | if (!wifiConnected()) return; 128 | if ((_nofussLastCheck > 0) && ((millis() - _nofussLastCheck) < _nofussInterval)) return; 129 | 130 | nofussRun(); 131 | 132 | } 133 | 134 | #endif // NOFUSS_SUPPORT 135 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/ntp.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | NTP MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if NTP_SUPPORT 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | // ----------------------------------------------------------------------------- 16 | // NTP 17 | // ----------------------------------------------------------------------------- 18 | 19 | void ntpConnect() { 20 | NTP.begin( 21 | getSetting("ntpServer1", NTP_SERVER), 22 | getSetting("ntpOffset", NTP_TIME_OFFSET).toInt(), 23 | getSetting("ntpDST", NTP_DAY_LIGHT).toInt() == 1 24 | ); 25 | if (getSetting("ntpServer2")) NTP.setNtpServerName(getSetting("ntpServer2"), 1); 26 | if (getSetting("ntpServer3")) NTP.setNtpServerName(getSetting("ntpServer3"), 2); 27 | NTP.setInterval(NTP_UPDATE_INTERVAL); 28 | } 29 | 30 | bool ntpConnected() { 31 | return (timeStatus() == timeSet); 32 | } 33 | 34 | String ntpDateTime() { 35 | if (!ntpConnected()) return String("Not set"); 36 | String value = NTP.getTimeDateString(); 37 | int hour = value.substring(0, 2).toInt(); 38 | int minute = value.substring(3, 5).toInt(); 39 | int second = value.substring(6, 8).toInt(); 40 | int day = value.substring(9, 11).toInt(); 41 | int month = value.substring(12, 14).toInt(); 42 | int year = value.substring(15, 19).toInt(); 43 | char buffer[20]; 44 | snprintf_P(buffer, sizeof(buffer), PSTR("%04d/%02d/%02d %02d:%02d:%02d"), year, month, day, hour, minute, second); 45 | return String(buffer); 46 | } 47 | 48 | void ntpSetup() { 49 | NTP.onNTPSyncEvent([](NTPSyncEvent_t error) { 50 | if (error) { 51 | if (error == noResponse) { 52 | DEBUG_MSG_P(PSTR("[NTP] Error: NTP server not reachable\n")); 53 | } else if (error == invalidAddress) { 54 | DEBUG_MSG_P(PSTR("[NTP] Error: Invalid NTP server address\n")); 55 | } 56 | #if WEB_SUPPORT 57 | wsSend_P(PSTR("{\"ntpStatus\": false}")); 58 | #endif 59 | } else { 60 | DEBUG_MSG_P(PSTR("[NTP] Time: %s\n"), (char *) ntpDateTime().c_str()); 61 | #if WEB_SUPPORT 62 | wsSend_P(PSTR("{\"ntpStatus\": true}")); 63 | #endif 64 | } 65 | }); 66 | } 67 | 68 | void ntpLoop() { 69 | now(); 70 | } 71 | 72 | #endif // NTP_SUPPORT 73 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/ota.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | OTA MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include "ArduinoOTA.h" 10 | #include 11 | 12 | // ----------------------------------------------------------------------------- 13 | // OTA 14 | // ----------------------------------------------------------------------------- 15 | 16 | void otaConfigure() { 17 | ArduinoOTA.setPort(OTA_PORT); 18 | ArduinoOTA.setHostname(getSetting("hostname").c_str()); 19 | ArduinoOTA.setPassword(getSetting("adminPass", ADMIN_PASS).c_str()); 20 | } 21 | 22 | void otaSetup() { 23 | 24 | otaConfigure(); 25 | 26 | ArduinoOTA.onStart([]() { 27 | DEBUG_MSG_P(PSTR("[OTA] Start\n")); 28 | wsSend("{\"message\": \"OTA update started\"}"); 29 | }); 30 | 31 | ArduinoOTA.onEnd([]() { 32 | customReset(CUSTOM_RESET_OTA); 33 | DEBUG_MSG_P(PSTR("\n[OTA] End\n")); 34 | wsSend("{\"action\": \"reload\"}"); 35 | delay(100); 36 | }); 37 | 38 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 39 | DEBUG_MSG_P(PSTR("[OTA] Progress: %u%%%%\r"), (progress / (total / 100))); 40 | }); 41 | 42 | ArduinoOTA.onError([](ota_error_t error) { 43 | #if DEBUG_PORT 44 | DEBUG_MSG_P(PSTR("\n[OTA] Error #%u: "), error); 45 | if (error == OTA_AUTH_ERROR) DEBUG_MSG_P(PSTR("Auth Failed\n")); 46 | else if (error == OTA_BEGIN_ERROR) DEBUG_MSG_P(PSTR("Begin Failed\n")); 47 | else if (error == OTA_CONNECT_ERROR) DEBUG_MSG_P(PSTR("Connect Failed\n")); 48 | else if (error == OTA_RECEIVE_ERROR) DEBUG_MSG_P(PSTR("Receive Failed\n")); 49 | else if (error == OTA_END_ERROR) DEBUG_MSG_P(PSTR("End Failed\n")); 50 | #endif 51 | }); 52 | 53 | ArduinoOTA.begin(); 54 | 55 | // Public ESPurna related txt for OTA discovery 56 | MDNS.addServiceTxt("arduino", "tcp", "app_name", APP_NAME); 57 | MDNS.addServiceTxt("arduino", "tcp", "app_version", APP_VERSION); 58 | MDNS.addServiceTxt("arduino", "tcp", "target_board", DEVICE_NAME); 59 | 60 | } 61 | 62 | void otaLoop() { 63 | ArduinoOTA.handle(); 64 | } 65 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/settings.h: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Stream Injector 3 | // ----------------------------------------------------------------------------- 4 | 5 | #pragma once 6 | 7 | #define STREAM_INJECTOR_BUFFER_SIZE 32 8 | 9 | class StreamInjector : public Stream { 10 | 11 | public: 12 | 13 | typedef std::function writeCallback; 14 | 15 | StreamInjector(Stream& serial) : _stream(serial) {} 16 | 17 | virtual void callback(writeCallback c) { 18 | _callback = c; 19 | } 20 | 21 | virtual size_t write(uint8_t ch) { 22 | if (_callback) _callback(ch); 23 | return _stream.write(ch); 24 | } 25 | 26 | virtual int read() { 27 | int ch = _stream.read(); 28 | if (ch == -1) { 29 | if (_buffer_read != _buffer_write) { 30 | ch = _buffer[_buffer_read]; 31 | _buffer_read = (_buffer_read + 1) % STREAM_INJECTOR_BUFFER_SIZE; 32 | } 33 | } 34 | return ch; 35 | } 36 | 37 | virtual int available() { 38 | unsigned int bytes = _stream.available(); 39 | if (_buffer_read > _buffer_write) { 40 | bytes += (_buffer_write - _buffer_read + STREAM_INJECTOR_BUFFER_SIZE); 41 | } else if (_buffer_read < _buffer_write) { 42 | bytes += (_buffer_write - _buffer_read); 43 | } 44 | return bytes; 45 | } 46 | 47 | virtual int peek() { 48 | int ch = _stream.peek(); 49 | if (ch == -1) { 50 | if (_buffer_read != _buffer_write) { 51 | ch = _buffer[_buffer_read]; 52 | } 53 | } 54 | return ch; 55 | } 56 | 57 | virtual void flush() { 58 | _stream.flush(); 59 | _buffer_read = _buffer_write; 60 | } 61 | 62 | virtual void inject(char *data, size_t len) { 63 | for (int i=0; i 6 | 7 | */ 8 | 9 | #include "Embedis.h" 10 | #include 11 | #include "spi_flash.h" 12 | #include 13 | 14 | #if TELNET_SUPPORT 15 | #include "settings.h" 16 | #ifdef DEBUG_PORT 17 | StreamInjector _serial = StreamInjector(DEBUG_PORT); 18 | #else 19 | StreamInjector _serial = StreamInjector(Serial); 20 | #endif 21 | Embedis embedis(_serial); 22 | #else 23 | #ifdef DEBUG_PORT 24 | Embedis embedis(DEBUG_PORT); 25 | #else 26 | Embedis embedis(_serial); 27 | #endif 28 | #endif 29 | 30 | bool _settings_save = false; 31 | 32 | // ----------------------------------------------------------------------------- 33 | // Settings 34 | // ----------------------------------------------------------------------------- 35 | 36 | #if TELNET_SUPPORT 37 | void settingsInject(void *data, size_t len) { 38 | _serial.inject((char *) data, len); 39 | } 40 | #endif 41 | 42 | size_t settingsMaxSize() { 43 | size_t size = EEPROM_SIZE; 44 | if (size > SPI_FLASH_SEC_SIZE) size = SPI_FLASH_SEC_SIZE; 45 | size = (size + 3) & (~3); 46 | return size; 47 | } 48 | 49 | unsigned long settingsSize() { 50 | unsigned pos = SPI_FLASH_SEC_SIZE - 1; 51 | while (size_t len = EEPROM.read(pos)) { 52 | pos = pos - len - 2; 53 | } 54 | return SPI_FLASH_SEC_SIZE - pos; 55 | } 56 | 57 | unsigned int settingsKeyCount() { 58 | unsigned count = 0; 59 | unsigned pos = SPI_FLASH_SEC_SIZE - 1; 60 | while (size_t len = EEPROM.read(pos)) { 61 | pos = pos - len - 2; 62 | count ++; 63 | } 64 | return count / 2; 65 | } 66 | 67 | String settingsKeyName(unsigned int index) { 68 | 69 | String s; 70 | 71 | unsigned count = 0; 72 | unsigned stop = index * 2 + 1; 73 | unsigned pos = SPI_FLASH_SEC_SIZE - 1; 74 | while (size_t len = EEPROM.read(pos)) { 75 | pos = pos - len - 2; 76 | count++; 77 | if (count == stop) { 78 | s.reserve(len); 79 | for (unsigned char i = 0 ; i < len; i++) { 80 | s += (char) EEPROM.read(pos + i + 1); 81 | } 82 | break; 83 | } 84 | } 85 | 86 | return s; 87 | 88 | } 89 | 90 | void settingsFactoryReset() { 91 | for (unsigned int i = 0; i < SPI_FLASH_SEC_SIZE; i++) { 92 | EEPROM.write(i, 0xFF); 93 | } 94 | EEPROM.commit(); 95 | } 96 | 97 | void settingsSetup() { 98 | 99 | EEPROM.begin(SPI_FLASH_SEC_SIZE); 100 | 101 | #if TELNET_SUPPORT 102 | _serial.callback([](uint8_t ch) { 103 | telnetWrite(ch); 104 | }); 105 | #endif 106 | 107 | Embedis::dictionary( F("EEPROM"), 108 | SPI_FLASH_SEC_SIZE, 109 | [](size_t pos) -> char { return EEPROM.read(pos); }, 110 | [](size_t pos, char value) { EEPROM.write(pos, value); }, 111 | #if SETTINGS_AUTOSAVE 112 | []() { _settings_save = true; } 113 | #else 114 | []() {} 115 | #endif 116 | ); 117 | 118 | Embedis::hardware( F("WIFI"), [](Embedis* e) { 119 | StreamString s; 120 | WiFi.printDiag(s); 121 | e->response(s); 122 | }, 0); 123 | 124 | // ------------------------------------------------------------------------- 125 | 126 | Embedis::command( F("RESET.WIFI"), [](Embedis* e) { 127 | wifiConfigure(); 128 | wifiDisconnect(); 129 | e->response(Embedis::OK); 130 | }); 131 | 132 | Embedis::command( F("RESET.MQTT"), [](Embedis* e) { 133 | mqttConfigure(); 134 | mqttDisconnect(); 135 | e->response(Embedis::OK); 136 | }); 137 | 138 | Embedis::command( F("INFO"), [](Embedis* e) { 139 | welcome(); 140 | e->response(Embedis::OK); 141 | }); 142 | 143 | Embedis::command( F("UPTIME"), [](Embedis* e) { 144 | e->stream->printf("Uptime: %d seconds\n", getUptime()); 145 | e->response(Embedis::OK); 146 | }); 147 | 148 | Embedis::command( F("RESET"), [](Embedis* e) { 149 | e->response(Embedis::OK); 150 | customReset(CUSTOM_RESET_TERMINAL); 151 | ESP.restart(); 152 | }); 153 | 154 | Embedis::command( F("ERASE.CONFIG"), [](Embedis* e) { 155 | e->response(Embedis::OK); 156 | customReset(CUSTOM_RESET_TERMINAL); 157 | ESP.eraseConfig(); 158 | *((int*) 0) = 0; // see https://github.com/esp8266/Arduino/issues/1494 159 | }); 160 | 161 | #if NOFUSS_SUPPORT 162 | Embedis::command( F("NOFUSS"), [](Embedis* e) { 163 | e->response(Embedis::OK); 164 | nofussRun(); 165 | }); 166 | #endif 167 | 168 | Embedis::command( F("FACTORY.RESET"), [](Embedis* e) { 169 | settingsFactoryReset(); 170 | e->response(Embedis::OK); 171 | }); 172 | 173 | Embedis::command( F("HEAP"), [](Embedis* e) { 174 | e->stream->printf("Free HEAP: %d bytes\n", ESP.getFreeHeap()); 175 | e->response(Embedis::OK); 176 | }); 177 | 178 | Embedis::command( F("EEPROM"), [](Embedis* e) { 179 | unsigned long freeEEPROM = SPI_FLASH_SEC_SIZE - settingsSize(); 180 | e->stream->printf("Number of keys: %d\n", settingsKeyCount()); 181 | e->stream->printf("Free EEPROM: %d bytes (%d%%)\n", freeEEPROM, 100 * freeEEPROM / SPI_FLASH_SEC_SIZE); 182 | e->response(Embedis::OK); 183 | }); 184 | 185 | Embedis::command( F("DUMP"), [](Embedis* e) { 186 | unsigned int size = settingsKeyCount(); 187 | for (unsigned int i=0; istream->printf("+%s => %s\n", key.c_str(), value.c_str()); 191 | } 192 | e->response(Embedis::OK); 193 | }); 194 | 195 | #if DEBUG_SUPPORT 196 | Embedis::command( F("CRASH"), [](Embedis* e) { 197 | debugDumpCrashInfo(); 198 | e->response(Embedis::OK); 199 | }); 200 | #endif 201 | 202 | Embedis::command( F("DUMP.RAW"), [](Embedis* e) { 203 | for (unsigned int i = 0; i < SPI_FLASH_SEC_SIZE; i++) { 204 | if (i % 16 == 0) e->stream->printf("\n[%04X] ", i); 205 | e->stream->printf("%02X ", EEPROM.read(i)); 206 | } 207 | e->stream->printf("\n"); 208 | e->response(Embedis::OK); 209 | }); 210 | 211 | DEBUG_MSG_P(PSTR("[SETTINGS] EEPROM size: %d bytes\n"), SPI_FLASH_SEC_SIZE); 212 | DEBUG_MSG_P(PSTR("[SETTINGS] Settings size: %d bytes\n"), settingsSize()); 213 | 214 | } 215 | 216 | void settingsDump() { 217 | unsigned int size = settingsKeyCount(); 218 | for (unsigned int i=0; i %s\n"), key.c_str(), value.c_str()); 222 | } 223 | } 224 | 225 | void settingsLoop() { 226 | if (_settings_save) { 227 | //DEBUG_MSG_P(PSTR("[SETTINGS] Saving\n")); 228 | EEPROM.commit(); 229 | _settings_save = false; 230 | } 231 | #if TERMINAL_SUPPORT 232 | embedis.process(); 233 | #endif 234 | } 235 | 236 | void moveSetting(const char * from, const char * to) { 237 | String value = getSetting(from); 238 | if (value.length() > 0) setSetting(to, value); 239 | delSetting(from); 240 | } 241 | 242 | template String getSetting(const String& key, T defaultValue) { 243 | String value; 244 | if (!Embedis::get(key, value)) value = String(defaultValue); 245 | return value; 246 | } 247 | 248 | template String getSetting(const String& key, unsigned int index, T defaultValue) { 249 | return getSetting(key + String(index), defaultValue); 250 | } 251 | 252 | String getSetting(const String& key) { 253 | return getSetting(key, ""); 254 | } 255 | 256 | template bool setSetting(const String& key, T value) { 257 | return Embedis::set(key, String(value)); 258 | } 259 | 260 | template bool setSetting(const String& key, unsigned int index, T value) { 261 | return setSetting(key + String(index), value); 262 | } 263 | 264 | bool delSetting(const String& key) { 265 | return Embedis::del(key); 266 | } 267 | 268 | bool delSetting(const String& key, unsigned int index) { 269 | return delSetting(key + String(index)); 270 | } 271 | 272 | bool hasSetting(const String& key) { 273 | return getSetting(key).length() != 0; 274 | } 275 | 276 | bool hasSetting(const String& key, unsigned int index) { 277 | return getSetting(key, index, "").length() != 0; 278 | } 279 | 280 | void saveSettings() { 281 | #if not SETTINGS_AUTOSAVE 282 | _settings_save = true; 283 | #endif 284 | //settingsDump(); 285 | } 286 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/sonoffsc.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Sonoff SC 4 | Copyright (C) 2017 by Xose Pérez 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | */ 20 | 21 | #include 22 | #include 23 | #include "config/all.h" 24 | #include "spi_flash.h" 25 | 26 | // ----------------------------------------------------------------------------- 27 | // METHODS 28 | // ----------------------------------------------------------------------------- 29 | 30 | bool ledStatus(bool status) { 31 | digitalWrite(LED_PIN, status ? LOW : HIGH); 32 | return status; 33 | } 34 | 35 | bool ledStatus() { 36 | return (digitalRead(LED_PIN) == LOW); 37 | } 38 | 39 | bool ledToggle() { 40 | return ledStatus(!ledStatus()); 41 | } 42 | 43 | void ledBlink(unsigned long delayOff, unsigned long delayOn) { 44 | static unsigned long next = millis(); 45 | if (next < millis()) { 46 | next += (ledToggle() ? delayOn : delayOff); 47 | } 48 | } 49 | 50 | void showStatus() { 51 | if (wifiConnected()) { 52 | if (WiFi.getMode() == WIFI_AP) { 53 | ledBlink(2000, 2000); 54 | } else { 55 | ledBlink(5000, 500); 56 | } 57 | } else { 58 | ledBlink(500, 500); 59 | } 60 | } 61 | 62 | void hardwareSetup() { 63 | 64 | EEPROM.begin(EEPROM_SIZE); 65 | 66 | #if DEBUG_SERIAL_SUPPORT 67 | DEBUG_PORT.begin(SERIAL_BAUDRATE); 68 | #if DEBUG_ESP_WIFI 69 | DEBUG_PORT.setDebugOutput(true); 70 | #endif 71 | #elif defined(SERIAL_BAUDRATE) 72 | Serial.begin(SERIAL_BAUDRATE); 73 | #endif 74 | 75 | #if SPIFFS_SUPPORT 76 | SPIFFS.begin(); 77 | #endif 78 | 79 | pinMode(LED_PIN, OUTPUT); 80 | 81 | } 82 | 83 | void hardwareLoop() { 84 | 85 | showStatus(); 86 | 87 | // System check 88 | static bool checked = false; 89 | if (!checked && (millis() > CRASH_SAFE_TIME)) { 90 | // Check system as stable 91 | systemCheck(true); 92 | checked = true; 93 | } 94 | 95 | // Heartbeat 96 | static unsigned long last_heartbeat = 0; 97 | if (mqttConnected()) { 98 | if ((millis() - last_heartbeat > HEARTBEAT_INTERVAL) || (last_heartbeat == 0)) { 99 | last_heartbeat = millis(); 100 | heartbeat(); 101 | } 102 | } 103 | 104 | } 105 | 106 | // ----------------------------------------------------------------------------- 107 | // BOOTING 108 | // ----------------------------------------------------------------------------- 109 | 110 | unsigned int sectors(size_t size) { 111 | return (int) (size + SPI_FLASH_SEC_SIZE - 1) / SPI_FLASH_SEC_SIZE; 112 | } 113 | 114 | void welcome() { 115 | 116 | DEBUG_MSG_P(PSTR("\n\n")); 117 | DEBUG_MSG_P(PSTR("[INIT] %s %s\n"), (char *) APP_NAME, (char *) APP_VERSION); 118 | DEBUG_MSG_P(PSTR("[INIT] %s\n"), (char *) APP_AUTHOR); 119 | DEBUG_MSG_P(PSTR("[INIT] %s\n\n"), (char *) APP_WEBSITE); 120 | DEBUG_MSG_P(PSTR("[INIT] CPU chip ID: 0x%06X\n"), ESP.getChipId()); 121 | DEBUG_MSG_P(PSTR("[INIT] CPU frequency: %d MHz\n"), ESP.getCpuFreqMHz()); 122 | DEBUG_MSG_P(PSTR("[INIT] SDK version: %s\n"), ESP.getSdkVersion()); 123 | DEBUG_MSG_P(PSTR("[INIT] Core version: %s\n"), ESP.getCoreVersion().c_str()); 124 | DEBUG_MSG_P(PSTR("\n")); 125 | 126 | // ------------------------------------------------------------------------- 127 | 128 | FlashMode_t mode = ESP.getFlashChipMode(); 129 | DEBUG_MSG_P(PSTR("[INIT] Flash chip ID: 0x%06X\n"), ESP.getFlashChipId()); 130 | DEBUG_MSG_P(PSTR("[INIT] Flash speed: %u Hz\n"), ESP.getFlashChipSpeed()); 131 | DEBUG_MSG_P(PSTR("[INIT] Flash mode: %s\n"), mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN"); 132 | DEBUG_MSG_P(PSTR("\n")); 133 | DEBUG_MSG_P(PSTR("[INIT] Flash sector size: %8u bytes\n"), SPI_FLASH_SEC_SIZE); 134 | DEBUG_MSG_P(PSTR("[INIT] Flash size (CHIP): %8u bytes\n"), ESP.getFlashChipRealSize()); 135 | DEBUG_MSG_P(PSTR("[INIT] Flash size (SDK): %8u bytes / %4d sectors\n"), ESP.getFlashChipSize(), sectors(ESP.getFlashChipSize())); 136 | DEBUG_MSG_P(PSTR("[INIT] Firmware size: %8u bytes / %4d sectors\n"), ESP.getSketchSize(), sectors(ESP.getSketchSize())); 137 | DEBUG_MSG_P(PSTR("[INIT] OTA size: %8u bytes / %4d sectors\n"), ESP.getFreeSketchSpace(), sectors(ESP.getFreeSketchSpace())); 138 | #if SPIFFS_SUPPORT 139 | FSInfo fs_info; 140 | bool fs = SPIFFS.info(fs_info); 141 | if (fs) { 142 | DEBUG_MSG_P(PSTR("[INIT] SPIFFS size: %8u bytes / %4d sectors\n"), fs_info.totalBytes, sectors(fs_info.totalBytes)); 143 | } 144 | #else 145 | DEBUG_MSG_P(PSTR("[INIT] SPIFFS size: %8u bytes / %4d sectors\n"), 0, 0); 146 | #endif 147 | DEBUG_MSG_P(PSTR("[INIT] EEPROM size: %8u bytes / %4d sectors\n"), settingsMaxSize(), sectors(settingsMaxSize())); 148 | DEBUG_MSG_P(PSTR("[INIT] Empty space: %8u bytes / 4 sectors\n"), 4 * SPI_FLASH_SEC_SIZE); 149 | DEBUG_MSG_P(PSTR("\n")); 150 | 151 | // ------------------------------------------------------------------------- 152 | 153 | #if SPIFFS_SUPPORT 154 | if (fs) { 155 | DEBUG_MSG_P(PSTR("[INIT] SPIFFS total size: %8u bytes\n"), fs_info.totalBytes); 156 | DEBUG_MSG_P(PSTR("[INIT] used size: %8u bytes\n"), fs_info.usedBytes); 157 | DEBUG_MSG_P(PSTR("[INIT] block size: %8u bytes\n"), fs_info.blockSize); 158 | DEBUG_MSG_P(PSTR("[INIT] page size: %8u bytes\n"), fs_info.pageSize); 159 | DEBUG_MSG_P(PSTR("[INIT] max files: %8u\n"), fs_info.maxOpenFiles); 160 | DEBUG_MSG_P(PSTR("[INIT] max length: %8u\n"), fs_info.maxPathLength); 161 | } else { 162 | DEBUG_MSG_P(PSTR("[INIT] No SPIFFS partition\n")); 163 | } 164 | DEBUG_MSG_P(PSTR("\n")); 165 | #endif 166 | 167 | // ------------------------------------------------------------------------- 168 | 169 | DEBUG_MSG_P(PSTR("[INIT] MANUFACTURER: %s\n"), MANUFACTURER); 170 | DEBUG_MSG_P(PSTR("[INIT] DEVICE: %s\n"), DEVICE_NAME); 171 | DEBUG_MSG_P(PSTR("[INIT] SUPPORT:")); 172 | 173 | #if ALEXA_SUPPORT 174 | DEBUG_MSG_P(PSTR(" ALEXA")); 175 | #endif 176 | #if DEBUG_SERIAL_SUPPORT 177 | DEBUG_MSG_P(PSTR(" DEBUG_SERIAL")); 178 | #endif 179 | #if DEBUG_UDP_SUPPORT 180 | DEBUG_MSG_P(PSTR(" DEBUG_UDP")); 181 | #endif 182 | #if DOMOTICZ_SUPPORT 183 | DEBUG_MSG_P(PSTR(" DOMOTICZ")); 184 | #endif 185 | #if MDNS_SUPPORT 186 | DEBUG_MSG_P(PSTR(" MDNS")); 187 | #endif 188 | #if NOFUSS_SUPPORT 189 | DEBUG_MSG_P(PSTR(" NOFUSS")); 190 | #endif 191 | #if NTP_SUPPORT 192 | DEBUG_MSG_P(PSTR(" NTP")); 193 | #endif 194 | #if SPIFFS_SUPPORT 195 | DEBUG_MSG_P(PSTR(" SPIFFS")); 196 | #endif 197 | #if TERMINAL_SUPPORT 198 | DEBUG_MSG_P(PSTR(" TERMINAL")); 199 | #endif 200 | 201 | DEBUG_MSG_P(PSTR("\n\n")); 202 | 203 | // ------------------------------------------------------------------------- 204 | 205 | unsigned char custom_reset = customReset(); 206 | if (custom_reset > 0) { 207 | char buffer[32]; 208 | strcpy_P(buffer, custom_reset_string[custom_reset-1]); 209 | DEBUG_MSG_P(PSTR("[INIT] Last reset reason: %s\n"), buffer); 210 | } else { 211 | DEBUG_MSG_P(PSTR("[INIT] Last reset reason: %s\n"), (char *) ESP.getResetReason().c_str()); 212 | } 213 | 214 | DEBUG_MSG_P(PSTR("[INIT] Free heap: %u bytes\n"), ESP.getFreeHeap()); 215 | #if ADC_VCC_ENABLED 216 | DEBUG_MSG_P(PSTR("[INIT] Power: %d mV\n"), ESP.getVcc()); 217 | #endif 218 | DEBUG_MSG_P(PSTR("\n")); 219 | 220 | } 221 | 222 | void setup() { 223 | 224 | // Wait for the ATMEGA to be ready 225 | delay(2000); 226 | 227 | // Init EEPROM, Serial and SPIFFS 228 | hardwareSetup(); 229 | 230 | // Question system stability 231 | systemCheck(false); 232 | 233 | // Show welcome message and system configuration 234 | welcome(); 235 | 236 | // Init persistance and terminal features 237 | settingsSetup(); 238 | if (getSetting("hostname").length() == 0) { 239 | setSetting("hostname", getIdentifier()); 240 | } 241 | 242 | wifiSetup(); 243 | otaSetup(); 244 | #if TELNET_SUPPORT 245 | telnetSetup(); 246 | #endif 247 | 248 | // Do not run the next services if system is flagged stable 249 | if (!systemCheck()) return; 250 | 251 | buttonSetup(); 252 | mqttSetup(); 253 | webSetup(); 254 | commsSetup(); 255 | lightsSetup(); 256 | 257 | #if NTP_SUPPORT 258 | ntpSetup(); 259 | #endif 260 | #if ALEXA_SUPPORT 261 | alexaSetup(); 262 | #endif 263 | #if NOFUSS_SUPPORT 264 | nofussSetup(); 265 | #endif 266 | #if DOMOTICZ_SUPPORT 267 | domoticzSetup(); 268 | #endif 269 | 270 | saveSettings(); 271 | 272 | sendNotification(true, NOTIFICATION_TIME); 273 | 274 | } 275 | 276 | void loop() { 277 | 278 | hardwareLoop(); 279 | settingsLoop(); 280 | wifiLoop(); 281 | otaLoop(); 282 | 283 | // Do not run the next services if system is flagged stable 284 | if (!systemCheck()) return; 285 | 286 | buttonLoop(); 287 | mqttLoop(); 288 | commsLoop(); 289 | 290 | #if NTP_SUPPORT 291 | ntpLoop(); 292 | #endif 293 | #if ALEXA_SUPPORT 294 | alexaLoop(); 295 | #endif 296 | #if NOFUSS_SUPPORT 297 | nofussLoop(); 298 | #endif 299 | 300 | } 301 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/telnet.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | TELNET MODULE 4 | 5 | Copyright (C) 2017 by Xose Pérez 6 | Parts of the code have been borrowed from Thomas Sarlandie's NetServer 7 | (https://github.com/sarfata/kbox-firmware/tree/master/src/esp) 8 | 9 | */ 10 | 11 | #if TELNET_SUPPORT 12 | 13 | #include 14 | 15 | AsyncServer * _telnetServer; 16 | AsyncClient * _telnetClients[TELNET_MAX_CLIENTS]; 17 | 18 | // ----------------------------------------------------------------------------- 19 | // Private methods 20 | // ----------------------------------------------------------------------------- 21 | 22 | void _telnetDisconnect(unsigned char clientId) { 23 | _telnetClients[clientId]->free(); 24 | _telnetClients[clientId] = NULL; 25 | delete _telnetClients[clientId]; 26 | DEBUG_MSG_P(PSTR("[TELNET] Client #%d disconnected\n"), clientId); 27 | } 28 | 29 | bool _telnetWrite(unsigned char clientId, void *data, size_t len) { 30 | if (_telnetClients[clientId] && _telnetClients[clientId]->connected()) { 31 | return (_telnetClients[clientId]->write((const char*) data, len) > 0); 32 | } 33 | return false; 34 | } 35 | 36 | unsigned char _telnetWrite(void *data, size_t len) { 37 | unsigned char count = 0; 38 | for (unsigned char i = 0; i < TELNET_MAX_CLIENTS; i++) { 39 | if (_telnetWrite(i, data, len)) ++count; 40 | } 41 | return count; 42 | } 43 | 44 | void _telnetData(unsigned char clientId, void *data, size_t len) { 45 | 46 | // Capture close connection 47 | char * p = (char *) data; 48 | if ((strncmp(p, "close", 5) == 0) || (strncmp(p, "quit", 4) == 0)) { 49 | _telnetClients[clientId]->close(); 50 | return; 51 | } 52 | 53 | // Inject into Embedis stream 54 | settingsInject(data, len); 55 | 56 | } 57 | 58 | void _telnetNewClient(AsyncClient *client) { 59 | 60 | #if TELNET_ONLY_AP 61 | if (client->localIP() != WiFi.softAPIP()) { 62 | DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Only local connections\n")); 63 | client->onDisconnect([](void *s, AsyncClient *c) { 64 | c->free(); 65 | delete c; 66 | }); 67 | client->close(true); 68 | return; 69 | } 70 | #endif 71 | 72 | for (unsigned char i = 0; i < TELNET_MAX_CLIENTS; i++) { 73 | if (!_telnetClients[i] || !_telnetClients[i]->connected()) { 74 | 75 | _telnetClients[i] = client; 76 | 77 | client->onAck([i](void *s, AsyncClient *c, size_t len, uint32_t time) { 78 | }, 0); 79 | 80 | client->onData([i](void *s, AsyncClient *c, void *data, size_t len) { 81 | _telnetData(i, data, len); 82 | }, 0); 83 | 84 | client->onDisconnect([i](void *s, AsyncClient *c) { 85 | _telnetDisconnect(i); 86 | }, 0); 87 | 88 | client->onError([i](void *s, AsyncClient *c, int8_t error) { 89 | DEBUG_MSG_P(PSTR("[TELNET] Error %s (%d) on client #%d\n"), c->errorToString(error), error, i); 90 | }, 0); 91 | 92 | client->onTimeout([i](void *s, AsyncClient *c, uint32_t time) { 93 | DEBUG_MSG_P(PSTR("[TELNET] Timeout on client #%d at %i\n"), i, time); 94 | c->close(); 95 | }, 0); 96 | 97 | DEBUG_MSG_P(PSTR("[TELNET] Client #%d connected\n"), i); 98 | 99 | // send info and crash data 100 | welcome(); 101 | debugDumpCrashInfo(); 102 | debugClearCrashInfo(); 103 | 104 | return; 105 | 106 | } 107 | 108 | } 109 | 110 | DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Too many connections\n")); 111 | client->onDisconnect([](void *s, AsyncClient *c) { 112 | c->free(); 113 | delete c; 114 | }); 115 | client->close(true); 116 | 117 | } 118 | 119 | // ----------------------------------------------------------------------------- 120 | // Public API 121 | // ----------------------------------------------------------------------------- 122 | 123 | unsigned char telnetWrite(unsigned char ch) { 124 | char data[1] = {ch}; 125 | return _telnetWrite(data, 1); 126 | } 127 | 128 | void telnetSetup() { 129 | 130 | _telnetServer = new AsyncServer(TELNET_PORT); 131 | _telnetServer->onClient([](void *s, AsyncClient* c) { 132 | _telnetNewClient(c); 133 | }, 0); 134 | _telnetServer->begin(); 135 | 136 | DEBUG_MSG_P(PSTR("[TELNET] Listening on port %d\n"), TELNET_PORT); 137 | 138 | } 139 | 140 | #endif // TELNET_SUPPORT 141 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/utils.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | UTILS MODULE 4 | 5 | Copyright (C) 2017 by Xose Pérez 6 | 7 | */ 8 | 9 | String getIdentifier() { 10 | char buffer[20]; 11 | snprintf_P(buffer, sizeof(buffer), PSTR("%s_%06X"), DEVICE_NAME, ESP.getChipId()); 12 | return String(buffer); 13 | } 14 | 15 | String buildTime() { 16 | 17 | const char time_now[] = __TIME__; // hh:mm:ss 18 | unsigned int hour = atoi(&time_now[0]); 19 | unsigned int minute = atoi(&time_now[3]); 20 | unsigned int second = atoi(&time_now[6]); 21 | 22 | const char date_now[] = __DATE__; // Mmm dd yyyy 23 | const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; 24 | unsigned int month = 0; 25 | for ( int i = 0; i < 12; i++ ) { 26 | if (strncmp(date_now, months[i], 3) == 0 ) { 27 | month = i + 1; 28 | break; 29 | } 30 | } 31 | unsigned int day = atoi(&date_now[3]); 32 | unsigned int year = atoi(&date_now[7]); 33 | 34 | char buffer[20]; 35 | snprintf_P( 36 | buffer, sizeof(buffer), PSTR("%04d/%02d/%02d %02d:%02d:%02d"), 37 | year, month, day, hour, minute, second 38 | ); 39 | 40 | return String(buffer); 41 | 42 | } 43 | 44 | 45 | unsigned long getUptime() { 46 | 47 | static unsigned long last_uptime = 0; 48 | static unsigned char uptime_overflows = 0; 49 | 50 | if (millis() < last_uptime) ++uptime_overflows; 51 | last_uptime = millis(); 52 | unsigned long uptime_seconds = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); 53 | 54 | return uptime_seconds; 55 | 56 | } 57 | 58 | void heartbeat() { 59 | mqttSend(MQTT_TOPIC_HEARTBEAT, "1"); 60 | DEBUG_MSG("[BEAT] Free heap: %d\n", ESP.getFreeHeap()); 61 | } 62 | 63 | // ----------------------------------------------------------------------------- 64 | 65 | void customReset(unsigned char status) { 66 | EEPROM.write(EEPROM_CUSTOM_RESET, status); 67 | EEPROM.commit(); 68 | } 69 | 70 | unsigned char customReset() { 71 | static unsigned char status = 255; 72 | if (status == 255) { 73 | status = EEPROM.read(EEPROM_CUSTOM_RESET); 74 | if (status > 0) customReset(0); 75 | if (status > CUSTOM_RESET_MAX) status = 0; 76 | } 77 | return status; 78 | } 79 | 80 | // ----------------------------------------------------------------------------- 81 | 82 | // Call this method on boot with start=true to increase the crash counter 83 | // Call it again once the system is stable to decrease the counter 84 | // If the counter reaches CRASH_COUNT_MAX then the system is flagged as unstable 85 | // setting _systemOK = false; 86 | // 87 | // An unstable system will only have serial access, WiFi in AP mode and OTA 88 | 89 | bool _systemStable = true; 90 | 91 | void systemCheck(bool stable) { 92 | unsigned char value = EEPROM.read(EEPROM_CRASH_COUNTER); 93 | if (stable) { 94 | value = 0; 95 | DEBUG_MSG_P(PSTR("[MAIN] System OK\n")); 96 | } else { 97 | if (++value > CRASH_COUNT_MAX) { 98 | _systemStable = false; 99 | value = 0; 100 | DEBUG_MSG_P(PSTR("[MAIN] System UNSTABLE\n")); 101 | } 102 | } 103 | EEPROM.write(EEPROM_CRASH_COUNTER, value); 104 | EEPROM.commit(); 105 | } 106 | 107 | bool systemCheck() { 108 | return _systemStable; 109 | } 110 | 111 | // ----------------------------------------------------------------------------- 112 | 113 | char * ltrim(char * s) { 114 | char *p = s; 115 | while ((unsigned char) *p == ' ') ++p; 116 | return p; 117 | } 118 | 119 | double roundTo(double num, unsigned char positions) { 120 | double multiplier = 1; 121 | while (positions-- > 0) multiplier *= 10; 122 | return round(num * multiplier) / multiplier; 123 | } 124 | -------------------------------------------------------------------------------- /esp8266/sonoffsc/wifi.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | WIFI MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include "JustWifi.h" 10 | #include 11 | 12 | // ----------------------------------------------------------------------------- 13 | // WIFI 14 | // ----------------------------------------------------------------------------- 15 | 16 | String getIP() { 17 | if (WiFi.getMode() == WIFI_AP) { 18 | return WiFi.softAPIP().toString(); 19 | } 20 | return WiFi.localIP().toString(); 21 | } 22 | 23 | String getNetwork() { 24 | if (WiFi.getMode() == WIFI_AP) { 25 | return jw.getAPSSID(); 26 | } 27 | return WiFi.SSID(); 28 | } 29 | 30 | void wifiDisconnect() { 31 | jw.disconnect(); 32 | } 33 | 34 | void resetConnectionTimeout() { 35 | jw.resetReconnectTimeout(); 36 | } 37 | 38 | bool wifiConnected() { 39 | return jw.connected(); 40 | } 41 | 42 | bool createAP() { 43 | jw.disconnect(); 44 | jw.resetReconnectTimeout(); 45 | return jw.createAP(); 46 | } 47 | 48 | void wifiConfigure() { 49 | 50 | jw.setHostname(getSetting("hostname").c_str()); 51 | jw.setSoftAP(getSetting("hostname").c_str(), getSetting("adminPass", ADMIN_PASS).c_str()); 52 | jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); 53 | jw.setReconnectTimeout(WIFI_RECONNECT_INTERVAL); 54 | jw.setAPMode(WIFI_AP_MODE); 55 | jw.cleanNetworks(); 56 | 57 | // If system is flagged unstable we do not init wifi networks 58 | if (!systemCheck()) return; 59 | 60 | int i; 61 | for (i = 0; i< WIFI_MAX_NETWORKS; i++) { 62 | if (getSetting("ssid" + String(i)).length() == 0) break; 63 | if (getSetting("ip" + String(i)).length() == 0) { 64 | jw.addNetwork( 65 | getSetting("ssid" + String(i)).c_str(), 66 | getSetting("pass" + String(i)).c_str() 67 | ); 68 | } else { 69 | jw.addNetwork( 70 | getSetting("ssid" + String(i)).c_str(), 71 | getSetting("pass" + String(i)).c_str(), 72 | getSetting("ip" + String(i)).c_str(), 73 | getSetting("gw" + String(i)).c_str(), 74 | getSetting("mask" + String(i)).c_str(), 75 | getSetting("dns" + String(i)).c_str() 76 | ); 77 | } 78 | } 79 | 80 | // Scan for best network only if we have more than 1 defined 81 | jw.scanNetworks(i>1); 82 | 83 | } 84 | 85 | void wifiStatus() { 86 | 87 | if (WiFi.getMode() == WIFI_AP_STA) { 88 | DEBUG_MSG_P(PSTR("[WIFI] MODE AP + STA --------------------------------\n")); 89 | } else if (WiFi.getMode() == WIFI_AP) { 90 | DEBUG_MSG_P(PSTR("[WIFI] MODE AP --------------------------------------\n")); 91 | } else if (WiFi.getMode() == WIFI_STA) { 92 | DEBUG_MSG_P(PSTR("[WIFI] MODE STA -------------------------------------\n")); 93 | } else { 94 | DEBUG_MSG_P(PSTR("[WIFI] MODE OFF -------------------------------------\n")); 95 | DEBUG_MSG_P(PSTR("[WIFI] No connection\n")); 96 | } 97 | 98 | if ((WiFi.getMode() & WIFI_AP) == WIFI_AP) { 99 | DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), jw.getAPSSID().c_str()); 100 | DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), getSetting("adminPass", ADMIN_PASS).c_str()); 101 | DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.softAPIP().toString().c_str()); 102 | DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.softAPmacAddress().c_str()); 103 | } 104 | 105 | if ((WiFi.getMode() & WIFI_STA) == WIFI_STA) { 106 | DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), WiFi.SSID().c_str()); 107 | DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.localIP().toString().c_str()); 108 | DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.macAddress().c_str()); 109 | DEBUG_MSG_P(PSTR("[WIFI] GW %s\n"), WiFi.gatewayIP().toString().c_str()); 110 | DEBUG_MSG_P(PSTR("[WIFI] DNS %s\n"), WiFi.dnsIP().toString().c_str()); 111 | DEBUG_MSG_P(PSTR("[WIFI] MASK %s\n"), WiFi.subnetMask().toString().c_str()); 112 | DEBUG_MSG_P(PSTR("[WIFI] HOST %s\n"), WiFi.hostname().c_str()); 113 | } 114 | 115 | DEBUG_MSG_P(PSTR("[WIFI] ----------------------------------------------\n")); 116 | 117 | } 118 | 119 | // Inject hardcoded networks 120 | void wifiInject() { 121 | 122 | #ifdef WIFI1_SSID 123 | if (getSetting("ssid", 0, "").length() == 0) setSetting("ssid", 0, WIFI1_SSID); 124 | #endif 125 | #ifdef WIFI1_PASS 126 | if (getSetting("pass", 0, "").length() == 0) setSetting("pass", 0, WIFI1_PASS); 127 | #endif 128 | #ifdef WIFI1_IP 129 | if (getSetting("ip", 0, "").length() == 0) setSetting("ip", 0, WIFI1_IP); 130 | #endif 131 | #ifdef WIFI1_GW 132 | if (getSetting("gw", 0, "").length() == 0) setSetting("gw", 0, WIFI1_GW); 133 | #endif 134 | #ifdef WIFI1_MASK 135 | if (getSetting("mask", 0, "").length() == 0) setSetting("mask", 0, WIFI1_MASK); 136 | #endif 137 | #ifdef WIFI1_DNS 138 | if (getSetting("dns", 0, "").length() == 0) setSetting("dns", 0, WIFI1_DNS); 139 | #endif 140 | 141 | #ifdef WIFI2_SSID 142 | if (getSetting("ssid", 1, "").length() == 0) setSetting("ssid", 1, WIFI2_SSID); 143 | #endif 144 | #ifdef WIFI2_PASS 145 | if (getSetting("pass", 1, "").length() == 0) setSetting("pass", 1, WIFI2_PASS); 146 | #endif 147 | #ifdef WIFI2_IP 148 | if (getSetting("ip", 1, "").length() == 0) setSetting("ip", 1, WIFI2_IP); 149 | #endif 150 | #ifdef WIFI2_GW 151 | if (getSetting("gw", 1, "").length() == 0) setSetting("gw", 1, WIFI2_GW); 152 | #endif 153 | #ifdef WIFI2_MASK 154 | if (getSetting("mask", 1, "").length() == 0) setSetting("mask", 1, WIFI2_MASK); 155 | #endif 156 | #ifdef WIFI2_DNS 157 | if (getSetting("dns", 1, "").length() == 0) setSetting("dns", 1, WIFI2_DNS); 158 | #endif 159 | 160 | } 161 | 162 | void wifiSetup() { 163 | 164 | wifiInject(); 165 | wifiConfigure(); 166 | 167 | // Message callbacks 168 | jw.onMessage([](justwifi_messages_t code, char * parameter) { 169 | 170 | #ifdef DEBUG_PORT 171 | 172 | if (code == MESSAGE_SCANNING) { 173 | DEBUG_MSG_P(PSTR("[WIFI] Scanning\n")); 174 | } 175 | 176 | if (code == MESSAGE_SCAN_FAILED) { 177 | DEBUG_MSG_P(PSTR("[WIFI] Scan failed\n")); 178 | } 179 | 180 | if (code == MESSAGE_NO_NETWORKS) { 181 | DEBUG_MSG_P(PSTR("[WIFI] No networks found\n")); 182 | } 183 | 184 | if (code == MESSAGE_NO_KNOWN_NETWORKS) { 185 | DEBUG_MSG_P(PSTR("[WIFI] No known networks found\n")); 186 | } 187 | 188 | if (code == MESSAGE_FOUND_NETWORK) { 189 | DEBUG_MSG_P(PSTR("[WIFI] %s\n"), parameter); 190 | } 191 | 192 | if (code == MESSAGE_CONNECTING) { 193 | DEBUG_MSG_P(PSTR("[WIFI] Connecting to %s\n"), parameter); 194 | } 195 | 196 | if (code == MESSAGE_CONNECT_WAITING) { 197 | // too much noise 198 | } 199 | 200 | if (code == MESSAGE_CONNECT_FAILED) { 201 | DEBUG_MSG_P(PSTR("[WIFI] Could not connect to %s\n"), parameter); 202 | } 203 | 204 | if (code == MESSAGE_CONNECTED) { 205 | wifiStatus(); 206 | } 207 | 208 | if (code == MESSAGE_ACCESSPOINT_CREATED) { 209 | wifiStatus(); 210 | } 211 | 212 | if (code == MESSAGE_DISCONNECTED) { 213 | DEBUG_MSG_P(PSTR("[WIFI] Disconnected\n")); 214 | } 215 | 216 | if (code == MESSAGE_ACCESSPOINT_CREATING) { 217 | DEBUG_MSG_P(PSTR("[WIFI] Creating access point\n")); 218 | } 219 | 220 | if (code == MESSAGE_ACCESSPOINT_FAILED) { 221 | DEBUG_MSG_P(PSTR("[WIFI] Could not create access point\n")); 222 | } 223 | 224 | #endif 225 | 226 | // Configure communications 227 | if (code == MESSAGE_CONNECTED) { 228 | commsConfigure(); 229 | } 230 | 231 | // Configure mDNS 232 | #if MDNS_SUPPORT 233 | 234 | if (code == MESSAGE_CONNECTED || code == MESSAGE_ACCESSPOINT_CREATED) { 235 | 236 | if (MDNS.begin(WiFi.getMode() == WIFI_AP ? APP_NAME : (char *) WiFi.hostname().c_str())) { 237 | 238 | DEBUG_MSG_P(PSTR("[MDNS] OK\n")); 239 | 240 | MDNS.addService("http", "tcp", getSetting("webPort", WEB_PORT).toInt()); 241 | #if TELNET_SUPPORT 242 | MDNS.addService("telnet", "tcp", TELNET_PORT); 243 | #endif 244 | 245 | if (code == MESSAGE_CONNECTED) mqttDiscover(); 246 | 247 | } else { 248 | 249 | DEBUG_MSG_P(PSTR("[MDNS] FAIL\n")); 250 | 251 | } 252 | 253 | } 254 | 255 | #endif 256 | 257 | // NTP connection reset 258 | #if NTP_SUPPORT 259 | if (code == MESSAGE_CONNECTED) { 260 | ntpConnect(); 261 | } 262 | #endif 263 | 264 | }); 265 | 266 | } 267 | 268 | void wifiLoop() { 269 | jw.loop(); 270 | } 271 | --------------------------------------------------------------------------------