Skip to content

Commit ab806e7

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
1 parent c47ea49 commit ab806e7

Some content is hidden

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

72 files changed

+2321
-924
lines changed

README.md

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ There is also a manifest file which contains the app name to use when adding the
273273
}
274274
```
275275

276-
## Back end overview
276+
## Back end
277277

278278
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.
279279

@@ -328,17 +328,125 @@ void loop() {
328328
}
329329
```
330330

331-
### Adding endpoints
331+
### Developing with the framework
332332

333-
There are some simple classes that support adding configurable services/features to the device:
333+
The framework promotes a modular design and exposes features you may re-use to speed up the development of your project. Where possible it is recommended that you use the features the frameworks supplies. These are documented below.
334334

335-
Class | Description
336-
----- | -----------
337-
[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.
338-
[SettingsService.h](lib/framework/SettingsService.h) | As above, however this class also handles persisting the settings as JSON to the file system.
339-
[AdminSettingsService.h](lib/framework/AdminSettingsService.h) | Extends SettingsService to secure the endpoint to administrators only, the authentication predicate can be overridden if required.
335+
The following diagram visualises how the framework's modular components fit together. They are described in detail below.
340336

341-
The demo project shows how these can be used, explore the framework classes for more examples.
337+
![framework diagram](/media/framework.png?raw=true "framework diagram")
338+
339+
#### Settings service
340+
341+
The [SettingsService.h](lib/framework/SettingsService.h) class is a responsible for managing settings and interfacing with code which wants to control or respond to changes in those settings. You can define a data class to hold settings then build a SettingsService instance to manage them:
342+
343+
```cpp
344+
class LightSettings {
345+
public:
346+
bool on = false;
347+
uint8_t brightness = 255;
348+
};
349+
350+
class LightSettingsService : public SettingsService<LightSettings> {
351+
};
352+
```
353+
354+
You may listen for changes to settings by registering an update handler callback. It is possible to remove an update handler later if required. An "origin" pointer is passed to the update handler which may point to the client or object which made the update.
355+
356+
```cpp
357+
// register an update handler
358+
update_handler_id_t myUpdateHandler = lightSettingsService.addUpdateHandler(
359+
[&](String originId) {
360+
Serial.println("The light settings have been updated");
361+
}
362+
);
363+
364+
// remove the update handler
365+
lightSettingsService.removeUpdateHandler(myUpdateHandler);
366+
```
367+
368+
SettingsService exposes a read function which you may use to safely read the settings. This function takes care of protecting against parallel access to the settings in multi-core enviornments such as the ESP32.
369+
370+
```cpp
371+
lightSettingsService.read([&](LightSettings& settings) {
372+
digitalWrite(LED_PIN, settings.on ? HIGH : LOW)
373+
});
374+
```
375+
376+
SettingsService also exposes an update function which allows the caller to update the settings. The update function takes care of calling the registered update handler callbacks once the update is complete.
377+
378+
```cpp
379+
lightSettingsService.update([&](LightSettings& settings) {
380+
settings.on = true; // turn on the lights!
381+
});
382+
```
383+
384+
#### Serialization
385+
386+
When transmitting settings over HTTP, WebSockets or MQTT they must to be marshalled into a serialzable form. The framework uses ArduinoJson for serialization and provides the abstract classes [SettingsSerializer.h](lib/framework/SettingsSerializer.h) and [SettingsDeserializer.h](lib/framework/SettingsDeserializer.h) to facilitate the seriliaztion of settings:
387+
388+
```cpp
389+
class LightSettingsSerializer : public SettingsSerializer<LightSettings> {
390+
public:
391+
void serialize(LightSettings& settings, JsonObject root) {
392+
root["on"] = settings.on;
393+
root["brightness"] = settings.brightness;
394+
}
395+
};
396+
397+
class LightSettingsDeserializer : public SettingsDeserializer<LightSettings> {
398+
public:
399+
void deserialize(LightSettings& settings, JsonObject root) {
400+
settings.on = root["on"] | false;
401+
settings.brightness = root["brightness"] | 255;
402+
}
403+
};
404+
```
405+
406+
It is recommended you make create singletons for your serialzers and that they are stateless:
407+
408+
```cpp
409+
static LightSettingsSerializer SERIALIZER;
410+
static LightSettingsDeserializer DESERIALIZER;
411+
```
412+
413+
#### Endpoints
414+
415+
The framework provides a [SettingsEndpoint.h](lib/framework/SettingsEndpoint.h) class which may be used to register GET and POST handlers to read and update the settings over HTTP. You may construct a SettingsEndpoint as a part of the SettingsService or separately if you prefer. The code below demonstrates how to extend the LightSettingsService class to provide an unsecured endpoint:
416+
417+
```cpp
418+
class LightSettingsService : public SettingsService<LightSettings> {
419+
public:
420+
LightSettingsService(AsyncWebServer* server, SecurityManager* securityManager) :
421+
_settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, "/rest/lightSettings") {
422+
}
423+
424+
private:
425+
SettingsEndpoint<LightSettings> _settingsEndpoint;
426+
};
427+
```
428+
429+
Endpoint security is provided by authentication predicates which are [documented below](#security-features). A security manager and authentication predicate may be provided if an secure endpoint is required. The demo app shows how endpoints can be secured.
430+
431+
#### Persistence
432+
433+
[SettingsPersistence.h](lib/framework/SettingsPersistence.h) allows you to save settings to the filesystem. SettingsPersistence automatically writes changes to the file system when settings are updated. This feature can be disabled by calling `disableAutomatic()` if manual control of persistence is required.
434+
435+
As with SettingsEndpoint you may elect to construct this as a part of a SettingsService class or separately. The code below demonstrates how to extend the LightSettingsService class to provide persistence:
436+
437+
```cpp
438+
class LightSettingsService : public SettingsService<LightSettings> {
439+
public:
440+
LightSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
441+
_settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, "/rest/lightSettings"),
442+
_settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, "/config/lightSettings.json") {
443+
}
444+
445+
private:
446+
SettingsEndpoint<LightSettings> _settingsEndpoint;
447+
SettingsPersistence<LightSettings> _settingsPersistence;
448+
};
449+
```
342450

343451
### Security features
344452

data/config/demoSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"blink_speed": 100
2+
"led_on": true
33
}

data/config/mqttSettings.json

Lines changed: 11 additions & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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_ENDPOINT_ROOT=http://192.168.0.99/rest/
4+
REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.99

interface/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

interface/package.json

Lines changed: 3 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
11
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
22
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
33
export const ENDPOINT_ROOT = process.env.REACT_APP_ENDPOINT_ROOT!;
4+
5+
// TODO use same approach for rest endpoint?
6+
export const WEB_SOCKET_ROOT = calculateWebSocketPrefix("/ws");
7+
8+
function calculateWebSocketPrefix(webSocketPath: string) {
9+
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT;
10+
if (!webSocketRoot || webSocketRoot.length === 0) {
11+
var loc = window.location, webSocketURI;
12+
if (loc.protocol === "https:") {
13+
webSocketURI = "wss:";
14+
} else {
15+
webSocketURI = "ws:";
16+
}
17+
return webSocketURI + "//" + loc.host + webSocketPath;
18+
}
19+
return webSocketRoot + webSocketPath;
20+
}

interface/src/components/MenuAppBar.tsx

Lines changed: 7 additions & 0 deletions
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 />

0 commit comments

Comments
 (0)