Added OTA firmware update
Added range customization (untested) Added static mode easing (untested)
This commit is contained in:
parent
abd12a3714
commit
b39f3ba826
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
||||
src/credentials.h
|
||||
*.sublime-workspace
|
||||
web/node_modules/
|
||||
web/update/
|
||||
|
3
build.ps1
Normal file
3
build.ps1
Normal file
@ -0,0 +1,3 @@
|
||||
& .\updateversion.ps1
|
||||
& platformio run
|
||||
Copy-Item .\.pioenvs\esp12e\firmware.elf .\web\update\
|
@ -23,6 +23,7 @@ The first byte of a request is the command. Further data depends on the command.
|
||||
| 0x04 | SetMode |
|
||||
| 0x05 | GetRange |
|
||||
| 0x06 | SetRange |
|
||||
| 0xFF | UpdateFirmware |
|
||||
|
||||
|
||||
Response
|
||||
@ -101,6 +102,31 @@ Output parameters:<br>
|
||||
_Same as input parameters_
|
||||
|
||||
|
||||
### UpdateFirmware
|
||||
Updates the firmware over WiFi. This functionality may be limited in the configuration which is hardcoded into the firmware. There are three modes of operation:
|
||||
|
||||
1. Disabled, firmware can not be updated over WiFi
|
||||
2. Enabled with a hardcoded URL. You may request an update check but any source provided will be ignored.
|
||||
3. Enabled with the URL as input parameters.
|
||||
|
||||
You can only request an update once every 5 seconds (by default), otherwise an error will be returned.
|
||||
|
||||
Please note that the entire packet must not exceed 254 bytes or the remainder will be discarded.
|
||||
|
||||
|
||||
For further information on implementing an update server, see [Arduino ESP8266's HTTPUpdate reference](https://github.com/esp8266/Arduino/blob/master/doc/ota_updates/readme.md#advanced-updater-1).
|
||||
|
||||
|
||||
Input parameters:<br>
|
||||
_If enabled_
|
||||
*port* (word): the port on which the HTTP update server runs
|
||||
*host* (null-terminated string): the host name or IP address on which the HTTP update server runs
|
||||
*path* (null-terminated string): the path to
|
||||
|
||||
Output parameters:<br>
|
||||
1 if succesful, 0 if no updates are available.
|
||||
|
||||
|
||||
Modes
|
||||
=====
|
||||
|
||||
|
28
src/config.h
28
src/config.h
@ -1,9 +1,14 @@
|
||||
#ifndef __Config
|
||||
#define __Config
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <stdint.h>
|
||||
#include "credentials.h"
|
||||
|
||||
|
||||
#define SerialDebug
|
||||
|
||||
|
||||
// The name of this device on the network
|
||||
static const char* WiFiHostname = "Stairs";
|
||||
|
||||
@ -25,4 +30,27 @@ static const uint8_t PinSCL = 12;
|
||||
static const uint8_t PWMDriverAddress = 0x40;
|
||||
static const uint16_t PWMDriverPWMFrequency = 1600;
|
||||
|
||||
|
||||
// Determines if OTA firmware updates are enabled
|
||||
// 0 - Disabled
|
||||
// 1 - Enabled (fixed URL)
|
||||
// 2 - Enabled (use URL in command)
|
||||
static const uint8_t OTAUpdateEnabled = 2;
|
||||
|
||||
static const char* OTAUpdateFixedHost = "";
|
||||
static const uint16_t OTAUpdateFixedPort = 80;
|
||||
static const char* OTAUpdateFixedPath = "/";
|
||||
|
||||
// The minimum amount of time (in milliseconds) between update requests
|
||||
static const uint32_t OTAUpdateThrottle = 5000;
|
||||
|
||||
|
||||
#ifdef SerialDebug
|
||||
#define _d(msg) Serial.print(msg)
|
||||
#define _dln(msg) Serial.println(msg)
|
||||
#else
|
||||
#define _d(msg) do { } while (0)
|
||||
#define _dln(msg) do { } while (0)
|
||||
#endif
|
||||
|
||||
#endif
|
216
src/main.cpp
216
src/main.cpp
@ -6,6 +6,7 @@
|
||||
#include <Stream.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <WiFiUdp.h>
|
||||
#include <ESP8266httpUpdate.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "protocol.h"
|
||||
@ -16,6 +17,7 @@
|
||||
#include "modes\slide.h"
|
||||
#include "modes\static.h"
|
||||
#include "stairs.h"
|
||||
#include "version.h"
|
||||
|
||||
|
||||
PCA9685* pwmDriver;
|
||||
@ -33,20 +35,35 @@ void setCurrentMode(IMode *mode, uint8_t identifier);
|
||||
void handleCurrentMode();
|
||||
|
||||
|
||||
|
||||
|
||||
void setup()
|
||||
{
|
||||
#ifdef SerialDebug
|
||||
Serial.begin(115200);
|
||||
delay(5000);
|
||||
#endif
|
||||
|
||||
|
||||
_dln("Initializing PCA9685");
|
||||
_d("Version: ");
|
||||
_dln(FirmwareVersion);
|
||||
|
||||
pwmDriver = new PCA9685();
|
||||
pwmDriver->setAddress(PWMDriverAddress, PinSDA, PinSCL);
|
||||
pwmDriver->setPWMFrequency(PWMDriverPWMFrequency);
|
||||
|
||||
_dln("Initializing Stairs");
|
||||
|
||||
stairs = new Stairs();
|
||||
stairs->init(pwmDriver);
|
||||
|
||||
_dln("Initializing WiFi");
|
||||
WiFi.hostname(WiFiHostname);
|
||||
WiFi.begin(WiFiSSID, WiFiPassword);
|
||||
|
||||
|
||||
// Run a little startup test sequence
|
||||
_dln("Starting initialization sequence");
|
||||
stairs->setAll(IStairs::Off);
|
||||
stairs->set(0, IStairs::On);
|
||||
delay(300);
|
||||
@ -61,6 +78,8 @@ void setup()
|
||||
stairs->set(StepCount - 1, IStairs::Off);
|
||||
|
||||
|
||||
_dln("Waiting for WiFi");
|
||||
|
||||
// Pulsate the bottom step while WiFi is connecting
|
||||
uint16_t brightness = 0;
|
||||
uint16_t speed = 16;
|
||||
@ -78,6 +97,11 @@ void setup()
|
||||
setCurrentMode(new StaticMode(), Mode::Static);
|
||||
|
||||
|
||||
_d("IP address: ");
|
||||
_dln(WiFi.localIP());
|
||||
|
||||
_dln("Starting UDP server");
|
||||
|
||||
// Start the UDP server
|
||||
udpServer.begin(UDPPort);
|
||||
}
|
||||
@ -88,11 +112,9 @@ uint32_t currentTime;
|
||||
// Note: the packet size must at least be able to accomodate the
|
||||
// command with the largest parameter list, there is no overflow
|
||||
// checking in the mode classes!
|
||||
//
|
||||
// At the time of writing, Get/SetRange is the largest:
|
||||
// Set Range (1) + UseScaling (1) + StepCount * 2x Word (4)
|
||||
// = 66 for the maximum of 16 steps
|
||||
uint8_t packet[100];
|
||||
const uint8_t maxPacketSize = 255;
|
||||
|
||||
uint8_t packet[maxPacketSize];
|
||||
uint8_t* packetRef;
|
||||
|
||||
void loop()
|
||||
@ -109,8 +131,10 @@ void checkRequest()
|
||||
int packetSize = udpServer.parsePacket();
|
||||
if (packetSize)
|
||||
{
|
||||
_dln("Handling incoming packet");
|
||||
|
||||
memset(packet, 0, sizeof(packet));
|
||||
int length = udpServer.read(packet, 50);
|
||||
int length = udpServer.read(packet, maxPacketSize - 1);
|
||||
if (length && packet[0])
|
||||
{
|
||||
packetRef = packet;
|
||||
@ -120,8 +144,137 @@ void checkRequest()
|
||||
}
|
||||
|
||||
|
||||
void handlePing(uint8_t* packet)
|
||||
{
|
||||
_dln("Handling Ping");
|
||||
|
||||
udpServer.write(Command::Ping);
|
||||
udpServer.write(StepCount);
|
||||
}
|
||||
|
||||
|
||||
void handleGetMode(uint8_t* packet)
|
||||
{
|
||||
_dln("Handling GetMode");
|
||||
|
||||
udpServer.write(Command::GetMode);
|
||||
udpServer.write(currentModeIdentifier);
|
||||
currentMode->write(&udpServer);
|
||||
}
|
||||
|
||||
|
||||
void handleSetMode(uint8_t* packet)
|
||||
{
|
||||
_dln("Handling SetMode");
|
||||
uint8_t newIdentifier = *packet;
|
||||
packet++;
|
||||
|
||||
IMode* newMode = createMode(newIdentifier);
|
||||
|
||||
if (newMode != NULL)
|
||||
{
|
||||
newMode->read(packet);
|
||||
|
||||
udpServer.write(Command::SetMode);
|
||||
udpServer.write(newIdentifier);
|
||||
newMode->write(&udpServer);
|
||||
|
||||
_dln("Updating current mode");
|
||||
setCurrentMode(newMode, newIdentifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
udpServer.write(Command::Error);
|
||||
udpServer.write(Command::SetMode);
|
||||
udpServer.write(newIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
uint32_t lastUpdateCheck = 0;
|
||||
|
||||
void handleUpdateFirmware(uint8_t* packet)
|
||||
{
|
||||
_dln("Handling UpdateFirmware");
|
||||
|
||||
HTTPUpdateResult result;
|
||||
|
||||
if (currentTime - lastUpdateCheck <= OTAUpdateThrottle)
|
||||
{
|
||||
udpServer.write(Command::Error);
|
||||
udpServer.write(Command::UpdateFirmware);
|
||||
udpServer.write((uint8_t)2);
|
||||
return;
|
||||
}
|
||||
|
||||
lastUpdateCheck = currentTime;
|
||||
|
||||
switch (OTAUpdateEnabled)
|
||||
{
|
||||
case 1:
|
||||
result = ESPhttpUpdate.update(OTAUpdateFixedHost, OTAUpdateFixedPort, OTAUpdateFixedPath, FirmwareVersion);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
{
|
||||
uint16_t port;
|
||||
memcpy(&port, packet, sizeof(port));
|
||||
packet += sizeof(port);
|
||||
|
||||
_d("Port: ");
|
||||
_dln(port);
|
||||
|
||||
char host[255];
|
||||
char path[255];
|
||||
|
||||
strcpy(host, (char*)packet);
|
||||
packet += strlen(host) + 1;
|
||||
|
||||
strcpy(path, (char*)packet);
|
||||
|
||||
_d("Host: ");
|
||||
_dln(host);
|
||||
_d("Path: ");
|
||||
_dln(path);
|
||||
|
||||
result = ESPhttpUpdate.update(host, port, path, FirmwareVersion);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
udpServer.write(Command::Error);
|
||||
udpServer.write(Command::UpdateFirmware);
|
||||
udpServer.write((uint8_t)0);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HTTP_UPDATE_NO_UPDATES:
|
||||
udpServer.write(Command::UpdateFirmware);
|
||||
udpServer.write((uint8_t)0);
|
||||
break;
|
||||
|
||||
case HTTP_UPDATE_OK:
|
||||
udpServer.write(Command::UpdateFirmware);
|
||||
udpServer.write((uint8_t)1);
|
||||
break;
|
||||
|
||||
default:
|
||||
udpServer.write(Command::Error);
|
||||
udpServer.write(Command::UpdateFirmware);
|
||||
udpServer.write((uint8_t)1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void handleRequest(uint8_t* packet)
|
||||
{
|
||||
_d("Handling request: ");
|
||||
_dln(*packet);
|
||||
|
||||
|
||||
// Every request will result in a reply, either containing the
|
||||
// requested data or a copy of the input parameters for verification.
|
||||
//
|
||||
@ -130,50 +283,19 @@ void handleRequest(uint8_t* packet)
|
||||
udpServer.beginPacket(udpServer.remoteIP(), udpServer.remotePort());
|
||||
udpServer.write(Command::Reply);
|
||||
|
||||
switch (*packet)
|
||||
uint8_t command = *packet;
|
||||
packet++;
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case Command::Ping:
|
||||
udpServer.write(Command::Ping);
|
||||
udpServer.write(StepCount);
|
||||
break;
|
||||
|
||||
case Command::GetMode:
|
||||
udpServer.write(Command::GetMode);
|
||||
udpServer.write(currentModeIdentifier);
|
||||
currentMode->write(&udpServer);
|
||||
break;
|
||||
|
||||
case Command::SetMode:
|
||||
{
|
||||
packet++;
|
||||
uint8_t newIdentifier = *packet;
|
||||
packet++;
|
||||
|
||||
IMode* newMode = createMode(newIdentifier);
|
||||
|
||||
if (newMode != NULL)
|
||||
{
|
||||
newMode->read(packet);
|
||||
|
||||
udpServer.write(Command::SetMode);
|
||||
udpServer.write(newIdentifier);
|
||||
newMode->write(&udpServer);
|
||||
|
||||
setCurrentMode(newMode, newIdentifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
udpServer.write(Command::Error);
|
||||
udpServer.write(Command::SetMode);
|
||||
udpServer.write(newIdentifier);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Command::Ping: handlePing(packet); break;
|
||||
case Command::GetMode: handleGetMode(packet); break;
|
||||
case Command::SetMode: handleSetMode(packet); break;
|
||||
case Command::UpdateFirmware: handleUpdateFirmware(packet); break;
|
||||
|
||||
default:
|
||||
udpServer.write(Command::Error);
|
||||
udpServer.write(*packet);
|
||||
udpServer.write(command);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <Stream.h>
|
||||
#include "../config.h"
|
||||
#include "../mode.h"
|
||||
|
||||
|
||||
@ -15,12 +16,16 @@ class BaseMode : public IMode
|
||||
public:
|
||||
virtual void read(uint8_t* data)
|
||||
{
|
||||
this->parameters = *reinterpret_cast<T*>(data);
|
||||
_d("Reading parameters, size ");
|
||||
_dln(sizeof(T));
|
||||
memcpy(&this->parameters, data, sizeof(T));
|
||||
}
|
||||
|
||||
virtual void write(Stream* stream)
|
||||
{
|
||||
stream->write(reinterpret_cast<uint8_t*>(&this->parameters), sizeof(T));
|
||||
_d("Writing parameters, size ");
|
||||
_dln(sizeof(T));
|
||||
stream->write((uint8_t*)&this->parameters, sizeof(T));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -13,7 +13,7 @@ void CustomMode::read(uint8_t* data)
|
||||
|
||||
void CustomMode::write(Stream* stream)
|
||||
{
|
||||
stream->write(reinterpret_cast<uint8_t*>(&this->values), sizeof(this->values));
|
||||
stream->write((uint8_t*)&this->values, sizeof(this->values));
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,10 +2,48 @@
|
||||
|
||||
void StaticMode::init(IStairs* stairs, uint32_t currentTime)
|
||||
{
|
||||
stairs->setAll(this->parameters.brightness);
|
||||
_dln("Initializing static mode:");
|
||||
_d("currentBrightness: "); _dln(this->currentBrightness);
|
||||
_d("brightness: "); _dln(this->parameters.brightness);
|
||||
_d("easeTime: "); _dln(this->parameters.easeTime);
|
||||
|
||||
if (this->parameters.easeTime > 0 && this->parameters.brightness != this->currentBrightness)
|
||||
{
|
||||
_dln("Easing...");
|
||||
|
||||
this->easeStartTime = currentTime;
|
||||
this->easeStartBrightness = currentBrightness;
|
||||
|
||||
if (this->parameters.brightness > this->currentBrightness)
|
||||
this->easeState = Up;
|
||||
else
|
||||
this->easeState = Down;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dln("Updating immediately...");
|
||||
this->easeState = None;
|
||||
|
||||
stairs->setAll(this->parameters.brightness);
|
||||
this->currentBrightness = this->parameters.brightness;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void StaticMode::tick(IStairs* stairs, uint32_t currentTime)
|
||||
{
|
||||
if (this->easeState == None)
|
||||
return;
|
||||
|
||||
this->currentBrightness = (currentTime - this->easeStartTime) * ((this->parameters.brightness - this->easeStartBrightness) / this->parameters.easeTime);
|
||||
|
||||
|
||||
if ((this->easeState == Up && this->currentBrightness >= this->parameters.brightness) ||
|
||||
(this->easeState == Down && this->currentBrightness <= this->parameters.brightness))
|
||||
{
|
||||
this->currentBrightness = this->parameters.brightness;
|
||||
this->easeState = None;
|
||||
}
|
||||
|
||||
stairs->setAll(this->currentBrightness);
|
||||
}
|
@ -8,16 +8,34 @@
|
||||
|
||||
struct StaticModeParameters
|
||||
{
|
||||
uint16_t brightness = 0;
|
||||
uint16_t brightness;
|
||||
uint32_t easeTime;
|
||||
};
|
||||
|
||||
enum EaseState
|
||||
{
|
||||
None,
|
||||
Up,
|
||||
Down
|
||||
};
|
||||
|
||||
|
||||
class StaticMode : public BaseMode<StaticModeParameters>
|
||||
{
|
||||
private:
|
||||
uint16_t currentBrightness;
|
||||
uint32_t easeStartTime;
|
||||
uint16_t easeStartBrightness;
|
||||
EaseState easeState;
|
||||
|
||||
public:
|
||||
StaticMode()
|
||||
{
|
||||
parameters.brightness = 0;
|
||||
parameters.easeTime = 0;
|
||||
|
||||
easeState = None;
|
||||
currentBrightness = 0;
|
||||
}
|
||||
|
||||
void init(IStairs* stairs, uint32_t currentTime);
|
||||
|
@ -12,8 +12,12 @@ class Command
|
||||
static const uint8_t Reply = 0x02;
|
||||
static const uint8_t GetMode = 0x03;
|
||||
static const uint8_t SetMode = 0x04;
|
||||
/*
|
||||
static const uint8_t GetRange = 0x05;
|
||||
static const uint8_t SetRange = 0x06;
|
||||
*/
|
||||
|
||||
static const uint8_t UpdateFirmware = 0xFF;
|
||||
};
|
||||
|
||||
|
||||
|
@ -17,8 +17,12 @@ struct Header
|
||||
|
||||
void Stairs::init(PCA9685* pwmDriver)
|
||||
{
|
||||
this->useScaling = false;
|
||||
memset(this->ranges, 0, sizeof(this->ranges));
|
||||
|
||||
this->pwmDriver = pwmDriver;
|
||||
|
||||
_dln("Loading range configuration");
|
||||
SPIFFS.begin();
|
||||
this->readRange();
|
||||
}
|
||||
@ -53,7 +57,7 @@ uint16_t Stairs::getPWMValue(uint8_t step, uint16_t brightness)
|
||||
if (step < 0 || step >= StepCount)
|
||||
return brightness;
|
||||
|
||||
Range* range = this->ranges[step];
|
||||
Range* range = &this->ranges[step];
|
||||
|
||||
if (this->useScaling)
|
||||
{
|
||||
@ -73,7 +77,7 @@ uint16_t Stairs::getPWMValue(uint8_t step, uint16_t brightness)
|
||||
void Stairs::getRange(Stream* stream)
|
||||
{
|
||||
stream->write(this->useScaling ? 1 : 0);
|
||||
stream->write(reinterpret_cast<uint8_t*>(&this->ranges), sizeof(this->ranges));
|
||||
stream->write((uint8_t*)&this->ranges, sizeof(this->ranges));
|
||||
}
|
||||
|
||||
|
||||
@ -96,16 +100,17 @@ void Stairs::readRange()
|
||||
return;
|
||||
|
||||
Header header;
|
||||
f.readBytes(reinterpret_cast<char*>(&header), sizeof(Header));
|
||||
f.readBytes((char*)&header, sizeof(Header));
|
||||
|
||||
if (header.version != 1)
|
||||
return;
|
||||
|
||||
this->useScaling = (header.useScaling == 1);
|
||||
|
||||
memset(this->ranges, 0, sizeof(this->ranges));
|
||||
f.readBytes(reinterpret_cast<char*>(&this->ranges), header.rangeCount * sizeof(Range));
|
||||
f.readBytes((char*)&this->ranges, header.rangeCount * sizeof(Range));
|
||||
f.close();
|
||||
|
||||
_d("- useScaling: ");
|
||||
_dln(this->useScaling);
|
||||
}
|
||||
|
||||
|
||||
@ -120,7 +125,7 @@ void Stairs::writeRange()
|
||||
header.useScaling = this->useScaling;
|
||||
header.rangeCount = StepCount;
|
||||
|
||||
f.write(reinterpret_cast<uint8_t*>(&header), sizeof(Header));
|
||||
f.write(reinterpret_cast<uint8_t*>(&this->ranges), sizeof(this->ranges));
|
||||
f.write((uint8_t*)&header, sizeof(Header));
|
||||
f.write((uint8_t*)&this->ranges, sizeof(this->ranges));
|
||||
f.close();
|
||||
}
|
@ -19,7 +19,7 @@ class Stairs : public IStairs
|
||||
PCA9685* pwmDriver;
|
||||
|
||||
bool useScaling;
|
||||
Range* ranges[StepCount];
|
||||
Range ranges[StepCount];
|
||||
|
||||
protected:
|
||||
void readRange();
|
||||
|
6
src/version.h
Normal file
6
src/version.h
Normal file
@ -0,0 +1,6 @@
|
||||
#ifndef __Version
|
||||
#define __Version
|
||||
|
||||
static const char* FirmwareVersion = "0.1.0+8";
|
||||
|
||||
#endif
|
23
updateversion.ps1
Normal file
23
updateversion.ps1
Normal file
@ -0,0 +1,23 @@
|
||||
$output = & GitVersion /output json /nofetch
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Verbose "$output"
|
||||
throw "GitVersion failed with exit code: $LASTEXITCODE"
|
||||
}
|
||||
|
||||
$version = $output | ConvertFrom-Json
|
||||
|
||||
@"
|
||||
#ifndef __Version
|
||||
#define __Version
|
||||
|
||||
static const char* FirmwareVersion = "{0}";
|
||||
|
||||
#endif
|
||||
"@ -f $version.FullSemVer | Out-File -Encoding UTF8 .\src\version.h
|
||||
|
||||
@"
|
||||
module.exports =
|
||||
{{
|
||||
Version: "{0}"
|
||||
}};
|
||||
"@ -f $version.FullSemVer | Out-File -Encoding UTF8 .\web\version.js
|
@ -1 +0,0 @@
|
||||
@platformio run --target upload
|
2
upload.ps1
Normal file
2
upload.ps1
Normal file
@ -0,0 +1,2 @@
|
||||
& .\updateversion.ps1
|
||||
& platformio run --target upload
|
69
web/app.js
69
web/app.js
@ -1,11 +1,38 @@
|
||||
var fs = require('fs');
|
||||
var express = require('express');
|
||||
var semverUtils = require('semver-utils')
|
||||
var client = require('./client');
|
||||
|
||||
var httpPort = 3127;
|
||||
|
||||
var stairsHost = '10.138.2.12';
|
||||
var stairsHost = '10.138.2.25';
|
||||
var stairsUdpPort = 3126;
|
||||
|
||||
var firmwareFile = './update/firmware.elf';
|
||||
|
||||
|
||||
function requireNoCache(filename)
|
||||
{
|
||||
delete require.cache[require.resolve(filename)];
|
||||
return require(filename);
|
||||
}
|
||||
|
||||
|
||||
function isNewer(version1, version2)
|
||||
{
|
||||
if (version1.major > version2.major) return true;
|
||||
if (version1.major < version2.major) return false;
|
||||
|
||||
if (version1.minor > version2.minor) return true;
|
||||
if (version1.minor < version2.minor) return false;
|
||||
|
||||
if (version1.patch > version2.patch) return true;
|
||||
if (version1.patch < version2.patch) return false;
|
||||
|
||||
if (version1.build > version2.build) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
client.init(stairsHost, stairsUdpPort);
|
||||
|
||||
@ -45,6 +72,46 @@ app.get('/setMode/:mode', function(req, res)
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/updateFirmware', function(req, res)
|
||||
{
|
||||
client.updateFirmware(req.query, function(data, error)
|
||||
{
|
||||
if (error)
|
||||
res.status(500);
|
||||
|
||||
res.send(data);
|
||||
});
|
||||
})
|
||||
|
||||
app.get('/checkUpdate', function(req, res)
|
||||
{
|
||||
if (!fs.existsSync(firmwareFile))
|
||||
{
|
||||
console.log('checkUpdate: ' + firmwareFile + ' not found!');
|
||||
res.sendStatus(304);
|
||||
return;
|
||||
}
|
||||
|
||||
var version = requireNoCache('./version.js');
|
||||
var deviceVersion = semverUtils.parse(req.headers['x-esp8266-version']);
|
||||
var localVersion = semverUtils.parse(version.Version);
|
||||
|
||||
console.log('checkUpdate:');
|
||||
console.log(' Device version = ' + semverUtils.stringify(deviceVersion));
|
||||
console.log(' Local version = ' + semverUtils.stringify(localVersion));
|
||||
|
||||
if (isNewer(localVersion, deviceVersion))
|
||||
{
|
||||
res.download(firmwareFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
res.sendStatus(304);
|
||||
}
|
||||
|
||||
res.send(304);
|
||||
});
|
||||
|
||||
app.use(express.static(__dirname + '/static'));
|
||||
|
||||
|
||||
|
@ -53,7 +53,7 @@ client.on('message', function (message, remote)
|
||||
});
|
||||
|
||||
|
||||
function requestResponse(buffer, callback)
|
||||
function requestResponse(buffer, callback, withTimeout)
|
||||
{
|
||||
if (buffer === null || buffer.length == 0) return;
|
||||
console.log('> ' + buffer.toString('hex'));
|
||||
@ -61,17 +61,21 @@ function requestResponse(buffer, callback)
|
||||
var command = buffer.readInt8(0);
|
||||
var cancelled = false;
|
||||
|
||||
var timeout = setTimeout(function()
|
||||
if (typeof(withTimeout) == 'undefined') withTimeout = true;
|
||||
if (withTimeout)
|
||||
{
|
||||
cancelled = true;
|
||||
callback(null, true);
|
||||
clearTimeout(timeout);
|
||||
}, 2000);
|
||||
var timeout = setTimeout(function()
|
||||
{
|
||||
cancelled = true;
|
||||
callback(null, true);
|
||||
clearTimeout(timeout);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
registerResponseHandler(command, function(reader, error)
|
||||
{
|
||||
if (cancelled) return;
|
||||
clearTimeout(timeout);
|
||||
if (withTimeout) clearTimeout(timeout);
|
||||
|
||||
callback(reader, error);
|
||||
return true;
|
||||
@ -149,11 +153,11 @@ function writeModeData(mode, data)
|
||||
|
||||
var valueCount = Math.min(16, brightness.length);
|
||||
var buffer = Buffer.alloc(2 + (valueCount * 2));
|
||||
buffer.writeInt8(protocol.Command.SetMode, 0);
|
||||
buffer.writeInt8(mode, 1);
|
||||
buffer.writeUInt8(protocol.Command.SetMode, 0);
|
||||
buffer.writeUInt8(mode, 1);
|
||||
|
||||
for (var index = 0; index < valueCount; index++)
|
||||
buffer.writeInt16LE(getBrightness(brightness[index]), 2 + (index * 2));
|
||||
buffer.writeUInt16LE(getBrightness(brightness[index]), 2 + (index * 2));
|
||||
|
||||
return buffer;
|
||||
|
||||
@ -239,5 +243,40 @@ module.exports =
|
||||
else
|
||||
callback(null, true);
|
||||
});
|
||||
},
|
||||
|
||||
updateFirmware: function(data, callback)
|
||||
{
|
||||
if (typeof(data.host) == 'undefined') data.host = '';
|
||||
if (typeof(data.port) == 'undefined') data.port = 80;
|
||||
if (typeof(data.path) == 'undefined') data.path = '';
|
||||
|
||||
var buffer = Buffer.alloc(1 + (data.host.length + 1) + 2 + (data.path.length + 1));
|
||||
buffer.writeUInt8(protocol.Command.UpdateFirmware, 0);
|
||||
var position = 1;
|
||||
|
||||
buffer.writeUInt16LE(data.port, position);
|
||||
position += 2;
|
||||
|
||||
buffer.write(data.host, position);
|
||||
position += data.host.length;
|
||||
buffer.writeUInt8(0, position);
|
||||
position++;
|
||||
|
||||
buffer.write(data.path, position);
|
||||
position += data.path.length;
|
||||
buffer.writeUInt8(0, position);
|
||||
|
||||
requestResponse(buffer,
|
||||
function(reader, error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
var data = { hasUpdates: reader.nextInt8() == 1 };
|
||||
callback(data, false);
|
||||
}
|
||||
else
|
||||
callback(null, true);
|
||||
}, false);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"buffer-reader": "^0.1.0",
|
||||
"express": "^4.15.2"
|
||||
"express": "^4.15.2",
|
||||
"semver-utils": "^1.1.1"
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ module.exports =
|
||||
Ping: 0x01,
|
||||
Reply: 0x02,
|
||||
GetMode: 0x03,
|
||||
SetMode: 0x04
|
||||
SetMode: 0x04,
|
||||
UpdateFirmware: 0xFF
|
||||
},
|
||||
|
||||
|
||||
|
@ -20,23 +20,29 @@
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="header">Mode</div>
|
||||
<div class="tabs">
|
||||
<div class="tab" data-bind="css: { active: tab() == 'mode' }, click: setTab.bind(this, 'mode')">Mode</div>
|
||||
<div class="tab" data-bind="css: { active: tab() == 'settings' }, click: setTab.bind(this, 'settings')">Settings</div>
|
||||
<div class="tab" data-bind="css: { active: tab() == 'update' }, click: setTab.bind(this, 'update')">Update</div>
|
||||
</div>
|
||||
|
||||
<!-- ko if:tab() == 'mode' -->
|
||||
<div class="mode">
|
||||
<div class="selection"><input type="radio" name="mode" value="Static" id="Static" data-bind="checked: mode" /><label for="Static">Static</label></div>
|
||||
<div class="selection"><input type="radio" name="mode" value="Custom" id="Custom" data-bind="checked: mode" /><label for="Custom">Custom</label></div>
|
||||
<div class="selection"><input type="radio" name="mode" value="Alternate" id="Alternate" data-bind="checked: mode" /><label for="Alternate">Alternating</label></div>
|
||||
<!--
|
||||
<div class="selection"><input type="radio" name="mode" value="Slide" id="Slide" data-bind="checked: mode" /><label for="Slide">Sliding</label></div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div class="parameters" data-bind="visible: mode() == 'Static'" style="display: none">
|
||||
<div class="header">Static parameters</div>
|
||||
<div class="parameter">
|
||||
Brightness: <input type="range" min="0" max="4095" data-bind="value: static.brightness, valueUpdate: 'input'" /> <input type="number" min="0" max="4095" data-bind="value: static.brightness" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="parameters" data-bind="visible: mode() == 'Custom'" style="display: none">
|
||||
<div class="header">Custom parameters</div>
|
||||
<!-- ko foreach: custom.brightness -->
|
||||
<div class="parameter">
|
||||
Step <span data-bind="text: $root.custom.brightness().length - $index()"></span>: <input type="range" min="0" max="4095" data-bind="value: $data.value, valueUpdate: 'input'" /> <input type="number" min="0" max="4095" data-bind="value: $data.value" />
|
||||
@ -45,7 +51,6 @@
|
||||
</div>
|
||||
|
||||
<div class="parameters" data-bind="visible: mode() == 'Alternate'" style="display: none">
|
||||
<div class="header">Alternating parameters</div>
|
||||
<div class="parameter">
|
||||
Interval: <input type="number" data-bind="value: alternate.interval" />
|
||||
</div>
|
||||
@ -54,12 +59,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="parameters" data-bind="visible: mode() == 'Slide'" style="display: none">
|
||||
<div class="header">Sliding parameters</div>
|
||||
<div class="parameter">
|
||||
Brightness: <input type="range" min="0" max="4095" data-bind="value: slide.brightness, valueUpdate: 'input'" /> <input type="number" min="0" max="4095" data-bind="value: slide.brightness" />
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<!-- /ko -->
|
||||
|
||||
|
||||
<!-- ko if:tab() == 'settings' -->
|
||||
<!-- /ko -->
|
||||
|
||||
|
||||
<!-- ko if:tab() == 'update' -->
|
||||
<div class="parameters">
|
||||
<div class="parameter">
|
||||
Host: <input type="text" data-bind="value: firmware.host" />
|
||||
</div>
|
||||
<div class="parameter">
|
||||
Port: <input type="number" data-bind="value: firmware.port" min="1" max="65536" />
|
||||
</div>
|
||||
<div class="parameter">
|
||||
Path: <input type="text" data-bind="value: firmware.path" />
|
||||
</div>
|
||||
|
||||
<button data-bind="click: updateFirmware">Check for update</button>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
@ -2,6 +2,8 @@ var StairsViewModel = function()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
self.tab = ko.observable('mode');
|
||||
|
||||
self.mode = ko.observable('Static');
|
||||
self.static =
|
||||
{
|
||||
@ -27,6 +29,13 @@ var StairsViewModel = function()
|
||||
fadeOutTime: ko.observable(0)
|
||||
}
|
||||
|
||||
self.firmware =
|
||||
{
|
||||
host: ko.observable(window.location.hostname),
|
||||
port: ko.observable(window.location.port),
|
||||
path: ko.observable('/checkUpdate')
|
||||
}
|
||||
|
||||
|
||||
self.loading = ko.observable(true);
|
||||
self.updatingFromServer = false;
|
||||
@ -129,6 +138,37 @@ var StairsViewModel = function()
|
||||
setTimeout(self.ping, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
self.setTab = function(tab)
|
||||
{
|
||||
self.tab(tab);
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
self.updateFirmware = function()
|
||||
{
|
||||
$.ajax(
|
||||
{
|
||||
url: '/updateFirmware',
|
||||
data: {
|
||||
host: self.firmware.host(),
|
||||
port: self.firmware.port(),
|
||||
path: self.firmware.path()
|
||||
},
|
||||
dataType: 'json',
|
||||
cache: false
|
||||
})
|
||||
.done(function(data)
|
||||
{
|
||||
alert('Update check running');
|
||||
})
|
||||
.fail(function()
|
||||
{
|
||||
alert('Could not check for firmware update');
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
@ -76,3 +76,29 @@ body
|
||||
{
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
|
||||
.tabs
|
||||
{
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
||||
.tab
|
||||
{
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
padding: 8px;
|
||||
|
||||
border: solid 1px #f0f0f0;
|
||||
}
|
||||
|
||||
|
||||
.tab.active
|
||||
{
|
||||
background-color: #bce3ff;
|
||||
border: solid 1px #0092ff;
|
||||
}
|
4
web/version.js
Normal file
4
web/version.js
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports =
|
||||
{
|
||||
Version: "0.1.0+8"
|
||||
};
|
Loading…
Reference in New Issue
Block a user