Browse Source

Merge pull request #49 from rjwats/ft_demo_project

Ft demo project
master
rjwats 5 years ago
committed by GitHub
parent
commit
67eb9d4017
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 91
      README.md
  2. 8
      data/config/apSettings.json
  3. 3
      data/config/demoSettings.json
  4. 6
      data/config/ntpSettings.json
  5. 8
      data/config/otaSettings.json
  6. 26
      data/config/securitySettings.json
  7. 10
      data/config/wifiSettings.json
  8. 6
      interface/.env
  9. 4
      interface/.env.development
  10. 2522
      interface/package-lock.json
  11. 16
      interface/package.json
  12. 5
      interface/src/AppRouting.js
  13. 3
      interface/src/authentication/Authentication.js
  14. 58
      interface/src/components/LoadingNotification.js
  15. 12
      interface/src/components/MenuAppBar.js
  16. 20
      interface/src/components/RestComponent.js
  17. 9
      interface/src/components/SectionContent.js
  18. 1
      interface/src/constants/App.js
  19. 2
      interface/src/constants/Endpoints.js
  20. 3
      interface/src/constants/Env.js
  21. 22
      interface/src/containers/APSettings.js
  22. 42
      interface/src/containers/APStatus.js
  23. 26
      interface/src/containers/ManageUsers.js
  24. 26
      interface/src/containers/NTPSettings.js
  25. 40
      interface/src/containers/NTPStatus.js
  26. 28
      interface/src/containers/OTASettings.js
  27. 20
      interface/src/containers/SecuritySettings.js
  28. 4
      interface/src/containers/SignInPage.js
  29. 42
      interface/src/containers/SystemStatus.js
  30. 2
      interface/src/containers/WiFiNetworkScanner.js
  31. 36
      interface/src/containers/WiFiSettings.js
  32. 42
      interface/src/containers/WiFiStatus.js
  33. 130
      interface/src/forms/APSettingsForm.js
  34. 174
      interface/src/forms/ManageUsersForm.js
  35. 103
      interface/src/forms/NTPSettingsForm.js
  36. 124
      interface/src/forms/OTASettingsForm.js
  37. 76
      interface/src/forms/SecuritySettingsForm.js
  38. 4
      interface/src/forms/WiFiNetworkSelector.js
  39. 258
      interface/src/forms/WiFiSettingsForm.js
  40. 82
      interface/src/project/DemoController.js
  41. 100
      interface/src/project/DemoInformation.js
  42. 37
      interface/src/project/DemoProject.js
  43. 30
      interface/src/project/ProjectMenu.js
  44. 32
      interface/src/project/ProjectRouting.js
  45. 1
      interface/src/sections/NetworkTime.js
  46. 12
      lib/framework/APSettingsService.cpp
  47. 5
      lib/framework/APSettingsService.h
  48. 6
      lib/framework/APStatus.cpp
  49. 7
      lib/framework/APStatus.h
  50. 45
      lib/framework/AdminSettingsService.h
  51. 0
      lib/framework/ArduinoJsonJWT.cpp
  52. 0
      lib/framework/ArduinoJsonJWT.h
  53. 0
      lib/framework/AsyncArduinoJson6.h
  54. 0
      lib/framework/AsyncJsonWebHandler.h
  55. 3
      lib/framework/AuthenticationService.cpp
  56. 5
      lib/framework/AuthenticationService.h
  57. 55
      lib/framework/ESP8266React.cpp
  58. 59
      lib/framework/ESP8266React.h
  59. 0
      lib/framework/NTPSettingsService.cpp
  60. 2
      lib/framework/NTPSettingsService.h
  61. 6
      lib/framework/NTPStatus.cpp
  62. 5
      lib/framework/NTPStatus.h
  63. 0
      lib/framework/OTASettingsService.cpp
  64. 4
      lib/framework/OTASettingsService.h
  65. 0
      lib/framework/SecurityManager.cpp
  66. 0
      lib/framework/SecurityManager.h
  67. 4
      lib/framework/SecuritySettingsService.cpp
  68. 4
      lib/framework/SecuritySettingsService.h
  69. 2
      lib/framework/SettingsPersistence.h
  70. 57
      lib/framework/SettingsService.h
  71. 27
      lib/framework/SimpleService.h
  72. 8
      lib/framework/SystemStatus.cpp
  73. 7
      lib/framework/SystemStatus.h
  74. 8
      lib/framework/WiFiScanner.cpp
  75. 2
      lib/framework/WiFiScanner.h
  76. 7
      lib/framework/WiFiSettingsService.cpp
  77. 2
      lib/framework/WiFiSettingsService.h
  78. 8
      lib/framework/WiFiStatus.cpp
  79. 5
      lib/framework/WiFiStatus.h
  80. 26
      src/DemoProject.cpp
  81. 34
      src/DemoProject.h
  82. 99
      src/main.cpp

91
README.md

@ -42,8 +42,9 @@ Resource | Description
---- | -----------
[data/](data) | The file system image directory
[interface/](interface) | React based front end
[src/](src) | C++ back end for the ESP8266 device
[src/](src) | The main.cpp and demo project to get you started
[platformio.ini](platformio.ini) | PlatformIO project configuration file
[lib/framework/](lib/framework) | C++ back end for the ESP8266 device
### Building the firmware
@ -247,13 +248,92 @@ There is also a manifest file which contains the app name to use when adding the
}
```
## Back End Overview
## Back end overview
The back end is a set of REST endpoints hosted by a [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) instance. The source is split up by feature, for example [WiFiScanner.h](src/WiFiScanner.h) implements the end points for scanning for available networks.
The back end is a set of REST endpoints hosted by a [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) instance. The ['lib/framework'](lib/framework) directory contains the majority of the back end code. The framework contains of a number of useful utility classes which you can use when extending it. The project also comes with a demo project to give you some help getting started.
There is an abstract class [SettingsService.h](src/SettingsService.h) that provides an easy means of adding configurable services/features to the device. It takes care of writing the settings as JSON to SPIFFS. All you need to do is extend the class with your required configuration and implement the functions which serialize the settings to/from JSON. JSON serialization utilizes the excellent [ArduinoJson](https://github.com/bblanchon/ArduinoJson) library.
The framework's source is split up by feature, for example [WiFiScanner.h](lib/framework/WiFiScanner.h) implements the end points for scanning for available networks where as [WiFiSettingsService.h](lib/framework/WiFiSettingsService.h) handles configuring the WiFi settings and managing the WiFi connection.
Here is a example of a service with username and password settings:
### Initializing the framework
The ['src/main.cpp'](src/main.cpp) file constructs the webserver and initializes the framework. You can add endpoints to the server here to support your IoT project. The main loop is also accessable so you can run your own code easily.
The following code creates the web server, esp8266React framework and the demo project instance:
```cpp
AsyncWebServer server(80);
ESP8266React esp8266React(&server, &SPIFFS);
DemoProject demoProject = DemoProject(&server, &SPIFFS, esp8266React.getSecurityManager());
```
Now in the `setup()` function the initialization is performed:
```cpp
void setup() {
// start serial and filesystem
Serial.begin(SERIAL_BAUD_RATE);
// start the file system (must be done before starting the framework)
SPIFFS.begin();
// start the framework and demo project
esp8266React.begin();
// start the demo project
demoProject.begin();
// start the server
server.begin();
}
```
Finally the loop calls the framework's loop function to service the frameworks features. You can add your own code in here, as shown with the demo project:
```cpp
void loop() {
// run the framework's loop function
esp8266React.loop();
// run the demo project's loop function
demoProject.loop();
}
```
### Adding endpoints
There are some simple classes that support adding configurable services/features to the device:
Class | Description
----- | -----------
[SimpleService.h](lib/framework/SimpleService.h) | Exposes an endpoint to read and write settings as JSON. Extend this class and implement the functions which serialize the settings to/from JSON.
[SettingsService.h](lib/framework/SettingsService.h) | As above, however this class also handles persisting the settings as JSON to the file system.
[AdminSettingsService.h](lib/framework/AdminSettingsService.h) | Extends SettingsService to secure the endpoint to administrators only, the authentication predicate can be overridden if required.
The demo project shows how these can be used, explore the framework classes for more examples.
### Security features
The framework has security features to prevent unauthorized use of the device. This is driven by [SecurityManager.h](lib/framework/SecurityManager.h).
On successful authentication, the /rest/signIn endpoint issues a JWT which is then sent using Bearer Authentication. The framework come with built in predicates for verifying a users access level. The built in AuthenticationPredicates can be found in [SecurityManager.h](lib/framework/SecurityManager.h):
Predicate | Description
-------------------- | -----------
NONE_REQUIRED | No authentication is required.
IS_AUTHENTICATED | Any authenticated principal is permitted.
IS_ADMIN | The authenticated principal must be an admin.
You can use the security manager to wrap any web handler with an authentication predicate:
```cpp
server->on("/rest/someService", HTTP_GET,
_securityManager->wrapRequest(std::bind(&SomeService::someService, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
);
```
Alternatively you can extend [AdminSettingsService.h](lib/framework/AdminSettingsService.h) and optionally override `getAuthenticationPredicate()` to secure an endpoint.
## Extending the framework
```cpp
#include <SettingsService.h>
@ -321,6 +401,7 @@ void reconfigureTheService() {
* [React](https://reactjs.org/)
* [Material-UI](https://material-ui-next.com/)
* [notistack](https://github.com/iamhosseindhv/notistack)
* [Time](https://github.com/PaulStoffregen/Time)
* [NtpClient](https://github.com/gmag11/NtpClient)
* [ArduinoJson](https://github.com/bblanchon/ArduinoJson)

8
data/config/apSettings.json

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

3
data/config/demoSettings.json

@ -0,0 +1,3 @@
{
"blink_speed": 100
}

6
data/config/ntpSettings.json

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

8
data/config/otaSettings.json

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

26
data/config/securitySettings.json

@ -1,15 +1,15 @@
{
"jwt_secret":"esp8266-react",
"users": [
{
"username": "admin",
"password": "admin",
"admin": true
},
{
"username": "guest",
"password": "guest",
"admin": false
}
]
"jwt_secret": "esp8266-react",
"users": [
{
"username": "admin",
"password": "admin",
"admin": true
},
{
"username": "guest",
"password": "guest",
"admin": false
}
]
}

10
data/config/wifiSettings.json

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

6
interface/.env

@ -1 +1,5 @@
REACT_APP_NAME=ESP8266 React
# This is the name of your project. It appears on the sign-in page and in the menu bar.
REACT_APP_PROJECT_NAME=ESP8266 React
# This is the url path your project will be exposed under.
REACT_APP_PROJECT_PATH=project

4
interface/.env.development

@ -1 +1,3 @@
REACT_APP_ENDPOINT_ROOT=http://192.168.0.11/rest/
# Change the IP address to that of your ESP device to enable local development of the UI.
# Remember to also enable CORS in platformio.ini before uploading the code to the device.
REACT_APP_ENDPOINT_ROOT=http://192.168.0.20/rest/

2522
interface/package-lock.json
File diff suppressed because it is too large
View File

16
interface/package.json

@ -3,20 +3,20 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.3.1",
"@material-ui/icons": "^4.2.1",
"@material-ui/core": "^4.4.3",
"@material-ui/icons": "^4.4.3",
"compression-webpack-plugin": "^2.0.0",
"jwt-decode": "^2.2.0",
"moment": "^2.24.0",
"notistack": "^0.8.9",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-form-validator-core": "^0.6.3",
"react-jss": "^10.0.0-alpha.23",
"react": "^16.10.1",
"react-dom": "^16.10.1",
"react-form-validator-core": "^0.6.4",
"react-jss": "^10.0.0",
"react-material-ui-form-validator": "^2.0.9",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1",
"react-router": "^5.1.1",
"react-router-dom": "^5.1.1",
"react-scripts": "3.0.1"
},
"scripts": {

5
interface/src/AppRouting.js

@ -2,18 +2,18 @@ import React, { Component } from 'react';
import { Redirect, Switch } from 'react-router';
import { PROJECT_PATH } from './constants/Env';
import * as Authentication from './authentication/Authentication';
import AuthenticationWrapper from './authentication/AuthenticationWrapper';
import AuthenticatedRoute from './authentication/AuthenticatedRoute';
import UnauthenticatedRoute from './authentication/UnauthenticatedRoute';
import SignInPage from './containers/SignInPage';
import WiFiConnection from './sections/WiFiConnection';
import AccessPoint from './sections/AccessPoint';
import NetworkTime from './sections/NetworkTime';
import Security from './sections/Security';
import System from './sections/System';
import ProjectRouting from './project/ProjectRouting';
class AppRouting extends Component {
@ -31,6 +31,7 @@ class AppRouting extends Component {
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
<AuthenticatedRoute exact path="/security/*" component={Security} />
<AuthenticatedRoute exact path="/system/*" component={System} />
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
<Redirect to="/" />
</Switch>
</AuthenticationWrapper>

3
interface/src/authentication/Authentication.js

@ -1,4 +1,5 @@
import history from '../history';
import { PROJECT_PATH } from '../constants/Env';
export const ACCESS_TOKEN = 'access_token';
export const LOGIN_PATHNAME = 'loginPathname';
@ -21,7 +22,7 @@ export function fetchLoginRedirect() {
const loginSearch = localStorage.getItem(LOGIN_SEARCH);
clearLoginRedirect();
return {
pathname: loginPathname || "/wifi/",
pathname: loginPathname || `/${PROJECT_PATH}/`,
search: (loginPathname && loginSearch) || undefined
};
}

58
interface/src/components/LoadingNotification.js

@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
const useStyles = makeStyles(theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
}
}));
export default function LoadingNotification(props) {
const classes = useStyles();
const { fetched, errorMessage, onReset, render } = props;
return (
<div>
{
fetched ?
errorMessage ?
<div className={classes.loadingSettings}>
<Typography variant="h6" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
:
render()
:
<div className={classes.loadingSettings}>
<LinearProgress className={classes.loadingSettingsDetails} />
<Typography variant="h6" className={classes.loadingSettingsDetails}>
Loading...
</Typography>
</div>
}
</div>
);
}
LoadingNotification.propTypes = {
fetched: PropTypes.bool.isRequired,
onReset: PropTypes.func.isRequired,
errorMessage: PropTypes.string,
render: PropTypes.func.isRequired
};

12
interface/src/components/MenuAppBar.js

@ -30,7 +30,8 @@ import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Avatar from '@material-ui/core/Avatar';
import { APP_NAME } from '../constants/App';
import ProjectMenu from '../project/ProjectMenu';
import { PROJECT_NAME } from '../constants/Env';
import { withAuthenticationContext } from '../authentication/Context.js';
const drawerWidth = 290;
@ -65,8 +66,7 @@ const styles = theme => ({
width: drawerWidth,
},
content: {
flexGrow: 1,
padding: theme.spacing(),
flexGrow: 1
},
authMenu: {
zIndex: theme.zIndex.tooltip,
@ -112,11 +112,13 @@ class MenuAppBar extends React.Component {
<div>
<Toolbar>
<Typography variant="h6" color="primary">
{APP_NAME}
{PROJECT_NAME}
</Typography>
<Divider absolute />
</Toolbar>
<Divider />
<ProjectMenu />
<Divider />
<List>
<ListItem to='/wifi/' selected={path.startsWith('/wifi/')} button component={Link}>
<ListItemIcon>
@ -195,7 +197,7 @@ class MenuAppBar extends React.Component {
</CardContent>
<Divider />
<CardActions className={classes.authMenuActions}>
<Button className={classes.authMenuButtons} variant="contained" color="primary" onClick={authenticationContext.signOut}>Sign Out</Button>
<Button variant="contained" color="primary" onClick={authenticationContext.signOut}>Sign Out</Button>
</CardActions>
</Card>
</ClickAwayListener>

20
interface/src/components/RestComponent.js

@ -1,6 +1,7 @@
import React from 'react';
import { withSnackbar } from 'notistack';
import { redirectingAuthorizedFetch } from '../authentication/Authentication';
/*
* It is unlikely this application will grow complex enough to require redux.
*
@ -51,10 +52,11 @@ export const restComponent = (endpointUrl, FormComponent) => {
})
.then(json => { this.setState({ data: json, fetched: true }) })
.catch(error => {
this.props.enqueueSnackbar("Problem fetching: " + error.message, {
const errorMessage = error.message || "Unknown error";
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, {
variant: 'error',
});
this.setState({ data: null, fetched: true, errorMessage: error.message });
this.setState({ data: null, fetched: true, errorMessage });
});
}
@ -79,19 +81,26 @@ export const restComponent = (endpointUrl, FormComponent) => {
});
this.setState({ data: json, fetched: true });
}).catch(error => {
this.props.enqueueSnackbar("Problem saving: " + error.message, {
const errorMessage = error.message || "Unknown error";
this.props.enqueueSnackbar("Problem saving: " + errorMessage, {
variant: 'error',
});
this.setState({ data: null, fetched: true, errorMessage: error.message });
this.setState({ data: null, fetched: true, errorMessage });
});
}
handleValueChange = name => event => {
handleValueChange = name => (event) => {
const { data } = this.state;
data[name] = event.target.value;
this.setState({ data });
};
handleSliderChange = name => (event, newValue) => {
const { data } = this.state;
data[name] = newValue;
this.setState({ data });
};
handleCheckboxChange = name => event => {
const { data } = this.state;
data[name] = event.target.checked;
@ -102,6 +111,7 @@ export const restComponent = (endpointUrl, FormComponent) => {
return <FormComponent
handleValueChange={this.handleValueChange}
handleCheckboxChange={this.handleCheckboxChange}
handleSliderChange={this.handleSliderChange}
setData={this.setData}
saveData={this.saveData}
loadData={this.loadData}

9
interface/src/components/SectionContent.js

@ -8,15 +8,15 @@ import Typography from '@material-ui/core/Typography';
const styles = theme => ({
content: {
padding: theme.spacing(2),
margin: theme.spacing(2),
margin: theme.spacing(3),
}
});
function SectionContent(props) {
const { children, classes, title } = props;
const { children, classes, title, titleGutter } = props;
return (
<Paper className={classes.content}>
<Typography variant="h6">
<Typography variant="h6" gutterBottom={titleGutter}>
{title}
</Typography>
{children}
@ -30,7 +30,8 @@ SectionContent.propTypes = {
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
title: PropTypes.string.isRequired
title: PropTypes.string.isRequired,
titleGutter: PropTypes.bool
};
export default withStyles(styles)(SectionContent);

1
interface/src/constants/App.js

@ -1 +0,0 @@
export const APP_NAME = process.env.REACT_APP_NAME;

2
interface/src/constants/Endpoints.js

@ -1,4 +1,4 @@
const ENDPOINT_ROOT = process.env.REACT_APP_ENDPOINT_ROOT;
import { ENDPOINT_ROOT } from '../constants/Env';
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";

3
interface/src/constants/Env.js

@ -0,0 +1,3 @@
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME;
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH;
export const ENDPOINT_ROOT = process.env.REACT_APP_ENDPOINT_ROOT;

22
interface/src/containers/APSettings.js

@ -1,7 +1,8 @@
import React, { Component } from 'react';
import { AP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { AP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent';
import APSettingsForm from '../forms/APSettingsForm';
@ -12,16 +13,21 @@ class APSettings extends Component {
}
render() {
const { data, fetched, errorMessage } = this.props;
const { fetched, errorMessage, data, saveData, loadData, handleValueChange } = this.props;
return (
<SectionContent title="AP Settings">
<APSettingsForm
apSettings={data}
apSettingsFetched={fetched}
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
onSubmit={this.props.saveData}
onReset={this.props.loadData}
handleValueChange={this.props.handleValueChange}
render={() =>
<APSettingsForm
apSettings={data}
onSubmit={saveData}
onReset={loadData}
handleValueChange={handleValueChange}
/>
}
/>
</SectionContent>
)

42
interface/src/containers/APStatus.js

@ -2,8 +2,6 @@ import React, { Component, Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
@ -15,6 +13,7 @@ import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import ComputerIcon from '@material-ui/icons/Computer';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent'
import * as Highlight from '../constants/Highlight';
@ -27,10 +26,6 @@ const styles = theme => ({
["apStatus_" + Highlight.IDLE]: {
backgroundColor: theme.palette.highlight_idle
},
fetching: {
margin: theme.spacing(4),
textAlign: "center"
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
@ -96,9 +91,7 @@ class APStatus extends Component {
return (
<div>
<List>
<Fragment>
{this.createListItems(data, classes)}
</Fragment>
{this.createListItems(data, classes)}
</List>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
Refresh
@ -108,30 +101,17 @@ class APStatus extends Component {
}
render() {
const { data, fetched, errorMessage, classes } = this.props;
const { fetched, errorMessage, data, loadData, classes } = this.props;
return (
<SectionContent title="AP Status">
{
!fetched ?
<div>
<LinearProgress className={classes.fetching} />
<Typography variant="h4" className={classes.fetching}>
Loading...
</Typography>
</div>
:
data ? this.renderAPStatus(data, classes)
:
<div>
<Typography variant="h4" className={classes.fetching}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
Refresh
</Button>
</div>
}
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
render={
() => this.renderAPStatus(data, classes)
}
/>
</SectionContent>
)
}

26
interface/src/containers/ManageUsers.js

@ -2,8 +2,9 @@ import React, { Component } from 'react';
import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent';
import ManageUsersForm from '../forms/ManageUsersForm';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent';
import ManageUsersForm from '../forms/ManageUsersForm';
class ManageUsers extends Component {
@ -12,17 +13,22 @@ class ManageUsers extends Component {
}
render() {
const { data, fetched, errorMessage } = this.props;
const { fetched, errorMessage, data, saveData, loadData, setData, handleValueChange } = this.props;
return (
<SectionContent title="Manage Users">
<ManageUsersForm
userData={data}
userDataFetched={fetched}
<SectionContent title="Manage Users" titleGutter>
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
onSubmit={this.props.saveData}
onReset={this.props.loadData}
setData={this.props.setData}
handleValueChange={this.props.handleValueChange}
render={() =>
<ManageUsersForm
userData={data}
onSubmit={saveData}
onReset={loadData}
setData={setData}
handleValueChange={handleValueChange}
/>
}
/>
</SectionContent>
)

26
interface/src/containers/NTPSettings.js

@ -1,27 +1,33 @@
import React, { Component } from 'react';
import { NTP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import {restComponent} from '../components/RestComponent';
import { NTP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent';
import NTPSettingsForm from '../forms/NTPSettingsForm';
class NTPSettings extends Component {
componentDidMount() {
this.props.loadData();
this.props.loadData();
}
render() {
const { data, fetched, errorMessage } = this.props;
const { fetched, errorMessage, data, saveData, loadData, handleValueChange } = this.props;
return (
<SectionContent title="NTP Settings">
<NTPSettingsForm
ntpSettings={data}
ntpSettingsFetched={fetched}
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
onSubmit={this.props.saveData}
onReset={this.props.loadData}
handleValueChange={this.props.handleValueChange}
render={() =>
<NTPSettingsForm
ntpSettings={data}
onSubmit={saveData}
onReset={loadData}
handleValueChange={handleValueChange}
/>
}
/>
</SectionContent>
)

40
interface/src/containers/NTPStatus.js

@ -2,8 +2,6 @@ import React, { Component, Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
@ -22,6 +20,7 @@ import * as Highlight from '../constants/Highlight';
import { unixTimeToTimeAndDate } from '../constants/TimeFormat';
import { NTP_STATUS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent';
import moment from 'moment';
@ -36,10 +35,6 @@ const styles = theme => ({
["ntpStatus_" + Highlight.WARN]: {
backgroundColor: theme.palette.highlight_warn
},
fetching: {
margin: theme.spacing(4),
textAlign: "center"
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
@ -131,32 +126,19 @@ class NTPStatus extends Component {
}
render() {
const { data, fetched, errorMessage, classes } = this.props;
const { data, fetched, errorMessage, loadData, classes } = this.props;
return (
<SectionContent title="NTP Status">
{
!fetched ?
<div>
<LinearProgress className={classes.fetching} />
<Typography variant="h4" className={classes.fetching}>
Loading...
</Typography>
</div>
:
data ? this.renderNTPStatus(data, classes)
:
<div>
<Typography variant="h4" className={classes.fetching}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
Refresh
</Button>
</div>
}
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
render={
() => this.renderNTPStatus(data, classes)
}
/>
</SectionContent>
)
);
}
}

28
interface/src/containers/OTASettings.js

@ -1,28 +1,34 @@
import React, { Component } from 'react';
import { OTA_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import {restComponent} from '../components/RestComponent';
import { OTA_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent';
import OTASettingsForm from '../forms/OTASettingsForm';
class OTASettings extends Component {
componentDidMount() {
this.props.loadData();
this.props.loadData();
}
render() {
const { data, fetched, errorMessage } = this.props;
const { fetched, errorMessage, data, saveData, loadData, handleValueChange, handleCheckboxChange } = this.props;
return (
<SectionContent title="OTA Settings">
<OTASettingsForm
otaSettings={data}
otaSettingsFetched={fetched}
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
onSubmit={this.props.saveData}
onReset={this.props.loadData}
handleValueChange={this.props.handleValueChange}
handleCheckboxChange={this.props.handleCheckboxChange}
render={() =>
<OTASettingsForm
otaSettings={data}
onSubmit={saveData}
onReset={loadData}
handleValueChange={handleValueChange}
handleCheckboxChange={handleCheckboxChange}
/>
}
/>
</SectionContent>
)

20
interface/src/containers/SecuritySettings.js

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SecuritySettingsForm from '../forms/SecuritySettingsForm';
import SectionContent from '../components/SectionContent';
@ -12,16 +13,21 @@ class SecuritySettings extends Component {
}
render() {
const { data, fetched, errorMessage } = this.props;
const { data, fetched, errorMessage, saveData, loadData, handleValueChange } = this.props;
return (
<SectionContent title="Security Settings">
<SecuritySettingsForm
securitySettings={data}
securitySettingsFetched={fetched}
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
onSubmit={this.props.saveData}
onReset={this.props.loadData}
handleValueChange={this.props.handleValueChange}
render={() =>
<SecuritySettingsForm
securitySettings={data}
onSubmit={saveData}
onReset={loadData}
handleValueChange={handleValueChange}
/>
}
/>
</SectionContent>
)

4
interface/src/containers/SignInPage.js

@ -4,7 +4,7 @@ import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import Fab from '@material-ui/core/Fab';
import { APP_NAME } from '../constants/App';
import { PROJECT_NAME } from '../constants/Env';
import ForwardIcon from '@material-ui/icons/Forward';
import { withSnackbar } from 'notistack';
import { SIGN_IN_ENDPOINT } from '../constants/Endpoints';
@ -97,7 +97,7 @@ class SignInPage extends Component {
return (
<div className={classes.loginPage}>
<Paper className={classes.loginPanel}>
<Typography variant="h4">{APP_NAME}</Typography>
<Typography variant="h4">{PROJECT_NAME}</Typography>
<ValidatorForm onSubmit={this.onSubmit}>
<TextValidator
disabled={processing}

42
interface/src/containers/SystemStatus.js

@ -2,8 +2,6 @@ import React, { Component, Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
@ -16,16 +14,12 @@ import ShowChartIcon from '@material-ui/icons/ShowChart';
import SdStorageIcon from '@material-ui/icons/SdStorage';
import DataUsageIcon from '@material-ui/icons/DataUsage';
import { SYSTEM_STATUS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent';
const styles = theme => ({
fetching: {
margin: theme.spacing(4),
textAlign: "center"
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
@ -85,12 +79,12 @@ class SystemStatus extends Component {
</ListItemAvatar>
<ListItemText primary="Flash Chip Size" secondary={data.flash_chip_size + ' bytes'} />
</ListItem>
<Divider variant="inset" component="li" />
<Divider variant="inset" component="li" />
</Fragment>
);
}
renderNTPStatus(data, classes) {
renderSystemStatus(data, classes) {
return (
<div>
<List>
@ -104,29 +98,17 @@ class SystemStatus extends Component {
}
render() {
const { data, fetched, errorMessage, classes } = this.props;
const { data, fetched, errorMessage, loadData, classes } = this.props;
return (
<SectionContent title="System Status">
{
!fetched ?
<div>
<LinearProgress className={classes.fetching} />
<Typography variant="h4" className={classes.fetching}>
Loading...
</Typography>
</div>
:
data ? this.renderNTPStatus(data, classes)
:
<div>
<Typography variant="h4" className={classes.fetching}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
Refresh
</Button>
</div>
}
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
render={
() => this.renderSystemStatus(data, classes)
}
/>
</SectionContent>
)
}

2
interface/src/containers/WiFiNetworkScanner.js

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withSnackbar } from 'notistack';
import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../constants/Endpoints';
import SectionContent from '../components/SectionContent';
import WiFiNetworkSelector from '../forms/WiFiNetworkSelector';
import { withSnackbar } from 'notistack';
import { redirectingAuthorizedFetch } from '../authentication/Authentication';
const NUM_POLLS = 10

36
interface/src/containers/WiFiSettings.js

@ -1,8 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { WIFI_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { WIFI_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent';
import WiFiSettingsForm from '../forms/WiFiSettingsForm';
@ -18,10 +19,10 @@ class WiFiSettings extends Component {
const { selectedNetwork } = this.props;
if (selectedNetwork) {
var wifiSettings = {
ssid:selectedNetwork.ssid,
password:"",
hostname:"esp8266-react",
static_ip_config:false,
ssid: selectedNetwork.ssid,
password: "",
hostname: "esp8266-react",
static_ip_config: false,
}
this.props.setData(wifiSettings);
} else {
@ -35,19 +36,24 @@ class WiFiSettings extends Component {
}
render() {
const { data, fetched, errorMessage, selectedNetwork } = this.props;
const { data, fetched, errorMessage, saveData, loadData, handleValueChange, handleCheckboxChange, selectedNetwork, deselectNetwork } = this.props;
return (
<SectionContent title="WiFi Settings">
<WiFiSettingsForm
wifiSettings={data}
wifiSettingsFetched={fetched}
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
selectedNetwork={selectedNetwork}
deselectNetwork={this.props.deselectNetwork}
onSubmit={this.props.saveData}
onReset={this.deselectNetworkAndLoadData}
handleValueChange={this.props.handleValueChange}
handleCheckboxChange={this.props.handleCheckboxChange}
render={() =>
<WiFiSettingsForm
wifiSettings={data}
selectedNetwork={selectedNetwork}
deselectNetwork={deselectNetwork}
onSubmit={saveData}
onReset={this.deselectNetworkAndLoadData}
handleValueChange={handleValueChange}
handleCheckboxChange={handleCheckboxChange}
/>
}
/>
</SectionContent>
)

42
interface/src/containers/WiFiStatus.js

@ -2,14 +2,10 @@ import React, { Component, Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import Avatar from '@material-ui/core/Avatar';
import Divider from '@material-ui/core/Divider';
import WifiIcon from '@material-ui/icons/Wifi';
@ -23,6 +19,7 @@ import { WIFI_STATUS_ENDPOINT } from '../constants/Endpoints';
import { isConnected, connectionStatus, connectionStatusHighlight } from '../constants/WiFiConnectionStatus';
import * as Highlight from '../constants/Highlight';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
const styles = theme => ({
["wifiStatus_" + Highlight.IDLE]: {
@ -37,10 +34,6 @@ const styles = theme => ({
["wifiStatus_" + Highlight.WARN]: {
backgroundColor: theme.palette.highlight_warn
},
fetching: {
margin: theme.spacing(4),
textAlign: "center"
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
@ -145,32 +138,21 @@ class WiFiStatus extends Component {
}
render() {
const { data, fetched, errorMessage, classes } = this.props;
const { data, fetched, errorMessage, loadData, classes } = this.props;
return (
<SectionContent title="WiFi Status">
{
!fetched ?
<div>
<LinearProgress className={classes.fetching} />
<Typography variant="h4" className={classes.fetching}>
Loading...
</Typography>
</div>
:
data ? this.renderWiFiStatus(data, classes)
:
<div>
<Typography variant="h4" className={classes.fetching}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
Refresh
</Button>
</div>
}
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
render={
() => this.renderWiFiStatus(data, classes)
}
/>
</SectionContent>
)
);
}
}
export default restComponent(WIFI_STATUS_ENDPOINT, withStyles(styles)(WiFiStatus));

130
interface/src/forms/APSettingsForm.js

@ -1,29 +1,19 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
import { isAPEnabled } from '../constants/WiFiAPModes';
import PasswordValidator from '../components/PasswordValidator';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import MenuItem from '@material-ui/core/MenuItem';
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
import {isAPEnabled} from '../constants/WiFiAPModes';
import PasswordValidator from '../components/PasswordValidator';
const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
textField: {
width: "100%"
},
selectField:{
selectField: {
width: "100%",
marginTop: theme.spacing(2),
marginBottom: theme.spacing(0.5)
@ -37,86 +27,54 @@ const styles = theme => ({
class APSettingsForm extends React.Component {
render() {
const { classes, apSettingsFetched, apSettings, errorMessage, handleValueChange, onSubmit, onReset } = this.props;
const { classes, apSettings, handleValueChange, onSubmit, onReset } = this.props;
return (
<div>
<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>
{
!apSettingsFetched ?
<div className={classes.loadingSettings}>
<LinearProgress className={classes.loadingSettingsDetails}/>
<Typography variant="h4" 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) &&
<Fragment>
<TextValidator
validators={['required', 'matchRegexp:^.{1,32}$']}
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
name="ssid"
label="Access Point SSID"
className={classes.textField}
value={apSettings.ssid}
onChange={handleValueChange('ssid')}
margin="normal"
/>
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,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"
/>
</Fragment>
}
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
}
</div>
isAPEnabled(apSettings.provision_mode) &&
<Fragment>
<TextValidator
validators={['required', 'matchRegexp:^.{1,32}$']}
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
name="ssid"
label="Access Point SSID"
className={classes.textField}
value={apSettings.ssid}
onChange={handleValueChange('ssid')}
margin="normal"
/>
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,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"
/>
</Fragment>
}
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
);
}
}
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

174
interface/src/forms/ManageUsersForm.js

@ -5,7 +5,6 @@ import { ValidatorForm } from 'react-material-ui-form-validator';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
@ -14,7 +13,6 @@ import TableFooter from '@material-ui/core/TableFooter';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Box from '@material-ui/core/Box';
import EditIcon from '@material-ui/icons/Edit';
import DeleteIcon from '@material-ui/icons/Delete';
import CloseIcon from '@material-ui/icons/Close';
@ -25,22 +23,12 @@ import UserForm from './UserForm';
import { withAuthenticationContext } from '../authentication/Context';
const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
},
table: {
'& td, & th': { padding: theme.spacing(0.5) }
},
actions: {
whiteSpace: "nowrap"
}
});
@ -134,98 +122,80 @@ class ManageUsersForm extends React.Component {
}
render() {
const { classes, userData, userDataFetched, errorMessage, onReset } = this.props;
const { classes, userData, onReset } = this.props;
const { user, creating } = this.state;
return (
!userDataFetched ?
<div className={classes.loadingSettings}>
<LinearProgress className={classes.loadingSettingsDetails} />
<Typography variant="h4" className={classes.loadingSettingsDetails}>
Loading...
</Typography>
</div>
:
userData ?
<Fragment>
<ValidatorForm onSubmit={this.onSubmit}>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Username</TableCell>
<TableCell align="center">Admin?</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{userData.users.sort(compareUsers).map(user => (
<TableRow key={user.username}>
<TableCell component="th" scope="row">
{user.username}
</TableCell>
<TableCell align="center">
{
user.admin ? <CheckIcon /> : <CloseIcon />
}
</TableCell>
<TableCell align="center">
<IconButton aria-label="Delete" onClick={() => this.removeUser(user)}>
<DeleteIcon />
</IconButton>
<IconButton aria-label="Edit" onClick={() => this.startEditingUser(user)}>
<EditIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2} />
<TableCell align="center">
<Button variant="contained" color="secondary" onClick={this.createUser}>
Add User
</Button>
</TableCell>
</TableRow>
</TableFooter>
</Table>
{
this.noAdminConfigured() &&
<Typography component="div" variant="body1">
<Box bgcolor="error.main" color="error.contrastText" p={2} mt={2} mb={2}>
You must have at least one admin user configured.
</Box>
</Typography>
}
<Button variant="contained" color="primary" className={classes.button} type="submit" disabled={this.noAdminConfigured()}>
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
{
user &&
<UserForm
user={user}
creating={creating}
onDoneEditing={this.doneEditingUser}
onCancelEditing={this.cancelEditingUser}
handleValueChange={this.handleUserValueChange}
handleCheckboxChange={this.handleUserCheckboxChange}
uniqueUsername={this.uniqueUsername}
/>
}
</Fragment>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
<Fragment>
<ValidatorForm onSubmit={this.onSubmit}>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Username</TableCell>
<TableCell align="center">Admin?</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{userData.users.sort(compareUsers).map(user => (
<TableRow key={user.username}>
<TableCell component="th" scope="row">
{user.username}
</TableCell>
<TableCell align="center">
{
user.admin ? <CheckIcon /> : <CloseIcon />
}
</TableCell>
<TableCell align="center">
<IconButton aria-label="Delete" onClick={() => this.removeUser(user)}>
<DeleteIcon />
</IconButton>
<IconButton aria-label="Edit" onClick={() => this.startEditingUser(user)}>
<EditIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2} />
<TableCell align="center">
<Button variant="contained" color="secondary" onClick={this.createUser}>
Add User
</Button>
</TableCell>
</TableRow>
</TableFooter>
</Table>
{
this.noAdminConfigured() &&
<Typography component="div" variant="body1">
<Box bgcolor="error.main" color="error.contrastText" p={2} mt={2} mb={2}>
You must have at least one admin user configured.
</Box>
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
}
<Button variant="contained" color="primary" className={classes.button} type="submit" disabled={this.noAdminConfigured()}>
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
{
user &&
<UserForm
user={user}
creating={creating}
onDoneEditing={this.doneEditingUser}
onCancelEditing={this.cancelEditingUser}
handleValueChange={this.handleUserValueChange}
handleCheckboxChange={this.handleUserCheckboxChange}
uniqueUsername={this.uniqueUsername}
/>
}
</Fragment>
);
}
@ -234,8 +204,6 @@ class ManageUsersForm extends React.Component {
ManageUsersForm.propTypes = {
classes: PropTypes.object.isRequired,
userData: PropTypes.object,
userDataFetched: PropTypes.bool.isRequired,
errorMessage: PropTypes.string,
onSubmit: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired,
setData: PropTypes.func.isRequired,

103
interface/src/forms/NTPSettingsForm.js

@ -1,24 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import Typography from '@material-ui/core/Typography';
import isIP from '../validators/isIP';
import isHostname from '../validators/isHostname';
import or from '../validators/or';
const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
textField: {
width: "100%"
},
@ -35,76 +26,44 @@ class NTPSettingsForm extends React.Component {
}
render() {
const { classes, ntpSettingsFetched, ntpSettings, errorMessage, handleValueChange, onSubmit, onReset } = this.props;
const { classes, ntpSettings, handleValueChange, onSubmit, onReset } = this.props;
return (
<div>
{
!ntpSettingsFetched ?
<div className={classes.loadingSettings}>
<LinearProgress className={classes.loadingSettingsDetails}/>
<Typography variant="h4" 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="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
}
</div>
<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="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
);
}
}
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,

124
interface/src/forms/OTASettingsForm.js

@ -4,9 +4,7 @@ import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Switch from '@material-ui/core/Switch';
import LinearProgress from '@material-ui/core/LinearProgress';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import Typography from '@material-ui/core/Typography';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import isIP from '../validators/isIP';
@ -15,13 +13,6 @@ import or from '../validators/or';
import PasswordValidator from '../components/PasswordValidator';
const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
switchControl: {
width: "100%",
marginTop: theme.spacing(2),
@ -43,88 +34,55 @@ class OTASettingsForm extends React.Component {
}
render() {
const { classes, otaSettingsFetched, otaSettings, errorMessage, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
const { classes, otaSettings, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
return (
<div>
{
!otaSettingsFetched ?
<div className={classes.loadingSettings}>
<LinearProgress className={classes.loadingSettingsDetails}/>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
Loading...
</Typography>
</div>
: otaSettings ?
<ValidatorForm onSubmit={onSubmit}>
<FormControlLabel className={classes.switchControl}
control={
<Switch
checked={otaSettings.enabled}
onChange={handleCheckboxChange('enabled')}
value="enabled"
color="primary"
/>
}
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"
/>
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,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="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
}
</div>
<ValidatorForm onSubmit={onSubmit}>
<FormControlLabel className={classes.switchControl}
control={
<Switch
checked={otaSettings.enabled}
onChange={handleCheckboxChange('enabled')}
value="enabled"
color="primary"
/>
}
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"
/>
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,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="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
);
}
}
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,

76
interface/src/forms/SecuritySettingsForm.js

@ -4,7 +4,6 @@ import { ValidatorForm } from 'react-material-ui-form-validator';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';
@ -12,13 +11,6 @@ import PasswordValidator from '../components/PasswordValidator';
import { withAuthenticationContext } from '../authentication/Context';
const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
textField: {
width: "100%"
},
@ -36,58 +28,38 @@ class SecuritySettingsForm extends React.Component {
}
render() {
const { classes, securitySettingsFetched, securitySettings, errorMessage, handleValueChange, onReset } = this.props;
const { classes, securitySettings, handleValueChange, onReset } = this.props;
return (
!securitySettingsFetched ?
<div className={classes.loadingSettings}>
<LinearProgress className={classes.loadingSettingsDetails} />
<Typography variant="h4" className={classes.loadingSettingsDetails}>
Loading...
</Typography>
</div>
:
securitySettings ?
<ValidatorForm onSubmit={this.onSubmit} ref="SecuritySettingsForm">
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,64}$']}
errorMessages={['JWT Secret Required', 'JWT Secret must be 64 characters or less']}
name="jwt_secret"
label="JWT Secret"
className={classes.textField}
value={securitySettings.jwt_secret}
onChange={handleValueChange('jwt_secret')}
margin="normal"
/>
<Typography component="div" variant="body1">
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
If you modify the JWT Secret, all users will be logged out.
</Box>
</Typography>
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
<ValidatorForm onSubmit={this.onSubmit} ref="SecuritySettingsForm">
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,64}$']}
errorMessages={['JWT Secret Required', 'JWT Secret must be 64 characters or less']}
name="jwt_secret"
label="JWT Secret"
className={classes.textField}
value={securitySettings.jwt_secret}
onChange={handleValueChange('jwt_secret')}
margin="normal"
/>
<Typography component="div" variant="body1">
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
If you modify the JWT Secret, all users will be logged out.
</Box>
</Typography>
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
);
}
}
SecuritySettingsForm.propTypes = {
classes: PropTypes.object.isRequired,
securitySettingsFetched: PropTypes.bool.isRequired,
securitySettings: PropTypes.object,
errorMessage: PropTypes.string,
onSubmit: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired,
handleValueChange: PropTypes.func.isRequired,

4
interface/src/forms/WiFiNetworkSelector.js

@ -69,7 +69,7 @@ class WiFiNetworkSelector extends Component {
scanningForNetworks ?
<div>
<LinearProgress className={classes.scanningProgress}/>
<Typography variant="h4" className={classes.scanningProgress}>
<Typography variant="h6" className={classes.scanningProgress}>
Scanning...
</Typography>
</div>
@ -80,7 +80,7 @@ class WiFiNetworkSelector extends Component {
</List>
:
<div>
<Typography variant="h4" className={classes.scanningProgress}>
<Typography variant="h6" className={classes.scanningProgress}>
{errorMessage}
</Typography>
</div>

258
interface/src/forms/WiFiSettingsForm.js

@ -3,10 +3,8 @@ import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
@ -28,13 +26,6 @@ import optional from '../validators/optional';
import PasswordValidator from '../components/PasswordValidator';
const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
textField: {
width: "100%"
},
@ -80,157 +71,124 @@ class WiFiSettingsForm extends React.Component {
}
render() {
const { classes, wifiSettingsFetched, wifiSettings, errorMessage, selectedNetwork, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
const { classes, wifiSettings, selectedNetwork, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
return (
<div>
<ValidatorForm onSubmit={onSubmit} ref="WiFiSettingsForm">
{
!wifiSettingsFetched ?
<div className={classes.loadingSettings}>
<LinearProgress className={classes.loadingSettingsDetails} />
<Typography variant="h4" className={classes.loadingSettingsDetails}>
Loading...
</Typography>
</div>
: wifiSettings ?
<ValidatorForm onSubmit={onSubmit} ref="WiFiSettingsForm">
{
selectedNetwork ? this.renderSelectedNetwork() :
<TextValidator
validators={['matchRegexp:^.{0,32}$']}
errorMessages={['SSID must be 32 characters or less']}
name="ssid"
label="SSID"
className={classes.textField}
value={wifiSettings.ssid}
onChange={handleValueChange('ssid')}
margin="normal"
/>
}
{
!isNetworkOpen(selectedNetwork) &&
<PasswordValidator
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 &&
<Fragment>
<TextValidator
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
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
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
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
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"
/>
</Fragment>
}
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
selectedNetwork ? this.renderSelectedNetwork() :
<TextValidator
validators={['matchRegexp:^.{0,32}$']}
errorMessages={['SSID must be 32 characters or less']}
name="ssid"
label="SSID"
className={classes.textField}
value={wifiSettings.ssid}
onChange={handleValueChange('ssid')}
margin="normal"
/>
}
{
!isNetworkOpen(selectedNetwork) &&
<PasswordValidator
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 &&
<Fragment>
<TextValidator
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
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
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
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
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"
/>
</Fragment>
}
</div>
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
);
}
}
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,

82
interface/src/project/DemoController.js

@ -0,0 +1,82 @@
import React, { Component } from 'react';
import { ValidatorForm } from 'react-material-ui-form-validator';
import { ENDPOINT_ROOT } from '../constants/Env';
import SectionContent from '../components/SectionContent';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import Slider from '@material-ui/core/Slider';
import { makeStyles } from '@material-ui/core/styles';
export const DEMO_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "demoSettings";
const valueToPercentage = (value) => `${Math.round(value / 255 * 100)}%`;
class DemoController extends Component {
componentDidMount() {
this.props.loadData();
}
render() {
const { data, fetched, errorMessage, saveData, loadData, handleSliderChange } = this.props;
return (
<SectionContent title="Controller" titleGutter>
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
render={() =>
<DemoControllerForm
demoSettings={data}
onReset={loadData}
onSubmit={saveData}
handleSliderChange={handleSliderChange}
/>
}
/>
</SectionContent>
)
}
}
const useStyles = makeStyles(theme => ({
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
},
blinkSpeedLabel: {
marginBottom: theme.spacing(5),
}
}));
function DemoControllerForm(props) {
const { demoSettings, onSubmit, onReset, handleSliderChange } = props;
const classes = useStyles();
return (
<ValidatorForm onSubmit={onSubmit}>
<Typography id="blink-speed-slider" className={classes.blinkSpeedLabel}>
Blink Speed
</Typography>
<Slider
value={demoSettings.blink_speed}
valueLabelFormat={valueToPercentage}
aria-labelledby="blink-speed-slider"
valueLabelDisplay="on"
min={0}
max={255}
onChange={handleSliderChange('blink_speed')}
/>
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
);
}
export default restComponent(DEMO_SETTINGS_ENDPOINT, DemoController);

100
interface/src/project/DemoInformation.js

@ -0,0 +1,100 @@
import React, { Component } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
import TableCell from '@material-ui/core/TableCell';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import Typography from '@material-ui/core/Typography';
import SectionContent from '../components/SectionContent';
const styles = theme => ({
fileTable: {
marginBottom: theme.spacing(2)
}
});
class DemoInformation extends Component {
render() {
const { classes } = this.props;
return (
<SectionContent title="Demo Project - Blink Speed Controller" titleGutter>
<Typography variant="body1" paragraph>
This simple demo project allows you to control the blink speed of the built-in LED.
It demonstrates how the esp8266-react framework may be extended for your own IoT project.
</Typography>
<Typography variant="body1" paragraph>
It is recommended that you keep your project interface code under the 'project' directory.
This serves to isolate your project code from the from the rest of the user interface which should
simplify merges should you wish to update your project with future framework changes.
</Typography>
<Typography variant="body1" paragraph>
The demo project interface code stored in the interface/project directory:
</Typography>
<Table className={classes.fileTable}>
<TableHead>
<TableRow>
<TableCell>
File
</TableCell>
<TableCell>
Description
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>
ProjectMenu.js
</TableCell>
<TableCell>
You can add your project's screens to the side bar here.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
ProjectRouting.js
</TableCell>
<TableCell>
The routing which controls the screens of your project.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
DemoProject.js
</TableCell>
<TableCell>
This screen, with tabs and tab routing.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
DemoInformation.js
</TableCell>
<TableCell>
The demo information tab.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
DemoController.js
</TableCell>
<TableCell>
The demo controller tab, to control the built-in LED.
</TableCell>
</TableRow>
</TableBody>
</Table>
<Typography variant="body1" paragraph>
See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project.
</Typography>
</SectionContent>
)
}
}
export default withStyles(styles)(DemoInformation);

37
interface/src/project/DemoProject.js

@ -0,0 +1,37 @@
import React, { Component } from 'react';
import { Redirect, Switch } from 'react-router-dom'
import { PROJECT_PATH } from '../constants/Env';
import MenuAppBar from '../components/MenuAppBar';
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
import DemoInformation from './DemoInformation';
import DemoController from './DemoController';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
class DemoProject extends Component {
handleTabChange = (event, path) => {
this.props.history.push(path);
};
render() {
return (
<MenuAppBar sectionTitle="Demo Project">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth">
<Tab value={`/${PROJECT_PATH}/demo/information`} label="Information" />
<Tab value={`/${PROJECT_PATH}/demo/controller`} label="Controller" />
</Tabs>
<Switch>
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/information`} component={DemoInformation} />
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/controller`} component={DemoController} />
<Redirect to={`/${PROJECT_PATH}/demo/information`} />
</Switch>
</MenuAppBar>
)
}
}
export default DemoProject;

30
interface/src/project/ProjectMenu.js

@ -0,0 +1,30 @@
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { PROJECT_PATH } from '../constants/Env';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import SettingsRemoteIcon from '@material-ui/icons/SettingsRemote';
class ProjectMenu extends Component {
render() {
const path = this.props.match.url;
return (
<List>
<ListItem to={`/${PROJECT_PATH}/demo/`} selected={path.startsWith(`/${PROJECT_PATH}/demo/`)} button component={Link}>
<ListItemIcon>
<SettingsRemoteIcon />
</ListItemIcon>
<ListItemText primary="Demo Project" />
</ListItem>
</List>
)
}
}
export default withRouter(ProjectMenu);

32
interface/src/project/ProjectRouting.js

@ -0,0 +1,32 @@
import React, { Component } from 'react';
import { Redirect, Switch } from 'react-router';
import { PROJECT_PATH } from '../constants/Env';
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
import DemoProject from './DemoProject';
class ProjectRouting extends Component {
render() {
return (
<Switch>
{
/*
* Add your project page routing below.
*/
}
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/*`} component={DemoProject} />
{
/*
* The redirect below caters for the default project route and redirecting invalid paths.
* The "to" property must match one of the routes above for this to work correctly.
*/
}
<Redirect to={`/${PROJECT_PATH}/demo/`} />
</Switch>
)
}
}
export default ProjectRouting;

1
interface/src/sections/NetworkTime.js

@ -32,6 +32,7 @@ class NetworkTime extends Component {
</MenuAppBar>
)
}
}
export default withAuthenticationContext(NetworkTime)

12
src/APSettingsService.cpp → lib/framework/APSettingsService.cpp

@ -1,11 +1,14 @@
#include <APSettingsService.h>
APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, AP_SETTINGS_SERVICE_PATH, AP_SETTINGS_FILE) {
onConfigUpdated();
}
APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, AP_SETTINGS_SERVICE_PATH, AP_SETTINGS_FILE) {}
APSettingsService::~APSettingsService() {}
void APSettingsService::begin() {
SettingsService::begin();
onConfigUpdated();
}
void APSettingsService::loop() {
unsigned long currentMillis = millis();
unsigned long manageElapsed = (unsigned long)(currentMillis - _lastManaged);
@ -80,7 +83,4 @@ void APSettingsService::writeToJsonObject(JsonObject& root) {
void APSettingsService::onConfigUpdated() {
_lastManaged = millis() - MANAGE_NETWORK_DELAY;
// stop softAP - forces reconfiguration in loop()
stopAP();
}

5
src/APSettingsService.h → lib/framework/APSettingsService.h

@ -1,7 +1,7 @@
#ifndef APSettingsConfig_h
#define APSettingsConfig_h
#include <SettingsService.h>
#include <AdminSettingsService.h>
#include <DNSServer.h>
#include <IPAddress.h>
@ -26,6 +26,7 @@ class APSettingsService : public AdminSettingsService {
APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
~APSettingsService();
void begin();
void loop();
protected:
@ -49,7 +50,7 @@ class APSettingsService : public AdminSettingsService {
void manageAP();
void startAP();
void stopAP();
void stopAP() ;
void handleDNS();
};

6
src/APStatus.cpp → lib/framework/APStatus.cpp

@ -1,8 +1,8 @@
#include <APStatus.h>
APStatus::APStatus(AsyncWebServer *server, SecurityManager* securityManager) : _server(server), _securityManager(securityManager) {
_server->on(AP_STATUS_SERVICE_PATH, HTTP_GET,
_securityManager->wrapRequest(std::bind(&APStatus::apStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
APStatus::APStatus(AsyncWebServer* server, SecurityManager* securityManager) {
server->on(AP_STATUS_SERVICE_PATH, HTTP_GET,
securityManager->wrapRequest(std::bind(&APStatus::apStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
);
}

7
src/APStatus.h → lib/framework/APStatus.h

@ -22,13 +22,10 @@ class APStatus {
public:
APStatus(AsyncWebServer *server, SecurityManager* securityManager);
APStatus(AsyncWebServer* server, SecurityManager* securityManager);
private:
AsyncWebServer* _server;
SecurityManager* _securityManager;
void apStatus(AsyncWebServerRequest *request);
};

45
lib/framework/AdminSettingsService.h

@ -0,0 +1,45 @@
#ifndef AdminSettingsService_h
#define AdminSettingsService_h
#include <SettingsService.h>
class AdminSettingsService : public SettingsService {
public:
AdminSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager, char const* servicePath, char const* filePath):
SettingsService(server, fs, servicePath, filePath), _securityManager(securityManager) {}
protected:
// will validate the requests with the security manager
SecurityManager* _securityManager;
void fetchConfig(AsyncWebServerRequest *request) {
// verify the request against the predicate
Authentication authentication = _securityManager->authenticateRequest(request);
if (!getAuthenticationPredicate()(authentication)) {
request->send(401);
return;
}
// delegate to underlying implemetation
SettingsService::fetchConfig(request);
}
void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument) {
// verify the request against the predicate
Authentication authentication = _securityManager->authenticateRequest(request);
if (!getAuthenticationPredicate()(authentication)) {
request->send(401);
return;
}
// delegate to underlying implemetation
SettingsService::updateConfig(request, jsonDocument);
}
// override this to replace the default authentication predicate, IS_ADMIN
AuthenticationPredicate getAuthenticationPredicate() {
return AuthenticationPredicates::IS_ADMIN;
}
};
#endif // end AdminSettingsService

0
src/ArduinoJsonJWT.cpp → lib/framework/ArduinoJsonJWT.cpp

0
src/ArduinoJsonJWT.h → lib/framework/ArduinoJsonJWT.h

0
src/AsyncArduinoJson6.h → lib/framework/AsyncArduinoJson6.h

0
src/AsyncJsonWebHandler.h → lib/framework/AsyncJsonWebHandler.h

3
src/AuthenticationService.cpp → lib/framework/AuthenticationService.cpp

@ -1,7 +1,6 @@
#include <AuthenticationService.h>
AuthenticationService::AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager):
_server(server), _securityManager(securityManager) {
AuthenticationService::AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager) : _securityManager(securityManager) {
server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, std::bind(&AuthenticationService::verifyAuthorization, this, std::placeholders::_1));
_signInHandler.setUri(SIGN_IN_PATH);

5
src/AuthenticationService.h → lib/framework/AuthenticationService.h

@ -15,12 +15,11 @@ class AuthenticationService {
public:
AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager) ;
AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager);
~AuthenticationService();
private:
// server instance
AsyncWebServer* _server;
SecurityManager* _securityManager;
AsyncJsonWebHandler _signInHandler;

55
lib/framework/ESP8266React.cpp

@ -0,0 +1,55 @@
#include <ESP8266React.h>
ESP8266React::ESP8266React(AsyncWebServer* server, FS* fs):
_securitySettingsService(server, fs),
_wifiSettingsService(server, fs, &_securitySettingsService),
_apSettingsService(server, fs, &_securitySettingsService),
_ntpSettingsService(server, fs, &_securitySettingsService),
_otaSettingsService(server, fs, &_securitySettingsService),
_authenticationService(server, &_securitySettingsService),
_wifiScanner(server, &_securitySettingsService),
_wifiStatus(server, &_securitySettingsService),
_ntpStatus(server, &_securitySettingsService),
_apStatus(server, &_securitySettingsService),
_systemStatus(server, &_securitySettingsService) {
// Serve 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/");
server->serveStatic("/favicon.ico", SPIFFS, "/www/favicon.ico");
// Serving all other get requests with "/www/index.htm"
// OPTIONS get a straight up 200 response
server->onNotFound([](AsyncWebServerRequest *request) {
if (request->method() == HTTP_GET) {
request->send(SPIFFS, "/www/index.html");
} else if (request->method() == HTTP_OPTIONS) {
request->send(200);
} else {
request->send(404);
}
});
// Disable CORS if required
#if defined(ENABLE_CORS)
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", CORS_ORIGIN);
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
#endif
}
void ESP8266React::begin() {
_securitySettingsService.begin();
_wifiSettingsService.begin();
_apSettingsService.begin();
_ntpSettingsService.begin();
_otaSettingsService.begin();
}
void ESP8266React::loop() {
_wifiSettingsService.loop();
_apSettingsService.loop();
_ntpSettingsService.loop();
_otaSettingsService.loop();
}

59
lib/framework/ESP8266React.h

@ -0,0 +1,59 @@
#ifndef ESP8266React_h
#define ESP8266React_h
#include <Arduino.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(ESP_PLATFORM)
#include <WiFi.h>
#include <AsyncTCP.h>
#include <SPIFFS.h>
#endif
#include <FS.h>
#include <SecuritySettingsService.h>
#include <WiFiSettingsService.h>
#include <APSettingsService.h>
#include <NTPSettingsService.h>
#include <OTASettingsService.h>
#include <AuthenticationService.h>
#include <WiFiScanner.h>
#include <WiFiStatus.h>
#include <NTPStatus.h>
#include <APStatus.h>
#include <SystemStatus.h>
class ESP8266React {
public:
ESP8266React(AsyncWebServer* server, FS* fs);
void begin();
void loop();
SecurityManager* getSecurityManager(){
return &_securitySettingsService;
}
private:
SecuritySettingsService _securitySettingsService;
WiFiSettingsService _wifiSettingsService;
APSettingsService _apSettingsService;
NTPSettingsService _ntpSettingsService;
OTASettingsService _otaSettingsService;
AuthenticationService _authenticationService;
WiFiScanner _wifiScanner;
WiFiStatus _wifiStatus;
NTPStatus _ntpStatus;
APStatus _apStatus;
SystemStatus _systemStatus;
};
#endif

0
src/NTPSettingsService.cpp → lib/framework/NTPSettingsService.cpp

2
src/NTPSettingsService.h → lib/framework/NTPSettingsService.h

@ -1,7 +1,7 @@
#ifndef NTPSettingsService_h
#define NTPSettingsService_h
#include <SettingsService.h>
#include <AdminSettingsService.h>
#include <TimeLib.h>
#include <NtpClientLib.h>

6
src/NTPStatus.cpp → lib/framework/NTPStatus.cpp

@ -1,8 +1,8 @@
#include <NTPStatus.h>
NTPStatus::NTPStatus(AsyncWebServer *server, SecurityManager* securityManager) : _server(server), _securityManager(securityManager) {
_server->on(NTP_STATUS_SERVICE_PATH, HTTP_GET,
_securityManager->wrapRequest(std::bind(&NTPStatus::ntpStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
NTPStatus::NTPStatus(AsyncWebServer* server, SecurityManager* securityManager) {
server->on(NTP_STATUS_SERVICE_PATH, HTTP_GET,
securityManager->wrapRequest(std::bind(&NTPStatus::ntpStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
);
}

5
src/NTPStatus.h → lib/framework/NTPStatus.h

@ -23,13 +23,10 @@ class NTPStatus {
public:
NTPStatus(AsyncWebServer *server, SecurityManager* securityManager);
NTPStatus(AsyncWebServer* server, SecurityManager* securityManager);
private:
AsyncWebServer* _server;
SecurityManager* _securityManager;
void ntpStatus(AsyncWebServerRequest *request);
};

0
src/OTASettingsService.cpp → lib/framework/OTASettingsService.cpp

4
src/OTASettingsService.h → lib/framework/OTASettingsService.h

@ -1,7 +1,7 @@
#ifndef OTASettingsService_h
#define OTASettingsService_h
#include <SettingsService.h>
#include <AdminSettingsService.h>
#if defined(ESP8266)
#include <ESP8266mDNS.h>
@ -52,4 +52,4 @@ class OTASettingsService : public AdminSettingsService {
};
#endif // end NTPSettingsService_h
#endif // end OTASettingsService_h

0
src/SecurityManager.cpp → lib/framework/SecurityManager.cpp

0
src/SecurityManager.h → lib/framework/SecurityManager.h

4
src/SecuritySettingsService.cpp → lib/framework/SecuritySettingsService.cpp

@ -29,7 +29,3 @@ void SecuritySettingsService::writeToJsonObject(JsonObject& root) {
user["admin"] = _user.isAdmin();
}
}
void SecuritySettingsService::begin() {
readFromFS();
}

4
src/SecuritySettingsService.h → lib/framework/SecuritySettingsService.h

@ -1,7 +1,7 @@
#ifndef SecuritySettingsService_h
#define SecuritySettingsService_h
#include <SettingsService.h>
#include <AdminSettingsService.h>
#include <SecurityManager.h>
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
@ -14,8 +14,6 @@ class SecuritySettingsService : public AdminSettingsService, public SecurityMana
SecuritySettingsService(AsyncWebServer* server, FS* fs);
~SecuritySettingsService();
void begin();
protected:
void readFromJsonObject(JsonObject& root);

2
src/SettingsPersistence.h → lib/framework/SettingsPersistence.h

@ -47,7 +47,7 @@ protected:
return true;
}
void readFromFS(){
void readFromFS() {
File configFile = _fs->open(_filePath, "r");
// use defaults if no config found

57
src/SettingsService.h → lib/framework/SettingsService.h

@ -16,7 +16,6 @@
#include <AsyncJsonWebHandler.h>
#include <AsyncArduinoJson6.h>
/*
* Abstraction of a service which stores it's settings as JSON in a file system.
*/
@ -24,30 +23,25 @@ class SettingsService : public SettingsPersistence {
public:
SettingsService(AsyncWebServer* server, FS* fs, char const* servicePath, char const* filePath):
SettingsPersistence(fs, filePath), _server(server) {
// configure fetch config handler
_server->on(servicePath, HTTP_GET, std::bind(&SettingsService::fetchConfig, this, std::placeholders::_1));
SettingsService(AsyncWebServer* server, FS* fs, char const* servicePath, char const* filePath): SettingsPersistence(fs, filePath), _servicePath(servicePath) {
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);
server->addHandler(&_updateHandler);
}
virtual ~SettingsService() {}
virtual void begin() {
void begin() {
// read the initial data from the file system
readFromFS();
}
protected:
// will serve setting endpoints from here
AsyncWebServer* _server;
char const* _servicePath;
AsyncJsonWebHandler _updateHandler;
virtual void fetchConfig(AsyncWebServerRequest *request) {
@ -82,43 +76,4 @@ protected:
};
class AdminSettingsService : public SettingsService {
public:
AdminSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager, char const* servicePath, char const* filePath):
SettingsService(server, fs, servicePath, filePath), _securityManager(securityManager) {
}
protected:
// will validate the requests with the security manager
SecurityManager* _securityManager;
void fetchConfig(AsyncWebServerRequest *request) {
// verify the request against the predicate
Authentication authentication = _securityManager->authenticateRequest(request);
if (!getAuthenticationPredicate()(authentication)) {
request->send(401);
return;
}
// delegate to underlying implemetation
SettingsService::fetchConfig(request);
}
void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument) {
// verify the request against the predicate
Authentication authentication = _securityManager->authenticateRequest(request);
if (!getAuthenticationPredicate()(authentication)) {
request->send(401);
return;
}
// delegate to underlying implemetation
SettingsService::updateConfig(request, jsonDocument);
}
// override to override the default authentication predicate, IS_ADMIN
AuthenticationPredicate getAuthenticationPredicate() {
return AuthenticationPredicates::IS_ADMIN;
}
};
#endif // end SettingsService

27
src/SimpleService.h → lib/framework/SimpleService.h

@ -33,7 +33,7 @@ private:
AsyncJsonWebHandler _updateHandler;
void fetchConfig(AsyncWebServerRequest *request){
void fetchConfig(AsyncWebServerRequest *request) {
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_SETTINGS_SIZE);
JsonObject jsonObject = response->getRoot();
writeToJsonObject(jsonObject);
@ -41,8 +41,8 @@ private:
request->send(response);
}
void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument){
if (jsonDocument.is<JsonObject>()){
void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument) {
if (jsonDocument.is<JsonObject>()) {
JsonObject newConfig = jsonDocument.as<JsonObject>();
readFromJsonObject(newConfig);
@ -59,32 +59,25 @@ private:
protected:
// will serve setting endpoints from here
AsyncWebServer* _server;
// reads the local config from the
virtual void readFromJsonObject(JsonObject& root){}
virtual void writeToJsonObject(JsonObject& root){}
virtual void readFromJsonObject(JsonObject& root) {}
virtual void writeToJsonObject(JsonObject& root) {}
// implement to perform action when config has been updated
virtual void onConfigUpdated(){}
virtual void onConfigUpdated() {}
public:
SimpleService(AsyncWebServer* server, char const* servicePath):
_server(server) {
// configure fetch config handler
_server->on(servicePath, HTTP_GET, std::bind(&SimpleService::fetchConfig, this, std::placeholders::_1));
SimpleService(AsyncWebServer* server, char const* servicePath) {
server->on(servicePath, HTTP_GET, std::bind(&SimpleService::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(&SimpleService::updateConfig, this, std::placeholders::_1, std::placeholders::_2));
_server->addHandler(&_updateHandler);
server->addHandler(&_updateHandler);
}
virtual ~SimpleService() {}
};

8
src/SystemStatus.cpp → lib/framework/SystemStatus.cpp

@ -1,12 +1,12 @@
#include <SystemStatus.h>
SystemStatus::SystemStatus(AsyncWebServer *server, SecurityManager* securityManager) : _server(server), _securityManager(securityManager) {
_server->on(SYSTEM_STATUS_SERVICE_PATH, HTTP_GET,
_securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
SystemStatus::SystemStatus(AsyncWebServer* server, SecurityManager* securityManager) {
server->on(SYSTEM_STATUS_SERVICE_PATH, HTTP_GET,
securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
);
}
void SystemStatus::systemStatus(AsyncWebServerRequest *request) {
void SystemStatus::systemStatus(AsyncWebServerRequest *request) {
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_ESP_STATUS_SIZE);
JsonObject root = response->getRoot();
#if defined(ESP8266)

7
src/SystemStatus.h → lib/framework/SystemStatus.h

@ -20,14 +20,11 @@
class SystemStatus {
public:
SystemStatus(AsyncWebServer *server, SecurityManager* securityManager);
SystemStatus(AsyncWebServer* server, SecurityManager* securityManager);
private:
AsyncWebServer* _server;
SecurityManager* _securityManager;
void systemStatus(AsyncWebServerRequest *request);
};

8
src/WiFiScanner.cpp → lib/framework/WiFiScanner.cpp

@ -1,13 +1,13 @@
#include <WiFiScanner.h>
WiFiScanner::WiFiScanner(AsyncWebServer *server, SecurityManager* securityManager) : _server(server) {
_server->on(SCAN_NETWORKS_SERVICE_PATH, HTTP_GET,
WiFiScanner::WiFiScanner(AsyncWebServer *server, SecurityManager* securityManager) {
server->on(SCAN_NETWORKS_SERVICE_PATH, HTTP_GET,
securityManager->wrapRequest(std::bind(&WiFiScanner::scanNetworks, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN)
);
_server->on(LIST_NETWORKS_SERVICE_PATH, HTTP_GET,
server->on(LIST_NETWORKS_SERVICE_PATH, HTTP_GET,
securityManager->wrapRequest(std::bind(&WiFiScanner::listNetworks, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN)
);
}
};
void WiFiScanner::scanNetworks(AsyncWebServerRequest *request) {
if (WiFi.scanComplete() != -1){

2
src/WiFiScanner.h → lib/framework/WiFiScanner.h

@ -28,8 +28,6 @@ class WiFiScanner {
private:
AsyncWebServer* _server;
void scanNetworks(AsyncWebServerRequest *request);
void listNetworks(AsyncWebServerRequest *request);

7
src/WiFiSettingsService.cpp → lib/framework/WiFiSettingsService.cpp

@ -1,9 +1,16 @@
#include <WiFiSettingsService.h>
WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, WIFI_SETTINGS_SERVICE_PATH, WIFI_SETTINGS_FILE) {
// Disable WiFi config persistance and auto reconnect
WiFi.persistent(false);
WiFi.setAutoReconnect(false);
#if defined(ESP8266)
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1));
#elif defined(ESP_PLATFORM)
// Init the wifi driver on ESP32
WiFi.mode(WIFI_MODE_MAX);
WiFi.mode(WIFI_MODE_NULL);
WiFi.onEvent(std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED);
#endif
}

2
src/WiFiSettingsService.h → lib/framework/WiFiSettingsService.h

@ -1,7 +1,7 @@
#ifndef WiFiSettingsService_h
#define WiFiSettingsService_h
#include <SettingsService.h>
#include <AdminSettingsService.h>
#include <IPAddress.h>
#define WIFI_SETTINGS_FILE "/config/wifiSettings.json"

8
src/WiFiStatus.cpp → lib/framework/WiFiStatus.cpp

@ -1,9 +1,9 @@
#include <WiFiStatus.h>
WiFiStatus::WiFiStatus(AsyncWebServer *server, SecurityManager* securityManager) : _server(server), _securityManager(securityManager) {
_server->on(WIFI_STATUS_SERVICE_PATH, HTTP_GET,
_securityManager->wrapRequest(std::bind(&WiFiStatus::wifiStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
);
WiFiStatus::WiFiStatus(AsyncWebServer* server, SecurityManager* securityManager) {
server->on(WIFI_STATUS_SERVICE_PATH, HTTP_GET,
securityManager->wrapRequest(std::bind(&WiFiStatus::wifiStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
);
#if defined(ESP8266)
_onStationModeConnectedHandler = WiFi.onStationModeConnected(onStationModeConnected);
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(onStationModeDisconnected);

5
src/WiFiStatus.h → lib/framework/WiFiStatus.h

@ -22,13 +22,10 @@ class WiFiStatus {
public:
WiFiStatus(AsyncWebServer *server, SecurityManager* securityManager);
WiFiStatus(AsyncWebServer* server, SecurityManager* securityManager);
private:
AsyncWebServer* _server;
SecurityManager* _securityManager;
#if defined(ESP8266)
// handler refrences for logging important WiFi events over serial
WiFiEventHandler _onStationModeConnectedHandler;

26
src/DemoProject.cpp

@ -0,0 +1,26 @@
#include <DemoProject.h>
DemoProject::DemoProject(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, DEMO_SETTINGS_PATH, DEMO_SETTINGS_FILE) {
pinMode(BLINK_LED, OUTPUT);
}
DemoProject::~DemoProject() {}
void DemoProject::loop() {
unsigned delay = MAX_DELAY / 255 * (255 - _blinkSpeed);
unsigned long currentMillis = millis();
if (!_lastBlink || (unsigned long)(currentMillis - _lastBlink) >= delay) {
_lastBlink = currentMillis;
digitalWrite(BLINK_LED, !digitalRead(BLINK_LED));
}
}
void DemoProject::readFromJsonObject(JsonObject& root) {
_blinkSpeed = root["blink_speed"] | DEFAULT_BLINK_SPEED;
}
void DemoProject::writeToJsonObject(JsonObject& root) {
// connection settings
root["blink_speed"] = _blinkSpeed;
}

34
src/DemoProject.h

@ -0,0 +1,34 @@
#ifndef DemoProject_h
#define DemoProject_h
#include <AdminSettingsService.h>
#define BLINK_LED 2
#define MAX_DELAY 1000
#define DEFAULT_BLINK_SPEED 100
#define DEMO_SETTINGS_FILE "/config/demoSettings.json"
#define DEMO_SETTINGS_PATH "/rest/demoSettings"
class DemoProject : public AdminSettingsService {
public:
DemoProject(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
~DemoProject();
void loop();
private:
unsigned long _lastBlink = 0;
uint8_t _blinkSpeed = 255;
protected:
void readFromJsonObject(JsonObject& root);
void writeToJsonObject(JsonObject& root);
};
#endif

99
src/main.cpp

@ -1,99 +1,34 @@
#include <Arduino.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(ESP_PLATFORM)
#include <WiFi.h>
#include <AsyncTCP.h>
#include <SPIFFS.h>
#endif
#include <ESP8266React.h>
#include <DemoProject.h>
#include <FS.h>
#include <SecuritySettingsService.h>
#include <WiFiSettingsService.h>
#include <APSettingsService.h>
#include <NTPSettingsService.h>
#include <OTASettingsService.h>
#include <AuthenticationService.h>
#include <WiFiScanner.h>
#include <WiFiStatus.h>
#include <NTPStatus.h>
#include <APStatus.h>
#include <SystemStatus.h>
#define SERIAL_BAUD_RATE 115200
AsyncWebServer server(80);
SecuritySettingsService securitySettingsService = SecuritySettingsService(&server, &SPIFFS);
WiFiSettingsService wifiSettingsService = WiFiSettingsService(&server, &SPIFFS, &securitySettingsService);
APSettingsService apSettingsService = APSettingsService(&server, &SPIFFS, &securitySettingsService);
NTPSettingsService ntpSettingsService = NTPSettingsService(&server, &SPIFFS, &securitySettingsService);
OTASettingsService otaSettingsService = OTASettingsService(&server, &SPIFFS, &securitySettingsService);
AuthenticationService authenticationService = AuthenticationService(&server, &securitySettingsService);
WiFiScanner wifiScanner = WiFiScanner(&server, &securitySettingsService);
WiFiStatus wifiStatus = WiFiStatus(&server, &securitySettingsService);
NTPStatus ntpStatus = NTPStatus(&server, &securitySettingsService);
APStatus apStatus = APStatus(&server, &securitySettingsService);
SystemStatus systemStatus = SystemStatus(&server, &securitySettingsService);;
ESP8266React esp8266React(&server, &SPIFFS);
DemoProject demoProject = DemoProject(&server, &SPIFFS, esp8266React.getSecurityManager());
void setup() {
// Disable wifi config persistance and auto reconnect
WiFi.persistent(false);
WiFi.setAutoReconnect(false);
#if defined(ESP_PLATFORM)
// Init the wifi driver on ESP32
WiFi.mode(WIFI_MODE_MAX);
WiFi.mode(WIFI_MODE_NULL);
#endif
// start serial and filesystem
Serial.begin(SERIAL_BAUD_RATE);
SPIFFS.begin();
// Start security settings service first
securitySettingsService.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/");
server.serveStatic("/favicon.ico", SPIFFS, "/www/favicon.ico");
// start the file system (must be done before starting the framework)
SPIFFS.begin();
// Serving all other get requests with "/www/index.htm"
// OPTIONS get a straight up 200 response
server.onNotFound([](AsyncWebServerRequest *request) {
if (request->method() == HTTP_GET) {
request->send(SPIFFS, "/www/index.html");
} else if (request->method() == HTTP_OPTIONS) {
request->send(200);
} else {
request->send(404);
}
});
// start the framework and demo project
esp8266React.begin();
// Disable CORS if required
#if defined(ENABLE_CORS)
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", CORS_ORIGIN);
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
#endif
// start the demo project
demoProject.begin();
// start the server
server.begin();
}
void loop() {
wifiSettingsService.loop();
apSettingsService.loop();
ntpSettingsService.loop();
otaSettingsService.loop();
// run the framework's loop function
esp8266React.loop();
// run the demo project's loop function
demoProject.loop();
}
Loading…
Cancel
Save