Browse Source

OTA auto update server and other small changes

fastled
Xose Pérez 8 years ago
parent
commit
8d02ecc19c
16 changed files with 1189 additions and 237 deletions
  1. +10
    -2
      README.md
  2. +0
    -2
      code/lib/DebounceEvent/DebounceEvent.cpp
  3. +3
    -3
      code/platformio.ini
  4. +398
    -230
      code/src/main.cpp
  5. +4
    -0
      server/.gitignore
  6. +8
    -0
      server/README.md
  7. +21
    -0
      server/composer.json
  8. +532
    -0
      server/composer.lock
  9. +14
    -0
      server/data/versions.json
  10. +10
    -0
      server/public/.htaccess
  11. +30
    -0
      server/public/index.php
  12. +36
    -0
      server/src/dependencies.php
  13. +4
    -0
      server/src/middleware.php
  14. +36
    -0
      server/src/routes.php
  15. +26
    -0
      server/src/settings.php
  16. +57
    -0
      server/templates/index.phtml

+ 10
- 2
README.md View File

@ -55,8 +55,8 @@ Once you have flashed it you can flash it again over-the-air using the ```ota```
When using OTA environment it defaults to the IP address of the device in SoftAP mode. If you want to flash it when connected to your home network best way is to supply the IP of the device: When using OTA environment it defaults to the IP address of the device in SoftAP mode. If you want to flash it when connected to your home network best way is to supply the IP of the device:
```bash ```bash
> platformio run --target upload -e ota --upload_port 192.168.1.151
> platformio run --target uploadfs -e ota --upload_port 192.168.1.151
> platformio run --target upload -e ota --upload-port 192.168.1.151
> platformio run --target uploadfs -e ota --upload-port 192.168.1.151
``` ```
@ -84,7 +84,15 @@ you will be able to switch it on/off sending "1"/"0" to "/home/living/switch/set
You can also use "{identifier}" as place holder in the topic. It will be translated to You can also use "{identifier}" as place holder in the topic. It will be translated to
your device ID (same as the soft AP network it creates). your device ID (same as the soft AP network it creates).
## Troubleshooting
After flashing the firmware via serial do a hard reset of the device (unplug & plug). There is an issue with the ESP.reset() method. Check [https://github.com/esp8266/Arduino/issues/1017][4] for more info.
Current version of ESP8266httpUpdate restarts the modules after SPIFFS update, thus preventing the firmware to be updated too. There is a recent commit fixing that which is not yet pushed to PLatformIO. Check [Fix example for ESP8266httpUpdate][5] for more info.
[1]: https://www.itead.cc/sonoff-wifi-wireless-switch.html [1]: https://www.itead.cc/sonoff-wifi-wireless-switch.html
[2]: http://tinkerman.cat/adding-rf-to-a-non-rf-itead-sonoff [2]: http://tinkerman.cat/adding-rf-to-a-non-rf-itead-sonoff
[3]: http://www.platformio.org [3]: http://www.platformio.org
[4]: https://github.com/esp8266/Arduino/issues/1017
[5]: https://github.com/esp8266/Arduino/pull/2251

+ 0
- 2
code/lib/DebounceEvent/DebounceEvent.cpp View File

@ -65,7 +65,6 @@ bool DebounceEvent::loop() {
} else if (millis() - _last_start < DOUBLE_CLICK_DELAY ) { } else if (millis() - _last_start < DOUBLE_CLICK_DELAY ) {
_event = EVENT_DOUBLE_CLICK; _event = EVENT_DOUBLE_CLICK;
} else { } else {
Serial.println("deferring");
changed = false; changed = false;
pending = true; pending = true;
//_event = EVENT_SINGLE_CLICK; //_event = EVENT_SINGLE_CLICK;
@ -84,7 +83,6 @@ bool DebounceEvent::loop() {
} }
if (pending && (millis() - _this_start > DOUBLE_CLICK_DELAY) && (!changed) && (_status == _defaultStatus)) { if (pending && (millis() - _this_start > DOUBLE_CLICK_DELAY) && (!changed) && (_status == _defaultStatus)) {
Serial.println("catched");
pending = false; pending = false;
changed = true; changed = true;
_event = EVENT_SINGLE_CLICK; _event = EVENT_SINGLE_CLICK;


+ 3
- 3
code/platformio.ini View File

@ -24,19 +24,19 @@
platform = espressif platform = espressif
framework = arduino framework = arduino
board = esp01_1m board = esp01_1m
lib_install = 89
lib_install = 89,64
[env:node] [env:node]
platform = espressif platform = espressif
framework = arduino framework = arduino
board = nodemcuv2 board = nodemcuv2
lib_install = 89
lib_install = 89,64
[env:ota] [env:ota]
platform = espressif platform = espressif
framework = arduino framework = arduino
board = esp01_1m board = esp01_1m
lib_install = 89
lib_install = 89,64
upload_speed = 115200 upload_speed = 115200
upload_port = "192.168.4.1" upload_port = "192.168.4.1"
upload_flags = --auth=fibonacci --port 8266 upload_flags = --auth=fibonacci --port 8266

code/src/code.ino → code/src/main.cpp View File

@ -21,6 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include <Arduino.h> #include <Arduino.h>
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#include <ESP8266WebServer.h> #include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266mDNS.h> #include <ESP8266mDNS.h>
#include <PubSubClient.h> #include <PubSubClient.h>
#include <DebounceEvent.h> #include <DebounceEvent.h>
@ -30,6 +31,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "FS.h" #include "FS.h"
#include <stdio.h> #include <stdio.h>
#include <EmonLiteESP.h> #include <EmonLiteESP.h>
#include <ArduinoJson.h>
#include <ESP8266httpUpdate.h>
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Configuració // Configuració
@ -39,14 +42,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define ENABLE_RF 1 #define ENABLE_RF 1
#define ENABLE_OTA 1 #define ENABLE_OTA 1
#define ENABLE_OTA_AUTO 0
#define ENABLE_MQTT 1 #define ENABLE_MQTT 1
#define ENABLE_WEBSERVER 1 #define ENABLE_WEBSERVER 1
#define ENABLE_ENERGYMONITOR 1
#define ENABLE_ENERGYMONITOR 0
#define APP_NAME "Espurna 0.9.1"
#define APP_NAME "Espurna"
#define APP_VERSION "0.9.2"
#define APP_AUTHOR "xose.perez@gmail.com" #define APP_AUTHOR "xose.perez@gmail.com"
#define APP_WEBSITE "http://tinkerman.cat" #define APP_WEBSITE "http://tinkerman.cat"
#define OTA_SERVER "http://192.168.1.100"
#define OTA_CHECK_INTERVAL 30000
#define MODEL "SONOFF" #define MODEL "SONOFF"
#define BUTTON_PIN 0 #define BUTTON_PIN 0
#define RELAY_PIN 12 #define RELAY_PIN 12
@ -165,6 +173,31 @@ char * getCompileTime(char * buffer) {
} }
char * getIdentifier() {
if (identifier[0] == 0) {
sprintf(identifier, "%s_%06X", MODEL, ESP.getChipId());
}
return identifier;
}
void blink(unsigned long delayOff, unsigned long delayOn) {
static unsigned long next = millis();
static bool status = HIGH;
if (next < millis()) {
status = !status;
digitalWrite(LED_PIN, status);
next += ((status) ? delayOff : delayOn);
}
}
void showStatus() {
if (WiFi.status() == WL_CONNECTED) {
blink(5000, 500);
} else {
blink(500, 500);
}
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Relay // Relay
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -214,16 +247,298 @@ void toggleRelay() {
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Wifi
// Configuration
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
char * getIdentifier() {
if (identifier[0] == 0) {
sprintf(identifier, "%s_%06X", MODEL, ESP.getChipId());
bool saveConfig() {
File file = SPIFFS.open(CONFIG_PATH, "w");
if (file) {
file.println("ssid0=" + configSSID[0]);
file.println("pass0=" + configPASS[0]);
file.println("ssid1=" + configSSID[1]);
file.println("pass1=" + configPASS[1]);
file.println("ssid2=" + configSSID[2]);
file.println("pass2=" + configPASS[2]);
#if ENABLE_MQTT
file.println("mqttServer=" + mqttServer);
file.println("mqttPort=" + mqttPort);
file.println("mqttTopic=" + mqttTopic);
#endif
#if ENABLE_RF
file.println("rfChannel=" + rfChannel);
file.println("rfDevice=" + rfDevice);
#endif
file.close();
return true;
} }
return identifier;
return false;
} }
bool loadConfig() {
if (SPIFFS.exists(CONFIG_PATH)) {
#ifdef DEBUG
Serial.println("Reading config file");
#endif
// Read contents
File file = SPIFFS.open(CONFIG_PATH, "r");
String content = file.readString();
file.close();
// Parse contents
content.replace("\r\n", "\n");
content.replace("\r", "\n");
int start = 0;
int end = content.indexOf("\n", start);
while (end > 0) {
String line = content.substring(start, end);
#ifdef DEBUG
Serial.println(line);
#endif
if (line.startsWith("ssid0=")) configSSID[0] = line.substring(6);
else if (line.startsWith("pass0=")) configPASS[0] = line.substring(6);
else if (line.startsWith("ssid1=")) configSSID[1] = line.substring(6);
else if (line.startsWith("pass1=")) configPASS[1] = line.substring(6);
else if (line.startsWith("ssid2=")) configSSID[2] = line.substring(6);
else if (line.startsWith("pass2=")) configPASS[2] = line.substring(6);
#if ENABLE_MQTT
else if (line.startsWith("mqttServer=")) mqttServer = line.substring(11);
else if (line.startsWith("mqttPort=")) mqttPort = line.substring(9);
else if (line.startsWith("mqttTopic=")) mqttTopic = line.substring(10);
#endif
#if ENABLE_RF
else if (line.startsWith("rfChannel=")) rfChannel = line.substring(10);
else if (line.startsWith("rfDevice=")) rfDevice = line.substring(9);
#endif
if (end < 0) break;
start = end + 1;
end = content.indexOf("\n", start);
}
return true;
}
return false;
}
// -----------------------------------------------------------------------------
// OTA
// -----------------------------------------------------------------------------
#if ENABLE_OTA
void OTASetup() {
// Port defaults to 8266
ArduinoOTA.setPort(8266);
// Hostname defaults to esp8266-[ChipID]
ArduinoOTA.setHostname(getIdentifier());
// No authentication by default
ArduinoOTA.setPassword((const char *) ADMIN_PASS);
ArduinoOTA.onStart([]() {
#if ENABLE_RF
RemoteReceiver::disable();
#endif
#ifdef DEBUG
Serial.println("OTA - Start");
#endif
});
ArduinoOTA.onEnd([]() {
#ifdef DEBUG
Serial.println("OTA - End");
#endif
#if ENABLE_RF
RemoteReceiver::enable();
#endif
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
#ifdef DEBUG
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
#endif
});
ArduinoOTA.onError([](ota_error_t error) {
#ifdef DEBUG
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
#endif
});
ArduinoOTA.begin();
}
#if ENABLE_OTA_AUTO
void OTAUpdate() {
static unsigned long last_check = 0;
if (WiFi.status() != WL_CONNECTED) return;
if ((last_check > 0) && (millis() - last_check < OTA_CHECK_INTERVAL)) return;
last_check = millis();
HTTPClient http;
char url[100];
sprintf(url, "%s/%s/%s", OTA_SERVER, MODEL, APP_VERSION);
http.begin(url);
int httpCode = http.GET();
#ifdef DEBUG
Serial.print("AUTO OTA UPDATE - GET ");
Serial.print(url);
Serial.print(" [");
Serial.print(httpCode);
Serial.println("]");
#endif
if (httpCode > 0) {
String payload = http.getString();
StaticJsonBuffer<500> jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(payload);
if (root.success()) {
const char* action = root["action"];
if (strcmp("update", action) == 0) {
bool error = false;
uint8_t updates = 0;
#ifdef DEBUG
const char* version = root["target"]["version"];
Serial.print("Updating with version: ");
Serial.println(version);
#endif
const char* spiffs = root["target"]["spiffs"];
if (spiffs[0] != 0) {
// Update SPIFFS
sprintf(url, "%s/%s", OTA_SERVER, spiffs);
#ifdef DEBUG
Serial.print("Updating file system from ");
Serial.println(url);
#endif
t_httpUpdate_return ret = ESPhttpUpdate.updateSpiffs(url);
if (ret == HTTP_UPDATE_FAILED) {
error = true;
#ifdef DEBUG
Serial.printf("HTTP_UPDATE_FAILD Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
#endif
} else if (ret == HTTP_UPDATE_OK) {
updates++;
#ifdef DEBUG
Serial.println("HTTP_UPDATE_OK");
#endif
} else {
#ifdef DEBUG
Serial.printf("HTTP_UPDATE_NO_UPDATES");
#endif
}
// Restore config
saveConfig();
} else {
#ifdef DEBUG
Serial.println("No file system binary available");
#endif
}
if (!error) {
const char* firmware = root["target"]["firmware"];
if (firmware[0] != 0) {
// Update binary
sprintf(url, "%s%s", OTA_SERVER, firmware);
#ifdef DEBUG
Serial.print("Updating firmware from ");
Serial.println(url);
#endif
t_httpUpdate_return ret = ESPhttpUpdate.update(url);
if (ret == HTTP_UPDATE_FAILED) {
#ifdef DEBUG
Serial.printf("HTTP_UPDATE_FAILD Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
#endif
} else if (ret == HTTP_UPDATE_OK) {
updates++;
#ifdef DEBUG
Serial.println("HTTP_UPDATE_OK");
#endif
} else {
#ifdef DEBUG
Serial.printf("HTTP_UPDATE_NO_UPDATES");
#endif
}
if (updates > 0) {
ESP.restart();
}
} else {
#ifdef DEBUG
Serial.println("No firmware binary available");
#endif
}
}
} else {
#ifdef DEBUG
Serial.println("Already in the latest version");
#endif
}
} else {
#ifdef DEBUG
Serial.println("Error parsing JSON");
#endif
}
}
http.end();
}
#endif
void OTALoop() {
ArduinoOTA.handle();
#if ENABLE_OTA_AUTO
OTAUpdate();
#endif
}
#endif
// -----------------------------------------------------------------------------
// Wifi
// -----------------------------------------------------------------------------
void wifiSetupAP() { void wifiSetupAP() {
// Set WIFI module AP mode // Set WIFI module AP mode
@ -235,6 +550,7 @@ void wifiSetupAP() {
// SoftAP mode // SoftAP mode
WiFi.softAP(getIdentifier(), ADMIN_PASS); WiFi.softAP(getIdentifier(), ADMIN_PASS);
status = WIFI_STATUS_AP; status = WIFI_STATUS_AP;
delay(100);
#ifdef DEBUG #ifdef DEBUG
Serial.print("[AP Mode] SSID: "); Serial.print("[AP Mode] SSID: ");
Serial.print(getIdentifier()); Serial.print(getIdentifier());
@ -310,6 +626,9 @@ void wifiSetupSTA(bool force) {
Serial.print(", IP address: "); Serial.print(", IP address: ");
Serial.println(WiFi.localIP()); Serial.println(WiFi.localIP());
#endif #endif
#if ENABLE_OTA_AUTO
OTAUpdate();
#endif
} else { } else {
#ifdef DEBUG #ifdef DEBUG
Serial.println("[STA Mode] NOT CONNECTED"); Serial.println("[STA Mode] NOT CONNECTED");
@ -329,6 +648,68 @@ void wifiLoop() {
} }
// -----------------------------------------------------------------------------
// RF
// -----------------------------------------------------------------------------
#if ENABLE_RF
void rfLoop() {
if (rfCode == 0) return;
#ifdef DEBUG
Serial.print("RF code: ");
Serial.println(rfCode);
#endif
if (rfCode == rfCodeON) switchRelayOn();
if (rfCode == rfCodeOFF) switchRelayOff();
rfCode = 0;
}
void rfBuildCodes() {
unsigned long code = 0;
// channel
unsigned int channel = rfChannel.toInt();
for (byte i = 0; i < 5; i++) {
code *= 3;
if (channel & 1) code += 1;
channel >>= 1;
}
// device
unsigned int device = rfDevice.toInt();
for (byte i = 0; i < 5; i++) {
code *= 3;
if (device != i) code += 2;
}
// status
code *= 9;
rfCodeOFF = code + 2;
rfCodeON = code + 6;
#ifdef DEBUG
Serial.print("RF code ON: ");
Serial.println(rfCodeON);
Serial.print("RF code OFF: ");
Serial.println(rfCodeOFF);
#endif
}
void rfCallback(unsigned long code, unsigned int period) {
rfCode = code;
}
void rfSetup() {
rfBuildCodes();
RemoteReceiver::init(RF_PIN, 3, rfCallback);
RemoteReceiver::enable();
}
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// WebServer // WebServer
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -416,7 +797,7 @@ void wifiLoop() {
// Replace placeholders // Replace placeholders
getCompileTime(buffer); getCompileTime(buffer);
content.replace("{appname}", String(APP_NAME) + "." + String(buffer));
content.replace("{appname}", String(APP_NAME) + " " + String(APP_VERSION) + " built " + String(buffer));
content.replace("{status}", digitalRead(RELAY_PIN) ? "1" : "0"); content.replace("{status}", digitalRead(RELAY_PIN) ? "1" : "0");
content.replace("{updateInterval}", String(STATUS_UPDATE_INTERVAL)); content.replace("{updateInterval}", String(STATUS_UPDATE_INTERVAL));
content.replace("{ssid0}", configSSID[0]); content.replace("{ssid0}", configSSID[0]);
@ -697,208 +1078,6 @@ void wifiLoop() {
#endif #endif
// -----------------------------------------------------------------------------
// RF
// -----------------------------------------------------------------------------
#if ENABLE_RF
void rfLoop() {
if (rfCode == 0) return;
#ifdef DEBUG
Serial.print("RF code: ");
Serial.println(rfCode);
#endif
if (rfCode == rfCodeON) switchRelayOn();
if (rfCode == rfCodeOFF) switchRelayOff();
rfCode = 0;
}
void rfBuildCodes() {
unsigned long code = 0;
// channel
unsigned int channel = rfChannel.toInt();
for (byte i = 0; i < 5; i++) {
code *= 3;
if (channel & 1) code += 1;
channel >>= 1;
}
// device
unsigned int device = rfDevice.toInt();
for (byte i = 0; i < 5; i++) {
code *= 3;
if (device != i) code += 2;
}
// status
code *= 9;
rfCodeOFF = code + 2;
rfCodeON = code + 6;
#ifdef DEBUG
Serial.print("RF code ON: ");
Serial.println(rfCodeON);
Serial.print("RF code OFF: ");
Serial.println(rfCodeOFF);
#endif
}
void rfCallback(unsigned long code, unsigned int period) {
rfCode = code;
}
void rfSetup() {
rfBuildCodes();
RemoteReceiver::init(RF_PIN, 3, rfCallback);
RemoteReceiver::enable();
}
#endif
// -----------------------------------------------------------------------------
// Configuration
// -----------------------------------------------------------------------------
bool saveConfig() {
File file = SPIFFS.open(CONFIG_PATH, "w");
if (file) {
file.println("ssid0=" + configSSID[0]);
file.println("pass0=" + configPASS[0]);
file.println("ssid1=" + configSSID[1]);
file.println("pass1=" + configPASS[1]);
file.println("ssid2=" + configSSID[2]);
file.println("pass2=" + configPASS[2]);
#if ENABLE_MQTT
file.println("mqttServer=" + mqttServer);
file.println("mqttPort=" + mqttPort);
file.println("mqttTopic=" + mqttTopic);
#endif
#if ENABLE_RF
file.println("rfChannel=" + rfChannel);
file.println("rfDevice=" + rfDevice);
#endif
file.close();
return true;
}
return false;
}
bool loadConfig() {
if (SPIFFS.exists(CONFIG_PATH)) {
#ifdef DEBUG
Serial.println("Reading config file");
#endif
// Read contents
File file = SPIFFS.open(CONFIG_PATH, "r");
String content = file.readString();
file.close();
// Parse contents
content.replace("\r\n", "\n");
content.replace("\r", "\n");
int start = 0;
int end = content.indexOf("\n", start);
while (end > 0) {
String line = content.substring(start, end);
#ifdef DEBUG
Serial.println(line);
#endif
if (line.startsWith("ssid0=")) configSSID[0] = line.substring(6);
else if (line.startsWith("pass0=")) configPASS[0] = line.substring(6);
else if (line.startsWith("ssid1=")) configSSID[1] = line.substring(6);
else if (line.startsWith("pass1=")) configPASS[1] = line.substring(6);
else if (line.startsWith("ssid2=")) configSSID[2] = line.substring(6);
else if (line.startsWith("pass2=")) configPASS[2] = line.substring(6);
#if ENABLE_MQTT
else if (line.startsWith("mqttServer=")) mqttServer = line.substring(11);
else if (line.startsWith("mqttPort=")) mqttPort = line.substring(9);
else if (line.startsWith("mqttTopic=")) mqttTopic = line.substring(10);
#endif
#if ENABLE_RF
else if (line.startsWith("rfChannel=")) rfChannel = line.substring(10);
else if (line.startsWith("rfDevice=")) rfDevice = line.substring(9);
#endif
if (end < 0) break;
start = end + 1;
end = content.indexOf("\n", start);
}
return true;
}
return false;
}
// -----------------------------------------------------------------------------
// OTA
// -----------------------------------------------------------------------------
#if ENABLE_OTA
void OTASetup() {
// Port defaults to 8266
ArduinoOTA.setPort(8266);
// Hostname defaults to esp8266-[ChipID]
ArduinoOTA.setHostname(getIdentifier());
// No authentication by default
ArduinoOTA.setPassword((const char *) ADMIN_PASS);
ArduinoOTA.onStart([]() {
#if ENABLE_RF
RemoteReceiver::disable();
#endif
#ifdef DEBUG
Serial.println("OTA - Start");
#endif
});
ArduinoOTA.onEnd([]() {
#ifdef DEBUG
Serial.println("OTA - End");
#endif
#if ENABLE_RF
RemoteReceiver::enable();
#endif
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
#ifdef DEBUG
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
#endif
});
ArduinoOTA.onError([](ota_error_t error) {
#ifdef DEBUG
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
#endif
});
ArduinoOTA.begin();
}
void OTALoop() {
ArduinoOTA.handle();
}
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Energy Monitor // Energy Monitor
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -964,24 +1143,6 @@ void hardwareSetup() {
EEPROM.read(0) == 1 ? switchRelayOn() : switchRelayOff(); EEPROM.read(0) == 1 ? switchRelayOn() : switchRelayOff();
} }
void blink(unsigned long delayOff, unsigned long delayOn) {
static unsigned long next = millis();
static bool status = HIGH;
if (next < millis()) {
status = !status;
digitalWrite(LED_PIN, status);
next += ((status) ? delayOff : delayOn);
}
}
void showStatus() {
if (WiFi.status() == WL_CONNECTED) {
blink(5000, 500);
} else {
blink(500, 500);
}
}
void hardwareLoop() { void hardwareLoop() {
if (button1.loop()) { if (button1.loop()) {
if (button1.getEvent() == EVENT_SINGLE_CLICK) toggleRelay(); if (button1.getEvent() == EVENT_SINGLE_CLICK) toggleRelay();
@ -996,10 +1157,16 @@ void hardwareLoop() {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void welcome() { void welcome() {
char buffer[BUFFER_SIZE];
getCompileTime(buffer);
Serial.println(); Serial.println();
Serial.println(APP_NAME);
Serial.println(APP_WEBSITE);
Serial.print(APP_NAME);
Serial.print(" ");
Serial.print(APP_VERSION);
Serial.print(" built ");
Serial.println(buffer);
Serial.println(APP_AUTHOR); Serial.println(APP_AUTHOR);
Serial.println(APP_WEBSITE);
Serial.println(); Serial.println();
Serial.print("Device: "); Serial.print("Device: ");
Serial.println(getIdentifier()); Serial.println(getIdentifier());
@ -1011,6 +1178,7 @@ void welcome() {
void setup() { void setup() {
hardwareSetup(); hardwareSetup();
delay(1000);
welcome(); welcome();
#if ENABLE_OTA #if ENABLE_OTA

+ 4
- 0
server/.gitignore View File

@ -0,0 +1,4 @@
/cache/
/vendor/
/logs/*
!/logs/README.md

+ 8
- 0
server/README.md View File

@ -0,0 +1,8 @@
# ESPurna Update Server
First version of an ESPurna update server, an API that responfs to ESPurna devices with information about the last available firmware.
## Use
1. Modify ```data/versions.js``` with info about available firmware versions depending on current model (device type) and firmware version.
1. Perform GET queries against http://<server>/<model>/<firmware_version>, for instance: ```http://192.168.1.105/espurna/0.9.1```

+ 21
- 0
server/composer.json View File

@ -0,0 +1,21 @@
{
"name": "tinkerman/espurna-update-server",
"description": "Update server that listen to ESPurna devices queries and returns last available firmware",
"keywords": ["esp8266", "ota", "espurna", "firmware"],
"homepage": "http://tinkerman.cat",
"license": "GPLv3",
"authors": [
{
"name": "Xose Pérez",
"email": "xose.perez@gmail.com",
"homepage": "http://tinkerman.cat/"
}
],
"require": {
"php": ">=5.5.0",
"slim/slim": "^3.1",
"slim/php-view": "^2.0",
"monolog/monolog": "^1.17",
"slim/twig-view": "^2.1"
}
}

+ 532
- 0
server/composer.lock View File

@ -0,0 +1,532 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "7cd735ef5dd6b16144dfd2a940224676",
"content-hash": "df0b70ace71b7e873d1119b4788805db",
"packages": [
{
"name": "container-interop/container-interop",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/container-interop/container-interop.git",
"reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/container-interop/container-interop/zipball/fc08354828f8fd3245f77a66b9e23a6bca48297e",
"reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e",
"shasum": ""
},
"type": "library",
"autoload": {
"psr-4": {
"Interop\\Container\\": "src/Interop/Container/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
"time": "2014-12-30 15:22:37"
},
{
"name": "monolog/monolog",
"version": "1.20.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "55841909e2bcde01b5318c35f2b74f8ecc86e037"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/55841909e2bcde01b5318c35f2b74f8ecc86e037",
"reference": "55841909e2bcde01b5318c35f2b74f8ecc86e037",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"psr/log": "~1.0"
},
"provide": {
"psr/log-implementation": "1.0.0"
},
"require-dev": {
"aws/aws-sdk-php": "^2.4.9",
"doctrine/couchdb": "~1.0@dev",
"graylog2/gelf-php": "~1.0",
"jakub-onderka/php-parallel-lint": "0.9",
"php-amqplib/php-amqplib": "~2.4",
"php-console/php-console": "^3.1.3",
"phpunit/phpunit": "~4.5",
"phpunit/phpunit-mock-objects": "2.3.0",
"ruflin/elastica": ">=0.90 <3.0",
"sentry/sentry": "^0.13",
"swiftmailer/swiftmailer": "~5.3"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-mongo": "Allow sending log messages to a MongoDB server",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"php-console/php-console": "Allow sending log messages to Google Chrome",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
"ruflin/elastica": "Allow sending log messages to an Elastic Search server",
"sentry/sentry": "Allow sending log messages to a Sentry server"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
"homepage": "http://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
],
"time": "2016-07-02 14:02:10"
},
{
"name": "nikic/fast-route",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/nikic/FastRoute.git",
"reference": "8ea928195fa9b907f0d6e48312d323c1a13cc2af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/FastRoute/zipball/8ea928195fa9b907f0d6e48312d323c1a13cc2af",
"reference": "8ea928195fa9b907f0d6e48312d323c1a13cc2af",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"FastRoute\\": "src/"
},
"files": [
"src/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Nikita Popov",
"email": "nikic@php.net"
}
],
"description": "Fast request router for PHP",
"keywords": [
"router",
"routing"
],
"time": "2016-06-12 19:08:51"
},
{
"name": "pimple/pimple",
"version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/silexphp/Pimple.git",
"reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/silexphp/Pimple/zipball/a30f7d6e57565a2e1a316e1baf2a483f788b258a",
"reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
}
},
"autoload": {
"psr-0": {
"Pimple": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
}
],
"description": "Pimple, a simple Dependency Injection Container",
"homepage": "http://pimple.sensiolabs.org",
"keywords": [
"container",
"dependency injection"
],
"time": "2015-09-11 15:10:35"
},
{
"name": "psr/http-message",
"version": "1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298",
"reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"time": "2015-05-04 20:22:00"
},
{
"name": "psr/log",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "fe0936ee26643249e916849d48e3a51d5f5e278b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b",
"reference": "fe0936ee26643249e916849d48e3a51d5f5e278b",
"shasum": ""
},
"type": "library",
"autoload": {
"psr-0": {
"Psr\\Log\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"keywords": [
"log",
"psr",
"psr-3"
],
"time": "2012-12-21 11:40:51"
},
{
"name": "slim/php-view",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/slimphp/PHP-View.git",
"reference": "8bae5b10d10c51596ef8d8113b3b63678718adcb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slimphp/PHP-View/zipball/8bae5b10d10c51596ef8d8113b3b63678718adcb",
"reference": "8bae5b10d10c51596ef8d8113b3b63678718adcb",
"shasum": ""
},
"require": {
"psr/http-message": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^5.0",
"slim/slim": "^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Slim\\Views\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Glenn Eggleton",
"email": "geggleto@gmail.com"
}
],
"description": "Render PHP view scripts into a PSR-7 Response object.",
"keywords": [
"framework",
"php",
"phtml",
"renderer",
"slim",
"template",
"view"
],
"time": "2016-03-04 09:48:50"
},
{
"name": "slim/slim",
"version": "3.4.2",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Slim.git",
"reference": "a132385f736063d00632b52b3f8a389fe66fe4fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/a132385f736063d00632b52b3f8a389fe66fe4fa",
"reference": "a132385f736063d00632b52b3f8a389fe66fe4fa",
"shasum": ""
},
"require": {
"container-interop/container-interop": "^1.1",
"nikic/fast-route": "^1.0",
"php": ">=5.5.0",
"pimple/pimple": "^3.0",
"psr/http-message": "^1.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0",
"squizlabs/php_codesniffer": "^2.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Slim\\": "Slim"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rob Allen",
"email": "rob@akrabat.com",
"homepage": "http://akrabat.com"
},
{
"name": "Josh Lockhart",
"email": "hello@joshlockhart.com",
"homepage": "https://joshlockhart.com"
},
{
"name": "Gabriel Manricks",
"email": "gmanricks@me.com",
"homepage": "http://gabrielmanricks.com"
},
{
"name": "Andrew Smith",
"email": "a.smith@silentworks.co.uk",
"homepage": "http://silentworks.co.uk"
}
],
"description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
"homepage": "http://slimframework.com",
"keywords": [
"api",
"framework",
"micro",
"router"
],
"time": "2016-05-25 11:23:38"
},
{
"name": "slim/twig-view",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Twig-View.git",
"reference": "16fded26a44b8e8e0e041f1cff32afa21daeb284"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slimphp/Twig-View/zipball/16fded26a44b8e8e0e041f1cff32afa21daeb284",
"reference": "16fded26a44b8e8e0e041f1cff32afa21daeb284",
"shasum": ""
},
"require": {
"php": ">=5.5.0",
"psr/http-message": "^1.0",
"twig/twig": "^1.18"
},
"require-dev": {
"phpunit/phpunit": "^4.8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Slim\\Views\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Josh Lockhart",
"email": "hello@joshlockhart.com",
"homepage": "http://joshlockhart.com"
}
],
"description": "Slim Framework 3 view helper built on top of the Twig templating component",
"homepage": "http://slimframework.com",
"keywords": [
"framework",
"slim",
"template",
"twig",
"view"
],
"time": "2016-03-13 20:58:41"
},
{
"name": "twig/twig",
"version": "v1.24.1",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "3566d311a92aae4deec6e48682dc5a4528c4a512"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/3566d311a92aae4deec6e48682dc5a4528c4a512",
"reference": "3566d311a92aae4deec6e48682dc5a4528c4a512",
"shasum": ""
},
"require": {
"php": ">=5.2.7"
},
"require-dev": {
"symfony/debug": "~2.7",
"symfony/phpunit-bridge": "~2.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.24-dev"
}
},
"autoload": {
"psr-0": {
"Twig_": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
},
{
"name": "Twig Team",
"homepage": "http://twig.sensiolabs.org/contributors",
"role": "Contributors"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "http://twig.sensiolabs.org",
"keywords": [
"templating"
],
"time": "2016-05-30 09:11:59"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.5.0"
},
"platform-dev": []
}

+ 14
- 0
server/data/versions.json View File

@ -0,0 +1,14 @@
[
{
"model": "SONOFF",
"firmware": {
"min": "*",
"max": "0.9.2"
},
"target": {
"version": "0.9.2",
"firmware": "/firmware/espurna-0.9.2.bin",
"spiffs": "/firmware/espurna-0.9.1-spiffs.bin"
}
}
]

+ 10
- 0
server/public/.htaccess View File

@ -0,0 +1,10 @@
RewriteEngine On
# Some hosts may require you to use the `RewriteBase` directive.
# If you need to use the `RewriteBase` directive, it should be the
# absolute physical path to the directory that contains this htaccess file.
#
# RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

+ 30
- 0
server/public/index.php View File

@ -0,0 +1,30 @@
<?php
if (PHP_SAPI == 'cli-server') {
// To help the built-in PHP dev server, check if the request was actually for
// something which should probably be served as a static file
$url = parse_url($_SERVER['REQUEST_URI']);
$file = __DIR__ . $url['path'];
if (is_file($file)) {
return false;
}
}
require __DIR__ . '/../vendor/autoload.php';
session_start();
// Instantiate the app
$settings = require __DIR__ . '/../src/settings.php';
$app = new \Slim\App($settings);
// Set up dependencies
require __DIR__ . '/../src/dependencies.php';
// Register middleware
require __DIR__ . '/../src/middleware.php';
// Register routes
require __DIR__ . '/../src/routes.php';
// Run app
$app->run();

+ 36
- 0
server/src/dependencies.php View File

@ -0,0 +1,36 @@
<?php
// DIC configuration
$container = $app->getContainer();
// view renderer
// Register component on container
$container['view'] = function ($container) {
$settings = $container->get('settings')['renderer'];
$view = new \Slim\Views\Twig($settings['template_path'], [
'cache' => $settings['cache_path']
]);
$view->addExtension(new \Slim\Views\TwigExtension(
$container['router'],
$container['request']->getUri()
));
return $view;
};
// monolog
$container['logger'] = function ($container) {
$settings = $container->get('settings')['logger'];
$logger = new Monolog\Logger($settings['name']);
$logger->pushProcessor(new Monolog\Processor\UidProcessor());
$logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], Monolog\Logger::DEBUG));
return $logger;
};
// version database
$container['data'] = function($container) {
$settings = $container->get('settings')['database'];
$json_data = file_get_contents($settings['path']);
$data = json_decode($json_data, true);
return $data;
};

+ 4
- 0
server/src/middleware.php View File

@ -0,0 +1,4 @@
<?php
// Application middleware
// e.g: $app->add(new \Slim\Csrf\Guard);

+ 36
- 0
server/src/routes.php View File

@ -0,0 +1,36 @@
<?php
// Routes
$app->get('/{model}/{current}', function($request, $response, $args) {
$found = false;
$model = $request->getAttribute('model');
$current = $request->getAttribute('current');
foreach ($this->get('data') as $version) {
if (($model == $version['model'])
&& (($version['firmware']['min'] == "*" || version_compare($version['firmware']['min'], $current, "<=")))
&& (($version['firmware']['max'] == "*" || version_compare($version['firmware']['max'], $current, ">")))) {
$response->getBody()->write(stripslashes(json_encode(array(
'action' => 'update',
'target' => $version["target"]
))));
$found = true;
break;
}
};
if (!$found) {
$response->getBody()->write(stripslashes(json_encode(array(
'action' => 'none',
))));
}
return $response;
});

+ 26
- 0
server/src/settings.php View File

@ -0,0 +1,26 @@
<?php
return [
'settings' => [
'displayErrorDetails' => true, // set to false in production
'addContentLengthHeader' => false, // Allow the web server to send the content-length header
// Renderer settings
'renderer' => [
'template_path' => __DIR__ . '/../templates/',
'cache_path' => __DIR__ . '/../cache/',
],
// Monolog settings
'logger' => [
'name' => 'espurna-update-server',
'path' => __DIR__ . '/../logs/app.log',
],
// Database
'database' => [
'type' => 'json',
'path' => __DIR__ . '/../data/versions.json',
],
],
];

+ 57
- 0
server/templates/index.phtml View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>ESPurna Update Server</title>
<link href='//fonts.googleapis.com/css?family=Lato:300' rel='stylesheet' type='text/css'>
<style>
body {
margin: 50px 0 0 0;
padding: 0;
width: 100%;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
text-align: center;
color: #aaa;
font-size: 18px;
}
h1 {
color: #719e40;
letter-spacing: -3px;
font-family: 'Lato', sans-serif;
font-size: 100px;
font-weight: 200;
margin-bottom: 0;
}
</style>
</head>
<body>
<h1>ESPurna Update Server</h1>
<table>
<thead>
<tr>
<th>Device</th>
<th>Minimum Hardware Version</th>
<th>Maximum Hardware Version</th>
<th>Minimum Firmware Version</th>
<th>Maximum Firmware Version</th>
<th>Latest Firmware Version</th>
</tr>
</thead>
<tbody>
{% for version in versions %}
<tr>
<td>{{ version.model }}</td>
<td>{{ version.hardware_min }}</td>
<td>{{ version.hardware_max }}</td>
<td>{{ version.firmware_min }}</td>
<td>{{ version.firmware_max }}</td>
<td>{{ version.firmware_version }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>

Loading…
Cancel
Save