Vue single file component implementation

- Using webpack for bundling
This commit is contained in:
Mark van Renswoude 2020-09-22 20:57:19 +02:00
parent 8d4886da7e
commit 7ae2f49ee8
38 changed files with 12187 additions and 4628 deletions

3
.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules
bin
web/dist
.pio

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@babel/preset-env'
]
}

View File

@ -13,7 +13,7 @@ app.use(bodyParser.json());
app.use(express.static('web'));
app.use(express.static('web/dist'));
app.get('/api/status', function(req, res)
app.get('/api/status', (req, res) =>
{
res.send({
systemID: 'dev-server',
@ -23,7 +23,7 @@ app.get('/api/status', function(req, res)
});
});
app.get('/api/connection', function(req, res)
app.get('/api/connection', (req, res) =>
{
res.send({
hostname: 'dev-server',
@ -38,7 +38,7 @@ app.get('/api/connection', function(req, res)
});
});
app.get('/api/connection/status', function(req, res)
app.get('/api/connection/status', (req, res) =>
{
res.send({
"ap": {
@ -53,12 +53,12 @@ app.get('/api/connection/status', function(req, res)
});
});
app.post('/api/connection', function(req, res)
app.post('/api/connection', (req, res) =>
{
res.sendStatus(200);
});
app.post('/api/firmware', function(req, res)
app.post('/api/firmware', (req, res) =>
{
res.sendStatus(200);
});
@ -73,12 +73,12 @@ var system = {
ledCount: 60
};
app.get('/api/system', function(req, res)
app.get('/api/system', (req, res) =>
{
res.send(system);
});
app.post('/api/system', function(req, res)
app.post('/api/system', (req, res) =>
{
var body = req.body;
if (body)
@ -96,12 +96,18 @@ app.post('/api/system', function(req, res)
app.get('/api/stacktrace/get', function(req, res)
app.get('/api/stacktrace/get', (req, res) =>
{
res.send("Nothing to see here, move along!");
});
app.get('/api/stacktrace/delete', function(req, res)
app.get('/api/stacktrace/delete', (req, res) =>
{
res.sendStatus(200);
});
app.get('/api/set/static', (req, res) =>
{
res.sendStatus(200);
});

View File

@ -19,6 +19,8 @@ const concat = require('gulp-concat');
const print = require('gulp-print');
const path = require('path');
const gzip = require('gulp-gzip');
const webpack = require('webpack')
const webpackConfig = require('./webpack.build.js')
const config = {
@ -31,6 +33,23 @@ const config = {
};
function runWebpack()
{
return new Promise((resolve, reject) =>
{
webpack(webpackConfig, (err, stats) =>
{
if (err)
return reject(err);
if (stats.hasErrors())
return reject(new Error(stats.compilation.errors.join('\n')));
resolve()
});
});
}
const HTMLMap = {
'index.html': 'Index'
};
@ -39,10 +58,13 @@ 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
@ -60,11 +82,10 @@ const JSSrc = [
const SCSSSrc = [
'web/site.scss'
]
*/
gulp.task('embedHTML', () =>
function embedHTML()
{
return gulp.src(config.assetsPath + '*.html')
.pipe(print(filepath => { return 'HTML: ' + filepath; }))
@ -81,9 +102,10 @@ gulp.task('embedHTML', () =>
byteArray: true
}))
.pipe(gulp.dest(config.outputPath));
});
}
/*
gulp.task('compileScss', () =>
{
return gulp.src(SCSSSrc)
@ -117,9 +139,10 @@ gulp.task('compileJS', () =>
.pipe(uglify())
.pipe(gulp.dest(config.distPath));
});
*/
gulp.task('embedJS', gulp.series('compileJS', () =>
function embedJS()
{
return gulp.src([config.distPath + 'bundle.js'])
.pipe(gzip({ append: false }))
@ -129,10 +152,11 @@ gulp.task('embedJS', gulp.series('compileJS', () =>
byteArray: true
}))
.pipe(gulp.dest(config.outputPath));
}));
}
gulp.task('embedCSS', gulp.series('compileScss', () =>
/*
function embedCSS()
{
return gulp.src([config.distPath + 'bundle.css'])
.pipe(gzip({ append: false }))
@ -142,18 +166,8 @@ gulp.task('embedCSS', gulp.series('compileScss', () =>
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')(); });
}
));
}
*/
@ -198,7 +212,7 @@ function getVersion(callback)
}
gulp.task('embedVersion', () =>
function embedVersion()
{
return getVersion(version =>
{
@ -229,10 +243,7 @@ gulp.task('embedVersion', () =>
});
})
});
});
gulp.task('embedAssets', gulp.series('embedHTML', 'embedJS', 'embedCSS', 'embedVersion'));
}
@ -309,6 +320,23 @@ function platformio(target)
});
}
gulp.task('upload', gulp.series('embedAssets', () => { return platformio('upload'); }));
gulp.task('build', gulp.series('embedAssets', () => { return platformio(false); }));
gulp.task('default', gulp.series('embedAssets'));
function platformIOUpload()
{
return platformio('upload');
}
function platformIOBuild()
{
return platformio(false);
}
const buildAndEmbedAssets = gulp.series(runWebpack, embedHTML, embedJS, embedVersion);
module.exports = {
upload: gulp.series(buildAndEmbedAssets, platformIOUpload),
build: gulp.series(buildAndEmbedAssets, platformIOBuild),
default: gulp.series(buildAndEmbedAssets)
};

7136
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,12 @@
"main": "gulpfile.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "node devserver.js"
"upload": "gulp upload",
"build": "gulp build",
"webpack": "gulp",
"dev": "npm-run-all -p start-webpack-devserver start-api-devserver",
"start-webpack-devserver": "webpack-dev-server --config ./webpack.dev.js",
"start-api-devserver": "supervisor -w \"devserver.js\" devserver.js"
},
"repository": {
"type": "git",
@ -14,10 +19,17 @@
"author": "Mark van Renswoude <mark@x2software.net>",
"license": "Unlicense",
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/preset-env": "^7.11.5",
"axios": "^0.20.0",
"babel-loader": "^8.1.0",
"body-parser": "^1.18.2",
"child_process": "^1.0.2",
"core-js": "^3.6.5",
"css-loader": "^4.3.0",
"express": "^4.16.2",
"file-loader": "^6.1.0",
"gulp": "^4.0.2",
"gulp-clean-css": "^3.9.0",
"gulp-concat": "^2.6.1",
"gulp-debounced-watch": "^1.0.4",
@ -27,16 +39,29 @@
"gulp-print": "^2.0.1",
"gulp-sass": "^3.1.0",
"gulp-uglify": "^3.0.0",
"html-webpack-plugin": "^4.4.1",
"lodash": "^4.17.4",
"node-sass": "^4.14.1",
"npm": "^6.14.8",
"npm-run-all": "^4.1.5",
"path": "^0.12.7",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"style-loader": "^1.2.1",
"terser-webpack-plugin": "^4.2.2",
"through2": "^2.0.3",
"vinyl": "^2.1.0",
"vue": "^2.5.13",
"vue": "^2.6.11",
"vue-i18n": "^7.3.3",
"vue-loader": "^15.9.3",
"vue-router": "^3.2.0",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"vuex": "^3.4.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^5.1.4",
"yargs": "^16.0.3"
},
"dependencies": {
"gulp": "^4.0.2",
"npm": "^6.14.8"
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@
#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)
@ -25,5 +24,4 @@ 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)); });
}

View File

@ -1,615 +0,0 @@
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,
settingStatic: 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,
},
ledCount: null
},
static: {
r: 0,
g: 0,
b: 0,
w: 0
}
},
created: function()
{
var self = this;
self.notificationTimer = null;
self.disableSetStatic = false;
self.setStaticTimer = false;
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, headers: { 'Content-Type': 'application/json' } })
.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, headers: { 'Content-Type': 'application/json' } })
.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'));
},
staticOff: function()
{
this.static = {
r: 0,
g: 0,
b: 0,
w: 0
};
},
staticChanged: function()
{
var self = this;
if (self.loading || self.disableStaticChanged) return;
if (self.setStaticTimer === false)
self.setStaticTimer = setTimeout(function() { self.setStatic(); }, 200);
},
setStatic: function()
{
var self = this;
if (self.settingStatic)
self.setStaticTimer = setTimeout(function() { self.setStatic(); }, 200);
self.settingStatic = true;
self.setStaticTimer = false;
axios.get('/api/set/static', { params: this.static })
.then(function(response)
{
})
.catch(self.handleAPIError.bind(self, 'error.setColor'))
.then(function()
{
self.settingStatic = false;
});
},
},
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);
}
},
watch: {
static: {
handler: function(newValue) { this.staticChanged(); },
deep: true
}
}
});
}

1
web/dist/bundle.css vendored

File diff suppressed because one or more lines are too long

1
web/dist/bundle.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,201 +0,0 @@
<!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,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6N0E2MTY4NzJGQjFBMTFFQThERTJFM0JGMzk3MjRDRDUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6N0E2MTY4NzNGQjFBMTFFQThERTJFM0JGMzk3MjRDRDUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3QTYxNjg3MEZCMUExMUVBOERFMkUzQkYzOTcyNENENSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3QTYxNjg3MUZCMUExMUVBOERFMkUzQkYzOTcyNENENSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgI/6YAAAAfkSURBVHjazFpbbBRV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" />
<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 class="slidercontainer">
<input type="range" min="0" max="255" class="slider red" v-model.number="static.r">
</div>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider green" v-model.number="static.g">
</div>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider blue" v-model.number="static.b">
</div>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider white" v-model.number="static.w">
</div>
<div class="buttons">
<a class="button button-secondary" @click.prevent="staticOff">{{ $t('status.staticOff') }}</a>
</div>
</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">
<span class="hint">{{ $t('connection.hostnameHint') }}</span>
<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>
<h3>{{ $t('system.ledStripTitle') }}</h3>
<div class="horizontal">
<label for="ledCount">{{ $t('system.ledCount') }}</label>
<input type="number" id="ledCount" v-model.number="system.ledCount">
</div>
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</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>

View File

@ -1,207 +0,0 @@
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',
staticOff: 'Off'
},
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',
hostnameHint: 'If specified, this module is available at <hostname>.local if your device supports mDNS (at the time of writing, Android does not).',
hostnamePlaceholder: 'Default: mac address'
},
system: {
tabTitle: 'System',
pinsTitle: 'Hardware pinout',
ledStripTitle: 'LED strip',
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)',
ledCount: 'Number of LEDs on strip'
},
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',
setColor: 'Could not set color',
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',
staticOff: 'Uit'
},
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',
hostnameHint: 'Indien ingevuld is deze module te bereiken op <hostnaam>.local als je apparaat mDNS ondersteund mDNS (op het moment van schrijven ondersteund Android dit niet).',
hostnamePlaceholder: 'Standaard: mac adres'
},
system: {
tabTitle: 'Systeem',
pinsTitle: 'Hardware aansluitingen',
ledStripTitle: 'LED strip',
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)',
ledCount: 'Aantal LEDs op strip'
},
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',
setColor: 'Kan kleur niet zetten',
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'
}
}
}

17
web/public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="theme-color" content="#000000">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,723 +0,0 @@
@import "variables.scss";
html
{
overscroll-behavior-x: contain;
box-sizing: border-box;
font-size: 62.5%;
}
*, *:before, *:after
{
box-sizing: inherit;
}
body
{
overscroll-behavior-x: contain;
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;
}
$sliderRedThumbColor: #ce3636;
$sliderGreenThumbColor: #32b732;
$sliderBlueThumbColor: #4646cc;
$sliderWhiteThumbColor: #fcf6cf;
.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;
}
&.red
{
&::-webkit-slider-thumb { background: $sliderRedThumbColor; }
&::-moz-range-thumb { background: $sliderRedThumbColor; }
}
&.green
{
&::-webkit-slider-thumb { background: $sliderGreenThumbColor; }
&::-moz-range-thumb { background: $sliderGreenThumbColor; }
}
&.blue
{
&::-webkit-slider-thumb { background: $sliderBlueThumbColor; }
&::-moz-range-thumb { background: $sliderBlueThumbColor; }
}
}
.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;
}

990
web/src/App.vue Normal file
View File

@ -0,0 +1,990 @@
<template>
<div id="app">
<div class="notificationContainer">
<div class="notification" :class="{ error: notification != null && notification.isError }" 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,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6N0E2MTY4NzJGQjFBMTFFQThERTJFM0JGMzk3MjRDRDUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6N0E2MTY4NzNGQjFBMTFFQThERTJFM0JGMzk3MjRDRDUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3QTYxNjg3MEZCMUExMUVBOERFMkUzQkYzOTcyNENENSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3QTYxNjg3MUZCMUExMUVBOERFMkUzQkYzOTcyNENENSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgI/6YAAAAfkSURBVHjazFpbbBRV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" />
<h1>{{ $t('title') }}</h1>
<h2>{{ $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">
<LoadingIndicator></LoadingIndicator>
</div>
<div v-else>
<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">
<router-link to="/" class="button" active-class="active" exact="true">{{ $t('status.tabTitle') }}</router-link><router-link to="/connection" class="button" active-class="active">{{ $t('connection.tabTitle') }}</router-link><router-link to="/system" class="button" active-class="active">{{ $t('system.tabTitle') }}</router-link>
</div>
<router-view/>
<div class="clearfix"></div>
</div>
<div class="version">
{{ $t('copyright') }}<br>
{{ $t('firmwareVersion') }}{{ status.version || '...' }}
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import LoadingIndicator from '@/components/loadingIndicator.vue';
export default {
components: {
LoadingIndicator
},
data()
{
return {
loading: true,
status: {
systemID: null,
version: null,
resetReason: null,
stackTrace: false
},
wifiStatus: {
ap: {
enabled: false,
ip: '0.0.0.0'
},
station: {
enabled: false,
status: 0,
ip: '0.0.0.0'
}
}
};
},
mounted()
{
const self = this;
self.updateStatus()
.then(() =>
{
self.updateWiFiStatus()
.then(() =>
{
self.loading = false;
});
});
},
computed: {
notification() { return this.$store.state.notification; },
saving() { return this.$store.state.saving; },
hasResetError()
{
/*
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 (this.status.resetReason === 1 ||
this.status.resetReason === 2 ||
this.status.resetReason === 3 ||
this.status.stackTrace);
}
},
methods: {
getWiFiStationStatus()
{
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()
{
if (!this.wifiStatus.station.enabled)
return this.$i18n.t('wifiStatus.stationmode.disabled');
switch (this.wifiStatus.station.status)
{
case 0: // WL_IDLE_STATUS
return this.$i18n.t('wifiStatus.stationmode.idle');
case 1: // WL_NO_SSID_AVAIL
return this.$i18n.t('wifiStatus.stationmode.noSSID');
case 2: // WL_SCAN_COMPLETED
return this.$i18n.t('wifiStatus.stationmode.scanCompleted');
case 3: // WL_CONNECTED
return this.wifiStatus.station.ip;
case 4: // WL_CONNECT_FAILED
return this.$i18n.t('wifiStatus.stationmode.connectFailed');
case 5: // WL_CONNECTION_LOST
return this.$i18n.t('wifiStatus.stationmode.connectionLost');
case 6: // WL_DISCONNECTED
default:
return this.$i18n.t('wifiStatus.stationmode.disconnected');
}
},
updateStatus()
{
var self = this;
return axios.get('/api/status', { retry: 10, retryDelay: 1000 })
.then(response =>
{
if (typeof response.data == 'object')
self.status = response.data;
})
.catch(e => self.handleAPIError('error.loadStatus', e));
},
updateWiFiStatus()
{
const self = this;
return new Promise((resolve, reject) =>
{
if (!self.saving)
{
axios.get('/api/connection/status', { retry: 10, retryDelay: 1000 })
.then(response =>
{
if (typeof response.data == 'object')
self.wifiStatus = response.data;
})
.catch(e =>
{
self.handleAPIError('error.updateWiFiStatus', e);
reject(e);
})
.then(function()
{
setTimeout(self.updateWiFiStatus, 5000);
resolve();
});
}
else
{
setTimeout(self.updateWiFiStatus, 5000);
resolve();
}
});
},
deleteStackTrace()
{
const self = this;
return axios.get('/api/stacktrace/delete', { retry: 10, retryDelay: 1000 })
.then(response =>
{
self.status.resetReason = 0;
self.status.stackTrace = false;
})
.catch(e => self.handleAPIError('error.stackTraceDeleteError', e));
},
handleAPIError(messageId, error)
{
this.$store.dispatch('notifyAPIError', { message: this.$i18n.t(messageId), error });
},
hideNotification()
{
this.$store.dispatch('hideNotification');
}
}
}
</script>
<style lang="scss">
@import "variables.scss";
html
{
overscroll-behavior-x: contain;
box-sizing: border-box;
font-size: 62.5%;
}
*, *:before, *:after
{
box-sizing: inherit;
}
body
{
overscroll-behavior-x: contain;
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;
}
#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;
}
$sliderRedThumbColor: #ce3636;
$sliderGreenThumbColor: #32b732;
$sliderBlueThumbColor: #4646cc;
$sliderWhiteThumbColor: #fcf6cf;
.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;
}
&.red
{
&::-webkit-slider-thumb { background: $sliderRedThumbColor; }
&::-moz-range-thumb { background: $sliderRedThumbColor; }
}
&.green
{
&::-webkit-slider-thumb { background: $sliderGreenThumbColor; }
&::-moz-range-thumb { background: $sliderGreenThumbColor; }
}
&.blue
{
&::-webkit-slider-thumb { background: $sliderBlueThumbColor; }
&::-moz-range-thumb { background: $sliderBlueThumbColor; }
}
}
.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;
}
</style>

147
web/src/app.js Normal file
View File

@ -0,0 +1,147 @@
function startApp()
{
var app = new Vue({
el: '#app',
data: {
},
created: function()
{
var self = this;
self.notificationTimer = null;
// 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: {
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, headers: { 'Content-Type': 'application/json' } })
.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, headers: { 'Content-Type': 'application/json' } })
.then(function(response)
{
self.showNotification(i18n.t('rebootPending'));
})
.catch(self.handleAPIError.bind(self, 'error.applySystem'))
.then(function()
{
self.saving = false;
});
},
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();
});
},
}
});
}

BIN
web/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,44 @@
<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>
</template>
<script>
export default {
props: {
title: String,
value: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
methods: {
handleClick()
{
if (this.disabled)
return;
this.value = !this.value;
this.$emit('input', this.value);
},
handleKeyDown(event)
{
if (event.keyCode == 32)
{
this.handleClick();
event.preventDefault();
}
}
}
}
</script>

View File

@ -0,0 +1,43 @@
<template>
<span>
{{ $t(messageId || 'loading') }} {{ indicator }}
</span>
</template>
<script>
export default {
props: {
messageId: null
},
data()
{
return {
stage: 0,
indicator: '|'
};
},
created()
{
const self = this;
self.timer = setInterval(() =>
{
self.stage++;
switch (self.stage)
{
case 1: self.indicator = '/'; break;
case 2: self.indicator = '-'; break;
case 3: self.indicator = '\\'; break;
case 4: self.indicator = '|'; self.stage = 0; break;
}
}, 250);
},
destroyed()
{
clearInterval(this.timer);
}
}
</script>

View File

@ -0,0 +1,42 @@
<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>
</template>
<script>
export default {
props: {
title: String,
value: null,
id: null,
disabled: {
type: Boolean,
default: false
}
},
methods: {
handleClick()
{
if (this.disabled)
return;
this.value = this.id;
this.$emit('input', this.value);
},
handleKeyDown(event)
{
if (event.keyCode == 32)
{
this.handleClick();
event.preventDefault();
}
}
}
}
</script>

View File

@ -0,0 +1,56 @@
<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>
</template>
<script>
export default {
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
}
}
}
</script>

102
web/src/i18n/en.js Normal file
View File

@ -0,0 +1,102 @@
export default {
title: 'RGBWifi',
systemID: 'System ID: ',
firmwareVersion: 'Firmware version: ',
copyright: 'Copyright © 2020 Mark van Renswoude',
loading: 'Please wait, loading...',
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',
staticOff: 'Off'
},
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',
hostnameHint: 'If specified, this module is available at <hostname>.local if your device supports mDNS (at the time of writing, Android does not).',
hostnamePlaceholder: 'Default: mac address'
},
system: {
tabTitle: 'System',
pinsTitle: 'Hardware pinout',
ledStripTitle: 'LED strip',
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)',
ledCount: 'Number of LEDs on strip'
},
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',
setColor: 'Could not set color',
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'
}
}

7
web/src/i18n/index.js Normal file
View File

@ -0,0 +1,7 @@
import en from './en'
import nl from './nl'
export default {
en,
nl
}

102
web/src/i18n/nl.js Normal file
View File

@ -0,0 +1,102 @@
export default {
title: 'RGBWifi',
systemID: 'Systeem ID: ',
firmwareVersion: 'Firmware versie: ',
copyright: 'Copyright © 2020 Mark van Renswoude',
loading: 'Een ogenblik geduld, bezig met laden...',
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',
staticOff: 'Uit'
},
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',
hostnameHint: 'Indien ingevuld is deze module te bereiken op <hostnaam>.local als je apparaat mDNS ondersteund mDNS (op het moment van schrijven ondersteund Android dit niet).',
hostnamePlaceholder: 'Standaard: mac adres'
},
system: {
tabTitle: 'Systeem',
pinsTitle: 'Hardware aansluitingen',
ledStripTitle: 'LED strip',
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)',
ledCount: 'Aantal LEDs op strip'
},
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',
setColor: 'Kan kleur niet zetten',
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'
}
}

127
web/src/index.html Normal file
View File

@ -0,0 +1,127 @@
<!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 v-if="activeTab == 'status'">
<!--
Status tab
-->
</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">
<span class="hint">{{ $t('connection.hostnameHint') }}</span>
<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>
<h3>{{ $t('system.ledStripTitle') }}</h3>
<div class="horizontal">
<label for="ledCount">{{ $t('system.ledCount') }}</label>
<input type="number" id="ledCount" v-model.number="system.ledCount">
</div>
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
</form>
</div>
<script language="javascript">
startApp();
</script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

63
web/src/main.js Normal file
View File

@ -0,0 +1,63 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import axios from 'axios'
import App from './App.vue'
import router from './router'
import store from './store'
import messages from './i18n'
// Source: https://github.com/axios/axios/issues/164
axios.interceptors.response.use(undefined, function axiosRetryInterceptor(err) {
const 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
const 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.use(VueI18n);
const i18n = new VueI18n({
locale: navigator.language.split('-')[0],
fallbackLocale: 'en',
messages: messages
});
Vue.config.productionTip = false;
new Vue({
router,
store,
i18n,
el: '#app',
render: h => h(App)
});

23
web/src/router/index.js Normal file
View File

@ -0,0 +1,23 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Status from '../views/Status.vue'
//import Connection from '../views/Connection.vue'
//import System from '../views/System.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Status',
component: Status
}
]
const router = new VueRouter({
routes
})
export default router;

109
web/src/store/index.js Normal file
View File

@ -0,0 +1,109 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
notification: null,
notificationTimeout: null,
saving: false,
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,
},
ledCount: null
}
/*
settingStatic: false,
loadingIndicator: '|',
uploadProgress: false,
static: {
r: 0,
g: 0,
b: 0,
w: 0
}
*/
},
mutations: {
_setNotification(state, payload)
{
state.notification = payload.notification;
state.notificationTimeout = payload.notificationTimeout;
}
},
actions: {
showNotification(context, payload)
{
const self = this;
if (context.state.notificationTimeout !== null)
clearTimeout(context.state.notificationTimeout);
const notificationTimeout = setTimeout(() =>
{
context.dispatch('hideNotification');
}, 5000);
context.commit('_setNotification', {
notification: payload,
notificationTimeout
});
},
hideNotification(context)
{
if (context.state.notificationTimeout !== null)
clearTimeout(context.state.notificationTimeout);
context.commit('_setNotification', {
notification: null,
notificationTimeout: null
});
},
notifyAPIError(context, payload)
{
console.log(payload.error);
let errorMessage = '';
if (payload.error.response)
{
errorMessage = 'HTTP response code ' + payload.error.response.status;
}
else if (payload.error.request)
{
errorMessage = 'No response';
}
else
{
errorMessage = payload.error.message;
}
context.dispatch('showNotification', { message: payload.message + '\n\n' + errorMessage, isError: true });
}
},
})

130
web/src/views/Status.vue Normal file
View File

@ -0,0 +1,130 @@
<template>
<div>
<h3>{{ $t('status.title') }}</h3>
<div v-if="static === null" class="loading">
<LoadingIndicator></LoadingIndicator>
</div>
<div v-else>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider red" v-model.number="static.r">
</div>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider green" v-model.number="static.g">
</div>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider blue" v-model.number="static.b">
</div>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider white" v-model.number="static.w">
</div>
<div class="buttons">
<a class="button button-secondary" @click.prevent="staticOff">{{ $t('status.staticOff') }}</a>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import LoadingIndicator from '@/components/loadingIndicator.vue';
export default {
components: {
LoadingIndicator
},
data()
{
return {
static: null
}
},
mounted()
{
const self = this;
self.disableSetStatic = false;
self.setStaticTimer = false;
// TODO load current settings (no API for it yet)
self.static = {
r: 0,
g: 0,
b: 0,
w: 0
};
self.$watch('static', () =>
{
self.staticChanged();
}, { deep: true });
},
methods: {
staticOff()
{
this.static = {
r: 0,
g: 0,
b: 0,
w: 0
};
},
staticChanged()
{
var self = this;
console.log(self.setStaticTimer);
if (self.setStaticTimer === false)
self.setStaticTimer = setTimeout(() =>
{
self.setStatic();
}, 200);
},
setStatic()
{
var self = this;
if (self.settingStatic)
{
self.setStaticTimer = setTimeout(() =>
{
self.setStatic();
}, 200);
return;
}
self.settingStatic = true;
self.setStaticTimer = false;
axios.get('/api/set/static', { params: this.static })
.then(response =>
{
})
.catch(e => self.handleAPIError('error.setColor', e))
.then(() =>
{
self.settingStatic = false;
});
},
handleAPIError(messageId, error)
{
this.$store.dispatch('notifyAPIError', { message: this.$i18n.t(messageId), error });
}
}
}
</script>

19
webpack.build.js Normal file
View File

@ -0,0 +1,19 @@
const { merge } = require('webpack-merge');
const config = require('./webpack.config.js');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = merge(config, {
mode: "production",
devtool: "#source-map",
output: {
filename: 'bundle.js'
},
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
}
});

68
webpack.config.js Normal file
View File

@ -0,0 +1,68 @@
'use strict'
const webpack = require('webpack');
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: [
'./web/src/main.js'
],
output: {
path: path.resolve(__dirname, './web/dist'),
filename: 'bundle.js'
},
resolve: {
alias: {
'@': path.resolve(__dirname, './web/src'),
vue$: 'vue/dist/vue.runtime.esm.js'
},
},
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
{
loader: 'css-loader',
options: {
esModule: false
}
},
'sass-loader'
]
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: file => (
/node_modules/.test(file) &&
!/\.vue\.js/.test(file)
)
},
{
test: /\.(png)?$/,
use: [{
loader: 'file-loader'
}]
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
title: 'RGBWifi',
template: 'web/public/index.html'
})
]
}

18
webpack.dev.js Normal file
View File

@ -0,0 +1,18 @@
const { merge } = require('webpack-merge');
const config = require("./webpack.config.js");
const webpack = require("webpack");
module.exports = merge(config, {
mode: 'development',
devServer: {
historyApiFallback: true,
proxy:{
'/api': {
target: 'http://localhost:3000'
}
},
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
]
});