diff --git a/.gitignore b/.gitignore index ca51fc1..f16f23d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ src/credentials.h *.sublime-workspace web/node_modules/ +web/update/ diff --git a/build.bat b/build.bat deleted file mode 100644 index e63c79f..0000000 --- a/build.bat +++ /dev/null @@ -1 +0,0 @@ -@platformio run \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..e1c1404 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,3 @@ +& .\updateversion.ps1 +& platformio run +Copy-Item .\.pioenvs\esp12e\firmware.elf .\web\update\ \ No newline at end of file diff --git a/docs/protocol.md b/docs/protocol.md index 436ac43..ffd7339 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -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:
_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:
+_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:
+1 if succesful, 0 if no updates are available. + + Modes ===== diff --git a/src/config.h b/src/config.h index ed66d1c..5f4266b 100644 --- a/src/config.h +++ b/src/config.h @@ -1,9 +1,14 @@ #ifndef __Config #define __Config +#include #include #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 \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 40b52a3..b99e866 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #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; } diff --git a/src/modes/base.h b/src/modes/base.h index f25e0ed..255756e 100644 --- a/src/modes/base.h +++ b/src/modes/base.h @@ -3,6 +3,7 @@ #include #include +#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(data); + _d("Reading parameters, size "); + _dln(sizeof(T)); + memcpy(&this->parameters, data, sizeof(T)); } virtual void write(Stream* stream) { - stream->write(reinterpret_cast(&this->parameters), sizeof(T)); + _d("Writing parameters, size "); + _dln(sizeof(T)); + stream->write((uint8_t*)&this->parameters, sizeof(T)); } }; diff --git a/src/modes/custom.cpp b/src/modes/custom.cpp index 2430e33..49119e2 100644 --- a/src/modes/custom.cpp +++ b/src/modes/custom.cpp @@ -13,7 +13,7 @@ void CustomMode::read(uint8_t* data) void CustomMode::write(Stream* stream) { - stream->write(reinterpret_cast(&this->values), sizeof(this->values)); + stream->write((uint8_t*)&this->values, sizeof(this->values)); } diff --git a/src/modes/static.cpp b/src/modes/static.cpp index 7b5fc88..26144bf 100644 --- a/src/modes/static.cpp +++ b/src/modes/static.cpp @@ -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); } \ No newline at end of file diff --git a/src/modes/static.h b/src/modes/static.h index c06edc3..8db5895 100644 --- a/src/modes/static.h +++ b/src/modes/static.h @@ -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 { + 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); diff --git a/src/protocol.h b/src/protocol.h index 789893e..d64defd 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -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; }; diff --git a/src/stairs.cpp b/src/stairs.cpp index 6d95f8b..0594b45 100644 --- a/src/stairs.cpp +++ b/src/stairs.cpp @@ -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(&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(&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(&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(&header), sizeof(Header)); - f.write(reinterpret_cast(&this->ranges), sizeof(this->ranges)); + f.write((uint8_t*)&header, sizeof(Header)); + f.write((uint8_t*)&this->ranges, sizeof(this->ranges)); f.close(); } \ No newline at end of file diff --git a/src/stairs.h b/src/stairs.h index 8b543e8..e98a171 100644 --- a/src/stairs.h +++ b/src/stairs.h @@ -19,7 +19,7 @@ class Stairs : public IStairs PCA9685* pwmDriver; bool useScaling; - Range* ranges[StepCount]; + Range ranges[StepCount]; protected: void readRange(); diff --git a/src/version.h b/src/version.h new file mode 100644 index 0000000..e73acae --- /dev/null +++ b/src/version.h @@ -0,0 +1,6 @@ +#ifndef __Version +#define __Version + +static const char* FirmwareVersion = "0.1.0+8"; + +#endif diff --git a/updateversion.ps1 b/updateversion.ps1 new file mode 100644 index 0000000..b183a54 --- /dev/null +++ b/updateversion.ps1 @@ -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 diff --git a/upload.bat b/upload.bat deleted file mode 100644 index 73e7389..0000000 --- a/upload.bat +++ /dev/null @@ -1 +0,0 @@ -@platformio run --target upload \ No newline at end of file diff --git a/upload.ps1 b/upload.ps1 new file mode 100644 index 0000000..32ed813 --- /dev/null +++ b/upload.ps1 @@ -0,0 +1,2 @@ +& .\updateversion.ps1 +& platformio run --target upload \ No newline at end of file diff --git a/web/app.js b/web/app.js index 3dc80ef..a75b138 100644 --- a/web/app.js +++ b/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')); diff --git a/web/client.js b/web/client.js index fcc8673..3776861 100644 --- a/web/client.js +++ b/web/client.js @@ -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); } } \ No newline at end of file diff --git a/web/package.json b/web/package.json index 77a8089..52f2816 100644 --- a/web/package.json +++ b/web/package.json @@ -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" } } diff --git a/web/protocol.js b/web/protocol.js index 667173e..f2d5b98 100644 --- a/web/protocol.js +++ b/web/protocol.js @@ -6,7 +6,8 @@ module.exports = Ping: 0x01, Reply: 0x02, GetMode: 0x03, - SetMode: 0x04 + SetMode: 0x04, + UpdateFirmware: 0xFF }, diff --git a/web/static/index.html b/web/static/index.html index 1a88fdb..4263b1e 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -20,23 +20,29 @@
-
Mode
+
+
Mode
+
Settings
+
Update
+
+ +
+
+ + + + + + + + + +
+
+ Host: +
+
+ Port: +
+
+ Path: +
+ + +
+
diff --git a/web/static/script.js b/web/static/script.js index 3bc9a0e..d6b602e 100644 --- a/web/static/script.js +++ b/web/static/script.js @@ -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'); + }); + }; }; diff --git a/web/static/style.css b/web/static/style.css index a0a0704..4c1fe3a 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -75,4 +75,30 @@ body .parameter { 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; } \ No newline at end of file diff --git a/web/version.js b/web/version.js new file mode 100644 index 0000000..42fe104 --- /dev/null +++ b/web/version.js @@ -0,0 +1,4 @@ +module.exports = +{ + Version: "0.1.0+8" +};