initial commit of C++ back end and react front end

This commit is contained in:
rjwats@gmail.com 2018-02-26 00:11:31 +00:00
commit 63a639eb22
87 changed files with 14975 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.pioenvs
.piolibdeps
.clang_complete
.gcc-flags.json
*Thumbs.db
/interface/build
/interface/node_modules

67
.travis.yml Normal file
View 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
View 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.

View File

@ -0,0 +1,5 @@
{
"provision_mode": 0,
"ssid": "ESP8266-React",
"password": "esp-react"
}

View File

@ -0,0 +1,4 @@
{
"server":"pool.ntp.org",
"interval":60
}

View File

@ -0,0 +1,5 @@
{
"enabled":true,
"port": 8266,
"password": "esp-react"
}

View File

@ -0,0 +1,6 @@
{
"ssid":"ssid",
"password":"password",
"hostname":"esp8266-react",
"static_ip_config":false
}

BIN
data/www/app/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View 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
View 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

Binary file not shown.

BIN
data/www/fonts/ro-me.w2 Normal file

Binary file not shown.

BIN
data/www/fonts/ro-re.w2 Normal file

Binary file not shown.

BIN
data/www/index.html.gz Normal file

Binary file not shown.

BIN
data/www/js/main.73ac.js.gz Normal file

Binary file not shown.

View 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

File diff suppressed because it is too large Load Diff

29
interface/package.json Normal file
View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View 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"
}

View 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;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
View 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

View 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;

View 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);

View 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);

View 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);

View 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;

View File

@ -0,0 +1,4 @@
export const IDLE = "idle";
export const SUCCESS = "success";
export const ERROR = "error";
export const WARN = "warn";

View 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";
}
}

View 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);

View 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;

View 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";
}
}

View 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";
}
}

View 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;

View 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;

View 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);

View 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

View 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;

View 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);

View 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

View 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;

View 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;

View 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);

View 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;

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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});
});
}

View 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
View 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
View 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"))

View 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);
}

View 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);
}

View File

@ -0,0 +1 @@
export default validator => value => !value || validator(value);

View File

@ -0,0 +1 @@
export default (validator1, validator2) => value => validator1(value) || validator2(value);

36
lib/readme.txt Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

49
src/APSettingsService.cpp Normal file
View 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
View 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
View 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
View 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

View 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_

View 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_

View 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
View 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

View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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();
}