initial commit of C++ back end and react front end
This commit is contained in:
commit
63a639eb22
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.pioenvs
|
||||||
|
.piolibdeps
|
||||||
|
.clang_complete
|
||||||
|
.gcc-flags.json
|
||||||
|
*Thumbs.db
|
||||||
|
/interface/build
|
||||||
|
/interface/node_modules
|
67
.travis.yml
Normal file
67
.travis.yml
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Continuous Integration (CI) is the practice, in software
|
||||||
|
# engineering, of merging all developer working copies with a shared mainline
|
||||||
|
# several times a day < http://docs.platformio.org/page/ci/index.html >
|
||||||
|
#
|
||||||
|
# Documentation:
|
||||||
|
#
|
||||||
|
# * Travis CI Embedded Builds with PlatformIO
|
||||||
|
# < https://docs.travis-ci.com/user/integration/platformio/ >
|
||||||
|
#
|
||||||
|
# * PlatformIO integration with Travis CI
|
||||||
|
# < http://docs.platformio.org/page/ci/travis.html >
|
||||||
|
#
|
||||||
|
# * User Guide for `platformio ci` command
|
||||||
|
# < http://docs.platformio.org/page/userguide/cmd_ci.html >
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Please choice one of the following templates (proposed below) and uncomment
|
||||||
|
# it (remove "# " before each line) or use own configuration according to the
|
||||||
|
# Travis CI documentation (see above).
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Template #1: General project. Test it using existing `platformio.ini`.
|
||||||
|
#
|
||||||
|
|
||||||
|
# language: python
|
||||||
|
# python:
|
||||||
|
# - "2.7"
|
||||||
|
#
|
||||||
|
# sudo: false
|
||||||
|
# cache:
|
||||||
|
# directories:
|
||||||
|
# - "~/.platformio"
|
||||||
|
#
|
||||||
|
# install:
|
||||||
|
# - pip install -U platformio
|
||||||
|
# - platformio update
|
||||||
|
#
|
||||||
|
# script:
|
||||||
|
# - platformio run
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Template #2: The project is intended to by used as a library with examples
|
||||||
|
#
|
||||||
|
|
||||||
|
# language: python
|
||||||
|
# python:
|
||||||
|
# - "2.7"
|
||||||
|
#
|
||||||
|
# sudo: false
|
||||||
|
# cache:
|
||||||
|
# directories:
|
||||||
|
# - "~/.platformio"
|
||||||
|
#
|
||||||
|
# env:
|
||||||
|
# - PLATFORMIO_CI_SRC=path/to/test/file.c
|
||||||
|
# - PLATFORMIO_CI_SRC=examples/file.ino
|
||||||
|
# - PLATFORMIO_CI_SRC=path/to/test/directory
|
||||||
|
#
|
||||||
|
# install:
|
||||||
|
# - pip install -U platformio
|
||||||
|
# - platformio update
|
||||||
|
#
|
||||||
|
# script:
|
||||||
|
# - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N
|
66
README.md
Normal file
66
README.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# ESP8266 React
|
||||||
|
|
||||||
|
A simple(ish) framework for getting up and running with the ESP8266 microchip and a react front end. Includes a GUI for configuring WiFi settings, a dynamic access point, NTP, and OTA updates.
|
||||||
|
|
||||||
|
Designed to work with the platformio IDE with limited setup.
|
||||||
|
|
||||||
|
## Why I made this project
|
||||||
|
|
||||||
|
I found I was repeating a lot of work when starting new projects with the ESP8266 chip. Most projects I've had demand a configuration interface for WiFi, the ability to synchronize with NTP, and OTA updates. I plan to use this as a basis for my upcoming personal projects and to extend and improve it as I go.
|
||||||
|
|
||||||
|
![Screenshots](/screenshots/screenshots.png?raw=true "Screenshots")
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
You will need the following before you can get started.
|
||||||
|
|
||||||
|
* [PlatformIO](https://platformio.org/) - IDE for development
|
||||||
|
* [NPM](https://www.npmjs.com/) - For building the interface
|
||||||
|
* Bash shell, or Git Bash if you are under windows
|
||||||
|
|
||||||
|
### Installing in PlatformIO
|
||||||
|
|
||||||
|
Pull the project and add it to PlatformIO as a project folder (File > Add Project Folder).
|
||||||
|
|
||||||
|
PlatformIO should download the ESP8266 platform and the project library dependencies automatically.
|
||||||
|
|
||||||
|
Once the platform and libraries are downloaded the back end should be compiling.
|
||||||
|
|
||||||
|
### Building the interface
|
||||||
|
|
||||||
|
The interface has been configured with create-react-app and react-app-rewired so I can customize the build for the MCU. The large artefacts are gzipped and source maps and service worker are excluded.
|
||||||
|
|
||||||
|
The interface code lives in the interface directory, change to this directory with your bash shell (or Git Bash) and use the standard commands you would with any react app built with create-react-app:
|
||||||
|
|
||||||
|
Download and install the node modules:
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
Build the interface:
|
||||||
|
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
NB: The build command will also delete the previously built interface (the ./data/www directory) and replace it with the freshly built one, ready for upload to the device.
|
||||||
|
|
||||||
|
## Configuration & Deployment
|
||||||
|
|
||||||
|
TODO...
|
||||||
|
|
||||||
|
## Design Overview
|
||||||
|
|
||||||
|
TODO...
|
||||||
|
|
||||||
|
## Libraries Used
|
||||||
|
|
||||||
|
* [React](https://reactjs.org/)
|
||||||
|
* [Material-UI](https://material-ui-next.com/)
|
||||||
|
* [Time](https://github.com/PaulStoffregen/Time)
|
||||||
|
* [NtpClient](https://github.com/gmag11/NtpClient)
|
||||||
|
* [ArduinoJson](https://github.com/bblanchon/ArduinoJson)
|
||||||
|
* [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer)
|
||||||
|
|
||||||
|
Note that the project doesn't currently fix it's dependencies to a particular revision.
|
||||||
|
|
||||||
|
This may be particularly problematic for material-ui-next which is under active development and breaking changes are being made frequently.
|
5
data/config/apSettings.json
Normal file
5
data/config/apSettings.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"provision_mode": 0,
|
||||||
|
"ssid": "ESP8266-React",
|
||||||
|
"password": "esp-react"
|
||||||
|
}
|
4
data/config/ntpSettings.json
Normal file
4
data/config/ntpSettings.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"server":"pool.ntp.org",
|
||||||
|
"interval":60
|
||||||
|
}
|
5
data/config/otaSettings.json
Normal file
5
data/config/otaSettings.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"enabled":true,
|
||||||
|
"port": 8266,
|
||||||
|
"password": "esp-react"
|
||||||
|
}
|
6
data/config/wifiSettings.json
Normal file
6
data/config/wifiSettings.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ssid":"ssid",
|
||||||
|
"password":"password",
|
||||||
|
"hostname":"esp8266-react",
|
||||||
|
"static_ip_config":false
|
||||||
|
}
|
BIN
data/www/app/icon.png
Normal file
BIN
data/www/app/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
12
data/www/app/manifest.json
Normal file
12
data/www/app/manifest.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name":"ESP8266 React",
|
||||||
|
"icons":[
|
||||||
|
{
|
||||||
|
"src":"/app/icon.png",
|
||||||
|
"sizes":"48x48 72x72 96x96 128x128 256x256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url":"/",
|
||||||
|
"display":"fullscreen",
|
||||||
|
"orientation":"any"
|
||||||
|
}
|
22
data/www/css/roboto.css
Normal file
22
data/www/css/roboto.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/* Just supporting latin due to size constrains on the esp chip */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/ro-li.w2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Roboto'), local('Roboto-Regular'), url(../fonts/ro-re.w2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/ro-me.w2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
|
}
|
BIN
data/www/fonts/ro-li.w2
Normal file
BIN
data/www/fonts/ro-li.w2
Normal file
Binary file not shown.
BIN
data/www/fonts/ro-me.w2
Normal file
BIN
data/www/fonts/ro-me.w2
Normal file
Binary file not shown.
BIN
data/www/fonts/ro-re.w2
Normal file
BIN
data/www/fonts/ro-re.w2
Normal file
Binary file not shown.
BIN
data/www/index.html.gz
Normal file
BIN
data/www/index.html.gz
Normal file
Binary file not shown.
BIN
data/www/js/main.73ac.js.gz
Normal file
BIN
data/www/js/main.73ac.js.gz
Normal file
Binary file not shown.
29
interface/config-overrides.js
Normal file
29
interface/config-overrides.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const CompressionPlugin = require("compression-webpack-plugin");
|
||||||
|
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||||
|
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
module.exports = function override(config, env) {
|
||||||
|
if (env === "production") {
|
||||||
|
// rename the ouput file, we need it's path to be short, for SPIFFS
|
||||||
|
config.output.filename = 'js/[name].[chunkhash:4].js';
|
||||||
|
|
||||||
|
// disable sourcemap for production build
|
||||||
|
config.devtool = false;
|
||||||
|
|
||||||
|
// take out the manifest and service worker
|
||||||
|
config.plugins = config.plugins.filter(plugin => !(plugin instanceof ManifestPlugin));
|
||||||
|
config.plugins = config.plugins.filter(plugin => !(plugin instanceof SWPrecacheWebpackPlugin));
|
||||||
|
|
||||||
|
// add compression plugin, compress javascript, html and css
|
||||||
|
config.plugins.push(new CompressionPlugin({
|
||||||
|
asset: "[path].gz[query]",
|
||||||
|
algorithm: "gzip",
|
||||||
|
test: /\.(js|html|css)$/,
|
||||||
|
deleteOriginalAssets: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
10960
interface/package-lock.json
generated
Normal file
10960
interface/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
interface/package.json
Normal file
29
interface/package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "fresh",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"compression-webpack-plugin": "^1.1.8",
|
||||||
|
"material-ui": "^1.0.0-beta.32",
|
||||||
|
"material-ui-icons": "^1.0.0-beta.17",
|
||||||
|
"moment": "^2.20.1",
|
||||||
|
"prop-types": "^15.6.0",
|
||||||
|
"react": "^16.2.0",
|
||||||
|
"react-autosuggest": "^9.3.3",
|
||||||
|
"react-dom": "^16.2.0",
|
||||||
|
"react-form-validator-core": "^0.3.0",
|
||||||
|
"react-material-ui-form-validator": "^2.0.0-beta.4",
|
||||||
|
"react-router": "^4.2.0",
|
||||||
|
"react-router-dom": "^4.2.2",
|
||||||
|
"react-scripts": "1.0.17"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-app-rewired start",
|
||||||
|
"build": "react-app-rewired build && rm -rf ../data/www && cp -r build ../data/www",
|
||||||
|
"test": "react-app-rewired test --env=jsdom",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"react-app-rewired": "^1.4.1"
|
||||||
|
}
|
||||||
|
}
|
BIN
interface/public/app/icon.png
Normal file
BIN
interface/public/app/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
12
interface/public/app/manifest.json
Normal file
12
interface/public/app/manifest.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name":"ESP8266 React",
|
||||||
|
"icons":[
|
||||||
|
{
|
||||||
|
"src":"/app/icon.png",
|
||||||
|
"sizes":"48x48 72x72 96x96 128x128 256x256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url":"/",
|
||||||
|
"display":"fullscreen",
|
||||||
|
"orientation":"any"
|
||||||
|
}
|
22
interface/public/css/roboto.css
Normal file
22
interface/public/css/roboto.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/* Just supporting latin due to size constrains on the esp chip */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/ro-li.w2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Roboto'), local('Roboto-Regular'), url(../fonts/ro-re.w2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/ro-me.w2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
|
}
|
BIN
interface/public/fonts/ro-li.w2
Normal file
BIN
interface/public/fonts/ro-li.w2
Normal file
Binary file not shown.
BIN
interface/public/fonts/ro-me.w2
Normal file
BIN
interface/public/fonts/ro-me.w2
Normal file
Binary file not shown.
BIN
interface/public/fonts/ro-re.w2
Normal file
BIN
interface/public/fonts/ro-re.w2
Normal file
Binary file not shown.
16
interface/public/index.html
Normal file
16
interface/public/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<link rel="stylesheet" href="%PUBLIC_URL%/css/roboto.css">
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/app/manifest.json">
|
||||||
|
<title>ESP8266 React</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
You need to enable JavaScript to run this app.
|
||||||
|
</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
54
interface/src/App.js
Normal file
54
interface/src/App.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import AppRouting from './AppRouting';
|
||||||
|
|
||||||
|
import JssProvider from 'react-jss/lib/JssProvider';
|
||||||
|
import { create } from 'jss';
|
||||||
|
|
||||||
|
import Reboot from 'material-ui/Reboot';
|
||||||
|
|
||||||
|
import blueGrey from 'material-ui/colors/blueGrey';
|
||||||
|
import indigo from 'material-ui/colors/indigo';
|
||||||
|
import orange from 'material-ui/colors/orange';
|
||||||
|
import red from 'material-ui/colors/red';
|
||||||
|
import green from 'material-ui/colors/green';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MuiThemeProvider,
|
||||||
|
createMuiTheme,
|
||||||
|
createGenerateClassName,
|
||||||
|
jssPreset,
|
||||||
|
} from 'material-ui/styles';
|
||||||
|
|
||||||
|
// Our theme
|
||||||
|
const theme = createMuiTheme({
|
||||||
|
palette: {
|
||||||
|
primary: indigo,
|
||||||
|
secondary: blueGrey,
|
||||||
|
highlight_idle: blueGrey[900],
|
||||||
|
highlight_warn: orange[500],
|
||||||
|
highlight_error: red[500],
|
||||||
|
highlight_success: green[500],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSS instance
|
||||||
|
const jss = create(jssPreset());
|
||||||
|
|
||||||
|
// Class name generator.
|
||||||
|
const generateClassName = createGenerateClassName();
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<JssProvider jss={jss} generateClassName={generateClassName}>
|
||||||
|
<MuiThemeProvider theme={theme}>
|
||||||
|
<Reboot />
|
||||||
|
<AppRouting />
|
||||||
|
</MuiThemeProvider>
|
||||||
|
</JssProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
25
interface/src/AppRouting.js
Normal file
25
interface/src/AppRouting.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { Route, Redirect, Switch } from 'react-router';
|
||||||
|
|
||||||
|
// containers
|
||||||
|
import WiFiConfiguration from './containers/WiFiConfiguration';
|
||||||
|
import NTPConfiguration from './containers/NTPConfiguration';
|
||||||
|
import OTAConfiguration from './containers/OTAConfiguration';
|
||||||
|
import APConfiguration from './containers/APConfiguration';
|
||||||
|
|
||||||
|
class AppRouting extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/wifi-configuration" component={WiFiConfiguration} />
|
||||||
|
<Route exact path="/ap-configuration" component={APConfiguration} />
|
||||||
|
<Route exact path="/ntp-configuration" component={NTPConfiguration} />
|
||||||
|
<Route exact path="/ota-configuration" component={OTAConfiguration} />
|
||||||
|
<Redirect to="/wifi-configuration" />
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppRouting;
|
189
interface/src/components/MenuAppBar.js
Normal file
189
interface/src/components/MenuAppBar.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Drawer from 'material-ui/Drawer';
|
||||||
|
import AppBar from 'material-ui/AppBar';
|
||||||
|
import Toolbar from 'material-ui/Toolbar';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
import IconButton from 'material-ui/IconButton';
|
||||||
|
import Hidden from 'material-ui/Hidden';
|
||||||
|
import Divider from 'material-ui/Divider';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import List, { ListItem, ListItemIcon, ListItemText } from 'material-ui/List';
|
||||||
|
|
||||||
|
import MenuIcon from 'material-ui-icons/Menu';
|
||||||
|
import WifiIcon from 'material-ui-icons/Wifi';
|
||||||
|
import SystemUpdateIcon from 'material-ui-icons/SystemUpdate';
|
||||||
|
import AccessTimeIcon from 'material-ui-icons/AccessTime';
|
||||||
|
import SettingsInputAntennaIcon from 'material-ui-icons/SettingsInputAntenna';
|
||||||
|
|
||||||
|
const drawerWidth = 250;
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {
|
||||||
|
zIndex: 1,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
toolbar: {
|
||||||
|
paddingLeft: theme.spacing.unit,
|
||||||
|
paddingRight: theme.spacing.unit,
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
paddingLeft: theme.spacing.unit * 3,
|
||||||
|
paddingRight: theme.spacing.unit * 3,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
appFrame: {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
appBar: {
|
||||||
|
position: 'absolute',
|
||||||
|
marginLeft: drawerWidth,
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
width: `calc(100% - ${drawerWidth}px)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
navIconHide: {
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
drawerPaper: {
|
||||||
|
width: drawerWidth,
|
||||||
|
height: '100%',
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
width: drawerWidth,
|
||||||
|
position:'fixed',
|
||||||
|
left:0,
|
||||||
|
top:0,
|
||||||
|
overflow:'auto'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
width:"100%",
|
||||||
|
marginTop: 56,
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
paddingLeft: drawerWidth
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
height: 'calc(100% - 64px)',
|
||||||
|
marginTop: 64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class MenuAppBar extends React.Component {
|
||||||
|
state = {
|
||||||
|
mobileOpen: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDrawerToggle = () => {
|
||||||
|
this.setState({ mobileOpen: !this.state.mobileOpen });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes, theme, children, sectionTitle } = this.props;
|
||||||
|
|
||||||
|
const drawer = (
|
||||||
|
<div>
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="title" color="primary">
|
||||||
|
ESP8266 React
|
||||||
|
</Typography>
|
||||||
|
<Divider absolute />
|
||||||
|
</Toolbar>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
<ListItem button component={Link} to='/wifi-configuration'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<WifiIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="WiFi Configuration" />
|
||||||
|
</ListItem>
|
||||||
|
<ListItem button component={Link} to='/ap-configuration'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<SettingsInputAntennaIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="AP Configuration" />
|
||||||
|
</ListItem>
|
||||||
|
<ListItem button component={Link} to='/ntp-configuration'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<AccessTimeIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="NTP Configuration" />
|
||||||
|
</ListItem>
|
||||||
|
<ListItem button component={Link} to='/ota-configuration'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<SystemUpdateIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="OTA Configuration" />
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<div className={classes.appFrame}>
|
||||||
|
<AppBar className={classes.appBar}>
|
||||||
|
<Toolbar className={classes.toolbar} disableGutters={true}>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
aria-label="open drawer"
|
||||||
|
onClick={this.handleDrawerToggle}
|
||||||
|
className={classes.navIconHide}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="title" color="inherit" noWrap>
|
||||||
|
{sectionTitle}
|
||||||
|
</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<Hidden mdUp>
|
||||||
|
<Drawer
|
||||||
|
variant="temporary"
|
||||||
|
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
|
||||||
|
open={this.state.mobileOpen}
|
||||||
|
classes={{
|
||||||
|
paper: classes.drawerPaper,
|
||||||
|
}}
|
||||||
|
onClose={this.handleDrawerToggle}
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true, // Better open performance on mobile.
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{drawer}
|
||||||
|
</Drawer>
|
||||||
|
</Hidden>
|
||||||
|
<Hidden smDown implementation="css">
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
open
|
||||||
|
classes={{
|
||||||
|
paper: classes.drawerPaper,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{drawer}
|
||||||
|
</Drawer>
|
||||||
|
</Hidden>
|
||||||
|
<main className={classes.content}>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuAppBar.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
theme: PropTypes.object.isRequired,
|
||||||
|
sectionTitle: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles, { withTheme: true })(MenuAppBar);
|
36
interface/src/components/SectionContent.js
Normal file
36
interface/src/components/SectionContent.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Paper from 'material-ui/Paper';
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
content: {
|
||||||
|
padding: theme.spacing.unit * 2,
|
||||||
|
margin: theme.spacing.unit * 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function SectionContent(props) {
|
||||||
|
const { children, classes, title } = props;
|
||||||
|
return (
|
||||||
|
<Paper className={classes.content}>
|
||||||
|
<Typography variant="display1">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionContent.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node
|
||||||
|
]).isRequired,
|
||||||
|
title: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(SectionContent);
|
74
interface/src/components/SnackbarNotification.js
Normal file
74
interface/src/components/SnackbarNotification.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Snackbar from 'material-ui/Snackbar';
|
||||||
|
import IconButton from 'material-ui/IconButton';
|
||||||
|
import CloseIcon from 'material-ui-icons/Close';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
close: {
|
||||||
|
width: theme.spacing.unit * 4,
|
||||||
|
height: theme.spacing.unit * 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class SnackbarNotification extends React.Component {
|
||||||
|
state = {
|
||||||
|
open: false,
|
||||||
|
message: null
|
||||||
|
};
|
||||||
|
|
||||||
|
raiseNotification = (message) => {
|
||||||
|
this.setState({ open: true, message:message });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClose = (event, reason) => {
|
||||||
|
if (reason === 'clickaway') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ open: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps){
|
||||||
|
if (nextProps.notificationRef){
|
||||||
|
nextProps.notificationRef(this.raiseNotification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<Snackbar
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
open={this.state.open}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={this.handleClose}
|
||||||
|
SnackbarContentProps={{
|
||||||
|
'aria-describedby': 'message-id',
|
||||||
|
}}
|
||||||
|
message={<span id="message-id">{this.state.message}</span>}
|
||||||
|
action={
|
||||||
|
<IconButton
|
||||||
|
key="close"
|
||||||
|
aria-label="Close"
|
||||||
|
color="inherit"
|
||||||
|
className={classes.close}
|
||||||
|
onClick={this.handleClose}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SnackbarNotification.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
notificationRef: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(SnackbarNotification);
|
28
interface/src/constants/Endpoints.js
Normal file
28
interface/src/constants/Endpoints.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export const ENDPOINT_ROOT = "";
|
||||||
|
|
||||||
|
export const NTP_STATUS_PATH = "/ntpStatus";
|
||||||
|
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + NTP_STATUS_PATH;
|
||||||
|
|
||||||
|
export const NTP_SETTINGS_PATH = "/ntpSettings";
|
||||||
|
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + NTP_SETTINGS_PATH;
|
||||||
|
|
||||||
|
export const AP_SETTINGS_PATH = "/apSettings";
|
||||||
|
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + AP_SETTINGS_PATH;
|
||||||
|
|
||||||
|
export const AP_STATUS_PATH = "/apStatus";
|
||||||
|
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + AP_STATUS_PATH;
|
||||||
|
|
||||||
|
export const SCAN_NETWORKS_PATH = "/scanNetworks";
|
||||||
|
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + SCAN_NETWORKS_PATH;
|
||||||
|
|
||||||
|
export const LIST_NETWORKS_PATH = "/listNetworks";
|
||||||
|
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + LIST_NETWORKS_PATH;
|
||||||
|
|
||||||
|
export const WIFI_SETTINGS_PATH = "/wifiSettings";
|
||||||
|
export const WIFI_SETTINGS_ENDPOINT = ENDPOINT_ROOT + WIFI_SETTINGS_PATH;
|
||||||
|
|
||||||
|
export const WIFI_STATUS_PATH = "/wifiStatus";
|
||||||
|
export const WIFI_STATUS_ENDPOINT = ENDPOINT_ROOT + WIFI_STATUS_PATH;
|
||||||
|
|
||||||
|
export const OTA_SETTINGS_PATH = "/otaSettings";
|
||||||
|
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + OTA_SETTINGS_PATH;
|
4
interface/src/constants/Highlight.js
Normal file
4
interface/src/constants/Highlight.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const IDLE = "idle";
|
||||||
|
export const SUCCESS = "success";
|
||||||
|
export const ERROR = "error";
|
||||||
|
export const WARN = "warn";
|
32
interface/src/constants/NTPStatus.js
Normal file
32
interface/src/constants/NTPStatus.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import * as Highlight from '../constants/Highlight';
|
||||||
|
|
||||||
|
export const NTP_TIME_NOT_SET = 0;
|
||||||
|
export const NTP_TIME_NEEDS_SYNC = 1;
|
||||||
|
export const NTP_TIME_SET = 2;
|
||||||
|
|
||||||
|
export const isSynchronized = ntpStatus => ntpStatus && (ntpStatus.status === NTP_TIME_NEEDS_SYNC || ntpStatus.status === NTP_TIME_SET);
|
||||||
|
|
||||||
|
export const ntpStatusHighlight = ntpStatus => {
|
||||||
|
switch (ntpStatus.status){
|
||||||
|
case NTP_TIME_SET:
|
||||||
|
return Highlight.SUCCESS;
|
||||||
|
case NTP_TIME_NEEDS_SYNC:
|
||||||
|
return Highlight.WARN;
|
||||||
|
case NTP_TIME_NOT_SET:
|
||||||
|
default:
|
||||||
|
return Highlight.ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ntpStatus = ntpStatus => {
|
||||||
|
switch (ntpStatus.status){
|
||||||
|
case NTP_TIME_SET:
|
||||||
|
return "Synchronized";
|
||||||
|
case NTP_TIME_NEEDS_SYNC:
|
||||||
|
return "Synchronization required";
|
||||||
|
case NTP_TIME_NOT_SET:
|
||||||
|
return "Time not set"
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
4
interface/src/constants/TimeFormat.js
Normal file
4
interface/src/constants/TimeFormat.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export const TIME_AND_DATE = 'DD/MM/YYYY HH:mm:ss';
|
||||||
|
export const unixTimeToTimeAndDate = unixTime => moment.unix(unixTime).format(TIME_AND_DATE);
|
5
interface/src/constants/WiFiAPModes.js
Normal file
5
interface/src/constants/WiFiAPModes.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const WIFI_AP_MODE_ALWAYS = 0;
|
||||||
|
export const WIFI_AP_MODE_DISCONNECTED = 1;
|
||||||
|
export const WIFI_AP_NEVER = 2;
|
||||||
|
|
||||||
|
export const isAPEnabled = apMode => apMode === WIFI_AP_MODE_ALWAYS || apMode === WIFI_AP_MODE_DISCONNECTED;
|
44
interface/src/constants/WiFiConnectionStatus.js
Normal file
44
interface/src/constants/WiFiConnectionStatus.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import * as Highlight from '../constants/Highlight';
|
||||||
|
|
||||||
|
export const WIFI_STATUS_IDLE = 0;
|
||||||
|
export const WIFI_STATUS_NO_SSID_AVAIL = 1;
|
||||||
|
export const WIFI_STATUS_CONNECTED = 3;
|
||||||
|
export const WIFI_STATUS_CONNECT_FAILED = 4;
|
||||||
|
export const WIFI_STATUS_CONNECTION_LOST = 5;
|
||||||
|
export const WIFI_STATUS_DISCONNECTED = 6;
|
||||||
|
|
||||||
|
export const isConnected = wifiStatus => wifiStatus && wifiStatus.status === WIFI_STATUS_CONNECTED;
|
||||||
|
|
||||||
|
export const connectionStatusHighlight = wifiStatus => {
|
||||||
|
switch (wifiStatus.status){
|
||||||
|
case WIFI_STATUS_IDLE:
|
||||||
|
case WIFI_STATUS_DISCONNECTED:
|
||||||
|
return Highlight.IDLE;
|
||||||
|
case WIFI_STATUS_CONNECTED:
|
||||||
|
return Highlight.SUCCESS;
|
||||||
|
case WIFI_STATUS_CONNECT_FAILED:
|
||||||
|
case WIFI_STATUS_CONNECTION_LOST:
|
||||||
|
return Highlight.ERROR;
|
||||||
|
default:
|
||||||
|
return Highlight.WARN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const connectionStatus = wifiStatus => {
|
||||||
|
switch (wifiStatus.status){
|
||||||
|
case WIFI_STATUS_IDLE:
|
||||||
|
return "Idle";
|
||||||
|
case WIFI_STATUS_NO_SSID_AVAIL:
|
||||||
|
return "No SSID Available";
|
||||||
|
case WIFI_STATUS_CONNECTED:
|
||||||
|
return "Connected";
|
||||||
|
case WIFI_STATUS_CONNECT_FAILED:
|
||||||
|
return "Connection Failed";
|
||||||
|
case WIFI_STATUS_CONNECTION_LOST:
|
||||||
|
return "Connection Lost";
|
||||||
|
case WIFI_STATUS_DISCONNECTED:
|
||||||
|
return "Disconnected";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
23
interface/src/constants/WiFiSecurityModes.js
Normal file
23
interface/src/constants/WiFiSecurityModes.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export const WIFI_SECURITY_WPA_TKIP = 2;
|
||||||
|
export const WIFI_SECURITY_WEP = 5;
|
||||||
|
export const WIFI_SECURITY_WPA_CCMP = 4;
|
||||||
|
export const WIFI_SECURITY_NONE = 7;
|
||||||
|
export const WIFI_SECURITY_AUTO = 8;
|
||||||
|
|
||||||
|
export const isNetworkOpen = selectedNetwork => selectedNetwork && selectedNetwork.encryption_type === WIFI_SECURITY_NONE;
|
||||||
|
|
||||||
|
export const networkSecurityMode = selectedNetwork => {
|
||||||
|
switch (selectedNetwork.encryption_type){
|
||||||
|
case WIFI_SECURITY_WPA_TKIP:
|
||||||
|
case WIFI_SECURITY_WPA_CCMP:
|
||||||
|
return "WPA";
|
||||||
|
case WIFI_SECURITY_WEP:
|
||||||
|
return "WEP";
|
||||||
|
case WIFI_SECURITY_AUTO:
|
||||||
|
return "Auto";
|
||||||
|
case WIFI_SECURITY_NONE:
|
||||||
|
return "None";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
48
interface/src/containers/APConfiguration.js
Normal file
48
interface/src/containers/APConfiguration.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import Tabs, { Tab } from 'material-ui/Tabs';
|
||||||
|
|
||||||
|
import MenuAppBar from '../components/MenuAppBar';
|
||||||
|
import APSettings from './APSettings';
|
||||||
|
import APStatus from './APStatus';
|
||||||
|
|
||||||
|
class APConfiguration extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
selectedTab: "apStatus",
|
||||||
|
selectedNetwork: null
|
||||||
|
};
|
||||||
|
this.selectNetwork = this.selectNetwork.bind(this);
|
||||||
|
this.deselectNetwork = this.deselectNetwork.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNetwork(network) {
|
||||||
|
this.setState({ selectedTab: "wifiSettings", selectedNetwork:network });
|
||||||
|
}
|
||||||
|
|
||||||
|
deselectNetwork(network) {
|
||||||
|
this.setState({ selectedNetwork:null });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabChange = (event, selectedTab) => {
|
||||||
|
this.setState({ selectedTab });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { selectedTab } = this.state;
|
||||||
|
return (
|
||||||
|
<MenuAppBar sectionTitle="AP Configuration">
|
||||||
|
<Tabs value={selectedTab} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" fullWidth centered scrollable>
|
||||||
|
<Tab value="apStatus" label="AP Status" />
|
||||||
|
<Tab value="apSettings" label="AP Settings" />
|
||||||
|
</Tabs>
|
||||||
|
{selectedTab === "apStatus" && <APStatus fullDetails={true} />}
|
||||||
|
{selectedTab === "apSettings" && <APSettings />}
|
||||||
|
</MenuAppBar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default APConfiguration;
|
70
interface/src/containers/APSettings.js
Normal file
70
interface/src/containers/APSettings.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { AP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
||||||
|
import SectionContent from '../components/SectionContent';
|
||||||
|
import SnackbarNotification from '../components/SnackbarNotification';
|
||||||
|
import APSettingsForm from '../forms/APSettingsForm';
|
||||||
|
import { simpleGet } from '../helpers/SimpleGet';
|
||||||
|
import { simplePost } from '../helpers/SimplePost';
|
||||||
|
|
||||||
|
class APSettings extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
apSettings:null,
|
||||||
|
apSettingsFetched: false,
|
||||||
|
errorMessage:null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState = this.setState.bind(this);
|
||||||
|
this.loadAPSettings = this.loadAPSettings.bind(this);
|
||||||
|
this.saveAPSettings = this.saveAPSettings.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadAPSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAPSettings() {
|
||||||
|
simpleGet(
|
||||||
|
AP_SETTINGS_ENDPOINT,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification,
|
||||||
|
"apSettings",
|
||||||
|
"apSettingsFetched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAPSettings(e) {
|
||||||
|
simplePost(
|
||||||
|
AP_SETTINGS_ENDPOINT,
|
||||||
|
this.state,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification,
|
||||||
|
"apSettings",
|
||||||
|
"apSettingsFetched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wifiSettingValueChange = name => event => {
|
||||||
|
const { apSettings } = this.state;
|
||||||
|
apSettings[name] = event.target.value;
|
||||||
|
this.setState({apSettings});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { apSettingsFetched, apSettings, errorMessage } = this.state;
|
||||||
|
return (
|
||||||
|
<SectionContent title="AP Settings">
|
||||||
|
<SnackbarNotification notificationRef={(raiseNotification)=>this.raiseNotification = raiseNotification} />
|
||||||
|
<APSettingsForm apSettingsFetched={apSettingsFetched} apSettings={apSettings} errorMessage={errorMessage}
|
||||||
|
onSubmit={this.saveAPSettings} onReset={this.loadAPSettings} handleValueChange={this.wifiSettingValueChange} />
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default APSettings;
|
165
interface/src/containers/APStatus.js
Normal file
165
interface/src/containers/APStatus.js
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Button from 'material-ui/Button';
|
||||||
|
import { LinearProgress } from 'material-ui/Progress';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
import List, { ListItem, ListItemText } from 'material-ui/List';
|
||||||
|
import Avatar from 'material-ui/Avatar';
|
||||||
|
import Divider from 'material-ui/Divider';
|
||||||
|
import SettingsInputAntennaIcon from 'material-ui-icons/SettingsInputAntenna';
|
||||||
|
import DeviceHubIcon from 'material-ui-icons/DeviceHub';
|
||||||
|
import ComputerIcon from 'material-ui-icons/Computer';
|
||||||
|
|
||||||
|
import SnackbarNotification from '../components/SnackbarNotification'
|
||||||
|
import SectionContent from '../components/SectionContent'
|
||||||
|
|
||||||
|
import * as Highlight from '../constants/Highlight';
|
||||||
|
import { AP_STATUS_ENDPOINT } from '../constants/Endpoints';
|
||||||
|
import { simpleGet } from '../helpers/SimpleGet';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
["apStatus_" + Highlight.SUCCESS]: {
|
||||||
|
backgroundColor: theme.palette.highlight_success
|
||||||
|
},
|
||||||
|
["apStatus_" + Highlight.IDLE]: {
|
||||||
|
backgroundColor: theme.palette.highlight_idle
|
||||||
|
},
|
||||||
|
fetching: {
|
||||||
|
margin: theme.spacing.unit * 4,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing.unit * 2,
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class APStatus extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
status:null,
|
||||||
|
fetched: false,
|
||||||
|
errorMessage:null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState = this.setState.bind(this);
|
||||||
|
this.loadAPStatus = this.loadAPStatus.bind(this);
|
||||||
|
this.raiseNotification=this.raiseNotification.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadAPStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAPStatus() {
|
||||||
|
simpleGet(
|
||||||
|
AP_STATUS_ENDPOINT,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
raiseNotification(errorMessage) {
|
||||||
|
this.snackbarNotification(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
apStatusHighlight(status){
|
||||||
|
return status.active ? Highlight.SUCCESS : Highlight.IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
apStatus(status){
|
||||||
|
return status.active ? "Active" : "Inactive";
|
||||||
|
}
|
||||||
|
|
||||||
|
// active, ip_address, mac_address, station_num
|
||||||
|
|
||||||
|
renderAPStatus(status, fullDetails, classes){
|
||||||
|
const listItems = [];
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="ap_status">
|
||||||
|
<Avatar className={classes["apStatus_" + this.apStatusHighlight(status)]}>
|
||||||
|
<SettingsInputAntennaIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="Status" secondary={this.apStatus(status)} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="ap_status_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="ip_address">
|
||||||
|
<Avatar>IP</Avatar>
|
||||||
|
<ListItemText primary="IP Address" secondary={status.ip_address} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="ip_address_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="mac_address">
|
||||||
|
<Avatar>
|
||||||
|
<DeviceHubIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="MAC Address" secondary={status.mac_address} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="mac_address_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="station_num">
|
||||||
|
<Avatar>
|
||||||
|
<ComputerIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="AP Clients" secondary={status.station_num} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="mac_address_divider" inset component="li" />);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<List>
|
||||||
|
{listItems}
|
||||||
|
</List>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={this.loadAPStatus}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { status, fetched, errorMessage } = this.state;
|
||||||
|
const { classes, fullDetails } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent title="AP Status">
|
||||||
|
<SnackbarNotification notificationRef={(snackbarNotification)=>this.snackbarNotification = snackbarNotification} />
|
||||||
|
{
|
||||||
|
!fetched ?
|
||||||
|
<div>
|
||||||
|
<LinearProgress className={classes.fetching}/>
|
||||||
|
<Typography variant="display1" className={classes.fetching}>
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
status ? this.renderAPStatus(status, fullDetails, classes)
|
||||||
|
:
|
||||||
|
<div>
|
||||||
|
<Typography variant="display1" className={classes.fetching}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={this.loadAPStatus}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(APStatus);
|
37
interface/src/containers/NTPConfiguration.js
Normal file
37
interface/src/containers/NTPConfiguration.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import MenuAppBar from '../components/MenuAppBar';
|
||||||
|
import NTPSettings from './NTPSettings';
|
||||||
|
import NTPStatus from './NTPStatus';
|
||||||
|
|
||||||
|
import Tabs, { Tab } from 'material-ui/Tabs';
|
||||||
|
|
||||||
|
class NTPConfiguration extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
selectedTab: "ntpStatus"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleTabChange = (event, selectedTab) => {
|
||||||
|
this.setState({ selectedTab });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { selectedTab } = this.state;
|
||||||
|
return (
|
||||||
|
<MenuAppBar sectionTitle="NTP Configuration">
|
||||||
|
<Tabs value={selectedTab} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" fullWidth centered scrollable>
|
||||||
|
<Tab value="ntpStatus" label="NTP Status" />
|
||||||
|
<Tab value="ntpSettings" label="NTP Settings" />
|
||||||
|
</Tabs>
|
||||||
|
{selectedTab === "ntpStatus" && <NTPStatus />}
|
||||||
|
{selectedTab === "ntpSettings" && <NTPSettings />}
|
||||||
|
</MenuAppBar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NTPConfiguration
|
70
interface/src/containers/NTPSettings.js
Normal file
70
interface/src/containers/NTPSettings.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { NTP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
||||||
|
import SectionContent from '../components/SectionContent';
|
||||||
|
import SnackbarNotification from '../components/SnackbarNotification';
|
||||||
|
import NTPSettingsForm from '../forms/NTPSettingsForm';
|
||||||
|
import { simpleGet } from '../helpers/SimpleGet';
|
||||||
|
import { simplePost } from '../helpers/SimplePost';
|
||||||
|
|
||||||
|
class NTPSettings extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
ntpSettings:{},
|
||||||
|
ntpSettingsFetched: false,
|
||||||
|
errorMessage:null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState = this.setState.bind(this);
|
||||||
|
this.loadNTPSettings = this.loadNTPSettings.bind(this);
|
||||||
|
this.saveNTPSettings = this.saveNTPSettings.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadNTPSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNTPSettings() {
|
||||||
|
simpleGet(
|
||||||
|
NTP_SETTINGS_ENDPOINT,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification,
|
||||||
|
"ntpSettings",
|
||||||
|
"ntpSettingsFetched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveNTPSettings(e) {
|
||||||
|
simplePost(
|
||||||
|
NTP_SETTINGS_ENDPOINT,
|
||||||
|
this.state,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification,
|
||||||
|
"ntpSettings",
|
||||||
|
"ntpSettingsFetched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ntpSettingValueChange = name => event => {
|
||||||
|
const { ntpSettings } = this.state;
|
||||||
|
ntpSettings[name] = event.target.value;
|
||||||
|
this.setState({ntpSettings});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { ntpSettingsFetched, ntpSettings, errorMessage } = this.state;
|
||||||
|
return (
|
||||||
|
<SectionContent title="NTP Settings">
|
||||||
|
<SnackbarNotification notificationRef={(raiseNotification)=>this.raiseNotification = raiseNotification} />
|
||||||
|
<NTPSettingsForm ntpSettingsFetched={ntpSettingsFetched} ntpSettings={ntpSettings} errorMessage={errorMessage}
|
||||||
|
onSubmit={this.saveNTPSettings} onReset={this.loadNTPSettings} handleValueChange={this.ntpSettingValueChange} />
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NTPSettings;
|
189
interface/src/containers/NTPStatus.js
Normal file
189
interface/src/containers/NTPStatus.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Button from 'material-ui/Button';
|
||||||
|
import { LinearProgress } from 'material-ui/Progress';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
import List, { ListItem, ListItemText } from 'material-ui/List';
|
||||||
|
import Avatar from 'material-ui/Avatar';
|
||||||
|
import Divider from 'material-ui/Divider';
|
||||||
|
import SwapVerticalCircleIcon from 'material-ui-icons/SwapVerticalCircle';
|
||||||
|
import AccessTimeIcon from 'material-ui-icons/AccessTime';
|
||||||
|
import DNSIcon from 'material-ui-icons/Dns';
|
||||||
|
import TimerIcon from 'material-ui-icons/Timer';
|
||||||
|
import UpdateIcon from 'material-ui-icons/Update';
|
||||||
|
import AvTimerIcon from 'material-ui-icons/AvTimer';
|
||||||
|
|
||||||
|
import { isSynchronized, ntpStatusHighlight, ntpStatus } from '../constants/NTPStatus';
|
||||||
|
import * as Highlight from '../constants/Highlight';
|
||||||
|
import { unixTimeToTimeAndDate } from '../constants/TimeFormat';
|
||||||
|
import { NTP_STATUS_ENDPOINT } from '../constants/Endpoints';
|
||||||
|
|
||||||
|
import SnackbarNotification from '../components/SnackbarNotification';
|
||||||
|
import SectionContent from '../components/SectionContent';
|
||||||
|
|
||||||
|
import { simpleGet } from '../helpers/SimpleGet';
|
||||||
|
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
["ntpStatus_" + Highlight.SUCCESS]: {
|
||||||
|
backgroundColor: theme.palette.highlight_success
|
||||||
|
},
|
||||||
|
["ntpStatus_" + Highlight.ERROR]: {
|
||||||
|
backgroundColor: theme.palette.highlight_error
|
||||||
|
},
|
||||||
|
["ntpStatus_" + Highlight.WARN]: {
|
||||||
|
backgroundColor: theme.palette.highlight_warn
|
||||||
|
},
|
||||||
|
fetching: {
|
||||||
|
margin: theme.spacing.unit * 4,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing.unit * 2,
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class NTPStatus extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
status:null,
|
||||||
|
fetched: false,
|
||||||
|
errorMessage:null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState = this.setState.bind(this);
|
||||||
|
this.loadNTPStatus = this.loadNTPStatus.bind(this);
|
||||||
|
this.raiseNotification=this.raiseNotification.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadNTPStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNTPStatus() {
|
||||||
|
simpleGet(
|
||||||
|
NTP_STATUS_ENDPOINT,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
raiseNotification(errorMessage) {
|
||||||
|
this.snackbarNotification(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNTPStatus(status, fullDetails, classes){
|
||||||
|
const listItems = [];
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="ntp_status">
|
||||||
|
<Avatar className={classes["ntpStatus_" + ntpStatusHighlight(status)]}>
|
||||||
|
<UpdateIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="Status" secondary={ntpStatus(status)} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="ntp_status_divider" inset component="li" />);
|
||||||
|
|
||||||
|
if (isSynchronized(status)) {
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="time_now">
|
||||||
|
<Avatar>
|
||||||
|
<AccessTimeIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="Time Now" secondary={unixTimeToTimeAndDate(status.now)} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="time_now_divider" inset component="li" />);
|
||||||
|
}
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="last_sync">
|
||||||
|
<Avatar>
|
||||||
|
<SwapVerticalCircleIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="Last Sync" secondary={status.last_sync > 0 ? unixTimeToTimeAndDate(status.last_sync) : "never" } />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="last_sync_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="ntp_server">
|
||||||
|
<Avatar>
|
||||||
|
<DNSIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="NTP Server" secondary={status.server} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="ntp_server_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="sync_interval">
|
||||||
|
<Avatar>
|
||||||
|
<TimerIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="Sync Interval" secondary={moment.duration(status.interval, 'seconds').humanize()} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="sync_interval_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="uptime">
|
||||||
|
<Avatar>
|
||||||
|
<AvTimerIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="Uptime" secondary={moment.duration(status.uptime, 'seconds').humanize()} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<List>
|
||||||
|
{listItems}
|
||||||
|
</List>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={this.loadNTPStatus}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { status, fetched, errorMessage } = this.state;
|
||||||
|
const { classes, fullDetails } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent title="NTP Status">
|
||||||
|
<SnackbarNotification notificationRef={(snackbarNotification)=>this.snackbarNotification = snackbarNotification} />
|
||||||
|
{
|
||||||
|
!fetched ?
|
||||||
|
<div>
|
||||||
|
<LinearProgress className={classes.fetching}/>
|
||||||
|
<Typography variant="display1" className={classes.fetching}>
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
status ? this.renderNTPStatus(status, fullDetails, classes)
|
||||||
|
:
|
||||||
|
<div>
|
||||||
|
<Typography variant="display1" className={classes.fetching}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={this.loadNTPStatus}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(NTPStatus);
|
15
interface/src/containers/OTAConfiguration.js
Normal file
15
interface/src/containers/OTAConfiguration.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import MenuAppBar from '../components/MenuAppBar';
|
||||||
|
import OTASettings from './OTASettings';
|
||||||
|
|
||||||
|
class OTAConfiguration extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<MenuAppBar sectionTitle="OTA Configuration">
|
||||||
|
<OTASettings />
|
||||||
|
</MenuAppBar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OTAConfiguration
|
77
interface/src/containers/OTASettings.js
Normal file
77
interface/src/containers/OTASettings.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { OTA_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
||||||
|
import SectionContent from '../components/SectionContent';
|
||||||
|
import SnackbarNotification from '../components/SnackbarNotification';
|
||||||
|
import OTASettingsForm from '../forms/OTASettingsForm';
|
||||||
|
import { simpleGet } from '../helpers/SimpleGet';
|
||||||
|
import { simplePost } from '../helpers/SimplePost';
|
||||||
|
|
||||||
|
class OTASettings extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
otaSettings:null,
|
||||||
|
otaSettingsFetched: false,
|
||||||
|
errorMessage:null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState = this.setState.bind(this);
|
||||||
|
this.loadOTASettings = this.loadOTASettings.bind(this);
|
||||||
|
this.saveOTASettings = this.saveOTASettings.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadOTASettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOTASettings() {
|
||||||
|
simpleGet(
|
||||||
|
OTA_SETTINGS_ENDPOINT,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification,
|
||||||
|
"otaSettings",
|
||||||
|
"otaSettingsFetched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveOTASettings(e) {
|
||||||
|
simplePost(
|
||||||
|
OTA_SETTINGS_ENDPOINT,
|
||||||
|
this.state,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification,
|
||||||
|
"otaSettings",
|
||||||
|
"otaSettingsFetched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
otaSettingValueChange = name => event => {
|
||||||
|
const { otaSettings } = this.state;
|
||||||
|
otaSettings[name] = event.target.value;
|
||||||
|
this.setState({otaSettings});
|
||||||
|
};
|
||||||
|
|
||||||
|
otaSettingCheckboxChange = name => event => {
|
||||||
|
const { otaSettings } = this.state;
|
||||||
|
otaSettings[name] = event.target.checked;
|
||||||
|
this.setState({otaSettings});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { otaSettingsFetched, otaSettings, errorMessage } = this.state;
|
||||||
|
return (
|
||||||
|
<SectionContent title="OTA Settings">
|
||||||
|
<SnackbarNotification notificationRef={(raiseNotification)=>this.raiseNotification = raiseNotification} />
|
||||||
|
<OTASettingsForm otaSettingsFetched={otaSettingsFetched} otaSettings={otaSettings} errorMessage={errorMessage}
|
||||||
|
onSubmit={this.saveOTASettings} onReset={this.loadOTASettings} handleValueChange={this.otaSettingValueChange}
|
||||||
|
handleCheckboxChange={this.otaSettingCheckboxChange} />
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OTASettings;
|
53
interface/src/containers/WiFiConfiguration.js
Normal file
53
interface/src/containers/WiFiConfiguration.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import Tabs, { Tab } from 'material-ui/Tabs';
|
||||||
|
|
||||||
|
import MenuAppBar from '../components/MenuAppBar';
|
||||||
|
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||||
|
import WiFiSettings from './WiFiSettings';
|
||||||
|
import WiFiStatus from './WiFiStatus';
|
||||||
|
|
||||||
|
class WiFiConfiguration extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
selectedTab: "wifiStatus",
|
||||||
|
selectedNetwork: null
|
||||||
|
};
|
||||||
|
this.selectNetwork = this.selectNetwork.bind(this);
|
||||||
|
this.deselectNetwork = this.deselectNetwork.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - slightly inapproperate use of callback ref possibly.
|
||||||
|
selectNetwork(network) {
|
||||||
|
this.setState({ selectedTab: "wifiSettings", selectedNetwork:network });
|
||||||
|
}
|
||||||
|
|
||||||
|
// deselects the network after the settings component mounts.
|
||||||
|
deselectNetwork(network) {
|
||||||
|
this.setState({ selectedNetwork:null });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabChange = (event, selectedTab) => {
|
||||||
|
this.setState({ selectedTab });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { selectedTab } = this.state;
|
||||||
|
return (
|
||||||
|
<MenuAppBar sectionTitle="WiFi Configuration">
|
||||||
|
<Tabs value={selectedTab} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" fullWidth centered scrollable>
|
||||||
|
<Tab value="wifiStatus" label="WiFi Status" />
|
||||||
|
<Tab value="networkScanner" label="Network Scanner" />
|
||||||
|
<Tab value="wifiSettings" label="WiFi Settings" />
|
||||||
|
</Tabs>
|
||||||
|
{selectedTab === "wifiStatus" && <WiFiStatus fullDetails={true} />}
|
||||||
|
{selectedTab === "networkScanner" && <WiFiNetworkScanner selectNetwork={this.selectNetwork} />}
|
||||||
|
{selectedTab === "wifiSettings" && <WiFiSettings deselectNetwork={this.deselectNetwork} selectedNetwork={this.state.selectedNetwork} />}
|
||||||
|
</MenuAppBar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WiFiConfiguration;
|
118
interface/src/containers/WiFiNetworkScanner.js
Normal file
118
interface/src/containers/WiFiNetworkScanner.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../constants/Endpoints';
|
||||||
|
import SectionContent from '../components/SectionContent';
|
||||||
|
import WiFiNetworkSelector from '../forms/WiFiNetworkSelector';
|
||||||
|
|
||||||
|
const NUM_POLLS = 10
|
||||||
|
const POLLING_FREQUENCY = 500
|
||||||
|
const RETRY_EXCEPTION_TYPE = "retry"
|
||||||
|
|
||||||
|
class WiFiNetworkScanner extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.pollCount = 0;
|
||||||
|
this.state = {
|
||||||
|
scanningForNetworks: true,
|
||||||
|
errorMessage:null,
|
||||||
|
networkList: null
|
||||||
|
};
|
||||||
|
this.pollNetworkList = this.pollNetworkList.bind(this);
|
||||||
|
this.requestNetworkScan = this.requestNetworkScan.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.scanNetworks();
|
||||||
|
}
|
||||||
|
|
||||||
|
requestNetworkScan() {
|
||||||
|
const { scanningForNetworks } = this.state;
|
||||||
|
if (!scanningForNetworks) {
|
||||||
|
this.scanNetworks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scanNetworks() {
|
||||||
|
this.pollCount = 0;
|
||||||
|
this.setState({scanningForNetworks:true, networkList: null, errorMessage:null});
|
||||||
|
fetch(SCAN_NETWORKS_ENDPOINT).then(response => {
|
||||||
|
if (response.status === 202) {
|
||||||
|
this.schedulePollTimeout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw Error("Scanning for networks returned unexpected response code: " + response.status);
|
||||||
|
}).catch(error => {
|
||||||
|
this.setState({scanningForNetworks:false, networkList: null, errorMessage:error.message});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
schedulePollTimeout() {
|
||||||
|
setTimeout(this.pollNetworkList, POLLING_FREQUENCY);
|
||||||
|
}
|
||||||
|
|
||||||
|
retryError() {
|
||||||
|
return {
|
||||||
|
name:RETRY_EXCEPTION_TYPE,
|
||||||
|
message:"Network list not ready, will retry in " + POLLING_FREQUENCY + "ms."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
compareNetworks(network1,network2) {
|
||||||
|
if (network1.rssi < network2.rssi)
|
||||||
|
return 1;
|
||||||
|
if (network1.rssi > network2.rssi)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pollNetworkList() {
|
||||||
|
fetch(LIST_NETWORKS_ENDPOINT)
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
if (response.status === 202) {
|
||||||
|
if (++this.pollCount < NUM_POLLS){
|
||||||
|
this.schedulePollTimeout();
|
||||||
|
throw this.retryError();
|
||||||
|
}else{
|
||||||
|
throw Error("Device did not return network list in timley manner.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Error("Device returned unexpected response code: " + response.status);
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
|
json.networks.sort(this.compareNetworks)
|
||||||
|
this.setState({scanningForNetworks:false, networkList: json, errorMessage:null})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error.message);
|
||||||
|
if (error.name !== RETRY_EXCEPTION_TYPE) {
|
||||||
|
this.setState({scanningForNetworks:false, networkList: null, errorMessage:error.message});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { scanningForNetworks, networkList, errorMessage } = this.state;
|
||||||
|
return (
|
||||||
|
<SectionContent title="Network Scanner">
|
||||||
|
<WiFiNetworkSelector scanningForNetworks={scanningForNetworks}
|
||||||
|
networkList={networkList}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
requestNetworkScan={this.requestNetworkScan}
|
||||||
|
selectNetwork={this.props.selectNetwork}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiNetworkScanner.propTypes = {
|
||||||
|
selectNetwork: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (WiFiNetworkScanner);
|
102
interface/src/containers/WiFiSettings.js
Normal file
102
interface/src/containers/WiFiSettings.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { WIFI_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
||||||
|
import SectionContent from '../components/SectionContent';
|
||||||
|
import SnackbarNotification from '../components/SnackbarNotification';
|
||||||
|
import WiFiSettingsForm from '../forms/WiFiSettingsForm';
|
||||||
|
import { simpleGet } from '../helpers/SimpleGet';
|
||||||
|
import { simplePost } from '../helpers/SimplePost';
|
||||||
|
|
||||||
|
class WiFiSettings extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
wifiSettingsFetched: false,
|
||||||
|
wifiSettings:{},
|
||||||
|
selectedNetwork: null,
|
||||||
|
errorMessage:null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState = this.setState.bind(this);
|
||||||
|
this.loadWiFiSettings = this.loadWiFiSettings.bind(this);
|
||||||
|
this.saveWiFiSettings = this.saveWiFiSettings.bind(this);
|
||||||
|
this.deselectNetwork = this.deselectNetwork.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { selectedNetwork, deselectNetwork } = this.props;
|
||||||
|
if (selectedNetwork){
|
||||||
|
var wifiSettings = {
|
||||||
|
ssid:selectedNetwork.ssid,
|
||||||
|
password:"",
|
||||||
|
hostname:"esp8266-react",
|
||||||
|
static_ip_config:false,
|
||||||
|
}
|
||||||
|
this.setState({wifiSettingsFetched:true, wifiSettings, selectedNetwork, errorMessage:null});
|
||||||
|
deselectNetwork();
|
||||||
|
}else {
|
||||||
|
this.loadWiFiSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadWiFiSettings() {
|
||||||
|
this.deselectNetwork();
|
||||||
|
|
||||||
|
simpleGet(
|
||||||
|
WIFI_SETTINGS_ENDPOINT,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification,
|
||||||
|
"wifiSettings",
|
||||||
|
"wifiSettingsFetched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveWiFiSettings(e) {
|
||||||
|
simplePost(
|
||||||
|
WIFI_SETTINGS_ENDPOINT,
|
||||||
|
this.state,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification,
|
||||||
|
"wifiSettings",
|
||||||
|
"wifiSettingsFetched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deselectNetwork(nextNetwork) {
|
||||||
|
this.setState({selectedNetwork:null});
|
||||||
|
}
|
||||||
|
|
||||||
|
wifiSettingValueChange = name => event => {
|
||||||
|
const { wifiSettings } = this.state;
|
||||||
|
wifiSettings[name] = event.target.value;
|
||||||
|
this.setState({wifiSettings});
|
||||||
|
};
|
||||||
|
|
||||||
|
wifiSettingCheckboxChange = name => event => {
|
||||||
|
const { wifiSettings } = this.state;
|
||||||
|
wifiSettings[name] = event.target.checked;
|
||||||
|
this.setState({wifiSettings});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { wifiSettingsFetched, wifiSettings, errorMessage, selectedNetwork } = this.state;
|
||||||
|
return (
|
||||||
|
<SectionContent title="WiFi Settings">
|
||||||
|
<SnackbarNotification notificationRef={(raiseNotification)=>this.raiseNotification = raiseNotification} />
|
||||||
|
<WiFiSettingsForm wifiSettingsFetched={wifiSettingsFetched} wifiSettings={wifiSettings} errorMessage={errorMessage} selectedNetwork={selectedNetwork} deselectNetwork={this.deselectNetwork}
|
||||||
|
onSubmit={this.saveWiFiSettings} onReset={this.loadWiFiSettings} handleValueChange={this.wifiSettingValueChange} handleCheckboxChange={this.wifiSettingCheckboxChange} />
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiSettings.propTypes = {
|
||||||
|
deselectNetwork: PropTypes.func,
|
||||||
|
selectedNetwork: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WiFiSettings;
|
188
interface/src/containers/WiFiStatus.js
Normal file
188
interface/src/containers/WiFiStatus.js
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Button from 'material-ui/Button';
|
||||||
|
import { LinearProgress } from 'material-ui/Progress';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
|
||||||
|
import SnackbarNotification from '../components/SnackbarNotification';
|
||||||
|
import SectionContent from '../components/SectionContent';
|
||||||
|
|
||||||
|
import { WIFI_STATUS_ENDPOINT } from '../constants/Endpoints';
|
||||||
|
import { isConnected, connectionStatus, connectionStatusHighlight } from '../constants/WiFiConnectionStatus';
|
||||||
|
import * as Highlight from '../constants/Highlight';
|
||||||
|
|
||||||
|
import { simpleGet } from '../helpers/SimpleGet';
|
||||||
|
|
||||||
|
import List, { ListItem, ListItemText } from 'material-ui/List';
|
||||||
|
import Avatar from 'material-ui/Avatar';
|
||||||
|
import Divider from 'material-ui/Divider';
|
||||||
|
import WifiIcon from 'material-ui-icons/Wifi';
|
||||||
|
import DNSIcon from 'material-ui-icons/Dns';
|
||||||
|
import SettingsInputComponentIcon from 'material-ui-icons/SettingsInputComponent';
|
||||||
|
import SettingsInputAntennaIcon from 'material-ui-icons/SettingsInputAntenna';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
["wifiStatus_" + Highlight.IDLE]: {
|
||||||
|
backgroundColor: theme.palette.highlight_idle
|
||||||
|
},
|
||||||
|
["wifiStatus_" + Highlight.SUCCESS]: {
|
||||||
|
backgroundColor: theme.palette.highlight_success
|
||||||
|
},
|
||||||
|
["wifiStatus_" + Highlight.ERROR]: {
|
||||||
|
backgroundColor: theme.palette.highlight_error
|
||||||
|
},
|
||||||
|
["wifiStatus_" + Highlight.WARN]: {
|
||||||
|
backgroundColor: theme.palette.highlight_warn
|
||||||
|
},
|
||||||
|
fetching: {
|
||||||
|
margin: theme.spacing.unit * 4,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing.unit * 2,
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class WiFiStatus extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
status:null,
|
||||||
|
fetched: false,
|
||||||
|
errorMessage:null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState = this.setState.bind(this);
|
||||||
|
this.loadWiFiStatus = this.loadWiFiStatus.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadWiFiStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsServers(status) {
|
||||||
|
if (!status.dns_ip_1){
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
return status.dns_ip_1 + (status.dns_ip_2 ? ','+status.dns_ip_2 : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadWiFiStatus() {
|
||||||
|
simpleGet(
|
||||||
|
WIFI_STATUS_ENDPOINT,
|
||||||
|
this.setState,
|
||||||
|
this.raiseNotification
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWiFiStatus(status, fullDetails, classes) {
|
||||||
|
const listItems = [];
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="connection_status">
|
||||||
|
<Avatar className={classes["wifiStatus_" + connectionStatusHighlight(status)]}>
|
||||||
|
<WifiIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="Connection Status" secondary={connectionStatus(status)} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fullDetails && isConnected(status)) {
|
||||||
|
listItems.push(<Divider key="connection_status_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="ssid">
|
||||||
|
<Avatar>
|
||||||
|
<SettingsInputAntennaIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="SSID" secondary={status.ssid} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="ssid_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="ip_address">
|
||||||
|
<Avatar>IP</Avatar>
|
||||||
|
<ListItemText primary="IP Address" secondary={status.local_ip} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="ip_address_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="subnet_mask">
|
||||||
|
<Avatar>#</Avatar>
|
||||||
|
<ListItemText primary="Subnet Mask" secondary={status.subnet_mask} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="subnet_mask_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="gateway_ip">
|
||||||
|
<Avatar>
|
||||||
|
<SettingsInputComponentIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="Gateway IP" secondary={status.gateway_ip ? status.gateway_ip : "none"} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
listItems.push(<Divider key="gateway_ip_divider" inset component="li" />);
|
||||||
|
|
||||||
|
listItems.push(
|
||||||
|
<ListItem key="dns_server_ip">
|
||||||
|
<Avatar>
|
||||||
|
<DNSIcon />
|
||||||
|
</Avatar>
|
||||||
|
<ListItemText primary="DNS Server IP" secondary={this.dnsServers(status)} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<List>
|
||||||
|
{listItems}
|
||||||
|
</List>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={this.loadWiFiStatus}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { status, fetched, errorMessage } = this.state;
|
||||||
|
const { classes, fullDetails } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent title="WiFi Status">
|
||||||
|
<SnackbarNotification notificationRef={(raiseNotification)=>this.raiseNotification = raiseNotification} />
|
||||||
|
{
|
||||||
|
!fetched ?
|
||||||
|
<div>
|
||||||
|
<LinearProgress className={classes.fetching}/>
|
||||||
|
<Typography variant="display1" className={classes.fetching}>
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
status ? this.renderWiFiStatus(status, fullDetails, classes)
|
||||||
|
:
|
||||||
|
<div>
|
||||||
|
<Typography variant="display1" className={classes.fetching}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={this.loadWiFiStatus}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(WiFiStatus);
|
124
interface/src/forms/APSettingsForm.js
Normal file
124
interface/src/forms/APSettingsForm.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Button from 'material-ui/Button';
|
||||||
|
import { LinearProgress } from 'material-ui/Progress';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
import { MenuItem } from 'material-ui/Menu';
|
||||||
|
|
||||||
|
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
|
import {isAPEnabled} from '../constants/WiFiAPModes';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
loadingSettings: {
|
||||||
|
margin: theme.spacing.unit,
|
||||||
|
},
|
||||||
|
loadingSettingsDetails: {
|
||||||
|
margin: theme.spacing.unit * 4,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
textField: {
|
||||||
|
width: "100%"
|
||||||
|
},
|
||||||
|
selectField:{
|
||||||
|
width: "100%",
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
marginBottom: theme.spacing.unit
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing.unit * 2,
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class APSettingsForm extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes, apSettingsFetched, apSettings, errorMessage, handleValueChange, onSubmit, onReset } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
!apSettingsFetched ?
|
||||||
|
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<LinearProgress className={classes.loadingSettingsDetails}/>
|
||||||
|
<Typography variant="display1" className={classes.loadingSettingsDetails}>
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
: apSettings ?
|
||||||
|
|
||||||
|
<ValidatorForm onSubmit={onSubmit} ref="APSettingsForm">
|
||||||
|
|
||||||
|
<SelectValidator name="provision_mode" label="Provide Access Point..." value={apSettings.provision_mode} className={classes.selectField}
|
||||||
|
onChange={handleValueChange('provision_mode')}>
|
||||||
|
<MenuItem value={0}>Always</MenuItem>
|
||||||
|
<MenuItem value={1}>When WiFi Disconnected</MenuItem>
|
||||||
|
<MenuItem value={2}>Never</MenuItem>
|
||||||
|
</SelectValidator>
|
||||||
|
|
||||||
|
{
|
||||||
|
isAPEnabled(apSettings.provision_mode) &&
|
||||||
|
[
|
||||||
|
<TextValidator key="ssid"
|
||||||
|
validators={['required', 'matchRegexp:^.{0,32}$']}
|
||||||
|
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characeters or less']}
|
||||||
|
name="ssid"
|
||||||
|
label="Access Point SSID"
|
||||||
|
className={classes.textField}
|
||||||
|
value={apSettings.ssid}
|
||||||
|
onChange={handleValueChange('ssid')}
|
||||||
|
margin="normal"
|
||||||
|
/>,
|
||||||
|
<TextValidator key="password"
|
||||||
|
validators={['required', 'matchRegexp:^.{0,64}$']}
|
||||||
|
errorMessages={['Access Point Password is required', 'Access Point Password must be 64 characters or less']}
|
||||||
|
name="password"
|
||||||
|
label="Access Point Password"
|
||||||
|
className={classes.textField}
|
||||||
|
value={apSettings.password}
|
||||||
|
onChange={handleValueChange('password')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button variant="raised" color="primary" className={classes.button} type="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</ValidatorForm>
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<Typography variant="display1" className={classes.loadingSettingsDetails}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
APSettingsForm.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
apSettingsFetched: PropTypes.bool.isRequired,
|
||||||
|
apSettings: PropTypes.object,
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
onReset: PropTypes.func.isRequired,
|
||||||
|
handleValueChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(APSettingsForm);
|
113
interface/src/forms/NTPSettingsForm.js
Normal file
113
interface/src/forms/NTPSettingsForm.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Button from 'material-ui/Button';
|
||||||
|
import { LinearProgress } from 'material-ui/Progress';
|
||||||
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
|
||||||
|
import isIP from '../validators/isIP';
|
||||||
|
import isHostname from '../validators/isHostname';
|
||||||
|
import or from '../validators/or';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
loadingSettings: {
|
||||||
|
margin: theme.spacing.unit,
|
||||||
|
},
|
||||||
|
loadingSettingsDetails: {
|
||||||
|
margin: theme.spacing.unit * 4,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
textField: {
|
||||||
|
width: "100%"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing.unit * 2,
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class NTPSettingsForm extends React.Component {
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes, ntpSettingsFetched, ntpSettings, errorMessage, handleValueChange, onSubmit, onReset } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
!ntpSettingsFetched ?
|
||||||
|
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<LinearProgress className={classes.loadingSettingsDetails}/>
|
||||||
|
<Typography variant="display1" className={classes.loadingSettingsDetails}>
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
: ntpSettings ?
|
||||||
|
|
||||||
|
<ValidatorForm onSubmit={onSubmit}>
|
||||||
|
|
||||||
|
<TextValidator
|
||||||
|
validators={['required', 'isIPOrHostname']}
|
||||||
|
errorMessages={['Server is required', "Not a valid IP address or hostname"]}
|
||||||
|
name="server"
|
||||||
|
label="Server"
|
||||||
|
className={classes.textField}
|
||||||
|
value={ntpSettings.server}
|
||||||
|
onChange={handleValueChange('server')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextValidator
|
||||||
|
validators={['required','isNumber','minNumber:60','maxNumber:86400']}
|
||||||
|
errorMessages={['Interval is required','Interval must be a number','Must be at least 60 seconds',"Must not be more than 86400 seconds (24 hours)"]}
|
||||||
|
name="interval"
|
||||||
|
label="Interval (Seconds)"
|
||||||
|
className={classes.textField}
|
||||||
|
value={ntpSettings.interval}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('interval')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="raised" color="primary" className={classes.button} type="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</ValidatorForm>
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<Typography variant="display1" className={classes.loadingSettingsDetails}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NTPSettingsForm.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
ntpSettingsFetched: PropTypes.bool.isRequired,
|
||||||
|
ntpSettings: PropTypes.object,
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
onReset: PropTypes.func.isRequired,
|
||||||
|
handleValueChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(NTPSettingsForm);
|
132
interface/src/forms/OTASettingsForm.js
Normal file
132
interface/src/forms/OTASettingsForm.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Button from 'material-ui/Button';
|
||||||
|
import Switch from 'material-ui/Switch';
|
||||||
|
import { LinearProgress } from 'material-ui/Progress';
|
||||||
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
import { FormControlLabel } from 'material-ui/Form';
|
||||||
|
|
||||||
|
import isIP from '../validators/isIP';
|
||||||
|
import isHostname from '../validators/isHostname';
|
||||||
|
import or from '../validators/or';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
loadingSettings: {
|
||||||
|
margin: theme.spacing.unit,
|
||||||
|
},
|
||||||
|
loadingSettingsDetails: {
|
||||||
|
margin: theme.spacing.unit * 4,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
switchControl: {
|
||||||
|
width: "100%",
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
marginBottom: theme.spacing.unit
|
||||||
|
},
|
||||||
|
textField: {
|
||||||
|
width: "100%"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing.unit * 2,
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class OTASettingsForm extends React.Component {
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes, otaSettingsFetched, otaSettings, errorMessage, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
!otaSettingsFetched ?
|
||||||
|
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<LinearProgress className={classes.loadingSettingsDetails}/>
|
||||||
|
<Typography variant="display1" className={classes.loadingSettingsDetails}>
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
: otaSettings ?
|
||||||
|
|
||||||
|
<ValidatorForm onSubmit={onSubmit}>
|
||||||
|
|
||||||
|
<FormControlLabel className={classes.switchControl}
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={otaSettings.enabled}
|
||||||
|
onChange={handleCheckboxChange('enabled')}
|
||||||
|
value="enabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enable OTA Updates?"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextValidator
|
||||||
|
validators={['required', 'isNumber', 'minNumber:1025', 'maxNumber:65535']}
|
||||||
|
errorMessages={['Port is required', "Must be a number", "Must be greater than 1024 ", "Max value is 65535"]}
|
||||||
|
name="port"
|
||||||
|
label="Port"
|
||||||
|
className={classes.textField}
|
||||||
|
value={otaSettings.port}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('port')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextValidator key="password"
|
||||||
|
validators={['required', 'matchRegexp:^.{0,64}$']}
|
||||||
|
errorMessages={['OTA Password is required', 'OTA Point Password must be 64 characters or less']}
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
className={classes.textField}
|
||||||
|
value={otaSettings.password}
|
||||||
|
onChange={handleValueChange('password')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="raised" color="primary" className={classes.button} type="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</ValidatorForm>
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<Typography variant="display1" className={classes.loadingSettingsDetails}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OTASettingsForm.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
otaSettingsFetched: PropTypes.bool.isRequired,
|
||||||
|
otaSettings: PropTypes.object,
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
onReset: PropTypes.func.isRequired,
|
||||||
|
handleValueChange: PropTypes.func.isRequired,
|
||||||
|
handleCheckboxChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(OTASettingsForm);
|
102
interface/src/forms/WiFiNetworkSelector.js
Normal file
102
interface/src/forms/WiFiNetworkSelector.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Button from 'material-ui/Button';
|
||||||
|
import { LinearProgress } from 'material-ui/Progress';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
|
||||||
|
import { isNetworkOpen, networkSecurityMode } from '../constants/WiFiSecurityModes';
|
||||||
|
|
||||||
|
import List, { ListItem, ListItemText, ListItemIcon, ListItemAvatar } from 'material-ui/List';
|
||||||
|
import Avatar from 'material-ui/Avatar';
|
||||||
|
import Badge from 'material-ui/Badge';
|
||||||
|
|
||||||
|
import WifiIcon from 'material-ui-icons/Wifi';
|
||||||
|
import LockIcon from 'material-ui-icons/Lock';
|
||||||
|
import LockOpenIcon from 'material-ui-icons/LockOpen';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
scanningProgress: {
|
||||||
|
margin: theme.spacing.unit * 4,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing.unit * 2,
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class WiFiNetworkSelector extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.renderNetwork = this.renderNetwork.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNetwork(network) {
|
||||||
|
return ([
|
||||||
|
<ListItem key={network.ssid} button onClick={() => this.props.selectNetwork(network)}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={network.ssid}
|
||||||
|
secondary={"Security: "+ networkSecurityMode(network) + ", Ch: " + network.channel}
|
||||||
|
/>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Badge badgeContent={network.rssi + "db"}>
|
||||||
|
<WifiIcon />
|
||||||
|
</Badge>
|
||||||
|
</ListItemIcon>
|
||||||
|
</ListItem>
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes, scanningForNetworks, networkList, errorMessage, requestNetworkScan } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
scanningForNetworks ?
|
||||||
|
<div>
|
||||||
|
<LinearProgress className={classes.scanningProgress}/>
|
||||||
|
<Typography variant="display1" className={classes.scanningProgress}>
|
||||||
|
Scanning...
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
networkList ?
|
||||||
|
<List>
|
||||||
|
{networkList.networks.map(this.renderNetwork)}
|
||||||
|
</List>
|
||||||
|
:
|
||||||
|
<div>
|
||||||
|
<Typography variant="display1" className={classes.scanningProgress}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={requestNetworkScan} disabled={scanningForNetworks}>
|
||||||
|
Scan again...
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiNetworkSelector.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
selectNetwork: PropTypes.func.isRequired,
|
||||||
|
scanningForNetworks: PropTypes.bool.isRequired,
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
networkList: PropTypes.object,
|
||||||
|
requestNetworkScan: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(WiFiNetworkSelector);
|
237
interface/src/forms/WiFiSettingsForm.js
Normal file
237
interface/src/forms/WiFiSettingsForm.js
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import Button from 'material-ui/Button';
|
||||||
|
import { LinearProgress } from 'material-ui/Progress';
|
||||||
|
import Checkbox from 'material-ui/Checkbox';
|
||||||
|
import { FormControlLabel } from 'material-ui/Form';
|
||||||
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
import List, { ListItem, ListItemText, ListItemSecondaryAction, ListItemAvatar } from 'material-ui/List';
|
||||||
|
|
||||||
|
import { isNetworkOpen, networkSecurityMode } from '../constants/WiFiSecurityModes';
|
||||||
|
|
||||||
|
import Avatar from 'material-ui/Avatar';
|
||||||
|
import IconButton from 'material-ui/IconButton';
|
||||||
|
import LockIcon from 'material-ui-icons/Lock';
|
||||||
|
import LockOpenIcon from 'material-ui-icons/LockOpen';
|
||||||
|
import DeleteIcon from 'material-ui-icons/Delete';
|
||||||
|
|
||||||
|
import isIP from '../validators/isIP';
|
||||||
|
import isHostname from '../validators/isHostname';
|
||||||
|
import optional from '../validators/optional';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
loadingSettings: {
|
||||||
|
margin: theme.spacing.unit,
|
||||||
|
},
|
||||||
|
loadingSettingsDetails: {
|
||||||
|
margin: theme.spacing.unit * 4,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
textField: {
|
||||||
|
width: "100%"
|
||||||
|
},
|
||||||
|
checkboxControl: {
|
||||||
|
width: "100%"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing.unit * 2,
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class WiFiSettingsForm extends React.Component {
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
ValidatorForm.addValidationRule('isIP', isIP);
|
||||||
|
ValidatorForm.addValidationRule('isHostname', isHostname);
|
||||||
|
ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSelectedNetwork() {
|
||||||
|
const { selectedNetwork, deselectNetwork } = this.props;
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
<ListItem key={selectedNetwork.ssid}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={selectedNetwork.ssid}
|
||||||
|
secondary={"Security: "+ networkSecurityMode(selectedNetwork) + ", Ch: " + selectedNetwork.channel}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes, formRef, wifiSettingsFetched, wifiSettings, errorMessage, selectedNetwork, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
|
||||||
|
return (
|
||||||
|
<div ref={formRef}>
|
||||||
|
{
|
||||||
|
!wifiSettingsFetched ?
|
||||||
|
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<LinearProgress className={classes.loadingSettingsDetails}/>
|
||||||
|
<Typography variant="display1" className={classes.loadingSettingsDetails}>
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
: wifiSettings ?
|
||||||
|
|
||||||
|
<ValidatorForm onSubmit={onSubmit} ref="WiFiSettingsForm">
|
||||||
|
{
|
||||||
|
selectedNetwork ? this.renderSelectedNetwork() :
|
||||||
|
<TextValidator
|
||||||
|
validators={['required', 'matchRegexp:^.{0,32}$']}
|
||||||
|
errorMessages={['SSID is required', 'SSID must be 32 characeters or less']}
|
||||||
|
name="ssid"
|
||||||
|
label="SSID"
|
||||||
|
className={classes.textField}
|
||||||
|
value={wifiSettings.ssid}
|
||||||
|
onChange={handleValueChange('ssid')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!isNetworkOpen(selectedNetwork) &&
|
||||||
|
<TextValidator
|
||||||
|
validators={['matchRegexp:^.{0,64}$']}
|
||||||
|
errorMessages={['Password must be 64 characters or less']}
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
className={classes.textField}
|
||||||
|
value={wifiSettings.password}
|
||||||
|
onChange={handleValueChange('password')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<TextValidator
|
||||||
|
validators={['required', 'isHostname']}
|
||||||
|
errorMessages={['Hostname is required', "Not a valid hostname"]}
|
||||||
|
name="hostname"
|
||||||
|
label="Hostname"
|
||||||
|
className={classes.textField}
|
||||||
|
value={wifiSettings.hostname}
|
||||||
|
onChange={handleValueChange('hostname')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel className={classes.checkboxControl}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
value="static_ip_config"
|
||||||
|
checked={wifiSettings.static_ip_config}
|
||||||
|
onChange={handleCheckboxChange("static_ip_config")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Static IP Config?"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
wifiSettings.static_ip_config &&
|
||||||
|
[
|
||||||
|
<TextValidator key="local_ip"
|
||||||
|
validators={['required', 'isIP']}
|
||||||
|
errorMessages={['Local IP is required', 'Must be an IP address']}
|
||||||
|
name="local_ip"
|
||||||
|
label="Local IP"
|
||||||
|
className={classes.textField}
|
||||||
|
value={wifiSettings.local_ip}
|
||||||
|
onChange={handleValueChange('local_ip')}
|
||||||
|
margin="normal"
|
||||||
|
/>,
|
||||||
|
<TextValidator key="gateway_ip"
|
||||||
|
validators={['required', 'isIP']}
|
||||||
|
errorMessages={['Gateway IP is required', 'Must be an IP address']}
|
||||||
|
name="gateway_ip"
|
||||||
|
label="Gateway"
|
||||||
|
className={classes.textField}
|
||||||
|
value={wifiSettings.gateway_ip}
|
||||||
|
onChange={handleValueChange('gateway_ip')}
|
||||||
|
margin="normal"
|
||||||
|
/>,
|
||||||
|
<TextValidator key="subnet_mask"
|
||||||
|
validators={['required', 'isIP']}
|
||||||
|
errorMessages={['Subnet mask is required', 'Must be an IP address']}
|
||||||
|
name="subnet_mask"
|
||||||
|
label="Subnet"
|
||||||
|
className={classes.textField}
|
||||||
|
value={wifiSettings.subnet_mask}
|
||||||
|
onChange={handleValueChange('subnet_mask')}
|
||||||
|
margin="normal"
|
||||||
|
/>,
|
||||||
|
<TextValidator key="dns_ip_1"
|
||||||
|
validators={['isOptionalIP']}
|
||||||
|
errorMessages={['Must be an IP address']}
|
||||||
|
name="dns_ip_1"
|
||||||
|
label="DNS IP #1"
|
||||||
|
className={classes.textField}
|
||||||
|
value={wifiSettings.dns_ip_1}
|
||||||
|
onChange={handleValueChange('dns_ip_1')}
|
||||||
|
margin="normal"
|
||||||
|
/>,
|
||||||
|
<TextValidator key="dns_ip_2"
|
||||||
|
validators={['isOptionalIP']}
|
||||||
|
errorMessages={['Must be an IP address']}
|
||||||
|
name="dns_ip_2"
|
||||||
|
label="DNS IP #2"
|
||||||
|
className={classes.textField}
|
||||||
|
value={wifiSettings.dns_ip_2}
|
||||||
|
onChange={handleValueChange('dns_ip_2')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button variant="raised" color="primary" className={classes.button} type="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</ValidatorForm>
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<Typography variant="display1" className={classes.loadingSettingsDetails}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="raised" color="secondary" className={classes.button} onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiSettingsForm.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
wifiSettingsFetched: PropTypes.bool.isRequired,
|
||||||
|
wifiSettings: PropTypes.object,
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
deselectNetwork: PropTypes.func,
|
||||||
|
selectedNetwork: PropTypes.object,
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
onReset: PropTypes.func.isRequired,
|
||||||
|
handleValueChange: PropTypes.func.isRequired,
|
||||||
|
handleCheckboxChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(WiFiSettingsForm);
|
35
interface/src/helpers/SimpleGet.js
Normal file
35
interface/src/helpers/SimpleGet.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Executes a get request for an endpoint, updating the local state of the calling
|
||||||
|
* component. The calling component must bind setState before using this
|
||||||
|
* function.
|
||||||
|
*
|
||||||
|
* This is designed for re-use in simple situations, we arn't using redux here!
|
||||||
|
*/
|
||||||
|
export const simpleGet = (
|
||||||
|
endpointUrl,
|
||||||
|
setState,
|
||||||
|
raiseNotification = null,
|
||||||
|
dataKey="status",
|
||||||
|
fetchedKey="fetched",
|
||||||
|
errorMessageKey = "errorMessage"
|
||||||
|
) => {
|
||||||
|
setState({
|
||||||
|
[dataKey]:null,
|
||||||
|
[fetchedKey]: false,
|
||||||
|
[errorMessageKey]:null
|
||||||
|
});
|
||||||
|
fetch(endpointUrl)
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw Error("Invalid status code: " + response.status);
|
||||||
|
})
|
||||||
|
.then(json => {setState({[dataKey]: json, [fetchedKey]:true})})
|
||||||
|
.catch(error =>{
|
||||||
|
if (raiseNotification) {
|
||||||
|
raiseNotification("Problem fetching. " + error.message);
|
||||||
|
}
|
||||||
|
setState({[dataKey]: null, [fetchedKey]:true, [errorMessageKey]:error.message});
|
||||||
|
});
|
||||||
|
}
|
38
interface/src/helpers/SimplePost.js
Normal file
38
interface/src/helpers/SimplePost.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Executes a post request for saving data to an endpoint, updating the local
|
||||||
|
* state with the response. The calling component must bind setState before
|
||||||
|
* using this function.
|
||||||
|
*
|
||||||
|
* This is designed for re-use in simple situations, we arn't using redux here!
|
||||||
|
*/
|
||||||
|
export const simplePost = (
|
||||||
|
endpointUrl,
|
||||||
|
state,
|
||||||
|
setState,
|
||||||
|
raiseNotification = null,
|
||||||
|
dataKey="settings",
|
||||||
|
fetchedKey="fetched",
|
||||||
|
errorMessageKey = "errorMessage"
|
||||||
|
) => {
|
||||||
|
setState({[fetchedKey]: false});
|
||||||
|
fetch(endpointUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(state[dataKey]),
|
||||||
|
headers: new Headers({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw Error("Invalid status code: " + response.status);
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
|
raiseNotification("Changes successfully applied.");
|
||||||
|
setState({[dataKey]: json, [fetchedKey]:true});
|
||||||
|
}).catch(error => {
|
||||||
|
raiseNotification("Problem saving. " + error.message);
|
||||||
|
setState({[dataKey]: null, [fetchedKey]:true, [errorMessageKey]:error.message});
|
||||||
|
});
|
||||||
|
}
|
5
interface/src/history.js
Normal file
5
interface/src/history.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
|
export default createBrowserHistory({
|
||||||
|
/* pass a configuration object here if needed */
|
||||||
|
})
|
16
interface/src/index.js
Normal file
16
interface/src/index.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
|
||||||
|
import history from './history';
|
||||||
|
import { Router, Route, Redirect, Switch } from 'react-router';
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
render((
|
||||||
|
<Router history={history}>
|
||||||
|
<Switch>
|
||||||
|
<Redirect exact from='/' to='/home'/>
|
||||||
|
<Route path="/" component={App} />
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
), document.getElementById("root"))
|
6
interface/src/validators/isHostname.js
Normal file
6
interface/src/validators/isHostname.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const hostnameLengthRegex = /^.{0,32}$/
|
||||||
|
const hostnamePatternRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/
|
||||||
|
|
||||||
|
export default function isHostname(hostname) {
|
||||||
|
return hostnameLengthRegex.test(hostname) && hostnamePatternRegex.test(hostname);
|
||||||
|
}
|
5
interface/src/validators/isIP.js
Normal file
5
interface/src/validators/isIP.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const ipAddressRegexp = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
||||||
|
|
||||||
|
export default function isIp(ipAddress) {
|
||||||
|
return ipAddressRegexp.test(ipAddress);
|
||||||
|
}
|
1
interface/src/validators/optional.js
Normal file
1
interface/src/validators/optional.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default validator => value => !value || validator(value);
|
1
interface/src/validators/or.js
Normal file
1
interface/src/validators/or.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default (validator1, validator2) => value => validator1(value) || validator2(value);
|
36
lib/readme.txt
Normal file
36
lib/readme.txt
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
This directory is intended for the project specific (private) libraries.
|
||||||
|
PlatformIO will compile them to static libraries and link to executable file.
|
||||||
|
|
||||||
|
The source code of each library should be placed in separate directory, like
|
||||||
|
"lib/private_lib/[here are source files]".
|
||||||
|
|
||||||
|
For example, see how can be organized `Foo` and `Bar` libraries:
|
||||||
|
|
||||||
|
|--lib
|
||||||
|
| |--Bar
|
||||||
|
| | |--docs
|
||||||
|
| | |--examples
|
||||||
|
| | |--src
|
||||||
|
| | |- Bar.c
|
||||||
|
| | |- Bar.h
|
||||||
|
| |--Foo
|
||||||
|
| | |- Foo.c
|
||||||
|
| | |- Foo.h
|
||||||
|
| |- readme.txt --> THIS FILE
|
||||||
|
|- platformio.ini
|
||||||
|
|--src
|
||||||
|
|- main.c
|
||||||
|
|
||||||
|
Then in `src/main.c` you should use:
|
||||||
|
|
||||||
|
#include <Foo.h>
|
||||||
|
#include <Bar.h>
|
||||||
|
|
||||||
|
// rest H/C/CPP code
|
||||||
|
|
||||||
|
PlatformIO will find your libraries automatically, configure preprocessor's
|
||||||
|
include paths and build them.
|
||||||
|
|
||||||
|
More information about PlatformIO Library Dependency Finder
|
||||||
|
- http://docs.platformio.org/page/librarymanager/ldf.html
|
22
platformio.ini
Normal file
22
platformio.ini
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; http://docs.platformio.org/page/projectconf.html
|
||||||
|
[env:esp12e]
|
||||||
|
platform = espressif8266
|
||||||
|
board = esp12e
|
||||||
|
framework = arduino
|
||||||
|
;upload_flags = --port=8266 --auth=esp-react
|
||||||
|
;upload_port = 192.168.0.6
|
||||||
|
board_f_cpu = 160000000L
|
||||||
|
build_flags= -D NO_GLOBAL_ARDUINOOTA
|
||||||
|
lib_deps =
|
||||||
|
https://github.com/PaulStoffregen/Time
|
||||||
|
https://github.com/gmag11/NtpClient
|
||||||
|
https://github.com/bblanchon/ArduinoJson
|
||||||
|
https://github.com/me-no-dev/ESPAsyncWebServer
|
BIN
screenshots/screenshots.png
Normal file
BIN
screenshots/screenshots.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
49
src/APSettingsService.cpp
Normal file
49
src/APSettingsService.cpp
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#include <APSettingsService.h>
|
||||||
|
|
||||||
|
APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs) : SettingsService(server, fs, AP_SETTINGS_SERVICE_PATH, AP_SETTINGS_FILE) {
|
||||||
|
}
|
||||||
|
|
||||||
|
APSettingsService::~APSettingsService() {}
|
||||||
|
|
||||||
|
void APSettingsService::loop() {
|
||||||
|
unsigned long now = millis();
|
||||||
|
if (_manageAtMillis <= now){
|
||||||
|
WiFiMode_t currentWiFiMode = WiFi.getMode();
|
||||||
|
if (_provisionMode == AP_MODE_ALWAYS || (_provisionMode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED)) {
|
||||||
|
if (currentWiFiMode == WIFI_OFF || currentWiFiMode == WIFI_STA){
|
||||||
|
Serial.println("Starting software access point");
|
||||||
|
WiFi.softAP(_ssid.c_str(), _password.c_str());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (currentWiFiMode == WIFI_AP || currentWiFiMode == WIFI_AP_STA){
|
||||||
|
Serial.println("Stopping software access point");
|
||||||
|
WiFi.softAPdisconnect(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_manageAtMillis = now + MANAGE_NETWORK_DELAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APSettingsService::readFromJsonObject(JsonObject& root) {
|
||||||
|
_provisionMode = root["provision_mode"] | AP_MODE_ALWAYS;
|
||||||
|
switch (_provisionMode) {
|
||||||
|
case AP_MODE_ALWAYS:
|
||||||
|
case AP_MODE_DISCONNECTED:
|
||||||
|
case AP_MODE_NEVER:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_provisionMode = AP_MODE_ALWAYS;
|
||||||
|
}
|
||||||
|
_ssid = root["ssid"] | AP_DEFAULT_SSID;
|
||||||
|
_password = root["password"] | AP_DEFAULT_PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APSettingsService::writeToJsonObject(JsonObject& root) {
|
||||||
|
root["provision_mode"] = _provisionMode;
|
||||||
|
root["ssid"] = _ssid;
|
||||||
|
root["password"] = _password;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APSettingsService::onConfigUpdated() {
|
||||||
|
_manageAtMillis = 0;
|
||||||
|
}
|
43
src/APSettingsService.h
Normal file
43
src/APSettingsService.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#ifndef APSettingsConfig_h
|
||||||
|
#define APSettingsConfig_h
|
||||||
|
|
||||||
|
#include <IPAddress.h>
|
||||||
|
#include <SettingsService.h>
|
||||||
|
|
||||||
|
#define MANAGE_NETWORK_DELAY 10000
|
||||||
|
|
||||||
|
#define AP_MODE_ALWAYS 0
|
||||||
|
#define AP_MODE_DISCONNECTED 1
|
||||||
|
#define AP_MODE_NEVER 2
|
||||||
|
|
||||||
|
#define AP_DEFAULT_SSID "ssid"
|
||||||
|
#define AP_DEFAULT_PASSWORD "password"
|
||||||
|
|
||||||
|
#define AP_SETTINGS_FILE "/config/apSettings.json"
|
||||||
|
#define AP_SETTINGS_SERVICE_PATH "/apSettings"
|
||||||
|
|
||||||
|
class APSettingsService : public SettingsService {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
APSettingsService(AsyncWebServer* server, FS* fs);
|
||||||
|
~APSettingsService();
|
||||||
|
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
void readFromJsonObject(JsonObject& root);
|
||||||
|
void writeToJsonObject(JsonObject& root);
|
||||||
|
void onConfigUpdated();
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
int _provisionMode;
|
||||||
|
String _ssid;
|
||||||
|
String _password;
|
||||||
|
unsigned long _manageAtMillis;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end APSettingsConfig_h
|
19
src/APStatus.cpp
Normal file
19
src/APStatus.cpp
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#include <APStatus.h>
|
||||||
|
|
||||||
|
APStatus::APStatus(AsyncWebServer *server) : _server(server) {
|
||||||
|
_server->on("/apStatus", HTTP_GET, std::bind(&APStatus::apStatus, this, std::placeholders::_1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void APStatus::apStatus(AsyncWebServerRequest *request) {
|
||||||
|
AsyncJsonResponse * response = new AsyncJsonResponse();
|
||||||
|
JsonObject& root = response->getRoot();
|
||||||
|
|
||||||
|
WiFiMode_t currentWiFiMode = WiFi.getMode();
|
||||||
|
root["active"] = (currentWiFiMode == WIFI_AP || currentWiFiMode == WIFI_AP_STA);
|
||||||
|
root["ip_address"] = WiFi.softAPIP().toString();
|
||||||
|
root["mac_address"] = WiFi.softAPmacAddress();
|
||||||
|
root["station_num"] = WiFi.softAPgetStationNum();
|
||||||
|
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
25
src/APStatus.h
Normal file
25
src/APStatus.h
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#ifndef APStatus_h
|
||||||
|
#define APStatus_h
|
||||||
|
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESPAsyncTCP.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <AsyncJson.h>
|
||||||
|
#include <IPAddress.h>
|
||||||
|
|
||||||
|
class APStatus {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
APStatus(AsyncWebServer *server);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
|
||||||
|
void apStatus(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end APStatus_h
|
31
src/AsyncJsonCallbackResponse.h
Normal file
31
src/AsyncJsonCallbackResponse.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#ifndef _AsyncJsonCallbackResponse_H_
|
||||||
|
#define _AsyncJsonCallbackResponse_H_
|
||||||
|
|
||||||
|
#include <AsyncJson.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Listens for a response being destroyed and calls a callback during said distruction.
|
||||||
|
* used so we can take action after the response has been rendered to the client.
|
||||||
|
*
|
||||||
|
* Avoids having to fork ESPAsyncWebServer with a callback feature, but not nice!
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef std::function<void()> AsyncJsonCallback;
|
||||||
|
|
||||||
|
class AsyncJsonCallbackResponse: public AsyncJsonResponse {
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
AsyncJsonCallback _callback;
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
AsyncJsonCallbackResponse(AsyncJsonCallback callback, bool isArray=false) : _callback{callback}, AsyncJsonResponse(isArray) {}
|
||||||
|
~AsyncJsonCallbackResponse() {
|
||||||
|
_callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end _AsyncJsonCallbackResponse_H_
|
115
src/AsyncJsonRequestWebHandler.h
Normal file
115
src/AsyncJsonRequestWebHandler.h
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
#ifndef Async_Json_Request_Web_Handler_H_
|
||||||
|
#define Async_Json_Request_Web_Handler_H_
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
#define ASYNC_JSON_REQUEST_DEFAULT_MAX_SIZE 1024
|
||||||
|
#define ASYNC_JSON_REQUEST_MIMETYPE "application/json"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Handy little utility for dealing with small JSON request body payloads.
|
||||||
|
*
|
||||||
|
* Need to be careful using this as we are somewhat limited by RAM.
|
||||||
|
*
|
||||||
|
* Really only of use where there is a determinate payload size.
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef std::function<void(AsyncWebServerRequest *request, JsonVariant &json)> JsonRequestCallback;
|
||||||
|
|
||||||
|
class AsyncJsonRequestWebHandler: public AsyncWebHandler {
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
String _uri;
|
||||||
|
WebRequestMethodComposite _method;
|
||||||
|
JsonRequestCallback _onRequest;
|
||||||
|
int _maxContentLength;
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
AsyncJsonRequestWebHandler() :
|
||||||
|
_uri(),
|
||||||
|
_method(HTTP_POST|HTTP_PUT|HTTP_PATCH),
|
||||||
|
_onRequest(NULL),
|
||||||
|
_maxContentLength(ASYNC_JSON_REQUEST_DEFAULT_MAX_SIZE) {}
|
||||||
|
|
||||||
|
~AsyncJsonRequestWebHandler() {}
|
||||||
|
|
||||||
|
void setUri(const String& uri) { _uri = uri; }
|
||||||
|
void setMethod(WebRequestMethodComposite method) { _method = method; }
|
||||||
|
void setMaxContentLength(int maxContentLength) { _maxContentLength = maxContentLength; }
|
||||||
|
void onRequest(JsonRequestCallback fn) { _onRequest = fn; }
|
||||||
|
|
||||||
|
virtual bool canHandle(AsyncWebServerRequest *request) override final {
|
||||||
|
if(!_onRequest)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(!(_method & request->method()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri+"/")))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!request->contentType().equalsIgnoreCase(ASYNC_JSON_REQUEST_MIMETYPE))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
request->addInterestingHeader("ANY");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void handleRequest(AsyncWebServerRequest *request) override final {
|
||||||
|
// no request configured
|
||||||
|
if(!_onRequest) {
|
||||||
|
request->send(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have been handed too much data, return a 413 (payload too large)
|
||||||
|
if (request->contentLength() > _maxContentLength) {
|
||||||
|
request->send(413);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse JSON and if possible handle the request
|
||||||
|
if (request->_tempObject) {
|
||||||
|
DynamicJsonBuffer jsonBuffer;
|
||||||
|
JsonVariant json = jsonBuffer.parse((uint8_t *) request->_tempObject);
|
||||||
|
if (json.success()) {
|
||||||
|
_onRequest(request, json);
|
||||||
|
}else{
|
||||||
|
request->send(400);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallthrough, we have a null pointer, return 500.
|
||||||
|
// this can be due to running out of memory or never recieving body data.
|
||||||
|
request->send(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final {
|
||||||
|
if (_onRequest) {
|
||||||
|
// don't allocate if data is too large
|
||||||
|
if (total > _maxContentLength){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to allocate memory on first call
|
||||||
|
// NB: the memory allocated here is freed by ~AsyncWebServerRequest
|
||||||
|
if(index == 0 && !request->_tempObject){
|
||||||
|
request->_tempObject = malloc(total);
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy the data into the buffer, if we have a buffer!
|
||||||
|
if (request->_tempObject) {
|
||||||
|
memcpy((uint8_t *) request->_tempObject+index, data, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool isRequestHandlerTrivial() override final {
|
||||||
|
return _onRequest ? false : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end Async_Json_Request_Web_Handler_H_
|
47
src/AuthSettingsService.cpp
Normal file
47
src/AuthSettingsService.cpp
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#include <AuthSettingsService.h>
|
||||||
|
|
||||||
|
AuthSettingsService::AuthSettingsService(AsyncWebServer* server, FS* fs) : SettingsService(server, fs, AUTH_SETTINGS_SERVICE_PATH, AUTH_SETTINGS_FILE) {
|
||||||
|
_server->on(AUTH_LOGOUT_PATH, HTTP_GET, std::bind(&AuthSettingsService::logout, this, std::placeholders::_1));
|
||||||
|
|
||||||
|
// configure authentication handler
|
||||||
|
_authenticationHandler.setUri(AUTH_AUTHENTICATE_PATH);
|
||||||
|
_authenticationHandler.setMethod(HTTP_POST);
|
||||||
|
_authenticationHandler.onRequest(std::bind(&AuthSettingsService::authenticate, this, std::placeholders::_1, std::placeholders::_2));
|
||||||
|
_server->addHandler(&_authenticationHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthSettingsService::~AuthSettingsService() {}
|
||||||
|
|
||||||
|
// checks the session is authenticated, refreshes the sessions timeout if so
|
||||||
|
bool AuthSettingsService::authenticated(AsyncWebServerRequest *request){
|
||||||
|
request->send(400);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AuthSettingsService::readFromJsonObject(JsonObject& root){
|
||||||
|
_username = root["username"] | AUTH_DEFAULT_USERNAME;
|
||||||
|
_password = root["password"] | AUTH_DEFAULT_PASSWORD;
|
||||||
|
_sessionTimeout= root["session_timeout"] | AUTH_DEFAULT_SESSION_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AuthSettingsService::writeToJsonObject(JsonObject& root){
|
||||||
|
root["username"] = _username;
|
||||||
|
root["password"] = _password;
|
||||||
|
root["session_timeout"] = _sessionTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AuthSettingsService::logout(AsyncWebServerRequest *request){
|
||||||
|
// revoke the current requests session
|
||||||
|
}
|
||||||
|
|
||||||
|
void AuthSettingsService::authenticate(AsyncWebServerRequest *request, JsonVariant &json){
|
||||||
|
if (json.is<JsonObject>()){
|
||||||
|
JsonObject& credentials = json.as<JsonObject>();
|
||||||
|
if (credentials["username"] == _username && credentials["password"] == _password){
|
||||||
|
// store cookie and write to response
|
||||||
|
}
|
||||||
|
request->send(401);
|
||||||
|
} else{
|
||||||
|
request->send(400);
|
||||||
|
}
|
||||||
|
}
|
56
src/AuthSettingsService.h
Normal file
56
src/AuthSettingsService.h
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#ifndef AuthSettingsService_h
|
||||||
|
#define AuthSettingsService_h
|
||||||
|
|
||||||
|
#include <SettingsService.h>
|
||||||
|
|
||||||
|
#define AUTH_DEFAULT_USERNAME "admin"
|
||||||
|
#define AUTH_DEFAULT_PASSWORD "admin"
|
||||||
|
#define AUTH_DEFAULT_SESSION_TIMEOUT 3600
|
||||||
|
|
||||||
|
#define AUTH_SETTINGS_FILE "/config/authSettings.json"
|
||||||
|
#define AUTH_SETTINGS_SERVICE_PATH "/authSettings"
|
||||||
|
|
||||||
|
#define AUTH_LOGOUT_PATH "/logout"
|
||||||
|
#define AUTH_AUTHENTICATE_PATH "/authenticate"
|
||||||
|
|
||||||
|
// max number of concurrently authenticated clients
|
||||||
|
#define AUTH_MAX_CLIENTS 10
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Will protect services with a cookie based authentication service.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class AuthSettingsService : public SettingsService {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
AuthSettingsService(AsyncWebServer* server, FS* fs);
|
||||||
|
~AuthSettingsService();
|
||||||
|
|
||||||
|
// checks the session is authenticated,
|
||||||
|
// refreshes the sessions timeout if found
|
||||||
|
bool authenticated(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
void readFromJsonObject(JsonObject& root);
|
||||||
|
void writeToJsonObject(JsonObject& root);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
// callback handler for authentication endpoint
|
||||||
|
AsyncJsonRequestWebHandler _authenticationHandler;
|
||||||
|
|
||||||
|
// only supporting one username at the moment
|
||||||
|
String _username;
|
||||||
|
String _password;
|
||||||
|
|
||||||
|
// session timeout in seconds
|
||||||
|
unsigned int _sessionTimeout;
|
||||||
|
|
||||||
|
void logout(AsyncWebServerRequest *request);
|
||||||
|
void authenticate(AsyncWebServerRequest *request, JsonVariant &json);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end AuthSettingsService_h
|
94
src/NTPSettingsService.cpp
Normal file
94
src/NTPSettingsService.cpp
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
#include <NTPSettingsService.h>
|
||||||
|
|
||||||
|
NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs) : SettingsService(server, fs, NTP_SETTINGS_SERVICE_PATH, NTP_SETTINGS_FILE) {
|
||||||
|
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1));
|
||||||
|
_onStationModeGotIPHandler = WiFi.onStationModeGotIP(std::bind(&NTPSettingsService::onStationModeGotIP, this, std::placeholders::_1));
|
||||||
|
|
||||||
|
NTP.onNTPSyncEvent ([this](NTPSyncEvent_t ntpEvent) {
|
||||||
|
_ntpEvent = ntpEvent;
|
||||||
|
_syncEventTriggered = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
NTPSettingsService::~NTPSettingsService() {}
|
||||||
|
|
||||||
|
void NTPSettingsService::loop() {
|
||||||
|
// detect when we need to re-configure NTP and do it in the main loop
|
||||||
|
if (_reconfigureNTP) {
|
||||||
|
_reconfigureNTP = false;
|
||||||
|
configureNTP();
|
||||||
|
}
|
||||||
|
|
||||||
|
// output sync event to serial
|
||||||
|
if (_syncEventTriggered) {
|
||||||
|
processSyncEvent(_ntpEvent);
|
||||||
|
_syncEventTriggered = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep time synchronized in background
|
||||||
|
now();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NTPSettingsService::readFromJsonObject(JsonObject& root) {
|
||||||
|
_server = root["server"] | NTP_SETTINGS_SERVICE_DEFAULT_SERVER;
|
||||||
|
_interval = root["interval"];
|
||||||
|
|
||||||
|
// validate server is specified, resorting to default
|
||||||
|
_server.trim();
|
||||||
|
if (!_server){
|
||||||
|
_server = NTP_SETTINGS_SERVICE_DEFAULT_SERVER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure interval is in bounds
|
||||||
|
if (_interval < NTP_SETTINGS_MIN_INTERVAL){
|
||||||
|
_interval = NTP_SETTINGS_MIN_INTERVAL;
|
||||||
|
} else if (_interval > NTP_SETTINGS_MAX_INTERVAL) {
|
||||||
|
_interval = NTP_SETTINGS_MAX_INTERVAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NTPSettingsService::writeToJsonObject(JsonObject& root) {
|
||||||
|
root["server"] = _server;
|
||||||
|
root["interval"] = _interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NTPSettingsService::onConfigUpdated() {
|
||||||
|
_reconfigureNTP = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NTPSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP& event) {
|
||||||
|
Serial.printf("Got IP address, starting NTP Synchronization\n");
|
||||||
|
_reconfigureNTP = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NTPSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) {
|
||||||
|
Serial.printf("WiFi connection dropped, stopping NTP.\n");
|
||||||
|
|
||||||
|
// stop NTP synchronization, ensuring no re-configuration can take place
|
||||||
|
_reconfigureNTP = false;
|
||||||
|
NTP.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NTPSettingsService::configureNTP() {
|
||||||
|
Serial.println("Configuring NTP...");
|
||||||
|
|
||||||
|
// disable sync
|
||||||
|
NTP.stop();
|
||||||
|
|
||||||
|
// enable sync
|
||||||
|
NTP.begin(_server);
|
||||||
|
NTP.setInterval(_interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NTPSettingsService::processSyncEvent(NTPSyncEvent_t ntpEvent) {
|
||||||
|
if (ntpEvent) {
|
||||||
|
Serial.print ("Time Sync error: ");
|
||||||
|
if (ntpEvent == noResponse)
|
||||||
|
Serial.println ("NTP server not reachable");
|
||||||
|
else if (ntpEvent == invalidAddress)
|
||||||
|
Serial.println ("Invalid NTP server address");
|
||||||
|
} else {
|
||||||
|
Serial.print ("Got NTP time: ");
|
||||||
|
Serial.println (NTP.getTimeDateString (NTP.getLastNTPSync ()));
|
||||||
|
}
|
||||||
|
}
|
56
src/NTPSettingsService.h
Normal file
56
src/NTPSettingsService.h
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#ifndef NTPSettingsService_h
|
||||||
|
#define NTPSettingsService_h
|
||||||
|
|
||||||
|
#include <SettingsService.h>
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
|
||||||
|
#include <TimeLib.h>
|
||||||
|
#include <NtpClientLib.h>
|
||||||
|
|
||||||
|
// default time server
|
||||||
|
#define NTP_SETTINGS_SERVICE_DEFAULT_SERVER "pool.ntp.org"
|
||||||
|
#define NTP_SETTINGS_SERVICE_DEFAULT_INTERVAL 3600
|
||||||
|
|
||||||
|
// min poll delay of 60 secs, max 1 day
|
||||||
|
#define NTP_SETTINGS_MIN_INTERVAL 60
|
||||||
|
#define NTP_SETTINGS_MAX_INTERVAL 86400
|
||||||
|
|
||||||
|
#define NTP_SETTINGS_FILE "/config/ntpSettings.json"
|
||||||
|
#define NTP_SETTINGS_SERVICE_PATH "/ntpSettings"
|
||||||
|
|
||||||
|
class NTPSettingsService : public SettingsService {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
NTPSettingsService(AsyncWebServer* server, FS* fs);
|
||||||
|
~NTPSettingsService();
|
||||||
|
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
void readFromJsonObject(JsonObject& root);
|
||||||
|
void writeToJsonObject(JsonObject& root);
|
||||||
|
void onConfigUpdated();
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
WiFiEventHandler _onStationModeDisconnectedHandler;
|
||||||
|
WiFiEventHandler _onStationModeGotIPHandler;
|
||||||
|
|
||||||
|
String _server;
|
||||||
|
int _interval;
|
||||||
|
|
||||||
|
bool _reconfigureNTP = false;
|
||||||
|
bool _syncEventTriggered = false;
|
||||||
|
NTPSyncEvent_t _ntpEvent;
|
||||||
|
|
||||||
|
void onStationModeGotIP(const WiFiEventStationModeGotIP& event);
|
||||||
|
void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event);
|
||||||
|
|
||||||
|
void configureNTP();
|
||||||
|
void processSyncEvent(NTPSyncEvent_t ntpEvent);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end NTPSettingsService_h
|
28
src/NTPStatus.cpp
Normal file
28
src/NTPStatus.cpp
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#include <NTPStatus.h>
|
||||||
|
|
||||||
|
NTPStatus::NTPStatus(AsyncWebServer *server) : _server(server) {
|
||||||
|
_server->on("/ntpStatus", HTTP_GET, std::bind(&NTPStatus::ntpStatus, this, std::placeholders::_1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void NTPStatus::ntpStatus(AsyncWebServerRequest *request) {
|
||||||
|
AsyncJsonResponse * response = new AsyncJsonResponse();
|
||||||
|
JsonObject& root = response->getRoot();
|
||||||
|
|
||||||
|
// request time now first, this can sometimes force a sync
|
||||||
|
time_t timeNow = now();
|
||||||
|
timeStatus_t status = timeStatus();
|
||||||
|
time_t lastSync = NTP.getLastNTPSync();
|
||||||
|
root["status"] = (int) status;
|
||||||
|
root["last_sync"] = lastSync;
|
||||||
|
root["server"] = NTP.getNtpServerName();
|
||||||
|
root["interval"] = NTP.getInterval();
|
||||||
|
root["uptime"] = NTP.getUptime();
|
||||||
|
|
||||||
|
// only add now to response if we have successfully synced
|
||||||
|
if (status != timeNotSet){
|
||||||
|
root["now"] = timeNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
26
src/NTPStatus.h
Normal file
26
src/NTPStatus.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#ifndef NTPStatus_h
|
||||||
|
#define NTPStatus_h
|
||||||
|
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESPAsyncTCP.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <AsyncJson.h>
|
||||||
|
#include <TimeLib.h>
|
||||||
|
#include <NtpClientLib.h>
|
||||||
|
|
||||||
|
class NTPStatus {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
NTPStatus(AsyncWebServer *server);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
|
||||||
|
void ntpStatus(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end NTPStatus_h
|
67
src/OTASettingsService.cpp
Normal file
67
src/OTASettingsService.cpp
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#include <OTASettingsService.h>
|
||||||
|
|
||||||
|
OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs) : SettingsService(server, fs, OTA_SETTINGS_SERVICE_PATH, OTA_SETTINGS_FILE) {}
|
||||||
|
|
||||||
|
OTASettingsService::~OTASettingsService() {}
|
||||||
|
|
||||||
|
void OTASettingsService::begin() {
|
||||||
|
// load settings
|
||||||
|
SettingsService::begin();
|
||||||
|
|
||||||
|
// configure arduino OTA
|
||||||
|
configureArduinoOTA();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OTASettingsService::loop() {
|
||||||
|
if (_enabled && _arduinoOTA){
|
||||||
|
_arduinoOTA->handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OTASettingsService::onConfigUpdated() {
|
||||||
|
configureArduinoOTA();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OTASettingsService::readFromJsonObject(JsonObject& root) {
|
||||||
|
_enabled = root["enabled"];
|
||||||
|
_port = root["port"];
|
||||||
|
_password = root["password"] | DEFAULT_OTA_PASSWORD;
|
||||||
|
|
||||||
|
// provide defaults
|
||||||
|
if (_port < 0){
|
||||||
|
_port = DEFAULT_OTA_PORT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OTASettingsService::writeToJsonObject(JsonObject& root) {
|
||||||
|
root["enabled"] = _enabled;
|
||||||
|
root["port"] = _port;
|
||||||
|
root["password"] = _password;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OTASettingsService::configureArduinoOTA() {
|
||||||
|
delete _arduinoOTA;
|
||||||
|
if (_enabled) {
|
||||||
|
_arduinoOTA = new ArduinoOTAClass;
|
||||||
|
_arduinoOTA->setPort(_port);
|
||||||
|
_arduinoOTA->setPassword(_password.c_str());
|
||||||
|
_arduinoOTA->onStart([]() {
|
||||||
|
Serial.println("Starting");
|
||||||
|
});
|
||||||
|
_arduinoOTA->onEnd([]() {
|
||||||
|
Serial.println("\nEnd");
|
||||||
|
});
|
||||||
|
_arduinoOTA->onProgress([](unsigned int progress, unsigned int total) {
|
||||||
|
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
|
||||||
|
});
|
||||||
|
_arduinoOTA->onError([](ota_error_t error) {
|
||||||
|
Serial.printf("Error[%u]: ", error);
|
||||||
|
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
|
||||||
|
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
|
||||||
|
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
|
||||||
|
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
|
||||||
|
else if (error == OTA_END_ERROR) Serial.println("End Failed");
|
||||||
|
});
|
||||||
|
_arduinoOTA->begin();
|
||||||
|
}
|
||||||
|
}
|
44
src/OTASettingsService.h
Normal file
44
src/OTASettingsService.h
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#ifndef OTASettingsService_h
|
||||||
|
#define OTASettingsService_h
|
||||||
|
|
||||||
|
#include <SettingsService.h>
|
||||||
|
#include <ESP8266WiFi.h> // ??
|
||||||
|
#include <ESP8266mDNS.h>
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include <ArduinoOTA.h>
|
||||||
|
|
||||||
|
// Emergency defaults
|
||||||
|
#define DEFAULT_OTA_PORT 8266
|
||||||
|
#define DEFAULT_OTA_PASSWORD "esp-react"
|
||||||
|
|
||||||
|
#define OTA_SETTINGS_FILE "/config/otaSettings.json"
|
||||||
|
#define OTA_SETTINGS_SERVICE_PATH "/otaSettings"
|
||||||
|
|
||||||
|
class OTASettingsService : public SettingsService {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
OTASettingsService(AsyncWebServer* server, FS* fs);
|
||||||
|
~OTASettingsService();
|
||||||
|
|
||||||
|
void begin();
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
void onConfigUpdated();
|
||||||
|
void readFromJsonObject(JsonObject& root);
|
||||||
|
void writeToJsonObject(JsonObject& root);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
ArduinoOTAClass *_arduinoOTA;
|
||||||
|
bool _enabled;
|
||||||
|
int _port;
|
||||||
|
String _password;
|
||||||
|
|
||||||
|
void configureArduinoOTA();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end NTPSettingsService_h
|
146
src/SettingsService.h
Normal file
146
src/SettingsService.h
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
#ifndef SettingsService_h
|
||||||
|
#define SettingsService_h
|
||||||
|
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESPAsyncTCP.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <FS.h>
|
||||||
|
#include <AsyncJson.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <AsyncJsonRequestWebHandler.h>
|
||||||
|
#include <AsyncJsonCallbackResponse.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* At the moment, not expecting settings service to have to deal with large JSON
|
||||||
|
* files this could be made configurable fairly simply, it's exposed on
|
||||||
|
* AsyncJsonRequestWebHandler with a setter.
|
||||||
|
*/
|
||||||
|
#define MAX_SETTINGS_SIZE 1024
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Abstraction of a service which stores it's settings as JSON in SPIFFS.
|
||||||
|
*/
|
||||||
|
class SettingsService {
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
char const* _filePath;
|
||||||
|
|
||||||
|
AsyncJsonRequestWebHandler _updateHandler;
|
||||||
|
|
||||||
|
bool writeToSPIFFS() {
|
||||||
|
// create and populate a new json object
|
||||||
|
DynamicJsonBuffer jsonBuffer;
|
||||||
|
JsonObject& root = jsonBuffer.createObject();
|
||||||
|
writeToJsonObject(root);
|
||||||
|
|
||||||
|
// serialize it to SPIFFS
|
||||||
|
File configFile = SPIFFS.open(_filePath, "w");
|
||||||
|
|
||||||
|
// failed to open file, return false
|
||||||
|
if (!configFile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.printTo(configFile);
|
||||||
|
configFile.close();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void readFromSPIFFS(){
|
||||||
|
File configFile = SPIFFS.open(_filePath, "r");
|
||||||
|
|
||||||
|
// use defaults if no config found
|
||||||
|
if (configFile) {
|
||||||
|
// Protect against bad data uploaded to SPIFFS
|
||||||
|
// We never expect the config file to get very large, so cap it.
|
||||||
|
size_t size = configFile.size();
|
||||||
|
if (size <= MAX_SETTINGS_SIZE) {
|
||||||
|
DynamicJsonBuffer jsonBuffer;
|
||||||
|
JsonObject& root = jsonBuffer.parseObject(configFile);
|
||||||
|
if (root.success()) {
|
||||||
|
readFromJsonObject(root);
|
||||||
|
configFile.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reach here we have not been successful in loading the config,
|
||||||
|
// hard-coded emergency defaults are now applied.
|
||||||
|
applyDefaultConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetchConfig(AsyncWebServerRequest *request){
|
||||||
|
AsyncJsonResponse * response = new AsyncJsonResponse();
|
||||||
|
writeToJsonObject(response->getRoot());
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateConfig(AsyncWebServerRequest *request, JsonVariant &json){
|
||||||
|
if (json.is<JsonObject>()){
|
||||||
|
JsonObject& newConfig = json.as<JsonObject>();
|
||||||
|
readFromJsonObject(newConfig);
|
||||||
|
writeToSPIFFS();
|
||||||
|
|
||||||
|
// write settings back with a callback to reconfigure the wifi
|
||||||
|
AsyncJsonCallbackResponse * response = new AsyncJsonCallbackResponse([this] () {onConfigUpdated();});
|
||||||
|
writeToJsonObject(response->getRoot());
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
} else{
|
||||||
|
request->send(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
// will serve setting endpoints from here
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
|
||||||
|
// will store and retrieve config from the file system
|
||||||
|
FS* _fs;
|
||||||
|
|
||||||
|
// reads the local config from the
|
||||||
|
virtual void readFromJsonObject(JsonObject& root){}
|
||||||
|
virtual void writeToJsonObject(JsonObject& root){}
|
||||||
|
|
||||||
|
// implement to perform action when config has been updated
|
||||||
|
virtual void onConfigUpdated(){}
|
||||||
|
|
||||||
|
// We assume the readFromJsonObject supplies sensible defaults if an empty object
|
||||||
|
// is supplied, this virtual function allows that to be changed.
|
||||||
|
virtual void applyDefaultConfig(){
|
||||||
|
DynamicJsonBuffer jsonBuffer;
|
||||||
|
JsonObject& root = jsonBuffer.createObject();
|
||||||
|
readFromJsonObject(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
SettingsService(AsyncWebServer* server, FS* fs, char const* servicePath, char const* filePath):
|
||||||
|
_server(server), _fs(fs), _filePath(filePath) {
|
||||||
|
|
||||||
|
// configure fetch config handler
|
||||||
|
_server->on(servicePath, HTTP_GET, std::bind(&SettingsService::fetchConfig, this, std::placeholders::_1));
|
||||||
|
|
||||||
|
// configure update settings handler
|
||||||
|
_updateHandler.setUri(servicePath);
|
||||||
|
_updateHandler.setMethod(HTTP_POST);
|
||||||
|
_updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE);
|
||||||
|
_updateHandler.onRequest(std::bind(&SettingsService::updateConfig, this, std::placeholders::_1, std::placeholders::_2));
|
||||||
|
_server->addHandler(&_updateHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~SettingsService() {}
|
||||||
|
|
||||||
|
virtual void begin() {
|
||||||
|
readFromSPIFFS();
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end SettingsService
|
38
src/WiFiScanner.cpp
Normal file
38
src/WiFiScanner.cpp
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#include <WiFiScanner.h>
|
||||||
|
|
||||||
|
WiFiScanner::WiFiScanner(AsyncWebServer *server) : _server(server) {
|
||||||
|
_server->on("/scanNetworks", HTTP_GET, std::bind(&WiFiScanner::scanNetworks, this, std::placeholders::_1));
|
||||||
|
_server->on("/listNetworks", HTTP_GET, std::bind(&WiFiScanner::listNetworks, this, std::placeholders::_1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiScanner::scanNetworks(AsyncWebServerRequest *request) {
|
||||||
|
if (WiFi.scanComplete() != -1){
|
||||||
|
WiFi.scanDelete();
|
||||||
|
WiFi.scanNetworks(true);
|
||||||
|
}
|
||||||
|
request->send(202);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiScanner::listNetworks(AsyncWebServerRequest *request) {
|
||||||
|
int numNetworks = WiFi.scanComplete();
|
||||||
|
if (numNetworks > -1){
|
||||||
|
AsyncJsonResponse * response = new AsyncJsonResponse();
|
||||||
|
JsonObject& root = response->getRoot();
|
||||||
|
JsonArray& networks = root.createNestedArray("networks");
|
||||||
|
for (int i=0; i<numNetworks ; i++){
|
||||||
|
JsonObject& network = networks.createNestedObject();
|
||||||
|
network["rssi"] = WiFi.RSSI(i);
|
||||||
|
network["ssid"] = WiFi.SSID(i);
|
||||||
|
network["bssid"] = WiFi.BSSIDstr(i);
|
||||||
|
network["channel"] = WiFi.channel(i);
|
||||||
|
network["encryption_type"] = WiFi.encryptionType(i);
|
||||||
|
network["hidden"] = WiFi.isHidden(i);
|
||||||
|
}
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
} else if (numNetworks == -1){
|
||||||
|
request->send(202);
|
||||||
|
}else{
|
||||||
|
scanNetworks(request);
|
||||||
|
}
|
||||||
|
}
|
26
src/WiFiScanner.h
Normal file
26
src/WiFiScanner.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#ifndef WiFiScanner_h
|
||||||
|
#define WiFiScanner_h
|
||||||
|
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESPAsyncTCP.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <AsyncJson.h>
|
||||||
|
#include <TimeLib.h>
|
||||||
|
|
||||||
|
class WiFiScanner {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
WiFiScanner(AsyncWebServer *server);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
|
||||||
|
void scanNetworks(AsyncWebServerRequest *request);
|
||||||
|
void listNetworks(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end WiFiScanner_h
|
85
src/WiFiSettingsService.cpp
Normal file
85
src/WiFiSettingsService.cpp
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
#include <WiFiSettingsService.h>
|
||||||
|
|
||||||
|
WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs) : SettingsService(server, fs, WIFI_SETTINGS_SERVICE_PATH, WIFI_SETTINGS_FILE) {
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiSettingsService::~WiFiSettingsService() {}
|
||||||
|
|
||||||
|
void WiFiSettingsService::begin() {
|
||||||
|
SettingsService::begin();
|
||||||
|
reconfigureWiFiConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiSettingsService::readFromJsonObject(JsonObject& root){
|
||||||
|
_ssid = root["ssid"] | "";
|
||||||
|
_password = root["password"] | "";
|
||||||
|
_hostname = root["hostname"] | "";
|
||||||
|
_staticIPConfig = root["static_ip_config"] | false;
|
||||||
|
|
||||||
|
// extended settings
|
||||||
|
readIP(root, "local_ip", _localIP);
|
||||||
|
readIP(root, "gateway_ip", _gatewayIP);
|
||||||
|
readIP(root, "subnet_mask", _subnetMask);
|
||||||
|
readIP(root, "dns_ip_1", _dnsIP1);
|
||||||
|
readIP(root, "dns_ip_2", _dnsIP2);
|
||||||
|
|
||||||
|
// Swap around the dns servers if 2 is populated but 1 is not
|
||||||
|
if (_dnsIP1 == 0U && _dnsIP2 != 0U){
|
||||||
|
_dnsIP1 = _dnsIP2;
|
||||||
|
_dnsIP2 = 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turning off static ip config if we don't meet the minimum requirements
|
||||||
|
// of ipAddress, gateway and subnet. This may change to static ip only
|
||||||
|
// as sensible defaults can be assumed for gateway and subnet
|
||||||
|
if (_staticIPConfig && (_localIP == 0U || _gatewayIP == 0U || _subnetMask == 0U)){
|
||||||
|
_staticIPConfig = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiSettingsService::writeToJsonObject(JsonObject& root){
|
||||||
|
// connection settings
|
||||||
|
root["ssid"] = _ssid;
|
||||||
|
root["password"] = _password;
|
||||||
|
root["hostname"] = _hostname;
|
||||||
|
root["static_ip_config"] = _staticIPConfig;
|
||||||
|
|
||||||
|
// extended settings
|
||||||
|
writeIP(root, "local_ip", _localIP);
|
||||||
|
writeIP(root, "gateway_ip", _gatewayIP);
|
||||||
|
writeIP(root, "subnet_mask", _subnetMask);
|
||||||
|
writeIP(root, "dns_ip_1", _dnsIP1);
|
||||||
|
writeIP(root, "dns_ip_2", _dnsIP2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiSettingsService::onConfigUpdated() {
|
||||||
|
reconfigureWiFiConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiSettingsService::reconfigureWiFiConnection() {
|
||||||
|
Serial.println("Reconfiguring WiFi...");
|
||||||
|
|
||||||
|
// disconnect and de-configure wifi and software access point
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
|
||||||
|
// configure static ip config for station mode (if set)
|
||||||
|
if (_staticIPConfig) {
|
||||||
|
WiFi.config(_localIP, _gatewayIP, _subnetMask, _dnsIP1, _dnsIP2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to the network
|
||||||
|
WiFi.hostname(_hostname);
|
||||||
|
WiFi.begin(_ssid.c_str(), _password.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiSettingsService::readIP(JsonObject& root, String key, IPAddress& _ip){
|
||||||
|
if (!root[key] || !_ip.fromString(root[key].as<String>())){
|
||||||
|
_ip = 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiSettingsService::writeIP(JsonObject& root, String key, IPAddress& _ip){
|
||||||
|
if (_ip != 0U){
|
||||||
|
root[key] = _ip.toString();
|
||||||
|
}
|
||||||
|
}
|
46
src/WiFiSettingsService.h
Normal file
46
src/WiFiSettingsService.h
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#ifndef WiFiSettingsService_h
|
||||||
|
#define WiFiSettingsService_h
|
||||||
|
|
||||||
|
#include <IPAddress.h>
|
||||||
|
#include <SettingsService.h>
|
||||||
|
|
||||||
|
#define WIFI_SETTINGS_FILE "/config/wifiSettings.json"
|
||||||
|
#define WIFI_SETTINGS_SERVICE_PATH "/wifiSettings"
|
||||||
|
|
||||||
|
class WiFiSettingsService : public SettingsService {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
WiFiSettingsService(AsyncWebServer* server, FS* fs);
|
||||||
|
~WiFiSettingsService();
|
||||||
|
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
void readFromJsonObject(JsonObject& root);
|
||||||
|
void writeToJsonObject(JsonObject& root);
|
||||||
|
void onConfigUpdated();
|
||||||
|
|
||||||
|
void reconfigureWiFiConnection();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// connection settings
|
||||||
|
String _ssid;
|
||||||
|
String _password;
|
||||||
|
String _hostname;
|
||||||
|
bool _staticIPConfig;
|
||||||
|
|
||||||
|
// optional configuration for static IP address
|
||||||
|
IPAddress _localIP;
|
||||||
|
IPAddress _gatewayIP;
|
||||||
|
IPAddress _subnetMask;
|
||||||
|
IPAddress _dnsIP1;
|
||||||
|
IPAddress _dnsIP2;
|
||||||
|
|
||||||
|
void readIP(JsonObject& root, String key, IPAddress& _ip);
|
||||||
|
void writeIP(JsonObject& root, String key, IPAddress& _ip);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end WiFiSettingsService_h
|
52
src/WiFiStatus.cpp
Normal file
52
src/WiFiStatus.cpp
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#include <WiFiStatus.h>
|
||||||
|
|
||||||
|
WiFiStatus::WiFiStatus(AsyncWebServer *server) : _server(server) {
|
||||||
|
_server->on("/wifiStatus", HTTP_GET, std::bind(&WiFiStatus::wifiStatus, this, std::placeholders::_1));
|
||||||
|
|
||||||
|
_onStationModeConnectedHandler = WiFi.onStationModeConnected(onStationModeConnected);
|
||||||
|
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(onStationModeDisconnected);
|
||||||
|
_onStationModeGotIPHandler = WiFi.onStationModeGotIP(onStationModeGotIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiStatus::onStationModeConnected(const WiFiEventStationModeConnected& event) {
|
||||||
|
Serial.print("WiFi Connected. SSID=");
|
||||||
|
Serial.println(event.ssid);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiStatus::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) {
|
||||||
|
Serial.print("WiFi Disconnected. Reason code=");
|
||||||
|
Serial.println(event.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiStatus::onStationModeGotIP(const WiFiEventStationModeGotIP& event) {
|
||||||
|
Serial.print("WiFi Got IP. localIP=");
|
||||||
|
Serial.print(event.ip);
|
||||||
|
Serial.print(", hostName=");
|
||||||
|
Serial.println(WiFi.hostname());
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiStatus::wifiStatus(AsyncWebServerRequest *request) {
|
||||||
|
AsyncJsonResponse * response = new AsyncJsonResponse();
|
||||||
|
JsonObject& root = response->getRoot();
|
||||||
|
wl_status_t status = WiFi.status();
|
||||||
|
root["status"] = (uint8_t) status;
|
||||||
|
if (status == WL_CONNECTED){
|
||||||
|
root["local_ip"] = WiFi.localIP().toString();
|
||||||
|
root["rssi"] = WiFi.RSSI();
|
||||||
|
root["ssid"] = WiFi.SSID();
|
||||||
|
root["bssid"] = WiFi.BSSIDstr();
|
||||||
|
root["channel"] = WiFi.channel();
|
||||||
|
root["subnet_mask"] = WiFi.subnetMask().toString();
|
||||||
|
root["gateway_ip"] = WiFi.gatewayIP().toString();
|
||||||
|
IPAddress dnsIP1 = WiFi.dnsIP(0);
|
||||||
|
IPAddress dnsIP2 = WiFi.dnsIP(1);
|
||||||
|
if (dnsIP1 != 0U){
|
||||||
|
root["dns_ip_1"] = dnsIP1.toString();
|
||||||
|
}
|
||||||
|
if (dnsIP2 != 0U){
|
||||||
|
root["dns_ip_2"] = dnsIP2.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
35
src/WiFiStatus.h
Normal file
35
src/WiFiStatus.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#ifndef WiFiStatus_h
|
||||||
|
#define WiFiStatus_h
|
||||||
|
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESPAsyncTCP.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <AsyncJson.h>
|
||||||
|
#include <IPAddress.h>
|
||||||
|
|
||||||
|
class WiFiStatus {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
WiFiStatus(AsyncWebServer *server);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
|
||||||
|
// handler refrences for logging important WiFi events over serial
|
||||||
|
WiFiEventHandler _onStationModeConnectedHandler;
|
||||||
|
WiFiEventHandler _onStationModeDisconnectedHandler;
|
||||||
|
WiFiEventHandler _onStationModeGotIPHandler;
|
||||||
|
|
||||||
|
// static functions for logging wifi events to the UART
|
||||||
|
static void onStationModeConnected(const WiFiEventStationModeConnected& event);
|
||||||
|
static void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event);
|
||||||
|
static void onStationModeGotIP(const WiFiEventStationModeGotIP& event);
|
||||||
|
|
||||||
|
void wifiStatus(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end WiFiStatus_h
|
62
src/main.cpp
Normal file
62
src/main.cpp
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESPAsyncTCP.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <FS.h>
|
||||||
|
#include <WiFiSettingsService.h>
|
||||||
|
#include <WiFiStatus.h>
|
||||||
|
#include <WiFiScanner.h>
|
||||||
|
#include <APSettingsService.h>
|
||||||
|
#include <NTPSettingsService.h>
|
||||||
|
#include <NTPStatus.h>
|
||||||
|
#include <OTASettingsService.h>
|
||||||
|
#include <APStatus.h>
|
||||||
|
|
||||||
|
#define SERIAL_BAUD_RATE 115200
|
||||||
|
|
||||||
|
AsyncWebServer server(80);
|
||||||
|
|
||||||
|
WiFiSettingsService wifiSettingsService = WiFiSettingsService(&server, &SPIFFS);
|
||||||
|
WiFiStatus wifiStatus = WiFiStatus(&server);
|
||||||
|
WiFiScanner wifiScanner = WiFiScanner(&server);
|
||||||
|
APSettingsService apSettingsService = APSettingsService(&server, &SPIFFS);
|
||||||
|
NTPSettingsService ntpSettingsService = NTPSettingsService(&server, &SPIFFS);
|
||||||
|
OTASettingsService otaSettingsService = OTASettingsService(&server, &SPIFFS);
|
||||||
|
NTPStatus ntpStatus = NTPStatus(&server);
|
||||||
|
APStatus apStatus = APStatus(&server);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Disable wifi config persistance
|
||||||
|
WiFi.persistent(false);
|
||||||
|
|
||||||
|
Serial.begin(SERIAL_BAUD_RATE);
|
||||||
|
SPIFFS.begin();
|
||||||
|
|
||||||
|
// start services
|
||||||
|
ntpSettingsService.begin();
|
||||||
|
otaSettingsService.begin();
|
||||||
|
apSettingsService.begin();
|
||||||
|
wifiSettingsService.begin();
|
||||||
|
|
||||||
|
// Serving static resources from /www/
|
||||||
|
server.serveStatic("/js/", SPIFFS, "/www/js/");
|
||||||
|
server.serveStatic("/css/", SPIFFS, "/www/css/");
|
||||||
|
server.serveStatic("/fonts/", SPIFFS, "/www/fonts/");
|
||||||
|
server.serveStatic("/app/", SPIFFS, "/www/app/");
|
||||||
|
|
||||||
|
// Serving all other get requests with "/www/index.htm"
|
||||||
|
server.onNotFound([](AsyncWebServerRequest *request) {
|
||||||
|
if (request->method() == HTTP_GET) {
|
||||||
|
request->send(SPIFFS, "/www/index.html");
|
||||||
|
} else {
|
||||||
|
request->send(404);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
apSettingsService.loop();
|
||||||
|
ntpSettingsService.loop();
|
||||||
|
otaSettingsService.loop();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user