Ported framework from Stairs project

This commit is contained in:
Mark van Renswoude 2020-09-19 19:24:34 +02:00
parent 2ff24cb099
commit 0ad50a20b6
48 changed files with 15731 additions and 0 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules
.pio

112
devserver.js Normal file
View File

@ -0,0 +1,112 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.use(express.static('web'));
app.use(express.static('web/dist'));
app.get('/api/status', function(req, res)
{
res.send({
systemID: 'dev-server',
version: 'dev-server',
resetReason: 2,
stackTrace: true
});
});
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.post('/api/firmware', function(req, res)
{
res.sendStatus(200);
});
var system = {
pins: {
ledAP: 4,
ledSTA: 5,
apButton: 2,
}
};
app.get('/api/system', function(req, res)
{
res.send(system);
});
app.post('/api/system', function(req, res)
{
var body = req.body;
if (body)
{
system.pins.ledAP = body.pins.ledAP || system.pins.ledAP;
system.pins.ledSTA = body.pins.ledSTA || system.pins.ledSTA;
system.pins.apButton = body.pins.apButton || system.pins.apButton;
res.sendStatus(200);
}
else
res.sendStatus(400);
});
app.get('/api/stacktrace/get', function(req, res)
{
res.send("Nothing to see here, move along!");
});
app.get('/api/stacktrace/delete', 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')
});

168
gulp-cppstringify.js Normal file
View File

@ -0,0 +1,168 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
'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);
};

304
gulpfile.js Normal file
View File

@ -0,0 +1,304 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
'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 path = require('path');
const gzip = require('gulp-gzip');
const config = {
assetsPath: 'web/',
distPath: 'web/dist/',
outputPath: 'src/assets/',
firmwareArtifact: '.pioenvs/esp12e/firmware.bin',
firmwareOutputPath: 'bin/'
};
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('embedHTML', () =>
{
return gulp.src(config.assetsPath + '*.html')
.pipe(print(filepath => { return 'HTML: ' + filepath; }))
.pipe(htmlmin({
collapseWhitespace: true,
removeComments: true,
minifyCSS: true,
minifyJS: true
}))
.pipe(gzip({ append: false }))
.pipe(cppstringify('html.h', {
headerDefineName: '__assets_html',
map: HTMLMap,
byteArray: true
}))
.pipe(gulp.dest(config.outputPath));
});
gulp.task('compileScss', () =>
{
return gulp.src(SCSSSrc)
.pipe(plumber({
errorHandler: error =>
{
console.log(error.toString());
this.emit('end');
}}))
.pipe(print(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', () =>
{
return gulp.src(JSSrc)
.pipe(plumber({
errorHandler: error =>
{
console.log(error.toString());
this.emit('end');
}}))
.pipe(print(filepath => { return 'JS: ' + filepath }))
.pipe(concat('bundle.js'))
.pipe(uglify())
.pipe(gulp.dest(config.distPath));
});
gulp.task('embedJS', gulp.series('compileJS', () =>
{
return gulp.src([config.distPath + 'bundle.js'])
.pipe(gzip({ append: false }))
.pipe(cppstringify('js.h', {
headerDefineName: '__assets_js',
map: JSMap,
byteArray: true
}))
.pipe(gulp.dest(config.outputPath));
}));
gulp.task('embedCSS', gulp.series('compileScss', () =>
{
return gulp.src([config.distPath + 'bundle.css'])
.pipe(gzip({ append: false }))
.pipe(cppstringify('css.h', {
headerDefineName: '__embed_css',
map: CSSMap,
byteArray: true
}))
.pipe(gulp.dest(config.outputPath));
}));
gulp.task('watch', gulp.series(
'compileScss',
'compileJS',
() =>
{
watch(config.assetsPath + '*.scss', () => { gulp.series('compileScss')(); });
watch(config.assetsPath + '*.js', () => { gulp.series('compileJS')(); });
}
));
var version = false;
function getVersion(callback)
{
return new Promise((resolve, reject) =>
{
if (version !== false)
{
resolve(version);
return;
}
var versionData = '';
const cmd = spawn('gitversion');
cmd.stdout.on('data', data =>
{
versionData += data;
});
cmd.stderr.on('data', data =>
{
console.log(data.toString().trim());
});
cmd.on('exit', code =>
{
if (code != 0)
{
reject(code);
return;
}
var version = JSON.parse(versionData);
Promise.resolve(callback(version))
.then(resolve)
.catch(reject);
});
});
}
gulp.task('embedVersion', () =>
{
return getVersion(version =>
{
return new Promise((resolve, reject) =>
{
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 ? version.BuildMetaData : '0') + ";\r\n";
headerFile += "const char VersionBranch[] = \"" + version.BranchName + "\";\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, err =>
{
if (err)
reject(err);
else
resolve();
});
})
});
});
gulp.task('copyBinary', () =>
{
if (!fs.existsSync(config.firmwareOutputPath))
fs.mkdirSync(config.firmwareOutputPath, '0777', true);
return getVersion(version =>
{
return new Promise((resolve, reject) =>
{
var target = path.join(config.firmwareOutputPath, version.FullSemVer + '.bin');
console.log('Target: ' + target);
var reader = fs.createReadStream(config.firmwareArtifact);
reader.pipe(fs.createWriteStream(target));
reader.on('end', resolve);
});
});
});
gulp.task('embedAssets', gulp.series('embedHTML', 'embedJS', 'embedCSS', 'embedVersion'));
// 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', data =>
{
console.log(data.toString().trim());
});
cmd.stderr.on('data', data =>
{
console.log(data.toString().trim());
});
cmd.on('exit', code =>
{
if (code != 0) return;
gulp.start('copyBinary');
});
}
gulp.task('upload', gulp.series('embedAssets', () => { platformio('upload'); }));
gulp.task('build', gulp.series('embedAssets', () => { platformio(false); }));
gulp.task('default', gulp.series('embedAssets'));

8721
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "rgbwifi",
"version": "1.0.0",
"description": "RGBWifi",
"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/RGBWifi.git"
},
"author": "Mark van Renswoude <mark@x2software.net>",
"license": "Unlicense",
"devDependencies": {
"axios": "^0.20.0",
"body-parser": "^1.18.2",
"child_process": "^1.0.2",
"express": "^4.16.2",
"gulp-clean-css": "^3.9.0",
"gulp-concat": "^2.6.1",
"gulp-debounced-watch": "^1.0.4",
"gulp-gzip": "^1.4.1",
"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",
"path": "^0.12.7",
"through2": "^2.0.3",
"vinyl": "^2.1.0",
"vue": "^2.5.13",
"vue-i18n": "^7.3.3",
"yargs": "^16.0.3"
},
"dependencies": {
"gulp": "^4.0.2",
"npm": "^6.14.8"
}
}

20
platformio.ini Normal file
View File

@ -0,0 +1,20 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; http://docs.platformio.org/page/projectconf.html
[env:esp12e]
platform = https://github.com/platformio/platform-espressif8266.git
board = esp12e
framework = arduino
upload_speed = 115200
lib_deps =
ArduinoJson
ESP Async WebServer
EspSaveCrash
NeoPixelBus

106
src/assets/css.h Normal file
View File

@ -0,0 +1,106 @@
#ifndef __embed_css
#define __embed_css
#include <pgmspace.h>
const uint8_t EmbeddedBundleCSS[] PROGMEM = {
0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0xad,0x59,0xdb,0x6e,0xa3,0x38,0x18,0x7e,0x95,0x48,
0xd5,0x48,0xd3,0x15,0x20,0x92,0x94,0xb4,0x05,0xed,0x6a,0x57,0xfb,0x06,0x7b,0xb1,0x37,0xa3,0x5e,0x18,
0x30,0xc1,0x2a,0xc1,0xc8,0x38,0x4d,0x5b,0xc4,0xbb,0xef,0xef,0x13,0xb1,0xc1,0xc9,0x64,0x66,0x47,0xa8,
0x4d,0xb0,0xfd,0x1f,0xfd,0x1f,0x3e,0x3b,0x35,0x3f,0x34,0x43,0x4e,0xdf,0xc3,0x9e,0x7c,0x92,0x76,0x9f,
0xe6,0x94,0x95,0x98,0x85,0x30,0x92,0x55,0xb4,0xe5,0x62,0x18,0xa7,0xbb,0x4d,0x94,0x7c,0x19,0x7f,0x0b,
0x52,0x54,0x71,0xcc,0x82,0x34,0xc7,0x15,0x65,0xd8,0x26,0x23,0x6d,0x8d,0x19,0xe1,0x63,0x4e,0xcb,0x8f,
0x21,0x47,0xc5,0xeb,0x9e,0xd1,0x63,0x5b,0x86,0x05,0x6d,0x28,0x4b,0xef,0xe2,0x38,0xce,0xf4,0xd7,0xaa,
0xaa,0x14,0xe7,0x0a,0x1d,0x48,0xf3,0x91,0xfe,0x8b,0x59,0x89,0x5a,0x14,0xfc,0xc5,0x08,0x6a,0x82,0x1e,
0xb5,0x7d,0xd8,0x03,0xab,0xca,0x12,0xbf,0x8e,0xb6,0xf8,0xa0,0xde,0x4f,0x98,0xec,0x6b,0x9e,0x6e,0x81,
0x5f,0x83,0x39,0x28,0x13,0xf6,0x1d,0x2a,0x84,0x06,0x51,0xbc,0x86,0x45,0x0d,0x69,0x71,0x58,0xab,0x45,
0x40,0x96,0x75,0xa8,0x2c,0x61,0x16,0xec,0xe1,0x9c,0x1e,0xd2,0x2d,0xc3,0x87,0xf1,0xcf,0x03,0x2e,0x09,
0x5a,0xf5,0x05,0xc3,0xb8,0x5d,0xa1,0xb6,0x5c,0x7d,0x3d,0x90,0x36,0x3c,0x91,0x92,0xd7,0xe9,0xe3,0xee,
0xa9,0x7b,0xbf,0x1f,0xa4,0x1d,0x86,0x98,0xd3,0x4e,0x51,0x8e,0x68,0xe0,0xf8,0x9d,0x87,0x25,0x2e,0x28,
0x43,0x9c,0xd0,0x36,0x6d,0x69,0x8b,0xc7,0x6f,0x6f,0x61,0xd1,0x50,0xf4,0xfa,0x32,0x94,0xa4,0xef,0x1a,
0xf4,0xa1,0x86,0xef,0x0a,0x50,0x19,0x81,0x46,0xcc,0x72,0x49,0x7a,0xb7,0x89,0xc5,0x93,0x1d,0x10,0xdb,
0x83,0x58,0xc1,0x7c,0x03,0xcc,0x8d,0xaa,0xe9,0x5a,0xbc,0x48,0xd7,0xd6,0xa8,0xa4,0xa7,0x34,0x5e,0xc5,
0xab,0x24,0xee,0xde,0x57,0x77,0x55,0x51,0xed,0x8a,0x2a,0x53,0x5b,0x94,0xf6,0xb4,0x21,0xe5,0x6a,0x2d,
0x26,0xc0,0xbd,0x37,0x59,0x65,0x29,0x64,0x8d,0x1b,0x4d,0x1a,0x5c,0xf1,0x14,0x1d,0x39,0x35,0x03,0x4c,
0xba,0x51,0x8c,0x8c,0x63,0x54,0x63,0x04,0x52,0x87,0x8e,0xf6,0x44,0x1a,0xce,0x70,0x03,0x1e,0x78,0xc3,
0x66,0x66,0x45,0x0e,0xfb,0xa1,0x02,0x2f,0xf0,0x54,0x30,0x72,0x79,0xac,0x2f,0xf9,0x1d,0xbd,0x4f,0x1a,
0x3e,0x0a,0x0d,0x0d,0xb3,0xe8,0x44,0x2a,0xd2,0x73,0xc4,0x8f,0xfd,0x50,0x34,0x18,0x31,0x08,0x4c,0x5e,
0xdb,0x3e,0x53,0x1b,0x72,0x8b,0xd5,0x3e,0x9e,0x93,0x19,0x28,0x07,0x3f,0x1e,0x39,0xce,0x94,0xa2,0x71,
0x26,0x78,0xc7,0x93,0xbd,0x36,0xd1,0x2a,0x22,0x6d,0x49,0x0a,0xc4,0x29,0x9b,0xf6,0x99,0xb4,0x32,0xe2,
0xf2,0x86,0x16,0xaf,0x99,0x92,0x2a,0xf7,0xcf,0x84,0xa0,0xda,0x4b,0x99,0x52,0x0c,0x95,0xe4,0xd8,0xa7,
0x49,0xfc,0xc5,0xf5,0x4d,0x94,0x08,0x4b,0xae,0xcb,0xfb,0x56,0x22,0x8e,0x42,0x35,0xfc,0x3b,0x6c,0x62,
0x8b,0x0b,0x8e,0xcb,0x17,0x4f,0xa6,0x6d,0x9f,0x77,0x3f,0xc2,0x0b,0xec,0xb0,0xd9,0xcd,0x23,0x6b,0xcf,
0xf0,0xc7,0x4f,0xa8,0x06,0x61,0xec,0xd3,0xad,0x7a,0xde,0xfe,0x08,0x33,0xcc,0x18,0x65,0x3e,0x3e,0x05,
0x84,0x7b,0x94,0x1f,0x21,0xa3,0xdb,0x20,0x2a,0x6a,0x5c,0xbc,0xae,0x22,0x11,0xd9,0x8c,0x36,0x41,0xd4,
0x52,0x0e,0x9c,0x0b,0x99,0x9d,0x41,0xd4,0xa1,0x16,0x37,0x2b,0xf5,0x11,0x8a,0xa4,0x9e,0x0d,0x29,0x6d,
0x82,0x48,0x6c,0x0e,0xb5,0xb8,0x9c,0x10,0x6b,0xc1,0x8a,0x40,0x4b,0xa9,0xb7,0x01,0x69,0xbb,0x23,0xff,
0xc6,0x3f,0x3a,0xfc,0x7b,0x7f,0xcc,0x0f,0x84,0xbf,0x04,0x3d,0x6e,0xc0,0x58,0xe3,0x35,0xe1,0x2f,0xe5,
0xb9,0xbb,0xf5,0x7a,0x3d,0xdb,0xf5,0x2d,0xa4,0x99,0x95,0xd3,0xa4,0xed,0x31,0x87,0xbc,0x16,0x34,0x6c,
0x9f,0xa3,0xaf,0x9b,0x24,0x09,0xcc,0x5f,0xb4,0xbe,0x0f,0xcc,0x82,0x50,0xac,0xd8,0x9a,0x55,0x71,0x20,
0x9e,0x68,0x7b,0x9e,0x8f,0x2f,0x32,0x89,0x9f,0xee,0x03,0x35,0xb7,0x99,0x91,0xaf,0x93,0x7b,0xe3,0xbe,
0x08,0x15,0x22,0x87,0x03,0xfd,0x9a,0xea,0x57,0x77,0xd2,0x9d,0x93,0x7e,0xd0,0x53,0xae,0x3b,0x2c,0x0f,
0xb5,0xc7,0x43,0x8e,0x99,0x33,0xd4,0xa1,0xbe,0x3f,0x81,0x4f,0x5e,0x3c,0x9e,0x74,0x78,0xab,0x19,0x51,
0x67,0x5f,0x02,0xf1,0x1f,0x31,0x8c,0xae,0xfb,0xf8,0xdc,0x64,0xe4,0xb0,0x99,0xf3,0x7a,0x7b,0xee,0x8c,
0x4d,0x62,0xdc,0xe4,0x75,0xe1,0xa8,0x03,0x40,0x6a,0x36,0xdc,0xd2,0xb7,0x46,0xb5,0x14,0x3a,0x55,0xfe,
0x4a,0x78,0x88,0xba,0x0e,0xaa,0x17,0x6a,0x0b,0x2c,0x7b,0x42,0x16,0x1e,0xe8,0xe7,0x62,0x70,0xf6,0x3e,
0x05,0xb7,0x2d,0xdc,0x71,0x98,0xbf,0xfe,0x98,0x0e,0x02,0xf6,0x80,0x99,0xa6,0xe1,0x96,0x65,0x99,0xd9,
0xed,0xe7,0x21,0x16,0x4f,0x56,0x1c,0x59,0x0f,0xd3,0x1d,0x25,0x2d,0xb4,0x51,0xa7,0x71,0xca,0xd2,0x6a,
0x22,0xa4,0xa2,0xc5,0xb1,0x9f,0x02,0xc4,0x7d,0xab,0xe9,0x1b,0x24,0x8f,0xb3,0xd0,0x59,0xe7,0x2c,0x5b,
0x5a,0xa1,0x49,0x3c,0xf1,0x70,0x71,0x42,0xb2,0x1a,0x2e,0x58,0x96,0xc4,0xe2,0xc9,0xe8,0x91,0x0b,0x6b,
0xd2,0xf8,0xff,0x46,0xb9,0xab,0xec,0xc5,0x19,0x4d,0x63,0xb4,0x2a,0x8a,0xc2,0xd1,0x6a,0xf3,0x24,0x1e,
0xa3,0x4b,0xd8,0x31,0x02,0xe5,0xff,0xc3,0xb7,0xa9,0x0e,0xd5,0x66,0x97,0xa0,0xf5,0x9c,0xca,0xdd,0x0d,
0x33,0x9a,0xfa,0x47,0x7f,0xb9,0xdb,0x1d,0xfd,0x76,0x8f,0x9b,0x7c,0x37,0x22,0x2d,0xd4,0x8f,0x8b,0xa2,
0x16,0xbd,0x91,0xbd,0x1c,0xb8,0xd6,0xc5,0x23,0x0e,0x5d,0xf8,0x0f,0xc3,0xc9,0x46,0x23,0xa2,0xfc,0xcd,
0x2a,0x69,0xec,0x2e,0x4f,0x2b,0xc2,0x7a,0x1e,0x16,0x35,0x69,0x4a,0x87,0x34,0x5e,0x56,0x60,0x59,0x2d,
0xe1,0x73,0xc6,0xa1,0x41,0x13,0x83,0x99,0x28,0x59,0x79,0x25,0xe1,0x5c,0xa8,0x70,0xd4,0x12,0x0b,0x65,
0x9f,0x21,0xb4,0x32,0xfc,0x9e,0xc2,0xc6,0x81,0xc3,0x7a,0x69,0xb7,0x8c,0x0a,0xd1,0x48,0x2d,0x44,0xfb,
0xd4,0xf1,0x4c,0x7a,0x0c,0x35,0x64,0xdf,0xa6,0x05,0x96,0x39,0x38,0x43,0x84,0xa3,0xd3,0xcb,0xfe,0x9e,
0xa0,0xdb,0x24,0xb6,0x22,0xef,0xb8,0xcc,0x26,0x00,0x69,0x84,0xef,0x76,0xbb,0xdb,0x70,0x91,0x9f,0xbd,
0x5a,0x93,0xc8,0x12,0x22,0x3d,0x09,0x88,0x65,0x74,0x75,0x71,0x23,0xe1,0xf9,0x11,0xe5,0x4f,0x73,0xc4,
0xba,0x8e,0x35,0x30,0xb5,0x71,0xff,0xac,0xe6,0x98,0x7a,0x15,0x25,0xa0,0xbd,0x36,0x5e,0x03,0x75,0x85,
0x88,0x17,0x58,0xf3,0x87,0xcd,0x1a,0x54,0x18,0x2d,0x4d,0x58,0x45,0x07,0xdc,0xf7,0x68,0x8f,0x87,0x53,
0x4d,0x38,0x96,0xc7,0x08,0x9c,0x76,0x0c,0xbb,0xcb,0x22,0x09,0x42,0x1c,0x7b,0x9f,0x1f,0xb7,0x68,0x0b,
0xf9,0x2c,0x81,0x87,0x46,0x0e,0xfe,0x7a,0x3c,0x33,0xf7,0xd8,0x8b,0xe3,0x8a,0xc4,0x0c,0xaa,0xec,0xdb,
0x92,0x5b,0x7a,0x62,0xa8,0xb3,0x43,0xc0,0xe3,0x15,0x31,0x34,0x2e,0x10,0x8f,0x8b,0x5d,0x06,0x4f,0xad,
0xbf,0x86,0x56,0x77,0xb0,0xcd,0x06,0xad,0x8a,0xef,0x1e,0x80,0xaf,0x05,0x36,0x28,0xc7,0x67,0x71,0xf2,
0xcd,0x6f,0xb7,0x9d,0x86,0xd2,0x0a,0xc8,0x05,0x0e,0x0e,0x6d,0x74,0xb8,0x83,0x75,0x9a,0xa9,0xfa,0x8f,
0xcb,0xb9,0x35,0x8b,0x71,0xc7,0xaa,0x5d,0x2c,0x1e,0xc3,0x02,0x54,0x40,0x79,0x83,0x4b,0x43,0x6a,0xde,
0x07,0xed,0x7e,0xd8,0x4e,0x90,0xdb,0xd0,0x13,0x2e,0xe7,0x24,0xae,0x4d,0xf3,0x61,0x2b,0x75,0xc7,0x05,
0x40,0x74,0xdf,0x05,0x84,0x95,0xc7,0xbc,0x39,0xd4,0x9f,0x13,0x9a,0x85,0xd6,0xb9,0x78,0x79,0x14,0x11,
0xbb,0xff,0x60,0xb2,0x4f,0x7c,0x51,0x5b,0x65,0xed,0xd4,0x4e,0x14,0x31,0xbf,0xa7,0x26,0x55,0x2c,0x87,
0xc9,0x96,0x64,0xe7,0xe7,0x7a,0x86,0x82,0x04,0x22,0x74,0xc3,0xca,0xb0,0xf1,0x6b,0x97,0x5c,0xd5,0x4e,
0x96,0x58,0xff,0xfe,0xba,0x8e,0xd2,0x27,0x8d,0x25,0xf2,0x7a,0xba,0x37,0xd5,0x5b,0x31,0x17,0x15,0x65,
0xa3,0xd0,0x5b,0xc6,0x01,0x28,0xf5,0x15,0x65,0x87,0x94,0x51,0x38,0x28,0xe0,0xaf,0xe1,0x43,0x52,0xe2,
0xfd,0xbd,0x6d,0xa1,0x84,0xcd,0xb1,0x0b,0xf4,0x1c,0x9c,0x67,0xd9,0x2d,0x58,0x85,0x26,0xcc,0xac,0xfc,
0x5b,0x43,0xae,0xfd,0x04,0xa4,0x9d,0x03,0x57,0x0f,0xf2,0x3a,0x17,0x44,0xa7,0x02,0xea,0x84,0x8c,0x21,
0x6a,0xcc,0xc9,0xe2,0x2a,0xf1,0x4d,0xb7,0x28,0xb6,0x04,0xdb,0x1c,0x70,0xe2,0x1e,0xbf,0x38,0xf6,0xb2,
0x45,0xbd,0x91,0xc7,0xf6,0x7a,0x3d,0x9c,0x1b,0xd7,0xe6,0xbc,0x08,0x3a,0x71,0xbd,0xd1,0x81,0xdc,0x93,
0x06,0x72,0xdc,0xb9,0xb2,0x99,0xad,0xdc,0xda,0x8d,0x70,0x09,0x8f,0x96,0xa4,0x67,0xc5,0xa5,0x12,0x0f,
0x83,0xbd,0xe2,0x81,0x4d,0xd6,0x98,0xa4,0x7d,0xf1,0xe4,0x7b,0x76,0x49,0xe6,0x4e,0x3c,0xa3,0x5b,0xc2,
0x9c,0xda,0x75,0xb5,0x00,0x4b,0xba,0x50,0x95,0xbc,0xc1,0x39,0xcb,0x6f,0x6e,0xbd,0x5f,0x8a,0x6a,0xca,
0xc8,0xa7,0xe8,0xb9,0x8d,0x85,0x8c,0x46,0x6b,0x78,0x75,0xb9,0xc0,0x3a,0xcb,0x3c,0x21,0x7a,0x61,0xfa,
0x1c,0xae,0x17,0x16,0xa8,0xd0,0xb5,0x27,0xa7,0x30,0xf6,0xd6,0x79,0x75,0xdb,0x23,0x2d,0xd7,0xc1,0x2b,
0x2b,0xde,0x99,0x5e,0xdd,0x17,0xda,0x06,0xc2,0x2c,0x34,0xc3,0x99,0xcf,0x5d,0x5c,0x64,0xed,0xd9,0x2c,
0x1a,0xf5,0x4d,0x09,0x48,0x15,0x91,0x31,0xcc,0x60,0xe4,0x12,0x50,0x8d,0x11,0xe0,0x57,0xda,0x89,0xf2,
0xd5,0x3b,0xd8,0x30,0xb1,0x0e,0x38,0xce,0xfd,0xd2,0x55,0x4c,0x26,0xb3,0x21,0xea,0xa1,0x62,0x01,0xb4,
0x1b,0x16,0x60,0xad,0xe7,0xb8,0x73,0xa4,0x6c,0xad,0x84,0x62,0xd3,0xc1,0xca,0xd7,0x60,0x05,0xe9,0x4a,
0x73,0x3e,0x5f,0xd1,0x39,0xa4,0x0f,0x46,0xc4,0x2a,0x7a,0x43,0xcd,0x11,0x7f,0xe7,0x06,0x2b,0x92,0xd9,
0x6c,0x37,0xb0,0x0b,0xdc,0xe7,0x86,0x5d,0x3c,0xb8,0x9e,0xab,0x93,0x29,0xf3,0x72,0x3b,0x66,0x28,0x1b,
0xce,0xd3,0x72,0x70,0x59,0xb8,0xac,0x53,0x99,0x92,0x94,0xa6,0x46,0x94,0x7a,0x0f,0x79,0x0d,0xf1,0x7b,
0x51,0xbe,0x5f,0x9f,0x8d,0x75,0xd9,0xb6,0xf1,0x5f,0xb6,0xd9,0xba,0xe8,0x1b,0x54,0x17,0x98,0x59,0x0a,
0x89,0xc3,0xb9,0xac,0x8c,0x5a,0x99,0x5f,0x29,0x44,0x5f,0x29,0x79,0xb0,0xe4,0x77,0x81,0xf0,0x62,0xaf,
0x5a,0x2a,0x2e,0xca,0xec,0x92,0xea,0x09,0x7e,0x19,0xd7,0x4e,0x75,0x91,0x77,0x5e,0x83,0x0f,0x69,0x9b,
0x9b,0x83,0xd1,0x77,0x3d,0x36,0x2c,0x4f,0x52,0xfa,0x34,0x95,0x4d,0x3f,0x17,0x08,0x5e,0xa6,0x59,0xbb,
0xf6,0x5c,0x6d,0x62,0x5e,0x79,0xba,0xf4,0x59,0x05,0x5f,0xd8,0xec,0x5b,0x28,0x0f,0xe5,0x22,0x87,0xad,
0x52,0x74,0x61,0xa5,0x46,0x7a,0xbe,0x39,0xe3,0x48,0x8f,0x42,0xea,0xf7,0x8c,0xd9,0xa9,0x70,0x3a,0x17,
0x3a,0xb6,0x6d,0x63,0xf1,0x4c,0xb6,0xab,0xa2,0x20,0xb9,0xe8,0xab,0x83,0xb9,0x53,0x6d,0xda,0xfc,0x01,
0x25,0x4f,0x8e,0x5f,0x74,0x7b,0xb9,0x02,0xdd,0xe5,0xc5,0x7c,0x74,0xc2,0xf8,0xb5,0x44,0x1f,0xfd,0x32,
0xa1,0xcd,0x8c,0x81,0xb3,0x8a,0xea,0x49,0x4c,0x55,0xa0,0x41,0x28,0xc3,0x24,0x34,0x77,0x23,0x72,0x08,
0x62,0xe5,0x0d,0xeb,0xa1,0x41,0x02,0x2d,0x55,0x63,0xa8,0xf8,0x91,0x85,0x7f,0xac,0xa2,0xa4,0xb7,0x89,
0x1d,0x2a,0x4e,0x07,0xbd,0x4c,0x84,0x91,0xcc,0x22,0x27,0xf8,0xe4,0x08,0x54,0x39,0x68,0x28,0xdc,0x73,
0x74,0xbe,0x62,0xe8,0xc3,0xf3,0x17,0x97,0xfc,0x96,0x5a,0xe9,0xae,0xbf,0xa5,0x66,0xaa,0xfe,0x92,0xb9,
0xa8,0x5f,0x32,0xc1,0x6d,0x79,0xab,0xc6,0x17,0x7a,0xe2,0x99,0xd1,0x45,0xdd,0x15,0x9e,0xb6,0x54,0x97,
0x8b,0x2f,0x29,0xae,0xef,0x38,0xae,0xe9,0xbd,0xec,0xbe,0x11,0xc3,0x3d,0xe6,0xff,0x60,0xd4,0xcf,0xee,
0x59,0x36,0x20,0xf6,0x3f,0x57,0x7a,0x1c,0xe6,0x0a,0x1c,0x00,0x00};
#endif

108
src/assets/html.h Normal file
View File

@ -0,0 +1,108 @@
#ifndef __assets_html
#define __assets_html
#include <pgmspace.h>
const uint8_t EmbeddedIndex[] PROGMEM = {
0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0xcd,0x58,0xd9,0x7a,0xe2,0x38,0x16,0x7e,0x15,0x97,
0x7b,0xa6,0xa9,0xfa,0xd2,0x61,0x0b,0x95,0xaa,0x64,0x20,0xd3,0x66,0xdf,0xc3,0x1e,0xe0,0x4e,0xb6,0x85,
0xad,0x20,0x2f,0x91,0x04,0x84,0xaa,0xce,0xbb,0x8f,0xe4,0x05,0x8c,0xe3,0x6c,0xd5,0x7d,0x31,0x5c,0x00,
0xd2,0xd9,0xfe,0x73,0x7c,0x16,0x59,0xc5,0x4f,0xd5,0xdb,0xca,0x64,0x31,0xa8,0x49,0x26,0xb3,0xf0,0x4d,
0x31,0xf8,0x86,0x40,0xbf,0x29,0x5a,0x90,0x01,0x49,0x33,0x01,0xa1,0x90,0x95,0xe4,0xe9,0xa4,0x7e,0xfe,
0x5d,0xbe,0x29,0x32,0xc4,0x30,0xbc,0x19,0x35,0xca,0x77,0x68,0x85,0x8a,0x19,0x7f,0xe9,0xf3,0xda,0xc0,
0x82,0x25,0x99,0x99,0xd0,0x82,0xe7,0x9a,0x83,0x1d,0x22,0x4b,0x9a,0x63,0x33,0x68,0x73,0xf1,0xdf,0xb2,
0xde,0x47,0x3e,0x61,0xdd,0x22,0xb8,0x73,0x1d,0xc2,0x22,0x7c,0x3b,0xa4,0x33,0xb3,0xa4,0xc3,0x2d,0xd2,
0xe0,0xb9,0xb7,0xf8,0x03,0xd9,0x88,0x21,0x80,0xcf,0xa9,0x06,0x30,0x2c,0xe5,0xb8,0x0a,0x8c,0xec,0xb5,
0x44,0x20,0x2e,0xc9,0x94,0xed,0x31,0xa4,0x26,0x84,0x5c,0x87,0x49,0xe0,0xaa,0x24,0xab,0x1b,0x5b,0xc7,
0x30,0xad,0x51,0xca,0x19,0xa9,0x46,0x90,0xcb,0x24,0x4a,0xb4,0x03,0xe1,0x5e,0xec,0x67,0x7c,0x02,0xff,
0xe3,0xbb,0xaa,0x3a,0xfa,0xfe,0xa6,0xa8,0xa3,0xad,0x84,0xf4,0x92,0x0c,0x5c,0x57,0xf6,0x57,0xdb,0x73,
0x0d,0x3b,0x60,0xed,0x2f,0x34,0x0c,0x28,0x2d,0xc9,0xb6,0xc3,0xb8,0xe7,0x1a,0x60,0xc8,0xb1,0x2b,0x1c,
0x36,0x40,0x36,0x24,0xf2,0x8b,0x2c,0xb2,0x74,0x1d,0xec,0xfe,0x94,0x20,0x21,0x0e,0xb9,0x96,0xa2,0x64,
0xe9,0x53,0x49,0xb2,0x37,0x18,0x4b,0xbf,0xff,0x7e,0xb2,0x9f,0xf6,0x78,0xa5,0x27,0x99,0x63,0x40,0xab,
0x53,0x95,0xa1,0x8c,0x2c,0xfd,0xa9,0x61,0xa4,0xad,0xd3,0x2e,0x81,0x5b,0x2f,0x7a,0x26,0xd2,0x61,0x3f,
0x6a,0x9c,0x87,0xc0,0x05,0x76,0x88,0xcb,0x82,0x94,0x02,0x03,0xca,0x37,0x3f,0x7f,0x9e,0x1a,0x0b,0x08,
0xd2,0xd3,0x13,0x0f,0x0d,0x17,0xe0,0x81,0xe1,0xee,0x84,0xdf,0x61,0x5c,0xb4,0x64,0x6f,0x45,0x08,0xbd,
0x3d,0x64,0x19,0x7e,0xa8,0x75,0xc0,0xc0,0x35,0xb2,0xb8,0xc6,0x8c,0x6b,0x1b,0xff,0x51,0x01,0x85,0x97,
0x85,0x3f,0xd0,0xac,0x7c,0x3b,0xda,0x65,0x3b,0x0d,0xc3,0x51,0xf8,0xa7,0x3f,0x9e,0x9a,0xb5,0xa9,0xc1,
0xff,0x55,0xc5,0x52,0xd9,0x55,0x94,0x05,0xff,0x29,0xcf,0x95,0xad,0xd5,0x14,0x1b,0x8d,0xf9,0xa8,0x7e,
0xd7,0x1c,0x4d,0xd4,0xfc,0x32,0xab,0xe7,0xeb,0xfb,0xe5,0xb0,0x5c,0x5e,0x36,0xae,0xd0,0x72,0x5c,0x6e,
0xab,0x77,0x75,0x7b,0x39,0x6b,0xe3,0xc5,0xdd,0xe8,0xab,0xa6,0x61,0x3c,0x10,0x02,0xf3,0x72,0x7b,0x54,
0xab,0x4f,0x61,0x9f,0xd0,0x3b,0xbd,0xd6,0x37,0xee,0x95,0x61,0x57,0x5b,0x94,0x35,0x65,0xa0,0x29,0x15,
0x7d,0xd8,0x2f,0x28,0xfd,0x7c,0xaf,0x52,0x30,0x46,0x74,0xd1,0xbe,0xaa,0xf5,0x75,0x65,0xb0,0x50,0xaa,
0x40,0xa9,0x42,0x57,0x9f,0x9a,0xbd,0xdc,0x43,0xfd,0x7e,0x43,0x0c,0xf7,0x6a,0xac,0xf5,0x9a,0x86,0xfe,
0x2d,0x77,0x31,0xbb,0x58,0xb1,0xa9,0xfb,0x15,0x36,0x8d,0x5e,0x3d,0x47,0x48,0xa3,0x06,0x36,0x97,0xb3,
0x66,0x35,0xdf,0xec,0xa9,0xcd,0xaf,0x0f,0xed,0xdb,0x6e,0x93,0x80,0xb3,0xd5,0xfa,0x87,0x4a,0x17,0x23,
0x6a,0xf6,0xbe,0xbb,0xdd,0x89,0x31,0x6d,0x19,0x63,0x63,0xbb,0xe9,0xf5,0x9c,0xc5,0xee,0x0c,0xf5,0x16,
0x13,0x72,0x39,0x34,0xfb,0x8b,0x1e,0xe9,0xa3,0xfe,0x7e,0xd7,0xea,0xe2,0xfd,0xac,0xa3,0x6b,0xfb,0xfd,
0x80,0x5a,0xda,0x88,0xee,0xa7,0x5f,0xb3,0x6b,0xa3,0xc9,0x86,0xc3,0x4d,0x5e,0xd1,0xfb,0xed,0xba,0x5b,
0x5d,0x2b,0x9d,0x42,0x2b,0xd3,0x6d,0xdd,0xf5,0xd4,0xbc,0x42,0x5b,0x65,0xed,0x21,0x8b,0x46,0x0d,0x38,
0x6c,0x0c,0x26,0xcb,0xd5,0xec,0x72,0x58,0xcb,0x9e,0x19,0xd5,0x46,0x3d,0x4f,0x1c,0xda,0xa8,0x19,0xbd,
0xe1,0x63,0x4b,0x31,0xed,0xa5,0x82,0x06,0xfd,0xef,0x85,0x8d,0x3b,0x5a,0x65,0x33,0xb7,0xd8,0xa5,0xdd,
0x4a,0xd9,0xbd,0xd8,0x3f,0x64,0x35,0xd3,0x60,0x95,0xe9,0x74,0x49,0x46,0xbb,0xcb,0x61,0xf5,0xf6,0xa2,
0x76,0xd7,0x1c,0x3f,0xd4,0xaf,0x18,0x20,0x4b,0x30,0xee,0xb4,0xe7,0xb0,0x5d,0xd5,0xd5,0x21,0xa6,0xb5,
0x6c,0xa7,0x7a,0xd9,0xee,0x67,0x3a,0xce,0x88,0x36,0xcc,0xc7,0x79,0xa7,0x82,0x2b,0x9d,0x66,0xbb,0xb5,
0x5a,0x4f,0xcc,0x5d,0xef,0xce,0x54,0x2e,0xf5,0xf2,0xd8,0xc1,0x23,0x74,0xbf,0x6e,0xdf,0xea,0xb9,0xe5,
0x74,0x7b,0xb5,0x1f,0x5e,0xdd,0xba,0x0f,0x6a,0xd3,0x45,0x60,0x3a,0x03,0x35,0x75,0x59,0xfb,0xc6,0x5a,
0xad,0x7b,0xa7,0xdc,0x99,0xef,0xa9,0x43,0x73,0x5a,0x61,0xf6,0x1d,0xaa,0xdd,0x9a,0xae,0x6e,0xf3,0xaa,
0xd6,0xa3,0xb5,0x6f,0xc6,0xfd,0xa6,0xac,0x6f,0xe7,0xa3,0x71,0xbb,0x50,0x3f,0xcb,0xec,0x1e,0x5a,0xf3,
0x39,0x69,0x35,0x76,0xd6,0xfc,0xe2,0xc7,0x0e,0x68,0xdd,0xaa,0x09,0xfb,0xb7,0x57,0xb9,0xdb,0xfb,0xee,
0xb0,0xa3,0xe7,0x0a,0xb3,0x5e,0xb5,0x62,0x2f,0x8c,0xca,0xe3,0xec,0xbe,0x75,0xd1,0x9f,0xc0,0x9c,0x35,
0x76,0x06,0xd5,0xc2,0xd5,0x63,0x61,0x4c,0x78,0x72,0x5c,0x3d,0x0c,0xec,0x02,0x74,0xb6,0x95,0x9e,0x97,
0x3d,0x35,0x5c,0x9f,0xac,0xc7,0x9b,0xa1,0x55,0xa9,0xf0,0x4c,0x34,0x73,0x22,0xc5,0xff,0xc5,0x3e,0xa7,
0xbc,0xfe,0x94,0xfa,0xe2,0x65,0x36,0xdf,0x2d,0x9a,0x79,0x41,0xa1,0x0c,0xb0,0x0d,0x4d,0xd3,0x3d,0x65,
0xd0,0x6a,0x55,0x79,0x41,0x05,0x55,0xf8,0x5f,0x4f,0x28,0xdc,0xe7,0x72,0x67,0x52,0xea,0x5a,0x4a,0xf1,
0x9f,0xb8,0x08,0xdf,0x4d,0xf9,0x5a,0xf3,0x27,0xd5,0xb0,0xe3,0x55,0xe5,0xf3,0x9e,0x56,0x09,0x2f,0x1e,
0x1b,0x6a,0x41,0x51,0x46,0xf6,0x91,0xad,0x8b,0x2a,0x14,0xcd,0xf2,0x5a,0xd4,0xcd,0xb9,0x2f,0xec,0x2b,
0x1a,0xfb,0x46,0x81,0x9b,0x86,0x36,0x50,0x31,0xd4,0x39,0xc0,0x54,0xa0,0x09,0xea,0x29,0x01,0x42,0x47,
0xf4,0xb8,0x21,0x07,0x25,0x1b,0x38,0x1f,0x55,0xa1,0x69,0xbc,0xc6,0x5d,0x07,0xd9,0x2c,0x7d,0x8c,0x89,
0xc4,0x19,0x5f,0xb2,0x73,0xba,0x8f,0x5c,0x6e,0xec,0x15,0x9d,0x1c,0x86,0x27,0x18,0x84,0xfa,0xd0,0x37,
0x7e,0xd5,0x7b,0x03,0xb2,0x3b,0x54,0xf7,0x2c,0x71,0x21,0xdf,0xe0,0xe7,0x2f,0x2f,0xfb,0x47,0x7d,0x46,
0xcb,0xd1,0x61,0xcc,0xbf,0x24,0x4d,0x13,0xf8,0xc8,0x3e,0x47,0x90,0xc6,0xbb,0x9d,0xdf,0x73,0x79,0xeb,
0xd7,0x91,0x6d,0xc8,0x21,0xd8,0x70,0x1d,0x9a,0x0f,0xd6,0x07,0x4b,0xc1,0xba,0x15,0xfa,0x74,0x1a,0x09,
0x5f,0xe7,0xa7,0x80,0x49,0xf4,0x7b,0x3e,0xcf,0x90,0x4a,0xfc,0x96,0x5e,0x0a,0x32,0xf0,0x34,0x3c,0x3b,
0x40,0x6c,0x0f,0x81,0x2f,0x6c,0x02,0x3a,0x82,0x7c,0x1a,0xd7,0xc4,0x6c,0xe0,0x9c,0x6e,0x88,0xc4,0x1b,
0x16,0x69,0x72,0xa0,0x05,0x4f,0xc1,0xe5,0x2c,0xa1,0x2a,0x8f,0x38,0x82,0x80,0x8a,0x47,0xf0,0x5c,0xcc,
0xa7,0xa4,0x23,0x99,0x1e,0xd9,0x8e,0x68,0xf3,0x81,0xd0,0x43,0xd0,0xb5,0xf5,0x84,0x00,0x0d,0xc6,0x54,
0x1e,0x09,0x47,0x24,0x20,0x44,0xa2,0x6e,0x18,0xe3,0x1e,0xfb,0x3f,0xe7,0x2e,0xe1,0x83,0x82,0xec,0xc3,
0xf1,0x9d,0x01,0x2e,0xca,0x78,0xe2,0x4c,0x88,0x67,0x0c,0x31,0xd9,0x3f,0x6a,0xb4,0xea,0xec,0x6c,0x11,
0xe7,0xc0,0x38,0xb8,0x91,0xe2,0xd6,0xc3,0xd1,0xc9,0x67,0x15,0xc4,0x90,0xc1,0xf1,0x3b,0x94,0x7a,0x8c,
0x07,0x95,0xcf,0x73,0xdc,0x06,0x5b,0x64,0xf8,0x4f,0x93,0x01,0x55,0x94,0xff,0x33,0xa3,0xc7,0xa3,0x40,
0x0a,0xf0,0x6a,0xd8,0x42,0xde,0x5c,0xfc,0x3f,0x13,0xa0,0xf2,0x1c,0x90,0x52,0xbe,0x93,0x29,0x31,0xf8,
0x43,0x84,0x11,0x86,0x03,0xfd,0x80,0x32,0x08,0x0a,0x37,0x38,0x89,0x74,0x3a,0xf0,0x4b,0xb6,0x8f,0x45,
0xfa,0xb2,0xfd,0x08,0xcf,0x01,0xc3,0x71,0xef,0x9f,0xc1,0xe1,0xf7,0xd8,0x57,0x62,0xe0,0xd3,0x8f,0x31,
0xf0,0xd6,0x09,0xb6,0xe3,0xc5,0x97,0x18,0x6a,0x31,0x2e,0x2e,0xe2,0xe1,0x8c,0x4e,0x8d,0x8b,0xb7,0x14,
0x9d,0xc4,0xa4,0xb8,0x72,0x88,0x25,0xfd,0x49,0x37,0xaa,0x85,0xd8,0xf1,0x64,0xc6,0xcf,0x94,0x78,0x5f,
0x89,0x76,0xc1,0xa3,0xd1,0x68,0xfc,0x62,0x86,0x35,0x13,0x6a,0x6b,0x6e,0x54,0xf4,0x35,0x9c,0x56,0x1d,
0x07,0x43,0x60,0x47,0xdb,0x69,0xb4,0x09,0xf3,0xd8,0x7a,0xf2,0x25,0x39,0xa6,0x35,0xc2,0x93,0xf2,0x7a,
0xa8,0xa7,0xf6,0xf4,0x6c,0x68,0x0a,0x05,0x09,0x88,0x22,0xb2,0x4d,0x4f,0x3e,0x72,0x48,0x7c,0x1b,0x5d,
0xd0,0x96,0x5f,0x44,0x16,0x69,0xdb,0x1f,0x46,0x16,0x91,0x8d,0x23,0x93,0x8a,0x18,0xa8,0x10,0x4b,0xfc,
0x59,0xf0,0xbe,0x41,0x91,0x9e,0x28,0xcf,0xf7,0x03,0x21,0x8f,0x9b,0x4b,0x21,0xdb,0xdd,0x30,0x89,0xed,
0x5d,0xf1,0x1e,0xc3,0x67,0x84,0xec,0x9d,0x7b,0x3d,0x05,0xa1,0x9b,0xa7,0xee,0x79,0x94,0xeb,0x70,0xf8,
0xf1,0xf6,0x9e,0xe0,0xfb,0x29,0x1a,0x97,0xfb,0xb4,0x73,0x48,0x22,0xa2,0x90,0xf6,0x0a,0xaa,0x83,0xb8,
0x87,0xec,0xb8,0x4a,0x42,0x77,0xa4,0xbe,0x85,0xf0,0x1d,0x4f,0x52,0x37,0x35,0xf7,0x4d,0x45,0x2f,0x3d,
0x66,0x21,0xcc,0x9f,0x6f,0xf8,0x44,0x45,0x89,0x9c,0x8b,0xb7,0x09,0xe2,0xe0,0x0f,0x3e,0x74,0xa1,0xe9,
0x59,0x1e,0x46,0x9a,0x30,0xaf,0x3b,0xc7,0x15,0x9c,0xa2,0xff,0x46,0xc2,0x8e,0xdc,0x24,0x6d,0xc8,0x05,
0xba,0xce,0x07,0x1d,0x7d,0x57,0x1e,0x70,0x1d,0x89,0x71,0x46,0x6f,0x06,0x46,0xfa,0xeb,0x2f,0x29,0x1e,
0xcb,0x58,0x92,0x6e,0x54,0x1b,0x32,0x0b,0xd0,0x75,0x62,0xaa,0x1e,0xa8,0xef,0x4b,0xd8,0xa3,0xb2,0xe4,
0xb4,0x8d,0xd0,0xff,0x2e,0x70,0x3e,0xf1,0xe0,0x0e,0xec,0x93,0x50,0x07,0xa4,0x77,0x41,0x0e,0xd5,0x24,
0xe2,0x3d,0x10,0x7f,0x01,0x6c,0xd0,0xbb,0x23,0x88,0x4d,0x87,0x32,0x71,0xff,0x90,0x04,0x39,0xa4,0xbd,
0x89,0xf9,0xda,0xc5,0xfc,0x4c,0x60,0x3a,0x98,0xbf,0xfa,0x3e,0x4b,0xf7,0x50,0xcb,0xe0,0xc8,0x23,0xb2,
0x5f,0xf8,0x79,0x30,0x9e,0xe8,0xe8,0x91,0xfa,0x66,0xc5,0x46,0x72,0xde,0x9f,0xac,0x22,0xe1,0xa3,0x38,
0xfd,0x09,0x74,0xa2,0x89,0xf2,0x13,0x8a,0x38,0x4d,0x5e,0x6f,0x01,0xde,0xc0,0x70,0x1d,0xbc,0xf4,0x78,
0x13,0xaa,0xec,0xa9,0x1a,0x7b,0xfb,0x3c,0x06,0xd7,0x71,0x4a,0xea,0x70,0x04,0x2f,0x66,0x44,0x15,0xbf,
0x39,0x63,0xc3,0x51,0xfd,0xc2,0x58,0xdc,0xb8,0xe2,0x9c,0x56,0x47,0xc4,0xe2,0x47,0x5d,0x78,0x3a,0x8a,
0xfd,0xa9,0xbe,0x0a,0x68,0x93,0xd8,0x64,0x8c,0xba,0xba,0x42,0x18,0xfa,0xe1,0x0d,0xb9,0xeb,0x62,0xe7,
0xff,0x20,0x48,0xc7,0xb8,0xf8,0x9e,0x0e,0x88,0x63,0x88,0x7e,0xe3,0xbd,0x71,0xae,0x00,0xa6,0x7e,0x16,
0xc6,0x88,0x4f,0x4f,0xff,0x3e,0x0d,0xf2,0x2b,0x67,0x8a,0xb1,0x17,0xa6,0xa4,0xc8,0xb9,0xc8,0xa6,0xf1,
0xa8,0x45,0xaf,0x6d,0x1c,0x82,0x7e,0x88,0x1b,0x1d,0x7c,0xda,0x29,0xb9,0x58,0xb7,0x56,0x55,0x06,0xf1,
0xe3,0x55,0xb8,0xff,0x4a,0x69,0xd8,0x1b,0x4b,0x85,0x24,0x18,0x4d,0xa1,0x9a,0xc3,0x54,0xf1,0xa9,0x3c,
0xa0,0x47,0x78,0x69,0x1e,0x71,0x61,0xe9,0xf9,0x61,0xfa,0x75,0x74,0xe3,0x89,0x92,0x0c,0x8f,0x13,0x3e,
0x84,0x4f,0x28,0x7a,0x0b,0xa0,0x67,0xec,0x43,0x08,0x95,0x81,0x9f,0x08,0x09,0x18,0x43,0xd2,0xfb,0x51,
0x1e,0x94,0xbd,0x8a,0x13,0xb8,0xa1,0xc9,0xa4,0xf2,0x7c,0xfe,0x3e,0xce,0xe7,0x3b,0x59,0xa1,0x47,0xf9,
0x45,0x8e,0x2d,0x24,0x14,0x45,0x5c,0xd0,0x1c,0x77,0x4f,0x90,0x61,0x06,0x93,0x57,0x25,0x91,0x7b,0x94,
0x80,0x37,0x7e,0x8d,0x12,0x16,0xe3,0xcc,0x27,0x7b,0xb7,0x29,0x31,0x89,0xc3,0x2d,0xca,0xb3,0xd7,0xf0,
0xe0,0xea,0x16,0x03,0xdb,0xd8,0x00,0x83,0x87,0xe5,0x1e,0x6c,0x81,0xbf,0x29,0xdf,0x70,0x2d,0x84,0x29,
0xae,0xfb,0xf9,0x4b,0xe4,0x2a,0xd7,0xbf,0xc4,0xcd,0x78,0x57,0xd8,0xff,0x03,0x5f,0x4f,0x1f,0xc2,0xd8,
0x16,0x00,0x00};
#endif

2385
src/assets/js.h Normal file

File diff suppressed because it is too large Load Diff

13
src/assets/version.h Normal file
View File

@ -0,0 +1,13 @@
#ifndef __assets_version
#define __assets_version
const uint8_t VersionMajor = 0;
const uint8_t VersionMinor = 1;
const uint8_t VersionPatch = 0;
const uint8_t VersionMetadata = 0;
const char VersionBranch[] = "master";
const char VersionSemVer[] = "0.1.0";
const char VersionFullSemVer[] = "0.1.0+0";
const char VersionCommitDate[] = "2020-09-19";
#endif

44
src/charproperties.cpp Normal file
View File

@ -0,0 +1,44 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./charproperties.h"
#include <cstddef>
#include <string.h>
#include "./debug.h"
void assignChar(char** field, const char* newValue)
{
if (*field != nullptr)
delete *field;
if (newValue != nullptr)
{
// Include the terminating null character
size_t length = strlen(newValue) + 1;
if (length > 0)
{
*field = new char[length];
strncpy(*field, newValue, length);
}
else
*field = nullptr;
}
else
*field = nullptr;
}
bool sameStr(const char* value1, const char* value2)
{
if ((value1 == nullptr) != (value2 == nullptr))
return true;
if (value1 == nullptr)
return false;
return strcmp(value1, value2) == 0;
}

15
src/charproperties.h Normal file
View File

@ -0,0 +1,15 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __charproperties
#define __charproperties
#include <stdint.h>
void assignChar(char** field, const char* newValue);
bool sameStr(const char* value1, const char* value2);
#endif

30
src/config.cpp Normal file
View File

@ -0,0 +1,30 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./config.h"
#ifdef SerialDebug
const uint32_t SerialDebugBaudrate = 115200;
const uint32_t SerialDebugStartupDelay = 2000;
#endif
const char* ConnectionSettingsFile = "/connection.json";
const char* SystemSettingsFile = "/system.json";
const char* DefaultAPSSIDPrefix = "RGBWifi-";
// Timeout when in AP + station mode (otherwise trying to connect
// to the STA will block the AP)
const uint32_t StationModeTimeout = 30000;
const uint16_t APButtonHoldTime = 2000;
const uint8_t InitialisationBrightness = 128;
const uint8_t InitialisationFadeTime = 250;
const uint8_t InitialisationBlinkCount = 2;

45
src/config.h Normal file
View File

@ -0,0 +1,45 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __config
#define __config
#include <stdint.h>
// Enables debug information to be output through the standard
// Serial connection, disable in production units to improve performance
#define SerialDebug
// Enables the crash API methods to cause crashes, you probably never
// want to leave this on unless you're debugging the exception handler
//#define EnableCrashAPI
#ifdef SerialDebug
extern const uint32_t SerialDebugBaudrate;
extern const uint32_t SerialDebugStartupDelay;
#endif
extern const char* ConnectionSettingsFile;
extern const char* SystemSettingsFile;
extern const char* DefaultAPSSIDPrefix;
extern const uint32_t StationModeTimeout;
extern const uint16_t APButtonHoldTime;
extern const uint32_t TimezoneRetryInterval;
extern const uint8_t InitialisationBrightness;
extern const uint8_t InitialisationFadeTime;
extern const uint8_t InitialisationBlinkCount;
#endif

18
src/debug.cpp Normal file
View File

@ -0,0 +1,18 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./debug.h"
void _dinit()
{
#ifdef SerialDebug
Serial.begin(SerialDebugBaudrate);
// Enable if you want detailed WiFi state logging
//Serial.setDebugOutput(true);
delay(SerialDebugStartupDelay);
#endif
}

24
src/debug.h Normal file
View File

@ -0,0 +1,24 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#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

20
src/global.cpp Normal file
View File

@ -0,0 +1,20 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./global.h"
ConnectionSettings* connectionSettings = new ConnectionSettings();
bool connectionSettingsChanged = false;
SystemSettings* systemSettings = new SystemSettings();
bool systemSettingsChanged = false;
bool shouldReboot = false;
uint32_t currentTime;
IPAddress emptyIP(0, 0, 0, 0);

28
src/global.h Normal file
View File

@ -0,0 +1,28 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __global
#define __global
#include <stdint.h>
#include <stdbool.h>
#include <IPAddress.h>
#include "./settings/connection.h"
#include "./settings/system.h"
extern ConnectionSettings* connectionSettings;
extern bool connectionSettingsChanged;
extern SystemSettings* systemSettings;
extern bool systemSettingsChanged;
extern bool shouldReboot;
extern uint32_t currentTime;
extern IPAddress emptyIP;
#endif

217
src/main.cpp Normal file
View File

@ -0,0 +1,217 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncWebServer.h>
#include <ESPAsyncTCP.h>
#include <NeoPixelBus.h>
#include <LittleFS.h>
#include "./config.h"
#include "./debug.h"
#include "./global.h"
#include "./main.wifi.h"
#include "./main.led.h"
#include "./main.debug.h"
#include "./server/static.h"
#include "./server/settings.h"
#include "./server/firmware.h"
#include "./server/api.h"
// Forward declarations
void handleNotFound(AsyncWebServerRequest* request);
AsyncWebServer server(80);
NeoPixelBus<NeoGrbwFeature, Neo800KbpsMethod>* strip = NULL;
#define colorSaturation 128
RgbwColor red(colorSaturation, 0, 0, 0);
RgbwColor green(0, colorSaturation, 0, 0);
RgbwColor blue(0, 0, colorSaturation, 0);
RgbwColor white(colorSaturation);
RgbwColor black(0);
void setup()
{
_dinit();
_dln("Initializing LED strip");
strip = new NeoPixelBus<NeoGrbwFeature, Neo800KbpsMethod>(5);
strip->Begin();
strip->Show();
currentTime = millis();
if (!LittleFS.begin())
_dln("Setup :: failed to mount file system");
connectionSettings->read();
systemSettings->read();
/*
stepsSettings->read();
timeTriggerSettings->read();
motionTriggerSettings->read();
*/
pinMode(systemSettings->pinAPButton(), INPUT_PULLUP);
pinMode(systemSettings->pinLEDAP(), OUTPUT);
pinMode(systemSettings->pinLEDSTA(), OUTPUT);
_dln("Setup :: starting initialization sequence");
/*
uint8_t bottomStep = stepsSettings->count() - 1;
for (uint8_t i = 0; i < InitialisationBlinkCount; i++)
{
stairs->set(bottomStep, InitialisationBrightness, InitialisationFadeTime);
waitForTransition();
stairs->set(bottomStep, 0, InitialisationFadeTime);
waitForTransition();
}
*/
_dln("Setup :: initializing WiFi");
WiFi.persistent(false);
WiFi.mode(WIFI_OFF);
initDebug();
initWiFi();
_dln("Setup :: registering routes");
registerStaticRoutes(&server);
registerAPIRoutes(&server);
registerSettingsRoutes(&server);
registerFirmwareRoutes(&server);
_dln("Setup :: starting HTTP server");
server.onNotFound(handleNotFound);
server.begin();
}
void loop()
{
/*
_dln("Looping");
delay(1000);
// set the colors,
// if they don't match in order, you need to use NeoGrbFeature feature
strip->SetPixelColor(0, red);
strip->SetPixelColor(1, green);
strip->SetPixelColor(2, blue);
strip->SetPixelColor(3, white);
strip->SetPixelColor(4, black);
// the following line demonstrates rgbw color support
// if the NeoPixels are rgbw types the following line will compile
// if the NeoPixels are anything else, the following line will give an error
//strip->SetPixelColor(3, RgbwColor(colorSaturation));
strip->Show();
delay(1000);
// turn off the pixels
strip->SetPixelColor(0, black);
strip->SetPixelColor(1, black);
strip->SetPixelColor(2, black);
strip->SetPixelColor(3, black);
strip->SetPixelColor(4, black);
strip->Show();
delay(1000);
return;
*/
if (shouldReboot || systemSettingsChanged)
{
_dln("Loop :: reboot requested, so long and thanks for all the fish!");
delay(100);
ESP.restart();
}
currentTime = millis();
updateDebugStatus();
if (connectionSettingsChanged)
{
_dln("Loop :: connection settings changed");
initWiFi();
connectionSettingsChanged = false;
}
updateWiFi();
updateLED();
//updateNTPClient();
//checkTriggers();
//stairs->tick();
}
void handleNotFound(AsyncWebServerRequest *request)
{
_d("HTTP :: not found: "); _dln(request->url());
request->send(404);
}
/*
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <WiFiUDP.h>
#include <ESPAsyncWebServer.h>
#include <ESPAsyncTCP.h>
#include <TimeLib.h>
#include <ArduinoJson.h>
#include <EspSaveCrash.h>
extern "C" {
#include <user_interface.h>
}
#include "./components/PCA9685.h"
#include "./settings/connection.h"
#include "./main.triggers.h"
ADC_MODE(ADC_VCC);
inline void waitForTransition()
{
while (stairs->inTransition())
{
currentTime = millis();
stairs->tick();
delay(1);
}
}
*/

68
src/main.debug.h Normal file
View File

@ -0,0 +1,68 @@
#ifdef SerialDebug
void wifiEvent(WiFiEvent_t event);
void initDebug()
{
// onEvent is already deprecated, but since I'm only using it
// for debug purposes we'll see how long it lasts...
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
WiFi.onEvent(wifiEvent);
_d("WiFi :: MAC address: ");
_dln(WiFi.macAddress());
#pragma GCC diagnostic pop
}
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;
default:
break;
}
}
uint32_t debugStatusTime = 0;
void updateDebugStatus()
{
if (currentTime - debugStatusTime < 5000) return;
debugStatusTime = currentTime;
_d("Status :: available heap: ");
_dln(ESP.getFreeHeap());
}
#else
#define initDebug() do { } while (0)
#define updateDebugStatus() do { } while (0)
#endif

70
src/main.led.h Normal file
View File

@ -0,0 +1,70 @@
enum LEDState
{
Off,
BlinkLow,
BlinkHigh,
On
};
bool ledAP = false;
LEDState ledWiFi = Off;
uint32_t blinkOnTime = 0;
void updateLED()
{
uint8_t value = (currentTime - blinkOnTime >= 1000) ? LOW : HIGH;
WiFiMode_t mode = WiFi.getMode();
if (mode == WIFI_AP_STA || mode == WIFI_AP)
{
if (!ledAP)
{
digitalWrite(systemSettings->pinLEDAP(), HIGH);
ledAP = true;
}
}
else
{
if (ledAP)
{
digitalWrite(systemSettings->pinLEDAP(), LOW);
ledAP = false;
}
}
if (mode == WIFI_AP_STA || mode == WIFI_STA)
{
wl_status_t status = WiFi.status();
if (status == WL_CONNECTED)
{
if (ledWiFi != On)
{
digitalWrite(systemSettings->pinLEDSTA(), HIGH);
ledWiFi = On;
}
}
else
{
LEDState expectedState = value == HIGH ? BlinkHigh : BlinkLow;
if (ledWiFi != expectedState)
{
digitalWrite(systemSettings->pinLEDSTA(), value);
ledWiFi = expectedState;
}
}
}
else
{
if (ledWiFi != Off)
{
digitalWrite(systemSettings->pinLEDSTA(), LOW);
ledWiFi = Off;
}
}
if (currentTime - blinkOnTime >= 2000)
blinkOnTime = currentTime;
}

121
src/main.wifi.h Normal file
View File

@ -0,0 +1,121 @@
bool accessPoint = false;
bool stationMode = false;
uint32_t stationModeStart = 0;
uint32_t apButtonStart = 0;
void startAccessPoint();
void startStationMode();
void initWiFi()
{
WiFi.disconnect();
WiFi.softAPdisconnect();
accessPoint = connectionSettings->flag(AccessPoint);
stationMode = connectionSettings->flag(StationMode) && connectionSettings->ssid() != nullptr;
WiFi.mode(accessPoint && stationMode ? WIFI_AP_STA :
accessPoint ? WIFI_AP :
stationMode ? WIFI_STA :
WIFI_OFF);
if (accessPoint)
startAccessPoint();
if (stationMode)
startStationMode();
}
void updateWiFi()
{
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);
}
}
if (!accessPoint)
{
if (digitalRead(systemSettings->pinAPButton()) == LOW)
{
if (apButtonStart == 0)
apButtonStart = currentTime;
else if (currentTime - apButtonStart >= APButtonHoldTime)
{
connectionSettings->flag(AccessPoint, true);
connectionSettings->write();
startAccessPoint();
apButtonStart = 0;
}
}
else if (apButtonStart > 0)
apButtonStart = 0;
}
}
void startAccessPoint()
{
_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");
}
void startStationMode()
{
_d("WiFi :: starting station mode to: ");
_dln(connectionSettings->ssid());
stationModeStart = currentTime;
if (connectionSettings->hostname() != nullptr)
WiFi.hostname(connectionSettings->hostname());
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");
}

192
src/server/api.cpp Normal file
View File

@ -0,0 +1,192 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./api.h"
#include <ArduinoJson.h>
#include <IPAddress.h>
#include <ESP8266WiFi.h>
#include "./shared.h"
#include "../assets/version.h"
#include "../debug.h"
#include "../global.h"
#include "../settings/connection.h"
/*
void handleSet(AsyncWebServerRequest *request)
{
_dln("API :: set");
AsyncWebParameter* param;
uint8_t value = 0;
param = request->getParam("value");
if (param != nullptr)
{
value = param->value().toInt();
}
else
{
param = request->getParam("percent");
if (param != nullptr)
{
value = map(param->value().toInt(), 0, 100, 0, 255);
}
else
{
request->send(400);
return;
}
}
uint16_t time = 0;
uint8_t from = 0;
param = request->getParam("time");
if (param != nullptr)
time = param->value().toInt();
param = request->getParam("from");
if (param != nullptr)
{
if (param->value() == "top")
from = 1;
else if (param->value() == "bottom")
from = 2;
}
if (from == 0 || time == 0)
stairs->setAll(value, time, 0);
else
stairs->sweep(value, time, from == 1);
request->send(200);
}
void handleGetStepValues(AsyncWebServerRequest *request)
{
_dln("API :: get steps");
DynamicJsonBuffer jsonBuffer(JSON_ARRAY_SIZE(17));
bool target = !request->hasParam("current");
JsonArray& root = jsonBuffer.createArray();
for (uint8_t step = 0; step < stepsSettings->count(); step++)
root.add(stairs->get(step, target));
AsyncResponseStream *response = request->beginResponseStream("application/json");
root.printTo(*response);
request->send(response);
}
void handlePostStepValues(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post steps");
DynamicJsonBuffer jsonBuffer(2*JSON_ARRAY_SIZE(17) + JSON_OBJECT_SIZE(3) + 130);
JsonObject& root = jsonBuffer.parseObject((char*)data);
if (!root.success())
{
request->send(400);
return;
}
uint16_t transitionTime = root["transitionTime"];
JsonArray& values = root["values"];
JsonArray& startTime = root["startTime"];
size_t startTimeCount = startTime.size();
size_t valueCount = values.size();
if (valueCount > stepsSettings->count())
valueCount = stepsSettings->count();
for (uint8_t step = 0; step < valueCount; step++)
stairs->set(step, values[step], transitionTime, step < startTimeCount ? startTime[step] : 0);
request->send(200);
}
void handleGetTimeTriggers(AsyncWebServerRequest *request)
{
_dln("API :: get time triggers");
AsyncResponseStream *response = request->beginResponseStream("application/json");
timeTriggerSettings->toJson(*response);
request->send(response);
}
void handlePostTimeTriggers(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post time triggers");
bool changed;
if (timeTriggerSettings->fromJson((char*)data, &changed))
{
timeTriggerSettings->write();
if (changed)
timeTriggerSettingsChanged = true;
request->send(200);
}
else
request->send(400);
}
void handleGetMotionTriggers(AsyncWebServerRequest *request)
{
_dln("API :: get motion triggers");
AsyncResponseStream *response = request->beginResponseStream("application/json");
motionTriggerSettings->toJson(*response);
request->send(response);
}
void handlePostMotionTriggers(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post motion triggers");
bool changed;
if (motionTriggerSettings->fromJson((char*)data, &changed))
{
motionTriggerSettings->write();
if (changed)
motionTriggerSettingsChanged = true;
request->send(200);
}
else
request->send(400);
}
*/
void registerAPIRoutes(AsyncWebServer* server)
{
/*
server->on("/api/set", HTTP_GET, handleSet);
server->on("/api/steps/values", HTTP_GET, handleGetStepValues);
server->on("/api/steps/values", HTTP_POST, devNullRequest, devNullFileUpload, handlePostStepValues);
server->on("/api/triggers/time", HTTP_GET, handleGetTimeTriggers);
server->on("/api/triggers/time", HTTP_POST, devNullRequest, devNullFileUpload, handlePostTimeTriggers);
server->on("/api/triggers/motion", HTTP_GET, handleGetMotionTriggers);
server->on("/api/triggers/motion", HTTP_POST, devNullRequest, devNullFileUpload, handlePostMotionTriggers);
*/
}

13
src/server/api.h Normal file
View File

@ -0,0 +1,13 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __server_api
#define __server_api
#include <ESPAsyncWebServer.h>
void registerAPIRoutes(AsyncWebServer* server);
#endif

74
src/server/firmware.cpp Normal file
View File

@ -0,0 +1,74 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./firmware.h"
#include "../config.h"
#include "../debug.h"
#include "../global.h"
void handleFirmware(AsyncWebServerRequest *request)
{
shouldReboot = !Update.hasError();
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", shouldReboot ? "OK" : "FAIL");
response->addHeader("Connection", "close");
request->send(response);
}
void handleFirmwareFile(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{
_d("Firmware :: file upload: index = "); _d(index);
_d(", len = "); _d(len);
_d(", final = "); _dln(final);
if (!index)
{
uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
_d("Firmware :: update start, max sketch space: ");
_dln(maxSketchSpace);
Update.runAsync(true);
if (!Update.begin(maxSketchSpace))
{
#ifdef SerialDebug
Update.printError(Serial);
#endif
}
}
if (!Update.hasError())
{
if (Update.write(data, len) != len)
{
#ifdef SerialDebug
Update.printError(Serial);
#endif
}
}
if (final)
{
if (Update.end(true))
{
_dln("Firmware :: success");
}
else
{
_dln("Firmware :: failed");
#ifdef SerialDebug
Update.printError(Serial);
#endif
}
}
}
void registerFirmwareRoutes(AsyncWebServer* server)
{
server->on("/api/firmware", HTTP_POST, handleFirmware, handleFirmwareFile);
}

13
src/server/firmware.h Normal file
View File

@ -0,0 +1,13 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __server_firmware
#define __server_firmware
#include <ESPAsyncWebServer.h>
void registerFirmwareRoutes(AsyncWebServer* server);
#endif

252
src/server/settings.cpp Normal file
View File

@ -0,0 +1,252 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./settings.h"
#include <ArduinoJson.h>
#include <IPAddress.h>
#include <ESP8266WiFi.h>
#include <EspSaveCrash.h>
#include "./shared.h"
#include "../assets/version.h"
#include "../config.h"
#include "../debug.h"
#include "../global.h"
#include "../settings/connection.h"
extern "C" {
#include <user_interface.h>
}
bool clearedResetReason = false;
/*
void handleStatus(AsyncWebServerRequest *request)
{
_dln("API :: status");
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(6));
JsonObject& root = jsonBuffer.createObject();
root["systemID"] = String(ESP.getChipId(), HEX);
root["version"] = String(VersionFullSemVer);
if (ntpClient != nullptr && hasTimezone)
{
root["time"] = ntpClient->getEpochTime();
root["timeOffset"] = timezoneOffset;
}
else
{
root["time"] = false;
root["timeOffset"] = 0;
}
root["resetReason"] = clearedResetReason ? 0 : ESP.getResetInfoPtr()->reason;
root["stackTrace"] = SaveCrash.count() > 0;
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 handleGetSystem(AsyncWebServerRequest *request)
{
_dln("API :: get system");
AsyncResponseStream *response = request->beginResponseStream("application/json");
systemSettings->toJson(*response);
request->send(response);
}
void handlePostSystem(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post system");
bool changed;
if (systemSettings->fromJson((char*)data, &changed))
{
systemSettings->write();
if (changed)
systemSettingsChanged = true;
request->send(200);
}
else
request->send(400);
}
void handleGetSteps(AsyncWebServerRequest *request)
{
_dln("API :: get steps");
AsyncResponseStream *response = request->beginResponseStream("application/json");
stepsSettings->toJson(*response);
request->send(response);
}
void handlePostSteps(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post steps");
bool changed;
if (stepsSettings->fromJson((char*)data, &changed))
{
stepsSettings->write();
if (changed)
stepsSettingsChanged = true;
request->send(200);
}
else
request->send(400);
}
void handleGetStackTrace(AsyncWebServerRequest *request)
{
_dln("API :: get stack trace");
if (SaveCrash.count() == 0)
{
request->send(404);
return;
}
AsyncResponseStream *response = request->beginResponseStream("application/octet-stream");
response->addHeader("Content-Disposition", "attachment; filename=\"stacktrace.txt\"");
SaveCrash.print(*response);
request->send(response);
}
void handleDeleteStackTrace(AsyncWebServerRequest *request)
{
_dln("API :: delete stack trace");
clearedResetReason = true;
SaveCrash.clear();
request->send(200);
}
#ifdef EnableCrashAPI
#pragma "!!! Crash API is enabled on this build !!!"
void handleCrashException(AsyncWebServerRequest *request)
{
_dln("API :: crash exception");
int* i = nullptr;
*i = 42;
}
void handleCrashSoftWDT(AsyncWebServerRequest *request)
{
_dln("API :: crash soft WDT");
while (true);
}
void handleCrashWDT(AsyncWebServerRequest *request)
{
_dln("API :: crash WDT");
ESP.wdtDisable();
while (true);
}
#endif
*/
void registerSettingsRoutes(AsyncWebServer* server)
{
/*
server->on("/api/status", HTTP_GET, handleStatus);
server->on("/api/connection/status", HTTP_GET, handleConnectionStatus);
server->on("/api/connection", HTTP_GET, handleGetConnection);
server->on("/api/connection", HTTP_POST, devNullRequest, devNullFileUpload, handlePostConnection);
server->on("/api/system", HTTP_GET, handleGetSystem);
server->on("/api/system", HTTP_POST, devNullRequest, devNullFileUpload, handlePostSystem);
server->on("/api/steps", HTTP_GET, handleGetSteps);
server->on("/api/steps", HTTP_POST, devNullRequest, devNullFileUpload, handlePostSteps);
server->on("/api/stacktrace/get", HTTP_GET, handleGetStackTrace);
server->on("/api/stacktrace/delete", HTTP_GET, handleDeleteStackTrace);
#ifdef EnableCrashAPI
server->on("/api/crash/exception", HTTP_GET, handleCrashException);
server->on("/api/crash/softwdt", HTTP_GET, handleCrashSoftWDT);
server->on("/api/crash/wdt", HTTP_GET, handleCrashWDT);
#endif
*/
}

13
src/server/settings.h Normal file
View File

@ -0,0 +1,13 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __server_settings
#define __server_settings
#include <ESPAsyncWebServer.h>
void registerSettingsRoutes(AsyncWebServer* server);
#endif

10
src/server/shared.cpp Normal file
View File

@ -0,0 +1,10 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./shared.h"
void devNullRequest(AsyncWebServerRequest *request) {}
void devNullFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {}

15
src/server/shared.h Normal file
View File

@ -0,0 +1,15 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __server_shared
#define __server_shared
#include <ESPAsyncWebServer.h>
void devNullRequest(AsyncWebServerRequest *request);
void devNullFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final);
#endif

29
src/server/static.cpp Normal file
View File

@ -0,0 +1,29 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./static.h"
#include "../debug.h"
#include "../assets/html.h"
#include "../assets/js.h"
#include "../assets/css.h"
void handleGzipped(AsyncWebServerRequest *request, const String& contentType, const uint8_t * content, size_t len)
{
_d("HTTP :: static: "); _dln(request->url());
AsyncWebServerResponse *response = request->beginResponse_P(200, contentType, content, len);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
}
void registerStaticRoutes(AsyncWebServer* server)
{
server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) { handleGzipped(request, "text/html", EmbeddedIndex, sizeof(EmbeddedIndex)); });
server->on("/bundle.js", HTTP_GET, [](AsyncWebServerRequest *request) { handleGzipped(request, "text/javascript", EmbeddedBundleJS, sizeof(EmbeddedBundleJS)); });
server->on("/bundle.css", HTTP_GET, [](AsyncWebServerRequest *request) { handleGzipped(request, "text/css", EmbeddedBundleCSS, sizeof(EmbeddedBundleCSS)); });
}

14
src/server/static.h Normal file
View File

@ -0,0 +1,14 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __server_static
#define __server_static
#include <ESPAsyncWebServer.h>
void registerStaticRoutes(AsyncWebServer* server);
#endif

View File

@ -0,0 +1,72 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./abstractjson.h"
#include <LittleFS.h>
void AbstractJsonSettings::read()
{
_d(getDebugPrefix()); _dln(" :: opening file");
File settingsFile = LittleFS.open(getFilename(), "r");
if (!settingsFile)
{
_d(getDebugPrefix()); _dln(" :: failed to open file");
return;
}
size_t size = settingsFile.size();
if (size > 1024)
{
_d(getDebugPrefix()); _dln(" :: file size is too large");
return;
}
if (size == 0)
{
_d(getDebugPrefix()); _dln(" :: zero size file");
return;
}
std::unique_ptr<char[]> buf(new char[size]);
settingsFile.readBytes(buf.get(), size);
settingsFile.close();
_dln(buf.get());
if (fromJson(buf.get()))
{
_d(getDebugPrefix());
_dln(" :: read from file");
}
else
{
_d(getDebugPrefix());
_dln(" :: failed to parse file");
}
}
void AbstractJsonSettings::write()
{
_d(getDebugPrefix()); _dln(" :: opening file for writing");
File settingsFile = LittleFS.open(getFilename(), "w");
if (!settingsFile)
{
_d(getDebugPrefix()); _dln(" :: failed to open file for writing");
return;
}
toJson(settingsFile);
settingsFile.close();
_d(getDebugPrefix()); _dln(" :: written to file");
}
bool AbstractJsonSettings::fromJson(char* data)
{
return fromJson(data, nullptr);
}

View File

@ -0,0 +1,28 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __settingsjson
#define __settingsjson
#include "../debug.h"
class AbstractJsonSettings
{
protected:
virtual const char* getFilename() = 0;
virtual const char* getDebugPrefix() = 0;
public:
void read();
void write();
virtual void toJson(Print &print) = 0;
virtual bool fromJson(char* data, bool* changed) = 0;
bool fromJson(char* data);
};
#endif

View File

@ -0,0 +1,97 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./connection.h"
#include <ArduinoJson.h>
#include "./abstractjson.h"
#include "../debug.h"
#include "../config.h"
#include "../global.h"
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, bool* changed)
{
/*
if (changed != nullptr)
*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 == nullptr || !jsonIP.fromString(jsonIPText)) jsonIP = emptyIP;
if (jsonSubnetMaskText == nullptr || !jsonSubnetMask.fromString(jsonSubnetMaskText)) jsonSubnetMask = emptyIP;
if (jsonGatewayText == nullptr || !jsonGateway.fromString(jsonGatewayText)) jsonGateway = emptyIP;
if (!(jsonAccessPoint || jsonStation))
jsonAccessPoint = true;
if ((!sameStr(jsonHostname, hostname())) ||
(jsonAccessPoint != flag(AccessPoint)) ||
(jsonStation != flag(StationMode)) ||
(!sameStr(jsonSSID, ssid())) ||
(!sameStr(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 != nullptr)
*changed = true;
}
return true;
*/
}

74
src/settings/connection.h Normal file
View File

@ -0,0 +1,74 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __settingsconnection
#define __settingsconnection
#include <stdint.h>
#include <stdbool.h>
#include <IPAddress.h>
#include "./abstractjson.h"
#include "../charproperties.h"
#include "../config.h"
enum ConnectionSettingsFlags
{
AccessPoint = 1,
StationMode = 2,
DHCP = 4
};
class ConnectionSettings : public AbstractJsonSettings
{
private:
char* mHostname = nullptr;
uint8_t mFlags = AccessPoint | DHCP;
char* mSSID = nullptr;
char* mPassword = nullptr;
IPAddress mIP = (uint32_t)0;
IPAddress mSubnetMask = (uint32_t)0;
IPAddress mGateway = (uint32_t)0;
protected:
virtual const char* getFilename() { return ConnectionSettingsFile; };
virtual const char* getDebugPrefix() { return "ConnectionSettings"; };
public:
void toJson(Print &print);
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

113
src/settings/system.cpp Normal file
View File

@ -0,0 +1,113 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#include "./system.h"
#include <ArduinoJson.h>
#include <FS.h>
#include "../debug.h"
#include "../global.h"
#include "../config.h"
void SystemSettings::toJson(Print &print)
{
/*
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(8) + JSON_OBJECT_SIZE(5));
JsonObject& root = jsonBuffer.createObject();
JsonObject& pins = root.createNestedObject("pins");
pins["ledAP"] = pinLEDAP();
pins["ledSTA"] = pinLEDSTA();
pins["apButton"] = pinAPButton();
pins["pwmSDA"] = pinPWMDriverSDA();
pins["pwmSCL"] = pinPWMDriverSCL();
root["pwmAddress"] = pwmDriverAddress();
root["pwmFrequency"] = pwmDriverFrequency();
root["mapsAPIKey"] = mapsAPIKey();
root.printTo(print);
*/
}
bool SystemSettings::fromJson(char* data, bool* changed)
{
/*
if (changed != nullptr)
*changed = false;
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(8) + JSON_OBJECT_SIZE(5) + 500);
JsonObject& root = jsonBuffer.parseObject(data);
if (!root.success())
return false;
const char* jsonNTPServer = root["ntpServer"];
uint32_t jsonNTPInterval = root["ntpInterval"];
double jsonLat = root["lat"];
double jsonLng = root["lng"];
JsonObject& pins = root["pins"];
uint8_t jsonPinLEDAP = pins["ledAP"];
uint8_t jsonPinLEDSTA = pins["ledSTA"];
uint8_t jsonPinAPButton = pins["apButton"];
uint8_t jsonPinPWMDriverSDA = pins["pwmSDA"];
uint8_t jsonPinPWMDriverSCL = pins["pwmSCL"];
uint8_t jsonPWMDriverAddress = root["pwmAddress"];
uint16_t jsonPWMDriverFrequency = root["pwmFrequency"];
const char* jsonMapAPIKey = root["mapsAPIKey"];
if (jsonNTPServer == nullptr) jsonNTPServer = DefaultNTPServer;
if (jsonNTPInterval == 0) jsonNTPInterval = 5;
if (jsonPinLEDAP == 0) jsonPinLEDAP = pinLEDAP();
if (jsonPinLEDSTA == 0) jsonPinLEDSTA = pinLEDSTA();
if (jsonPinAPButton == 0) jsonPinAPButton = pinAPButton();
if (jsonPinPWMDriverSDA == 0) jsonPinPWMDriverSDA = pinPWMDriverSDA();
if (jsonPinPWMDriverSCL == 0) jsonPinPWMDriverSCL = pinPWMDriverSCL();
if (jsonPWMDriverAddress == 0) jsonPWMDriverAddress = pwmDriverAddress();
if (jsonPWMDriverFrequency == 0) jsonPWMDriverFrequency = pwmDriverFrequency();
if ((jsonPinLEDAP != pinLEDAP()) ||
(jsonPinLEDSTA != pinLEDSTA()) ||
(jsonPinAPButton != pinAPButton()) ||
(jsonPinPWMDriverSDA != pinPWMDriverSDA()) ||
(jsonPinPWMDriverSCL != pinPWMDriverSCL()) ||
(jsonPWMDriverAddress != pwmDriverAddress()) ||
(jsonPWMDriverFrequency != pwmDriverFrequency()) ||
(!sameStr(jsonMapAPIKey, mapsAPIKey())) ||
(jsonLat != latitude()) ||
(jsonLng != longitude()) ||
(!sameStr(jsonNTPServer, ntpServer())) ||
(jsonNTPInterval != ntpInterval()))
{
latitude(jsonLat);
longitude(jsonLng);
pinLEDAP(jsonPinLEDAP);
pinLEDSTA(jsonPinLEDSTA);
pinAPButton(jsonPinAPButton);
pinPWMDriverSDA(jsonPinPWMDriverSDA);
pinPWMDriverSCL(jsonPinPWMDriverSCL);
pwmDriverAddress(jsonPWMDriverAddress);
pwmDriverFrequency(jsonPWMDriverFrequency);
mapsAPIKey(jsonMapAPIKey);
ntpServer(jsonNTPServer);
ntpInterval(jsonNTPInterval);
if (changed != nullptr)
*changed = true;
}
return true;
*/
}

47
src/settings/system.h Normal file
View File

@ -0,0 +1,47 @@
/*
* ESP8266 RGBW controller
* Copyright 2020 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/RGBWifi
*/
#ifndef __settingssystem
#define __settingssystem
#include <Arduino.h>
#include <stdint.h>
#include <stdbool.h>
#include "../charproperties.h"
#include "./abstractjson.h"
class SystemSettings : public AbstractJsonSettings
{
private:
uint8_t mPinLEDAP = 4;
uint8_t mPinLEDSTA = 5;
uint8_t mPinAPButton = 2;
protected:
virtual const char* getFilename() { return SystemSettingsFile; };
virtual const char* getDebugPrefix() { return "SystemSettings"; };
public:
SystemSettings()
{
}
void toJson(Print &print);
bool fromJson(char* data, bool* changed);
uint8_t pinLEDAP() { return mPinLEDAP; }
void pinLEDAP(uint8_t value) { mPinLEDAP = value; }
uint8_t pinLEDSTA() { return mPinLEDSTA; }
void pinLEDSTA(uint8_t value) { mPinLEDSTA = value; }
uint8_t pinAPButton() { return mPinAPButton; }
void pinAPButton(uint8_t value) { mPinAPButton = value; }
};
#endif

555
web/app.js Normal file
View File

@ -0,0 +1,555 @@
function startApp()
{
// Source: https://github.com/axios/axios/issues/164
axios.interceptors.response.use(undefined, function axiosRetryInterceptor(err) {
var config = err.config;
// If config does not exist or the retry option is not set, reject
if(!config || !config.retry) return Promise.reject(err);
// Set the variable for keeping track of the retry count
config.__retryCount = config.__retryCount || 0;
// Check if we've maxed out the total number of retries
if(config.__retryCount >= config.retry) {
// Reject with the error
return Promise.reject(err);
}
// Increase the retry count
config.__retryCount += 1;
// Create new promise to handle exponential backoff
var backoff = new Promise(function(resolve) {
setTimeout(function() {
resolve();
}, config.retryDelay || 1);
});
// Return the promise in which recalls axios to retry the request
return backoff.then(function() {
return axios(config);
});
});
Vue.component('check', {
template: '<div class="check" :class="{ checked: value, disabled: disabled }" @keydown="handleKeyDown" @click="handleClick" tabindex="0"><div class="control"><div class="inner"></div></div><div class="label">{{ title }}</div></div>',
props: {
title: String,
value: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
methods: {
handleClick: function()
{
if (this.disabled) return;
this.value = !this.value;
this.$emit('input', this.value);
},
handleKeyDown: function(event)
{
if (event.keyCode == 32)
{
this.handleClick();
event.preventDefault();
}
}
}
});
Vue.component('radio', {
template: '<div class="radio" :class="{ checked: value == id, disabled: disabled }" @keydown="handleKeyDown" @click="handleClick" tabindex="0"><div class="control"><div class="inner"></div></div><div class="label">{{ title }}</div></div>',
props: {
title: String,
value: null,
id: null,
disabled: {
type: Boolean,
default: false
}
},
methods: {
handleClick: function()
{
if (this.disabled) return;
this.value = this.id;
this.$emit('input', this.value);
},
handleKeyDown: function(event)
{
if (event.keyCode == 32)
{
this.handleClick();
event.preventDefault();
}
}
}
});
Vue.component('range', {
template: '<div>' +
'<div class="start">' +
'<span class="value">{{ value.start }}</span>' +
'<div class="slidercontainer">' +
'<input type="range" min="0" max="4094" class="slider" v-model.number="value.start">' +
'</div>' +
'</div>' +
'<div class="end">' +
'<span class="value">{{ value.end }}</span>' +
'<div class="slidercontainer">' +
'<input type="range" min="1" max="4095" class="slider" v-model.number="value.end">' +
'</div>' +
'</div>' +
'</div>',
props: ['value'],
mounted: function()
{
this.oldValue = { start: this.value.start, end: this.value.end };
},
watch: {
value: {
handler: function(newValue)
{
if (newValue.start != this.oldValue.start)
{
if (newValue.start > newValue.end)
{
newValue.end = newValue.start + 1;
this.$emit('input', newValue);
}
}
else if (newValue.end != this.oldValue.end)
{
if (newValue.end < newValue.start)
{
newValue.start = newValue.end - 1;
this.$emit('input', newValue);
}
}
this.oldValue.start = newValue.start;
this.oldValue.end = newValue.end;
},
deep: true
}
}
});
var i18n = new VueI18n({
locale: navigator.language.split('-')[0],
fallbackLocale: 'en',
messages: messages
});
var app = new Vue({
el: '#app',
i18n: i18n,
data: {
notification: null,
loading: true,
saving: false,
loadingIndicator: '|',
uploadProgress: false,
activeTab: 'status',
status: {
systemID: 'loading...',
version: 'loading...',
resetReason: null,
stackTrace: false
},
wifiStatus: {
ap: {
enabled: false,
ip: '0.0.0.0'
},
station: {
enabled: false,
status: 0,
ip: '0.0.0.0'
}
},
connection: {
hostname: null,
accesspoint: true,
station: false,
ssid: null,
password: null,
dhcp: true,
ip: null,
subnetmask: null,
gateway: null
},
system: {
pins: {
ledAP: null,
ledSTA: null,
apButton: null,
}
}
},
created: function()
{
var self = this;
self.notificationTimer = null;
document.title = i18n.t('title');
var hash = window.location.hash.substr(1);
if (hash)
self.activeTab = hash;
self.startLoadingIndicator();
self.updateWiFiStatus();
// Sequential loading of all the settings makes sure
// we don't overload the ESP8266 with requests, as that
// can cause it to run out of memory easily.
// This is a horrible way to implement it, but I don't feel like
// including a big library or working out a clean short solution
// at the moment, and it works :)
self.loadStatus().then(function()
{
self.loadConnection().then(function()
{
self.loadSystem().then(function()
{
self.stopLoadingIndicator();
self.loading = false;
});
});
});
},
methods: {
showNotification: function(message, error)
{
var self = this;
self.notification = {
message: message,
error: error
};
if (self.notificationTimer != null)
clearTimeout(self.notificationTimer);
self.notificationTimer = setTimeout(function()
{
self.notification = null;
self.notificationTimer = null;
}, 5000);
},
hideNotification: function()
{
var self = this;
self.notification = null;
if (self.notificationTimer != null)
{
clearTimeout(self.notificationTimer);
self.notificationTimer = null;
}
},
handleAPIError: function(messageId, error)
{
var self = this;
console.log(error);
var errorMessage = '';
if (error.response)
{
errorMessage = 'HTTP response code ' + error.response.status;
}
else if (error.request)
{
errorMessage = 'No response';
}
else
{
errorMessage = error.message;
}
self.showNotification(i18n.t(messageId) + '\n\n' + errorMessage, true);
},
loadStatus: function()
{
var self = this;
return axios.get('/api/status', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.status = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.loadStatus'));
},
loadConnection: function()
{
var self = this;
return axios.get('/api/connection', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.connection = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.loadConnection'));
},
loadSystem: function()
{
var self = this;
return axios.get('/api/system', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.system = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.loadSystem'));
},
applyConnection: function()
{
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,
}, { retry: 10, retryDelay: 1000 })
.then(function(response)
{
})
.catch(self.handleAPIError.bind(self, 'error.applyConnection'))
.then(function()
{
self.saving = false;
});
},
applySystem: function()
{
var self = this;
if (self.saving) return;
self.saving = true;
axios.post('/api/system', self.system, { retry: 10, retryDelay: 1000 })
.then(function(response)
{
self.showNotification(i18n.t('rebootPending'));
})
.catch(self.handleAPIError.bind(self, 'error.applySystem'))
.then(function()
{
self.saving = false;
});
},
startLoadingIndicator: function()
{
var self = this;
self.loadingStage = 0;
self.loadingTimer = setInterval(function()
{
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)
{
axios.get('/api/connection/status', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.wifiStatus = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.updateWiFiStatus'))
.then(function()
{
setTimeout(self.updateWiFiStatus, 5000);
});
}
else
setTimeout(self.updateWiFiStatus, 5000);
},
uploadFirmware: function()
{
var self = this;
if (self.saving) return;
self.saving = true;
self.uploadProgress = 0;
var data = new FormData();
data.append('file', document.getElementById('firmwareFile').files[0]);
var config = {
timeout: 360000,
onUploadProgress: function(progressEvent)
{
self.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
}
};
axios.post('/api/firmware', data, config)
.then(function(response)
{
self.showNotification(i18n.t('rebootPending'));
})
.catch(self.handleAPIError.bind(self, 'error.uploadFirmware'))
.then(function()
{
self.uploadProgress = false;
self.saving = false;
document.getElementById('firmware').reset();
});
},
deleteStackTrace: function()
{
var self = this;
return axios.get('/api/stacktrace/delete', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
self.status.resetReason = 0;
self.status.stackTrace = false;
})
.catch(self.handleAPIError.bind(self, 'error.stackTraceDeleteError'));
}
},
computed: {
hasResetError: function()
{
var self = this;
/*
REASON_DEFAULT_RST = 0 normal startup by power on
REASON_WDT_RST = 1 hardware watch dog reset
REASON_EXCEPTION_RST = 2 exception reset, GPIO status wont change
REASON_SOFT_WDT_RST = 3 software watch dog reset, GPIO status wont change
REASON_SOFT_RESTART = 4 software restart ,system_restart , GPIO status wont change
REASON_DEEP_SLEEP_AWAKE = 5 wake up from deep-sleep
REASON_EXT_SYS_RST = 6 system reset
*/
return (self.status.resetReason === 1 ||
self.status.resetReason === 2 ||
self.status.resetReason === 3 ||
self.status.stackTrace);
}
}
});
}

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

File diff suppressed because one or more lines are too long

172
web/index.html Normal file
View File

@ -0,0 +1,172 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>RGBWifi</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 class="notificationContainer">
<div class="notification" :class="{ error: notification != null && notification.error }" v-if="notification != null" @click.prevent="hideNotification">
<span class="message">{{ notification.message }}</span>
</div>
</div>
<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>{{ status.systemID !== null ? $t('systemID') + ': ' + status.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="warning" v-if="hasResetError">
<p>
{{ $t('error.resetError') }}
</p>
<p class="resetReason">
{{ $t('error.resetReason.' + status.resetReason) }}
</p>
<p v-if="status.stackTrace">
{{ $t('error.stackTrace') }}
</p>
<a class="button button-primary" href="/api/stacktrace/get" v-if="status.stackTrace">{{ $t('error.stackTraceDownload') }}</a>
<a class="button" @click="deleteStackTrace">{{ $t('error.stackTraceDelete') }}</a>
</div>
<div class="navigation tabs">
<a class="button" :class="{ 'active': activeTab == 'status' }" @click="activeTab = 'status'">{{ $t('status.tabTitle') }}</a><a class="button" :class="{ 'active': activeTab == 'connection' }" @click="activeTab = 'connection'">{{ $t('connection.tabTitle') }}</a><a class="button" :class="{ 'active': activeTab == 'system' }" @click="activeTab = 'system'">{{ $t('system.tabTitle') }}</a>
</div>
<div v-if="activeTab == 'status'">
<!--
Status tab
-->
<h3>{{ $t('status.title') }}</h3>
</div>
<div v-if="activeTab == 'connection'">
<!--
Connection tab
-->
<form @submit.prevent="applyConnection">
<h3>{{ $t('connection.title') }}</h3>
<check v-model.boolean="connection.accesspoint" :title="$t('connection.accesspoint')"></check>
<span class="hint">{{ $t('connection.accesspointHint') }}</span>
<check v-model.boolean="connection.station" :title="$t('connection.stationmode')"></check>
<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">
<check v-model.boolean="connection.dhcp" :disabled="!connection.station" :title="$t('connection.dhcp')" class="form-control"></check>
<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 type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
</form>
</div>
<div v-if="activeTab == 'system'">
<!--
System tab
-->
<form @submit.prevent="uploadFirmware">
<h3>{{ $t('system.firmwareTitle') }}</h3>
<input type="file" id="firmwareFile">
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
<div v-if="uploadProgress !== false">
{{ uploadProgress }}%
</div>
</form>
<form @submit.prevent="applySystem">
<h3>{{ $t('system.pinsTitle') }}</h3>
<div class="horizontal">
<label for="pinLEDAP">{{ $t('system.pinLEDAP') }}</label>
<input type="number" id="pinLEDAP" v-model.number="system.pins.ledAP">
</div>
<div class="horizontal">
<label for="pinLEDSTA">{{ $t('system.pinLEDSTA') }}</label>
<input type="number" id="pinLEDSTA" v-model.number="system.pins.ledSTA">
</div>
<div class="horizontal">
<label for="pinAPButton">{{ $t('system.pinAPButton') }}</label>
<input type="number" id="pinAPButton" v-model.number="system.pins.apButton">
</div>
</form>
</div>
</div>
<div class="clearfix"></div>
</div>
<div class="version">
{{ $t('copyright') }}<br>
{{ status.version !== null ? $t('firmwareVersion') + status.version : '' }}
</div>
</div>
</div>
<script language="javascript">
startApp();
</script>
</body>
</html>

191
web/lang.js Normal file
View File

@ -0,0 +1,191 @@
var messages = {
en: {
title: 'RGBWifi',
systemID: 'System ID',
firmwareVersion: 'Firmware version: ',
copyright: 'Copyright © 2020 Mark van Renswoude',
loading: 'Please wait, loading configuration...',
rebootPending: 'The system will be rebooted, please refresh this page afterwards',
applyButton: 'Apply',
applyButtonSaving: 'Saving...',
deviceTime: 'Time: ',
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'
},
connection: {
tabTitle: 'Connection',
title: 'Connection parameters',
accesspoint: 'Enable access point',
accesspointHint: 'Allows for a direct connection from your device to this RGBWifi module for configuration purposes. The RGBWifi 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 RGBWifi 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 RGBWifi 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'
},
system: {
tabTitle: 'System',
pinsTitle: 'Hardware pinout',
firmwareTitle: 'Firmware update',
pinLEDAP: 'Access Point status LED pin (+3.3v)',
pinLEDSTA: 'Station Mode status LED pin (+3.3v)',
pinAPButton: 'Enable Access Point button pin (active low)'
},
error: {
loadStatus: 'Could not load system status',
loadConnection: 'Could not load connection settings',
loadSystem: 'Could not load system settings',
applyConnection: 'Could not save connection settings',
applySystem: 'Could not save system settings',
updateWiFiStatus: 'Could not retrieve WiFi status',
uploadFirmware: 'Error while uploading firmware',
resetError: 'The system reports that it has been reset unexpectedly. The last power up status is:',
resetReason: {
0: 'Normal startup',
1: 'Unresponsive, reset by hardware watchdog',
2: 'Unhandled exception',
3: 'Unresponsive, reset by software watchdog',
4: 'System restart requested',
5: 'Wake up from deep sleep',
6: 'System reset'
},
stackTrace: 'A stack trace is available. Please send it to your nearest developer and/or delete it from this RGBWifi module to remove this message.',
stackTraceDownload: 'Download',
stackTraceDelete: 'Hide',
stackTraceDeleteError: 'Could not remove stack trace'
}
},
nl: {
title: 'RGBWifi',
systemID: 'Systeem ID',
firmwareVersion: 'Firmware versie: ',
copyright: 'Copyright © 2020 Mark van Renswoude',
loading: 'Een ogenblik geduld, bezig met laden van configuratie...',
rebootPending: 'Het systeem wordt opnieuw opgestart, ververse deze pagina nadien',
applyButton: 'Opslaan',
applyButtonSaving: 'Bezig met opslaan...',
deviceTime: 'Tijd: ',
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'
},
connection: {
tabTitle: 'Verbinding',
title: 'Verbinding configuratie',
accesspoint: 'Access point inschakelen',
accesspointHint: 'Maakt het mogelijk om een directe connectie vanaf een apparaat naar deze RGBWifi module te maken om de module te configureren. De RGBWifi 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 RGBWifi 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 RGBWifi 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'
},
system: {
tabTitle: 'Systeem',
pinsTitle: 'Hardware aansluitingen',
firmwareTitle: 'Firmware bijwerken',
pinLEDAP: 'Access Point status LED pin (+3.3v)',
pinLEDSTA: 'WiFi status LED pin (+3.3v)',
pinAPButton: 'Access Point inschakelen knop pin (actief laag)'
},
error: {
loadStatus: 'Kan systeemstatus niet ophalen',
loadConnection: 'Kan verbinding instellingen niet ophalen',
loadSystem: 'Kan systeem instellingen niet ophalen',
applyConnection: 'Kan verbinding instellingen niet opslaan',
applySystem: 'Kan systeem instellingen niet opslaan',
updateWiFiStatus: 'Kan WiFi status niet ophalen',
uploadFirmware: 'Fout tijdens bijwerken van firmware',
resetError: 'Het systeem is onverwachts herstart. De laatste status is:',
resetReason: {
0: 'Normaal opgestart',
1: 'Reageert niet, herstart door hardware watchdog',
2: 'Onafgehandelde fout',
3: 'Reageert niet, herstart door software watchdog',
4: 'Herstart verzoek door systeem',
5: 'Wakker geworden uit diepe slaap',
6: 'Systeem gereset'
},
stackTrace: 'Een stack trace is beschikbaar. Stuur het naar de dichtsbijzijnde ontwikkelaar en/of verwijder het van deze RGBWifi module om dit bericht te verbergen.',
stackTraceDownload: 'Downloaden',
stackTraceDelete: 'Verbergen',
stackTraceDeleteError: 'Kan stack trace niet verwijderen'
}
}
}

333
web/logo.ai Normal file
View 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&#xA;AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK&#xA;DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f&#xA;Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAD4AwER&#xA;AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA&#xA;AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB&#xA;UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE&#xA;1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ&#xA;qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy&#xA;obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp&#xA;0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo&#xA;+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;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Þÿàûÿó]•ûD yšó¿L9{¼7\Q<>âr ¥g¹…1ÄŠñ) V5º€¿íM.Wÿ!7]üŠž CÕPt
endstream endobj 26 0 obj <</BitsPerComponent 8/ColorSpace 27 0 R/Filter[/ASCII85Decode/FlateDecode]/Height 64/Length 40/Width 64>>stream
8;Z]L!=]#/!5bE.$"(^o%O_;W!8uZ9(]Y:<E5V~>
endstream endobj 27 0 obj [/Indexed/DeviceRGB 255 28 0 R] endobj 28 0 obj <</Filter[/ASCII85Decode/FlateDecode]/Length 428>>stream
8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0
b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup`
E1r!/,*0[*9.aFIR2&b-C#s<Xl5FH@[<=!#6V)uDBXnIr.F>oRZ7Dl%MLY\.?d>Mn
6%Q2oYfNRF$$+ON<+]RUJmC0I<jlL.oXisZ;SYU[/7#<&37rclQKqeJe#,UF7Rgb1
VNWFKf>nDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j<etJICj7e7nPMb=O6S7UOH<
PO7r\I.Hu&e0d&E<.')fERr/l+*W,)q^D*ai5<uuLX.7g/>$XKrcYp0n+Xl_nU*O(
l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~>
endstream endobj 21 0 obj <</Intent 29 0 R/Name(Logo)/Type/OCG/Usage 30 0 R>> endobj 29 0 obj [/View/Design] endobj 30 0 obj <</CreatorInfo<</Creator(Adobe Illustrator 16.0)/Subtype/Artwork>>>> endobj 25 0 obj <</AIS false/BM/Normal/CA 1.0/OP false/OPM 1/SA true/SMask/None/Type/ExtGState/ca 1.0/op false>> endobj 24 0 obj <</LastModified(D:20171230161716+02'00')/Private 31 0 R>> endobj 31 0 obj <</AIMetaData 32 0 R/AIPrivateData1 33 0 R/AIPrivateData2 34 0 R/ContainerVersion 11/CreatorVersion 16/NumBlock 2/RoundtripStreamType 1/RoundtripVersion 16>> endobj 32 0 obj <</Length 964>>stream
%!PS-Adobe-3.0
%%Creator: Adobe Illustrator(R) 16.0
%%AI8_CreatorVersion: 16.0.0
%%For: (PsychoMark) ()
%%Title: (logo.ai)
%%CreationDate: 12/30/2017 4:17 PM
%%Canvassize: 16383
%%BoundingBox: 113 218 501 623
%%HiResBoundingBox: 113.6367 218.375 500.3867 622.625
%%DocumentProcessColors:
%AI5_FileFormat 12.0
%AI12_BuildNumber: 682
%AI3_ColorUsage: Color
%AI7_ImageSettings: 0
%%RGBProcessColor: 0 0 0 ([Registration])
%AI3_Cropmarks: 41.5 164.5 553.5 676.5
%AI3_TemplateBox: 297.5 420.5 297.5 420.5
%AI3_TileBox: -8.5 24.5 603.5 816.5
%AI3_DocumentPreview: None
%AI5_ArtSize: 14400 14400
%AI5_RulerUnits: 2
%AI9_ColorModel: 1
%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0
%AI5_TargetResolution: 800
%AI5_NumLayers: 1
%AI9_OpenToView: -1232 1218 0.5 1789 914 26 0 0 82 117 0 0 0 1 1 1 1 1 0 1
%AI5_OpenViewLayers: 7
%%PageOrigin:0 0
%AI7_GridSettings: 72 8 72 8 1 0 0.8 0.8 0.8 0.9 0.9 0.9
%AI9_Flatten: 1
%AI12_CMSettings: 00.MS
%%EndComments
endstream endobj 33 0 obj <</Length 2175>>stream
%%BoundingBox: 113 218 501 623
%%HiResBoundingBox: 113.6367 218.375 500.3867 622.625
%AI7_Thumbnail: 124 128 8
%%BeginData: 2021 Hex Bytes
%0000330000660000990000CC0033000033330033660033990033CC0033FF
%0066000066330066660066990066CC0066FF009900009933009966009999
%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66
%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333
%3333663333993333CC3333FF3366003366333366663366993366CC3366FF
%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99
%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033
%6600666600996600CC6600FF6633006633336633666633996633CC6633FF
%6666006666336666666666996666CC6666FF669900669933669966669999
%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33
%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF
%9933009933339933669933999933CC9933FF996600996633996666996699
%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33
%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF
%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399
%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933
%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF
%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC
%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699
%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33
%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100
%000011111111220000002200000022222222440000004400000044444444
%550000005500000055555555770000007700000077777777880000008800
%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB
%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF
%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF
%524C45FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDF7FFFF
%%EndData
endstream endobj 34 0 obj <</Length 24380>>stream
%AI12_CompressedDataxœÜ½gwò:Ó0z>ïµò(Iè`z ½)Š)¡Æ†]žïo?ÜdãFÙïÙç¹÷ºsg¤Ñhšf¤;c£åÊL¶#Òåw†›?îîr9Üo©˜ýl¨¬VzOÁŸ¬MÁr°U¦°-;$E/¶›zÆ<-Â÷­ úŸñ|û4¤6ƒÕo/ö+<Ymg[÷paãñ<C3A3>÷óÃ=xäõyü„ÇGxÆ@ üi<¡6ÃÍŸCš^ülòGüðÇìö°™,6³ìöoð«×oðy#† á5„|èyyÑ$ii#wÈ
Ãn8ZnüòùÜ!_¾–ߎkr³oPÛ1IÓ¹íjKÑ1ð$S Š‹ ·îAOÑX3¯o<C2AF>=,V“çÃzDq‡">ô»€Þ}¥‡3ÐmôýTÖ৹߃ŽÑ1"Y³”Å1Ö<7F>&9[  4ú²q<C2B2>©ín  ÞxÝA@”ø úÁßP8ä²íÚäz·„EÃ÷EÃàqÀG€¿Øg®)jæŠÀç^ˆ€ð"^ž@òÏùWÌð¼Ý<C2BC>,m2Ô¾ÅÌP @Ì_öQó°"©×ÍbúËçi;!Wà Dq5D4Aÿy…¿lö<E280B9>š{0±ÛÕa<C395>¸.ÂcPþ¸Ôwä¦½í ®º¼>¿LàHo85D½ƒ/„°DÀ3Àpbî?‚ï!q8ÂpÚ`&ëÔb¶ØĸN†%j1¦7ì3D˜?h îöÿ(÷¶¿`ôû=¹áX+÷„1
á~jA¬…Í$·]É ÑJÜûTøž; ôÃÌ[ƒZl à›?ž™gAcuKÔö°«l¦Û?¬Œè<>c°ÐÁäN õÑø4bbCŽ ð<>oöΦŒ“" ÌSð.ú
þ<EFBFBD>Âõ¼Ÿ'§`
˜_ ?ÉÕv‡æn&†·!µÓ¼±n†”=àa×'C@2ºð›.°€kv€Hè%ÔFöHªá~nÈ®ÈÍ„æ¡3_%<25>g~Ô²õÏz´]-è5ÿ…ÿ¬³w{ÚÔ7L7©=7´·Û•@¦ûH˜djϼóŸAÂ7Cþw<C3BE>熫ÕbF wóÅX¾Ìs»º¸hŒ¨.‡QüˆGvüÆ ÇD…É?…e­Ú¦õ×p?žñ2¢†ÔÔXpz§Ít´uXìIa¾¶ë4u ­ùp†-XKnp®\.MÕežA¿ÿgE­ã©n¶mÐ7°Ž¬@$«ý—Íày®Iƒ´i-€ùAò<41>CþÁUlwi?îp<C3AE>ð é…£¡ˆ} |>ÔŽ°oØ׺ø‚—ûöüö>ý€ßþ2x Óáã0LnÀCᘀ®0¢;~ó‡Áz
? aR`ƒÒ¦Nc¸ê˜d†Ñéï·Ô<C2B7>Cèþ¤œÔÂkŒÁÿ³Boxœ§MR MÌñ<C38C>™&gò„—`‚ú¸ß0é¯JØNs†%ðGÜ <Ü<0F>×ÅgHýÃü@<Mr¸2X<32>#'5rºï WÀøNöi¼j…frø¤9Ù‡ž E ÿQ€ŽúƒÀ8dÿA8[ÐaøÌ`Ev,cîI0å40´ÿaœôÊãÑvHM ^°$€ñ;¶nkF?3Xá
aÛy¹42Mö'ÆŽglz¡ MrÕÞ6ìLw[z{<7B>ûØ—‘?€|ýïz…¾d<C2BE>äÆÍõžå¬Ãqî~„ˆ u»Oµg0Rú‚·þ^¯6 <36> ˆEj1:ìIZnbþ¯€¹¬Ýx<AŠäØÃDz÷þÙÿ³ãHi½ßЃ?‡,ÂñÞöOŒ©ÑZ¡áŸ®¥ó©6ÀÓÔC¥Õv¼$'ºÈÄ5½&ãž?ÀÑE*¼z xøtÌ¢Ô1P¼ù5™âr¢xÕ‰¢<E280B0>áhEêZzfúÿsF8](ÄþÔ/`ÛÿÃÃqŽô~»þˆ¿•EcôÂиëP?§þßX7 CÿµþüïZÊôô¯ÿÿÿ@Ó«Åø<C3B8>÷GBÀ5äŒ{ÅAÿµ˜ìçºf<C2BA>mùŸ<C3B9>Ý"àöinN.fs]â…oúŸ·?¤5¾t íÊ®Ñ٣ⷿ´†õ·>)ô_ç´*Žg´Ý# 8ß]ÇðŽ_úÏØHP¶¶jL¢ýÍÿ†AôÜ¢kr?œ
<EFBFBD>‰^Üó„<C3B3>céâ9¬µSv1ùCÜâÝ 'œçJp `=¤—ÒEAï¶{i»ájÁ±l˜[\“ÝÂÍ wö§ñvE }ËT ™Ã~khé= ÷ù¤P3¯×À À°Üçz{ØfL [­íÞÂMjÃîF¡ÝDië¨a7Ü<E28098>+(•° l2änc¸µ,°ÁžnèÝÌïøЯÅÄ@ ãð…|Þ€"4ƒôî »íŒ"ñ¥Õ˜âc^¿—+7õâ}Ðl÷A³1Ö˜_Á7å°LFK —W4• Š¤IêOÒÐ&ÿÞ£<C39E>Žáh±Zì9ÕñFaX2@ÑIiXÛÝ„[çƒ&4à.,oP£¦¤PAbê‚Ônôð' "8~oH¨P‡räjUø{/,Q.|„YW6òï9Þn&'¾T\P´¢âj»¥Pß: zÁP½µpÅ!É¿Æ8Õ5c<°öv'š­“æ<E2809C>‡r<íá°÷„igödfçZÂ0e²-€\Ùqü»ý“¤vp‡Öxc¼ZìÀê<C380>nýß`õÌ€Hâej<65>_>¢W($9]¢D ÃhF:æÅÇ9YÖ!µåëxÛ­5~geuŠ<|´÷|o!Èï“ÀUÿDÒs^~#)Š¡ãÜBôJý°ß!®ñ! ø¨µáfv€©"<22>íŽW<0F>h4 1[ü¦"[<5B>ÊqÊnÿîö¸w7jsh¹`rŽè&! kµÌTŠ‡ÕŠë!›¹žrš<72>7ùeIq8&3ÙJkiÁL'Mˆ<4D>ƒPn )œ_Ð{ŒÇT ÃΈ€Ÿ¶p¡ààöø<E28099>5UÂÔ“ß…Õg½§Ù¶É«² âxQÃún8T®¦åȃ÷7‰F¼*m±î†A¿JK=<3D>E>ˆ¨¯j4@ŒwVƒÓ)MòâQ[¡ÞË_cÏ µ˜<E28093>àSW¦m7eÜó÷)Œª-XÝŠÊÉŠ6<C5A0>ÂÄðp³_€ <¤±)
ª&IICwµØ<C2B5>zOm—¼?Hh´Þc‰“UA˜OJ<10>HD^ ¾aÑ®±¢ÌTyÐàÄœó%4Wûxˆ‰@W<>Ójv ­ö?IÄr²š9ðŸ`<60> ¢~ƒŒå<06>MbñˆÀCI^ö<>5N•3F<33>\ZPc<50>!Ô<ÐÉta="a¾çt±"«¤0<C2A4>2Mßæñ\®)Ã¥¢¶0¡Ø´ÈÕi×ÈaS0»"ƒß+˜¢æ­Ãˆ›âXM¨3„nÄaûñš<E28093>ç×àyÞîÅÏ9QøÑ€±Q$ã!²Úv,ç5ÖÀ+\C%¯nŠa6˜Ïœa5»K,z ç eNÊÁ½Åe ‚˜ƒ&eŽ5)"“ò¨m<C2A8>!-E<>#jÖ$ n<>¢6 Ý<Lþ®!+2K½¶u©y¬M,¯¡Ðh<C390>@-ØZ\ L<>ôbkLÜN<C39C>bL#<23>$c!ª¸QÃf+ø†Å¹05‡ÏVÒX­=B<Òó³¹wÀ覀ñ-íØQ;ˆz¸Zi4£—Ý t)FÒv †C“°”FÓñvµ°õØß‘¾À…-N÷¶aƆ, Å·#˜Zi€TÖIqQª£J‡áèHIœíuÔ[z¿rO<18>¨Ó<Ý4(ßc_Àˆz^ÚMÖàñj£¿_»‰~ðÌž¾ðŠÜôïvÛN¥°ÛÎTbˆU+Ò†øöX8ªØL´ÏD(µZÀâ÷
ÞúZî·;í2M) ¿J?™ÆÌ&GzÀw@4M £ y
X"”ú\@0˜=F5N¹•6å`[Ü£P'mw4ý¨ŠÙjÈ I/f\ÁûԄˆÝ3Q…‰Z"KIOÃ!=Zì×Ã<C397>~Q&ž˜ã±¯(7\£MÆÛ `ƒ= ò« 6åµ âcNˆ‡}²Í©‰øfÀ¦Êy¤-§@ Ï·Ôÿpî†B³pªÊñÌ­ÎŒl#.<2E> 35¥Íhè©ðÐ4þ©1Rz¼[<5B>¥öq£ñæ(h+m´¶2¾Ï¡0J0e«áN9؆jØÍÖK7¹<37>)6ª6£ar¸>^FíIŽ-=/<2F>U°‡õ \g°„£¶@ @«L"¨ä;?ΓV£1µÝiµ<69>ÔZí(,_/Üo )ZmJ…a©­…D­÷ø¸´ãº(¤ØšJz¢£5¦µã=[YÓÍÞ=YiHD¦ÑŽšn7ªâ¶£<C2B6>³äfÃý“T³ŠA“Ñš‰j“N»7älˆ(´ùÛŠ <0A>^Úh@[yÇÈ)ç£uòÌ@»éùØ餵`+r£WÐ?<3F>²ð$ÍÂrÚàï<C3A0>[¼ù<C2BC>º'׌¢¤; hGC®é쨩BCVeb±7¹¹2!=<3D>¦@ªc…AÇ¡Ø¡Uh¹Ý<C2B9>Õ$jA«M?j19¨;è*¼¯aœŒÖ¡JP³éa3Vã¦àùFC —† ŸÚ ï£fš^Æx<C386>[ZÖWwËmx#G†Üø¦çµõVo|Ú ú4ü½5°q<05> Ã&Ú”kÄmHŽ×ÿ¨9ÜXËí~®ép3FªÜJÑÒ"GD‡<µ—ž"ðšÌoÇ(ì)ï§g¸7qO<1D>Þ…}ÇÆâorÕ ©)9ÞãbƒÛÕdBY%j8<6A>&.* gvFU÷B™·j06oá[(Ùáè-1.&t22æ5ØNÞzå‰Öê” H¦fXša¶O•¤¤ˆØˆªxC†©;”ÃÓ˜Me±3‡S#È·ó5ƒ¼°òñž ZF¬C~áQƒ"Ç ¹ öѦÆæ¸çGmæۿʉܸQ.ŒóлáQ(<28>%ûÞ
„óE6*
mâínÏçÇíÈPgá±ÓpXÏÅa»pžìgeZ¹J%Ì“PŽ¡§<C2A1>TýþÃx{°Ä‡]ç£ÿ¾îʦ©Òz<6D>E£ÓjÉ-†nú6ôZ.„̱ôk)ùHÅjŸ§4u‡‹ßSäΘ ÎÿägNâ6ï»íé„sG§éªÏФã5#ŵzÜggå—Z: [¹ÅCrœw»-³#\µI çw±ð{iŸÿùÊÞ]ÎÌz[£3•Ö~îH†Ì‡b>pûýYYÞšü”xÉB» G§áÎËÇg¦<67>sw”±âíb_éIJø•ŽÑîµ#ï¼;­¥É Aô*~ê‡üôë-œ]¥WÝØ4;ßçæáw¯ˆ"ß÷ù±·öN¤,o Ði:ןõ·àÓýo¾2©³®ÈÏm¦å2o˜Nt‡“@ý±:Æ…qðÅšñDæÎïÈ>;¿éœåµ˜#ödçÑ<<3C><>ÇÃ%ü´p¦µ9ƒÚKx†ajqû'ÙÕ]Ê⢟‡L­uÿ `KÇçð°œP¼ó•ÎlƵãá)î ¯?á°‡žú3Ô¸âu,c^ä8ÿHwåÂ2üæ'&±EÎ3“ì}z°ºœdvn¬™1ôjwé\%a~+8£AX^<5E>¯|„ÌÉpnÛw$:“<>˜odþBp“;0¤dÈn†Óòz ½l ©’Ù¥-äb´Þ/óSÞ3LÜ<17>Žw
¢ Á}5hˆ©@_Ébý”x+T™ö9gá<C3A1>æëù*€ƒ»„#™,8}ùÔì<C394>ôö<C3B4>ˆO~žûh6ùxõl<C3B5>CZeù.| ]ðZš°@¿<05>Ùü;OÒ©@è=ô3δó?ŽüÔSý- ÛlhôúmÜu_3õ\¶‘Ÿ¶¿é߯<Â%èµ AßC“÷ÂÀëèdÝL½˜ÿyä?!O|º¾›sS»Ð0ù7'[a¤õ»®fê5{µ˜·Mª u8Z3K²À~ç~q¤:Ã_fHÉPd˜Ž·÷¦Lûq8œ„¸%¸ÉèRFT ¬ zî¥Þ'w3ßw"•'ŠŸi?âƒÄw¢˜<bw:²Ûh_:_bââ“ËMÃ>©9}@t£ÁIõX+e<06>^Ä7 Ç.þ]´N«î h¿ûl·ýÓ1EB‡f”,Zïw¶Ü<Ô\5wQàW°z[$lZ… dÔ4X]ë{08³-7èp|üÚÌ„ß}oÒyh”WpS©àrŽ¢rÓ]’Õ@“i?M@Ø$£ùl­·”ë/jŠµ+õÂS°x
>ÂW
<³Ï¾Q´5Vñb>øîs$Kß°_×xZ,äƒálÈUï Ùãö–;N„5ïùÞÙ³?ûÉ:»Út¶™ö¼{ `T<€]Á¹}öï]á<>Ls:·€×¾î³!GmΊN[~j)‡@·sF$:Ý2'цêWÛ_LϺPÞçG<C3A7>ù[æõnLÛÝgší<C5A1>UägåJ é&èˆFh°}$Ù¥u·(&J^+&êß›ö!N [0Ñí%F‡õá÷MÐ<§<E28099>¡­ûð t<>l5ˆ|;âz
ä÷ØÌz«<>Lëû1Ç><3E>f¾Ó‰rÎ š|w€<¨Ýg½ï‡A¦uh„§¨1”ià—ušÇî™)꧗HÕsp¸:4<13>û<EFBFBD>4Qµ® àÏ$KT½“ óé±÷ŸºIðÛ0<C39B>ÿæ<C3BF> ÞtȾYµÎrÜ?à“-Ú`^b_ç00€h»"h—jO?y¦ ÛxÇ¢á;[ယu^
ë>ÿÆãrg»Ãô öA<01>@h˜"<22>°[¶lù<E280B9>…†Æ…ÐÀ×¥Tâ;-ÅZK°Ôð˜Ñ|¤°×÷õhõš>285~k?Í<11>!é»å™ƒÒÏÄ…hij%?¿§N†d&4>c‰£è*Ûþ#ÉJD¦<>ð•aÄ>õq'”F£gZИOHö©è+ä 47
ì¡ÂïÆ>.<2E>_Žé… 4rôb†ˆÁQ5l,KWÄ Ø¨Y
D“¼Ä"äaˆ»ƒ­VfÀIV<08>ÁÚM2ëF˜2ñÔ2äƒ ™ö€rK†]™ð5¸pVE¸~ع<C398> N%·&Ó ªw™ÁaœÆϾð³g¹³˜M ;`†hhMCì¶P<C2B6>û„†ÂŒÕò)¶ í™v§º)¦-døS®?¢™ О³Zñû©+ín¨Örp€ëØ3ùeúÍü:Ë-úƒta¼ü½gtfc÷ ew·EoÈÝ<C388>ÞšØãA]ÍÏ\¶c6*¶OAé‰ü1ÂœîX¬ÑÜüÐ|ÏW«¿Y|HÙ™inòõt«O<C2AB> q»J¯½b4äZÄž‰Õ áú.n¥O=&ÛpT;»ÂØÙödCNŸ s-½ÙÑ,OÚ eÁäBÔã!*ІµÎ>#uÌ»Ã- à¿B8º*n§ŒÙv ¯¹¶ž§ægv ¾_â¼èð\<5C>u¡ó¢ÃsáÌAžQ]ppŒc-ò<>ûKÁƒ‰¬ÜT™<54>×»%k6u³ÈÂH%€ú_çx ó¸ÊŸùj6dl£ìKúÞ}¹ûÚ#S™x,yÃ2¤Â]ÄýÉPÕ<50>¯ü„‡å ã‰ÇNAfSwVµv¦þZl)ãBO”|†¤ÉPÈίƒRvš} ß ܃㮒õ|iyG€á<E282AC>1øäÜ—v¦Q4ꈞÅpk
>LÂÂ
R¦µJ;Àë³_︙ªûÒ¯Æ(ëζí$ ×2 ð½/͹ù§1z<p,ðåvC)<29>]>9€£Ò˜ø…ƒ~C?»ô=±ÉzEÀ©zyÒ¡l1QøtóPÂÅþ—Ý\,lpyúî¢ëÃ_Î[ÏG$<®=f_é†ÏÖ[?³]=ôî2/Ï­tɹo?~–ÌÛ{"¼<ŒDmç^•è:IIPsËSýÚ¨ž3iŠš¿bOo)„!N¤âßÐÌÓh®¯ï²ålà<6C>%¼ÎWÚ(š‡<C5A1>~&¯ºøíÀ¯{a¼µ3M™ËE»Í¢ë 8@Îɨ«f±8°çp¬þtbÔߤß^÷±¹£µÚ0úƒØ“ÕG¡Ž²+ÿHÙ¿YN;æc!É4åW²š¾eì JD1­•ìâ”êËë \ÆÀ[ÜNs z×0*
¿9 HÑ î‹(snû½¥\u…šÄ¼ã3”¶^cŸ©ö¼f.ªÏ“üdur,h5w>Eäse3£œ\[cÒÍ ˜hÛÛC0F~ß“,©Ê OvånzÐÖÙ" ƒóù5òUôï>@÷gÆôo¶oåUØœç> ƒC·w€Ñ
ÔA³ÂÈó¾[6H~ ŸUáµ4 ÂoO»XŸ3Mà <>,ô룰XâPöLm…±ƒø̬Ñ
ß"Œ}Àâ÷›Ó‰Ôë}¾²^|dƒ<64>®;óboÌ2Ͷ£×Í]é+œ]z'nÀ0
æÁ“¯|ÔìÐi¢&@V'ZrÝ<72>´ïo[Å<>ù¾—ŸeW[gþÑj³"“G<E2809C>ȼ˜Ý-gDMñémk.ƒ5Ôj“.8ì¡)®ïà|íÙ~ºs»ßó¨k`šgÆоV„Na6Øv»DÏ,F¢òQJ°0x•âºO?Óilayy¶ž3SÌ÷Êv¹Á‰ÚMükûñ©Öªc?<3F>ãuI“8Ù·-QÜ-ö</
Îí»ˆÉ 
¿Jë½ÝÛýðÂ戗Ì4Ð-OsƼI…º¥Üs®“0»÷A?€IýÛ3<>þÅÌFá~ƒgd¢ý™ypƒ…Z …Š¿åD~U
2;}³1?m}os Ómðw žÙ$+[9FBsy¯¾ï¢OS<4F>7S<37>ªéxv‡ålßÖ4加„NgÂ_“çü4úí)¼“û{ I£?”¥ l<>~/³<>×í<C397>hbá­(ôõSè>èVª±Jò!ä—ãè2ShgÂÑ&²'ùé •4#5ØþÍÅ\ækVÌ¥gÍò탷‰zù&ûOºÓ® ‹‰üë<0E>ËfS:öFÎÒ5@¡L4ŸÍŽæ ðRc®w¬Œý'†’(­žÖŽd¸T†úæ÷=Þãgˤf<C2A4>†!þÀ#?÷éδ}À€'C<>‡¼»9q¥cÏ?KÌrÚ!Sq“c·nøuûñÁ­ˆìèCÑXVž9ÙÿæÈMpŠ”ë‡|¹OÓyç¨XHØnï^Gö~³ëz2xÌ4/& Î–ïÈ€Š{­î€ ×öä§m
øƒÄt<EFBFBD>
ýõ]º+«iÊÜßç§[Ê-÷Q Gg6¸9Ô-æ¦)¿°Œ8ÿZ a¾eL¯âm÷gAøËy¾”¼#<23>Ý 1Ûqç<71>æáÆÖÊä : ŸPZtód¢‰ƒCðùm"¿Ýj©ø0J?™º7ëÞxj™N$wf|QrÖø4ñ`„$hç{(ù(d®ê ^êj¿èkË*)S±¶)Œƒ€Úå¶ÉÐË’ ·žŠ`z<>Ì63µ* Ô¯F”`X<>QEÌbO?Эí{£®%+â~vn”·'ö`9·7—*¾Öú&
ã̧˻_®±s ½ÃM¥^z…¶9“Ãñðøþˆ¨¡¯ì*œ¾Ež»!;ŠŽfÌ5MPüXñXÅSÙFdW«Ý·ÄÏÇ8 Så£ÂˆHXpûžbdVCîi˜áfinkû¼³þ;á ¨å<C2A8>ï¶Hß 4÷…±)ô<#U,Dše©ò[nÖ<6E>;Ÿ®ïï<C3AF>wÿL$«à Èï‚M8D£bhÄß
ŽºÅhØw©µK><3E>õX½¨n9Ô <0A>ý-<2D>Xm¸<6D>.…ÖR^€p-ÿæž Ño¤`Iú=*J(:ᨬ/¶ô`ž= s,ÁÚº\«â`å <01>Z¥ÃÝY!)Ñ{¸‘î*Z]<5D>œ÷uMÇÉØýŒºiÃQ'×ȼD:5&Å¢ó<C2A2>‰Œv3LC ñûþ2,#pKħ
â­=ïÁÞà÷˜çÀ(ǯ7ÚÆx«XLϳ·í‰6<>gŒ(½„'ÀCo>`¤$ž2-gÌÍ̾¡A<ZLƒLÄå6AS1$a2 Ÿ6d6°œLB<4C>ׯN$œÊ•só<73>‰ º`¦tâÅßÏW« ¿ï,} ÚiÅá}!ÿziøÅAøœ¿c ôß—@ Ow™æ*ø&ÉÃnš¶uMùê£p¿9»““®\@ã=Üà9Õ iî!? †L çásJþÚ ÀM²ÿÅ1Ë”¼¬âÅ'žX™F³>GãÞñ<C39E>›ò(ôä+nM¶ hªïnû.!"LÆí˜ohodW.o»Ô}ïnÀÜì³Bü<42>iòÜŠß_‡9Ìà° çɵq¶h2ð 'СæÌŒL=®70û¯ÖÌf4£y9'Ù÷­VÄò6Dp;À®í {w@Ľ„§,§½eÚ¯á¶@¾è2ýSƒ_¿‰Âœ<C382>Òßo@gí:C$žwX Ñ+kI´óö…gœpöªÙLÄY<C384>ðîp4hÕòvõ¹ bM¿°±²ãL½Z†û®³¤„OÔ§ÐäG|ø¶ý<C2B6>.®§ðsW,D·B¿;²ÇÕ“iÿz­ü°Â¦VüT^´íãÛÂ8d fŸl¶ExSyq ‡]Æ~¶ÀSѸúö½àêEfÙÀß…±À¢ïœeÚyÚú­SÀƒ¶ï+á#ä¡ØàºÅ–ù¯EÏŒ±¹ÿ@­•`ø_róôÄ•¯¬ro3€ð½|šÖj6ç¦ñîÜb$EÐC¼ŒX@5Xµ AALŠP¿Cc¢<An.˜òäIû‰‰8>+ ¿œ™MÊØ(~[¿|pQ|æ<1F>SBè <>Wêåf(Ö}<1D>Ñ<EFBFBD>EÖv ÇÊ<C387>Gˆ¡ýg>ñ8(fþÒ•µŒ…'G¬>‡¬¥6]Ôëýî6úZø¸<C3B8>ÎÞ ' úV†1}w?Óž<C393>÷°—ÿ')d¢Šsëòè€9<E282AC>õ¢Øj† jG}>¹lz˜øÙÈ™û´òÛ¿6ÌYÃèz¦Ìh«ZwÀ½[Òû7r<04><>QIÙ01¥
J­Ì‰kݦkx-Z‰׶IÎô6EÃÍŠ
iÛÂKº*¢Bžãôe®-Ÿ„îÆM­²HQë¶z}Ž¨mV+‰ZÜéÅf©Þ6ÛryÖ:Àò•ëêm}°mk?ÜL†”Z=×´M w;¾Ööœ¤I¼l?' l½¯<C2BD>Í”¥ ÍÉ<C38D><C389>þ ×ÈpƒßGh€¹ö†! 2i¹S݆W<1A>ÅÀþÙ »xi»1<C2BB>|P3àfÃÅâcˆœ€ŒuˆgØo!ˆ1iX ªý¡a5üžvF¶Z0gMèÃx»WÙäQ<C3A4>ª†Á¶“½ÛNô ÚpØ,áÕ:nu ÁS<53>Ìá²+<2B>-ÀW`(<28>î˜#û|1<>|Yêi»ÙŽçÔvM
Bç(;]¶þç„cÑÔ8Ž9š <f.k„äƒÂ²±ð<>$\IŒI<C592>-Qc[ÀN¨¡<>×»‡7Ü“íùa=Ú +Z—p‰ñê‚?4P¾ôIá½£™ø‚#=áµ—Ã#¥/¨‰^9)>RR™ÔðŸ3¨"~íˆ(êUüòBU|õtJÞ:…žðÕÈ)·x±NT‰
_V¡K@^yê¨jqæ¨ h àÅ'
eÃRóã<EFBFBD>Ôªuåµ:— : œ¿¶Ô²¦QöªøbV³hJñÕæÙ½ ¯ªüNbB1é±ÝÆNÑ=»ÈP†*`0sW¡hUÿ©”X!€ž“0Ö<>ÎC@©z<10>ŒŠÍé}Q£îUj(dÆÔv4Ü3wÞêPʬ"®lÆÀÀ@GBbÚXíEV™ÀÛ“Þö0_Øã'±w4*He^Ö}†Ž†r0ò ~<Æy/W& 'éBï BöÂÄ7D<37>'Ì¢ÌñÞèqáekø'ù öX™££Ž½aìâEÉ<45><EFBFBD>õ˜ËaY"÷ðæÄ“øOòUlŽdð;ìHuFÂãxr9Ró½/1?ÿà…ÿ„Äý©ö>ŸFK˲¹ù0„û`Iþ©ÏñÐ Í<>6ùÁèòXàF¸Ñ\&Œ¶ç÷¨Ñ9_€gßS·ÑqˆµŒÎ§nÞè"ž|„ç¡gEøƒÆœí%@ûè'лü2<C3BC>ª'ýÙˆ?z­ßQVŠ¡O‰ò€„•|eœ»çÇt5J'#åÄ›»¸}t
Ôç;/öÚŇÌÃØkÏ„7,ÿíiƒCÛ,h|0:;ýŠÑqEŒŽ±»Ç<15>¿ŒÎÁ$h´-+£mŸ]ÀÁÙðÁœMø) zò™AIFË·?Â<>àÞmü²ä}–¤)/iåöÐ/e.÷Á×ÒŠ˜Ü÷òÜ@j4EÅéõ¹ŠT O ÅÐU xó®ëO¿X­ÍäÞˆQ&<26>úÊÙÓn-rp$KÆ{a2ÖÀ¶ÙÚ(aíS_ƒyKM¤ôðüµïY¬ýÐ{C kÙ¼yÞå±&Œ´¹ÓCi˲Ã}y°$c¹<63>G9¬Žø8<C3B8>2;ïÏA V„!ôÞ‰b6T—Åj*þDn7þxC+QœŽËŠXï}<7D>^<5E>Yž2à ô&D©DõäÇZü}õåÚ#8¯<38>íѼZÖ1kÃbAX4ìÔú©ŽgŽ°ÎÄSûA}å; ˆÕvÌP<C38C>¯ÀÞýêǺ+?Âì1bk虈•°Ž©~ÄÛ•Çú\°C‡'+@# ¦Í»lQ k9åÛxßå±l=G|õú,ÕT b|j«/Y¬!ó]0L)`í ˆâ¶Ý°Þ :cqéöá¾FÎ_e±^"ű޻Œnv^ ŸË¢hñ˜à.(mI;iˆØ~4Üò`Í-ÙðNŠµVyþb±ö\VÉXÃA·û]À
9 î{¨­_ÂòX+¦C¤ÖGd±ÖWdY+@Suÿîœòà |8ˆVí÷ <20>µêï׊ŔQk»²¨Xá܈·žBío%¬y¢Cô£òXk.s{2°Çå°BaÓé?ЊÃíXÉÁN ëñæJe°•CXépß<70>í¾"ÖUÀú"¾†ß<39>ÓLϯeìîÙ)Kä~¿;Pĺ%SÖ™Ö'â»PË#¬ˆÓ¤«öÕkêî_*rX)êÙef±Žü6ÉâqDí?ƒuhÚ—¬ÈäHcÔÁŸ# bçÖúƒõ·_è¦ÖJªybÛž“źŒÚ%X簾)Â
Ðø²=oE,+šDáù£±ºŽåⳋÓ<¹½”Â…Â<E280A6>Áš²œV(¡=tpn™8X å¿<C3A5>åªb¹H:«çkèÎôÐ+—ÖG£«Ñ—­ÏV47±·d¸?Û‡ÂÁšz­ÕÄê<1D>ùg¾EúŽ(7røSß~môÅ6#ÉSAIû÷Fÿd½“=`'$•žR€#ž
O“£<E2809C>¨˜Gšúý<C3BA>¨¥ý>ôôXÞ¿§ˆZ3Pzš!j“j=hdäˆ'ã[DéõQWšJOD+·ÜKžb͇“huÌF…×?ÜD;ýdUz$^<5E>¿nù§ #×ÍÁfK­2Ñy5>0OÅ+>}$:3WJéi<C3A9><78>fx¢7x&ÞB…œÒë âÝþRzúC|ußí§Ñ>ÃÄ×ÏÔ©ðúg”è¿ÆýJOÓÄ 4ˆ*<ÀÍV¯Û^ *4Z½Ñoß“ÂÓٽ¶¼ÕÓ£2ÑÆ[ïóÒ·Px}bóö~ª÷òOƒýM;žØ¿+ÍG7ŒÛ#²µ¬ÀÓ±ÎÝåty*•‚¾ýʘ<þÈ? Ø>Q™oö¢Ôàˈ¯ØÓœËÞd<C39E>B Ý’;ë¤íÈnégF%vw˜óVø$³œè”øoH$RTÊ{—öì-¹b;î!4ÿ^H¸ÁoD1—u<E28094>s¹¬§ê¯µvìkßw+ØCmö[SýqÍøC¯»Õ‡ D=O<4F>ø™Ýò‡€¨<E282AC>&xÇÖìY<Œl`)š
ÀŠ5D¢2ù, ç ՟ĶDÌa Îíw ãVk ×UÆj*N<¬P­ñˆ‰b;û¢€ÍõÅÓP ë·
Ö’+*¶&÷÷f+EÇ4<>Õ/Â)ºóZœÃZZ‰(l²âX­[€F@L¾½nE¬ˆç°<02>øŸòX½Æ<>‹ˆ¼ö)bE®…"VèWL¬p4"Ĥ2ÖÈóKW+4R) Ð@;e 4Ü—£©5Çœ,~ô‰e÷˜z;$lئq] Ÿ5}íl ¦ŒGP ‰ãI¢E\<wÞòcWÁ°™E†!#g8â
k?íu¾z<C2BE>üŸnnXCƒ<43>'íð…õíØ@M,v—lÜoa¡¥ßJã2
â ÞÝ¡?pJß$Þ‹¡Î÷(½Ë"jÅ!|L±¡©Òk|½¿cÿ »yÁ<05>õ@ã<>¼·ðE—ؘ ;f¬ÓÉÂûÇù´eÄxœpÆø0'¢!Nú¬gNæï CƒˆŠz·ø&
}rœÌ–¤^,”ˆ¹¸ á:<08>þ°#D®»ìFa„Œö<$ÿ§‰ûH2sìï'­9<1C>ÅvŸq™$ñR†µø™>wQt<51>dÜq"½”9")jl/Ïó¥ÞN<C39E>¿æbZ™¿ˆ©×ÔÓA}uÒ#[P_[u¯ 8!Gznn´é%Bîíê9w" üøxˆæ„<>”0|§ERÈv<C388>xSqyöœ{ű=¢ýaɇBµ2ä+|v¤Ë—ãòË“ñŒd—òVk:ÇÌ >>Ñà\fõÁ1Ȇû>¹³%vòd.5ÖиUÐO¸î™qÉó|¿@<40>ôòM}Hh4ÇS&á ¤3øNøåÙ½T½—ò"N³•÷\ƒ6eÅ GCî_=,ñ Í8@²ÐF´"4EP¸/@ïîhŽ|¿:F-·úÄèQF¬**:8X€øääÅ“ìÞh<,ú¾Y´Mågƒ÷
³
ÿ°Ýg¢ßÇ F(–‡"Pάõ‰q<°¾9ûFLsî…aÖ_ä¬3a†Y¢‰§%ð{4-Ë¥®ôÔ¥<C394>H­<48>qui%M%kMŠMIa†KË,O±¦Ò=É“wxk)ë0ª0“Ã+·E é5jÑëadUïrØA·|Ù÷è£|·x3OÉÖ÷ɬ¦=…Ô¡LI—·ªÖ†YË#À§QÃÖ;aL<03> ÈŽ(fnN†&V0ÚÃEç´©å·{=¢i s³¾_<C2BE>hév2ÑØH³Ù'%Þ­/Ûí+Ê#©å,˜Í¬è÷wV+5[W^g—áîNY]¦áÖ·¬´4]kyáwÚ¦03LñßÆ«µ¤íê¸TF£(!N˜ª<CB9C>¬W#ô„s<Ô;£%4{‚¶ðºßÅdQ¨' CKüÇد¼ÎÚ'iõ'ç<¢­<51>BáqÄF|öì–=ú©y0,èÇ
ˆ-/zc<E(-”ŸŠÄ8…šÝõioÖ+
$é<Gk¯rQÀ(‚ŸŠ~Q >½Ã"7Ât´É‰„ײ¤œØ
Š@̼)k~ïµgŠW!=÷aV§¾©‚ë<¢Òç¯"G°AHýôR]ìrôBPPú˜^'.v½Ä+ÝÍ©5Ñb_?Š»‚§òßÆL&]ÁÕ ÃúQÅ<äâ™<C3A2>Çìæe"
Zí@.RÒ1¯åˆÊúÑŸê´Ÿ‡„<E280A1>f,Äý%. „°Rà él/TË⸞hªb¯;H$Ò7<C392>6ÁÓÂ%
±<EFBFBD>GIlG…¡Íò! ±™<C2B1>Š5ö3ÛŸz½3JèÏgBªZÚÇó°«žc<>˜µR¯^õ°¡ÔJVÒ9í©Õa%ïªb½xæºI½>Ø.aw†¡wU©BÔÆÆÙ=öb+BlHÂhô é.p¦(À9­Ï€0ÍûÄ%*+(?3H¬ù4ôBÃYº2ÊÏP-ئ.ø”¿Y×Ú£ÆVŸrø¡Û:swW©±Æw£ôh@•ð„ÆGiÕ×<C395>Ž(-€¦¾
õIh¸x¼—†ñáÌÙ¬(
;…Ú€ì'uG~‡r\e\¸es —Žî° 0êºAs_ Vy¢q±ÍvìvvÛoO`Â@“xfg«Ø1Ñ®Î<E28098>BEjNî}zv²¸(‡Ä<E280A1>—“nÀµ Ú$Ò üÓaÞ™ÑÆŠ†yÑ}½Xº¡¹çIö½št dqê€vtS0 `@Û±€ûúökH<6B>ã„
eNÓ¤Gœ¨Caò¡ãz$‰vw<Š¾?rØ·ŽŽ—Ž"a2“æPÛSbw á<1F>`¸ˆ£ŒÊã– tŠìÒœ1ðžÉЕѕÏ*ŠÙ:-¶nG<6E>¸¼ö<{O¸B¬SKèvô&TÈIA
”z;u§[‡™<E280A1>fNœ0¥,l4)þŠP$Ú“tiV…w»Õ7ò5éÍiâ3S×#v^<5E>[PŽx€ØtqÉ!Õ<>CSM‡Ï£á{3Na÷íÖÿ§r„`ªëÖ<C3AB>šªõ/§•"·Ú™Ö?>¯^Sð*jmhzºŽZ€.×FLñÅÓÅÉI¨;Ê‘=Q¶<51>6 ¯¢~TWŽrÂÆkr<6B>®U”cÌŠ²»¥ú1f=ÑÉPH4Ì<34>ž|ìù¸€Fj<6A>Ú¡(<28>bVe—Q-OéÊTÚ¾ęF°Ìvò:WìZäêá5É+&ZH}6Å»¸ª.˜ÐŽûSnw
»ª1oåŒ=¥>al!Žuj¯397Ηíö\5%IÆЇVtãzjj
O<EFBFBD>ƒ2M&;NJlw.<2E>´ÈÄ1­á<éÍÆ jssÒö„¦ÁË'S|Ùžéþ»Cšèˆ3tæôBSXö#Z.ÓCƒs†fþ%ê“r4YºÈ¸¹DŒðå1‡ÇÛ²\ÍΪÛKÖç¥Ug8È'‡B1zîÛÉFÕò4×)çs¨ÖòÝ`EK•óáZǵ|7ø1—”ó©×òÝ¥—•óaXejùðDåÊùÔkùnðÒÅKÊùÔkùÄÔå|êµ|7xéâ%å|êµ|7âÒÅóËùÔkùXµvy9Ÿz-bèk”ó©×ò±£9­œOœ­R€ÇÇø-<-ƒO¹ìWGŸôˆ´rÅŸ¶ê;<E280BA>—îœ<>ƾKÄ ËIùë*;…yq@ê RqõmGe>Ç©l"§L­P5åOv§PšV—Â<08>*VtñéáQ ëÂÔ W™>aùi*ÝÒ
t©ôi'±¡O­ß;AìôáÁfA÷ë{õüÖñÐbR¸dƒOd@¡ñ]Þƒû4K'ŸÎ£Yvwj~‰\\Ý<E28093>i>­Ó<C2AD>Ò*»ÓŸ_¢,:mdRLÎòRCë-Ó“à•ù¶³E˜o4
1õɯï LÑ]÷:4 LÚ“![ôz$¡‹ê•'Ú`ØÒ§ ETmâX#4<34>âˆJ ¦ I¹ FNYÕáåqºêÑ"{-㔚Âþᤂ•šBA&W3º°—r<E28094>©X„#5³µ2ïKj™÷õ&‘֘ö¦P+ó^M¡lRú ^BM¡$ê©ÅL…24<32>
½c<éŽòüÉÃÔ>ÆàhÊ{çM£þçD¢ù/<2F>3‡eŠ¨˜,<2C>ò]fnY*)Z–½¾Ò=¹¡Í<>?ˆÖ¹WSVÏÊrþ \έpŸÒ(>ìj¾'<27>`ÜŸC I¸H\\~º•È¢U²§0K¸q[>ÚXQqÔËþd×­D
hRDw B1ª®U°w¤ïì8¤€dHÇYªbL­„Àš§¤•º_ôl/Gõ¬ÒKla*04P¦'8l¸9¨Q«§!€”˲Ž’“ζÓ~*Šk_Xó¼Ð$•îu+—â„qÚOå´X<C2B4>JxðÁ‘ï©“´kôD}ºá9•íÖIÑ•>¡$ëp•Zôæ†?qL_·NÞHÝ(qÜøa/<2F>ÞÀ²¨K¢7§­/ŽÞøocÖ[9OO$ltz<15>§Go”ãëÇ£7`p«Jʈþz8<7A>ѵs P=Ü¥ÑX çÇ<C387>çæ%=êˆÞ°ëF³VP9z£«:ˆ·l <20>N*RM¼ðH é®ÒK£4V—!]U̹¸9¥B2ö¢1ŠžÃqF—?õÖQ̪ãسª<C2B3>¥«˜A¬kp6½\ŠâiòŒZÕ™ ¡UÆ“!ıÎ3Kü´%.ã{j”øé©hU]ÁL!¦JœNOÚ F ¥°©„¡•V²$LtCü¦Ì˜öÖÅÙ•yo<>+λvežzÚõÕ*ó49í:•yLtð¨8ït@ê•y'$Ã^R™‡ÛÐXqÞµ+óŽ<C3B3>B½‰'Uæ©¥ZqFè*ónð <0B>dÖã•*óØÌ{iqÞµ+óææÚ•yÞšÒ¾P©³½Fá?TyùëºS/(¯N«SGê%€æ»†4oéØô5 ð„Ë×ÊQò›Å<08>ØÅÖìŽl̺ø…]Œ¸öÄvyNJ±V<àX6ó^WŠ5°Ò“véz,|æÔiÈæêX<C3AA>½»åc<C3A5>SÖ/úS¡-¿Ò¤C¦)¦B[~u¸ª˜ê9¹l‰“Ù­s¥£Ž ­³pô¸¸ЉëQ^­u®²;ŠÊ<11>Ùè4ü´ÑIC:$1áQÂüíd·G#®ƒj*žÏ¬—†oW-}»jìÛ•Šd?¯P$ë5¹®S$ ]§Hº¼HB¹J,,£Ó>èZš;¨XFŠÊQ°ºò—Äé<PV¯ÇuqŒ6X]ÞŹBrEy"µ¦'WH<L½Þ=67ŠþŠò°åÉÖåý+Eyçûž'å)ï¯Z”Ç&Yh”k^\”wƒN†í«ë<16>܈V”QŒqÉ}ÕW✠íÓïoøÛãd&YzpòÙG±Dû•ÝB:9ð ;¶—µûαlzÒ#”µw&nKåa9<61>Ž= µ“‡¡`÷ x£T«]u-é“ä<s1[ài×êé®5ŸEg<45>È Ä/”·6oÃFçü§o“‡•Åðâõ<C3A2>Ñ^<5E>{<7B>Î|ó ^¼Þ‚¯·<C2AF>öv&?5`ÓœÑU Owf•ÓÃv‰wšO‰¯Ðc*ÂpßSTv×ð8¥Ep´åv_1+.»³ŽnKÅb¿€íS­ì®ïRÄŠn"WÀ2£Ú¬åb¿¾ZÙ<5A>1¨ŒÞDÎcõËÜS¨\—0~©\+÷nbX%WèÁÛ¹oýs{ú;S*Eë©•ÝÍ Öñ=…_eåb¿»¥§3RÂ:TÁZ"bb«SZÛ8{4+b5~šC¯Jû™Š•{ÉÔÂuëBøÑ'®(ð0ÑÕ®n&%ÄJMMõ£<0E>Ôa°ÄO‡£LT!žÆ-bðºÓ*ѱj1gíüÞcã¶q¿æçKã°gÝ0wzíãTòå;½Ò²gQ+gt)i^Qs¼&¼ìz=qŸÒÿk]¯'7}zÃv éâL,@ü×¾mE”;¨r·ôZ=nÖSá<>ö]xZ­èá<>ÜipgWÅêºcE/á5®Ÿ8aÝÓZàS©ð8©ò­|SxµÂ>9ïãÔ˜<C394>ŽÂ>9/A>lwQaŸ\UŸ¢ }~aŸ\ _³°O®ª­›ëöɱ¿·v½Â>åp÷U ûäÒS0 }­Â>ÍLÈëöÉíó(ìâ^RØ'žW¦ªOecåÜÂ>…]Ükö©T¬\³°Oï]öaùþ|UŸüöêE…}rʉ‰@]µ°O®OøÖ÷•
ûäªú$]×(ì“C~Ý\¯°O·Y|ÅÂ>¹ª>¹Š• ûÎ&Úi…}ZD»RaŸ\Uß©DÓQØ'WÕw£PéuAaŸœMz#­ú¾¼°ONö`JúZ…}r-ŒïyÕÂ>1€{M÷ÌÂ>¹¹VÞX9»°Ÿ%ngFMßœYاà{*RäÌÂ>Œ¼<>y£\PvnaŸÜ<C5B8>n°““”ìUØ­ }D&_ FYóšv‡Z¡׉Ÿ
òq7êüê+M¹!19®qŸÉq¥[ü2WøÉšúH¥y0ƪLRù²<ÍR}}|Y€Ôº³×®oúæÊêW>Á_íÊ=­»zeûtœ2»¥gaëêç…ê6*¤
<EFBFBD> l]&ÿmté»L<C2BB>R—é8±I3Sð=%÷ÿ<C3B7>Y<'„™óltÚë:/ÿã8M}
.¾ü<C2BE>‰rhÝÿ§³¦ïG„_tùf§©Üÿ§¿TJáò¿Sƒ<53>g^þ'„”Þÿwb½Ôñå7GžËÝÿwF PØ5ö\<5C>‡bR®JñÜõj<vÕëœIŽæ&örq5QU=¿CH¸Ô¬é“Ëp<5»NüÅ·U3<><33>`4¯íSÎ Ñ]‡nìÓH Ó—¤©ÖÇЊ÷<C5A0>‰öOlÖcwŠ4ŸÈ[;iÃFõu×—©¥Äƒ®Ôc駤Z!<21>à:©VèlË9 Ð\O•žBÌ+ì¤ ("SVŽt:E*'ŽA@g-Å#(Àϯ›“+íqhʇ$ê¹¼^ZíûCZ<43>ª}HíÝ(ýh?¤®ûeì>™$rÍ®K°aÊLš­ ¢¦Ž¹Á<C2B9>MµÍ)`ŧlw˜ã|Õ÷=u sGù(gƼ^õrÇ×ë]îø*)¡8W
t¶'÷
nº‹îC€¢ž>z
 +:ï†o±©d#<23> ŠL Å%›ÅÀ&;^<5E>à7™Šs÷gÜû'_ýwæzÄ@]ñ®o­{ÿtšê—ÞûÇ™êŠWÿ<57>Xú£pïß©.î™÷þɨ5ñÕ—Œë(fsÚe}RhÚ÷þIĪվgÝû§ûøöÒçïÅõe<C3B5>€Õ§qò†þj_¯IÖµäú´j_¯é´ûååƒ)<29>椎ºÍBL®ô^ðΤ+ø¥‘Ñ…]Ví 0Úóòjß7Õ³íÒH{žR}”µ£ëAe#­¥:ÎÚ<C38E>ed¯êM6 &—gF¯²O,¤ê¢½*eµ&_K¥V-7RŽö±œ¦ÛÁÐæ:r¥Þ½ÂòЖ:§ŽBª.rñ¯ä{(u?ÿÈ÷TPR1«[%cRÓb”†@·Tk©NµGÌÆ­ øùÇÏÁB¹ v¼x÷ŒºÛœ[9•O
73ïU*Ó.9Uë˜hW» sD [¹Š»Qz-›œ;¬C<C2AC>Þè¼@°;ÐQªânº§ïj—a>©ž2¢»îVXÆÉàÓF!; 5š¢|{XÅ >F{°Ô° ÿ¤<C3BF>Ž±ûæP<C3A6>¡Û_ÞòÓw/!ûIT#GÑÞ;JÀŠZ|5<>ÉÊWæ%L÷Š•yÔáÛ­\Ñ$|3éM|áj:™[ùjµwµ*įĿ‘"NŸ[ŠXï½<C3AF>ƒ±Ö‰b<E280B0>@CGž³-l¸â9Ú<'•*ó"åäïíš«¤2φÕÞð·ÇaD~ð)b%ŠQâYkÈ|Û\ûúrX8\•òGS(c-9¿;ŠX-¤5<Ç]Ò’@·ÖšE+E¿&<26>rX¦1ýåj¦ŒrøÑ'v2l¹÷ÉZO»þa½ …¦!óo¿Ð­k ÎYîc',ÑùÌHÌQ,ɺ>ŠÀ”²Îâ5ŸŽìLÖa—ÙQúÈ_åÚ…<tÆŠM/­l$•[ùDæ°ìµnz»¥7áóx¯L¤ÀäÕÏÒkç%AàitÇ<74>>òš7À` —ê¤:%qK«<ήè€<C3A8>V§” z#9ƒXW<>˜¦œ5G¨™¯·ØäêëF k§éëò=§®¸r<>ã<\±.s¸'sÚ5<<3C>ú9»z#·=çþX˜˜h…ÏŽzŠ<7A>ÞMðÂu¶‰z.óâi…kB ë¹e|£t°™¾D½AP6¤ªX¨q©¾zDýukZ^MA5L_<MœŠâ=Þù~ÕG­ Wô¤G~“žÉÐQ¥?4=arë8<C3AB>çăº½Tþç97L῾Ì2²“Þ«#<23>I*)7̱9šuvÀ¤Sк
Ú° ÉIIݤ;¡ -r<>¨“K*½FG1á5Ft*×Ù)Å ÃTPGQÒSë±9”ìJÊž/ »¶TóÖŸ£9TÌZF(ÍR!ÝAÙAS,¡½áï[;ÚÉŒ¯F4Íj¢“ˆv­óQгˆ¦RwTÅ,Þ,>³$Q¯Ùˆ×ßœQ(^žÊõˆ72ÇœP(î„r=â<>ä>éKõÖ#rŠ@ÑGRqÚEƒr{k'”$*ˆ½£zD9‡ý„DmU£”ýpRI¢I<>ê%Ô©%‰ºÜCéöªxH'U.iÔ#*{ÒõWùq<C3B9>uY!.þÅË
q7ꨶñz—j…®tY¡ ´åÇJeÚ¿tY!R=÷b@½—òa»÷²BÄi°[ÿîe…xæý¿xY¡|øv”½vÙÕLw´NµÂo;äÓà.½ð<C2BD>cUùÛOͶS¼ððœS­Î¸ðPqp×8ÕJ¸ðð¢ªÞ\áT+tááNµÒs᡾zÏ/<$ô<>cè³.<LªÞvˆ9—]xxbAÙ¹JÙB|ÛáQ̹ªîFõ~Ï.<T¿íP%ÖyÚ…‡êCbLõ+\x¨™ {<7B> Õ/ceÚåª/cÞT—@<40>z᡺æ“Úig_x(J:ºíPÖ¿9çÂCõ¼,,íú² u”\ãÂCõÛy}sé…‡'ÞSxî…‡b(ÒÛ<0E>Äg^x¨œ®Æ¢¹Î…‡êû1¬L»üÂCÕê&WýZÕjÊ·âÁEªïó [ધx+FnO½ðP© ߊ¸°ä#") QrAª]xx¸äÂCžÝdo;<ŠÙœ{á¡:”Å{
//¹Àn;¼¬”äUÑC—ì6ëKíÕqá¡zí ¾¿Ï»ðPýäÁñ¸ðÂC¾ðK¶bû/ZÒ_u|á¡™v<E284A2> Õo;¼ÜTg/<¼B9¶ž õ¸¸W¸ð<C2B8>ƒ"ïq©µS/<Ô¼¢PEØœqá¡Z¡Çç/ë{^~á¡,“ñ·^&ÓÞô?ø¾çEª?XöÃe
—+ >5:¨xáá鵸g]x( Åê+'\xx~F—Ì…‡çêß°_x(Z<>2h®pá!WU'Û!+Ó.¿ðP=‰Š1n¯p᡺wÏíj…Wò·žëMzááù¾çI*ÉCæ¶Ãk¥(æÔo;¼‘­ö…‡ê·²&ÇåªéÊ+é3.<T¿íPËNÓ_¤«zÛ!„¼èÂÃs,3.<TœM´$ÞúVƒ¦qá¡®¼ÎË/<T×(¤*UäþØv¿)ë
VM¥‘ðá`β!¦¯1<C2AF>4" ~S÷êùþâ"J¦”ãª<C3A3>&¨‘€hŸÖ±Á‰€êø:¬¡i“cI
@IdŽ¢ÍEÚ³O<f¼·Ï4åQXÕUv´CÊx÷é¸7¨ÑÒ/Ï<>Œ#žü<C5BE>:ñfÏÑ^,·D¡ðã!
Q¬¾d "(n5¢ôuµÊ󀨭§3¢¾"çDëÉ%Ú•E‡x¥çS¢CìWD§ÿp Þ\+ñî|µ_#S<>è÷»SbÐôï‰o×B|?Y_(Š*x(ús§ÞM ¡ƒˆ•¶„ï¡<C3AF>ÐÜÃjSk1)×ËÏÑiºóõ>3Úïïº<C3AF>ÛH|•½k´J<C2B4>÷óå<C3B3>Éõ<[Í«ñ]%½¯<C2BD>~ºù„<C3B9>+J4œ»Àó7šTÐ3ÅvûŽ¸%'àçÆVVœ°sƒJ^i¸ÖŒ®l/€]<5D>ÉÞ°ËJè…ˆ‰Vz‰E¿Äw¢o£¨g—CîÑX¶®#á·¤‰b¶š%ŠÓñ#Qª?¯is?4Å´Faò·àxˆ<žâÀxÃTH¾…Nx€î?$<uÒ)1ÂÄËHt•á·u-Di±Á ÚH°˜úß{£-ñ0º<,Î<11>ÑiK§áo<C3A1>éÖ)£+ãŸ<18>OݲÑþkcþFXÖfîõ9 ½õÚsÞ8@“[ =^8´A:^3RhŠßS|jí¼ÎÙ, >u~ûOx&7zÝ]ï Âêö ¯PBZÙO?¤¼Q¶1§_³nÊö—˜Õ¿ºØ¯µ ȃ}a¼ü<C2BC>ž`…Ȭ·5:S}{ûò`eû™ Û„h ܃ˆ{6¼Í=pÏr.á<>/ûþšâT<Â`“Í2܃º—Гv·"<¥;ú ¢<>?ãØK9§ÐG]ª¸Í<>v Û><00>Ùæû¢àAÓ¾úS¯Fðµ¶óÈvD,Ð(:a+;0ÁÉ,,P.F€l­E€pªƒùmÔ=0"íDº|}EpíÌy~ÏSkéï¶ýÍ 3|êÜkÂÓ«¹p¢}sh¾Áì‡Þ]yÏ0qm,¯ÅD!ðƒÉOFºº/¸w˹¸œ6®E9ùùA,p <0A>œˆE]´=F^¢R1m!ÛùÊäÁÈùž`h]/ÇÆmŸ#ï¼;­åJÅg[ Ì,“õ†~èCœÉFu¤’Ð5¼__=,÷<>º„wT«ØÐÜŒ>¼ŒEìõ}ÉhÅ·Ÿ†~îÓ$€`°ó»l"<22>.¤ÑÁ×®‡}wùApŸúÏ<>€F­%…幜ø±g}£PØ>ª¾ï§ß;v4Ëu<36>Äɾmš‡Šõì*bŽ`a<>dñ°BŠ˜{d+ó…‰r>³ïºy¬ceö}Â[ò¦­àÓÐË}šø„v@g7Á÷?IwàhæÁ}§h·%ãáŽËôTü~º‹"Eë¿Ðaâ±ä‚ùú
9
<EFBFBD>n9Síf <0A>ε˜$?MÊB÷4Û¿Š¨VºðBæ<02><>o]£íùÝÊ
ÑC ÝÊ\5Õ6Ú«¤Ñµyù„5*w´¿ãáyè1,žŒ­=Öyò“m2ü…¯G2 ½¬nðu°ç´l<C2B4>鶿Pt:XÑ ïr‹ÎÞÖ <0A>[|–´ñ Í>4)<29>èÀ!à™&*žEwïf_ <67> i Ó
<EFBFBD>raªó\=føì  SG¢- e¹NW<4E>H~C Ž¦êq$Ã<>rî{™™ˆDl<44> ‚£* bT½p4üRô²¿!0A³:xölçGhcªß‚•ù ÞüƈS$D<>÷“]™6EoÙUôB^24Ë©&z/vIàñ/m0̤)ƒïÈÚKͱ¯ÆS4S…`®:ø7Í~?›ŸÑ/±ûm>7“ÎJá-K6ÓÓ¶÷<õµŠQs0YŒÞ6'¹¯Gs9~˜ƒß :`<60>ÛKœiµL >ðÜÂÉu3ÁûûµHòÝÞ¦c…ßϘ?—dÚ¹þg¡ÜXxÓ¿O&_Y/èb~åͲ&D ¿àÌ;ú™=d3HX¬7 Ì,âc¬fºàX¼Ï3íÎ'£Ô2Ó:çáßr³V-îÂûÄ4GË  ‰˜…w·ýÊAÃ#:Œ^}ÌǨß|y·;ñV쿼éø㲩>`&-Œ9jr$÷F‰£ˆ óÁe÷¸fÏ£(
ò+dŽÕš€àä"»´Z_Ô<00>êT<y&êxõ6sÕó¥ÒÃ2ÓÎûO&¼Ï—íy+:¨~MNÛ[Ü<>rÖ"ö[ÑI6Pt²<Wø,þòÆpÜŸjïÓ¹EñžJw,Ýßô¶LõŠgÙœ©Ï5@„Àm~úB7Ñ¢¤<C2A2>_<1¹ÛäÁòŒ½s}8‰æz Ð`4·Ïè;äûøòãzJnôg
½£¿€Ýš“F/7t†2æù@Y;Þ[S«™šH¸ö‰üaXª™Úªg;UÍ”ƒF
HÒ|+[ð•ÓÑBdì,PýX?þ^Û$K«N÷š´‡ÞÍÑ E¶ÃeË^ÂøŒSx¼ì/8»\.»´l>óî°…F“Y@粿DÎA¢i<C2A2>ùb9ÇX6ZcV0`·Ý-ZP(Ô’|Ø r¨™àýµfš‰<C5A1>l\•dÍ!µv¡”×±æ šK¥¼µ~§ šýtÔŒSx}SF<53>Z»È”¥5»õ­MîS|ÌnŒ¾Ñ5ðKF Ãv2KìBN;²âš³E
´ Ýj¡Ä}:yQ8àÉ¡3ó²F§¯øÌ|µÙ³>£ë³Ð‚^` :€UðÀEÁZ‰±<E280B0>&N£+âê<C3AA>B6N1ž!ïM NcÃlæ„2ö4²§ Ò¸˜¾¶ÛAÐùM¶ÞHóÁàw¦ÿ±æ+¡G0#VS:*ºù@ò¯0 žùÝcÃ|¨tàúQwÈt§úPóÐ6¾Cõ¹œy…“$6Æûù\¬¾ñÀžŽÎywóÊM6#½– Ä*±,N6ªP0å“R§tGR@Ÿ>×mTÉœÙú>æzG}¡Z;É€RЬ—áQ·W1*ÔQÃ<1A>“݈Ӆíuv£½¿ÚñÎU ýùnÔI<<3C>ô<EFBFBD>´ gj9•X<E280A2>œÃîÛßã+횈cF=üè2™ð¢3ßôA%Ë»5<C2BB>ôÍ*P4:“Gø atþîÞ˜mÇ‹)5JÝhûžFØpä`âgÎwý·ô͹¢È^ÆT¿¾O«Sß\Šº0ü.,Ó_Ï7Àß„Gè<47> m·Îÿ»úæTUÇØZcV06<>Û¼zà†Ë~¸Šø†DÕqœv<C593>¢ÉOïwfh\<5C>ƹ¡sùrÆÕE•xA8e<38><65>Éél&äùa3}¨esÂ:;s±ip×,j2í“RÇ€qNÓ³ÎÎ5@sÒ:;sè8§éYg:ÜÛëü
A!:Èe¿¢7sY7á+¼e{ÏçŽHgÆ/ÏÅ|Ê|Ì8·1Kþ½¸ÿÊ”CßÏéi{/P_£z<åíDòæÉ¡<C389> þb;î ÁBhŠã…— ­=€æÚ<C3A6>z9CzÒWŽh覜¸9#Z™µœÄý÷=ida³ÞÚ5#rCÇÓyþ'‡s²š3×FnÏÞ28¶µÏˆ§<CB86>ÊãQÄËwöqWÃÓøW#·Š.ž8žæ½ žf½ýpÃØY~mœäËÈ-wNIKW¼×Bîsóð©s±ÿ:ØtÄàܱ¼˜”&1ÏRÕ»âRMûg¦ýúÓ×%çÐ'>w0V`[¤ò˜¤Õ¦8X®ìÅÌjø£Ï<C2A3> G6¦ÈZÁENOå95·0~Ø4ƒ)§3œ?¹ì÷•8Ê´ðe»Ã½ Iíý7gŸÛ<C5B8>ü—gM=¼ÿ×fM=@ó/Ïþ ÛD—Ì>šz>£ë_ýSv
/˜}4õŒ°ù7gM8
ÿ«³¯Ï¸U<C2B8>}ÆðÚŽ“³p ͤŽ³D³ŠÆàÞâ …ê¸ùE5GõuØ<75>ëU˜”ô4¦¸ïƒÛ"† ýfÊeLü^UGtKÀ&€ÚóÞ¹(H`8r·©` ú¬Ñž_úÍdJ¦Jœ»µãã‡o*ï(ÏÆôH`t»<74> caÅŠ* ‚¥`e2û
Ÿq—ÿ>þážYÞé-õÝL%#ú ÌaÇ<61>Ô=€ÙvŸ4—8ØK=(4Bxñ¿Ñ—ÀŽø¸þ3éM<E28099>„ÜàtOpnO¿Ù‡ØHüê¶YS_ «<C2A0>ó·cÃÍÍÁ(äöôÇ=·<m]øÀÌí°;â«×gœ¡zÎ K´¬­êànî)¹M?¯h}1×^Áüy¾;+<2B>ôÌE­ òt ¦0tøÄépø¨ótèŠéàà§vnÿ ü#Ž1/F„Üû¤ÏÁífÛ…)M:Øo˜î¤r¿øªbÎ<…D`±²×´hÓÁ]z¬þp}ÉÁwðê`64äX¼kÜz
¨lïC ƒutÛüT`#ä]Âø-á&ŽÓTaØÔah°s!Õ ¡´,µ•as^Ú —xyžíc h7¨zU†çÒ<C3A7>"ÑykyOâMèÁpÚN_çbv Ɔ> Æ<>Ó©CïŒ8]r¢‰8[ÇŒøÓ/V+ `˜ªKƒ<4B>ÎKYËI(à\\m^ÔT\éŒeãôq0Bfa<66>ѦR5'Âå± ín7þxƒ»†oí·“r‰§©<C2A7>Ës†ršGQN`w<>,— ‡]÷Ú÷œË%§yˆK¢,€tsšÇwI'Nóøµ—]9ðð”s00¨dÐ/ê„OCqúF<C3BA>>ç¥<C3A7>p‰•ô93âsë` €FmF|zL9!Æ .WÒ>¯ø«Xøü‚Ç$'¥iÂ|pJŠ¥íTì½@õÝï™r蹜ΠKŠ†˜Òkþ
L..ófe|Q™O¡K°•FurÃÂu—.ß <q¢Â+T”9*Às#×ÆŸz51ÕT\<5C><04>ÿ=ØÐW`ʦ·àkÌÁ…†Ø¶`ÚÅ|ý¢ITdñß&ßlEV?=ãêh«XW½NbóÍ?°ãrw#ô€á´ªöê<C3B6>ð/¹ñ³‡)ÿ€À¸n+sîÁ3S<33>ë-ÕLCþ7¼ÇJ°úî1ÿÌ…?ØÅ Ë<{¸Z¶gE¼åP òË3³ñŽúw<ðÓd´ó²ëæÅÅ;/·Œ~cÂ
Þq(½È«ðŽËuô•…;~ÿð²E—½¢ƒ/aejÎ|¶Pl­íÆ·àW­»3jë§ãDçVðíù(G“9HrD€âQ„Â…t"ðp•ÇL'¯q*”| <gÂ_+±½·Œ\!ꇯ,þÊg;L}ß0ÞðåËP°¼1 íË¿W¼ §çÇõ ûiõêc?>úL-FÁ6Ü°Ðú
¾¬·| 20ïiòÝ ï Ûú«Á¼>84Yà߶.Ï_ïxYíw|òÃ? úMv4#l4ßoÆÊjfWî™=ÓOkùjÅØb1°bÚŠŽáÏòºÇá¶'²þ»C…„å
,â/³ºï}¯¦Jý>þd?å‡ ¾<>Ÿm×Yg¤Ý©çÚ_ÅüÊ4Î4ÛŸÂÈ•~ƒâ)ÁD9æ\Ùî~bBL<­8Š™ž{6ŽÂ,ž§ ûZ<C3BB>´3„´<´ì¨>ä"ñc<>·ïéO½†ŒL!‡8-e+SJÒK¨ÖÊv¶|Þkòøò®¶|E© Î~˹ái ]žUmèhäIùÊ‚«Ä†çŒq‡<71>ždÀ
+/ÝÄÄj<C384>qž14+Å“Ôc¯³æ€YÕÅ<C395>
,Oþ1±%@û¢¯¹<C2AF>.WL§DtÁÄ( ¿ÜìKi\OpF¬„.]ÌÜŒVB—j®äJ-ˆH-¸ º\,/eþ0[ y}Oó°"©:µ˜-6çÍñ›?<™Š×ûº™lI¶É¿÷ùíø°&7{CÌàÉ´r•J$˜'ÇÛ _€sþæeŸå6c?BE”Jˆø0ŸFK˲¹ù0ÌO‰^RšhhÙµas™Ýƒ»ƒ¹ƒp®e´i'üÚcb;LøTr ³ÈP4×nΓTöPp<ÕÞ$KÑïvéæ bV=K *­ü{>5<>§¼wÖ¶¾oå¾*«|áãÛ1DEÂÇÇÀÔ¢JÆJ³áh$ÕÙV×Ðw
áŸ8<Ø¢atyîû2ÕÙw`!AîÓŸŽe@Ò
7¼]2BÉòÐcf¶;@ÝÅ®–à' ¿ò:Öi³Ÿ¬L!=XŒvñbDkÓɧ/Lsn´xÝü¸ýííÓŸêì²Ü2
/Äë +ÃÆì<C386>d¢Á¶€GpÏðEæû:ôÓB96¶ú0ü¥²{l8j°ÜÄ'^`ØÁ²Hn“d#çJuŠ3{
EéÕ‡
ãñCÀê†çV°•å<E280A2>ŠKðƒàAJÌiÔÜÌé–‡.°'M®t/ø3<C3B8> \°n6̹$>Û­ÛÆ4ü Ž^ÝÍ¡9í i ½pJB¯éƒ“1ÎùJ`;N¯±\æõ>þ<>‡«»L;³ycŽÂÈ~Øû(ó Y
ã.¯àÚ~žÉ¾ñI՜؉Ì<>£¦9ì¢ð™·1Ÿ†¦s ¦PÄ»¿ŒR¡Ñqè;¿£Y­Ö4Z¾ºa¸Û f.bAø³\øá(Ë¡Úÿ7‰³§ÌÞƒühF\<5C>þI'”ðJŒSqXN{-<2D>3åþüÐóŒ<C3B3>ùTø|t0Ÿ°žïg~ô›/Ûs$Ý ½¯Ë«üôŽ†~P:‘ꘓ,€Ý ­Ò•5Hw÷Èþ Ì~J &~X”Ïi‡%x!šb»pþîêò‡ñÇÉØ1\ü@¯)s¦Ø&úM™3íÄÐ'˜2gÚ1ÜÞµ^SæL;† ˆ©A…˜£óJ}øÄ ,ˆüöÏ?(Ëm+Þžó/ܶ"å<>lMRT"ÕDåÓ阳%/<2F>æžÉ•¢_evQÍø6ì»ïQ²…Jõ¿ ûr à`ïËø ´,Èl@º0Þ³/Y°ì€ŸÜüoþ707Î<òÆJ1au¿§ÎŸyAÒÌe_ØÉd‰øºÉï^ö˜ÔnãNº{ WÜ%»—èŒuŽh'oÜA:H7%ÔãhL<68>_J üòtèãt@›Ê,Þq"€îàD€g숉À14N‡aâY ƒÿcå<63>`‰`yºOßÊæ3XôìG¨1Cú ¸…Ëf³b»ÙòÌPj õ©ÄPʘD™ÖæèŒXuLºœ:ŒÞNÇÊPÀ<>X~É@êËžæyÙ@ÐÙ<C390>¬è<w seÖÒ%QZõÂPT<1D>¯ú,ÑN]ç<>Þö$AGsãsw™°i ¤ÒŠ'šîiŒ(ùNàÞ%‡AÒå|$ó½ÃÑèÈòpæ"»áîbI6¶Fmb¨­ÅehzkQ~ê<>%Dë}òÔÃúK¿Ê¢#s{\Ò•pš4±hô{™êR.8M/·öæÚ\rÔ )ѲŒrÂ@69j"NÓ=ê\$pZï`ºlí¿Í:”´<E2809D>þ`-<2D>yK”À8ºH!%="µˆ¡Ñ‰¹‚â:Áê5bŒôel1Úì%}¦ýê@òJzÇz­Ð|œÆ¬Q,riý°`Àµ ª|àò»ËÂvZ:y!!8ÁyŠQÊë„(µ(åuB”hnÔ¢”× Q"4jQÊë„(Ñn¯Z”ò:!ÊîÌU¥(åuB”\\@1Jy<4A>%@£¥¼Nˆmø«E)¯¢D9†jQÊë„(9) ¥¼NˆºÔ¢”:C”Yá j6#ƒ‘<16>@ GmÌQ¸CÓ#š%תó 7uÝPjY1©ÅË679èµ¢ëÁŸ þa;“Œ14µ˜]ˆÈà_îœð´ Õ,ÀSÄßIg‡âδ '˜hêô5ö!{?Ë´ŸþÄ6ð¹]üœ×i™{Ød žsÝÔ¤ªƒï4L´«ìÐñâ(l—ý’ :ûaKÐàOƕ츰[L(Âl³{K¯Ö2.{ÎniõÏn6xæ"@!¡ˆ`.ì¸|;6|¸{ ÷9á©ïp7sPØLð<4C>Lðóø©Eî;Ô$8ȳŦ6ü‡¤nþð˜ÿðüŽ¼¾ˆÁ /Aøkmt󇵶<C2B5>mm†Ä9ðd¨}~1Þ/¶›!õ<>!†~ë>Õ^+yCÌÀ6<C380>¬ ´¿ÛÐöéöqpóaÈÀ?Ý¿àß&üC°øá]ôÐËþÖýöñ|ú1n_Ðð—ÁKž _„arÞ(áŽúÂaƒ?t ë›?a¿Ûòr¿Ônþh)u<>í#ðÿÎÝ ˆ)f˜ÁÌ06Õ34´4„Šc†‰<E280A0>ž9ÈVSK¨ƦÆz&0"Ì011Ó3‡‡!Ô D’ê#˜;,-Eˆ0ÃÇ šâ€ œÞtA¥°j@bzjHQbf(¦'–¥*$æåå—$–¤¥ÒRRŠ3òËA" Mp ÀäìïÆËM˜Ù
endstream endobj 5 0 obj <</Intent 14 0 R/Name(Logo)/Type/OCG/Usage 15 0 R>> endobj 14 0 obj [/View/Design] endobj 15 0 obj <</CreatorInfo<</Creator(Adobe Illustrator 16.0)/Subtype/Artwork>>>> endobj 22 0 obj [21 0 R] endobj 35 0 obj <</CreationDate(D:20171230161300+01'00')/Creator(Adobe Illustrator CS6 \(Windows\))/ModDate(D:20171230161716+01'00')>> endobj xref
0 36
0000000004 65535 f
0000000016 00000 n
0000000159 00000 n
0000009448 00000 n
0000000000 00000 f
0000039045 00000 n
0000000000 00000 f
0000009499 00000 n
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000039112 00000 n
0000039143 00000 n
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000010825 00000 n
0000039228 00000 n
0000009835 00000 n
0000011122 00000 n
0000011009 00000 n
0000010082 00000 n
0000010263 00000 n
0000010311 00000 n
0000010893 00000 n
0000010924 00000 n
0000011196 00000 n
0000011370 00000 n
0000012385 00000 n
0000014612 00000 n
0000039253 00000 n
trailer
<</Size 36/Root 1 0 R/Info 35 0 R/ID[<95FCB4CF9C1B59448ABEC1DF1DD64C33><716E2DB4294F6749B3E5ADF7E13AF65C>]>>
startxref
39388
%%EOF

BIN
web/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

694
web/site.scss Normal file
View File

@ -0,0 +1,694 @@
@import "variables.scss";
html
{
box-sizing: border-box;
font-size: 62.5%;
}
*, *:before, *:after
{
box-sizing: inherit;
}
body
{
background-color: rgb(20, 20, 20);
color: white;
font-family: 'Verdana', 'Arial', sans-serif;
font-size: 1.3em;
font-weight: 300;
letter-spacing: .01em;
line-height: 1.3;
padding-bottom: 3rem;
@media #{$mediumScreen}
{
padding-top: 3rem;
}
}
a
{
text-decoration: none;
}
/*
Hide VueJS container until the template has been processed
*/
[v-cloak]
{
display: none;
}
#container
{
background: $containerBackground;
margin-top: 2rem;
padding: 1rem;
box-shadow: 0 0 50px $containerShadowColor;
border: solid 1px black;
@media #{$mediumScreen}
{
width: 768px;
margin-left: auto;
margin-right: auto;
}
}
.header
{
position: relative;
img
{
float: left;
margin-right: 1rem;
}
.wifistatus
{
@media #{$smallScreen}
{
clear: both;
margin-top: 3rem;
}
@media #{$mediumScreen}
{
position: absolute;
right: 0;
top: 0;
}
.indicator
{
display: inline-block;
width: 1rem;
height: 1rem;
border-radius: 50%;
margin-right: 0.5rem;
&[data-status=connected] { background-color: #339966; }
&[data-status=disconnected] { border: solid 1px #808080; }
&[data-status=connecting] { background-color: #ff9933; }
&[data-status=error] { background-color: #cc0000; }
}
}
}
%outset
{
border: 1px solid #111111;
border-radius: 3px;
box-shadow: inset 0 1px rgba(255,255,255,0.1), inset 0 -1px 3px rgba(0,0,0,0.3), inset 0 0 0 1px rgba(255,255,255,0.08), 0 1px 2px rgba(0,0,0,0.15);
}
%inset
{
border: 1px solid #111111;
border-color: black #111111 #111111;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.25),0 1px rgba(255,255,255,0.08);
}
button, input
{
font-family: 'Verdana', 'Arial', sans-serif;
}
@mixin removeSafariStyling
{
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
input
{
@include removeSafariStyling;
}
button, .button, input[type=submit]
{
@extend %outset;
display: inline-block;
padding: 0 12px;
color: $buttonTextColor;
background: $buttonBackground;
cursor: pointer;
line-height: 3rem;
&:hover, &:focus, &.focus
{
color: $buttonHoverTextColor;
background: $buttonHoverBackground;
outline: none
}
&:active, &.active
{
@extend %inset;
color: $buttonActiveTextColor;
background: $buttonActiveBackground;
}
}
input[type=submit], .button-primary
{
background: $buttonPrimaryBackground;
&:hover, &:focus, &.focus
{
background: $buttonPrimaryHoverBackground;
}
}
a.button
{
text-decoration: none
}
.navigation
{
clear: both;
margin-top: 3rem;
}
.tabs
{
&>.button
{
margin-left: -1px;
border-radius: 0;
&:first-child
{
margin-left: 0;
border-radius: 3px 0 0 3px
}
&:last-child
{
border-radius: 0 3px 3px 0
}
&:focus
{
position: relative;
z-index: 1
}
}
}
.version
{
color: $versionTextColor;
font-size: 8pt;
text-align: center;
margin-top: 2rem;
}
.notificationContainer
{
position: fixed;
top: 2rem;
z-index: 666;
@media #{$mediumScreen}
{
width: 512px;
left: 50%;
}
}
.notification
{
@extend %outset;
background: $notificationBackground;
/* border: solid 1px $notificationBorderColor;*/
box-shadow: 0 0 10px black;
color: white;
cursor: pointer;
padding: .5em;
margin-bottom: 2rem;
position: relative;
@media #{$mediumScreen}
{
left: -50%;
}
.message
{
white-space: pre;
}
&.error
{
background: $notificationErrorBackground;
}
}
.check, .radio
{
display: inline-block;
cursor: pointer;
user-select: none;
white-space: nowrap;
margin-top: .5em;
margin-bottom: .5em;
.control
{
@extend %outset;
background: $checkRadioBackground;
display: inline-block;
width: 16px;
height: 16px;
position: relative;
}
.label
{
display: inline-block;
margin-left: .5em;
vertical-align: top;
}
&.checked
{
.control
{
background: $checkRadioSelectedBackground;
.inner
{
}
}
}
&.disabled
{
cursor: not-allowed;
.label
{
color: $inputDisabledTextColor;
}
}
}
.radio
{
.control, .control .inner
{
border-radius: 50%;
}
.control .inner
{
color: black;
position: absolute;
top: 4px;
left: 4px;
width: 6px;
height: 6px;
}
&.checked .control .inner
{
background: #cccccc;
box-shadow: 0 1px rgba(0,0,0,0.5);
}
}
.check
{
.control .inner
{
position: absolute;
top: 5px;
left: 4px;
width: 6px;
height: 3px;
}
&.checked .control .inner
{
border: solid rgba(255,255,255,0.8);
border-width: 0 0 2px 2px;
transform: rotate(-45deg);
box-shadow: -1px 0 rgba(0,0,0,0.2), 0 1px rgba(0,0,0,0.5)
}
}
.form-control
{
margin-top: 1em;
}
input[type=text], input[type=number], input[type=password], textarea
{
@extend %inset;
background: $inputBackground;
color: $inputTextColor;
padding: .5em;
width: 100%;
}
select
{
@extend %outset;
background: $selectBackground;
color: $inputTextColor;
font-family: 'Verdana', 'Arial', sans-serif;
padding: .5em;
}
input[type=range]
{
margin-top: 1rem;
margin-bottom: 1rem;
}
h1
{
font-size: 2rem;
margin: 0;
}
h2
{
color: #c0c0c0;
font-size: 1.2rem;
margin: 0;
}
h3
{
@extend %outset;
color: $sectionHeaderTextColor;
background: $sectionHeaderBackground;
font-size: 1.2rem;
padding: .5rem;
}
h4
{
font-size: 1.4rem;
}
input[disabled]
{
cursor: not-allowed;
color: $inputDisabledTextColor;
background: $inputDisabledBackground;
}
label
{
display: block;
margin-top: .5em;
margin-bottom: .5em;
}
.label-inline
{
margin-right: 2rem;
}
@media #{$mediumScreen}
{
.horizontal
{
clear: both;
label
{
display: inline-block;
}
input[type=text], input[type=number], input[type=password], textarea
{
display: inline-block;
float: right;
width: 50%;
}
&:after
{
clear: both;
}
}
}
.hint
{
display: block;
font-size: 8pt;
color: #808080;
margin-bottom: 1.5rem;
}
.loading
{
margin-top: 3rem;
text-align: center;
}
.suboptions
{
margin-left: 5rem;
}
.buttons
{
clear: both;
text-align: center;
margin-top: 1rem;
}
.sliders
{
margin-top: 2rem;
}
.slidercontainer
{
margin-top: 1rem;
}
.slider
{
-webkit-appearance: none;
width: 100%;
height: $sliderBarSize;
border-radius: $sliderBarSize / 2;
background: $sliderBarColor;
outline: none;
&::-webkit-slider-thumb
{
-webkit-appearance: none;
appearance: none;
width: $sliderThumbSize;
height: $sliderThumbSize;
border-radius: 50%;
background: $sliderThumbColor;
cursor: pointer;
}
&::-moz-range-thumb
{
width: $sliderThumbSize;
height: $sliderThumbSize;
border-radius: 50%;
background: $sliderThumbColor;
cursor: pointer;
}
}
.warning
{
@extend %outset;
background: #973a38;
padding: .5em;
margin-bottom: 2rem;
margin-top: 1rem;
}
.nodata
{
color: #808080;
text-align: center;
}
.clear
{
clear: both;
}
.panel
{
margin-bottom: 2rem;
padding: 0;
.panel-header
{
@extend %outset;
border-radius: 3px 3px 0 0;
border-bottom-width: 0;
padding: .5em;
label {
font-size: 1em;
}
background: $panelHeaderBackground;
color: $panelHeaderTextColor;
.actions
{
float: right;
}
a, .label
{
color: $panelHeaderLinkColor;
}
}
.panel-body
{
@extend %outset;
border-radius: 0 0 3px 3px;
background: $panelBodyBackground;
padding: 2rem;
}
&.active
{
.panel-header
{
background: $panelActiveHeaderBackground;
color: $panelActiveHeaderTextColor;
}
}
}
.inline
{
display: inline-block;
width: auto;
}
.fade-enter-active, .fade-leave-active
{
transition: opacity .5s;
}
.fade-enter, .fade-leave-to
{
opacity: 0;
}
.range
{
clear: both;
.start
{
position: relative;
display: inline-block;
width: 49%;
.slidercontainer
{
margin-right: 4em;
}
.value
{
position: absolute;
right: 0;
top: 1.5rem;
color: $sliderValueColor;
}
}
.end
{
position: relative;
display: inline-block;
float: right;
width: 50%;
.slidercontainer
{
margin-left: 4em;
}
.value
{
position: absolute;
left: 0;
top: 1.5rem;
color: $sliderValueColor;
}
}
&:after
{
clear: both;
}
}
.resetReason
{
margin-left: 2em;
}

48
web/variables.scss Normal file
View File

@ -0,0 +1,48 @@
$smallScreen: "screen and (max-width: 767px)";
$mediumScreen: "screen and (min-width: 768px)";
$inputBackground: #404040;
$inputTextColor: white;
$inputDisabledBackground: #262626;
$inputDisabledTextColor: #808080;
$containerBackground: #202020;
$containerShadowColor: #fcf6cf;
$buttonBackground: $inputBackground;
$buttonTextColor: #dddddd;
$buttonHoverBackground: #505050;
$buttonHoverTextColor: #dddddd;
$buttonActiveBackground: #282828;
$buttonActiveTextColor: #cccccc;
$buttonPrimaryBackground: #2265a1;
$buttonPrimaryHoverBackground: #2672b6;
$checkRadioBackground: $inputBackground;
$checkRadioSelectedBackground: #606060;
$panelBorderColor: #404040;
$panelBodyBackground: #303030;
$panelHeaderBackground: #404040;
$panelHeaderTextColor: white;
$panelActiveHeaderBackground: #3b4a58;
$panelActiveHeaderTextColor: $panelHeaderTextColor;
$panelHeaderLinkColor: white;
$selectBackground: $inputBackground;
$notificationBackground: #297ab8;
$notificationErrorBackground: #973a38;
$versionTextColor: #808080;
$sectionHeaderBackground: #282828;
$sectionHeaderTextColor: #808080;
$sliderBarColor: #404040;
$sliderBarSize: .5rem;
$sliderThumbColor: #fcf6cf;
$sliderThumbSize: 2rem;
$sliderValueColor: #808080;