Skip to content

Commit a1f4e57

Browse files
authored
Rework backend add MQTT and WebSocket support
* Update back end to add MQTT and WebSocket support * Update demo project to demonstrate MQTT and WebSockets * Update documentation to describe newly added and modified functionallity * Introduce separate MQTT pub/sub, HTTP get/post and WebSocket rx/tx classes * Significant reanaming - more accurate class names * Use PROGMEM_WWW as default * Update README documenting PROGMEM_WWW as default * Update README with API changes
1 parent c47ea49 commit a1f4e57

File tree

77 files changed

+2877
-1162
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+2877
-1162
lines changed

README.md

+236-128
Large diffs are not rendered by default.

data/config/demoSettings.json

-3
This file was deleted.

data/config/mqttSettings.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"enabled": false,
3+
"host": "test.mosquitto.org",
4+
"port": 1883,
5+
"authenticated": false,
6+
"username": "mqttuser",
7+
"password": "mqttpassword",
8+
"keepAlive": 16,
9+
"cleanSession": true,
10+
"maxTopicLength": 128
11+
}

interface/.env.development

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# Change the IP address to that of your ESP device to enable local development of the UI.
22
# Remember to also enable CORS in platformio.ini before uploading the code to the device.
3-
REACT_APP_ENDPOINT_ROOT=http://192.168.0.21/rest/
3+
REACT_APP_HTTP_ROOT=http://192.168.0.99
4+
REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.99

interface/.env.production

-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
REACT_APP_ENDPOINT_ROOT=/rest/
21
GENERATE_SOURCEMAP=false

interface/package-lock.json

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

interface/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"@material-ui/core": "^4.9.8",
77
"@material-ui/icons": "^4.9.1",
88
"@types/jwt-decode": "^2.2.1",
9+
"@types/lodash": "^4.14.149",
910
"@types/node": "^12.12.32",
1011
"@types/react": "^16.9.27",
1112
"@types/react-dom": "^16.9.5",
@@ -14,6 +15,7 @@
1415
"@types/react-router-dom": "^5.1.3",
1516
"compression-webpack-plugin": "^3.0.1",
1617
"jwt-decode": "^2.2.0",
18+
"lodash": "^4.17.15",
1719
"mime-types": "^2.1.25",
1820
"moment": "^2.24.0",
1921
"notistack": "^0.9.7",
@@ -24,6 +26,7 @@
2426
"react-router": "^5.1.2",
2527
"react-router-dom": "^5.1.2",
2628
"react-scripts": "3.4.1",
29+
"sockette": "^2.0.6",
2730
"typescript": "^3.7.5",
2831
"zlib": "^1.0.5"
2932
},

interface/src/AppRouting.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Security from './security/Security';
1515
import System from './system/System';
1616

1717
import { PROJECT_PATH } from './api';
18+
import Mqtt from './mqtt/Mqtt';
1819

1920
class AppRouting extends Component {
2021

@@ -31,6 +32,7 @@ class AppRouting extends Component {
3132
<AuthenticatedRoute exact path="/wifi/*" component={WiFiConnection} />
3233
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
3334
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
35+
<AuthenticatedRoute exact path="/mqtt/*" component={Mqtt} />
3436
<AuthenticatedRoute exact path="/security/*" component={Security} />
3537
<AuthenticatedRoute exact path="/system/*" component={System} />
3638
<Redirect to="/" />

interface/src/api/Endpoints.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks";
99
export const WIFI_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "wifiSettings";
1010
export const WIFI_STATUS_ENDPOINT = ENDPOINT_ROOT + "wifiStatus";
1111
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings";
12+
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "mqttSettings";
13+
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + "mqttStatus";
1214
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
1315
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
1416
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";

interface/src/api/Env.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
11
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
22
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
3-
export const ENDPOINT_ROOT = process.env.REACT_APP_ENDPOINT_ROOT!;
3+
4+
export const ENDPOINT_ROOT = calculateEndpointRoot("/rest/");
5+
export const WEB_SOCKET_ROOT = calculateWebSocketRoot("/ws/");
6+
7+
function calculateEndpointRoot(endpointPath: string) {
8+
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
9+
if (httpRoot) {
10+
return httpRoot + endpointPath;
11+
}
12+
const location = window.location;
13+
return location.protocol + "//" + location.host + endpointPath;
14+
}
15+
16+
function calculateWebSocketRoot(webSocketPath: string) {
17+
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT;
18+
if (webSocketRoot) {
19+
return webSocketRoot + webSocketPath;
20+
}
21+
const location = window.location;
22+
const webProtocol = location.protocol === "https:" ? "wss:" : "ws:";
23+
return webProtocol + "//" + location.host + webSocketPath;
24+
}

interface/src/authentication/Authentication.ts

+10
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,13 @@ export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestIni
6161
});
6262
});
6363
}
64+
65+
export function addAccessTokenParameter(url: string) {
66+
const accessToken = localStorage.getItem(ACCESS_TOKEN);
67+
if (!accessToken) {
68+
return url;
69+
}
70+
const parsedUrl = new URL(url);
71+
parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken);
72+
return parsedUrl.toString();
73+
}

interface/src/components/MenuAppBar.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import SettingsIcon from '@material-ui/icons/Settings';
1313
import AccessTimeIcon from '@material-ui/icons/AccessTime';
1414
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
1515
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
16+
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
1617
import LockIcon from '@material-ui/icons/Lock';
1718
import MenuIcon from '@material-ui/icons/Menu';
1819

@@ -136,6 +137,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
136137
</ListItemIcon>
137138
<ListItemText primary="Network Time" />
138139
</ListItem>
140+
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
141+
<ListItemIcon>
142+
<DeviceHubIcon />
143+
</ListItemIcon>
144+
<ListItemText primary="MQTT" />
145+
</ListItem>
139146
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
140147
<ListItemIcon>
141148
<LockIcon />

interface/src/components/RestController.tsx

+11-18
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ import { redirectingAuthorizedFetch } from '../authentication';
55

66
export interface RestControllerProps<D> extends WithSnackbarProps {
77
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
8-
handleSliderChange: (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => void;
98

10-
setData: (data: D) => void;
9+
setData: (data: D, callback?: () => void) => void;
1110
saveData: () => void;
1211
loadData: () => void;
1312

@@ -16,13 +15,7 @@ export interface RestControllerProps<D> extends WithSnackbarProps {
1615
errorMessage?: string;
1716
}
1817

19-
interface RestControllerState<D> {
20-
data?: D;
21-
loading: boolean;
22-
errorMessage?: string;
23-
}
24-
25-
const extractValue = (event: React.ChangeEvent<HTMLInputElement>) => {
18+
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
2619
switch (event.target.type) {
2720
case "number":
2821
return event.target.valueAsNumber;
@@ -33,6 +26,12 @@ const extractValue = (event: React.ChangeEvent<HTMLInputElement>) => {
3326
}
3427
}
3528

29+
interface RestControllerState<D> {
30+
data?: D;
31+
loading: boolean;
32+
errorMessage?: string;
33+
}
34+
3635
export function restController<D, P extends RestControllerProps<D>>(endpointUrl: string, RestController: React.ComponentType<P & RestControllerProps<D>>) {
3736
return withSnackbar(
3837
class extends React.Component<Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps, RestControllerState<D>> {
@@ -43,12 +42,12 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
4342
errorMessage: undefined
4443
};
4544

46-
setData = (data: D) => {
45+
setData = (data: D, callback?: () => void) => {
4746
this.setState({
4847
data,
4948
loading: false,
5049
errorMessage: undefined
51-
});
50+
}, callback);
5251
}
5352

5453
loadData = () => {
@@ -95,19 +94,13 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
9594
}
9695

9796
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
98-
const data = { ...this.state.data!, [name]: extractValue(event) };
97+
const data = { ...this.state.data!, [name]: extractEventValue(event) };
9998
this.setState({ data });
10099
}
101100

102-
handleSliderChange = (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => {
103-
const data = { ...this.state.data!, [name]: value };
104-
this.setState({ data });
105-
};
106-
107101
render() {
108102
return <RestController
109103
handleValueChange={this.handleValueChange}
110-
handleSliderChange={this.handleSliderChange}
111104
setData={this.setData}
112105
saveData={this.saveData}
113106
loadData={this.loadData}

interface/src/components/RestFormLoader.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import React from 'react';
22

33
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
44
import { Button, LinearProgress, Typography } from '@material-ui/core';
5-
import { RestControllerProps } from './RestController';
5+
6+
import { RestControllerProps } from '.';
67

78
const useStyles = makeStyles((theme: Theme) =>
89
createStyles({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import React from 'react';
2+
import Sockette from 'sockette';
3+
import throttle from 'lodash/throttle';
4+
import { withSnackbar, WithSnackbarProps } from 'notistack';
5+
6+
import { addAccessTokenParameter } from '../authentication';
7+
import { extractEventValue } from '.';
8+
9+
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
10+
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
11+
12+
setData: (data: D, callback?: () => void) => void;
13+
saveData: () => void;
14+
saveDataAndClear(): () => void;
15+
16+
connected: boolean;
17+
data?: D;
18+
}
19+
20+
interface WebSocketControllerState<D> {
21+
ws: Sockette;
22+
connected: boolean;
23+
clientId?: string;
24+
data?: D;
25+
}
26+
27+
enum WebSocketMessageType {
28+
ID = "id",
29+
PAYLOAD = "payload"
30+
}
31+
32+
interface WebSocketIdMessage {
33+
type: typeof WebSocketMessageType.ID;
34+
id: string;
35+
}
36+
37+
interface WebSocketPayloadMessage<D> {
38+
type: typeof WebSocketMessageType.PAYLOAD;
39+
origin_id: string;
40+
payload: D;
41+
}
42+
43+
export type WebSocketMessage<D> = WebSocketIdMessage | WebSocketPayloadMessage<D>;
44+
45+
export function webSocketController<D, P extends WebSocketControllerProps<D>>(wsUrl: string, wsThrottle: number, WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>) {
46+
return withSnackbar(
47+
class extends React.Component<Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps, WebSocketControllerState<D>> {
48+
constructor(props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps) {
49+
super(props);
50+
this.state = {
51+
ws: new Sockette(addAccessTokenParameter(wsUrl), {
52+
onmessage: this.onMessage,
53+
onopen: this.onOpen,
54+
onclose: this.onClose,
55+
}),
56+
connected: false
57+
}
58+
}
59+
60+
componentWillUnmount() {
61+
this.state.ws.close();
62+
}
63+
64+
onMessage = (event: MessageEvent) => {
65+
const rawData = event.data;
66+
if (typeof rawData === 'string' || rawData instanceof String) {
67+
this.handleMessage(JSON.parse(rawData as string) as WebSocketMessage<D>);
68+
}
69+
}
70+
71+
handleMessage = (message: WebSocketMessage<D>) => {
72+
switch (message.type) {
73+
case WebSocketMessageType.ID:
74+
this.setState({ clientId: message.id });
75+
break;
76+
case WebSocketMessageType.PAYLOAD:
77+
const { clientId, data } = this.state;
78+
if (clientId && (!data || clientId !== message.origin_id)) {
79+
this.setState(
80+
{ data: message.payload }
81+
);
82+
}
83+
break;
84+
}
85+
}
86+
87+
onOpen = () => {
88+
this.setState({ connected: true });
89+
}
90+
91+
onClose = () => {
92+
this.setState({ connected: false, clientId: undefined, data: undefined });
93+
}
94+
95+
setData = (data: D, callback?: () => void) => {
96+
this.setState({ data }, callback);
97+
}
98+
99+
saveData = throttle(() => {
100+
const { ws, connected, data } = this.state;
101+
if (connected) {
102+
ws.json(data);
103+
}
104+
}, wsThrottle);
105+
106+
saveDataAndClear = throttle(() => {
107+
const { ws, connected, data } = this.state;
108+
if (connected) {
109+
this.setState({
110+
data: undefined
111+
}, () => ws.json(data));
112+
}
113+
}, wsThrottle);
114+
115+
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
116+
const data = { ...this.state.data!, [name]: extractEventValue(event) };
117+
this.setState({ data });
118+
}
119+
120+
render() {
121+
return <WebSocketController
122+
handleValueChange={this.handleValueChange}
123+
setData={this.setData}
124+
saveData={this.saveData}
125+
saveDataAndClear={this.saveDataAndClear}
126+
connected={this.state.connected}
127+
data={this.state.data}
128+
{...this.props as P}
129+
/>;
130+
}
131+
132+
});
133+
}

0 commit comments

Comments
 (0)