Changed from UDP to HTTP
Implemented access point, station mode and basic web configuration page Actual lighting control is completely broken
This commit is contained in:
parent
3099dd8a0e
commit
f87a8b6eb7
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,9 +1,4 @@
|
||||
.pioenvs
|
||||
.piolibdeps
|
||||
src/credentials.h
|
||||
*.sublime-workspace
|
||||
web/node_modules/
|
||||
web/update/
|
||||
src/version.h
|
||||
web/version.js
|
||||
web/static/bower_components/
|
||||
node_modules
|
||||
|
57
DEVELOPING.md
Normal file
57
DEVELOPING.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Stairs
|
||||
|
||||
This Stairs firmware was developed using [PlatformIO Core](http://platformio.org/). You can probably use the PlatformIO IDE as well, although I have no experience using it, so this guide will only use the command line tools.
|
||||
|
||||
## Programming the ESP8266
|
||||
|
||||
You can either use an ESP8266 module with built-in USB like the Wemos D1, a programming fixture (my method of choice, search for "esp8266 fixture" on Google or AliExpress) or wire it up yourself using a CH340 or FTDI USB-to-serial module.
|
||||
|
||||
To upload the code, open a console, go to the Stairs folder and run:
|
||||
|
||||
```
|
||||
platformio run -t upload
|
||||
```
|
||||
|
||||
It should auto-detect the USB COM port.
|
||||
|
||||
|
||||
## Frontend development
|
||||
|
||||
The frontend is compiled into C++ source files so that all the files can be served directly from the ESP8266, since there is no internet connection when running in access point mode. These steps are performed by a [Gulp script](https://gulpjs.com/). The Gulp script also updates the version based on the [GitVersion](http://gitversion.readthedocs.io/en/stable/) of the working copy.
|
||||
|
||||
Note that GitVersion requires Windows, so some changes are probably required if you want to build on a different platform.
|
||||
|
||||
To get started:
|
||||
|
||||
1. Install [Node.js](https://nodejs.org/en/)
|
||||
1. Install the [GitVersion command line](http://gitversion.readthedocs.io/en/stable/usage/command-line/) tool
|
||||
1. Open a command line and navigate to the Stairs folder
|
||||
1. Run ```npm update``` to install all the dependencies
|
||||
|
||||
|
||||
### Compiling the assets
|
||||
|
||||
Run ```gulp``` to compile the SASS files, and embed the CSS, JavaScript, images and HTML into C++ header files located in the src\assets folder.
|
||||
|
||||
You may need to run ```npm install -g gulp``` to install Gulp into the global Node packages.
|
||||
|
||||
|
||||
### Development server
|
||||
|
||||
To make it easier to develop the frontend, a development server is included which serves the webpages and acts as a mock service for the API.
|
||||
|
||||
To start the development server, run:
|
||||
|
||||
```node devserver.js```
|
||||
|
||||
You can now open the frontend on [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
If you make any changes to the SCSS files, make sure to run ```gulp compileSass``` and ```gulp compileJS``` to update the CSS/JS files so your changes are visible in the development server. To keep gulp running and watch for changes in the SCSS and JS files, run ```gulp watch```
|
||||
|
||||
|
||||
## Building and/or uploading
|
||||
|
||||
To rebuild all the assets and compile or upload the source in one go, two tasks have been added to the gulpfile.js:
|
||||
|
||||
1. ```gulp build``` first runs all the tasks run by a regular ```gulp```, then builds the source code using ```platformio run```
|
||||
1. ```gulp upload``` is similar, but executes ```platformio run -t upload``` to directly upload the newly compiled source to the ESP8266
|
17
README.md
Normal file
17
README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Stairs
|
||||
|
||||
ESP8266 firmware for controlling up to 16 LED strips on a flight of stairs.
|
||||
|
||||
## Features
|
||||
|
||||
- Configurable using WiFi (can act as an access point or connect to a router)
|
||||
- Turn on or off at specific times
|
||||
- Turn on when movement is detected by connecting one or two PIR sensors
|
||||
- ReST API for configuration and controlling the lights
|
||||
|
||||
Most notably it does not support RGB LED strips out of the box, but feel free to add support if you're up to the task!
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Wiring information](WIRING.md)
|
||||
- [Programming the ESP8266 and/or modifying the source](DEVELOPING.md)
|
@ -5,18 +5,8 @@
|
||||
"path": ".",
|
||||
"file_exclude_patterns": ["*.sublime-project"]
|
||||
}
|
||||
]/*,
|
||||
"build_systems":
|
||||
[
|
||||
{
|
||||
"name": "PlatformIO - Build",
|
||||
"cmd": ["platformio", "run"],
|
||||
"working_dir": "$project_path"
|
||||
},
|
||||
{
|
||||
"name": "PlatformIO - Upload",
|
||||
"cmd": ["platformio", "run", "--target", "upload"],
|
||||
"working_dir": "$project_path"
|
||||
}
|
||||
]*/
|
||||
],
|
||||
"completions":[
|
||||
["t", "{{ \\$t('${1:}') }}"]
|
||||
]
|
||||
}
|
4
WIRING.md
Normal file
4
WIRING.md
Normal file
@ -0,0 +1,4 @@
|
||||
## Bill of materials
|
||||
|
||||
1. ESP8266 module (I based mine on a vanilla ESP8266 ESP-12F, but friendlier modules like the Wemos D1 should work as well)
|
||||
1. PCA9685 16-channel PWM module
|
@ -1,3 +0,0 @@
|
||||
& .\updateversion.ps1
|
||||
& platformio run
|
||||
Copy-Item .\.pioenvs\esp12e\firmware.bin .\web\update\
|
@ -1,97 +0,0 @@
|
||||
caseBottom();
|
||||
|
||||
$fa = 1;
|
||||
$fs = 1;
|
||||
|
||||
module caseBottom()
|
||||
{
|
||||
innerX = 80;
|
||||
innerY = 70;
|
||||
innerZ = 35;
|
||||
wallThickness = 2;
|
||||
|
||||
totalX = innerX + (2 * wallThickness);
|
||||
totalY = innerY + (2 * wallThickness);
|
||||
totalZ = innerZ + wallThickness;
|
||||
|
||||
difference()
|
||||
{
|
||||
cube([totalX, totalY, totalZ]);
|
||||
translate([wallThickness, wallThickness, wallThickness])
|
||||
cube([innerX, innerY, totalZ]);
|
||||
|
||||
// Hole for the LED cables
|
||||
translate([-1, totalY / 2, 25])
|
||||
rotate([0, 90, 0])
|
||||
cylinder(d = 15, h = wallThickness + 2);
|
||||
|
||||
// Hole for the power cable
|
||||
translate([62, totalY + 1, wallThickness + 2.5])
|
||||
rotate([90, 0, 0])
|
||||
cylinder(d = 5, h = wallThickness + 2);
|
||||
}
|
||||
|
||||
translate([wallThickness, wallThickness, wallThickness])
|
||||
{
|
||||
translate([4, 4, 0])
|
||||
PCA9685Mount();
|
||||
|
||||
translate([40, 20, 0])
|
||||
ESP8266PlusPowerMount();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module PCA9685Mount()
|
||||
{
|
||||
totalX = 25.5;
|
||||
totalY = 62.5;
|
||||
pinDistanceX = 19;
|
||||
pinDistanceY = 56;
|
||||
pinDiameter = 2.3;
|
||||
pinHeight = 3;
|
||||
supportDiameter = 4;
|
||||
supportHeight = 4;
|
||||
|
||||
offsetX = (totalX - pinDistanceX) / 2;
|
||||
offsetY = (totalY - pinDistanceY) / 2;
|
||||
|
||||
for (x = [offsetX, offsetX + pinDistanceX])
|
||||
for (y = [offsetY, offsetY + pinDistanceY])
|
||||
translate([x, y, 0])
|
||||
{
|
||||
cylinder(d = supportDiameter, h = supportHeight);
|
||||
translate([0, 0, supportHeight])
|
||||
cylinder(d = pinDiameter, h = pinHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// I didn't put any actual mount points in the perfboard,
|
||||
// so I'll settle for some corner pieces to align it and
|
||||
// use the ultimate maker's friend: hot glue.
|
||||
module ESP8266PlusPowerMount()
|
||||
{
|
||||
innerX = 34;
|
||||
innerY = 35;
|
||||
cornerSize = 6;
|
||||
cornerThickness = 2;
|
||||
cornerHeight = 2;
|
||||
|
||||
totalX = innerX + (2 * cornerThickness);
|
||||
totalY = innerY + (2 * cornerThickness);
|
||||
|
||||
difference()
|
||||
{
|
||||
cube([totalX, totalY, cornerHeight]);
|
||||
|
||||
translate([cornerThickness, cornerThickness, -1])
|
||||
cube([innerX, innerY, cornerHeight + 2]);
|
||||
|
||||
translate([-1, cornerSize, -1])
|
||||
cube([totalX + 2, totalY - (2 * cornerSize), cornerHeight + 2]);
|
||||
|
||||
translate([cornerSize, -1, -1])
|
||||
cube([totalX - (2 * cornerSize), totalY + 2, cornerHeight + 2]);
|
||||
}
|
||||
}
|
60
devserver.js
Normal file
60
devserver.js
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
app.use(express.static('web'));
|
||||
app.use(express.static('web/dist'));
|
||||
|
||||
app.get('/api/version', function(req, res)
|
||||
{
|
||||
res.send({
|
||||
systemID: 'dev-server',
|
||||
version: 'dev-server'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/connection', function(req, res)
|
||||
{
|
||||
res.send({
|
||||
hostname: 'dev-server',
|
||||
accesspoint: true,
|
||||
station: true,
|
||||
ssid: 'MyWiFiSSID',
|
||||
password: 'supersecret',
|
||||
dhcp: true,
|
||||
ip: '192.168.1.234',
|
||||
subnetmask: '255.255.255.0',
|
||||
gateway: '192.168.1.0'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/connection/status', function(req, res)
|
||||
{
|
||||
res.send({
|
||||
"ap": {
|
||||
"enabled": true,
|
||||
"ip": "192.168.4.1"
|
||||
},
|
||||
"station": {
|
||||
"enabled": true,
|
||||
"status": 1,
|
||||
"ip": "0.0.0.0"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/connection', function(req, res)
|
||||
{
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.listen(3000, function()
|
||||
{
|
||||
console.log('Development server listening on port 3000')
|
||||
console.log('Press Ctrl-C to stop')
|
||||
});
|
170
docs/protocol.md
170
docs/protocol.md
@ -1,170 +0,0 @@
|
||||
The Stairs firmware on the ESP8266 can be accessed using a custom light-weight UDP protocol. It is not intended to be accessed directly from the internet, and thus there is no security on the device itself. This is by design. Authentication should in my opinion be handled by a device with more processing power, such as a Raspberry Pi.
|
||||
|
||||
A Node.js application is included which provides a ReST interface. It is also not intended to be accessible from the internet, and does not provide any authentication either!
|
||||
|
||||
|
||||
Protocol
|
||||
========
|
||||
|
||||
The default port for UDP communication is 3126. Every request message will result in a response message which is either an error, the requested information or a confirmation of the newly stored data. Each message should be a separate packet.
|
||||
|
||||
16-bit (word) values are expected in little endian order (least significant byte first).
|
||||
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
The first byte of a request is the command. Further data depends on the command.
|
||||
|
||||
| Command | Name |
|
||||
| ------- | ---- |
|
||||
| 0x01 | Ping |
|
||||
| 0x03 | GetMode |
|
||||
| 0x04 | SetMode |
|
||||
| 0x05 | GetRange |
|
||||
| 0x06 | SetRange |
|
||||
| 0xFF | UpdateFirmware |
|
||||
|
||||
|
||||
Response
|
||||
--------
|
||||
|
||||
A response is sent to the source address and port and starts with the Reply command.
|
||||
|
||||
| Command | Name |
|
||||
| ------- | ---- |
|
||||
| 0x02 | Reply |
|
||||
|
||||
|
||||
The second byte is the request command for which this reply is intended, or Error if the request command was not recognized or contained invalid parameters.
|
||||
|
||||
| Command | Name |
|
||||
| ------- | ---- |
|
||||
| 0x00 | Error |
|
||||
|
||||
In the case of an Error, the third byte will be the actual request command (or unrecognized value).
|
||||
|
||||
|
||||
### Ping
|
||||
A no-op command which can be used to tell if the device is responding. Returns the number of steps.
|
||||
|
||||
Input parameters:<br>
|
||||
_none_
|
||||
|
||||
Output parameters:<br>
|
||||
**steps** (byte): The number of steps.
|
||||
|
||||
|
||||
### GetMode
|
||||
Returns the current mode.
|
||||
|
||||
Input parameters:<br>
|
||||
_none_
|
||||
|
||||
Output parameters:<br>
|
||||
**mode** (byte): The identifier of the current mode. See Modes below.<br>
|
||||
**data** (0..n bytes): The parameters specific to the current mode. See Modes below.
|
||||
|
||||
|
||||
### SetMode
|
||||
Changed the current mode.
|
||||
|
||||
Input parameters:<br>
|
||||
**mode** (byte): The identifier of the current mode.<br>
|
||||
**data** (0..n bytes): The parameters specific to the current mode.
|
||||
|
||||
Output parameters:<br>
|
||||
_Same as input parameters_
|
||||
|
||||
|
||||
### GetRange
|
||||
Gets the current range configuration. Each step has it's own parameters which adjust the PWM curve, which can be used to compensate for the differences in LED strips or signal strength.
|
||||
|
||||
The ranges are stored on the ESP8266's filesystem and will be restored after a power loss.
|
||||
|
||||
Input parameters:<br>
|
||||
_none_
|
||||
|
||||
Output parameters:<br>
|
||||
*useScaling* (byte): 0 (off) or 1 (on), default 1. If enabled, the brightness value will be converted to a PWM value using an exponential function. If disabled, the brightness is used as is (but still accounting for rangeStart/rangeEnd).
|
||||
_repeated for each step:_<br>
|
||||
*rangeStart* (word): value in range 0 (off) to 4095 (fully on), default 0. Determines the minimum PWM on value for a brightness of 1.
|
||||
*rangeEnd* (word): value in range 0 (off) to 4095 (fully on), default 4094. Determines the maximum PWM on value for a brightness of 4094.
|
||||
|
||||
|
||||
### SetRange
|
||||
Sets the current range configuration.
|
||||
|
||||
Input parameters:<br>
|
||||
_See GetRange_
|
||||
|
||||
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
|
||||
=====
|
||||
|
||||
| Mode | Name |
|
||||
| ---- | ---- |
|
||||
| 0x01 | Static |
|
||||
| 0x02 | Custom |
|
||||
| 0x03 | Alternate |
|
||||
| 0x04 | Slide |
|
||||
|
||||
### Static
|
||||
Sets all steps to the same brightness.
|
||||
|
||||
Parameters:<br>
|
||||
**brightness** (word): value in range 0 (off) to 4095 (fully on).
|
||||
**easeTime** (word): the time in milliseconds to ease into the new brightness value (only applies if the mode was already set to static before).
|
||||
|
||||
|
||||
### Custom
|
||||
Sets the brightness for each of the steps individually.
|
||||
|
||||
Parameters:<br>
|
||||
**brightness** (word[stepCount]): array of brightness values in range 0 - 4095. The number of values must be equal to the number of steps are reported in the Ping response. Bottom step first.
|
||||
|
||||
|
||||
### Alternate
|
||||
Alternates between even and odd steps being lit. Bring out our next contestant!
|
||||
|
||||
Parameters:<br>
|
||||
**interval** (word): The time each set of steps is lit in milliseconds.<br>
|
||||
**brightness** (word): value in range 0 (off) to 4095 (fully on).
|
||||
|
||||
|
||||
### Slide
|
||||
Lights one step at a time, moving up or down.
|
||||
|
||||
Parameters:<br>
|
||||
**interval** (word): How long each step is lit before moving to the next.<br>
|
||||
**brightness** (word): value in range 0 (off) to 4095 (fully on).
|
||||
**direction** (byte): Determines the starting step / direction. Either Bottom/Up (0) or Top/Down (1).<br>
|
||||
**fadeOutTime** (word): If greater than 0 each step will fade out instead of turning off instantly after moving to the next. Specified in milliseconds.
|
168
gulp-cppstringify.js
Normal file
168
gulp-cppstringify.js
Normal file
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// Borrowed heavily from gulp-concat:
|
||||
// https://github.com/contra/gulp-concat/
|
||||
//
|
||||
// It's very much hardcoded for the ESP8266 Arduino at the moment,
|
||||
// but feel free to hack away at it if you need it for other purposes!
|
||||
var through = require('through2');
|
||||
var path = require('path');
|
||||
var File = require('vinyl');
|
||||
var _ = require('lodash');
|
||||
var Readable = require('stream').Readable;
|
||||
|
||||
|
||||
function escapeContent(content, lineLength)
|
||||
{
|
||||
var lineRegexp = new RegExp('(.{1,' + (lineLength - 1) + '}[^\\\\])', 'g');
|
||||
|
||||
return content
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\r?\n/g, '\\r\\n')
|
||||
.replace(lineRegexp, ' "$1"\r\n')
|
||||
.replace(/\r\n$/, '');
|
||||
};
|
||||
|
||||
|
||||
function escapeContentAsByteArray(content, lineLength)
|
||||
{
|
||||
var bytesPerLine = Math.floor(lineLength / 5);
|
||||
var lineRegexp = new RegExp('((?:0x..,){1,' + bytesPerLine + '})', 'g');
|
||||
|
||||
return content
|
||||
.replace(/(.{2})/g, '0x$1,')
|
||||
.replace(lineRegexp, ' $1\r\n')
|
||||
.replace(/,\r\n$/, '');
|
||||
};
|
||||
|
||||
|
||||
function encodeFile(file, opts)
|
||||
{
|
||||
var variableName;
|
||||
|
||||
if (opts.map.hasOwnProperty(file.relative))
|
||||
variableName = opts.map[file.relative];
|
||||
else
|
||||
variableName = file.relative.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||
|
||||
if (variableName === null)
|
||||
return '';
|
||||
|
||||
variableName = opts.variablePrefix + variableName;
|
||||
|
||||
var escapedContent;
|
||||
var output;
|
||||
|
||||
if (opts.byteArray)
|
||||
{
|
||||
escapedContent = escapeContentAsByteArray(file.contents.toString('hex'), opts.lineLength);
|
||||
output = "const uint8_t " + variableName + "[] PROGMEM = {\r\n" + escapedContent + "};\r\n\r\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
escapedContent = escapeContent(file.contents.toString('utf-8'), opts.lineLength);
|
||||
output = "const char " + variableName + "[] PROGMEM = \r\n" + escapedContent + ";\r\n\r\n";
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
module.exports = function(file, opts)
|
||||
{
|
||||
if (!file)
|
||||
throw new Error('gulp-cppstringify: Missing file option');
|
||||
|
||||
opts = _.extend({
|
||||
map: [],
|
||||
headerDefineName: '__Embedded',
|
||||
variablePrefix: 'Embedded',
|
||||
lineLength: 100,
|
||||
byteArray: false
|
||||
}, opts || {});
|
||||
|
||||
var fileName;
|
||||
var latestFile = false;
|
||||
var latestMod = 0;
|
||||
var output = null;
|
||||
|
||||
|
||||
if (typeof file === 'string')
|
||||
fileName = file;
|
||||
else if (typeof file.path === 'string')
|
||||
fileName = path.basename(file.path);
|
||||
else
|
||||
throw new Error('gulp-cppstringify: Missing path in file options');
|
||||
|
||||
|
||||
function bufferContents(file, enc, cb)
|
||||
{
|
||||
if (file.isNull())
|
||||
{
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.isStream())
|
||||
{
|
||||
this.emit('error', new Error('gulp-cppstringify: Streaming not supported'));
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!latestMod || file.stat && file.stat.mtime > latestMod)
|
||||
{
|
||||
latestFile = file;
|
||||
latestMod = file.stat && file.stat.mtime;
|
||||
}
|
||||
|
||||
if (output == null)
|
||||
{
|
||||
output = new Readable();
|
||||
output._read = function noop() {};
|
||||
|
||||
output.push("#ifndef " + opts.headerDefineName + "\r\n");
|
||||
output.push("#define " + opts.headerDefineName + "\r\n\r\n");
|
||||
output.push("#include <pgmspace.h>\r\n\r\n");
|
||||
}
|
||||
|
||||
output.push(encodeFile(file, opts));
|
||||
cb();
|
||||
}
|
||||
|
||||
|
||||
function endStream(cb)
|
||||
{
|
||||
if (!latestFile)
|
||||
{
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
var headerFile;
|
||||
|
||||
if (typeof file === 'string')
|
||||
{
|
||||
headerFile = latestFile.clone({contents: false});
|
||||
headerFile.path = path.join(latestFile.base, file);
|
||||
}
|
||||
else
|
||||
headerFile = new File(file);
|
||||
|
||||
output.push("#endif\r\n");
|
||||
output.push(null);
|
||||
headerFile.contents = output;
|
||||
|
||||
this.push(headerFile);
|
||||
cb();
|
||||
}
|
||||
|
||||
return through.obj(bufferContents, endStream);
|
||||
};
|
244
gulpfile.js
Normal file
244
gulpfile.js
Normal file
@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
const htmlmin = require('gulp-htmlmin');
|
||||
const cppstringify = require('./gulp-cppstringify');
|
||||
const fs = require('fs');
|
||||
const plumber = require('gulp-plumber');
|
||||
const sass = require('gulp-sass');
|
||||
const cleanCSS = require('gulp-clean-css');
|
||||
const watch = require('gulp-debounced-watch');
|
||||
const uglify = require('gulp-uglify');
|
||||
const concat = require('gulp-concat');
|
||||
const print = require('gulp-print');
|
||||
|
||||
|
||||
const config = {
|
||||
assetsPath: 'web/',
|
||||
distPath: 'web/dist/',
|
||||
outputPath: 'src/assets/'
|
||||
};
|
||||
|
||||
|
||||
const HTMLMap = {
|
||||
'index.html': 'Index'
|
||||
};
|
||||
|
||||
const JSMap = {
|
||||
'bundle.js': 'BundleJS'
|
||||
};
|
||||
|
||||
const CSSMap = {
|
||||
'bundle.css': 'BundleCSS'
|
||||
};
|
||||
|
||||
|
||||
// There is an issue in the AsyncWebServer where it's apparantly running
|
||||
// out of memory on simultaneous requests. We'll work around it by
|
||||
// merging all the JS into one big file.
|
||||
//
|
||||
// https://github.com/me-no-dev/ESPAsyncWebServer/issues/256
|
||||
const JSSrc = [
|
||||
'node_modules/axios/dist/axios.min.js',
|
||||
'node_modules/vue/dist/vue.min.js',
|
||||
'node_modules/vue-i18n/dist/vue-i18n.min.js',
|
||||
'web/lang.js',
|
||||
'web/app.js'
|
||||
];
|
||||
|
||||
const SCSSSrc = [
|
||||
'web/site.scss'
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
gulp.task('default',
|
||||
[
|
||||
'embedAssets'
|
||||
],
|
||||
function(){});
|
||||
|
||||
|
||||
gulp.task('watch',
|
||||
[
|
||||
'compileScss',
|
||||
'compileJS'
|
||||
],
|
||||
function()
|
||||
{
|
||||
watch(config.assetsPath + '*.scss', function() { gulp.start('compileScss'); });
|
||||
watch(config.assetsPath + '*.js', function() { gulp.start('compileJS'); });
|
||||
});
|
||||
|
||||
|
||||
|
||||
gulp.task('embedHTML', function()
|
||||
{
|
||||
return gulp.src(config.assetsPath + '*.html')
|
||||
.pipe(print(function(filepath) { return 'HTML: ' + filepath }))
|
||||
.pipe(htmlmin({
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true
|
||||
}))
|
||||
.pipe(cppstringify('html.h', {
|
||||
headerDefineName: '__assets_html',
|
||||
map: HTMLMap
|
||||
}))
|
||||
.pipe(gulp.dest(config.outputPath));
|
||||
});
|
||||
|
||||
|
||||
gulp.task('compileScss', function()
|
||||
{
|
||||
return gulp.src(SCSSSrc)
|
||||
.pipe(plumber({
|
||||
errorHandler: function (error)
|
||||
{
|
||||
console.log(error.toString());
|
||||
this.emit('end');
|
||||
}}))
|
||||
.pipe(print(function(filepath) { return 'SCSS: ' + filepath }))
|
||||
.pipe(sass({
|
||||
includePaths: ['node_modules/milligram/src']
|
||||
}))
|
||||
.pipe(cleanCSS({compatibility: 'ie9'}))
|
||||
.pipe(concat('bundle.css'))
|
||||
.pipe(gulp.dest(config.distPath));
|
||||
});
|
||||
|
||||
|
||||
gulp.task('compileJS', function()
|
||||
{
|
||||
return gulp.src(JSSrc)
|
||||
.pipe(plumber({
|
||||
errorHandler: function (error)
|
||||
{
|
||||
console.log(error.toString());
|
||||
this.emit('end');
|
||||
}}))
|
||||
.pipe(print(function(filepath) { return 'JS: ' + filepath }))
|
||||
.pipe(concat('bundle.js'))
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest(config.distPath));
|
||||
});
|
||||
|
||||
|
||||
gulp.task('embedJS', ['compileJS'], function()
|
||||
{
|
||||
return gulp.src([config.distPath + 'bundle.js'])
|
||||
.pipe(cppstringify('js.h', {
|
||||
headerDefineName: '__assets_js',
|
||||
map: JSMap
|
||||
}))
|
||||
.pipe(gulp.dest(config.outputPath));
|
||||
});
|
||||
|
||||
|
||||
gulp.task('embedCSS', ['compileScss'], function()
|
||||
{
|
||||
return gulp.src([config.distPath + 'bundle.css'])
|
||||
.pipe(cppstringify('css.h', {
|
||||
headerDefineName: '__embed_css',
|
||||
map: CSSMap
|
||||
}))
|
||||
.pipe(gulp.dest(config.outputPath));
|
||||
});
|
||||
|
||||
|
||||
gulp.task('embedVersion', function()
|
||||
{
|
||||
var versionData = '';
|
||||
const cmd = spawn('gitversion');
|
||||
|
||||
cmd.stdout.on('data', function(data)
|
||||
{
|
||||
versionData += data;
|
||||
});
|
||||
|
||||
cmd.stderr.on('data', function(data)
|
||||
{
|
||||
console.log(data.toString().trim());
|
||||
});
|
||||
|
||||
cmd.on('exit', function(code)
|
||||
{
|
||||
if (code != 0) return;
|
||||
|
||||
var version = JSON.parse(versionData);
|
||||
var headerFile = "#ifndef __assets_version\r\n";
|
||||
headerFile += "#define __assets_version\r\n\r\n";
|
||||
headerFile += "const uint8_t VersionMajor = " + version.Major + ";\r\n";
|
||||
headerFile += "const uint8_t VersionMinor = " + version.Minor + ";\r\n";
|
||||
headerFile += "const uint8_t VersionPatch = " + version.Patch + ";\r\n";
|
||||
headerFile += "const uint8_t VersionMetadata = " + version.BuildMetaData + ";\r\n";
|
||||
|
||||
headerFile += "const char VersionBranch[] = \"" + version.BranchName + "\";\r\n";
|
||||
headerFile += "const char VersionSha[] = \"" + version.Sha + "\";\r\n";
|
||||
|
||||
headerFile += "const char VersionSemVer[] = \"" + version.SemVer + "\";\r\n";
|
||||
headerFile += "const char VersionFullSemVer[] = \"" + version.FullSemVer + "\";\r\n";
|
||||
|
||||
headerFile += "const char VersionCommitDate[] = \"" + version.CommitDate + "\";\r\n";
|
||||
|
||||
headerFile += "\r\n#endif\r\n";
|
||||
|
||||
fs.writeFile(config.outputPath + 'version.h', headerFile, function(err)
|
||||
{
|
||||
if (err) throw err;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
gulp.task('embedAssets', ['embedHTML', 'embedJS', 'embedCSS', 'embedVersion'], function() { })
|
||||
|
||||
|
||||
|
||||
// PlatformIO
|
||||
const spawn = require('child_process').spawn;
|
||||
const argv = require('yargs').argv;
|
||||
|
||||
var platformio = function(target)
|
||||
{
|
||||
var args = ['run'];
|
||||
if ("e" in argv)
|
||||
{
|
||||
args.push('-e');
|
||||
args.push(argv.e);
|
||||
}
|
||||
|
||||
if ("p" in argv)
|
||||
{
|
||||
args.push('--upload-port');
|
||||
args.push(argv.p);
|
||||
}
|
||||
|
||||
if (target)
|
||||
{
|
||||
args.push('-t');
|
||||
args.push(target);
|
||||
}
|
||||
|
||||
const cmd = spawn('platformio', args);
|
||||
cmd.stdout.on('data', function(data)
|
||||
{
|
||||
console.log(data.toString().trim());
|
||||
});
|
||||
|
||||
cmd.stderr.on('data', function(data)
|
||||
{
|
||||
console.log(data.toString().trim());
|
||||
});
|
||||
}
|
||||
|
||||
gulp.task('upload', ['embedHTML', 'embedAssets'], function() { platformio('upload'); });
|
||||
gulp.task('build', ['embedHTML', 'embedAssets'], function() { platformio(false); });
|
40
package.json
Normal file
40
package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "stairs",
|
||||
"version": "2.0.0",
|
||||
"description": "Stairs",
|
||||
"main": "gulpfile.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "node devserver.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.x2software.net/pub/Stairs.git"
|
||||
},
|
||||
"author": "Mark van Renswoude <mark@x2software.net>",
|
||||
"license": "Unlicense",
|
||||
"devDependencies": {
|
||||
"axios": "^0.17.1",
|
||||
"child_process": "^1.0.2",
|
||||
"express": "^4.16.2",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-clean-css": "^3.9.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-debounced-watch": "^1.0.4",
|
||||
"gulp-htmlmin": "^3.0.0",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
"gulp-print": "^2.0.1",
|
||||
"gulp-sass": "^3.1.0",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"milligram": "^1.3.0",
|
||||
"path": "^0.12.7",
|
||||
"through2": "^2.0.3",
|
||||
"vinyl": "^2.1.0",
|
||||
"vue": "^2.5.13",
|
||||
"yargs": "^10.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue-i18n": "^7.3.3"
|
||||
}
|
||||
}
|
@ -9,7 +9,11 @@
|
||||
; http://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:esp12e]
|
||||
platform = espressif8266_stage
|
||||
platform = https://github.com/platformio/platform-espressif8266.git#feature/stage
|
||||
board = esp12e
|
||||
framework = arduino
|
||||
upload_speed = 115200
|
||||
upload_speed = 115200
|
||||
lib_deps =
|
||||
Hash
|
||||
ArduinoJson
|
||||
ESP Async WebServer
|
111
src/assets/css.h
Normal file
111
src/assets/css.h
Normal file
@ -0,0 +1,111 @@
|
||||
#ifndef __embed_css
|
||||
#define __embed_css
|
||||
|
||||
#include <pgmspace.h>
|
||||
|
||||
const char EmbeddedBundleCSS[] PROGMEM =
|
||||
"*,:after,:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;fo"
|
||||
"nt-family:Roboto,'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.6em;font-weight:300;letter-"
|
||||
"spacing:.01em;line-height:1.6}blockquote{border-left:.3rem solid #404040;margin-left:0;margin-right:"
|
||||
"0;padding:1rem 1.5rem}blockquote :last-child{margin-bottom:0}.button,button,input[type=button],input"
|
||||
"[type=reset],input[type=submit]{background-color:#06f;border:.1rem solid #06f;border-radius:.4rem;co"
|
||||
"lor:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-s"
|
||||
"pacing:.1rem;line-height:3.8rem;padding:0 3rem;text-align:center;text-decoration:none;text-transform"
|
||||
":uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type=butto"
|
||||
"n]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]"
|
||||
":focus,input[type=submit]:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}."
|
||||
"button[disabled],button[disabled],input[type=button][disabled],input[type=reset][disabled],input[typ"
|
||||
"e=submit][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button"
|
||||
"[disabled]:focus,button[disabled]:hover,input[type=button][disabled]:focus,input[type=button][disabl"
|
||||
"ed]:hover,input[type=reset][disabled]:focus,input[type=reset][disabled]:hover,input[type=submit][dis"
|
||||
"abled]:focus,input[type=submit][disabled]:hover{background-color:#06f;border-color:#06f}.button.butt"
|
||||
"on-outline,button.button-outline,input[type=button].button-outline,input[type=reset].button-outline,"
|
||||
"input[type=submit].button-outline{background-color:transparent;color:#06f}.button.button-outline:foc"
|
||||
"us,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type=b"
|
||||
"utton].button-outline:focus,input[type=button].button-outline:hover,input[type=reset].button-outline"
|
||||
":focus,input[type=reset].button-outline:hover,input[type=submit].button-outline:focus,input[type=sub"
|
||||
"mit].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.bu"
|
||||
"tton-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:"
|
||||
"focus,button.button-outline[disabled]:hover,input[type=button].button-outline[disabled]:focus,input["
|
||||
"type=button].button-outline[disabled]:hover,input[type=reset].button-outline[disabled]:focus,input[t"
|
||||
"ype=reset].button-outline[disabled]:hover,input[type=submit].button-outline[disabled]:focus,input[ty"
|
||||
"pe=submit].button-outline[disabled]:hover{border-color:inherit;color:#06f}.button.button-clear,butto"
|
||||
"n.button-clear,input[type=button].button-clear,input[type=reset].button-clear,input[type=submit].but"
|
||||
"ton-clear{background-color:transparent;border-color:transparent;color:#06f}.button.button-clear:focu"
|
||||
"s,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type=button]."
|
||||
"button-clear:focus,input[type=button].button-clear:hover,input[type=reset].button-clear:focus,input["
|
||||
"type=reset].button-clear:hover,input[type=submit].button-clear:focus,input[type=submit].button-clear"
|
||||
":hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disa"
|
||||
"bled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-c"
|
||||
"lear[disabled]:hover,input[type=button].button-clear[disabled]:focus,input[type=button].button-clear"
|
||||
"[disabled]:hover,input[type=reset].button-clear[disabled]:focus,input[type=reset].button-clear[disab"
|
||||
"led]:hover,input[type=submit].button-clear[disabled]:focus,input[type=submit].button-clear[disabled]"
|
||||
":hover{color:#06f}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:."
|
||||
"2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:.3rem solid #06f;overflow-y:hidden}"
|
||||
"pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:.1"
|
||||
"rem solid #f4f5f6;margin:3rem 0}input[type=email],input[type=number],input[type=password],input[type"
|
||||
"=search],input[type=tel],input[type=text],input[type=url],select,textarea{appearance:none;background"
|
||||
"-color:transparent;border:.1rem solid #404040;border-radius:.4rem;box-shadow:none;box-sizing:inherit"
|
||||
";height:3.8rem;padding:.6rem 1rem;width:100%}input[type=email]:focus,input[type=number]:focus,input["
|
||||
"type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[typ"
|
||||
"e=url]:focus,select:focus,textarea:focus{border-color:#06f;outline:0}select{background:url('data:ima"
|
||||
"ge/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"14\" viewBox=\"0 0 29 14\" width="
|
||||
"\"29\"><path fill=\"#d1d1d1\" d=\"M9.37727 3.625l5.08154 6.93523L19.54036 3.625\"/></svg>') center r"
|
||||
"ight no-repeat;padding-right:3rem}select:focus{background-image:url('data:image/svg+xml;utf8,<svg xm"
|
||||
"lns=\"http://www.w3.org/2000/svg\" height=\"14\" viewBox=\"0 0 29 14\" width=\"29\"><path fill=\"#9b"
|
||||
"4dca\" d=\"M9.37727 3.625l5.08154 6.93523L19.54036 3.625\"/></svg>')}textarea{min-height:6.5rem}labe"
|
||||
"l,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;"
|
||||
"padding:0}input[type=checkbox],input[type=radio]{display:inline}.label-inline{display:inline-block;f"
|
||||
"ont-weight:400;margin-left:.5rem}.container{margin:0 auto;max-width:112rem;padding:0 2rem;position:r"
|
||||
"elative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{"
|
||||
"padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-item"
|
||||
"s:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretc"
|
||||
"h{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 aut"
|
||||
"o;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column"
|
||||
".column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-"
|
||||
"offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-le"
|
||||
"ft:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column"
|
||||
".column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-"
|
||||
"offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{f"
|
||||
"lex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,."
|
||||
"row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-w"
|
||||
"idth:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-w"
|
||||
"idth:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .co"
|
||||
"lumn.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .co"
|
||||
"lumn.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .colum"
|
||||
"n .column-bottom{align-self:flex-end}.row .column .column-center{align-self:center}@media (min-width"
|
||||
":40rem){.row{flex-direction:row;margin-left:-1rem;width:calc(100% + 2rem)}.row .column{margin-bottom"
|
||||
":inherit;padding:0 1rem}}a{color:#06f;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{li"
|
||||
"st-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font"
|
||||
"-size:90%;margin:1.5rem 0 1.5rem 3rem}ol{list-style:decimal inside}ul{list-style:circle inside}.butt"
|
||||
"on,button,dd,dt,li{margin-bottom:1rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote"
|
||||
",dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{borde"
|
||||
"r-bottom:.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{pad"
|
||||
"ding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:700}p{margin-top:0}h1,h"
|
||||
"2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2rem;margin-top:0}h1{font-size:4.6"
|
||||
"rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font"
|
||||
"-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line"
|
||||
"-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{"
|
||||
"clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}[v-cloak]{displ"
|
||||
"ay:none}body{background-color:#000;color:#fff;font-family:Verdana,Arial,sans-serif;font-size:10pt;pa"
|
||||
"dding-bottom:3rem}@media screen and (min-width:768px){body{padding-top:3rem}}input,textarea{color:#f"
|
||||
"ff}#container{background-color:#202020;margin-top:2rem;padding:1rem;box-shadow:0 0 50px #fcf6cf;bord"
|
||||
"er:solid 1px #000}@media screen and (min-width:768px){#container{width:768px;margin-left:auto;margin"
|
||||
"-right:auto}}.header{position:relative}.header img{float:left;margin-right:1rem}.header .wifistatus{"
|
||||
"position:absolute;right:0;top:0}.header .wifistatus .indicator{display:inline-block;width:1rem;heigh"
|
||||
"t:1rem;border-radius:50%;margin-right:.5rem}.header .wifistatus .indicator[data-status=connected]{ba"
|
||||
"ckground-color:#396}.header .wifistatus .indicator[data-status=disconnected]{border:solid 1px grey}."
|
||||
"header .wifistatus .indicator[data-status=connecting]{background-color:#f93}.header .wifistatus .ind"
|
||||
"icator[data-status=error]{background-color:#c00}h1{font-size:16pt;margin:0}h2{color:silver;font-size"
|
||||
":10pt;margin:0}h3{color:grey;background-color:#282828;font-size:14pt;border-bottom:solid 1px grey}.v"
|
||||
"ersion{color:grey;font-size:8pt;text-align:center;margin-top:2rem}.tabs{clear:both;margin-top:3rem}."
|
||||
"tabs .button{background-color:#404040;color:#fff!important;border-color:grey}.tabs .button.button-ou"
|
||||
"tline{background-color:transparent}input[disabled]{cursor:not-allowed;color:grey;background-color:#2"
|
||||
"62626}.label-inline{margin-right:2rem}.hint{display:block;font-size:8pt;color:grey;margin-bottom:1.5"
|
||||
"rem}.loading{margin-top:3rem;text-align:center}.suboptions{margin-left:5rem}.buttons{text-align:cent"
|
||||
"er}.slider{-webkit-appearance:none;width:100%;height:.5rem;border-radius:.25rem;background:#404040;o"
|
||||
"utline:0}.slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:2rem;height:2re"
|
||||
"m;border-radius:50%;background:#fcf6cf;cursor:pointer}.slider::-moz-range-thumb{width:2rem;height:2r"
|
||||
"em;border-radius:50%;background:#fcf6cf;cursor:pointer}";
|
||||
|
||||
#endif
|
59
src/assets/html.h
Normal file
59
src/assets/html.h
Normal file
@ -0,0 +1,59 @@
|
||||
#ifndef __assets_html
|
||||
#define __assets_html
|
||||
|
||||
#include <pgmspace.h>
|
||||
|
||||
const char EmbeddedIndex[] PROGMEM =
|
||||
"<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title></title><meta name=\"theme-color\" content"
|
||||
"=\"#000000\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><link rel=\"styl"
|
||||
"esheet\" href=\"bundle.css\"><script src=\"bundle.js\"></script></head><body><div id=\"app\"><div v-"
|
||||
"cloak><div id=\"container\"><div class=\"header\"><img src=\"data:image/png;base64,iVBORw0KGgoAAAANS"
|
||||
"UhEUgAAADAAAAAwCAYAAABXAvmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAXBJREFUeNrsWdENgjAQL"
|
||||
"cYBcAPcACdQN4AN2MC4gRsYJ9ENdAPYADaADepdUhM1qFjurgp9ScMHgd713V3ftUp5eHgMF1rrGEau6VHD2HMbH5qJOLHra+fkz"
|
||||
"bsYRshM8pLTgUIgSgvuMMoYw+iMYTr6QhNYMrNiNywILlyVKdcyyPsmcRsyU50kgHtQQu2AdNJFpDkAK4I/LIWMb2AsIBcq0iRGe"
|
||||
"QGPTZfV6QE0+gDGF2rosGEgMQxIAhnZAiPN84upRf0/OlpsLCBp3yq0chgtCUUZrRw6QDO3EWHSqF9tarZaSKJXeJDdbQlsE0KD6"
|
||||
"JN/KoRsGhxXKClCKHJIfkThwMWhA6dBSolRijkJOd1ZUv9yQ9OpqbHpiaUVaEbZE7tIIjoBKXysos1c4V8ebLEdbv2bcMsE7gjuB"
|
||||
"dvXRSJ4F+/wqIXXrIGwmX3zwacLDheNO91OjLQKd14VMDCnYgCxVjI3NTe1mSoPD49x4SrAAG9qPn4eovCMAAAAAElFTkSuQmCC"
|
||||
"\"><h1>{{ $t('title') }}</h1><h2>{{ version.systemID !== null ? $t('systemID') + ': ' + version.syst"
|
||||
"emID : '' }}</h2><div class=\"wifistatus\"><div class=\"connection\"><div class=\"indicator\" :data-"
|
||||
"status=\"wifiStatus.ap.enabled ? 'connected' : 'disconnected'\"></div>{{ $t('wifiStatus.accesspoint."
|
||||
"title') }} {{ wifiStatus.ap.enabled ? wifiStatus.ap.ip : $t('wifiStatus.accesspoint.disabled') }}</d"
|
||||
"iv><div class=\"connection\"><div class=\"indicator\" :data-status=\"getWiFiStationStatus()\"></div>"
|
||||
"{{ $t('wifiStatus.stationmode.title') }} {{ getWiFiStationStatusText() }}</div></div></div><div v-if"
|
||||
"=\"loading\" class=\"loading\">{{ $t('loading') }} {{ loadingIndicator }}</div><div v-if=\"!loading"
|
||||
"\"><div class=\"tabs\"><button class=\"button\" :class=\"{ 'button-outline': activeTab != 'status' }"
|
||||
"\" @click=\"activeTab = 'status'\">{{ $t('status.tabTitle') }}</button> <button class=\"button\" :cl"
|
||||
"ass=\"{ 'button-outline': activeTab != 'triggers' }\" @click=\"activeTab = 'triggers'\">{{ $t('trigg"
|
||||
"ers.tabTitle') }}</button> <button class=\"button\" :class=\"{ 'button-outline': activeTab != 'conne"
|
||||
"ction' }\" @click=\"activeTab = 'connection'\">{{ $t('connection.tabTitle') }}</button></div><div v-"
|
||||
"if=\"activeTab == 'status'\"><h3>{{ $t('status.title') }}</h3><div class=\"slidecontainer\" v-for=\""
|
||||
"(step, index) in steps\">{{ index + 1 }} <input type=\"range\" min=\"0\" max=\"100\" class=\"slider"
|
||||
"\" v-model=\"step.value\"> {{ step.value }}</div></div><div v-if=\"activeTab == 'triggers'\"><form @"
|
||||
"submit.prevent=\"applyTriggers\"><fieldset><h3>{{ $t('triggers.timeTitle') }}</h3></fieldset><fields"
|
||||
"et><h3>{{ $t('triggers.motionTitle') }}</h3></fieldset></form></div><div v-if=\"activeTab == 'connec"
|
||||
"tion'\"><form @submit.prevent=\"applyConnection\"><fieldset><h3>{{ $t('connection.title') }}</h3><in"
|
||||
"put type=\"checkbox\" id=\"accesspoint\" v-model=\"connection.accesspoint\"><label class=\"label-inl"
|
||||
"ine\" for=\"accesspoint\">{{ $t('connection.accesspoint') }}</label><span class=\"hint\">{{ $t('conn"
|
||||
"ection.accesspointHint') }}</span> <input type=\"checkbox\" id=\"station\" v-model=\"connection.stat"
|
||||
"ion\"><label class=\"label-inline\" for=\"station\">{{ $t('connection.stationmode') }}</label><span "
|
||||
"class=\"hint\">{{ $t('connection.stationmodeHint') }}</span><label for=\"ssid\">{{ $t('connection.ss"
|
||||
"id') }}</label><input type=\"text\" id=\"ssid\" v-model=\"connection.ssid\" :disabled=\"!connection."
|
||||
"station\"><label for=\"password\">{{ $t('connection.password') }}</label><input type=\"password\" id"
|
||||
"=\"password\" v-model=\"connection.password\" :disabled=\"!connection.station\"> <input type=\"check"
|
||||
"box\" id=\"dhcp\" v-model=\"connection.dhcp\" :disabled=\"!connection.station\"><label class=\"label"
|
||||
"-inline\" for=\"dhcp\">{{ $t('connection.dhcp') }}</label><span class=\"hint\">{{ $t('connection.dhc"
|
||||
"pHint') }}</span><div class=\"suboptions\"><label for=\"ip\">{{ $t('connection.ipaddress') }}</label"
|
||||
"><input type=\"text\" id=\"ip\" v-model=\"connection.ip\" :disabled=\"!connection.station || connect"
|
||||
"ion.dhcp\"><label for=\"subnetmask\">{{ $t('connection.subnetmask') }}</label><input type=\"text\" i"
|
||||
"d=\"subnetmask\" v-model=\"connection.subnetmask\" :disabled=\"!connection.station || connection.dhc"
|
||||
"p\"><label for=\"gateway\">{{ $t('connection.gateway') }}</label><input type=\"text\" id=\"gateway\""
|
||||
" v-model=\"connection.gateway\" :disabled=\"!connection.station || connection.dhcp\"></div><label fo"
|
||||
"r=\"hostname\">{{ $t('connection.hostname') }}</label><input type=\"text\" :placeholder=\"$t('connec"
|
||||
"tion.hostnamePlaceholder')\" id=\"hostname\" v-model=\"connection.hostname\" :disabled=\"!connection"
|
||||
".station\"><div class=\"buttons\"><input class=\"button-primary\" type=\"submit\" :disabled=\"saving"
|
||||
"\" :value=\"saving ? $t('applyButtonSaving') : $t('applyButton')\"></div></fieldset></form></div></d"
|
||||
"iv><div class=\"clearfix\"></div></div><div class=\"version\">{{ $t('copyright') }}<br>{{ version.ve"
|
||||
"rsion !== null ? $t('firmwareVersion') + version.version : '' }}</div></div></div><script language="
|
||||
"\"javascript\">console.log(\"Initializing...\"),startApp()</script></body></html>";
|
||||
|
||||
#endif
|
32
src/assets/images.h
Normal file
32
src/assets/images.h
Normal file
@ -0,0 +1,32 @@
|
||||
#ifndef __assets_images
|
||||
#define __assets_images
|
||||
|
||||
#include <pgmspace.h>
|
||||
|
||||
const uint8_t EmbeddedLogo[] PROGMEM = {
|
||||
0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a,0x00,0x00,0x00,0x0d,0x49,0x48,0x44,0x52,0x00,0x00,0x00,0x30,
|
||||
0x00,0x00,0x00,0x30,0x08,0x06,0x00,0x00,0x00,0x57,0x02,0xf9,0x87,0x00,0x00,0x00,0x19,0x74,0x45,0x58,
|
||||
0x74,0x53,0x6f,0x66,0x74,0x77,0x61,0x72,0x65,0x00,0x41,0x64,0x6f,0x62,0x65,0x20,0x49,0x6d,0x61,0x67,
|
||||
0x65,0x52,0x65,0x61,0x64,0x79,0x71,0xc9,0x65,0x3c,0x00,0x00,0x01,0x70,0x49,0x44,0x41,0x54,0x78,0xda,
|
||||
0xec,0x59,0xd1,0x0d,0x82,0x30,0x10,0x2d,0xc6,0x01,0x70,0x03,0xdc,0x00,0x27,0x50,0x37,0x80,0x0d,0xd8,
|
||||
0xc0,0xb8,0x81,0x1b,0x18,0x27,0xd1,0x0d,0x74,0x03,0xd8,0x00,0x36,0x80,0x0d,0xea,0x5d,0x52,0x13,0x35,
|
||||
0xa8,0x58,0xee,0xae,0x0a,0x7d,0x49,0xc3,0x07,0x81,0xde,0xf5,0xdd,0x5d,0xdf,0xb5,0x4a,0x79,0x78,0x78,
|
||||
0x0c,0x17,0x5a,0xeb,0x18,0x46,0xae,0xe9,0x51,0xc3,0xd8,0x73,0x1b,0x1f,0x9a,0x89,0x38,0xb1,0xeb,0x6b,
|
||||
0xe7,0xe4,0xcd,0xbb,0x18,0x46,0xc8,0x4c,0xf2,0x92,0xd3,0x81,0x42,0x20,0x4a,0x0b,0xee,0x30,0xca,0x18,
|
||||
0xc3,0xe8,0x8c,0x61,0x3a,0xfa,0x42,0x13,0x58,0x32,0xb3,0x62,0x37,0x2c,0x08,0x2e,0x5c,0x95,0x29,0xd7,
|
||||
0x32,0xc8,0xfb,0x26,0x71,0x1b,0x32,0x53,0x9d,0x24,0x80,0x7b,0x50,0x42,0xed,0x80,0x74,0xd2,0x45,0xa4,
|
||||
0x39,0x00,0x2b,0x82,0x3f,0x2c,0x85,0x8c,0x6f,0x60,0x2c,0x20,0x17,0x2a,0xd2,0x24,0x46,0x79,0x01,0x8f,
|
||||
0x4d,0x97,0xd5,0xe9,0x01,0x34,0xfa,0x00,0xc6,0x17,0x6a,0xe8,0xb0,0x61,0x20,0x31,0x0c,0x48,0x02,0x19,
|
||||
0xd9,0x02,0x23,0xcd,0xf3,0x8b,0xa9,0x45,0xfd,0x3f,0x3a,0x5a,0x6c,0x2c,0x20,0x69,0xdf,0x2a,0xb4,0x72,
|
||||
0x18,0x2d,0x09,0x45,0x19,0xad,0x1c,0x3a,0x40,0x33,0xb7,0x11,0x61,0xd2,0xa8,0x5f,0x6d,0x6a,0xb6,0x5a,
|
||||
0x48,0xa2,0x57,0x78,0x90,0xdd,0x6d,0x09,0x6c,0x13,0x42,0x83,0xe8,0x93,0x7f,0x2a,0x84,0x6c,0x1a,0x1c,
|
||||
0x57,0x28,0x29,0x42,0x28,0x72,0x48,0x7e,0x44,0xe1,0xc0,0xc5,0xa1,0x03,0xa7,0x41,0x4a,0x89,0x51,0x8a,
|
||||
0x39,0x09,0x39,0xdd,0x59,0x52,0xff,0x72,0x43,0xd3,0xa9,0xa9,0xb1,0xe9,0x89,0xa5,0x15,0x68,0x46,0xd9,
|
||||
0x13,0xbb,0x48,0x22,0x3a,0x01,0x29,0x7c,0xac,0xa2,0xcd,0x5c,0xe1,0x5f,0x1e,0x6c,0xb1,0x1d,0x6e,0xfd,
|
||||
0x9b,0x70,0xcb,0x04,0xee,0x08,0xee,0x05,0xdb,0xd7,0x45,0x22,0x78,0x17,0xef,0xf0,0xa8,0x85,0xd7,0xac,
|
||||
0x81,0xb0,0x99,0x7d,0xf3,0xc1,0xa7,0x0b,0x0e,0x17,0x8d,0x3b,0xdd,0x4e,0x8c,0xb4,0x0a,0x77,0x5e,0x15,
|
||||
0x30,0x30,0xa7,0x62,0x00,0xb1,0x56,0x32,0x37,0x35,0x37,0xb5,0x99,0x2a,0x0f,0x0f,0x8f,0x71,0xe1,0x2a,
|
||||
0xc0,0x00,0x6f,0x6a,0x3e,0x7e,0x1e,0xa2,0xf0,0x8c,0x00,0x00,0x00,0x00,0x49,0x45,0x4e,0x44,0xae,0x42,
|
||||
0x60,0x82};
|
||||
|
||||
#endif
|
1248
src/assets/js.h
Normal file
1248
src/assets/js.h
Normal file
File diff suppressed because it is too large
Load Diff
14
src/assets/version.h
Normal file
14
src/assets/version.h
Normal file
@ -0,0 +1,14 @@
|
||||
#ifndef __assets_version
|
||||
#define __assets_version
|
||||
|
||||
const uint8_t VersionMajor = 2;
|
||||
const uint8_t VersionMinor = 0;
|
||||
const uint8_t VersionPatch = 0;
|
||||
const uint8_t VersionMetadata = 0;
|
||||
const char VersionBranch[] = "release/2.0";
|
||||
const char VersionSha[] = "3099dd8a0e2e5fa771e6a864a1daa83de5998df0";
|
||||
const char VersionSemVer[] = "2.0.0-beta.1";
|
||||
const char VersionFullSemVer[] = "2.0.0-beta.1+0";
|
||||
const char VersionCommitDate[] = "2017-10-05";
|
||||
|
||||
#endif
|
26
src/charproperties.cpp
Normal file
26
src/charproperties.cpp
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#include "charproperties.h"
|
||||
#include <cstddef>
|
||||
#include <string.h>
|
||||
#include "debug.h"
|
||||
|
||||
void CharProperties::assignChar(char** field, const char* newValue)
|
||||
{
|
||||
if (*field != NULL)
|
||||
delete *field;
|
||||
|
||||
if (newValue != NULL)
|
||||
{
|
||||
// Include the terminating null character
|
||||
size_t length = strlen(newValue) + 1;
|
||||
*field = new char[length];
|
||||
strncpy(*field, newValue, length);
|
||||
}
|
||||
else
|
||||
*field = NULL;
|
||||
}
|
18
src/charproperties.h
Normal file
18
src/charproperties.h
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#ifndef __charproperties
|
||||
#define __charproperties
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class CharProperties
|
||||
{
|
||||
protected:
|
||||
void assignChar(char** field, const char* newValue);
|
||||
};
|
||||
|
||||
#endif
|
52
src/config.h
52
src/config.h
@ -1,26 +1,20 @@
|
||||
#ifndef __Config
|
||||
#define __Config
|
||||
#ifndef __config
|
||||
#define __config
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <stdint.h>
|
||||
#include "credentials.h"
|
||||
|
||||
#define SerialDebug
|
||||
static const uint32_t SerialDebugBaudrate = 115200;
|
||||
static const uint32_t SerialDebugStartupDelay = 2000;
|
||||
|
||||
|
||||
//#define SerialDebug
|
||||
|
||||
|
||||
// The name of this device on the network
|
||||
static const char* WiFiHostname = "Stairs";
|
||||
|
||||
|
||||
// The number of steps (assumed to be <= 16, as the code currently only controls 1 PCA9685 board)
|
||||
static const uint8_t StepCount = 14;
|
||||
|
||||
|
||||
// The port number on which the UDP server listens
|
||||
static const uint16_t UDPPort = 3126;
|
||||
static const char* ConnectionSettingsFile = "/settings.json";
|
||||
static const char* DefaultAPSSIDPrefix = "Stairs-";
|
||||
|
||||
static const uint32_t StationModeTimeout = 30000;
|
||||
|
||||
|
||||
/*
|
||||
// Pins for the I2C bus
|
||||
static const uint8_t PinSDA = 13;
|
||||
static const uint8_t PinSCL = 12;
|
||||
@ -29,28 +23,6 @@ static const uint8_t PinSCL = 12;
|
||||
// I2C address and PWM frequency of the PCA9685 board
|
||||
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
|
@ -1,3 +0,0 @@
|
||||
// Create a copy of this file called "credentials.h"
|
||||
static const char* WiFiSSID = "example";
|
||||
static const char* WiFiPassword = "example";
|
17
src/debug.cpp
Normal file
17
src/debug.cpp
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#include "debug.h"
|
||||
|
||||
|
||||
void _dinit()
|
||||
{
|
||||
#ifdef SerialDebug
|
||||
Serial.begin(SerialDebugBaudrate);
|
||||
Serial.setDebugOutput(true);
|
||||
delay(SerialDebugStartupDelay);
|
||||
#endif
|
||||
}
|
24
src/debug.h
Normal file
24
src/debug.h
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#ifndef __serialdebug
|
||||
#define __serialdebug
|
||||
|
||||
#include "config.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
void _dinit();
|
||||
|
||||
#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
|
15
src/global.cpp
Normal file
15
src/global.cpp
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#include "global.h"
|
||||
|
||||
ConnectionSettings* connectionSettings = new ConnectionSettings();
|
||||
bool connectionSettingsChanged = false;
|
||||
|
||||
uint32_t currentTime;
|
||||
|
||||
|
||||
IPAddress emptyIP(0, 0, 0, 0);
|
21
src/global.h
Normal file
21
src/global.h
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#ifndef __global
|
||||
#define __global
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <IPAddress.h>
|
||||
#include "settings/connection.h"
|
||||
|
||||
extern ConnectionSettings* connectionSettings;
|
||||
extern bool connectionSettingsChanged;
|
||||
extern uint32_t currentTime;
|
||||
|
||||
extern IPAddress emptyIP;
|
||||
|
||||
#endif
|
232
src/main.cpp
232
src/main.cpp
@ -1,28 +1,225 @@
|
||||
/*
|
||||
* Stairs lighting
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <Stream.h>
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <WiFiUdp.h>
|
||||
#include <ESP8266httpUpdate.h>
|
||||
#include <ESPAsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
}
|
||||
|
||||
#include "config.h"
|
||||
#include "protocol.h"
|
||||
#include "components\PCA9685.h"
|
||||
//#include "modes\adc.h"
|
||||
#include "modes\alternate.h"
|
||||
#include "modes\custom.h"
|
||||
#include "modes\slide.h"
|
||||
#include "modes\static.h"
|
||||
#include "stairs.h"
|
||||
#include "version.h"
|
||||
#include "debug.h"
|
||||
#include "settings/connection.h"
|
||||
#include "global.h"
|
||||
#include "server/static.h"
|
||||
#include "server/api.h"
|
||||
|
||||
|
||||
PCA9685* pwmDriver;
|
||||
Stairs* stairs;
|
||||
WiFiUDP udpServer;
|
||||
ADC_MODE(ADC_VCC);
|
||||
|
||||
// Forward declarations
|
||||
void initWiFi();
|
||||
#ifdef SerialDebug
|
||||
void wifiEvent(WiFiEvent_t event);
|
||||
#endif
|
||||
void updateLED();
|
||||
|
||||
void startServer();
|
||||
void stopServer();
|
||||
void handleNotFound(AsyncWebServerRequest* request);
|
||||
|
||||
|
||||
AsyncWebServer server(80);
|
||||
bool accessPoint = false;
|
||||
bool stationMode = false;
|
||||
bool forceAccessPoint = false;
|
||||
|
||||
uint32_t stationModeStart = 0;
|
||||
|
||||
|
||||
void setup()
|
||||
{
|
||||
_dinit();
|
||||
currentTime = millis();
|
||||
|
||||
if (!SPIFFS.begin())
|
||||
_dln("Setup :: failed to mount file system");
|
||||
|
||||
connectionSettings->read();
|
||||
|
||||
_dln("Setup :: initializing WiFi");
|
||||
WiFi.persistent(false);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
|
||||
#ifdef SerialDebug
|
||||
// onEvent is already deprecated, but since I'm only using it
|
||||
// for debug purposes we'll see how long it lasts...
|
||||
WiFi.onEvent(wifiEvent);
|
||||
_d("WiFi :: MAC address: ");
|
||||
_dln(WiFi.macAddress());
|
||||
#endif
|
||||
|
||||
initWiFi();
|
||||
|
||||
_dln("Setup :: registering routes");
|
||||
registerStaticRoutes(&server);
|
||||
registerAPIRoutes(&server);
|
||||
|
||||
_dln("Setup :: starting HTTP server");
|
||||
server.onNotFound(handleNotFound);
|
||||
server.begin();
|
||||
}
|
||||
|
||||
|
||||
void loop()
|
||||
{
|
||||
currentTime = millis();
|
||||
|
||||
if (connectionSettingsChanged)
|
||||
{
|
||||
_dln("Loop :: connection settings changed");
|
||||
initWiFi();
|
||||
connectionSettingsChanged = false;
|
||||
}
|
||||
|
||||
|
||||
if (stationModeStart > 0)
|
||||
{
|
||||
bool isConnected = WiFi.status() == WL_CONNECTED;
|
||||
|
||||
if (isConnected)
|
||||
{
|
||||
_d("WiFi :: connected, IP address: ");
|
||||
_dln(WiFi.localIP());
|
||||
|
||||
stationModeStart = 0;
|
||||
}
|
||||
else if (stationMode && accessPoint &&
|
||||
currentTime - stationModeStart >= StationModeTimeout)
|
||||
{
|
||||
_dln("WiFi :: unable to connect, switching off station mode, status:");
|
||||
_dln(WiFi.status());
|
||||
|
||||
#ifdef SerialDebug
|
||||
WiFi.printDiag(Serial);
|
||||
#endif
|
||||
|
||||
// Connecting to access point is taking too long and is blocking
|
||||
// the access point mode, stop trying
|
||||
stationMode = false;
|
||||
WiFi.disconnect();
|
||||
WiFi.mode(WIFI_AP);
|
||||
}
|
||||
}
|
||||
|
||||
updateLED();
|
||||
}
|
||||
|
||||
|
||||
void initWiFi()
|
||||
{
|
||||
WiFi.disconnect();
|
||||
WiFi.softAPdisconnect();
|
||||
|
||||
accessPoint = connectionSettings->flag(AccessPoint) || forceAccessPoint;
|
||||
stationMode = connectionSettings->flag(StationMode) && connectionSettings->ssid() != NULL;
|
||||
|
||||
WiFi.mode(accessPoint && stationMode ? WIFI_AP_STA :
|
||||
accessPoint ? WIFI_AP :
|
||||
stationMode ? WIFI_STA :
|
||||
WIFI_OFF);
|
||||
|
||||
if (accessPoint)
|
||||
{
|
||||
_dln("WiFi :: starting access point");
|
||||
String ssidString = DefaultAPSSIDPrefix + String(ESP.getChipId(), HEX);
|
||||
if (WiFi.softAP((const char *)ssidString.c_str()))
|
||||
{
|
||||
_d("WiFi :: IP address: ");
|
||||
_dln(WiFi.softAPIP());
|
||||
}
|
||||
else
|
||||
_d("WiFi :: failed to start soft access point");
|
||||
}
|
||||
|
||||
if (stationMode)
|
||||
{
|
||||
_d("WiFi :: starting station mode to: ");
|
||||
_dln(connectionSettings->ssid());
|
||||
|
||||
stationModeStart = currentTime;
|
||||
|
||||
if (WiFi.begin(connectionSettings->ssid(), connectionSettings->password()))
|
||||
{
|
||||
if (connectionSettings->flag(DHCP))
|
||||
// I've had the same issue as described here with config(0, 0, 0):
|
||||
// https://stackoverflow.com/questions/40069654/how-to-clear-static-ip-configuration-and-start-dhcp
|
||||
wifi_station_dhcpc_start();
|
||||
else
|
||||
WiFi.config(connectionSettings->ip(), connectionSettings->gateway(), connectionSettings->subnetMask());
|
||||
}
|
||||
else
|
||||
_d("WiFi :: failed to start station mode");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#ifdef SerialDebug
|
||||
void wifiEvent(WiFiEvent_t event)
|
||||
{
|
||||
switch (event)
|
||||
{
|
||||
case WIFI_EVENT_STAMODE_CONNECTED:
|
||||
_dln("WiFi:: station mode: connected"); break;
|
||||
|
||||
case WIFI_EVENT_STAMODE_DISCONNECTED:
|
||||
_dln("WiFi:: station mode: disconnected"); break;
|
||||
|
||||
case WIFI_EVENT_STAMODE_AUTHMODE_CHANGE:
|
||||
_dln("WiFi:: station mode: authmode change"); break;
|
||||
|
||||
case WIFI_EVENT_STAMODE_GOT_IP:
|
||||
_dln("WiFi:: station mode: got IP");
|
||||
_dln(WiFi.localIP());
|
||||
break;
|
||||
|
||||
case WIFI_EVENT_STAMODE_DHCP_TIMEOUT:
|
||||
_dln("WiFi:: station mode: DHCP timeout"); break;
|
||||
|
||||
case WIFI_EVENT_SOFTAPMODE_STACONNECTED:
|
||||
_dln("WiFi:: soft AP mode: station connected"); break;
|
||||
|
||||
case WIFI_EVENT_SOFTAPMODE_STADISCONNECTED:
|
||||
_dln("WiFi:: soft AP mode: station disconnected"); break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
void updateLED()
|
||||
{
|
||||
//while (WiFi.status() != WL_CONNECTED)
|
||||
//{
|
||||
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
void handleNotFound(AsyncWebServerRequest *request)
|
||||
{
|
||||
_d("HTTP :: not found: "); _dln(request->url());
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
uint8_t currentModeIdentifier;
|
||||
IMode* currentMode;
|
||||
|
||||
@ -362,4 +559,5 @@ void setCurrentMode(IMode* mode, uint8_t identifier)
|
||||
void handleCurrentMode()
|
||||
{
|
||||
currentMode->tick(stairs, currentTime);
|
||||
}
|
||||
}
|
||||
*/
|
@ -1,27 +0,0 @@
|
||||
#include "alternate.h"
|
||||
#include <Stream.h>
|
||||
|
||||
|
||||
void AlternateMode::init(IStairs* stairs, uint32_t currentTime)
|
||||
{
|
||||
stairs->setAll(0);
|
||||
|
||||
this->lastChange = currentTime;
|
||||
this->even = false;
|
||||
}
|
||||
|
||||
|
||||
void AlternateMode::tick(IStairs* stairs, uint32_t currentTime)
|
||||
{
|
||||
if (currentTime - this->lastChange < this->parameters.interval)
|
||||
return;
|
||||
|
||||
this->lastChange = currentTime;
|
||||
this->even = !this->even;
|
||||
|
||||
uint8_t stepCount = stairs->getCount();
|
||||
for (uint8_t step = 0; step < stepCount; step++)
|
||||
{
|
||||
stairs->set(step, ((step % 2) == 0) == this->even ? this->parameters.brightness : 0);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
#ifndef __AlternateMode
|
||||
#define __AlternateMode
|
||||
|
||||
#include <stdint.h>
|
||||
#include "base.h"
|
||||
#include "../config.h"
|
||||
|
||||
|
||||
struct AlternateModeParameters
|
||||
{
|
||||
uint16_t interval;
|
||||
uint16_t brightness;
|
||||
};
|
||||
|
||||
|
||||
class AlternateMode : public BaseMode<AlternateModeParameters>
|
||||
{
|
||||
private:
|
||||
uint32_t lastChange;
|
||||
bool even = false;
|
||||
|
||||
public:
|
||||
AlternateMode()
|
||||
{
|
||||
parameters.interval = 500;
|
||||
parameters.brightness = IStairs::On;
|
||||
}
|
||||
|
||||
void init(IStairs* stairs, uint32_t currentTime);
|
||||
void tick(IStairs* stairs, uint32_t currentTime);
|
||||
};
|
||||
|
||||
#endif
|
@ -1,32 +0,0 @@
|
||||
#ifndef __BaseMode
|
||||
#define __BaseMode
|
||||
|
||||
#include <stdint.h>
|
||||
#include <Stream.h>
|
||||
#include "../config.h"
|
||||
#include "../mode.h"
|
||||
|
||||
|
||||
template <class T>
|
||||
class BaseMode : public IMode
|
||||
{
|
||||
protected:
|
||||
T parameters;
|
||||
|
||||
public:
|
||||
virtual void read(uint8_t* data)
|
||||
{
|
||||
_d("Reading parameters, size ");
|
||||
_dln(sizeof(T));
|
||||
memcpy(&this->parameters, data, sizeof(T));
|
||||
}
|
||||
|
||||
virtual void write(Stream* stream)
|
||||
{
|
||||
_d("Writing parameters, size ");
|
||||
_dln(sizeof(T));
|
||||
stream->write((uint8_t*)&this->parameters, sizeof(T));
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
@ -1,29 +0,0 @@
|
||||
#include "custom.h"
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
void CustomMode::read(uint8_t* data)
|
||||
{
|
||||
// The packet is zeroed before we get our hands on it and
|
||||
// the size should also be larger as noted in main.cpp,
|
||||
// so a straight-up copy should be safe.
|
||||
memcpy(this->values, data, sizeof(this->values));
|
||||
}
|
||||
|
||||
|
||||
void CustomMode::write(Stream* stream)
|
||||
{
|
||||
stream->write((uint8_t*)&this->values, sizeof(this->values));
|
||||
}
|
||||
|
||||
|
||||
void CustomMode::init(IStairs* stairs, uint32_t currentTime)
|
||||
{
|
||||
for (uint8_t step = 0; step < StepCount; step++)
|
||||
stairs->set(step, this->values[step]);
|
||||
}
|
||||
|
||||
|
||||
void CustomMode::tick(IStairs* stairs, uint32_t currentTime)
|
||||
{
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
#ifndef __CustomMode
|
||||
#define __CustomMode
|
||||
|
||||
#include <stdint.h>
|
||||
#include "base.h"
|
||||
#include "../config.h"
|
||||
|
||||
class CustomMode : public IMode
|
||||
{
|
||||
private:
|
||||
uint16_t values[StepCount];
|
||||
|
||||
public:
|
||||
void read(uint8_t* data);
|
||||
void write(Stream* stream);
|
||||
|
||||
void init(IStairs* stairs, uint32_t currentTime);
|
||||
void tick(IStairs* stairs, uint32_t currentTime);
|
||||
};
|
||||
|
||||
#endif
|
@ -1,15 +0,0 @@
|
||||
#ifndef __SlideMode
|
||||
#define __SlideMode
|
||||
|
||||
#include <stdint.h>
|
||||
#include "base.h"
|
||||
#include "../config.h"
|
||||
|
||||
class SlideMode : public IMode
|
||||
{
|
||||
private:
|
||||
uint16_t interval;
|
||||
bool up;
|
||||
};
|
||||
|
||||
#endif
|
@ -1,52 +0,0 @@
|
||||
#include "static.h"
|
||||
|
||||
void StaticMode::init(IStairs* stairs, uint32_t currentTime)
|
||||
{
|
||||
_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;
|
||||
|
||||
uint32_t elapsedTime = currentTime - this->easeStartTime;
|
||||
uint32_t diff = this->easeState == Up ? this->parameters.brightness - this->easeStartBrightness : this->easeStartBrightness - this->parameters.brightness;
|
||||
uint32_t delta = (diff * elapsedTime) / this->parameters.easeTime;
|
||||
|
||||
this->currentBrightness = this->easeState == Up ? this->easeStartBrightness + delta : this->easeStartBrightness - delta;
|
||||
|
||||
|
||||
if (elapsedTime >= this->parameters.easeTime)
|
||||
{
|
||||
this->currentBrightness = this->parameters.brightness;
|
||||
this->easeState = None;
|
||||
}
|
||||
|
||||
stairs->setAll(this->currentBrightness);
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
#ifndef __StaticMode
|
||||
#define __StaticMode
|
||||
|
||||
#include <stdint.h>
|
||||
#include "base.h"
|
||||
#include "../config.h"
|
||||
|
||||
|
||||
struct StaticModeParameters
|
||||
{
|
||||
uint16_t brightness;
|
||||
uint16_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);
|
||||
void tick(IStairs* stairs, uint32_t currentTime);
|
||||
};
|
||||
|
||||
#endif
|
101
src/server/api.cpp
Normal file
101
src/server/api.cpp
Normal file
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#include "api.h"
|
||||
#include <ArduinoJson.h>
|
||||
#include <IPAddress.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include "../assets/version.h"
|
||||
#include "../debug.h"
|
||||
#include "../global.h"
|
||||
#include "../settings/connection.h"
|
||||
|
||||
|
||||
void handleVersion(AsyncWebServerRequest *request)
|
||||
{
|
||||
_dln("API :: version");
|
||||
|
||||
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(2));
|
||||
|
||||
JsonObject& root = jsonBuffer.createObject();
|
||||
root["systemID"] = String(ESP.getChipId(), HEX);
|
||||
root["version"] = String(VersionFullSemVer) + " sha." + String(VersionSha);
|
||||
|
||||
AsyncResponseStream *response = request->beginResponseStream("application/json");
|
||||
root.printTo(*response);
|
||||
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
|
||||
void handleConnectionStatus(AsyncWebServerRequest *request)
|
||||
{
|
||||
_dln("API :: connection status");
|
||||
|
||||
WiFiMode_t mode = WiFi.getMode();
|
||||
|
||||
|
||||
DynamicJsonBuffer jsonBuffer((2 * JSON_OBJECT_SIZE(2)) + JSON_OBJECT_SIZE(3));
|
||||
|
||||
JsonObject& root = jsonBuffer.createObject();
|
||||
JsonObject& ap = root.createNestedObject("ap");
|
||||
ap["enabled"] = (mode == WIFI_AP || mode == WIFI_AP_STA);
|
||||
ap["ip"] = WiFi.softAPIP().toString();
|
||||
|
||||
JsonObject& station = root.createNestedObject("station");
|
||||
station["enabled"] = (mode == WIFI_STA || mode == WIFI_AP_STA);
|
||||
station["status"] = (uint8_t)WiFi.status();
|
||||
station["ip"] = WiFi.localIP().toString();
|
||||
|
||||
AsyncResponseStream *response = request->beginResponseStream("application/json");
|
||||
root.printTo(*response);
|
||||
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
|
||||
void handleGetConnection(AsyncWebServerRequest *request)
|
||||
{
|
||||
_dln("API :: get connection");
|
||||
|
||||
AsyncResponseStream *response = request->beginResponseStream("application/json");
|
||||
connectionSettings->toJson(*response);
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
|
||||
void handlePostConnection(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
|
||||
{
|
||||
_dln("API :: post connection");
|
||||
|
||||
bool changed;
|
||||
if (connectionSettings->fromJson((char*)data, &changed))
|
||||
{
|
||||
connectionSettings->write();
|
||||
|
||||
if (changed)
|
||||
connectionSettingsChanged = true;
|
||||
|
||||
request->send(200);
|
||||
}
|
||||
else
|
||||
request->send(400);
|
||||
}
|
||||
|
||||
|
||||
void devNullRequest(AsyncWebServerRequest *request) { }
|
||||
void devNullFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { }
|
||||
|
||||
|
||||
void registerAPIRoutes(AsyncWebServer* server)
|
||||
{
|
||||
server->on("/api/version", HTTP_GET, handleVersion);
|
||||
|
||||
server->on("/api/connection/status", HTTP_GET, handleConnectionStatus);
|
||||
|
||||
server->on("/api/connection", HTTP_GET, handleGetConnection);
|
||||
server->on("/api/connection", HTTP_POST, devNullRequest, devNullFileUpload, handlePostConnection);
|
||||
}
|
13
src/server/api.h
Normal file
13
src/server/api.h
Normal file
@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#ifndef __server_api
|
||||
#define __server_api
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
void registerAPIRoutes(AsyncWebServer* server);
|
||||
|
||||
#endif
|
35
src/server/static.cpp
Normal file
35
src/server/static.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#include "static.h"
|
||||
#include "../debug.h"
|
||||
#include "../assets/html.h"
|
||||
#include "../assets/js.h"
|
||||
#include "../assets/css.h"
|
||||
#include "../assets/images.h"
|
||||
|
||||
|
||||
void handleStatic(AsyncWebServerRequest *request, const String& contentType, PGM_P content)
|
||||
{
|
||||
_d("HTTP :: static: "); _dln(request->url());
|
||||
request->send_P(200, contentType, content);
|
||||
}
|
||||
|
||||
|
||||
void registerStaticRoutes(AsyncWebServer* server)
|
||||
{
|
||||
server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) { handleStatic(request, "text/html", EmbeddedIndex); });
|
||||
|
||||
server->on("/bundle.js", HTTP_GET, [](AsyncWebServerRequest *request) { handleStatic(request, "text/javascript", EmbeddedBundleJS); });
|
||||
server->on("/bundle.css", HTTP_GET, [](AsyncWebServerRequest *request) { handleStatic(request, "text/css", EmbeddedBundleCSS); });
|
||||
|
||||
server->on("/logo.png", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
_d("HTTP :: static: "); _dln(request->url());
|
||||
AsyncWebServerResponse *response = request->beginResponse_P(200, "image/png", EmbeddedLogo, sizeof(EmbeddedLogo));
|
||||
request->send(response);
|
||||
});
|
||||
}
|
14
src/server/static.h
Normal file
14
src/server/static.h
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#ifndef __server_static
|
||||
#define __server_static
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
void registerStaticRoutes(AsyncWebServer* server);
|
||||
|
||||
#endif
|
149
src/settings/connection.cpp
Normal file
149
src/settings/connection.cpp
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#include "connection.h"
|
||||
#include <ArduinoJson.h>
|
||||
#include <FS.h>
|
||||
#include "../debug.h"
|
||||
#include "../global.h"
|
||||
#include "../config.h"
|
||||
|
||||
|
||||
|
||||
void ConnectionSettings::read()
|
||||
{
|
||||
_dln("ConnectionSettings :: opening file");
|
||||
File settingsFile = SPIFFS.open(ConnectionSettingsFile, "r");
|
||||
if (!settingsFile)
|
||||
{
|
||||
_dln("ConnectionSettings :: failed to open file");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t size = settingsFile.size();
|
||||
if (size > 1024)
|
||||
{
|
||||
_dln("ConnectionSettings :: file size is too large");
|
||||
return;
|
||||
}
|
||||
|
||||
if (size == 0)
|
||||
{
|
||||
_dln("ConnectionSettings :: zero size file");
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_ptr<char[]> buf(new char[size]);
|
||||
settingsFile.readBytes(buf.get(), size);
|
||||
|
||||
_dln(buf.get());
|
||||
|
||||
if (fromJson(buf.get()))
|
||||
_dln("ConnectionSettings :: read from file");
|
||||
else
|
||||
_dln("ConnectionSettings :: failed to parse file");
|
||||
}
|
||||
|
||||
|
||||
void ConnectionSettings::write()
|
||||
{
|
||||
_dln("ConnectionSettings :: opening file for writing");
|
||||
File settingsFile = SPIFFS.open(ConnectionSettingsFile, "w");
|
||||
if (!settingsFile)
|
||||
{
|
||||
_dln("ConnectionSettings:: failed to open file for writing");
|
||||
return;
|
||||
}
|
||||
|
||||
toJson(settingsFile);
|
||||
_dln("ConnectionSettings:: written to file");
|
||||
}
|
||||
|
||||
|
||||
void ConnectionSettings::toJson(Print &print)
|
||||
{
|
||||
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(9));
|
||||
|
||||
JsonObject& root = jsonBuffer.createObject();
|
||||
root["hostname"] = hostname();
|
||||
root["accesspoint"] = flag(AccessPoint);
|
||||
root["station"] = flag(StationMode);
|
||||
root["ssid"] = ssid();
|
||||
root["password"] = password();
|
||||
root["dhcp"] = flag(DHCP);
|
||||
root["ip"] = ip() != 0 ? ip().toString() : "";
|
||||
root["subnetmask"] = subnetMask() != 0 ? subnetMask().toString() : "";
|
||||
root["gateway"] = gateway() != 0 ? gateway().toString() : "";
|
||||
|
||||
root.printTo(print);
|
||||
}
|
||||
|
||||
|
||||
bool ConnectionSettings::fromJson(char* data)
|
||||
{
|
||||
return fromJson(data, NULL);
|
||||
}
|
||||
|
||||
|
||||
bool ConnectionSettings::fromJson(char* data, bool* changed)
|
||||
{
|
||||
if (changed != NULL)
|
||||
*changed = false;
|
||||
|
||||
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(9) + 250);
|
||||
JsonObject& root = jsonBuffer.parseObject(data);
|
||||
|
||||
if (!root.success())
|
||||
return false;
|
||||
|
||||
IPAddress jsonIP;
|
||||
IPAddress jsonSubnetMask;
|
||||
IPAddress jsonGateway;
|
||||
|
||||
const char* jsonHostname = root["hostname"];
|
||||
bool jsonAccessPoint = root["accesspoint"];
|
||||
bool jsonStation = root["station"];
|
||||
const char* jsonSSID = root["ssid"];
|
||||
const char* jsonPassword = root["password"];
|
||||
bool jsonDHCP = root["dhcp"];
|
||||
const char* jsonIPText = root["ip"];
|
||||
const char* jsonSubnetMaskText = root["subnetmask"];
|
||||
const char* jsonGatewayText = root["gateway"];
|
||||
|
||||
if (jsonIPText == NULL || !jsonIP.fromString(jsonIPText)) jsonIP = emptyIP;
|
||||
if (jsonSubnetMaskText == NULL || !jsonSubnetMask.fromString(jsonSubnetMaskText)) jsonSubnetMask = emptyIP;
|
||||
if (jsonGatewayText == NULL || !jsonGateway.fromString(jsonGatewayText)) jsonGateway = emptyIP;
|
||||
|
||||
|
||||
if (!(jsonAccessPoint || jsonStation))
|
||||
jsonAccessPoint = true;
|
||||
|
||||
if ((jsonHostname != hostname()) ||
|
||||
(jsonAccessPoint != flag(AccessPoint)) ||
|
||||
(jsonStation != flag(StationMode)) ||
|
||||
(jsonSSID != ssid()) ||
|
||||
(jsonPassword != password()) ||
|
||||
(jsonDHCP != flag(DHCP)) ||
|
||||
(jsonIP != ip()) ||
|
||||
(jsonSubnetMask != subnetMask()) ||
|
||||
(jsonGateway != gateway()))
|
||||
{
|
||||
hostname(jsonHostname);
|
||||
flag(AccessPoint, jsonAccessPoint);
|
||||
flag(StationMode, jsonStation);
|
||||
ssid(jsonSSID);
|
||||
password(jsonPassword);
|
||||
flag(DHCP, jsonDHCP);
|
||||
ip(jsonIP);
|
||||
subnetMask(jsonSubnetMask);
|
||||
gateway(jsonGateway);
|
||||
|
||||
if (changed != NULL)
|
||||
*changed = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
73
src/settings/connection.h
Normal file
73
src/settings/connection.h
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Stairs
|
||||
* Copyright 2017 (c) Mark van Renswoude
|
||||
*
|
||||
* https://git.x2software.net/pub/Stairs
|
||||
*/
|
||||
#ifndef __settingsconnection
|
||||
#define __settingsconnection
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <IPAddress.h>
|
||||
#include "../charproperties.h"
|
||||
|
||||
|
||||
enum ConnectionSettingsFlags
|
||||
{
|
||||
AccessPoint = 1,
|
||||
StationMode = 2,
|
||||
DHCP = 4
|
||||
};
|
||||
|
||||
|
||||
class ConnectionSettings : CharProperties
|
||||
{
|
||||
private:
|
||||
char* mHostname = NULL;
|
||||
uint8_t mFlags = AccessPoint | DHCP;
|
||||
char* mSSID = NULL;
|
||||
char* mPassword = NULL;
|
||||
IPAddress mIP = (uint32_t)0;
|
||||
IPAddress mSubnetMask = (uint32_t)0;
|
||||
IPAddress mGateway = (uint32_t)0;
|
||||
|
||||
public:
|
||||
void read();
|
||||
void write();
|
||||
|
||||
|
||||
void toJson(Print &print);
|
||||
bool fromJson(char* data);
|
||||
bool fromJson(char* data, bool* changed);
|
||||
|
||||
|
||||
char* hostname() { return mHostname; }
|
||||
void hostname(const char* value) { assignChar(&mHostname, value); }
|
||||
|
||||
bool flag(ConnectionSettingsFlags flag) { return (mFlags & flag) != 0; }
|
||||
void flag(ConnectionSettingsFlags flag, bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
mFlags |= flag;
|
||||
else
|
||||
mFlags &= ~flag;
|
||||
}
|
||||
|
||||
char* ssid() { return mSSID; }
|
||||
void ssid(const char* value) { assignChar(&mSSID, value); }
|
||||
|
||||
char* password() { return mPassword; }
|
||||
void password(const char* value) { assignChar(&mPassword, value); }
|
||||
|
||||
IPAddress ip() { return mIP; }
|
||||
void ip(IPAddress value) { mIP = value; }
|
||||
|
||||
IPAddress subnetMask() { return mSubnetMask; }
|
||||
void subnetMask(IPAddress value) { mSubnetMask = value; }
|
||||
|
||||
IPAddress gateway() { return mGateway; }
|
||||
void gateway(IPAddress value) { mGateway = value; }
|
||||
};
|
||||
|
||||
#endif
|
@ -1,6 +1,7 @@
|
||||
#include "stairs.h"
|
||||
#include <Math.h>
|
||||
#include <FS.h>
|
||||
#include "stairs.h"
|
||||
#include "debug.h"
|
||||
|
||||
|
||||
|
||||
@ -19,11 +20,13 @@ void Stairs::init(PCA9685* pwmDriver)
|
||||
{
|
||||
this->useScaling = false;
|
||||
|
||||
/*
|
||||
for (uint8_t i = 0; i < StepCount; i++)
|
||||
{
|
||||
this->ranges[i].start = IStairs::Off;
|
||||
this->ranges[i].end = IStairs::On;
|
||||
}
|
||||
*/
|
||||
|
||||
this->pwmDriver = pwmDriver;
|
||||
|
||||
@ -35,7 +38,7 @@ void Stairs::init(PCA9685* pwmDriver)
|
||||
|
||||
uint8_t Stairs::getCount()
|
||||
{
|
||||
return StepCount;
|
||||
return 0;//StepCount;
|
||||
}
|
||||
|
||||
|
||||
@ -49,8 +52,10 @@ void Stairs::setAll(uint16_t brightness)
|
||||
{
|
||||
//pwmDriver->setAll(this->getPWMValue(brightness));
|
||||
|
||||
/*
|
||||
for (uint8_t step = 0; step < StepCount; step++)
|
||||
pwmDriver->setPWM(step, this->getPWMValue(step, brightness));
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
@ -63,7 +68,7 @@ uint16_t Stairs::getPWMValue(uint8_t step, uint16_t brightness)
|
||||
return brightness;
|
||||
}
|
||||
|
||||
if (step < 0 || step >= StepCount)
|
||||
if (step < 0 || step >= getCount())
|
||||
{
|
||||
_dln("Step out of bounds, returning input");
|
||||
return brightness;
|
||||
@ -141,7 +146,7 @@ void Stairs::writeRange()
|
||||
Header header;
|
||||
header.version = 1;
|
||||
header.useScaling = this->useScaling;
|
||||
header.rangeCount = StepCount;
|
||||
header.rangeCount = getCount();
|
||||
|
||||
f.write((uint8_t*)&header, sizeof(Header));
|
||||
f.write((uint8_t*)&this->ranges, sizeof(this->ranges));
|
||||
|
@ -2,8 +2,8 @@
|
||||
#define __Stairs
|
||||
|
||||
#include "components/PCA9685.h"
|
||||
#include "modes/base.h"
|
||||
#include "config.h"
|
||||
#include "mode.h"
|
||||
|
||||
|
||||
struct Range
|
||||
@ -19,7 +19,7 @@ class Stairs : public IStairs
|
||||
PCA9685* pwmDriver;
|
||||
|
||||
bool useScaling;
|
||||
Range ranges[StepCount];
|
||||
Range ranges[16];
|
||||
|
||||
protected:
|
||||
void readRange();
|
||||
|
@ -1,23 +0,0 @@
|
||||
$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,2 +0,0 @@
|
||||
& .\updateversion.ps1
|
||||
& platformio run --target upload
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"directory": "static/bower_components"
|
||||
}
|
380
web/app.js
380
web/app.js
@ -1,160 +1,248 @@
|
||||
var fs = require('fs');
|
||||
var md5File = require('md5-file');
|
||||
var express = require('express');
|
||||
var semverUtils = require('semver-utils')
|
||||
var client = require('./client');
|
||||
|
||||
var httpPort = 3127;
|
||||
|
||||
var stairsHost = '10.138.2.25';
|
||||
var stairsUdpPort = 3126;
|
||||
|
||||
var firmwareFile = './update/firmware.bin';
|
||||
var alwaysUpdate = true;
|
||||
|
||||
|
||||
function requireNoCache(filename)
|
||||
function startApp()
|
||||
{
|
||||
delete require.cache[require.resolve(filename)];
|
||||
return require(filename);
|
||||
}
|
||||
|
||||
|
||||
function isNewer(version1, version2)
|
||||
{
|
||||
if (alwaysUpdate) return true;
|
||||
|
||||
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 (parseInt(version1.build, 10) > parseInt(version2.build, 10)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
client.init(stairsHost, stairsUdpPort);
|
||||
|
||||
|
||||
var app = express();
|
||||
|
||||
app.get('/ping', function(req, res)
|
||||
{
|
||||
client.ping(function(data, error)
|
||||
{
|
||||
if (error)
|
||||
res.status(500);
|
||||
|
||||
res.send(data);
|
||||
var i18n = new VueI18n({
|
||||
locale: navigator.language,
|
||||
fallbackLocale: 'en',
|
||||
messages: messages
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/getMode', function(req, res)
|
||||
{
|
||||
client.getMode(function(data, error)
|
||||
{
|
||||
if (error)
|
||||
res.status(500);
|
||||
var app = new Vue({
|
||||
el: '#app',
|
||||
|
||||
res.send(data);
|
||||
});
|
||||
});
|
||||
i18n: i18n,
|
||||
|
||||
app.get('/setMode/:mode', function(req, res)
|
||||
{
|
||||
client.setMode(req.params.mode, req.query, function(data, error)
|
||||
{
|
||||
if (error)
|
||||
res.status(500);
|
||||
data: {
|
||||
loading: true,
|
||||
saving: false,
|
||||
loadingIndicator: '|',
|
||||
|
||||
res.send(data);
|
||||
});
|
||||
});
|
||||
activeTab: 'status',
|
||||
|
||||
app.get('/getRange', function(req, res)
|
||||
{
|
||||
client.getRange(function(data, error)
|
||||
{
|
||||
if (error)
|
||||
res.status(500);
|
||||
version: {
|
||||
systemID: 'loading...',
|
||||
version: 'loading...',
|
||||
},
|
||||
|
||||
res.send(data);
|
||||
});
|
||||
});
|
||||
wifiStatus: {
|
||||
ap: {
|
||||
enabled: false,
|
||||
ip: '0.0.0.0'
|
||||
},
|
||||
station: {
|
||||
enabled: false,
|
||||
status: 0,
|
||||
ip: '0.0.0.0'
|
||||
}
|
||||
},
|
||||
|
||||
app.get('/setRange', function(req, res)
|
||||
{
|
||||
client.setRange(req.query, function(data, error)
|
||||
{
|
||||
if (error)
|
||||
res.status(500);
|
||||
connection: {
|
||||
hostname: null,
|
||||
accesspoint: true,
|
||||
station: false,
|
||||
ssid: null,
|
||||
password: null,
|
||||
dhcp: true,
|
||||
ip: null,
|
||||
subnetmask: null,
|
||||
gateway: null
|
||||
},
|
||||
|
||||
res.send(data);
|
||||
});
|
||||
});
|
||||
steps: [
|
||||
{ value: 50 },
|
||||
{ value: 0 },
|
||||
{ value: 0 },
|
||||
{ value: 0 },
|
||||
{ value: 0 },
|
||||
{ value: 70 },
|
||||
{ value: 0 },
|
||||
{ value: 0 },
|
||||
{ value: 0 },
|
||||
{ value: 0 },
|
||||
{ value: 25 },
|
||||
{ value: 0 },
|
||||
{ value: 0 },
|
||||
{ value: 0 }
|
||||
]
|
||||
},
|
||||
|
||||
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))
|
||||
{
|
||||
console.log('Sending update');
|
||||
|
||||
md5File(firmwareFile, function(err, hash)
|
||||
created: function()
|
||||
{
|
||||
if (err)
|
||||
var self = this;
|
||||
|
||||
document.title = i18n.t('title');
|
||||
self.startLoadingIndicator();
|
||||
|
||||
self.updateWiFiStatus();
|
||||
setInterval(self.updateWiFiStatus, 5000);
|
||||
|
||||
axios.get('/api/version')
|
||||
.then(function(response)
|
||||
{
|
||||
if (typeof response.data == 'object')
|
||||
self.version = response.data;
|
||||
})
|
||||
.catch(function(error)
|
||||
{
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
axios.all([
|
||||
axios.get('/api/connection')
|
||||
.then(function(response)
|
||||
{
|
||||
if (typeof response.data == 'object')
|
||||
self.connection = response.data;
|
||||
})
|
||||
.catch(function(error)
|
||||
{
|
||||
console.log(error);
|
||||
})/*,
|
||||
|
||||
axios.get('/api/actions')
|
||||
.then(function(response)
|
||||
{
|
||||
if (typeof response.data == 'object')
|
||||
self.actions = response.data;
|
||||
})
|
||||
.catch(function(error)
|
||||
{
|
||||
console.log(error);
|
||||
})*/
|
||||
])
|
||||
.then(axios.spread(function(acct, perms) {
|
||||
self.stopLoadingIndicator();
|
||||
self.loading = false;
|
||||
}));
|
||||
},
|
||||
|
||||
methods: {
|
||||
applyConnection: function()
|
||||
{
|
||||
res.sendStatus(500);
|
||||
return;
|
||||
var self = this;
|
||||
if (self.saving) return;
|
||||
|
||||
self.saving = true;
|
||||
|
||||
axios.post('/api/connection', {
|
||||
hostname: self.connection.hostname,
|
||||
accesspoint: self.connection.accesspoint,
|
||||
station: self.connection.station,
|
||||
ssid: self.connection.ssid,
|
||||
password: self.connection.password,
|
||||
dhcp: self.connection.dhcp,
|
||||
ip: self.connection.ip,
|
||||
subnetmask: self.connection.subnetmask,
|
||||
gateway: self.connection.gateway,
|
||||
})
|
||||
.then(function(response)
|
||||
{
|
||||
})
|
||||
.catch(function(error)
|
||||
{
|
||||
console.log(error);
|
||||
})
|
||||
.then(function()
|
||||
{
|
||||
self.saving = false;
|
||||
})
|
||||
},
|
||||
|
||||
startLoadingIndicator: function()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
self.loadingStage = 0;
|
||||
self.loadingTimer = setInterval(function()
|
||||
{
|
||||
self.loadingStage++;
|
||||
console.log(self.loadingStage);
|
||||
switch (self.loadingStage)
|
||||
{
|
||||
case 1: self.loadingIndicator = '/'; break;
|
||||
case 2: self.loadingIndicator = '-'; break;
|
||||
case 3: self.loadingIndicator = '\\'; break;
|
||||
case 4: self.loadingIndicator = '|'; self.loadingStage = 0; break;
|
||||
}
|
||||
}, 250);
|
||||
},
|
||||
|
||||
stopLoadingIndicator: function()
|
||||
{
|
||||
clearInterval(this.loadingTimer);
|
||||
},
|
||||
|
||||
getWiFiStationStatus: function()
|
||||
{
|
||||
if (!this.wifiStatus.station.enabled)
|
||||
return 'disconnected';
|
||||
|
||||
switch (this.wifiStatus.station.status)
|
||||
{
|
||||
case 0: // WL_IDLE_STATUS
|
||||
case 2: // WL_SCAN_COMPLETED
|
||||
return 'connecting';
|
||||
|
||||
case 1: // WL_NO_SSID_AVAIL
|
||||
case 4: // WL_CONNECT_FAILED
|
||||
case 5: // WL_CONNECTION_LOST
|
||||
return 'error';
|
||||
|
||||
case 3: // WL_CONNECTED
|
||||
return 'connected';
|
||||
|
||||
case 6: // WL_DISCONNECTED
|
||||
default:
|
||||
return 'disconnected';
|
||||
}
|
||||
},
|
||||
|
||||
getWiFiStationStatusText: function()
|
||||
{
|
||||
if (!this.wifiStatus.station.enabled)
|
||||
return i18n.t('wifiStatus.stationmode.disabled');
|
||||
|
||||
switch (this.wifiStatus.station.status)
|
||||
{
|
||||
case 0: // WL_IDLE_STATUS
|
||||
return i18n.t('wifiStatus.stationmode.idle');
|
||||
|
||||
case 1: // WL_NO_SSID_AVAIL
|
||||
return i18n.t('wifiStatus.stationmode.noSSID');
|
||||
|
||||
case 2: // WL_SCAN_COMPLETED
|
||||
return i18n.t('wifiStatus.stationmode.scanCompleted');
|
||||
|
||||
case 3: // WL_CONNECTED
|
||||
return this.wifiStatus.station.ip;
|
||||
|
||||
case 4: // WL_CONNECT_FAILED
|
||||
return i18n.t('wifiStatus.stationmode.connectFailed');
|
||||
|
||||
case 5: // WL_CONNECTION_LOST
|
||||
return i18n.t('wifiStatus.stationmode.connectionLost');
|
||||
|
||||
case 6: // WL_DISCONNECTED
|
||||
default:
|
||||
return i18n.t('wifiStatus.stationmode.disconnected');
|
||||
}
|
||||
},
|
||||
|
||||
updateWiFiStatus: function()
|
||||
{
|
||||
var self = this;
|
||||
if (self.saving) return;
|
||||
|
||||
axios.get('/api/connection/status')
|
||||
.then(function(response)
|
||||
{
|
||||
if (typeof response.data == 'object')
|
||||
self.wifiStatus = response.data;
|
||||
})
|
||||
.catch(function(error)
|
||||
{
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
res.set('Content-Length', fs.statSync(firmwareFile).size);
|
||||
res.set('x-MD5', hash);
|
||||
res.download(firmwareFile);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log('No update required');
|
||||
res.sendStatus(304);
|
||||
}
|
||||
});
|
||||
|
||||
app.use(express.static(__dirname + '/static'));
|
||||
|
||||
|
||||
|
||||
app.listen(httpPort, function ()
|
||||
{
|
||||
console.log('Stairs ReST service running on port ' + httpPort);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "stairs",
|
||||
"description": "Stairs lighting project",
|
||||
"main": "index.html",
|
||||
"authors": [
|
||||
"Mark van Renswoude"
|
||||
],
|
||||
"license": "ISC",
|
||||
"homepage": "",
|
||||
"private": true,
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"src/bower_components"
|
||||
],
|
||||
"dependencies": {
|
||||
"knockout": "^3.4.2",
|
||||
"jquery": "^3.2.1",
|
||||
"crossroads": "^0.12.2",
|
||||
"hasher": "^1.2.0",
|
||||
"requirejs": "^2.3.3",
|
||||
"text": "requirejs/text#^2.0.15",
|
||||
"bootstrap": "v4.0.0-alpha.6",
|
||||
"nprogress": "^0.2.0"
|
||||
}
|
||||
}
|
342
web/client.js
342
web/client.js
@ -1,342 +0,0 @@
|
||||
var dgram = require('dgram');
|
||||
var protocol = require('./protocol');
|
||||
var BufferReader = require('buffer-reader');
|
||||
|
||||
|
||||
var responseHandlers = {};
|
||||
|
||||
function registerResponseHandler(command, callback)
|
||||
{
|
||||
if (!responseHandlers.hasOwnProperty(command))
|
||||
responseHandlers[command] = [callback];
|
||||
else
|
||||
responseHandlers[command].push(callback);
|
||||
}
|
||||
|
||||
|
||||
function callResponseHandlers(command, reader, error)
|
||||
{
|
||||
if (!responseHandlers.hasOwnProperty(command))
|
||||
return;
|
||||
|
||||
newHandlers = [];
|
||||
responseHandlers[command].forEach(function(callback)
|
||||
{
|
||||
if (!callback(reader, error))
|
||||
newHandlers.push(callback);
|
||||
});
|
||||
|
||||
responseHandlers[command] = newHandlers;
|
||||
}
|
||||
|
||||
|
||||
var serverHost = '';
|
||||
var serverPort = 0;
|
||||
|
||||
var client = dgram.createSocket('udp4');
|
||||
client.on('message', function (message, remote)
|
||||
{
|
||||
console.log(message.toString('hex'));
|
||||
if (message.length < 2)
|
||||
return;
|
||||
|
||||
var reader = new BufferReader(message);
|
||||
|
||||
if (reader.nextInt8() !== protocol.Command.Reply)
|
||||
return;
|
||||
|
||||
var command = reader.nextInt8();
|
||||
if (command === protocol.Command.Error)
|
||||
callResponseHandlers(reader.nextInt8(), reader, true)
|
||||
else
|
||||
callResponseHandlers(command, reader, false);
|
||||
});
|
||||
|
||||
|
||||
function requestResponse(buffer, callback, withTimeout)
|
||||
{
|
||||
if (buffer === null || buffer.length == 0) return;
|
||||
console.log('> ' + buffer.toString('hex'));
|
||||
|
||||
var command = buffer.readInt8(0);
|
||||
var cancelled = false;
|
||||
|
||||
if (typeof(withTimeout) == 'undefined') withTimeout = true;
|
||||
if (withTimeout)
|
||||
{
|
||||
var timeout = setTimeout(function()
|
||||
{
|
||||
cancelled = true;
|
||||
callback(null, true);
|
||||
clearTimeout(timeout);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
registerResponseHandler(command, function(reader, error)
|
||||
{
|
||||
if (cancelled) return;
|
||||
if (withTimeout) clearTimeout(timeout);
|
||||
|
||||
callback(reader, error);
|
||||
return true;
|
||||
});
|
||||
|
||||
client.send(buffer, 0, buffer.length, serverPort, serverHost, function(err, bytes)
|
||||
{
|
||||
if (err)
|
||||
onError();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function readModeData(mode, reader)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case protocol.Mode.Static:
|
||||
return {
|
||||
brightness: reader.nextInt16LE(),
|
||||
easeTime: reader.nextInt16LE()
|
||||
};
|
||||
|
||||
case protocol.Mode.Custom:
|
||||
var values = [];
|
||||
while (reader.tell() < reader.buf.length)
|
||||
values.push(reader.nextInt16LE());
|
||||
|
||||
return {
|
||||
brightness: values
|
||||
};
|
||||
|
||||
case protocol.Mode.Alternate:
|
||||
return {
|
||||
interval: reader.nextInt16LE(),
|
||||
brightness: reader.nextInt16LE()
|
||||
};
|
||||
|
||||
case protocol.Mode.Slide:
|
||||
return {
|
||||
interval: reader.nextInt16LE(),
|
||||
brightness: reader.nextInt16LE(),
|
||||
direction: reader.nextInt8(),
|
||||
fadeOutTime: reader.nextInt16LE()
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function readRangeData(reader)
|
||||
{
|
||||
var data = { useScaling: reader.nextInt8() == 1, values: [] };
|
||||
|
||||
while (reader.tell() < reader.buf.length)
|
||||
data.values.push({ start: reader.nextInt16LE(), end: reader.nextInt16LE() });
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
function lsb(value) { return value & 0xFF; }
|
||||
function msb(value) { return (value >> 8) & 0xFF; }
|
||||
|
||||
|
||||
function getBrightness(value)
|
||||
{
|
||||
if (typeof(value) == 'string' && value.substr(-1) === '%')
|
||||
return (Number(value.substr(0, value.length - 1)) * 4096 / 100);
|
||||
|
||||
return Number(value) || 0;
|
||||
}
|
||||
|
||||
|
||||
function writeModeData(mode, data)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case protocol.Mode.Static:
|
||||
var brightness = getBrightness(data.brightness);
|
||||
|
||||
return new Buffer([protocol.Command.SetMode, mode, lsb(brightness), msb(brightness), lsb(500), msb(500)]);
|
||||
|
||||
case protocol.Mode.Custom:
|
||||
var brightness = typeof(data.brightness) !== 'undefined' ? data.brightness.split(',') : [];
|
||||
|
||||
var valueCount = Math.min(16, brightness.length);
|
||||
var buffer = Buffer.alloc(2 + (valueCount * 2));
|
||||
buffer.writeUInt8(protocol.Command.SetMode, 0);
|
||||
buffer.writeUInt8(mode, 1);
|
||||
|
||||
for (var index = 0; index < valueCount; index++)
|
||||
buffer.writeUInt16LE(getBrightness(brightness[index]), 2 + (index * 2));
|
||||
|
||||
return buffer;
|
||||
|
||||
case protocol.Mode.Alternate:
|
||||
var brightness = getBrightness(data.brightness);
|
||||
if (typeof(data.interval) == 'undefined') data.interval = 500;
|
||||
|
||||
return new Buffer([protocol.Command.SetMode, mode,
|
||||
lsb(data.interval), msb(data.interval),
|
||||
lsb(brightness), msb(brightness)]);
|
||||
|
||||
case protocol.Mode.Slide:
|
||||
var brightness = getBrightness(data.brightness);
|
||||
if (typeof(data.interval) == 'undefined') data.interval = 500;
|
||||
if (typeof(data.direction) == 'undefined') data.direction = 0;
|
||||
if (typeof(data.fadeOutTime) == 'undefined') data.fadeOutTime = 0;
|
||||
|
||||
return new Buffer([protocol.Command.SetMode, mode,
|
||||
lsb(data.interval), msb(data.interval),
|
||||
lsb(brightness), msb(brightness),
|
||||
data.direction,
|
||||
lsb(data.fadeOutTime), msb(data.fadeOutTime)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function writeRangeData(data)
|
||||
{
|
||||
var start = typeof(data.start) !== 'undefined' ? data.start.split(',') : [];
|
||||
var end = typeof(data.end) !== 'undefined' ? data.end.split(',') : [];
|
||||
|
||||
var valueCount = Math.min(16, start.length, end.length);
|
||||
var buffer = Buffer.alloc(2 + (valueCount * 4));
|
||||
buffer.writeUInt8(protocol.Command.SetRange, 0);
|
||||
buffer.writeUInt8(data.useScaling ? 1 : 0, 1);
|
||||
|
||||
for (var index = 0; index < valueCount; index++)
|
||||
{
|
||||
buffer.writeUInt16LE(getBrightness(start[index]), 2 + (index * 4));
|
||||
buffer.writeUInt16LE(getBrightness(end[index]), 4 + (index * 4));
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
module.exports =
|
||||
{
|
||||
init: function(host, port)
|
||||
{
|
||||
serverHost = host;
|
||||
serverPort = port;
|
||||
},
|
||||
|
||||
|
||||
ping: function(callback)
|
||||
{
|
||||
requestResponse(new Buffer([protocol.Command.Ping]),
|
||||
function(reader, error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
callback(
|
||||
{
|
||||
stepCount: reader.nextInt8()
|
||||
}, false);
|
||||
}
|
||||
else
|
||||
callback(null, true);
|
||||
});
|
||||
},
|
||||
|
||||
getMode: function(callback)
|
||||
{
|
||||
requestResponse(new Buffer([protocol.Command.GetMode]),
|
||||
function(reader, error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
var data = { mode: reader.nextInt8() };
|
||||
data.data = readModeData(data.mode, reader);
|
||||
callback(data, false);
|
||||
}
|
||||
else
|
||||
callback(null, true);
|
||||
});
|
||||
},
|
||||
|
||||
setMode: function(mode, data, callback)
|
||||
{
|
||||
if (!protocol.Mode.hasOwnProperty(mode))
|
||||
return;
|
||||
|
||||
requestResponse(writeModeData(protocol.Mode[mode], data),
|
||||
function(reader, error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
var data = { mode: reader.nextInt8() };
|
||||
data.data = readModeData(data.mode, reader);
|
||||
callback(data, false);
|
||||
}
|
||||
else
|
||||
callback(null, true);
|
||||
});
|
||||
},
|
||||
|
||||
getRange: function(callback)
|
||||
{
|
||||
requestResponse(new Buffer([protocol.Command.GetRange]),
|
||||
function(reader, error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
callback(readRangeData(reader), false);
|
||||
}
|
||||
else
|
||||
callback(null, true);
|
||||
});
|
||||
},
|
||||
|
||||
setRange: function(data, callback)
|
||||
{
|
||||
requestResponse(writeRangeData(data),
|
||||
function(reader, error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
callback(readRangeData(reader), false);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
1
web/dist/bundle.css
vendored
Normal file
1
web/dist/bundle.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/bundle.js
vendored
Normal file
1
web/dist/bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,96 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var ts = require('gulp-typescript');
|
||||
var uglify = require('gulp-uglify');
|
||||
var sass = require('gulp-sass');
|
||||
var cleanCSS = require('gulp-clean-css');
|
||||
var concat = require('gulp-concat');
|
||||
var watch = require('gulp-debounced-watch');
|
||||
var plumber = require('gulp-plumber');
|
||||
|
||||
|
||||
var config =
|
||||
{
|
||||
dest: 'static/assets/dist/',
|
||||
typescriptBase: 'static/assets/ts/',
|
||||
sassBase: 'static/assets/sass/',
|
||||
|
||||
assets: ['static/assets/ts/**/*.html', 'static/assets/js/**/*.js']
|
||||
};
|
||||
|
||||
config.typescriptSrc = config.typescriptBase + '**/*.ts';
|
||||
config.sassSrc = config.sassBase + '**/*.scss';
|
||||
|
||||
|
||||
gulp.task('default',
|
||||
[
|
||||
'compileTypescript',
|
||||
'compileSass',
|
||||
'copyAssets'
|
||||
],
|
||||
function(){});
|
||||
|
||||
gulp.task('watch',
|
||||
[
|
||||
'compileTypescript',
|
||||
'compileSass',
|
||||
'copyAssets'
|
||||
],
|
||||
function()
|
||||
{
|
||||
watch(config.typescriptSrc, function() { gulp.start('compileTypescript'); });
|
||||
watch(config.sassSrc, function() { gulp.start('compileSass'); });
|
||||
watch(config.assets, function() { gulp.start('copyAssets'); });
|
||||
});
|
||||
|
||||
|
||||
gulp.task('compileTypescript', function()
|
||||
{
|
||||
return gulp.src(config.typescriptSrc, { base: config.typescriptBase })
|
||||
.pipe(plumber({
|
||||
errorHandler: function (error)
|
||||
{
|
||||
console.log(error.message);
|
||||
this.emit('end');
|
||||
}}))
|
||||
.pipe(ts(
|
||||
{
|
||||
noImplicitAny: true,
|
||||
removeComments: true,
|
||||
preserveConstEnums: true,
|
||||
sourceMap: true,
|
||||
module: 'amd'
|
||||
}))
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest(config.dest));
|
||||
});
|
||||
|
||||
|
||||
gulp.task('compileSass', function()
|
||||
{
|
||||
return gulp.src(config.sassSrc)
|
||||
.pipe(plumber({
|
||||
errorHandler: function (error)
|
||||
{
|
||||
console.log(error.message);
|
||||
this.emit('end');
|
||||
}}))
|
||||
.pipe(sass())
|
||||
.pipe(concat('bundle.css'))
|
||||
.pipe(cleanCSS())
|
||||
.pipe(gulp.dest(config.dest));
|
||||
});
|
||||
|
||||
|
||||
gulp.task('copyAssets', function()
|
||||
{
|
||||
return gulp.src(config.assets)
|
||||
.pipe(plumber({
|
||||
errorHandler: function (error)
|
||||
{
|
||||
console.log(error.message);
|
||||
this.emit('end');
|
||||
}}))
|
||||
.pipe(gulp.dest(config.dest));
|
||||
});
|
124
web/index.html
Normal file
124
web/index.html
Normal file
@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="bundle.css">
|
||||
<script src="bundle.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div v-cloak>
|
||||
<div id="container">
|
||||
<div class="header">
|
||||
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAXBJREFUeNrsWdENgjAQLcYBcAPcACdQN4AN2MC4gRsYJ9ENdAPYADaADepdUhM1qFjurgp9ScMHgd713V3ftUp5eHgMF1rrGEau6VHD2HMbH5qJOLHra+fkzbsYRshM8pLTgUIgSgvuMMoYw+iMYTr6QhNYMrNiNywILlyVKdcyyPsmcRsyU50kgHtQQu2AdNJFpDkAK4I/LIWMb2AsIBcq0iRGeQGPTZfV6QE0+gDGF2rosGEgMQxIAhnZAiPN84upRf0/OlpsLCBp3yq0chgtCUUZrRw6QDO3EWHSqF9tarZaSKJXeJDdbQlsE0KD6JN/KoRsGhxXKClCKHJIfkThwMWhA6dBSolRijkJOd1ZUv9yQ9OpqbHpiaUVaEbZE7tIIjoBKXysos1c4V8ebLEdbv2bcMsE7gjuBdvXRSJ4F+/wqIXXrIGwmX3zwacLDheNO91OjLQKd14VMDCnYgCxVjI3NTe1mSoPD49x4SrAAG9qPn4eovCMAAAAAElFTkSuQmCC" />
|
||||
<h1>{{ $t('title') }}</h1>
|
||||
<h2>{{ version.systemID !== null ? $t('systemID') + ': ' + version.systemID : '' }}</h2>
|
||||
|
||||
<div class="wifistatus">
|
||||
<div class="connection">
|
||||
<div class="indicator" :data-status="wifiStatus.ap.enabled ? 'connected' : 'disconnected'"></div> {{ $t('wifiStatus.accesspoint.title') }} {{ wifiStatus.ap.enabled ? wifiStatus.ap.ip : $t('wifiStatus.accesspoint.disabled') }}
|
||||
</div>
|
||||
<div class="connection">
|
||||
<div class="indicator" :data-status="getWiFiStationStatus()"></div> {{ $t('wifiStatus.stationmode.title') }} {{ getWiFiStationStatusText() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
{{ $t('loading') }} {{ loadingIndicator }}
|
||||
</div>
|
||||
|
||||
<div v-if="!loading">
|
||||
<div class="tabs">
|
||||
<button class="button" :class="{ 'button-outline': activeTab != 'status' }" @click="activeTab = 'status'">{{ $t('status.tabTitle') }}</button>
|
||||
<button class="button" :class="{ 'button-outline': activeTab != 'triggers' }" @click="activeTab = 'triggers'">{{ $t('triggers.tabTitle') }}</button>
|
||||
<button class="button" :class="{ 'button-outline': activeTab != 'connection' }" @click="activeTab = 'connection'">{{ $t('connection.tabTitle') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab == 'status'">
|
||||
<h3>{{ $t('status.title') }}</h3>
|
||||
|
||||
<div class="slidecontainer" v-for="(step, index) in steps">
|
||||
{{ index + 1 }}
|
||||
<input type="range" min="0" max="100" class="slider" v-model="step.value">
|
||||
{{ step.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab == 'triggers'">
|
||||
<form @submit.prevent="applyTriggers">
|
||||
<fieldset>
|
||||
<h3>{{ $t('triggers.timeTitle') }}</h3>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<h3>{{ $t('triggers.motionTitle') }}</h3>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab == 'connection'">
|
||||
<form @submit.prevent="applyConnection">
|
||||
<fieldset>
|
||||
<h3>{{ $t('connection.title') }}</h3>
|
||||
|
||||
<input type="checkbox" id="accesspoint" v-model="connection.accesspoint">
|
||||
<label class="label-inline" for="accesspoint">{{ $t('connection.accesspoint') }}</label>
|
||||
<span class="hint">{{ $t('connection.accesspointHint') }}</span>
|
||||
|
||||
<input type="checkbox" id="station" v-model="connection.station">
|
||||
<label class="label-inline" for="station">{{ $t('connection.stationmode') }}</label>
|
||||
<span class="hint">{{ $t('connection.stationmodeHint') }}</span>
|
||||
|
||||
<label for="ssid">{{ $t('connection.ssid') }}</label>
|
||||
<input type="text" id="ssid" v-model="connection.ssid" :disabled="!connection.station">
|
||||
|
||||
<label for="password">{{ $t('connection.password') }}</label>
|
||||
<input type="password" id="password" v-model="connection.password" :disabled="!connection.station">
|
||||
|
||||
<input type="checkbox" id="dhcp" v-model="connection.dhcp" :disabled="!connection.station">
|
||||
<label class="label-inline" for="dhcp">{{ $t('connection.dhcp') }}</label>
|
||||
<span class="hint">{{ $t('connection.dhcpHint') }}</span>
|
||||
|
||||
|
||||
<div class="suboptions">
|
||||
<label for="ip">{{ $t('connection.ipaddress') }}</label>
|
||||
<input type="text" id="ip" v-model="connection.ip" :disabled="!connection.station || connection.dhcp">
|
||||
|
||||
<label for="subnetmask">{{ $t('connection.subnetmask') }}</label>
|
||||
<input type="text" id="subnetmask" v-model="connection.subnetmask" :disabled="!connection.station || connection.dhcp">
|
||||
|
||||
<label for="gateway">{{ $t('connection.gateway') }}</label>
|
||||
<input type="text" id="gateway" v-model="connection.gateway" :disabled="!connection.station || connection.dhcp">
|
||||
</div>
|
||||
|
||||
|
||||
<label for="hostname">{{ $t('connection.hostname') }}</label>
|
||||
<input type="text" :placeholder="$t('connection.hostnamePlaceholder')" id="hostname" v-model="connection.hostname" :disabled="!connection.station">
|
||||
|
||||
<div class="buttons">
|
||||
<input class="button-primary" type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="version">
|
||||
{{ $t('copyright') }}<br>
|
||||
{{ version.version !== null ? $t('firmwareVersion') + version.version : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script language="javascript">
|
||||
console.log('Initializing...');
|
||||
startApp();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
127
web/lang.js
Normal file
127
web/lang.js
Normal file
@ -0,0 +1,127 @@
|
||||
var messages = {
|
||||
en: {
|
||||
title: 'Stairs',
|
||||
systemID: 'System ID',
|
||||
firmwareVersion: 'Firmware version: ',
|
||||
copyright: 'Copyright © 2017 Mark van Renswoude',
|
||||
loading: 'Please wait, loading configuration...',
|
||||
|
||||
applyButton: 'Apply',
|
||||
applyButtonSaving: 'Saving...',
|
||||
|
||||
wifiStatus: {
|
||||
accesspoint: {
|
||||
title: 'AP: ',
|
||||
disabled: 'Disabled'
|
||||
},
|
||||
|
||||
stationmode: {
|
||||
title: 'WiFi: ',
|
||||
disabled: 'Disabled',
|
||||
idle: 'Idle',
|
||||
noSSID: 'SSID not found',
|
||||
scanCompleted: 'Scan completed',
|
||||
connectFailed: 'Failed to connect',
|
||||
connectionLost: 'Connection lost',
|
||||
disconnected: 'Disconnected'
|
||||
}
|
||||
},
|
||||
|
||||
status: {
|
||||
tabTitle: 'Status',
|
||||
title: 'Current status'
|
||||
},
|
||||
|
||||
triggers: {
|
||||
tabTitle: 'Triggers',
|
||||
timeTitle: 'Time',
|
||||
motionTitle: 'Motion'
|
||||
},
|
||||
|
||||
connection: {
|
||||
tabTitle: 'Connection',
|
||||
title: 'Connection parameters',
|
||||
|
||||
accesspoint: 'Enable access point',
|
||||
accesspointHint: 'Allows for a direct connection from your device to this Stairs module for configuration purposes. The Stairs configuration is available on http://192.168.1.4/ when you are connected to it. Turn it off as soon as station mode is configured, as it is not secured in any way. You can always turn this option back on by pushing the access point button until the LED lights up.',
|
||||
|
||||
stationmode: 'Enable station mode',
|
||||
stationmodeHint: 'Connect this Stairs module to your own WiFi router. Please enter the SSID, password and further configuration below.',
|
||||
|
||||
ssid: 'SSID',
|
||||
password: 'Password',
|
||||
|
||||
dhcp: 'Use DHCP',
|
||||
dhcpHint: 'Automatically assigns an IP address to this Stairs module. You probably want to keep this on unless you know what you\'re doing.',
|
||||
|
||||
ipaddress: 'IP address',
|
||||
subnetmask: 'Subnet mask',
|
||||
gateway: 'Gateway',
|
||||
hostname: 'Hostname',
|
||||
hostnamePlaceholder: 'Default: mac address'
|
||||
}
|
||||
},
|
||||
|
||||
nl: {
|
||||
title: 'Trap',
|
||||
systemID: 'Systeem ID',
|
||||
firmwareVersion: 'Firmware versie: ',
|
||||
copyright: 'Copyright © 2017 Mark van Renswoude',
|
||||
loading: 'Een ogenblik geduld, bezig met laden van configuratie...',
|
||||
|
||||
applyButton: 'Apply',
|
||||
applyButtonSaving: 'Saving...',
|
||||
|
||||
wifiStatus: {
|
||||
accesspoint: {
|
||||
title: 'AP: ',
|
||||
disabled: 'Uitgeschakeld'
|
||||
},
|
||||
|
||||
stationmode: {
|
||||
title: 'WiFi: ',
|
||||
disabled: 'Uitgeschakeld',
|
||||
idle: 'Slaapstand',
|
||||
noSSID: 'SSID niet gevonden',
|
||||
scanCompleted: 'Scan afgerond',
|
||||
connectFailed: 'Kan geen verbinding maken',
|
||||
connectionLost: 'Verbinding verloren',
|
||||
disconnected: 'Niet verbonden'
|
||||
}
|
||||
},
|
||||
|
||||
status: {
|
||||
tabTitle: 'Status',
|
||||
title: 'Huidige status'
|
||||
},
|
||||
|
||||
triggers: {
|
||||
tabTitle: 'Triggers',
|
||||
timeTitle: 'Tijd',
|
||||
motionTitle: 'Beweging'
|
||||
},
|
||||
|
||||
connection: {
|
||||
tabTitle: 'Verbinding',
|
||||
title: 'Verbinding configuratie',
|
||||
|
||||
accesspoint: 'Access point inschakelen',
|
||||
accesspointhint: 'Maakt het mogelijk om een directe connectie vanaf een apparaat naar deze Trap module te maken om de module te configureren. De Trap module is te benaderen via http://192.168.1.4/ nadat je connectie hebt gemaakt. Schakel deze optie uit na het configureren, aangezien deze niet beveiligd is. Je kunt deze optie ook inschakelen door op de Access point knop te drukken totdat de LED aan gaat.',
|
||||
|
||||
stationmode: 'Verbinding met WiFi maken',
|
||||
stationmodehint: 'Verbind deze Trap module aan je eigen WiFi router. Vul hieronder het SSID en wachtwoord in, en configureer eventuel de overige opties.',
|
||||
|
||||
ssid: 'SSID',
|
||||
password: 'Wachtwoord',
|
||||
|
||||
dhcp: 'Gebruik DHCP',
|
||||
dhcphint: 'Automatisch een IP adres toewijzen aan deze Trap module. Waarschijnlijk wil je deze optie aan laten, tenzij je weet waar je mee bezig bent.',
|
||||
|
||||
ipaddress: 'IP adres',
|
||||
subnetmask: 'Subnet masker',
|
||||
gateway: 'Gateway',
|
||||
hostname: 'Hostnaam',
|
||||
hostnamePlaceholder: 'Standaard: mac adres'
|
||||
}
|
||||
}
|
||||
}
|
333
web/logo.ai
Normal file
333
web/logo.ai
Normal file
@ -0,0 +1,333 @@
|
||||
%PDF-1.5
%âãÏÓ
|
||||
1 0 obj
<</Metadata 2 0 R/OCProperties<</D<</ON[5 0 R 21 0 R]/Order 22 0 R/RBGroups[]>>/OCGs[5 0 R 21 0 R]>>/Pages 3 0 R/Type/Catalog>>
endobj
2 0 obj
<</Length 9212/Subtype/XML/Type/Metadata>>stream
|
||||
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 ">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
|
||||
xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/">
|
||||
<xmp:CreatorTool>Adobe Illustrator CS6 (Windows)</xmp:CreatorTool>
|
||||
<xmp:CreateDate>2017-12-30T16:13+01:00</xmp:CreateDate>
|
||||
<xmp:MetadataDate>2017-12-30T16:17:16+01:00</xmp:MetadataDate>
|
||||
<xmp:ModifyDate>2017-12-30T16:17:16+01:00</xmp:ModifyDate>
|
||||
<xmp:Thumbnails>
|
||||
<rdf:Alt>
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<xmpGImg:width>248</xmpGImg:width>
|
||||
<xmpGImg:height>256</xmpGImg:height>
|
||||
<xmpGImg:format>JPEG</xmpGImg:format>
|
||||
<xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA
AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK
DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f
Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAD4AwER
AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA
AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB
UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE
1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ
qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy
obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp
0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo
+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q==</xmpGImg:image>
|
||||
</rdf:li>
|
||||
</rdf:Alt>
|
||||
</xmp:Thumbnails>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
|
||||
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
|
||||
xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/">
|
||||
<xmpTPg:NPages>1</xmpTPg:NPages>
|
||||
<xmpTPg:HasVisibleTransparency>False</xmpTPg:HasVisibleTransparency>
|
||||
<xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint>
|
||||
<xmpTPg:MaxPageSize rdf:parseType="Resource">
|
||||
<stDim:w>512.000000</stDim:w>
|
||||
<stDim:h>512.000000</stDim:h>
|
||||
<stDim:unit>Points</stDim:unit>
|
||||
</xmpTPg:MaxPageSize>
|
||||
<xmpTPg:SwatchGroups>
|
||||
<rdf:Seq>
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<xmpG:groupName>Default Swatch Group</xmpG:groupName>
|
||||
<xmpG:groupType>0</xmpG:groupType>
|
||||
</rdf:li>
|
||||
</rdf:Seq>
|
||||
</xmpTPg:SwatchGroups>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<dc:format>application/pdf</dc:format>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/">
|
||||
<illustrator:Type>Document</illustrator:Type>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
|
||||
xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#">
|
||||
<xmpMM:DocumentID>xmp.did:89DEF0EC73EDE7119CCADB31B93B2005</xmpMM:DocumentID>
|
||||
<xmpMM:InstanceID>uuid:b03a2483-4f32-4465-b083-e0550ab05b88</xmpMM:InstanceID>
|
||||
<xmpMM:OriginalDocumentID>xmp.did:89DEF0EC73EDE7119CCADB31B93B2005</xmpMM:OriginalDocumentID>
|
||||
<xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass>
|
||||
<xmpMM:DerivedFrom rdf:parseType="Resource"/>
|
||||
<xmpMM:History>
|
||||
<rdf:Seq>
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<stEvt:action>saved</stEvt:action>
|
||||
<stEvt:instanceID>xmp.iid:89DEF0EC73EDE7119CCADB31B93B2005</stEvt:instanceID>
|
||||
<stEvt:when>2017-12-30T16:13:01+01:00</stEvt:when>
|
||||
<stEvt:softwareAgent>Adobe Illustrator CS6 (Windows)</stEvt:softwareAgent>
|
||||
<stEvt:changed>/</stEvt:changed>
|
||||
</rdf:li>
|
||||
</rdf:Seq>
|
||||
</xmpMM:History>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<?xpacket end="w"?>
|
||||
endstream
endobj
3 0 obj
<</Count 1/Kids[7 0 R]/Type/Pages>>
endobj
7 0 obj
<</ArtBox[72.1367 53.875 458.887 458.125]/BleedBox[0.0 0.0 512.0 512.0]/Contents 23 0 R/LastModified(D:20171230161716+02'00')/MediaBox[0.0 0.0 512.0 512.0]/Parent 3 0 R/PieceInfo<</Illustrator 24 0 R>>/Resources<</ExtGState<</GS0 25 0 R>>/Properties<</MC0 21 0 R>>>>/Thumb 26 0 R/TrimBox[0.0 0.0 512.0 512.0]/Type/Page>>
endobj
23 0 obj
<</Filter/FlateDecode/Length 177>>stream
|
||||
H‰”‘Á
|
||||
Â0†ïyŠ¼Àº&M¶õj•<6A>0dîàˆxqÂôàëÛ
|
||||
º¦ ¡¥)ßßÐr°ì‚ÅÕ: L`Q‰_«HÛí¼aª}*†±Ù"”í`ñ|‡)¶6VdÄáºFfÂãévnØ8'ñ|<7C>úEÞÿàûÿó]•ûDyšó¿L9{¼7\Q<>âr
¥g¹…1ÄŠñ)–4ãV5º€¿íM.Wÿ!7]üŠž |