Added OTA firmware update

Added range customization (untested)
Added static mode easing (untested)
This commit is contained in:
Mark van Renswoude 2017-04-16 13:40:34 +02:00
parent abd12a3714
commit b39f3ba826
25 changed files with 566 additions and 81 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
src/credentials.h
*.sublime-workspace
web/node_modules/
web/update/

View File

@ -1 +0,0 @@
@platformio run

3
build.ps1 Normal file
View File

@ -0,0 +1,3 @@
& .\updateversion.ps1
& platformio run
Copy-Item .\.pioenvs\esp12e\firmware.elf .\web\update\

View File

@ -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
=====

View File

@ -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

View File

@ -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;
}

View File

@ -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));
}
};

View File

@ -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));
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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;
};

View File

@ -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();
}

View File

@ -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
View File

@ -0,0 +1,6 @@
#ifndef __Version
#define __Version
static const char* FirmwareVersion = "0.1.0+8";
#endif

23
updateversion.ps1 Normal file
View 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

View File

@ -1 +0,0 @@
@platformio run --target upload

2
upload.ps1 Normal file
View File

@ -0,0 +1,2 @@
& .\updateversion.ps1
& platformio run --target upload

View File

@ -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'));

View File

@ -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);
}
}

View File

@ -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"
}
}

View File

@ -6,7 +6,8 @@ module.exports =
Ping: 0x01,
Reply: 0x02,
GetMode: 0x03,
SetMode: 0x04
SetMode: 0x04,
UpdateFirmware: 0xFF
},

View File

@ -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>

View File

@ -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');
});
};
};

View File

@ -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
View File

@ -0,0 +1,4 @@
module.exports =
{
Version: "0.1.0+8"
};