commit
67eb9d4017
91
README.md
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)
|
||||
|
3
data/config/demoSettings.json
Normal file
3
data/config/demoSettings.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"blink_speed": 100
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"server":"pool.ntp.org",
|
||||
"interval":3600
|
||||
"server": "pool.ntp.org",
|
||||
"interval": 3600
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"enabled":true,
|
||||
"enabled": true,
|
||||
"port": 8266,
|
||||
"password": "esp-react"
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"jwt_secret":"esp8266-react",
|
||||
"jwt_secret": "esp8266-react",
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"ssid":"",
|
||||
"password":"password",
|
||||
"hostname":"esp8266-react",
|
||||
"static_ip_config":false
|
||||
"ssid": "",
|
||||
"password": "password",
|
||||
"hostname": "esp8266-react",
|
||||
"static_ip_config": false
|
||||
}
|
@ -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
|
||||
|
@ -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/
|
||||
|
2516
interface/package-lock.json
generated
2516
interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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": {
|
||||
|
@ -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>
|
||||
|
@ -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
Normal file
58
interface/src/components/LoadingNotification.js
Normal file
@ -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
|
||||
};
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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 +0,0 @@
|
||||
export const APP_NAME = process.env.REACT_APP_NAME;
|
@ -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
Normal file
3
interface/src/constants/Env.js
Normal file
@ -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;
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
|
||||
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">
|
||||
<LoadingNotification
|
||||
onReset={loadData}
|
||||
fetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
render={() =>
|
||||
<APSettingsForm
|
||||
apSettings={data}
|
||||
apSettingsFetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
onSubmit={this.props.saveData}
|
||||
onReset={this.props.loadData}
|
||||
handleValueChange={this.props.handleValueChange}
|
||||
onSubmit={saveData}
|
||||
onReset={loadData}
|
||||
handleValueChange={handleValueChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
|
@ -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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -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">
|
||||
<SectionContent title="Manage Users" titleGutter>
|
||||
<LoadingNotification
|
||||
onReset={loadData}
|
||||
fetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
render={() =>
|
||||
<ManageUsersForm
|
||||
userData={data}
|
||||
userDataFetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
onSubmit={this.props.saveData}
|
||||
onReset={this.props.loadData}
|
||||
setData={this.props.setData}
|
||||
handleValueChange={this.props.handleValueChange}
|
||||
onSubmit={saveData}
|
||||
onReset={loadData}
|
||||
setData={setData}
|
||||
handleValueChange={handleValueChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { NTP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
||||
import {restComponent} from '../components/RestComponent';
|
||||
import { restComponent } from '../components/RestComponent';
|
||||
import LoadingNotification from '../components/LoadingNotification';
|
||||
import SectionContent from '../components/SectionContent';
|
||||
import NTPSettingsForm from '../forms/NTPSettingsForm';
|
||||
|
||||
@ -12,16 +13,21 @@ class NTPSettings extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, fetched, errorMessage } = this.props;
|
||||
const { fetched, errorMessage, data, saveData, loadData, handleValueChange } = this.props;
|
||||
return (
|
||||
<SectionContent title="NTP Settings">
|
||||
<LoadingNotification
|
||||
onReset={loadData}
|
||||
fetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
render={() =>
|
||||
<NTPSettingsForm
|
||||
ntpSettings={data}
|
||||
ntpSettingsFetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
onSubmit={this.props.saveData}
|
||||
onReset={this.props.loadData}
|
||||
handleValueChange={this.props.handleValueChange}
|
||||
onSubmit={saveData}
|
||||
onReset={loadData}
|
||||
handleValueChange={handleValueChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { OTA_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
||||
import {restComponent} from '../components/RestComponent';
|
||||
import { restComponent } from '../components/RestComponent';
|
||||
import LoadingNotification from '../components/LoadingNotification';
|
||||
import SectionContent from '../components/SectionContent';
|
||||
import OTASettingsForm from '../forms/OTASettingsForm';
|
||||
|
||||
@ -12,17 +13,22 @@ class OTASettings extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, fetched, errorMessage } = this.props;
|
||||
const { fetched, errorMessage, data, saveData, loadData, handleValueChange, handleCheckboxChange } = this.props;
|
||||
return (
|
||||
<SectionContent title="OTA Settings">
|
||||
<LoadingNotification
|
||||
onReset={loadData}
|
||||
fetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
render={() =>
|
||||
<OTASettingsForm
|
||||
otaSettings={data}
|
||||
otaSettingsFetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
onSubmit={this.props.saveData}
|
||||
onReset={this.props.loadData}
|
||||
handleValueChange={this.props.handleValueChange}
|
||||
handleCheckboxChange={this.props.handleCheckboxChange}
|
||||
onSubmit={saveData}
|
||||
onReset={loadData}
|
||||
handleValueChange={handleValueChange}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
|
@ -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">
|
||||
<LoadingNotification
|
||||
onReset={loadData}
|
||||
fetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
render={() =>
|
||||
<SecuritySettingsForm
|
||||
securitySettings={data}
|
||||
securitySettingsFetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
onSubmit={this.props.saveData}
|
||||
onReset={this.props.loadData}
|
||||
handleValueChange={this.props.handleValueChange}
|
||||
onSubmit={saveData}
|
||||
onReset={loadData}
|
||||
handleValueChange={handleValueChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
|
@ -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}
|
||||
|
@ -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),
|
||||
@ -90,7 +84,7 @@ class SystemStatus extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
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">
|
||||
<LoadingNotification
|
||||
onReset={loadData}
|
||||
fetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
render={() =>
|
||||
<WiFiSettingsForm
|
||||
wifiSettings={data}
|
||||
wifiSettingsFetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
selectedNetwork={selectedNetwork}
|
||||
deselectNetwork={this.props.deselectNetwork}
|
||||
onSubmit={this.props.saveData}
|
||||
deselectNetwork={deselectNetwork}
|
||||
onSubmit={saveData}
|
||||
onReset={this.deselectNetworkAndLoadData}
|
||||
handleValueChange={this.props.handleValueChange}
|
||||
handleCheckboxChange={this.props.handleCheckboxChange}
|
||||
handleValueChange={handleValueChange}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
|
@ -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));
|
||||
|
@ -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,30 +27,15 @@ 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>
|
||||
{
|
||||
!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>
|
||||
@ -86,37 +61,20 @@ class APSettingsForm extends React.Component {
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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,18 +122,9 @@ 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}>
|
||||
@ -217,15 +196,6 @@ class ManageUsersForm extends React.Component {
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
:
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
@ -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,23 +26,9 @@ 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"]}
|
||||
@ -62,10 +39,9 @@ class NTPSettingsForm extends React.Component {
|
||||
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)"]}
|
||||
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}
|
||||
@ -74,37 +50,20 @@ class NTPSettingsForm extends React.Component {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -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,23 +34,9 @@ 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
|
||||
@ -71,7 +48,6 @@ class OTASettingsForm extends React.Component {
|
||||
}
|
||||
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"]}
|
||||
@ -83,7 +59,6 @@ class OTASettingsForm extends React.Component {
|
||||
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']}
|
||||
@ -94,37 +69,20 @@ class OTASettingsForm extends React.Component {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -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,17 +28,8 @@ 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}$']}
|
||||
@ -70,24 +53,13 @@ class SecuritySettingsForm extends React.Component {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -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>
|
||||
|
@ -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,21 +71,8 @@ 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>
|
||||
{
|
||||
!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() :
|
||||
@ -122,7 +100,6 @@ class WiFiSettingsForm extends React.Component {
|
||||
margin="normal"
|
||||
/>
|
||||
}
|
||||
|
||||
<TextValidator
|
||||
validators={['required', 'isHostname']}
|
||||
errorMessages={['Hostname is required', "Not a valid hostname"]}
|
||||
@ -133,7 +110,6 @@ class WiFiSettingsForm extends React.Component {
|
||||
onChange={handleValueChange('hostname')}
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<FormControlLabel className={classes.checkboxControl}
|
||||
control={
|
||||
<Checkbox
|
||||
@ -144,7 +120,6 @@ class WiFiSettingsForm extends React.Component {
|
||||
}
|
||||
label="Static IP Config?"
|
||||
/>
|
||||
|
||||
{
|
||||
wifiSettings.static_ip_config &&
|
||||
<Fragment>
|
||||
@ -200,37 +175,20 @@ class WiFiSettingsForm extends React.Component {
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
Normal file
82
interface/src/project/DemoController.js
Normal file
@ -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
Normal file
100
interface/src/project/DemoInformation.js
Normal file
@ -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
Normal file
37
interface/src/project/DemoProject.js
Normal file
@ -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
Normal file
30
interface/src/project/ProjectMenu.js
Normal file
@ -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
Normal file
32
interface/src/project/ProjectRouting.js
Normal file
@ -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;
|
@ -32,6 +32,7 @@ class NetworkTime extends Component {
|
||||
</MenuAppBar>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withAuthenticationContext(NetworkTime)
|
||||
|
@ -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();
|
||||
}
|
@ -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();
|
||||
|
||||
};
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
@ -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
Normal file
45
lib/framework/AdminSettingsService.h
Normal file
@ -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
|
@ -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);
|
@ -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
Normal file
55
lib/framework/ESP8266React.cpp
Normal file
@ -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
Normal file
59
lib/framework/ESP8266React.h
Normal file
@ -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
|
@ -1,7 +1,7 @@
|
||||
#ifndef NTPSettingsService_h
|
||||
#define NTPSettingsService_h
|
||||
|
||||
#include <SettingsService.h>
|
||||
#include <AdminSettingsService.h>
|
||||
|
||||
#include <TimeLib.h>
|
||||
#include <NtpClientLib.h>
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
};
|
@ -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
|
@ -29,7 +29,3 @@ void SecuritySettingsService::writeToJsonObject(JsonObject& root) {
|
||||
user["admin"] = _user.isAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
void SecuritySettingsService::begin() {
|
||||
readFromFS();
|
||||
}
|
@ -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);
|
@ -47,7 +47,7 @@ protected:
|
||||
return true;
|
||||
}
|
||||
|
||||
void readFromFS(){
|
||||
void readFromFS() {
|
||||
File configFile = _fs->open(_filePath, "r");
|
||||
|
||||
// use defaults if no config found
|
@ -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) {
|
||||
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 fetch config handler
|
||||
_server->on(servicePath, HTTP_GET, std::bind(&SettingsService::fetchConfig, this, std::placeholders::_1));
|
||||
|
||||
// configure update settings handler
|
||||
_updateHandler.setUri(servicePath);
|
||||
_updateHandler.setMethod(HTTP_POST);
|
||||
_updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE);
|
||||
_updateHandler.onRequest(std::bind(&SettingsService::updateConfig, this, std::placeholders::_1, std::placeholders::_2));
|
||||
_server->addHandler(&_updateHandler);
|
||||
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
|
@ -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,30 +59,23 @@ 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) {
|
||||
SimpleService(AsyncWebServer* server, char const* servicePath) {
|
||||
server->on(servicePath, HTTP_GET, std::bind(&SimpleService::fetchConfig, this, std::placeholders::_1));
|
||||
|
||||
// configure fetch config handler
|
||||
_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() {}
|
@ -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)
|
@ -21,13 +21,10 @@ class SystemStatus {
|
||||
|
||||
public:
|
||||
|
||||
SystemStatus(AsyncWebServer *server, SecurityManager* securityManager);
|
||||
SystemStatus(AsyncWebServer* server, SecurityManager* securityManager);
|
||||
|
||||
private:
|
||||
|
||||
AsyncWebServer* _server;
|
||||
SecurityManager* _securityManager;
|
||||
|
||||
void systemStatus(AsyncWebServerRequest *request);
|
||||
|
||||
};
|
@ -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){
|
@ -28,8 +28,6 @@ class WiFiScanner {
|
||||
|
||||
private:
|
||||
|
||||
AsyncWebServer* _server;
|
||||
|
||||
void scanNetworks(AsyncWebServerRequest *request);
|
||||
void listNetworks(AsyncWebServerRequest *request);
|
||||
|
@ -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
|
||||
}
|
@ -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"
|
@ -1,8 +1,8 @@
|
||||
#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);
|
@ -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
Normal file
26
src/DemoProject.cpp
Normal file
@ -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
Normal file
34
src/DemoProject.h
Normal file
@ -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
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);
|
||||
|
||||
// start the file system (must be done before starting the framework)
|
||||
SPIFFS.begin();
|
||||
|
||||
// Start security settings service first
|
||||
securitySettingsService.begin();
|
||||
// start the framework and demo project
|
||||
esp8266React.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");
|
||||
|
||||
// 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
|
||||
// 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…
Reference in New Issue
Block a user