From ab806e76e70a81723ae356fcccc6686136a84750 Mon Sep 17 00:00:00 2001 From: rjwats Date: Tue, 7 Apr 2020 10:19:51 +0100 Subject: [PATCH 01/35] Rework backend add MQTT and WebSocket support Update back end to add MQTT and WebSocket support Update demo project to demonstrate MQTT and WebSockets --- README.md | 126 +++++++++++++-- data/config/demoSettings.json | 2 +- data/config/mqttSettings.json | 11 ++ interface/.env.development | 3 +- interface/package-lock.json | 10 ++ interface/package.json | 3 + interface/src/AppRouting.tsx | 2 + interface/src/api/Endpoints.ts | 2 + interface/src/api/Env.ts | 17 ++ interface/src/components/MenuAppBar.tsx | 7 + interface/src/components/RestController.tsx | 29 ++-- interface/src/components/RestFormLoader.tsx | 3 +- interface/src/components/SocketController.tsx | 132 +++++++++++++++ interface/src/components/SocketFormLoader.tsx | 40 +++++ interface/src/components/index.ts | 4 + interface/src/mqtt/MQTT.tsx | 37 +++++ interface/src/mqtt/MQTTSettingsController.tsx | 30 ++++ interface/src/mqtt/MQTTSettingsForm.tsx | 131 +++++++++++++++ interface/src/mqtt/MQTTStatus.ts | 45 ++++++ interface/src/mqtt/MQTTStatusController.tsx | 29 ++++ interface/src/mqtt/MQTTStatusForm.tsx | 83 ++++++++++ interface/src/mqtt/types.ts | 29 ++++ interface/src/ntp/NTPSettingsForm.tsx | 3 +- interface/src/ntp/TZ.tsx | 2 +- interface/src/project/DemoController.tsx | 75 --------- interface/src/project/DemoProject.tsx | 14 +- .../project/LightBrokerSettingsController.tsx | 93 +++++++++++ .../project/LightSettingsRestController.tsx | 70 ++++++++ .../project/LightSettingsSocketController.tsx | 62 +++++++ interface/src/project/types.ts | 9 ++ interface/src/security/ManageUsersForm.tsx | 8 +- .../src/security/SecuritySettingsForm.tsx | 8 +- lib/framework/APSettingsService.cpp | 36 +---- lib/framework/APSettingsService.h | 41 ++++- lib/framework/AdminSettingsService.h | 50 ------ lib/framework/AsyncJsonCallbackResponse.h | 33 ---- lib/framework/AsyncJsonWebHandler.h | 131 --------------- lib/framework/AuthenticationService.cpp | 18 +-- lib/framework/AuthenticationService.h | 6 +- lib/framework/ESP8266React.cpp | 5 +- lib/framework/ESP8266React.h | 14 +- lib/framework/JsonUtils.h | 17 ++ lib/framework/MQTTSettingsService.cpp | 135 ++++++++++++++++ lib/framework/MQTTSettingsService.h | 127 +++++++++++++++ lib/framework/MQTTStatus.cpp | 24 +++ lib/framework/MQTTStatus.h | 31 ++++ lib/framework/NTPSettingsService.cpp | 51 ++---- lib/framework/NTPSettingsService.h | 41 +++-- lib/framework/OTASettingsService.cpp | 32 ++-- lib/framework/OTASettingsService.h | 32 +++- lib/framework/SecurityManager.h | 9 +- lib/framework/SecuritySettingsService.cpp | 84 +++++----- lib/framework/SecuritySettingsService.h | 57 +++++-- lib/framework/SettingsBroker.h | 106 ++++++++++++ lib/framework/SettingsDeserializer.h | 12 ++ lib/framework/SettingsEndpoint.h | 96 +++++++++++ lib/framework/SettingsPersistence.h | 131 ++++++++------- lib/framework/SettingsSerializer.h | 12 ++ lib/framework/SettingsService.h | 152 +++++------------- lib/framework/SettingsSocket.h | 126 +++++++++++++++ lib/framework/SimpleService.h | 87 ---------- lib/framework/WiFiSettingsService.cpp | 70 +------- lib/framework/WiFiSettingsService.h | 66 ++++++-- media/framework.png | Bin 0 -> 112054 bytes platformio.ini | 3 +- src/DemoProject.cpp | 27 ---- src/DemoProject.h | 34 ---- src/LightBrokerSettingsService.cpp | 13 ++ src/LightBrokerSettingsService.h | 55 +++++++ src/LightSettingsService.cpp | 67 ++++++++ src/LightSettingsService.h | 75 +++++++++ src/main.cpp | 20 ++- 72 files changed, 2321 insertions(+), 924 deletions(-) create mode 100644 data/config/mqttSettings.json create mode 100644 interface/src/components/SocketController.tsx create mode 100644 interface/src/components/SocketFormLoader.tsx create mode 100644 interface/src/mqtt/MQTT.tsx create mode 100644 interface/src/mqtt/MQTTSettingsController.tsx create mode 100644 interface/src/mqtt/MQTTSettingsForm.tsx create mode 100644 interface/src/mqtt/MQTTStatus.ts create mode 100644 interface/src/mqtt/MQTTStatusController.tsx create mode 100644 interface/src/mqtt/MQTTStatusForm.tsx create mode 100644 interface/src/mqtt/types.ts delete mode 100644 interface/src/project/DemoController.tsx create mode 100644 interface/src/project/LightBrokerSettingsController.tsx create mode 100644 interface/src/project/LightSettingsRestController.tsx create mode 100644 interface/src/project/LightSettingsSocketController.tsx create mode 100644 interface/src/project/types.ts delete mode 100644 lib/framework/AdminSettingsService.h delete mode 100644 lib/framework/AsyncJsonCallbackResponse.h delete mode 100644 lib/framework/AsyncJsonWebHandler.h create mode 100644 lib/framework/JsonUtils.h create mode 100644 lib/framework/MQTTSettingsService.cpp create mode 100644 lib/framework/MQTTSettingsService.h create mode 100644 lib/framework/MQTTStatus.cpp create mode 100644 lib/framework/MQTTStatus.h create mode 100644 lib/framework/SettingsBroker.h create mode 100644 lib/framework/SettingsDeserializer.h create mode 100644 lib/framework/SettingsEndpoint.h create mode 100644 lib/framework/SettingsSerializer.h create mode 100644 lib/framework/SettingsSocket.h delete mode 100644 lib/framework/SimpleService.h create mode 100644 media/framework.png delete mode 100644 src/DemoProject.cpp delete mode 100644 src/DemoProject.h create mode 100644 src/LightBrokerSettingsService.cpp create mode 100644 src/LightBrokerSettingsService.h create mode 100644 src/LightSettingsService.cpp create mode 100644 src/LightSettingsService.h diff --git a/README.md b/README.md index 249aeef6..4d8f7f87 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ There is also a manifest file which contains the app name to use when adding the } ``` -## Back end overview +## Back end 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. @@ -328,17 +328,125 @@ void loop() { } ``` -### Adding endpoints +### Developing with the framework -There are some simple classes that support adding configurable services/features to the device: +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. -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 following diagram visualises how the framework's modular components fit together. They are described in detail below. -The demo project shows how these can be used, explore the framework classes for more examples. +![framework diagram](/media/framework.png?raw=true "framework diagram") + +#### Settings service + +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: + +```cpp +class LightSettings { + public: + bool on = false; + uint8_t brightness = 255; +}; + +class LightSettingsService : public SettingsService { +}; +``` + +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. + +```cpp +// register an update handler +update_handler_id_t myUpdateHandler = lightSettingsService.addUpdateHandler( + [&](String originId) { + Serial.println("The light settings have been updated"); + } +); + +// remove the update handler +lightSettingsService.removeUpdateHandler(myUpdateHandler); +``` + +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. + +```cpp +lightSettingsService.read([&](LightSettings& settings) { + digitalWrite(LED_PIN, settings.on ? HIGH : LOW) +}); +``` + +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. + +```cpp +lightSettingsService.update([&](LightSettings& settings) { + settings.on = true; // turn on the lights! +}); +``` + +#### Serialization + +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: + +```cpp +class LightSettingsSerializer : public SettingsSerializer { + public: + void serialize(LightSettings& settings, JsonObject root) { + root["on"] = settings.on; + root["brightness"] = settings.brightness; + } +}; + +class LightSettingsDeserializer : public SettingsDeserializer { + public: + void deserialize(LightSettings& settings, JsonObject root) { + settings.on = root["on"] | false; + settings.brightness = root["brightness"] | 255; + } +}; +``` + +It is recommended you make create singletons for your serialzers and that they are stateless: + +```cpp +static LightSettingsSerializer SERIALIZER; +static LightSettingsDeserializer DESERIALIZER; +``` + +#### Endpoints + +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: + +```cpp +class LightSettingsService : public SettingsService { + public: + LightSettingsService(AsyncWebServer* server, SecurityManager* securityManager) : + _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, "/rest/lightSettings") { + } + + private: + SettingsEndpoint _settingsEndpoint; +}; +``` + +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. + +#### Persistence + +[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. + +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: + +```cpp +class LightSettingsService : public SettingsService { + public: + LightSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : + _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, "/rest/lightSettings"), + _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, "/config/lightSettings.json") { + } + + private: + SettingsEndpoint _settingsEndpoint; + SettingsPersistence _settingsPersistence; +}; +``` ### Security features diff --git a/data/config/demoSettings.json b/data/config/demoSettings.json index a003cd0e..2fc488c8 100644 --- a/data/config/demoSettings.json +++ b/data/config/demoSettings.json @@ -1,3 +1,3 @@ { - "blink_speed": 100 + "led_on": true } \ No newline at end of file diff --git a/data/config/mqttSettings.json b/data/config/mqttSettings.json new file mode 100644 index 00000000..0f83c4cc --- /dev/null +++ b/data/config/mqttSettings.json @@ -0,0 +1,11 @@ +{ + "enabled": false, + "host": "test.mosquitto.org", + "port": 1883, + "authenticated": false, + "username": "mqttuser", + "password": "mqttpassword", + "keepAlive": 16, + "cleanSession": true, + "maxTopicLength": 128 +} diff --git a/interface/.env.development b/interface/.env.development index 4ead142d..a641dd7c 100644 --- a/interface/.env.development +++ b/interface/.env.development @@ -1,3 +1,4 @@ # 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.21/rest/ +REACT_APP_ENDPOINT_ROOT=http://192.168.0.99/rest/ +REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.99 diff --git a/interface/package-lock.json b/interface/package-lock.json index 1e169af9..8d102516 100644 --- a/interface/package-lock.json +++ b/interface/package-lock.json @@ -1611,6 +1611,11 @@ "resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz", "integrity": "sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A==" }, + "@types/lodash": { + "version": "4.14.149", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", + "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==" + }, "@types/material-ui": { "version": "0.21.7", "resolved": "https://registry.npmjs.org/@types/material-ui/-/material-ui-0.21.7.tgz", @@ -12041,6 +12046,11 @@ "kind-of": "^3.2.0" } }, + "sockette": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/sockette/-/sockette-2.0.6.tgz", + "integrity": "sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==" + }, "sockjs": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", diff --git a/interface/package.json b/interface/package.json index 76395055..f6e42f63 100644 --- a/interface/package.json +++ b/interface/package.json @@ -6,6 +6,7 @@ "@material-ui/core": "^4.9.8", "@material-ui/icons": "^4.9.1", "@types/jwt-decode": "^2.2.1", + "@types/lodash": "^4.14.149", "@types/node": "^12.12.32", "@types/react": "^16.9.27", "@types/react-dom": "^16.9.5", @@ -14,6 +15,7 @@ "@types/react-router-dom": "^5.1.3", "compression-webpack-plugin": "^3.0.1", "jwt-decode": "^2.2.0", + "lodash": "^4.17.15", "mime-types": "^2.1.25", "moment": "^2.24.0", "notistack": "^0.9.7", @@ -24,6 +26,7 @@ "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-scripts": "3.4.1", + "sockette": "^2.0.6", "typescript": "^3.7.5", "zlib": "^1.0.5" }, diff --git a/interface/src/AppRouting.tsx b/interface/src/AppRouting.tsx index d60ede5d..f8d7874d 100644 --- a/interface/src/AppRouting.tsx +++ b/interface/src/AppRouting.tsx @@ -15,6 +15,7 @@ import Security from './security/Security'; import System from './system/System'; import { PROJECT_PATH } from './api'; +import MQTT from './mqtt/MQTT'; class AppRouting extends Component { @@ -31,6 +32,7 @@ class AppRouting extends Component { + diff --git a/interface/src/api/Endpoints.ts b/interface/src/api/Endpoints.ts index d1584509..40a61bac 100644 --- a/interface/src/api/Endpoints.ts +++ b/interface/src/api/Endpoints.ts @@ -9,6 +9,8 @@ export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks"; export const WIFI_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "wifiSettings"; export const WIFI_STATUS_ENDPOINT = ENDPOINT_ROOT + "wifiStatus"; export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings"; +export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "mqttSettings"; +export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + "mqttStatus"; export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus"; export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn"; export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization"; diff --git a/interface/src/api/Env.ts b/interface/src/api/Env.ts index 5809187f..6722b571 100644 --- a/interface/src/api/Env.ts +++ b/interface/src/api/Env.ts @@ -1,3 +1,20 @@ 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!; + +// TODO use same approach for rest endpoint? +export const WEB_SOCKET_ROOT = calculateWebSocketPrefix("/ws"); + +function calculateWebSocketPrefix(webSocketPath: string) { + const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT; + if (!webSocketRoot || webSocketRoot.length === 0) { + var loc = window.location, webSocketURI; + if (loc.protocol === "https:") { + webSocketURI = "wss:"; + } else { + webSocketURI = "ws:"; + } + return webSocketURI + "//" + loc.host + webSocketPath; + } + return webSocketRoot + webSocketPath; +} diff --git a/interface/src/components/MenuAppBar.tsx b/interface/src/components/MenuAppBar.tsx index c13c3c23..7baabdc1 100644 --- a/interface/src/components/MenuAppBar.tsx +++ b/interface/src/components/MenuAppBar.tsx @@ -13,6 +13,7 @@ import SettingsIcon from '@material-ui/icons/Settings'; import AccessTimeIcon from '@material-ui/icons/AccessTime'; import AccountCircleIcon from '@material-ui/icons/AccountCircle'; import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna'; +import DeviceHubIcon from '@material-ui/icons/DeviceHub'; import LockIcon from '@material-ui/icons/Lock'; import MenuIcon from '@material-ui/icons/Menu'; @@ -136,6 +137,12 @@ class MenuAppBar extends React.Component { + + + + + + diff --git a/interface/src/components/RestController.tsx b/interface/src/components/RestController.tsx index 5128611e..e4d7c08f 100644 --- a/interface/src/components/RestController.tsx +++ b/interface/src/components/RestController.tsx @@ -5,9 +5,8 @@ import { redirectingAuthorizedFetch } from '../authentication'; export interface RestControllerProps extends WithSnackbarProps { handleValueChange: (name: keyof D) => (event: React.ChangeEvent) => void; - handleSliderChange: (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => void; - setData: (data: D) => void; + setData: (data: D, callback?: () => void) => void; saveData: () => void; loadData: () => void; @@ -16,13 +15,7 @@ export interface RestControllerProps extends WithSnackbarProps { errorMessage?: string; } -interface RestControllerState { - data?: D; - loading: boolean; - errorMessage?: string; -} - -const extractValue = (event: React.ChangeEvent) => { +export const extractEventValue = (event: React.ChangeEvent) => { switch (event.target.type) { case "number": return event.target.valueAsNumber; @@ -33,6 +26,12 @@ const extractValue = (event: React.ChangeEvent) => { } } +interface RestControllerState { + data?: D; + loading: boolean; + errorMessage?: string; +} + export function restController>(endpointUrl: string, RestController: React.ComponentType

>) { return withSnackbar( class extends React.Component> & WithSnackbarProps, RestControllerState> { @@ -43,12 +42,12 @@ export function restController>(endpointUrl: errorMessage: undefined }; - setData = (data: D) => { + setData = (data: D, callback?: () => void) => { this.setState({ data, loading: false, errorMessage: undefined - }); + }, callback); } loadData = () => { @@ -95,19 +94,13 @@ export function restController>(endpointUrl: } handleValueChange = (name: keyof D) => (event: React.ChangeEvent) => { - const data = { ...this.state.data!, [name]: extractValue(event) }; + const data = { ...this.state.data!, [name]: extractEventValue(event) }; this.setState({ data }); } - handleSliderChange = (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => { - const data = { ...this.state.data!, [name]: value }; - this.setState({ data }); - }; - render() { return createStyles({ diff --git a/interface/src/components/SocketController.tsx b/interface/src/components/SocketController.tsx new file mode 100644 index 00000000..53f7ba8f --- /dev/null +++ b/interface/src/components/SocketController.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import Sockette from 'sockette'; +import throttle from 'lodash/throttle'; +import { withSnackbar, WithSnackbarProps } from 'notistack'; + +import { extractEventValue } from '.'; + +export interface SocketControllerProps extends WithSnackbarProps { + handleValueChange: (name: keyof D) => (event: React.ChangeEvent) => void; + + setData: (data: D, callback?: () => void) => void; + saveData: () => void; + saveDataAndClear(): () => void; + + connected: boolean; + data?: D; +} + +interface SocketControllerState { + ws: Sockette; + connected: boolean; + clientId?: string; + data?: D; +} + +enum SocketMessageType { + ID = "id", + PAYLOAD = "payload" +} + +interface SocketIdMessage { + type: typeof SocketMessageType.ID; + id: string; +} + +interface SocketPayloadMessage { + type: typeof SocketMessageType.PAYLOAD; + origin_id: string; + payload: D; +} + +export type SocketMessage = SocketIdMessage | SocketPayloadMessage; + +export function socketController>(wsUrl: string, wsThrottle: number, SocketController: React.ComponentType

>) { + return withSnackbar( + class extends React.Component> & WithSnackbarProps, SocketControllerState> { + constructor(props: Omit> & WithSnackbarProps) { + super(props); + this.state = { + ws: new Sockette(wsUrl, { + onmessage: this.onMessage, + onopen: this.onOpen, + onclose: this.onClose, + }), + connected: false + } + } + + componentWillUnmount() { + this.state.ws.close(); + } + + onMessage = (event: MessageEvent) => { + const rawData = event.data; + if (typeof rawData === 'string' || rawData instanceof String) { + this.handleMessage(JSON.parse(rawData as string) as SocketMessage); + } + } + + handleMessage = (socketMessage: SocketMessage) => { + switch (socketMessage.type) { + case SocketMessageType.ID: + this.setState({ clientId: socketMessage.id }); + break; + case SocketMessageType.PAYLOAD: + const { clientId, data } = this.state; + if (clientId && (!data || clientId !== socketMessage.origin_id)) { + this.setState( + { data: socketMessage.payload } + ); + } + break; + } + } + + onOpen = () => { + this.setState({ connected: true }); + } + + onClose = () => { + this.setState({ connected: false, clientId: undefined, data: undefined }); + } + + setData = (data: D, callback?: () => void) => { + this.setState({ data }, callback); + } + + saveData = throttle(() => { + const { ws, connected, data } = this.state; + if (connected) { + ws.json(data); + } + }, wsThrottle); + + saveDataAndClear = throttle(() => { + const { ws, connected, data } = this.state; + if (connected) { + this.setState({ + data: undefined + }, () => ws.json(data)); + } + }, wsThrottle); + + handleValueChange = (name: keyof D) => (event: React.ChangeEvent) => { + const data = { ...this.state.data!, [name]: extractEventValue(event) }; + this.setState({ data }); + } + + render() { + return ; + } + + }); +} diff --git a/interface/src/components/SocketFormLoader.tsx b/interface/src/components/SocketFormLoader.tsx new file mode 100644 index 00000000..9b7b9900 --- /dev/null +++ b/interface/src/components/SocketFormLoader.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; +import { LinearProgress, Typography } from '@material-ui/core'; + +import { SocketControllerProps } from '.'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + loadingSettings: { + margin: theme.spacing(0.5), + }, + loadingSettingsDetails: { + margin: theme.spacing(4), + textAlign: "center" + } + }) +); + +export type SocketFormProps = Omit, "connected"> & { data: D }; + +interface SocketFormLoaderProps extends SocketControllerProps { + render: (props: SocketFormProps) => JSX.Element; +} + +export default function SocketFormLoader(props: SocketFormLoaderProps) { + const { connected, render, data, ...rest } = props; + const classes = useStyles(); + if (!connected || !data) { + return ( +

+ + + Connecting to WebSocket... + +
+ ); + } + return render({ ...rest, data }); +} diff --git a/interface/src/components/index.ts b/interface/src/components/index.ts index 6b6a5ffb..24963b48 100644 --- a/interface/src/components/index.ts +++ b/interface/src/components/index.ts @@ -6,6 +6,10 @@ export { default as MenuAppBar } from './MenuAppBar'; export { default as PasswordValidator } from './PasswordValidator'; export { default as RestFormLoader } from './RestFormLoader'; export { default as SectionContent } from './SectionContent'; +export { default as SocketFormLoader } from './SocketFormLoader'; export * from './RestFormLoader'; export * from './RestController'; + +export * from './SocketFormLoader'; +export * from './SocketController'; diff --git a/interface/src/mqtt/MQTT.tsx b/interface/src/mqtt/MQTT.tsx new file mode 100644 index 00000000..ee9a7db5 --- /dev/null +++ b/interface/src/mqtt/MQTT.tsx @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import { Redirect, Switch, RouteComponentProps } from 'react-router-dom' + +import { Tabs, Tab } from '@material-ui/core'; + +import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication'; +import { MenuAppBar } from '../components'; +import MQTTStatusController from './MQTTStatusController'; +import MQTTSettingsController from './MQTTSettingsController'; + +type AccessPointProps = AuthenticatedContextProps & RouteComponentProps; + +class AccessPoint extends Component { + + handleTabChange = (event: React.ChangeEvent<{}>, path: string) => { + this.props.history.push(path); + }; + + render() { + const { authenticatedContext } = this.props; + return ( + + + + + + + + + + + + ) + } +} + +export default withAuthenticatedContext(AccessPoint); diff --git a/interface/src/mqtt/MQTTSettingsController.tsx b/interface/src/mqtt/MQTTSettingsController.tsx new file mode 100644 index 00000000..06bb0503 --- /dev/null +++ b/interface/src/mqtt/MQTTSettingsController.tsx @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; + +import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components'; +import { MQTT_SETTINGS_ENDPOINT } from '../api'; + +import MQTTSettingsForm from './MQTTSettingsForm'; +import { MQTTSettings } from './types'; + +type MQTTSettingsControllerProps = RestControllerProps; + +class MQTTSettingsController extends Component { + + componentDidMount() { + this.props.loadData(); + } + + render() { + return ( + + } + /> + + ) + } + +} + +export default restController(MQTT_SETTINGS_ENDPOINT, MQTTSettingsController); diff --git a/interface/src/mqtt/MQTTSettingsForm.tsx b/interface/src/mqtt/MQTTSettingsForm.tsx new file mode 100644 index 00000000..2ea256f1 --- /dev/null +++ b/interface/src/mqtt/MQTTSettingsForm.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator'; + +import { Checkbox, TextField } from '@material-ui/core'; +import SaveIcon from '@material-ui/icons/Save'; + +import { RestFormProps, FormActions, FormButton, BlockFormControlLabel, PasswordValidator } from '../components'; +import { isIP, isHostname, or } from '../validators'; + +import { MQTTSettings } from './types'; + +type MQTTSettingsFormProps = RestFormProps; + +class MQTTSettingsForm extends React.Component { + + componentDidMount() { + ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname)); + } + + render() { + const { data, handleValueChange, saveData, loadData } = this.props; + return ( + + + } + label="Enable MQTT?" + /> + + + + + + + + } + label="Clean Session?" + /> + + + } variant="contained" color="primary" type="submit"> + Save + + + Reset + + + + ); + } +} + +export default MQTTSettingsForm; diff --git a/interface/src/mqtt/MQTTStatus.ts b/interface/src/mqtt/MQTTStatus.ts new file mode 100644 index 00000000..d25fd56f --- /dev/null +++ b/interface/src/mqtt/MQTTStatus.ts @@ -0,0 +1,45 @@ +import { Theme } from "@material-ui/core"; +import { MQTTStatus, MQTTDisconnectReason } from "./types"; + +export const mqttStatusHighlight = ({ enabled, connected }: MQTTStatus, theme: Theme) => { + if (!enabled) { + return theme.palette.info.main; + } + if (connected) { + return theme.palette.success.main; + } + return theme.palette.error.main; +} + +export const mqttStatus = ({ enabled, connected }: MQTTStatus) => { + if (!enabled) { + return "Not enabled"; + } + if (connected) { + return "Connected"; + } + return "Disconnected"; +} + +export const disconnectReason = ({ disconnect_reason }: MQTTStatus) => { + switch (disconnect_reason) { + case MQTTDisconnectReason.TCP_DISCONNECTED: + return "TCP disconnected"; + case MQTTDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION: + return "Unacceptable protocol version"; + case MQTTDisconnectReason.MQTT_IDENTIFIER_REJECTED: + return "Client ID rejected"; + case MQTTDisconnectReason.MQTT_SERVER_UNAVAILABLE: + return "Server unavailable"; + case MQTTDisconnectReason.MQTT_MALFORMED_CREDENTIALS: + return "Malformed credentials"; + case MQTTDisconnectReason.MQTT_NOT_AUTHORIZED: + return "Not authorized"; + case MQTTDisconnectReason.ESP8266_NOT_ENOUGH_SPACE: + return "Device out of memory"; + case MQTTDisconnectReason.TLS_BAD_FINGERPRINT: + return "Server fingerprint invalid"; + default: + return "Unknown" + } +} diff --git a/interface/src/mqtt/MQTTStatusController.tsx b/interface/src/mqtt/MQTTStatusController.tsx new file mode 100644 index 00000000..28887985 --- /dev/null +++ b/interface/src/mqtt/MQTTStatusController.tsx @@ -0,0 +1,29 @@ +import React, { Component } from 'react'; + +import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components'; +import { MQTT_STATUS_ENDPOINT } from '../api'; + +import MQTTStatusForm from './MQTTStatusForm'; +import { MQTTStatus } from './types'; + +type MQTTStatusControllerProps = RestControllerProps; + +class MQTTStatusController extends Component { + + componentDidMount() { + this.props.loadData(); + } + + render() { + return ( + + } + /> + + ) + } +} + +export default restController(MQTT_STATUS_ENDPOINT, MQTTStatusController); diff --git a/interface/src/mqtt/MQTTStatusForm.tsx b/interface/src/mqtt/MQTTStatusForm.tsx new file mode 100644 index 00000000..ebf3e802 --- /dev/null +++ b/interface/src/mqtt/MQTTStatusForm.tsx @@ -0,0 +1,83 @@ +import React, { Component, Fragment } from 'react'; + +import { WithTheme, withTheme } from '@material-ui/core/styles'; +import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core'; + +import DeviceHubIcon from '@material-ui/icons/DeviceHub'; +import RefreshIcon from '@material-ui/icons/Refresh'; +import ReportIcon from '@material-ui/icons/Report'; + +import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components'; +import { mqttStatusHighlight, mqttStatus, disconnectReason } from './MQTTStatus'; +import { MQTTStatus } from './types'; + +type MQTTStatusFormProps = RestFormProps & WithTheme; + +class MQTTStatusForm extends Component { + + renderConnectionStatus() { + const { data } = this.props + if (data.connected) { + return ( + + + + # + + + + + + ); + } + return ( + + + + + + + + + + + + ); + } + + createListItems() { + const { data, theme } = this.props + return ( + + + + + + + + + + + {data.enabled && this.renderConnectionStatus()} + + ); + } + + render() { + return ( + + + {this.createListItems()} + + + } variant="contained" color="secondary" onClick={this.props.loadData}> + Refresh + + + + ); + } + +} + +export default withTheme(MQTTStatusForm); diff --git a/interface/src/mqtt/types.ts b/interface/src/mqtt/types.ts new file mode 100644 index 00000000..e92ea5f4 --- /dev/null +++ b/interface/src/mqtt/types.ts @@ -0,0 +1,29 @@ +export enum MQTTDisconnectReason { + TCP_DISCONNECTED = 0, + MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1, + MQTT_IDENTIFIER_REJECTED = 2, + MQTT_SERVER_UNAVAILABLE = 3, + MQTT_MALFORMED_CREDENTIALS = 4, + MQTT_NOT_AUTHORIZED = 5, + ESP8266_NOT_ENOUGH_SPACE = 6, + TLS_BAD_FINGERPRINT = 7 +} + +export interface MQTTStatus { + enabled: boolean; + connected: boolean; + client_id: string; + disconnect_reason: MQTTDisconnectReason; +} + +export interface MQTTSettings { + enabled: boolean; + host: string; + port: number; + username: string; + password: string; + client_id: string; + keep_alive: number; + clean_session: boolean; + max_topic_length: number; +} diff --git a/interface/src/ntp/NTPSettingsForm.tsx b/interface/src/ntp/NTPSettingsForm.tsx index daa37b15..5c2a630c 100644 --- a/interface/src/ntp/NTPSettingsForm.tsx +++ b/interface/src/ntp/NTPSettingsForm.tsx @@ -56,11 +56,10 @@ class NTPSettingsForm extends React.Component { validators={['required']} errorMessages={['Time zone is required']} name="tz_label" - labelId="tz_label" label="Time zone" fullWidth variant="outlined" - native + native="true" value={selectedTimeZone(data.tz_label, data.tz_format)} onChange={this.changeTimeZone} margin="normal" diff --git a/interface/src/ntp/TZ.tsx b/interface/src/ntp/TZ.tsx index 1f5ea9d1..b6557b3f 100644 --- a/interface/src/ntp/TZ.tsx +++ b/interface/src/ntp/TZ.tsx @@ -474,6 +474,6 @@ export function selectedTimeZone(label: string, format: string) { export function timeZoneSelectItems() { return Object.keys(TIME_ZONES).map(label => ( - {label} + {label} )); } diff --git a/interface/src/project/DemoController.tsx b/interface/src/project/DemoController.tsx deleted file mode 100644 index 2ff971ac..00000000 --- a/interface/src/project/DemoController.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { Component } from 'react'; -import { ValidatorForm } from 'react-material-ui-form-validator'; - -import { Typography, Slider, Box } from '@material-ui/core'; -import SaveIcon from '@material-ui/icons/Save'; - -import { ENDPOINT_ROOT } from '../api'; -import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent } from '../components'; - -export const DEMO_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "demoSettings"; - -interface DemoSettings { - blink_speed: number; -} - -type DemoControllerProps = RestControllerProps; - -class DemoController extends Component { - - componentDidMount() { - this.props.loadData(); - } - - render() { - return ( - - ( - - )} - /> - - ) - } - -} - -export default restController(DEMO_SETTINGS_ENDPOINT, DemoController); - -const valueToPercentage = (value: number) => `${Math.round(value / 255 * 100)}%`; - -type DemoControllerFormProps = RestFormProps; - -function DemoControllerForm(props: DemoControllerFormProps) { - const { data, saveData, loadData, handleSliderChange } = props; - return ( - - - Blink Speed - - - - - - } variant="contained" color="primary" type="submit"> - Save - - - Reset - - - - ); -} - - diff --git a/interface/src/project/DemoProject.tsx b/interface/src/project/DemoProject.tsx index 99bdd198..bdfa2d70 100644 --- a/interface/src/project/DemoProject.tsx +++ b/interface/src/project/DemoProject.tsx @@ -8,7 +8,9 @@ import { MenuAppBar } from '../components'; import { AuthenticatedRoute } from '../authentication'; import DemoInformation from './DemoInformation'; -import DemoController from './DemoController'; +import LightSettingsRestController from './LightSettingsRestController'; +import LightSettingsSocketController from './LightSettingsSocketController'; +import LightBrokerSettingsController from './LightBrokerSettingsController'; class DemoProject extends Component { @@ -20,12 +22,16 @@ class DemoProject extends Component { return ( - - + + + + - + + + diff --git a/interface/src/project/LightBrokerSettingsController.tsx b/interface/src/project/LightBrokerSettingsController.tsx new file mode 100644 index 00000000..c2b26ee2 --- /dev/null +++ b/interface/src/project/LightBrokerSettingsController.tsx @@ -0,0 +1,93 @@ +import React, { Component } from 'react'; +import { ValidatorForm, TextValidator } from 'react-material-ui-form-validator'; + +import { Typography, Box } from '@material-ui/core'; +import SaveIcon from '@material-ui/icons/Save'; + +import { ENDPOINT_ROOT } from '../api'; +import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent } from '../components'; + +import { LightBrokerSettings } from './types'; + +export const LIGHT_BROKER_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "brokerSettings"; + +type LightBrokerSettingsControllerProps = RestControllerProps; + +class LightBrokerSettingsController extends Component { + + componentDidMount() { + this.props.loadData(); + } + + render() { + return ( + + ( + + )} + /> + + ) + } + +} + +export default restController(LIGHT_BROKER_SETTINGS_ENDPOINT, LightBrokerSettingsController); + +type LightBrokerSettingsControllerFormProps = RestFormProps; + +function LightBrokerSettingsControllerForm(props: LightBrokerSettingsControllerFormProps) { + const { data, saveData, loadData, handleValueChange } = props; + return ( + + + + The LED is controllable via MQTT with the demo project designed to work with Home Assistant's auto discovery feature. + + + + + + + } variant="contained" color="primary" type="submit"> + Save + + + Reset + + + + ); +} diff --git a/interface/src/project/LightSettingsRestController.tsx b/interface/src/project/LightSettingsRestController.tsx new file mode 100644 index 00000000..a9ba3823 --- /dev/null +++ b/interface/src/project/LightSettingsRestController.tsx @@ -0,0 +1,70 @@ +import React, { Component } from 'react'; +import { ValidatorForm } from 'react-material-ui-form-validator'; + +import { Typography, Box, Checkbox } from '@material-ui/core'; +import SaveIcon from '@material-ui/icons/Save'; + +import { ENDPOINT_ROOT } from '../api'; +import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent, BlockFormControlLabel } from '../components'; + +import { LightSettings } from './types'; + +export const LIGHT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "lightSettings"; + +type LightSettingsRestControllerProps = RestControllerProps; + +class LightSettingsRestController extends Component { + + componentDidMount() { + this.props.loadData(); + } + + render() { + return ( + + ( + + )} + /> + + ) + } + +} + +export default restController(LIGHT_SETTINGS_ENDPOINT, LightSettingsRestController); + +type LightSettingsRestControllerFormProps = RestFormProps; + +function LightSettingsRestControllerForm(props: LightSettingsRestControllerFormProps) { + const { data, saveData, loadData, handleValueChange } = props; + return ( + + + + The form below controls the LED via the RESTful service exposed by the ESP device. + + + + } + label="LED State?" + /> + + } variant="contained" color="primary" type="submit"> + Save + + + Reset + + + + ); +} diff --git a/interface/src/project/LightSettingsSocketController.tsx b/interface/src/project/LightSettingsSocketController.tsx new file mode 100644 index 00000000..ce8175bd --- /dev/null +++ b/interface/src/project/LightSettingsSocketController.tsx @@ -0,0 +1,62 @@ +import React, { Component } from 'react'; +import { ValidatorForm } from 'react-material-ui-form-validator'; + +import { Typography, Box, Switch } from '@material-ui/core'; +import { WEB_SOCKET_ROOT } from '../api'; +import { SocketControllerProps, SocketFormLoader, SocketFormProps, socketController } from '../components'; +import { SectionContent, BlockFormControlLabel } from '../components'; + +import { LightSettings } from './types'; + +export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "/lightSettings"; + +type LightSettingsSocketControllerProps = SocketControllerProps; + +class LightSettingsSocketController extends Component { + + render() { + return ( + + ( + + )} + /> + + ) + } + +} + +export default socketController(LIGHT_SETTINGS_WEBSOCKET_URL, 100, LightSettingsSocketController); + +type LightSettingsSocketControllerFormProps = SocketFormProps; + +function LightSettingsSocketControllerForm(props: LightSettingsSocketControllerFormProps) { + const { data, saveData, setData } = props; + + const changeLedOn = (event: React.ChangeEvent) => { + setData({ led_on: event.target.checked }, saveData); + } + + return ( + + + + The switch below controls the LED via the WebSocket. It will automatically update whenever the LED state changes. + + + + } + label="LED State?" + /> + + ); +} diff --git a/interface/src/project/types.ts b/interface/src/project/types.ts new file mode 100644 index 00000000..ec1ac0ba --- /dev/null +++ b/interface/src/project/types.ts @@ -0,0 +1,9 @@ +export interface LightSettings { + led_on: boolean; +} + +export interface LightBrokerSettings { + unique_id : string; + name: string; + mqtt_path : string; +} diff --git a/interface/src/security/ManageUsersForm.tsx b/interface/src/security/ManageUsersForm.tsx index 70544536..78d1dece 100644 --- a/interface/src/security/ManageUsersForm.tsx +++ b/interface/src/security/ManageUsersForm.tsx @@ -154,11 +154,13 @@ class ManageUsersForm extends React.Component { this.noAdminConfigured() && - + ( - You must have at least one admin user configured. + + You must have at least one admin user configured. + - + ) } } variant="contained" color="primary" type="submit" disabled={this.noAdminConfigured()}> diff --git a/interface/src/security/SecuritySettingsForm.tsx b/interface/src/security/SecuritySettingsForm.tsx index 22d23b50..1d3ac375 100644 --- a/interface/src/security/SecuritySettingsForm.tsx +++ b/interface/src/security/SecuritySettingsForm.tsx @@ -33,11 +33,11 @@ class SecuritySettingsForm extends React.Component { onChange={handleValueChange('jwt_secret')} margin="normal" /> - - + + If you modify the JWT Secret, all users will be logged out. - - + + } variant="contained" color="primary" type="submit"> Save diff --git a/lib/framework/APSettingsService.cpp b/lib/framework/APSettingsService.cpp index 8fc0ebf7..c273f459 100644 --- a/lib/framework/APSettingsService.cpp +++ b/lib/framework/APSettingsService.cpp @@ -1,14 +1,16 @@ #include -APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - AdminSettingsService(server, fs, securityManager, AP_SETTINGS_SERVICE_PATH, AP_SETTINGS_FILE) { -} +static APSettingsSerializer SERIALIZER; +static APSettingsDeserializer DESERIALIZER; -APSettingsService::~APSettingsService() { +APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : + _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, AP_SETTINGS_SERVICE_PATH, securityManager), + _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, AP_SETTINGS_FILE) { + addUpdateHandler([&](String originId) { reconfigureAP(); }, false); } void APSettingsService::begin() { - SettingsService::begin(); + _settingsPersistence.readFromFS(); reconfigureAP(); } @@ -68,27 +70,3 @@ void APSettingsService::handleDNS() { _dnsServer->processNextRequest(); } } - -void APSettingsService::readFromJsonObject(JsonObject& root) { - _settings.provisionMode = root["provision_mode"] | AP_MODE_ALWAYS; - switch (_settings.provisionMode) { - case AP_MODE_ALWAYS: - case AP_MODE_DISCONNECTED: - case AP_MODE_NEVER: - break; - default: - _settings.provisionMode = AP_MODE_ALWAYS; - } - _settings.ssid = root["ssid"] | AP_DEFAULT_SSID; - _settings.password = root["password"] | AP_DEFAULT_PASSWORD; -} - -void APSettingsService::writeToJsonObject(JsonObject& root) { - root["provision_mode"] = _settings.provisionMode; - root["ssid"] = _settings.ssid; - root["password"] = _settings.password; -} - -void APSettingsService::onConfigUpdated() { - reconfigureAP(); -} diff --git a/lib/framework/APSettingsService.h b/lib/framework/APSettingsService.h index ea29e7fc..a2f7d391 100644 --- a/lib/framework/APSettingsService.h +++ b/lib/framework/APSettingsService.h @@ -1,7 +1,9 @@ #ifndef APSettingsConfig_h #define APSettingsConfig_h -#include +#include +#include + #include #include @@ -26,20 +28,43 @@ class APSettings { String password; }; -class APSettingsService : public AdminSettingsService { +class APSettingsSerializer : public SettingsSerializer { + public: + void serialize(APSettings& settings, JsonObject root) { + root["provision_mode"] = settings.provisionMode; + root["ssid"] = settings.ssid; + root["password"] = settings.password; + } +}; + +class APSettingsDeserializer : public SettingsDeserializer { + public: + void deserialize(APSettings& settings, JsonObject root) { + settings.provisionMode = root["provision_mode"] | AP_MODE_ALWAYS; + switch (settings.provisionMode) { + case AP_MODE_ALWAYS: + case AP_MODE_DISCONNECTED: + case AP_MODE_NEVER: + break; + default: + settings.provisionMode = AP_MODE_ALWAYS; + } + settings.ssid = root["ssid"] | AP_DEFAULT_SSID; + settings.password = root["password"] | AP_DEFAULT_PASSWORD; + } +}; + +class APSettingsService : public SettingsService { public: APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); - ~APSettingsService(); void begin(); void loop(); - protected: - void readFromJsonObject(JsonObject& root); - void writeToJsonObject(JsonObject& root); - void onConfigUpdated(); - private: + SettingsEndpoint _settingsEndpoint; + SettingsPersistence _settingsPersistence; + // for the mangement delay loop unsigned long _lastManaged; diff --git a/lib/framework/AdminSettingsService.h b/lib/framework/AdminSettingsService.h deleted file mode 100644 index 2d6c8abb..00000000 --- a/lib/framework/AdminSettingsService.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef AdminSettingsService_h -#define AdminSettingsService_h - -#include - -template -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 diff --git a/lib/framework/AsyncJsonCallbackResponse.h b/lib/framework/AsyncJsonCallbackResponse.h deleted file mode 100644 index d20ba5d2..00000000 --- a/lib/framework/AsyncJsonCallbackResponse.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef _AsyncJsonCallbackResponse_H_ -#define _AsyncJsonCallbackResponse_H_ - -#include -#include - -/* - * Listens for a response being destroyed and calls a callback during said distruction. - * used so we can take action after the response has been rendered to the client. - * - * Avoids having to fork ESPAsyncWebServer with a callback feature, but not nice! - */ - -typedef std::function AsyncJsonCallback; - -class AsyncJsonCallbackResponse : public AsyncJsonResponse { - private: - AsyncJsonCallback _callback; - - public: - AsyncJsonCallbackResponse(AsyncJsonCallback callback, - bool isArray = false, - size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : - AsyncJsonResponse(isArray, maxJsonBufferSize), - _callback{callback} { - } - - ~AsyncJsonCallbackResponse() { - _callback(); - } -}; - -#endif // end _AsyncJsonCallbackResponse_H_ diff --git a/lib/framework/AsyncJsonWebHandler.h b/lib/framework/AsyncJsonWebHandler.h deleted file mode 100644 index e353000a..00000000 --- a/lib/framework/AsyncJsonWebHandler.h +++ /dev/null @@ -1,131 +0,0 @@ -#ifndef Async_Json_Request_Web_Handler_H_ -#define Async_Json_Request_Web_Handler_H_ - -#include -#include - -#define ASYNC_JSON_REQUEST_DEFAULT_MAX_SIZE 1024 -#define ASYNC_JSON_REQUEST_MIMETYPE "application/json" - -/* - * Handy little utility for dealing with small JSON request body payloads. - * - * Need to be careful using this as we are somewhat limited by RAM. - * - * Really only of use where there is a determinate payload size. - */ - -typedef std::function JsonRequestCallback; - -class AsyncJsonWebHandler : public AsyncWebHandler { - private: - WebRequestMethodComposite _method; - JsonRequestCallback _onRequest; - size_t _maxContentLength; - - protected: - String _uri; - - public: - AsyncJsonWebHandler() : - _method(HTTP_POST | HTTP_PUT | HTTP_PATCH), - _onRequest(nullptr), - _maxContentLength(ASYNC_JSON_REQUEST_DEFAULT_MAX_SIZE), - _uri() { - } - - ~AsyncJsonWebHandler() { - } - - void setUri(const String& uri) { - _uri = uri; - } - void setMethod(WebRequestMethodComposite method) { - _method = method; - } - void setMaxContentLength(size_t maxContentLength) { - _maxContentLength = maxContentLength; - } - void onRequest(JsonRequestCallback fn) { - _onRequest = fn; - } - - virtual bool canHandle(AsyncWebServerRequest* request) override final { - if (!_onRequest) - return false; - - if (!(_method & request->method())) - return false; - - if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) - return false; - - if (!request->contentType().equalsIgnoreCase(ASYNC_JSON_REQUEST_MIMETYPE)) - return false; - - request->addInterestingHeader("ANY"); - return true; - } - - virtual void handleRequest(AsyncWebServerRequest* request) override final { - // no request configured - if (!_onRequest) { - Serial.print("No request callback was configured for endpoint: "); - Serial.println(_uri); - request->send(500); - return; - } - - // we have been handed too much data, return a 413 (payload too large) - if (request->contentLength() > _maxContentLength) { - request->send(413); - return; - } - - // parse JSON and if possible handle the request - if (request->_tempObject) { - DynamicJsonDocument jsonDocument(_maxContentLength); - DeserializationError error = deserializeJson(jsonDocument, (uint8_t*)request->_tempObject); - if (error == DeserializationError::Ok) { - _onRequest(request, jsonDocument); - } else { - request->send(400); - } - return; - } - - // fallthrough, we have a null pointer, return 500. - // this can be due to running out of memory or never receiving body data. - request->send(500); - } - - virtual void handleBody(AsyncWebServerRequest* request, - uint8_t* data, - size_t len, - size_t index, - size_t total) override final { - if (_onRequest) { - // don't allocate if data is too large - if (total > _maxContentLength) { - return; - } - - // try to allocate memory on first call - // NB: the memory allocated here is freed by ~AsyncWebServerRequest - if (index == 0 && !request->_tempObject) { - request->_tempObject = malloc(total); - } - - // copy the data into the buffer, if we have a buffer! - if (request->_tempObject) { - memcpy((uint8_t*)request->_tempObject + index, data, len); - } - } - } - - virtual bool isRequestHandlerTrivial() override final { - return _onRequest ? false : true; - } -}; - -#endif // end Async_Json_Request_Web_Handler_H_ diff --git a/lib/framework/AuthenticationService.cpp b/lib/framework/AuthenticationService.cpp index 4acf9439..cd497789 100644 --- a/lib/framework/AuthenticationService.cpp +++ b/lib/framework/AuthenticationService.cpp @@ -1,21 +1,17 @@ #include AuthenticationService::AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager) : - _securityManager(securityManager) { + _securityManager(securityManager), + _signInHandler(SIGN_IN_PATH, + std::bind(&AuthenticationService::signIn, this, std::placeholders::_1, std::placeholders::_2)) { server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, std::bind(&AuthenticationService::verifyAuthorization, this, std::placeholders::_1)); - _signInHandler.setUri(SIGN_IN_PATH); _signInHandler.setMethod(HTTP_POST); _signInHandler.setMaxContentLength(MAX_AUTHENTICATION_SIZE); - _signInHandler.onRequest( - std::bind(&AuthenticationService::signIn, this, std::placeholders::_1, std::placeholders::_2)); server->addHandler(&_signInHandler); } -AuthenticationService::~AuthenticationService() { -} - /** * Verifys that the request supplied a valid JWT. */ @@ -28,10 +24,10 @@ void AuthenticationService::verifyAuthorization(AsyncWebServerRequest* request) * Signs in a user if the username and password match. Provides a JWT to be used in the Authorization header in * subsequent requests. */ -void AuthenticationService::signIn(AsyncWebServerRequest* request, JsonDocument& jsonDocument) { - if (jsonDocument.is()) { - String username = jsonDocument["username"]; - String password = jsonDocument["password"]; +void AuthenticationService::signIn(AsyncWebServerRequest* request, JsonVariant& json) { + if (json.is()) { + String username = json["username"]; + String password = json["password"]; Authentication authentication = _securityManager->authenticate(username, password); if (authentication.authenticated) { User* user = authentication.user; diff --git a/lib/framework/AuthenticationService.h b/lib/framework/AuthenticationService.h index 6e68bdf1..9f970f94 100644 --- a/lib/framework/AuthenticationService.h +++ b/lib/framework/AuthenticationService.h @@ -2,7 +2,6 @@ #define AuthenticationService_H_ #include -#include #include #include @@ -14,14 +13,13 @@ class AuthenticationService { public: AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager); - ~AuthenticationService(); private: SecurityManager* _securityManager; - AsyncJsonWebHandler _signInHandler; + AsyncCallbackJsonWebHandler _signInHandler; // endpoint functions - void signIn(AsyncWebServerRequest* request, JsonDocument& jsonDocument); + void signIn(AsyncWebServerRequest* request, JsonVariant& json); void verifyAuthorization(AsyncWebServerRequest* request); }; diff --git a/lib/framework/ESP8266React.cpp b/lib/framework/ESP8266React.cpp index bb3f8b6a..a86db569 100644 --- a/lib/framework/ESP8266React.cpp +++ b/lib/framework/ESP8266React.cpp @@ -6,12 +6,14 @@ ESP8266React::ESP8266React(AsyncWebServer* server, FS* fs) : _apSettingsService(server, fs, &_securitySettingsService), _ntpSettingsService(server, fs, &_securitySettingsService), _otaSettingsService(server, fs, &_securitySettingsService), + _mqttSettingsService(server, fs, &_securitySettingsService), _restartService(server, &_securitySettingsService), _authenticationService(server, &_securitySettingsService), _wifiScanner(server, &_securitySettingsService), _wifiStatus(server, &_securitySettingsService), _ntpStatus(server, &_securitySettingsService), _apStatus(server, &_securitySettingsService), + _mqttStatus(server, &_mqttSettingsService, &_securitySettingsService), _systemStatus(server, &_securitySettingsService) { #ifdef PROGMEM_WWW // Serve static resources from PROGMEM @@ -71,11 +73,12 @@ void ESP8266React::begin() { _apSettingsService.begin(); _ntpSettingsService.begin(); _otaSettingsService.begin(); + _mqttSettingsService.begin(); } void ESP8266React::loop() { _wifiSettingsService.loop(); _apSettingsService.loop(); - _ntpSettingsService.loop(); _otaSettingsService.loop(); + _mqttSettingsService.loop(); } diff --git a/lib/framework/ESP8266React.h b/lib/framework/ESP8266React.h index 625adb1f..5b4b94e1 100644 --- a/lib/framework/ESP8266React.h +++ b/lib/framework/ESP8266React.h @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include #include @@ -41,7 +43,7 @@ class ESP8266React { return &_securitySettingsService; } - SettingsService* getSecuritySettingsService() { + SettingsService* getSecuritySettingsService() { return &_securitySettingsService; } @@ -61,12 +63,21 @@ class ESP8266React { return &_otaSettingsService; } + SettingsService* getMQTTSettingsService() { + return &_mqttSettingsService; + } + + AsyncMqttClient* getMQTTClient() { + return _mqttSettingsService.getMqttClient(); + } + private: SecuritySettingsService _securitySettingsService; WiFiSettingsService _wifiSettingsService; APSettingsService _apSettingsService; NTPSettingsService _ntpSettingsService; OTASettingsService _otaSettingsService; + MQTTSettingsService _mqttSettingsService; RestartService _restartService; AuthenticationService _authenticationService; @@ -75,6 +86,7 @@ class ESP8266React { WiFiStatus _wifiStatus; NTPStatus _ntpStatus; APStatus _apStatus; + MQTTStatus _mqttStatus; SystemStatus _systemStatus; }; diff --git a/lib/framework/JsonUtils.h b/lib/framework/JsonUtils.h new file mode 100644 index 00000000..e34599d5 --- /dev/null +++ b/lib/framework/JsonUtils.h @@ -0,0 +1,17 @@ +#include +#include +#include + +class JsonUtils { + public: + static void readIP(JsonObject& root, String key, IPAddress& _ip) { + if (!root[key].is() || !_ip.fromString(root[key].as())) { + _ip = INADDR_NONE; + } + } + static void writeIP(JsonObject& root, String key, IPAddress& _ip) { + if (_ip != INADDR_NONE) { + root[key] = _ip.toString(); + } + } +}; diff --git a/lib/framework/MQTTSettingsService.cpp b/lib/framework/MQTTSettingsService.cpp new file mode 100644 index 00000000..85a2e197 --- /dev/null +++ b/lib/framework/MQTTSettingsService.cpp @@ -0,0 +1,135 @@ +#include + +static MQTTSettingsSerializer SERIALIZER; +static MQTTSettingsDeserializer DESERIALIZER; + +MQTTSettingsService::MQTTSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : + _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, MQTT_SETTINGS_SERVICE_PATH, securityManager), + _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, MQTT_SETTINGS_FILE) { +#ifdef ESP32 + WiFi.onEvent( + std::bind(&MQTTSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), + WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); + WiFi.onEvent(std::bind(&MQTTSettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), + WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); +#elif defined(ESP8266) + _onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected( + std::bind(&MQTTSettingsService::onStationModeDisconnected, this, std::placeholders::_1)); + _onStationModeGotIPHandler = + WiFi.onStationModeGotIP(std::bind(&MQTTSettingsService::onStationModeGotIP, this, std::placeholders::_1)); +#endif + _mqttClient.onConnect(std::bind(&MQTTSettingsService::onMqttConnect, this, std::placeholders::_1)); + _mqttClient.onDisconnect(std::bind(&MQTTSettingsService::onMqttDisconnect, this, std::placeholders::_1)); + addUpdateHandler([&](String originId) { onConfigUpdated(); }, false); +} + +MQTTSettingsService::~MQTTSettingsService() { +} + +void MQTTSettingsService::begin() { + _settingsPersistence.readFromFS(); +} + +void MQTTSettingsService::loop() { + if (_reconfigureMqtt || (_disconnectedAt && (unsigned long)(millis() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) { + // reconfigure MQTT client + configureMQTT(); + + // clear the reconnection flags + _reconfigureMqtt = false; + _disconnectedAt = 0; + } +} + +bool MQTTSettingsService::isEnabled() { + return _settings.enabled; +} + +bool MQTTSettingsService::isConnected() { + return _connected; +} + +const char* MQTTSettingsService::getClientId() { + return _mqttClient.getClientId(); +} + +AsyncMqttClientDisconnectReason MQTTSettingsService::getDisconnectReason() { + return _disconnectReason; +} + +AsyncMqttClient* MQTTSettingsService::getMqttClient() { + return &_mqttClient; +} + +void MQTTSettingsService::onMqttConnect(bool sessionPresent) { + Serial.print("Connected to MQTT, "); + Serial.print(sessionPresent ? "with" : "without"); + Serial.println(" persistent session"); + _connected = true; +} + +void MQTTSettingsService::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { + Serial.print("Disconnected from MQTT reason: "); + Serial.println((uint8_t)reason); + _connected = false; + _disconnectReason = reason; + _disconnectedAt = millis(); +} + +void MQTTSettingsService::onConfigUpdated() { + _reconfigureMqtt = true; + _disconnectedAt = 0; +} + +#ifdef ESP32 +void MQTTSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { + if (_settings.enabled) { + Serial.println("WiFi connection dropped, starting MQTT client."); + onConfigUpdated(); + } +} + +void MQTTSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) { + if (_settings.enabled) { + Serial.println("WiFi connection dropped, stopping MQTT client."); + onConfigUpdated(); + } +} +#elif defined(ESP8266) +void MQTTSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP& event) { + if (_settings.enabled) { + Serial.println("WiFi connection dropped, starting MQTT client."); + onConfigUpdated(); + } +} + +void MQTTSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) { + if (_settings.enabled) { + Serial.println("WiFi connection dropped, stopping MQTT client."); + onConfigUpdated(); + } +} +#endif + +void MQTTSettingsService::configureMQTT() { + // disconnect if currently connected + _mqttClient.disconnect(); + + // only connect if WiFi is connected and MQTT is enabled + if (_settings.enabled && WiFi.isConnected()) { + Serial.println("Connecting to MQTT..."); + _mqttClient.setServer(_settings.host.c_str(), _settings.port); + if (_settings.username.length() > 0) { + const char* username = _settings.username.c_str(); + const char* password = _settings.password.length() > 0 ? _settings.password.c_str() : nullptr; + _mqttClient.setCredentials(username, password); + } else { + _mqttClient.setCredentials(nullptr, nullptr); + } + _mqttClient.setClientId(_settings.clientId.c_str()); + _mqttClient.setKeepAlive(_settings.keepAlive); + _mqttClient.setCleanSession(_settings.cleanSession); + _mqttClient.setMaxTopicLength(_settings.maxTopicLength); + _mqttClient.connect(); + } +} diff --git a/lib/framework/MQTTSettingsService.h b/lib/framework/MQTTSettingsService.h new file mode 100644 index 00000000..54c1c532 --- /dev/null +++ b/lib/framework/MQTTSettingsService.h @@ -0,0 +1,127 @@ +#ifndef MQTTSettingsService_h +#define MQTTSettingsService_h + +#include +#include +#include + +#define MQTT_RECONNECTION_DELAY 5000 + +#define MQTT_SETTINGS_FILE "/config/mqttSettings.json" +#define MQTT_SETTINGS_SERVICE_PATH "/rest/mqttSettings" + +#define MAX_MQTT_STATUS_SIZE 1024 +#define MQTT_STATUS_SERVICE_PATH "/rest/mqttStatus" + +#define MQTT_SETTINGS_SERVICE_DEFAULT_ENABLED false +#define MQTT_SETTINGS_SERVICE_DEFAULT_HOST "test.mosquitto.org" +#define MQTT_SETTINGS_SERVICE_DEFAULT_PORT 1883 +#define MQTT_SETTINGS_SERVICE_DEFAULT_USERNAME "" +#define MQTT_SETTINGS_SERVICE_DEFAULT_PASSWORD "" +#define MQTT_SETTINGS_SERVICE_DEFAULT_CLIENT_ID generateClientId() +#define MQTT_SETTINGS_SERVICE_DEFAULT_KEEP_ALIVE 16 +#define MQTT_SETTINGS_SERVICE_DEFAULT_CLEAN_SESSION true +#define MQTT_SETTINGS_SERVICE_DEFAULT_MAX_TOPIC_LENGTH 128 + +class MQTTSettings { + public: + // host and port - if enabled + bool enabled; + String host; + uint16_t port; + + // username and password + String username; + String password; + + // client id settings + String clientId; + + // connection settings + uint16_t keepAlive; + bool cleanSession; + uint16_t maxTopicLength; +}; + +static String generateClientId() { +#ifdef ESP32 + return "esp32-" + String((unsigned long)ESP.getEfuseMac(), HEX); +#elif defined(ESP8266) + return "esp8266-" + String(ESP.getChipId(), HEX); +#endif +} + +class MQTTSettingsSerializer : public SettingsSerializer { + public: + void serialize(MQTTSettings& settings, JsonObject root) { + root["enabled"] = settings.enabled; + root["host"] = settings.host; + root["port"] = settings.port; + root["username"] = settings.username; + root["password"] = settings.password; + root["client_id"] = settings.clientId; + root["keep_alive"] = settings.keepAlive; + root["clean_session"] = settings.cleanSession; + root["max_topic_length"] = settings.maxTopicLength; + } +}; + +class MQTTSettingsDeserializer : public SettingsDeserializer { + public: + void deserialize(MQTTSettings& settings, JsonObject root) { + settings.enabled = root["enabled"] | MQTT_SETTINGS_SERVICE_DEFAULT_ENABLED; + settings.host = root["host"] | MQTT_SETTINGS_SERVICE_DEFAULT_HOST; + settings.port = root["port"] | MQTT_SETTINGS_SERVICE_DEFAULT_PORT; + settings.username = root["username"] | MQTT_SETTINGS_SERVICE_DEFAULT_USERNAME; + settings.password = root["password"] | MQTT_SETTINGS_SERVICE_DEFAULT_PASSWORD; + settings.clientId = root["client_id"] | MQTT_SETTINGS_SERVICE_DEFAULT_CLIENT_ID; + settings.keepAlive = root["keep_alive"] | MQTT_SETTINGS_SERVICE_DEFAULT_KEEP_ALIVE; + settings.cleanSession = root["clean_session"] | MQTT_SETTINGS_SERVICE_DEFAULT_CLEAN_SESSION; + settings.maxTopicLength = root["max_topic_length"] | MQTT_SETTINGS_SERVICE_DEFAULT_MAX_TOPIC_LENGTH; + } +}; + +class MQTTSettingsService : public SettingsService { + public: + MQTTSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); + ~MQTTSettingsService(); + + void begin(); + void loop(); + bool isEnabled(); + bool isConnected(); + const char* getClientId(); + AsyncMqttClientDisconnectReason getDisconnectReason(); + AsyncMqttClient* getMqttClient(); + + protected: + void onConfigUpdated(); + + private: + SettingsEndpoint _settingsEndpoint; + SettingsPersistence _settingsPersistence; + + AsyncMqttClient _mqttClient; + bool _reconfigureMqtt; + unsigned long _disconnectedAt; + + // connection status + bool _connected; + AsyncMqttClientDisconnectReason _disconnectReason; + +#ifdef ESP32 + void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info); + void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info); +#elif defined(ESP8266) + WiFiEventHandler _onStationModeDisconnectedHandler; + WiFiEventHandler _onStationModeGotIPHandler; + void onStationModeGotIP(const WiFiEventStationModeGotIP& event); + void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event); +#endif + + void onMqttConnect(bool sessionPresent); + void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); + void configureMQTT(); +}; + +#endif // end MQTTSettingsService_h diff --git a/lib/framework/MQTTStatus.cpp b/lib/framework/MQTTStatus.cpp new file mode 100644 index 00000000..723f757c --- /dev/null +++ b/lib/framework/MQTTStatus.cpp @@ -0,0 +1,24 @@ +#include + +MQTTStatus::MQTTStatus(AsyncWebServer* server, + MQTTSettingsService* mqttSettingsService, + SecurityManager* securityManager) : + _mqttSettingsService(mqttSettingsService) { + server->on(MQTT_STATUS_SERVICE_PATH, + HTTP_GET, + securityManager->wrapRequest(std::bind(&MQTTStatus::mqttStatus, this, std::placeholders::_1), + AuthenticationPredicates::IS_AUTHENTICATED)); +} + +void MQTTStatus::mqttStatus(AsyncWebServerRequest* request) { + AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_MQTT_STATUS_SIZE); + JsonObject root = response->getRoot(); + + root["enabled"] = _mqttSettingsService->isEnabled(); + root["connected"] = _mqttSettingsService->isConnected(); + root["client_id"] = _mqttSettingsService->getClientId(); + root["disconnect_reason"] = (uint8_t) _mqttSettingsService->getDisconnectReason(); + + response->setLength(); + request->send(response); +} diff --git a/lib/framework/MQTTStatus.h b/lib/framework/MQTTStatus.h new file mode 100644 index 00000000..438e35cf --- /dev/null +++ b/lib/framework/MQTTStatus.h @@ -0,0 +1,31 @@ +#ifndef MQTTStatus_h +#define MQTTStatus_h + +#ifdef ESP32 +#include +#include +#elif defined(ESP8266) +#include +#include +#endif + +#include +#include +#include +#include +#include + +#define MAX_MQTT_STATUS_SIZE 1024 +#define MQTT_STATUS_SERVICE_PATH "/rest/mqttStatus" + +class MQTTStatus { + public: + MQTTStatus(AsyncWebServer* server, MQTTSettingsService* mqttSettingsService, SecurityManager* securityManager); + + private: + MQTTSettingsService* _mqttSettingsService; + + void mqttStatus(AsyncWebServerRequest* request); +}; + +#endif // end MQTTStatus_h diff --git a/lib/framework/NTPSettingsService.cpp b/lib/framework/NTPSettingsService.cpp index 5056198f..e5d37ee8 100644 --- a/lib/framework/NTPSettingsService.cpp +++ b/lib/framework/NTPSettingsService.cpp @@ -1,7 +1,11 @@ #include +static NTPSettingsSerializer SERIALIZER; +static NTPSettingsDeserializer DESERIALIZER; + NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - AdminSettingsService(server, fs, securityManager, NTP_SETTINGS_SERVICE_PATH, NTP_SETTINGS_FILE) { + _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager), + _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, NTP_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent( std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), @@ -14,64 +18,39 @@ NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityM _onStationModeGotIPHandler = WiFi.onStationModeGotIP(std::bind(&NTPSettingsService::onStationModeGotIP, this, std::placeholders::_1)); #endif + addUpdateHandler([&](String originId) { configureNTP(); }, false); } -NTPSettingsService::~NTPSettingsService() { -} - -void NTPSettingsService::loop() { - // detect when we need to re-configure NTP and do it in the main loop - if (_reconfigureNTP) { - _reconfigureNTP = false; - configureNTP(); - } -} - -void NTPSettingsService::readFromJsonObject(JsonObject& root) { - _settings.enabled = root["enabled"] | NTP_SETTINGS_SERVICE_DEFAULT_ENABLED; - _settings.server = root["server"] | NTP_SETTINGS_SERVICE_DEFAULT_SERVER; - _settings.tzLabel = root["tz_label"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_LABEL; - _settings.tzFormat = root["tz_format"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_FORMAT; -} - -void NTPSettingsService::writeToJsonObject(JsonObject& root) { - root["enabled"] = _settings.enabled; - root["server"] = _settings.server; - root["tz_label"] = _settings.tzLabel; - root["tz_format"] = _settings.tzFormat; -} - -void NTPSettingsService::onConfigUpdated() { - _reconfigureNTP = true; +void NTPSettingsService::begin() { + _settingsPersistence.readFromFS(); + configureNTP(); } #ifdef ESP32 void NTPSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { Serial.println("Got IP address, starting NTP Synchronization"); - _reconfigureNTP = true; + configureNTP(); } void NTPSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) { Serial.println("WiFi connection dropped, stopping NTP."); - _reconfigureNTP = false; - sntp_stop(); + configureNTP(); } #elif defined(ESP8266) void NTPSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP& event) { Serial.println("Got IP address, starting NTP Synchronization"); - _reconfigureNTP = true; + configureNTP(); } void NTPSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) { Serial.println("WiFi connection dropped, stopping NTP."); - _reconfigureNTP = false; - sntp_stop(); + configureNTP(); } #endif void NTPSettingsService::configureNTP() { - Serial.println("Configuring NTP..."); - if (_settings.enabled) { + if (WiFi.isConnected() && _settings.enabled) { + Serial.println("Starting NTP..."); #ifdef ESP32 configTzTime(_settings.tzFormat.c_str(), _settings.server.c_str()); #elif defined(ESP8266) diff --git a/lib/framework/NTPSettingsService.h b/lib/framework/NTPSettingsService.h index 28b394d2..ad355a27 100644 --- a/lib/framework/NTPSettingsService.h +++ b/lib/framework/NTPSettingsService.h @@ -1,7 +1,8 @@ #ifndef NTPSettingsService_h #define NTPSettingsService_h -#include +#include +#include #include #ifdef ESP32 @@ -16,10 +17,6 @@ #define NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_FORMAT "GMT0BST,M3.5.0/1,M10.5.0" #define NTP_SETTINGS_SERVICE_DEFAULT_SERVER "time.google.com" -// min poll delay of 60 secs, max 1 day -#define NTP_SETTINGS_MIN_INTERVAL 60 -#define NTP_SETTINGS_MAX_INTERVAL 86400 - #define NTP_SETTINGS_FILE "/config/ntpSettings.json" #define NTP_SETTINGS_SERVICE_PATH "/rest/ntpSettings" @@ -31,20 +28,35 @@ class NTPSettings { String server; }; -class NTPSettingsService : public AdminSettingsService { +class NTPSettingsSerializer : public SettingsSerializer { public: - NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); - ~NTPSettingsService(); + void serialize(NTPSettings& settings, JsonObject root) { + root["enabled"] = settings.enabled; + root["server"] = settings.server; + root["tz_label"] = settings.tzLabel; + root["tz_format"] = settings.tzFormat; + } +}; - void loop(); +class NTPSettingsDeserializer : public SettingsDeserializer { + public: + void deserialize(NTPSettings& settings, JsonObject root) { + settings.enabled = root["enabled"] | NTP_SETTINGS_SERVICE_DEFAULT_ENABLED; + settings.server = root["server"] | NTP_SETTINGS_SERVICE_DEFAULT_SERVER; + settings.tzLabel = root["tz_label"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_LABEL; + settings.tzFormat = root["tz_format"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_FORMAT; + } +}; - protected: - void readFromJsonObject(JsonObject& root); - void writeToJsonObject(JsonObject& root); - void onConfigUpdated(); +class NTPSettingsService : public SettingsService { + public: + NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); + + void begin(); private: - bool _reconfigureNTP = false; + SettingsEndpoint _settingsEndpoint; + SettingsPersistence _settingsPersistence; #ifdef ESP32 void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info); @@ -56,7 +68,6 @@ class NTPSettingsService : public AdminSettingsService { void onStationModeGotIP(const WiFiEventStationModeGotIP& event); void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event); #endif - void configureNTP(); }; diff --git a/lib/framework/OTASettingsService.cpp b/lib/framework/OTASettingsService.cpp index 3203f3c8..dc971881 100644 --- a/lib/framework/OTASettingsService.cpp +++ b/lib/framework/OTASettingsService.cpp @@ -1,7 +1,11 @@ #include +static OTASettingsSerializer SERIALIZER; +static OTASettingsDeserializer DESERIALIZER; + OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - AdminSettingsService(server, fs, securityManager, OTA_SETTINGS_SERVICE_PATH, OTA_SETTINGS_FILE) { + _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, OTA_SETTINGS_SERVICE_PATH, securityManager), + _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, OTA_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); @@ -9,33 +13,20 @@ OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityM _onStationModeGotIPHandler = WiFi.onStationModeGotIP(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1)); #endif + addUpdateHandler([&](String originId) { configureArduinoOTA(); }, false); } -OTASettingsService::~OTASettingsService() { +void OTASettingsService::begin() { + _settingsPersistence.readFromFS(); + configureArduinoOTA(); } void OTASettingsService::loop() { - if ( _settings.enabled && _arduinoOTA) { + if (_settings.enabled && _arduinoOTA) { _arduinoOTA->handle(); } } -void OTASettingsService::onConfigUpdated() { - configureArduinoOTA(); -} - -void OTASettingsService::readFromJsonObject(JsonObject& root) { - _settings.enabled = root["enabled"] | DEFAULT_OTA_ENABLED; - _settings.port = root["port"] | DEFAULT_OTA_PORT; - _settings.password = root["password"] | DEFAULT_OTA_PASSWORD; -} - -void OTASettingsService::writeToJsonObject(JsonObject& root) { - root["enabled"] = _settings.enabled; - root["port"] = _settings.port; - root["password"] = _settings.password; -} - void OTASettingsService::configureArduinoOTA() { if (_arduinoOTA) { #ifdef ESP32 @@ -45,7 +36,7 @@ void OTASettingsService::configureArduinoOTA() { _arduinoOTA = nullptr; } if (_settings.enabled) { - Serial.println("Starting OTA Update Service"); + Serial.println("Starting OTA Update Service..."); _arduinoOTA = new ArduinoOTAClass; _arduinoOTA->setPort(_settings.port); _arduinoOTA->setPassword(_settings.password.c_str()); @@ -70,6 +61,7 @@ void OTASettingsService::configureArduinoOTA() { _arduinoOTA->begin(); } } + #ifdef ESP32 void OTASettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { configureArduinoOTA(); diff --git a/lib/framework/OTASettingsService.h b/lib/framework/OTASettingsService.h index 17a873b6..ae76949d 100644 --- a/lib/framework/OTASettingsService.h +++ b/lib/framework/OTASettingsService.h @@ -1,7 +1,8 @@ #ifndef OTASettingsService_h #define OTASettingsService_h -#include +#include +#include #ifdef ESP32 #include @@ -27,19 +28,34 @@ class OTASettings { String password; }; -class OTASettingsService : public AdminSettingsService { +class OTASettingsSerializer : public SettingsSerializer { + public: + void serialize(OTASettings& settings, JsonObject root) { + root["enabled"] = settings.enabled; + root["port"] = settings.port; + root["password"] = settings.password; + } +}; + +class OTASettingsDeserializer : public SettingsDeserializer { + public: + void deserialize(OTASettings& settings, JsonObject root) { + settings.enabled = root["enabled"] | DEFAULT_OTA_ENABLED; + settings.port = root["port"] | DEFAULT_OTA_PORT; + settings.password = root["password"] | DEFAULT_OTA_PASSWORD; + } +}; + +class OTASettingsService : public SettingsService { public: OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); - ~OTASettingsService(); + void begin(); void loop(); - protected: - void onConfigUpdated(); - void readFromJsonObject(JsonObject& root); - void writeToJsonObject(JsonObject& root); - private: + SettingsEndpoint _settingsEndpoint; + SettingsPersistence _settingsPersistence; ArduinoOTAClass* _arduinoOTA; void configureArduinoOTA(); diff --git a/lib/framework/SecurityManager.h b/lib/framework/SecurityManager.h index b275b4be..c9e2681e 100644 --- a/lib/framework/SecurityManager.h +++ b/lib/framework/SecurityManager.h @@ -3,6 +3,7 @@ #include #include +#include #include #define DEFAULT_JWT_SECRET "esp8266-react" @@ -59,7 +60,7 @@ class SecurityManager { /* * Authenticate, returning the user if found */ - virtual Authentication authenticate(String username, String password) = 0; + virtual Authentication authenticate(String& username, String& password) = 0; /* * Check the request header for the Authorization token @@ -76,6 +77,12 @@ class SecurityManager { */ virtual ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate) = 0; + + /** + * Wrap the provided json request callback to provide validation against an AuthenticationPredicate. + */ + virtual ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback, + AuthenticationPredicate predicate) = 0; }; #endif // end SecurityManager_h \ No newline at end of file diff --git a/lib/framework/SecuritySettingsService.cpp b/lib/framework/SecuritySettingsService.cpp index 51477d23..099cac7a 100644 --- a/lib/framework/SecuritySettingsService.cpp +++ b/lib/framework/SecuritySettingsService.cpp @@ -1,45 +1,21 @@ #include -SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : - AdminSettingsService(server, fs, this, SECURITY_SETTINGS_PATH, SECURITY_SETTINGS_FILE), - SecurityManager() { -} -SecuritySettingsService::~SecuritySettingsService() { -} +static SecuritySettingsSerializer SERIALIZER; +static SecuritySettingsDeserializer DESERIALIZER; -void SecuritySettingsService::readFromJsonObject(JsonObject& root) { - // secret - _jwtHandler.setSecret(root["jwt_secret"] | DEFAULT_JWT_SECRET); - - // users - _settings.users.clear(); - if (root["users"].is()) { - for (JsonVariant user : root["users"].as()) { - _settings.users.push_back(User(user["username"], user["password"], user["admin"])); - } - } else { - _settings.users.push_back(User(DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_USERNAME, true)); - _settings.users.push_back(User(DEFAULT_GUEST_USERNAME, DEFAULT_GUEST_USERNAME, false)); - } +SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : + _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, SECURITY_SETTINGS_PATH, this), + _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, SECURITY_SETTINGS_FILE) { + addUpdateHandler([&](String originId) { configureJWTHandler(); }, false); } -void SecuritySettingsService::writeToJsonObject(JsonObject& root) { - // secret - root["jwt_secret"] = _jwtHandler.getSecret(); - - // users - JsonArray users = root.createNestedArray("users"); - for (User _user : _settings.users) { - JsonObject user = users.createNestedObject(); - user["username"] = _user.username; - user["password"] = _user.password; - user["admin"] = _user.admin; - } +void SecuritySettingsService::begin() { + _settingsPersistence.readFromFS(); + configureJWTHandler(); } - -Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest *request) { - AsyncWebHeader *authorizationHeader = request->getHeader(AUTHORIZATION_HEADER); +Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest* request) { + AsyncWebHeader* authorizationHeader = request->getHeader(AUTHORIZATION_HEADER); if (authorizationHeader) { String value = authorizationHeader->value(); if (value.startsWith(AUTHORIZATION_HEADER_PREFIX)) { @@ -50,7 +26,11 @@ Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerReques return Authentication(); } -Authentication SecuritySettingsService::authenticateJWT(String jwt) { +void SecuritySettingsService::configureJWTHandler() { + _jwtHandler.setSecret(_settings.jwtSecret); +} + +Authentication SecuritySettingsService::authenticateJWT(String& jwt) { DynamicJsonDocument payloadDocument(MAX_JWT_SIZE); _jwtHandler.parseJWT(jwt, payloadDocument); if (payloadDocument.is()) { @@ -65,7 +45,7 @@ Authentication SecuritySettingsService::authenticateJWT(String jwt) { return Authentication(); } -Authentication SecuritySettingsService::authenticate(String username, String password) { +Authentication SecuritySettingsService::authenticate(String& username, String& password) { for (User _user : _settings.users) { if (_user.username == username && _user.password == password) { return Authentication(_user); @@ -74,28 +54,28 @@ Authentication SecuritySettingsService::authenticate(String username, String pas return Authentication(); } -inline void populateJWTPayload(JsonObject &payload, User *user) { +inline void populateJWTPayload(JsonObject& payload, User* user) { payload["username"] = user->username; payload["admin"] = user->admin; } -boolean SecuritySettingsService::validatePayload(JsonObject &parsedPayload, User *user) { - DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE); - JsonObject payload = _jsonDocument.to(); +boolean SecuritySettingsService::validatePayload(JsonObject& parsedPayload, User* user) { + DynamicJsonDocument jsonDocument(MAX_JWT_SIZE); + JsonObject payload = jsonDocument.to(); populateJWTPayload(payload, user); return payload == parsedPayload; } -String SecuritySettingsService::generateJWT(User *user) { - DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE); - JsonObject payload = _jsonDocument.to(); +String SecuritySettingsService::generateJWT(User* user) { + DynamicJsonDocument jsonDocument(MAX_JWT_SIZE); + JsonObject payload = jsonDocument.to(); populateJWTPayload(payload, user); return _jwtHandler.buildJWT(payload); } ArRequestHandlerFunction SecuritySettingsService::wrapRequest(ArRequestHandlerFunction onRequest, - AuthenticationPredicate predicate) { - return [this, onRequest, predicate](AsyncWebServerRequest *request) { + AuthenticationPredicate predicate) { + return [this, onRequest, predicate](AsyncWebServerRequest* request) { Authentication authentication = authenticateRequest(request); if (!predicate(authentication)) { request->send(401); @@ -104,3 +84,15 @@ ArRequestHandlerFunction SecuritySettingsService::wrapRequest(ArRequestHandlerFu onRequest(request); }; } + +ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequestHandlerFunction callback, + AuthenticationPredicate predicate) { + return [this, callback, predicate](AsyncWebServerRequest* request, JsonVariant& json) { + Authentication authentication = authenticateRequest(request); + if (!predicate(authentication)) { + request->send(401); + return; + } + callback(request, json); + }; +} diff --git a/lib/framework/SecuritySettingsService.h b/lib/framework/SecuritySettingsService.h index 37225197..8998575b 100644 --- a/lib/framework/SecuritySettingsService.h +++ b/lib/framework/SecuritySettingsService.h @@ -1,8 +1,9 @@ #ifndef SecuritySettingsService_h #define SecuritySettingsService_h -#include #include +#include +#include #define DEFAULT_ADMIN_USERNAME "admin" #define DEFAULT_GUEST_USERNAME "guest" @@ -16,28 +17,66 @@ class SecuritySettings { std::list users; }; -class SecuritySettingsService : public AdminSettingsService, public SecurityManager { +class SecuritySettingsSerializer : public SettingsSerializer { + public: + void serialize(SecuritySettings& settings, JsonObject root) { + // secret + root["jwt_secret"] = settings.jwtSecret; + + // users + JsonArray users = root.createNestedArray("users"); + for (User user : settings.users) { + JsonObject userRoot = users.createNestedObject(); + userRoot["username"] = user.username; + userRoot["password"] = user.password; + userRoot["admin"] = user.admin; + } + } +}; + +class SecuritySettingsDeserializer : public SettingsDeserializer { + public: + void deserialize(SecuritySettings& settings, JsonObject root) { + // secret + settings.jwtSecret = root["jwt_secret"] | DEFAULT_JWT_SECRET; + + // users + settings.users.clear(); + if (root["users"].is()) { + for (JsonVariant user : root["users"].as()) { + settings.users.push_back(User(user["username"], user["password"], user["admin"])); + } + } else { + settings.users.push_back(User(DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_USERNAME, true)); + settings.users.push_back(User(DEFAULT_GUEST_USERNAME, DEFAULT_GUEST_USERNAME, false)); + } + } +}; + +class SecuritySettingsService : public SettingsService, public SecurityManager { public: SecuritySettingsService(AsyncWebServer* server, FS* fs); - ~SecuritySettingsService(); + + void begin(); // Functions to implement SecurityManager - Authentication authenticate(String username, String password); + Authentication authenticate(String& username, String& password); Authentication authenticateRequest(AsyncWebServerRequest* request); String generateJWT(User* user); ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); - - protected: - void readFromJsonObject(JsonObject& root); - void writeToJsonObject(JsonObject& root); + ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback, AuthenticationPredicate predicate); private: + SettingsEndpoint _settingsEndpoint; + SettingsPersistence _settingsPersistence; ArduinoJsonJWT _jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET); + void configureJWTHandler(); + /* * Lookup the user by JWT */ - Authentication authenticateJWT(String jwt); + Authentication authenticateJWT(String& jwt); /* * Verify the payload is correct diff --git a/lib/framework/SettingsBroker.h b/lib/framework/SettingsBroker.h new file mode 100644 index 00000000..9a0c383f --- /dev/null +++ b/lib/framework/SettingsBroker.h @@ -0,0 +1,106 @@ +#ifndef SettingsBroker_h +#define SettingsBroker_h + +#include +#include +#include +#include + +#define MAX_SETTINGS_SIZE 1024 +#define SETTINGS_BROKER_ORIGIN_ID "broker" + +/** + * SettingsBroker is designed to operate with Home Assistant and takes care of observing state change requests over MQTT + * and acting on them. + * + * The broker listens to changes on a "set" topic and publish it's state on a "state" topic. It also has + * an optional config topic which can be used for home assistant's auto discovery feature if required. + * + * Settings are automatically published to the state topic when a connection to the broker is established or when + * settings are updated. + * + * When a message is recieved on the set topic the settings are deserialized from the payload and applied. The state + * topic is then updated as normal. + */ +template +class SettingsBroker { + public: + SettingsBroker(SettingsSerializer* settingsSerializer, + SettingsDeserializer* settingsDeserializer, + SettingsService* settingsService, + AsyncMqttClient* mqttClient) : + _settingsSerializer(settingsSerializer), + _settingsDeserializer(settingsDeserializer), + _settingsService(settingsService), + _mqttClient(mqttClient) { + _settingsService->addUpdateHandler([&](String originId) { publish(); }, false); + _mqttClient->onConnect(std::bind(&SettingsBroker::configureMQTT, this)); + _mqttClient->onMessage(std::bind(&SettingsBroker::onMqttMessage, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5, + std::placeholders::_6)); + } + + void configureBroker(String setTopic, String stateTopic) { + _setTopic = setTopic; + _stateTopic = stateTopic; + configureMQTT(); + } + + protected: + virtual void configureMQTT() { + if (_setTopic.length() > 0) { + _mqttClient->subscribe(_setTopic.c_str(), 2); + } + publish(); + } + + private: + SettingsSerializer* _settingsSerializer; + SettingsDeserializer* _settingsDeserializer; + SettingsService* _settingsService; + AsyncMqttClient* _mqttClient; + String _setTopic; + String _stateTopic; + + void publish() { + if (_stateTopic.length() > 0 && _mqttClient->connected()) { + // serialize to json doc + DynamicJsonDocument json(MAX_SETTINGS_SIZE); + _settingsService->read([&](T& settings) { _settingsSerializer->serialize(settings, json.to()); }); + + // serialize to string + String payload; + serializeJson(json, payload); + + // publish the payload + _mqttClient->publish(_stateTopic.c_str(), 0, false, payload.c_str()); + } + } + + void onMqttMessage(char* topic, + char* payload, + AsyncMqttClientMessageProperties properties, + size_t len, + size_t index, + size_t total) { + // we only care about the topic we are watching in this class + if (strcmp(_setTopic.c_str(), topic)) { + return; + } + + // deserialize from string + DynamicJsonDocument json(MAX_SETTINGS_SIZE); + DeserializationError error = deserializeJson(json, payload, len); + if (!error && json.is()) { + _settingsService->update( + [&](T& settings) { _settingsDeserializer->deserialize(settings, json.as()); }, SETTINGS_BROKER_ORIGIN_ID); + } + } +}; + +#endif // end SettingsBroker_h diff --git a/lib/framework/SettingsDeserializer.h b/lib/framework/SettingsDeserializer.h new file mode 100644 index 00000000..8ca40916 --- /dev/null +++ b/lib/framework/SettingsDeserializer.h @@ -0,0 +1,12 @@ +#ifndef SettingsDeserializer_h +#define SettingsDeserializer_h + +#include + +template +class SettingsDeserializer { + public: + virtual void deserialize(T& settings, JsonObject root) = 0; +}; + +#endif // end SettingsDeserializer diff --git a/lib/framework/SettingsEndpoint.h b/lib/framework/SettingsEndpoint.h new file mode 100644 index 00000000..0ec25272 --- /dev/null +++ b/lib/framework/SettingsEndpoint.h @@ -0,0 +1,96 @@ +#ifndef SettingsEndpoint_h +#define SettingsEndpoint_h + +#include + +#include +#include + +#include +#include +#include +#include + +#define MAX_SETTINGS_SIZE 1024 +#define SETTINGS_ENDPOINT_ORIGIN_ID "endpoint" + +template +class SettingsEndpoint { + public: + SettingsEndpoint(SettingsSerializer* settingsSerializer, + SettingsDeserializer* settingsDeserializer, + SettingsService* settingsManager, + AsyncWebServer* server, + const String& servicePath, + SecurityManager* securityManager, + AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : + _settingsSerializer(settingsSerializer), + _settingsDeserializer(settingsDeserializer), + _settingsService(settingsManager), + _updateHandler( + servicePath, + securityManager->wrapCallback( + std::bind(&SettingsEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2), + authenticationPredicate)) { + server->on(servicePath.c_str(), + HTTP_GET, + securityManager->wrapRequest(std::bind(&SettingsEndpoint::fetchSettings, this, std::placeholders::_1), + authenticationPredicate)); + _updateHandler.setMethod(HTTP_POST); + _updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE); + server->addHandler(&_updateHandler); + } + + SettingsEndpoint(SettingsSerializer* settingsSerializer, + SettingsDeserializer* settingsDeserializer, + SettingsService* settingsManager, + AsyncWebServer* server, + const String& servicePath) : + _settingsSerializer(settingsSerializer), + _settingsDeserializer(settingsDeserializer), + _settingsService(settingsManager), + _updateHandler(servicePath, + std::bind(&SettingsEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2)) { + server->on(servicePath.c_str(), HTTP_GET, std::bind(&SettingsEndpoint::fetchSettings, this, std::placeholders::_1)); + _updateHandler.setMethod(HTTP_POST); + _updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE); + server->addHandler(&_updateHandler); + } + + protected: + SettingsSerializer* _settingsSerializer; + SettingsDeserializer* _settingsDeserializer; + SettingsService* _settingsService; + AsyncCallbackJsonWebHandler _updateHandler; + + void fetchSettings(AsyncWebServerRequest* request) { + AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SETTINGS_SIZE); + _settingsService->read( + [&](T& settings) { _settingsSerializer->serialize(settings, response->getRoot().to()); }); + response->setLength(); + request->send(response); + } + + void updateSettings(AsyncWebServerRequest* request, JsonVariant& json) { + if (json.is()) { + AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SETTINGS_SIZE); + + // use callback to update the settings once the response is complete + request->onDisconnect([this]() { _settingsService->callUpdateHandlers(SETTINGS_ENDPOINT_ORIGIN_ID); }); + + // update the settings, deferring the call to the update handlers to when the response is complete + _settingsService->updateWithoutPropogation([&](T& settings) { + _settingsDeserializer->deserialize(settings, json.as()); + _settingsSerializer->serialize(settings, response->getRoot().as()); + }); + + // write the response to the client + response->setLength(); + request->send(response); + } else { + request->send(400); + } + } +}; + +#endif // end SettingsEndpoint diff --git a/lib/framework/SettingsPersistence.h b/lib/framework/SettingsPersistence.h index e2a1fe39..d1f3f32f 100644 --- a/lib/framework/SettingsPersistence.h +++ b/lib/framework/SettingsPersistence.h @@ -1,95 +1,108 @@ #ifndef SettingsPersistence_h #define SettingsPersistence_h -#include -#include -#include -#include +#include +#include +#include #include -/** - * At the moment, not expecting settings service to have to deal with large JSON - * files this could be made configurable fairly simply, it's exposed on - * AsyncJsonWebHandler with a setter. - */ #define MAX_SETTINGS_SIZE 1024 -/* - * Mixin for classes which need to save settings to/from a file on the the file system as JSON. +/** + * SettingsPersistance takes care of loading and saving settings when they change. + * + * SettingsPersistence automatically registers writeToFS as an update handler with the settings manager + * when constructed, saving any updates to the file system. */ +template class SettingsPersistence { - protected: - // will store and retrieve config from the file system - FS* _fs; + public: + SettingsPersistence(SettingsSerializer* settingsSerializer, + SettingsDeserializer* settingsDeserializer, + SettingsService* settingsManager, + FS* fs, + char const* filePath) : + _settingsSerializer(settingsSerializer), + _settingsDeserializer(settingsDeserializer), + _settingsService(settingsManager), + _fs(fs), + _filePath(filePath) { + enableAutomatic(); + } - // the file path our settings will be saved to - char const* _filePath; + void readFromFS() { + File settingsFile = _fs->open(_filePath, "r"); + + if (settingsFile) { + if (settingsFile.size() <= MAX_SETTINGS_SIZE) { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE); + DeserializationError error = deserializeJson(jsonDocument, settingsFile); + if (error == DeserializationError::Ok && jsonDocument.is()) { + readSettings(jsonDocument.as()); + settingsFile.close(); + return; + } + } + settingsFile.close(); + } + + // If we reach here we have not been successful in loading the config, + // hard-coded emergency defaults are now applied. + readDefaults(); + } bool writeToFS() { // create and populate a new json object DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE); - JsonObject root = jsonDocument.to(); - writeToJsonObject(root); + _settingsService->read( + [&](T& settings) { _settingsSerializer->serialize(settings, jsonDocument.to()); }); // serialize it to filesystem - File configFile = _fs->open(_filePath, "w"); + File settingsFile = _fs->open(_filePath, "w"); // failed to open file, return false - if (!configFile) { + if (!settingsFile) { return false; } - serializeJson(jsonDocument, configFile); - configFile.close(); - + // serialize the data to the file + serializeJson(jsonDocument, settingsFile); + settingsFile.close(); return true; } - void readFromFS() { - File configFile = _fs->open(_filePath, "r"); - - // use defaults if no config found - if (configFile) { - // Protect against bad data uploaded to file system - // We never expect the config file to get very large, so cap it. - size_t size = configFile.size(); - if (size <= MAX_SETTINGS_SIZE) { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE); - DeserializationError error = deserializeJson(jsonDocument, configFile); - if (error == DeserializationError::Ok && jsonDocument.is()) { - JsonObject root = jsonDocument.as(); - readFromJsonObject(root); - configFile.close(); - return; - } - } - configFile.close(); + void disableAutomatic() { + if (_updateHandlerId) { + _settingsService->removeUpdateHandler(_updateHandlerId); + _updateHandlerId = 0; } - - // If we reach here we have not been successful in loading the config, - // hard-coded emergency defaults are now applied. - applyDefaultConfig(); } - // serialization routene, from local config to JsonObject - virtual void readFromJsonObject(JsonObject& root) { + void enableAutomatic() { + if (!_updateHandlerId) { + _updateHandlerId = _settingsService->addUpdateHandler([&](String originId) { writeToFS(); }); + } } - virtual void writeToJsonObject(JsonObject& root) { + + private: + SettingsSerializer* _settingsSerializer; + SettingsDeserializer* _settingsDeserializer; + SettingsService* _settingsService; + FS* _fs; + char const* _filePath; + update_handler_id_t _updateHandlerId = 0; + + // read the settings, but do not call propogate + void readSettings(JsonObject root) { + _settingsService->read([&](T& settings) { _settingsDeserializer->deserialize(settings, root); }); } + protected: // We assume the readFromJsonObject supplies sensible defaults if an empty object // is supplied, this virtual function allows that to be changed. - virtual void applyDefaultConfig() { + virtual void readDefaults() { DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE); - JsonObject root = jsonDocument.to(); - readFromJsonObject(root); - } - - public: - SettingsPersistence(FS* fs, char const* filePath) : _fs(fs), _filePath(filePath) { - } - - virtual ~SettingsPersistence() { + readSettings(jsonDocument.to()); } }; diff --git a/lib/framework/SettingsSerializer.h b/lib/framework/SettingsSerializer.h new file mode 100644 index 00000000..25dab3ea --- /dev/null +++ b/lib/framework/SettingsSerializer.h @@ -0,0 +1,12 @@ +#ifndef SettingsSerializer_h +#define SettingsSerializer_h + +#include + +template +class SettingsSerializer { + public: + virtual void serialize(T& settings, JsonObject root) = 0; +}; + +#endif // end SettingsSerializer diff --git a/lib/framework/SettingsService.h b/lib/framework/SettingsService.h index 7fab5851..f8125336 100644 --- a/lib/framework/SettingsService.h +++ b/lib/framework/SettingsService.h @@ -1,58 +1,34 @@ #ifndef SettingsService_h #define SettingsService_h -#include +#include +#include +#include #ifdef ESP32 -#include -#include -#elif defined(ESP8266) -#include -#include +#include +#include #endif -#include -#include -#include -#include -#include -#include -#include - typedef size_t update_handler_id_t; -typedef std::function SettingsUpdateCallback; -static update_handler_id_t currentUpdateHandlerId; +typedef std::function SettingsUpdateCallback; +static update_handler_id_t currentUpdatedHandlerId; typedef struct SettingsUpdateHandlerInfo { update_handler_id_t _id; SettingsUpdateCallback _cb; bool _allowRemove; SettingsUpdateHandlerInfo(SettingsUpdateCallback cb, bool allowRemove) : - _id(++currentUpdateHandlerId), - _cb(cb), - _allowRemove(allowRemove){}; + _id(++currentUpdatedHandlerId), _cb(cb), _allowRemove(allowRemove){}; } SettingsUpdateHandlerInfo_t; -/* - * Abstraction of a service which stores it's settings as JSON in a file system. - */ template -class SettingsService : public SettingsPersistence { +class SettingsService { public: - 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)); - _updateHandler.setUri(servicePath); - _updateHandler.setMethod(HTTP_POST); - _updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE); - _updateHandler.onRequest( - std::bind(&SettingsService::updateConfig, this, std::placeholders::_1, std::placeholders::_2)); - server->addHandler(&_updateHandler); - } - - virtual ~SettingsService() { +#ifdef ESP32 + SettingsService() : _updateMutex(xSemaphoreCreateRecursiveMutex()) { } +#endif update_handler_id_t addUpdateHandler(SettingsUpdateCallback cb, bool allowRemove = true) { if (!cb) { @@ -73,94 +49,44 @@ class SettingsService : public SettingsPersistence { } } - T fetch() { - return _settings; + void updateWithoutPropogation(std::function callback) { + read(callback); } - void update(T& settings) { - _settings = settings; - writeToFS(); - callUpdateHandlers(); - } - - void fetchAsString(String& config) { - DynamicJsonDocument jsonDocument(MAX_SETTINGS_SIZE); - fetchAsDocument(jsonDocument); - serializeJson(jsonDocument, config); - } - - void updateFromString(String& config) { - DynamicJsonDocument jsonDocument(MAX_SETTINGS_SIZE); - deserializeJson(jsonDocument, config); - updateFromDocument(jsonDocument); + void update(std::function callback, String originId) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); +#endif + callback(_settings); + callUpdateHandlers(originId); +#ifdef ESP32 + xSemaphoreGiveRecursive(_updateMutex); +#endif } - void fetchAsDocument(JsonDocument& jsonDocument) { - JsonObject jsonObject = jsonDocument.to(); - writeToJsonObject(jsonObject); + void read(std::function callback) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); +#endif + callback(_settings); +#ifdef ESP32 + xSemaphoreGiveRecursive(_updateMutex); +#endif } - void updateFromDocument(JsonDocument& jsonDocument) { - if (jsonDocument.is()) { - JsonObject newConfig = jsonDocument.as(); - readFromJsonObject(newConfig); - writeToFS(); - callUpdateHandlers(); + void callUpdateHandlers(String originId) { + for (const SettingsUpdateHandlerInfo_t& handler : _settingsUpdateHandlers) { + handler._cb(originId); } } - void begin() { - // read the initial data from the file system - readFromFS(); - } - protected: T _settings; - char const* _servicePath; - AsyncJsonWebHandler _updateHandler; +#ifdef ESP32 + SemaphoreHandle_t _updateMutex; +#endif + private: std::list _settingsUpdateHandlers; - - virtual void fetchConfig(AsyncWebServerRequest* request) { - // handle the request - AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SETTINGS_SIZE); - JsonObject jsonObject = response->getRoot(); - writeToJsonObject(jsonObject); - response->setLength(); - request->send(response); - } - - virtual void updateConfig(AsyncWebServerRequest* request, JsonDocument& jsonDocument) { - // handle the request - if (jsonDocument.is()) { - JsonObject newConfig = jsonDocument.as(); - readFromJsonObject(newConfig); - writeToFS(); - - // write settings back with a callback to reconfigure the wifi - AsyncJsonCallbackResponse* response = - new AsyncJsonCallbackResponse([this]() { callUpdateHandlers(); }, false, MAX_SETTINGS_SIZE); - JsonObject jsonObject = response->getRoot(); - writeToJsonObject(jsonObject); - response->setLength(); - request->send(response); - } else { - request->send(400); - } - } - - void callUpdateHandlers() { - // call the classes own config update function - onConfigUpdated(); - - // call all setting update handlers - for (const SettingsUpdateHandlerInfo_t& handler : _settingsUpdateHandlers) { - handler._cb(); - } - } - - // implement to perform action when config has been updated - virtual void onConfigUpdated() { - } }; -#endif // end SettingsService +#endif // end SettingsService_h diff --git a/lib/framework/SettingsSocket.h b/lib/framework/SettingsSocket.h new file mode 100644 index 00000000..7b59da24 --- /dev/null +++ b/lib/framework/SettingsSocket.h @@ -0,0 +1,126 @@ +#ifndef SettingsSocket_h +#define SettingsSocket_h + +#include +#include +#include +#include + +#define MAX_SIMPLE_MSG_SIZE 1024 + +#define SETTINGS_SOCKET_CLIENT_ID_MSG_SIZE 128 +#define SETTINGS_SOCKET_ORIGIN "socket" +#define SETTINGS_SOCKET_CLIENT_ORIGIN_ID_PREFIX "socket:" + +/** + * SettingsSocket is designed to provide WebSocket based communication for making and observing updates to settings. + * + * TODO - Security via a parameter, optional on construction to start! + */ +template +class SettingsSocket { + public: + SettingsSocket(SettingsSerializer* settingsSerializer, + SettingsDeserializer* settingsDeserializer, + SettingsService* settingsService, + AsyncWebServer* server, + char const* socketPath) : + _settingsSerializer(settingsSerializer), + _settingsDeserializer(settingsDeserializer), + _settingsService(settingsService), + _server(server), + _webSocket(socketPath) { + _webSocket.onEvent(std::bind(&SettingsSocket::onWSEvent, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5, + std::placeholders::_6)); + _settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); + _server->addHandler(&_webSocket); + } + + private: + SettingsSerializer* _settingsSerializer; + SettingsDeserializer* _settingsDeserializer; + SettingsService* _settingsService; + AsyncWebServer* _server; + AsyncWebSocket _webSocket; + + /** + * Responds to the WSEvent by sending the current settings to the clients when they connect and by applying the changes + * sent to the socket directly to the settings service. + */ + void onWSEvent(AsyncWebSocket* server, + AsyncWebSocketClient* client, + AwsEventType type, + void* arg, + uint8_t* data, + size_t len) { + if (type == WS_EVT_CONNECT) { + // when a client connects, we transmit it's id and the current payload + transmitId(client); + transmitData(client, SETTINGS_SOCKET_ORIGIN); + } else if (type == WS_EVT_DATA) { + AwsFrameInfo* info = (AwsFrameInfo*)arg; + if (info->final && info->index == 0 && info->len == len) { + if (info->opcode == WS_TEXT) { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SIMPLE_MSG_SIZE); + DeserializationError error = deserializeJson(jsonDocument, (char*)data); + if (!error && jsonDocument.is()) { + _settingsService->update( + [&](T& settings) { _settingsDeserializer->deserialize(settings, jsonDocument.as()); }, + clientId(client)); + } + } + } + } + } + + void transmitId(AsyncWebSocketClient* client) { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(SETTINGS_SOCKET_CLIENT_ID_MSG_SIZE); + JsonObject root = jsonDocument.to(); + root["type"] = "id"; + root["id"] = clientId(client); + size_t len = measureJson(jsonDocument); + AsyncWebSocketMessageBuffer* buffer = _webSocket.makeBuffer(len); + if (buffer) { + serializeJson(jsonDocument, (char*)buffer->get(), len + 1); + client->text(buffer); + } + } + + String clientId(AsyncWebSocketClient* client) { + return SETTINGS_SOCKET_CLIENT_ORIGIN_ID_PREFIX + String(client->id()); + } + + /** + * Broadcasts the payload to the destination, if provided. Otherwise broadcasts to all clients except the origin, if + * specified. + * + * Original implementation sent clients their own IDs so they could ignore updates they initiated. This approach + * simplifies the client and the server implementation but may not be sufficent for all use-cases. + */ + void transmitData(AsyncWebSocketClient* client, String originId) { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SIMPLE_MSG_SIZE); + JsonObject root = jsonDocument.to(); + root["type"] = "payload"; + root["origin_id"] = originId; + JsonObject payload = root.createNestedObject("payload"); + _settingsService->read([&](T& settings) { _settingsSerializer->serialize(settings, payload); }); + + size_t len = measureJson(jsonDocument); + AsyncWebSocketMessageBuffer* buffer = _webSocket.makeBuffer(len); + if (buffer) { + serializeJson(jsonDocument, (char*)buffer->get(), len + 1); + if (client) { + client->text(buffer); + } else { + _webSocket.textAll(buffer); + } + } + } +}; +#endif // end SettingsSocket_h diff --git a/lib/framework/SimpleService.h b/lib/framework/SimpleService.h deleted file mode 100644 index 2cc29e2f..00000000 --- a/lib/framework/SimpleService.h +++ /dev/null @@ -1,87 +0,0 @@ -#ifndef Service_h -#define Service_h - -#ifdef ESP32 -#include -#include -#elif defined(ESP8266) -#include -#include -#endif - -#include -#include -#include -#include -#include - -/** - * At the moment, not expecting services to have to deal with large JSON - * files this could be made configurable fairly simply, it's exposed on - * AsyncJsonWebHandler with a setter. - */ -#define MAX_SETTINGS_SIZE 1024 - -/* - * Abstraction of a service which reads and writes data from an endpoint. - * - * Not currently used, but indended for use by features which do not - * require setting persistance. - */ -class SimpleService { - private: - AsyncJsonWebHandler _updateHandler; - - void fetchConfig(AsyncWebServerRequest* request) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SETTINGS_SIZE); - JsonObject jsonObject = response->getRoot(); - writeToJsonObject(jsonObject); - response->setLength(); - request->send(response); - } - - void updateConfig(AsyncWebServerRequest* request, JsonDocument& jsonDocument) { - if (jsonDocument.is()) { - JsonObject newConfig = jsonDocument.as(); - readFromJsonObject(newConfig); - - // write settings back with a callback to reconfigure the wifi - AsyncJsonCallbackResponse* response = - new AsyncJsonCallbackResponse([this]() { onConfigUpdated(); }, false, MAX_SETTINGS_SIZE); - JsonObject jsonObject = response->getRoot(); - writeToJsonObject(jsonObject); - response->setLength(); - request->send(response); - } else { - request->send(400); - } - } - - protected: - // reads the local config from the - virtual void readFromJsonObject(JsonObject& root) { - } - virtual void writeToJsonObject(JsonObject& root) { - } - - // implement to perform action when config has been updated - virtual void onConfigUpdated() { - } - - public: - SimpleService(AsyncWebServer* server, char const* servicePath) { - server->on(servicePath, HTTP_GET, std::bind(&SimpleService::fetchConfig, this, std::placeholders::_1)); - - _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); - } - - virtual ~SimpleService() { - } -}; - -#endif // end SimpleService diff --git a/lib/framework/WiFiSettingsService.cpp b/lib/framework/WiFiSettingsService.cpp index 53f844c4..d463690e 100644 --- a/lib/framework/WiFiSettingsService.cpp +++ b/lib/framework/WiFiSettingsService.cpp @@ -1,7 +1,11 @@ #include +static WiFiSettingsSerializer SERIALIZER; +static WiFiSettingsDeserializer DESERIALIZER; + WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - AdminSettingsService(server, fs, securityManager, WIFI_SETTINGS_SERVICE_PATH, WIFI_SETTINGS_FILE) { + _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, WIFI_SETTINGS_SERVICE_PATH, securityManager), + _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, WIFI_SETTINGS_FILE) { // We want the device to come up in opmode=0 (WIFI_OFF), when erasing the flash this is not the default. // If needed, we save opmode=0 before disabling persistence so the device boots with WiFi disabled in the future. if (WiFi.getMode() != WIFI_OFF) { @@ -24,60 +28,12 @@ WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, Securit _onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected( std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1)); #endif -} -WiFiSettingsService::~WiFiSettingsService() { + addUpdateHandler([&](String originId) { reconfigureWiFiConnection(); }, false); } void WiFiSettingsService::begin() { - SettingsService::begin(); - reconfigureWiFiConnection(); -} - -void WiFiSettingsService::readFromJsonObject(JsonObject& root) { - _settings.ssid = root["ssid"] | ""; - _settings.password = root["password"] | ""; - _settings.hostname = root["hostname"] | ""; - _settings.staticIPConfig = root["static_ip_config"] | false; - - // extended settings - readIP(root, "local_ip", _settings.localIP); - readIP(root, "gateway_ip", _settings.gatewayIP); - readIP(root, "subnet_mask", _settings.subnetMask); - readIP(root, "dns_ip_1", _settings.dnsIP1); - readIP(root, "dns_ip_2", _settings.dnsIP2); - - // Swap around the dns servers if 2 is populated but 1 is not - if (_settings.dnsIP1 == INADDR_NONE && _settings.dnsIP2 != INADDR_NONE) { - _settings.dnsIP1 = _settings.dnsIP2; - _settings.dnsIP2 = INADDR_NONE; - } - - // Turning off static ip config if we don't meet the minimum requirements - // of ipAddress, gateway and subnet. This may change to static ip only - // as sensible defaults can be assumed for gateway and subnet - if (_settings.staticIPConfig && - (_settings.localIP == INADDR_NONE || _settings.gatewayIP == INADDR_NONE || _settings.subnetMask == INADDR_NONE)) { - _settings.staticIPConfig = false; - } -} - -void WiFiSettingsService::writeToJsonObject(JsonObject& root) { - // connection settings - root["ssid"] = _settings.ssid; - root["password"] = _settings.password; - root["hostname"] = _settings.hostname; - root["static_ip_config"] = _settings.staticIPConfig; - - // extended settings - writeIP(root, "local_ip", _settings.localIP); - writeIP(root, "gateway_ip", _settings.gatewayIP); - writeIP(root, "subnet_mask", _settings.subnetMask); - writeIP(root, "dns_ip_1", _settings.dnsIP1); - writeIP(root, "dns_ip_2", _settings.dnsIP2); -} - -void WiFiSettingsService::onConfigUpdated() { + _settingsPersistence.readFromFS(); reconfigureWiFiConnection(); } @@ -95,18 +51,6 @@ void WiFiSettingsService::reconfigureWiFiConnection() { #endif } -void WiFiSettingsService::readIP(JsonObject& root, String key, IPAddress& _ip) { - if (!root[key].is() || !_ip.fromString(root[key].as())) { - _ip = INADDR_NONE; - } -} - -void WiFiSettingsService::writeIP(JsonObject& root, String key, IPAddress& _ip) { - if (_ip != INADDR_NONE) { - root[key] = _ip.toString(); - } -} - void WiFiSettingsService::loop() { unsigned long currentMillis = millis(); if (!_lastConnectionAttempt || (unsigned long)(currentMillis - _lastConnectionAttempt) >= WIFI_RECONNECTION_DELAY) { diff --git a/lib/framework/WiFiSettingsService.h b/lib/framework/WiFiSettingsService.h index a303f86d..e520c984 100644 --- a/lib/framework/WiFiSettingsService.h +++ b/lib/framework/WiFiSettingsService.h @@ -1,8 +1,9 @@ #ifndef WiFiSettingsService_h #define WiFiSettingsService_h -#include -#include +#include +#include +#include #define WIFI_SETTINGS_FILE "/config/wifiSettings.json" #define WIFI_SETTINGS_SERVICE_PATH "/rest/wifiSettings" @@ -24,20 +25,65 @@ class WiFiSettings { IPAddress dnsIP2; }; -class WiFiSettingsService : public AdminSettingsService { +class WiFiSettingsSerializer : public SettingsSerializer { + public: + void serialize(WiFiSettings& settings, JsonObject root) { + // connection settings + root["ssid"] = settings.ssid; + root["password"] = settings.password; + root["hostname"] = settings.hostname; + root["static_ip_config"] = settings.staticIPConfig; + + // extended settings + JsonUtils::writeIP(root, "local_ip", settings.localIP); + JsonUtils::writeIP(root, "gateway_ip", settings.gatewayIP); + JsonUtils::writeIP(root, "subnet_mask", settings.subnetMask); + JsonUtils::writeIP(root, "dns_ip_1", settings.dnsIP1); + JsonUtils::writeIP(root, "dns_ip_2", settings.dnsIP2); + } +}; + +class WiFiSettingsDeserializer : public SettingsDeserializer { + public: + void deserialize(WiFiSettings& settings, JsonObject root) { + settings.ssid = root["ssid"] | ""; + settings.password = root["password"] | ""; + settings.hostname = root["hostname"] | ""; + settings.staticIPConfig = root["static_ip_config"] | false; + + // extended settings + JsonUtils::readIP(root, "local_ip", settings.localIP); + JsonUtils::readIP(root, "gateway_ip", settings.gatewayIP); + JsonUtils::readIP(root, "subnet_mask", settings.subnetMask); + JsonUtils::readIP(root, "dns_ip_1", settings.dnsIP1); + JsonUtils::readIP(root, "dns_ip_2", settings.dnsIP2); + + // Swap around the dns servers if 2 is populated but 1 is not + if (settings.dnsIP1 == INADDR_NONE && settings.dnsIP2 != INADDR_NONE) { + settings.dnsIP1 = settings.dnsIP2; + settings.dnsIP2 = INADDR_NONE; + } + + // Turning off static ip config if we don't meet the minimum requirements + // of ipAddress, gateway and subnet. This may change to static ip only + // as sensible defaults can be assumed for gateway and subnet + if (settings.staticIPConfig && + (settings.localIP == INADDR_NONE || settings.gatewayIP == INADDR_NONE || settings.subnetMask == INADDR_NONE)) { + settings.staticIPConfig = false; + } + } +}; + +class WiFiSettingsService : public SettingsService { public: WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); - ~WiFiSettingsService(); void begin(); void loop(); - protected: - void readFromJsonObject(JsonObject& root); - void writeToJsonObject(JsonObject& root); - void onConfigUpdated(); - private: + SettingsEndpoint _settingsEndpoint; + SettingsPersistence _settingsPersistence; unsigned long _lastConnectionAttempt; #ifdef ESP32 @@ -49,8 +95,6 @@ class WiFiSettingsService : public AdminSettingsService { void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event); #endif - void readIP(JsonObject& root, String key, IPAddress& _ip); - void writeIP(JsonObject& root, String key, IPAddress& _ip); void reconfigureWiFiConnection(); void manageSTA(); }; diff --git a/media/framework.png b/media/framework.png new file mode 100644 index 0000000000000000000000000000000000000000..2e107662a329b593e13966886b932efd51faa6fb GIT binary patch literal 112054 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.0.0,<7.0.0 ESP Async WebServer@>=1.2.0,<2.0.0 - + AsyncMqttClient@>=0.8.2,<1.0.0 + [env:esp12e] platform = espressif8266 board = esp12e diff --git a/src/DemoProject.cpp b/src/DemoProject.cpp deleted file mode 100644 index 4ad924ea..00000000 --- a/src/DemoProject.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include - -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 - _settings.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) { - _settings.blinkSpeed = root["blink_speed"] | DEFAULT_BLINK_SPEED; -} - -void DemoProject::writeToJsonObject(JsonObject& root) { - // connection settings - root["blink_speed"] = _settings.blinkSpeed; -} diff --git a/src/DemoProject.h b/src/DemoProject.h deleted file mode 100644 index bd36e57f..00000000 --- a/src/DemoProject.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef DemoProject_h -#define DemoProject_h - -#include -#include - -#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 DemoSettings { - public: - uint8_t blinkSpeed; -}; - -class DemoProject : public AdminSettingsService { - public: - DemoProject(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); - ~DemoProject(); - - void loop(); - - private: - unsigned long _lastBlink = 0; - - protected: - void readFromJsonObject(JsonObject& root); - void writeToJsonObject(JsonObject& root); -}; - -#endif diff --git a/src/LightBrokerSettingsService.cpp b/src/LightBrokerSettingsService.cpp new file mode 100644 index 00000000..40835a32 --- /dev/null +++ b/src/LightBrokerSettingsService.cpp @@ -0,0 +1,13 @@ +#include + +static LightBrokerSettingsSerializer SERIALIZER; +static LightBrokerSettingsDeserializer DESERIALIZER; + +LightBrokerSettingsService::LightBrokerSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : + _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, LIGHT_BROKER_SETTINGS_PATH, securityManager), + _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, LIGHT_BROKER_SETTINGS_FILE) { +} + +void LightBrokerSettingsService::begin() { + _settingsPersistence.readFromFS(); +} diff --git a/src/LightBrokerSettingsService.h b/src/LightBrokerSettingsService.h new file mode 100644 index 00000000..d18d49d9 --- /dev/null +++ b/src/LightBrokerSettingsService.h @@ -0,0 +1,55 @@ +#ifndef LightBrokerSettingsService_h +#define LightBrokerSettingsService_h + +#include +#include +#include +#include + +#define LIGHT_BROKER_SETTINGS_FILE "/config/brokerSettings.json" +#define LIGHT_BROKER_SETTINGS_PATH "/rest/brokerSettings" + +class LightBrokerSettings { + public: + String mqttPath; + String name; + String uniqueId; +}; + +static String defaultDeviceValue(String prefix = "") { +#ifdef ESP32 + return prefix + String((unsigned long)ESP.getEfuseMac(), HEX); +#elif defined(ESP8266) + return prefix + String(ESP.getChipId(), HEX); +#endif +} + +class LightBrokerSettingsSerializer : public SettingsSerializer { + public: + void serialize(LightBrokerSettings& settings, JsonObject root) { + root["mqtt_path"] = settings.mqttPath; + root["name"] = settings.name; + root["unique_id"] = settings.uniqueId; + } +}; + +class LightBrokerSettingsDeserializer : public SettingsDeserializer { + public: + void deserialize(LightBrokerSettings& settings, JsonObject root) { + settings.mqttPath = root["mqtt_path"] | defaultDeviceValue("homeassistant/light/"); + settings.name = root["name"] | defaultDeviceValue("light-"); + settings.uniqueId = root["unique_id"] | defaultDeviceValue("light-"); + } +}; + +class LightBrokerSettingsService : public SettingsService { + public: + LightBrokerSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); + void begin(); + + private: + SettingsEndpoint _settingsEndpoint; + SettingsPersistence _settingsPersistence; +}; + +#endif // end LightBrokerSettingsService_h diff --git a/src/LightSettingsService.cpp b/src/LightSettingsService.cpp new file mode 100644 index 00000000..7a5f992f --- /dev/null +++ b/src/LightSettingsService.cpp @@ -0,0 +1,67 @@ +#include + +static LightSettingsSerializer SERIALIZER; +static LightSettingsDeserializer DESERIALIZER; + +static HomeAssistantSerializer HA_SERIALIZER; +static HomeAssistantDeserializer HA_DESERIALIZER; + +LightSettingsService::LightSettingsService(AsyncWebServer* server, + SecurityManager* securityManager, + AsyncMqttClient* mqttClient, + LightBrokerSettingsService* lightBrokerSettingsService) : + _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, LIGHT_SETTINGS_ENDPOINT_PATH, securityManager), + _settingsBroker(&HA_SERIALIZER, &HA_DESERIALIZER, this, mqttClient), + _settingsSocket(&SERIALIZER, &DESERIALIZER, this, server, LIGHT_SETTINGS_SOCKET_PATH), + _mqttClient(mqttClient), + _lightBrokerSettingsService(lightBrokerSettingsService) { + // configure blink led to be output + pinMode(BLINK_LED, OUTPUT); + + // configure MQTT callback + _mqttClient->onConnect(std::bind(&LightSettingsService::registerConfig, this)); + + // configure update handler for when the light settings change + _lightBrokerSettingsService->addUpdateHandler([&](String originId) { registerConfig(); }, false); + + // configure settings service update handler to update LED state + addUpdateHandler([&](String originId) { onConfigUpdated(); }, false); +} + +void LightSettingsService::begin() { + _settings.ledOn = DEFAULT_LED_STATE; + onConfigUpdated(); +} + +void LightSettingsService::onConfigUpdated() { + digitalWrite(BLINK_LED, _settings.ledOn ? LED_ON : LED_OFF); +} + +void LightSettingsService::registerConfig() { + if (!_mqttClient->connected()) { + return; + } + String configTopic; + String setTopic; + String stateTopic; + + DynamicJsonDocument doc(256); + _lightBrokerSettingsService->read([&](LightBrokerSettings& settings) { + configTopic = settings.mqttPath + "/config"; + setTopic = settings.mqttPath + "/set"; + stateTopic = settings.mqttPath + "/state"; + doc["~"] = settings.mqttPath; + doc["name"] = settings.name; + doc["unique_id"] = settings.uniqueId; + }); + doc["cmd_t"] = "~/set"; + doc["stat_t"] = "~/state"; + doc["schema"] = "json"; + doc["brightness"] = false; + + String payload; + serializeJson(doc, payload); + _mqttClient->publish(configTopic.c_str(), 0, false, payload.c_str()); + + _settingsBroker.configureBroker(setTopic, stateTopic); +} \ No newline at end of file diff --git a/src/LightSettingsService.h b/src/LightSettingsService.h new file mode 100644 index 00000000..693f2a39 --- /dev/null +++ b/src/LightSettingsService.h @@ -0,0 +1,75 @@ +#ifndef LightSettingsService_h +#define LightSettingsService_h + +#include +#include +#include +#include +#include + +#define BLINK_LED 2 +#define PRINT_DELAY 5000 + +#define DEFAULT_LED_STATE false +#define OFF_STATE "OFF" +#define ON_STATE "ON" +#define LED_ON 0x0 +#define LED_OFF 0x1 + +#define LIGHT_SETTINGS_ENDPOINT_PATH "/rest/lightSettings" +#define LIGHT_SETTINGS_SOCKET_PATH "/ws/lightSettings" + +class LightSettings { + public: + bool ledOn; +}; + +class LightSettingsSerializer : public SettingsSerializer { + public: + void serialize(LightSettings& settings, JsonObject root) { + root["led_on"] = settings.ledOn; + } +}; + +class LightSettingsDeserializer : public SettingsDeserializer { + public: + void deserialize(LightSettings& settings, JsonObject root) { + settings.ledOn = root["led_on"] | DEFAULT_LED_STATE; + } +}; + +class HomeAssistantSerializer : public SettingsSerializer { + public: + void serialize(LightSettings& settings, JsonObject root) { + root["state"] = settings.ledOn ? ON_STATE : OFF_STATE; + } +}; + +class HomeAssistantDeserializer : public SettingsDeserializer { + public: + void deserialize(LightSettings& settings, JsonObject root) { + String state = root["state"]; + settings.ledOn = strcmp(ON_STATE, state.c_str()) ? false : true; + } +}; + +class LightSettingsService : public SettingsService { + public: + LightSettingsService(AsyncWebServer* server, + SecurityManager* securityManager, + AsyncMqttClient* mqttClient, + LightBrokerSettingsService* lightBrokerSettingsService); + void begin(); + + private: + SettingsEndpoint _settingsEndpoint; + SettingsBroker _settingsBroker; + SettingsSocket _settingsSocket; + AsyncMqttClient* _mqttClient; + LightBrokerSettingsService* _lightBrokerSettingsService; + + void registerConfig(); + void onConfigUpdated(); +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp index 4e1254f0..ea9c980e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,12 +1,18 @@ -#include #include +#include +#include #include #define SERIAL_BAUD_RATE 115200 AsyncWebServer server(80); ESP8266React esp8266React(&server, &SPIFFS); -DemoProject demoProject = DemoProject(&server, &SPIFFS, esp8266React.getSecurityManager()); +LightBrokerSettingsService lightBrokerSettingsService = + LightBrokerSettingsService(&server, &SPIFFS, esp8266React.getSecurityManager()); +LightSettingsService lightSettingsService = LightSettingsService(&server, + esp8266React.getSecurityManager(), + esp8266React.getMQTTClient(), + &lightBrokerSettingsService); void setup() { // start serial and filesystem @@ -22,8 +28,11 @@ void setup() { // start the framework and demo project esp8266React.begin(); - // start the demo project - demoProject.begin(); + // load the initial light settings + lightSettingsService.begin(); + + // start the light service + lightBrokerSettingsService.begin(); // start the server server.begin(); @@ -32,7 +41,4 @@ void setup() { void loop() { // run the framework's loop function esp8266React.loop(); - - // run the demo project's loop function - demoProject.loop(); } From 5f396f4b2dc4ec2b1fa48a63c4948b3f23dab471 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Tue, 7 Apr 2020 22:21:01 +0100 Subject: [PATCH 02/35] de-dupe define names remove unused defines (cherry picked from commit 20d81c9d6a331b11ec6cee60a6615e75689e461b) --- lib/framework/MQTTSettingsService.h | 3 --- lib/framework/SettingsBroker.h | 6 +++--- lib/framework/SettingsEndpoint.h | 10 +++++----- lib/framework/SettingsPersistence.h | 10 +++++----- lib/framework/SettingsSocket.h | 6 +++--- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/framework/MQTTSettingsService.h b/lib/framework/MQTTSettingsService.h index 54c1c532..a08cea07 100644 --- a/lib/framework/MQTTSettingsService.h +++ b/lib/framework/MQTTSettingsService.h @@ -10,9 +10,6 @@ #define MQTT_SETTINGS_FILE "/config/mqttSettings.json" #define MQTT_SETTINGS_SERVICE_PATH "/rest/mqttSettings" -#define MAX_MQTT_STATUS_SIZE 1024 -#define MQTT_STATUS_SERVICE_PATH "/rest/mqttStatus" - #define MQTT_SETTINGS_SERVICE_DEFAULT_ENABLED false #define MQTT_SETTINGS_SERVICE_DEFAULT_HOST "test.mosquitto.org" #define MQTT_SETTINGS_SERVICE_DEFAULT_PORT 1883 diff --git a/lib/framework/SettingsBroker.h b/lib/framework/SettingsBroker.h index 9a0c383f..2ba3766d 100644 --- a/lib/framework/SettingsBroker.h +++ b/lib/framework/SettingsBroker.h @@ -6,7 +6,7 @@ #include #include -#define MAX_SETTINGS_SIZE 1024 +#define MAX_MESSAGE_SIZE 1024 #define SETTINGS_BROKER_ORIGIN_ID "broker" /** @@ -70,7 +70,7 @@ class SettingsBroker { void publish() { if (_stateTopic.length() > 0 && _mqttClient->connected()) { // serialize to json doc - DynamicJsonDocument json(MAX_SETTINGS_SIZE); + DynamicJsonDocument json(MAX_MESSAGE_SIZE); _settingsService->read([&](T& settings) { _settingsSerializer->serialize(settings, json.to()); }); // serialize to string @@ -94,7 +94,7 @@ class SettingsBroker { } // deserialize from string - DynamicJsonDocument json(MAX_SETTINGS_SIZE); + DynamicJsonDocument json(MAX_MESSAGE_SIZE); DeserializationError error = deserializeJson(json, payload, len); if (!error && json.is()) { _settingsService->update( diff --git a/lib/framework/SettingsEndpoint.h b/lib/framework/SettingsEndpoint.h index 0ec25272..def7c039 100644 --- a/lib/framework/SettingsEndpoint.h +++ b/lib/framework/SettingsEndpoint.h @@ -11,7 +11,7 @@ #include #include -#define MAX_SETTINGS_SIZE 1024 +#define MAX_CONTENT_LENGTH 1024 #define SETTINGS_ENDPOINT_ORIGIN_ID "endpoint" template @@ -37,7 +37,7 @@ class SettingsEndpoint { securityManager->wrapRequest(std::bind(&SettingsEndpoint::fetchSettings, this, std::placeholders::_1), authenticationPredicate)); _updateHandler.setMethod(HTTP_POST); - _updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE); + _updateHandler.setMaxContentLength(MAX_CONTENT_LENGTH); server->addHandler(&_updateHandler); } @@ -53,7 +53,7 @@ class SettingsEndpoint { std::bind(&SettingsEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2)) { server->on(servicePath.c_str(), HTTP_GET, std::bind(&SettingsEndpoint::fetchSettings, this, std::placeholders::_1)); _updateHandler.setMethod(HTTP_POST); - _updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE); + _updateHandler.setMaxContentLength(MAX_CONTENT_LENGTH); server->addHandler(&_updateHandler); } @@ -64,7 +64,7 @@ class SettingsEndpoint { AsyncCallbackJsonWebHandler _updateHandler; void fetchSettings(AsyncWebServerRequest* request) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SETTINGS_SIZE); + AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); _settingsService->read( [&](T& settings) { _settingsSerializer->serialize(settings, response->getRoot().to()); }); response->setLength(); @@ -73,7 +73,7 @@ class SettingsEndpoint { void updateSettings(AsyncWebServerRequest* request, JsonVariant& json) { if (json.is()) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SETTINGS_SIZE); + AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); // use callback to update the settings once the response is complete request->onDisconnect([this]() { _settingsService->callUpdateHandlers(SETTINGS_ENDPOINT_ORIGIN_ID); }); diff --git a/lib/framework/SettingsPersistence.h b/lib/framework/SettingsPersistence.h index d1f3f32f..5f81164d 100644 --- a/lib/framework/SettingsPersistence.h +++ b/lib/framework/SettingsPersistence.h @@ -6,7 +6,7 @@ #include #include -#define MAX_SETTINGS_SIZE 1024 +#define MAX_FILE_SIZE 1024 /** * SettingsPersistance takes care of loading and saving settings when they change. @@ -34,8 +34,8 @@ class SettingsPersistence { File settingsFile = _fs->open(_filePath, "r"); if (settingsFile) { - if (settingsFile.size() <= MAX_SETTINGS_SIZE) { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE); + if (settingsFile.size() <= MAX_FILE_SIZE) { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); DeserializationError error = deserializeJson(jsonDocument, settingsFile); if (error == DeserializationError::Ok && jsonDocument.is()) { readSettings(jsonDocument.as()); @@ -53,7 +53,7 @@ class SettingsPersistence { bool writeToFS() { // create and populate a new json object - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE); + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); _settingsService->read( [&](T& settings) { _settingsSerializer->serialize(settings, jsonDocument.to()); }); @@ -101,7 +101,7 @@ class SettingsPersistence { // We assume the readFromJsonObject supplies sensible defaults if an empty object // is supplied, this virtual function allows that to be changed. virtual void readDefaults() { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE); + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); readSettings(jsonDocument.to()); } }; diff --git a/lib/framework/SettingsSocket.h b/lib/framework/SettingsSocket.h index 7b59da24..a9128534 100644 --- a/lib/framework/SettingsSocket.h +++ b/lib/framework/SettingsSocket.h @@ -6,7 +6,7 @@ #include #include -#define MAX_SIMPLE_MSG_SIZE 1024 +#define MAX_SETTINGS_SOCKET_MSG_SIZE 1024 #define SETTINGS_SOCKET_CLIENT_ID_MSG_SIZE 128 #define SETTINGS_SOCKET_ORIGIN "socket" @@ -67,7 +67,7 @@ class SettingsSocket { AwsFrameInfo* info = (AwsFrameInfo*)arg; if (info->final && info->index == 0 && info->len == len) { if (info->opcode == WS_TEXT) { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SIMPLE_MSG_SIZE); + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SOCKET_MSG_SIZE); DeserializationError error = deserializeJson(jsonDocument, (char*)data); if (!error && jsonDocument.is()) { _settingsService->update( @@ -104,7 +104,7 @@ class SettingsSocket { * simplifies the client and the server implementation but may not be sufficent for all use-cases. */ void transmitData(AsyncWebSocketClient* client, String originId) { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SIMPLE_MSG_SIZE); + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SOCKET_MSG_SIZE); JsonObject root = jsonDocument.to(); root["type"] = "payload"; root["origin_id"] = originId; From 9c94ce4e735ec14a4a90ff7d3d21392a1043e346 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Tue, 7 Apr 2020 22:57:05 +0100 Subject: [PATCH 03/35] infer endpoint root in production mode (cherry picked from commit 57367c8832d8e986b278e6027bc5199d4b45bff4) --- interface/.env.development | 2 +- interface/.env.production | 1 - interface/src/api/Env.ts | 30 +++++++++++-------- .../project/LightSettingsSocketController.tsx | 2 +- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/interface/.env.development b/interface/.env.development index a641dd7c..7aaf530e 100644 --- a/interface/.env.development +++ b/interface/.env.development @@ -1,4 +1,4 @@ # 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.99/rest/ +REACT_APP_HTTP_ROOT=http://192.168.0.99 REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.99 diff --git a/interface/.env.production b/interface/.env.production index 5f7447a0..ba7cc18d 100644 --- a/interface/.env.production +++ b/interface/.env.production @@ -1,2 +1 @@ -REACT_APP_ENDPOINT_ROOT=/rest/ GENERATE_SOURCEMAP=false diff --git a/interface/src/api/Env.ts b/interface/src/api/Env.ts index 6722b571..9992e681 100644 --- a/interface/src/api/Env.ts +++ b/interface/src/api/Env.ts @@ -1,20 +1,24 @@ 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!; -// TODO use same approach for rest endpoint? -export const WEB_SOCKET_ROOT = calculateWebSocketPrefix("/ws"); +export const ENDPOINT_ROOT = calculateEndpointRoot("/rest/"); +export const WEB_SOCKET_ROOT = calculateWebSocketRoot("/ws/"); -function calculateWebSocketPrefix(webSocketPath: string) { +function calculateEndpointRoot(endpointPath: string) { + const httpRoot = process.env.REACT_APP_HTTP_ROOT; + if (httpRoot) { + return httpRoot + endpointPath; + } + const location = window.location; + return location.protocol + "//" + location.host + endpointPath; +} + +function calculateWebSocketRoot(webSocketPath: string) { const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT; - if (!webSocketRoot || webSocketRoot.length === 0) { - var loc = window.location, webSocketURI; - if (loc.protocol === "https:") { - webSocketURI = "wss:"; - } else { - webSocketURI = "ws:"; - } - return webSocketURI + "//" + loc.host + webSocketPath; + if (webSocketRoot) { + return webSocketRoot + webSocketPath; } - return webSocketRoot + webSocketPath; + const location = window.location; + const webProtocol = location.protocol === "https:" ? "wss:" : "ws:"; + return webProtocol + "//" + location.host + webSocketPath; } diff --git a/interface/src/project/LightSettingsSocketController.tsx b/interface/src/project/LightSettingsSocketController.tsx index ce8175bd..25de5be7 100644 --- a/interface/src/project/LightSettingsSocketController.tsx +++ b/interface/src/project/LightSettingsSocketController.tsx @@ -8,7 +8,7 @@ import { SectionContent, BlockFormControlLabel } from '../components'; import { LightSettings } from './types'; -export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "/lightSettings"; +export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "lightSettings"; type LightSettingsSocketControllerProps = SocketControllerProps; From 96d95b91511c5844301e0ebc948b361fd96a049c Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Tue, 7 Apr 2020 23:19:52 +0100 Subject: [PATCH 04/35] Start improving documentation to describe newly added and modified functionallity (cherry picked from commit b25ab4f347a875ab78126e430578cc1d0836220d) --- README.md | 110 ++++++---------------------------- data/config/demoSettings.json | 3 - 2 files changed, 17 insertions(+), 96 deletions(-) delete mode 100644 data/config/demoSettings.json diff --git a/README.md b/README.md index 4d8f7f87..4d3539dc 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Provides many of the features required for IoT projects: * Configurable WiFi - Network scanner and WiFi configuration screen * Configurable Access Point - Can be continuous or automatically enabled when WiFi connection fails * Network Time - Synchronization with NTP +* MQTT - Connection to an MQTT broker for automation and monitoring * Remote Firmware Updates - Enable secured OTA updates * Security - Protected RESTful endpoints and a secured user interface @@ -154,11 +155,12 @@ The config files can be found in the ['data/config'](data/config) directory: File | Description ---- | ----------- -[apSettings.json](data/config/apSettings.json) | Access point settings -[ntpSettings.json](data/config/ntpSettings.json) | NTP synchronization settings -[otaSettings.json](data/config/otaSettings.json) | OTA update configuration +[apSettings.json](data/config/apSettings.json) | Access point settings +[mqttSettings.json](data/config/mqttSettings.json) | MQTT connection settings +[ntpSettings.json](data/config/ntpSettings.json) | NTP synchronization settings +[otaSettings.json](data/config/otaSettings.json) | OTA update configuration [securitySettings.json](data/config/securitySettings.json) | Security settings and user credentials -[wifiSettings.json](data/config/wifiSettings.json) | WiFi connection settings +[wifiSettings.json](data/config/wifiSettings.json) | WiFi connection settings ### Access point settings @@ -283,12 +285,11 @@ The framework's source is split up by feature, for example [WiFiScanner.h](lib/f 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: +The following code creates the web server and esp8266React framework: ```cpp AsyncWebServer server(80); ESP8266React esp8266React(&server, &SPIFFS); -DemoProject demoProject = DemoProject(&server, &SPIFFS, esp8266React.getSecurityManager()); ``` Now in the `setup()` function the initialization is performed: @@ -308,23 +309,17 @@ void setup() { // 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: +Finally the loop calls the framework's loop function to service the frameworks features. ```cpp void loop() { // run the framework's loop function esp8266React.loop(); - - // run the demo project's loop function - demoProject.loop(); } ``` @@ -460,7 +455,7 @@ 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: +You can use the security manager to wrap any request handler function with an authentication predicate: ```cpp server->on("/rest/someService", HTTP_GET, @@ -468,86 +463,9 @@ server->on("/rest/someService", HTTP_GET, ); ``` -Alternatively you can extend [AdminSettingsService.h](lib/framework/AdminSettingsService.h) and optionally override `getAuthenticationPredicate()` to secure an endpoint. - -## Extending the framework - -It is recommend that you explore the framework code to gain a better understanding of how to use it's features. The framework provides APIs so you can add your own services or features or, if required, directly configure or observe changes to core framework features. Some of these capabilities are detailed below. - -### Adding a service with persistant settings - -The following code demonstrates how you might extend the framework with a feature which requires a username and password to be configured to drive an unspecified feature. - -```cpp -#include - -class ExampleSettings { - public: - String username; - String password; -}; - -class ExampleSettingsService : public SettingsService { - - public: - - ExampleSettingsService(AsyncWebServer* server, FS* fs) - : SettingsService(server, fs, "/exampleSettings", "/config/exampleSettings.json") {} - - ~ExampleSettingsService(){} - - protected: - - void readFromJsonObject(JsonObject& root) { - _settings.username = root["username"] | ""; - _settings.password = root["password"] | ""; - } - - void writeToJsonObject(JsonObject& root) { - root["username"] = _settings.username; - root["password"] = _settings.password; - } - -}; -``` - -Now this can be constructed, added to the server, and started as such: - -```cpp -ExampleSettingsService exampleSettingsService = ExampleSettingsService(&server, &SPIFFS); - -exampleSettingsService.begin(); -``` - -There will now be a REST service exposed on "/exampleSettings" for reading and writing (GET/POST) the settings. Any modifications will be persisted in SPIFFS, in this case to "/config/exampleSettings.json" - -Sometimes you need to perform an action when the settings are updated, you can achieve this by overriding the onConfigUpdated() function which gets called every time the settings are updated. You can also perform an action when the service starts by overriding the begin() function, being sure to call SettingsService::begin(). You can also provide a "loop" function in order to allow your service class continuously perform an action, calling this from the main loop. - -```cpp - -void begin() { - // make sure we call super, so the settings get read! - SettingsService::begin(); - reconfigureTheService(); -} - -void onConfigUpdated() { - reconfigureTheService(); -} - -void reconfigureTheService() { - // do whatever is required to react to the new settings -} - -void loop() { - // execute somthing as part of the main loop -} - -``` - ### Accessing settings and services -The framework supplies access to it's SettingsService instances and the SecurityManager via getter functions: +The framework supplies access to various features via getter functions: SettingsService | Description ---------------------------- | ---------------------------------------------- @@ -557,8 +475,13 @@ getWiFiSettingsService() | Configures and manages the WiFi network connectio getAPSettingsService() | Configures and manages the Access Point getNTPSettingsService() | Configures and manages the network time getOTASettingsService() | Configures and manages the Over-The-Air update feature +getMQTTSettingsService() | Configures and manages the MQTT connection +getMQTTClient() | Provides direct access to the MQTT client instance + +These can be used to observe changes to settings. They can also be used to fetch or update settings. -These can be used to observe changes to settings. They can also be used to fetch or update settings directly via objects, JSON strings and JsonObjects. Here are some examples of how you may use this. +------ TODO ----- +Fix documentation, provide serialization examples Inspect the current WiFi settings: @@ -592,3 +515,4 @@ esp8266React.getWiFiSettingsService()->addUpdateHandler([]() { * [notistack](https://github.com/iamhosseindhv/notistack) * [ArduinoJson](https://github.com/bblanchon/ArduinoJson) * [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) +* [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) diff --git a/data/config/demoSettings.json b/data/config/demoSettings.json deleted file mode 100644 index 2fc488c8..00000000 --- a/data/config/demoSettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "led_on": true -} \ No newline at end of file From 2a52c78f5caac33c2b1d98a75ba2421430e67bd5 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Mon, 13 Apr 2020 13:23:16 +0100 Subject: [PATCH 05/35] add security to websockets --- .../src/authentication/Authentication.ts | 10 +++++ interface/src/components/SocketController.tsx | 3 +- lib/framework/SecurityManager.h | 7 ++++ lib/framework/SecuritySettingsService.cpp | 11 +++++ lib/framework/SecuritySettingsService.h | 1 + lib/framework/SettingsSocket.h | 41 ++++++++++++++++--- src/LightSettingsService.cpp | 2 +- 7 files changed, 68 insertions(+), 7 deletions(-) diff --git a/interface/src/authentication/Authentication.ts b/interface/src/authentication/Authentication.ts index d1cf8178..17310b73 100644 --- a/interface/src/authentication/Authentication.ts +++ b/interface/src/authentication/Authentication.ts @@ -61,3 +61,13 @@ export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestIni }); }); } + +export function addAccessTokenParameter(url: string) { + const accessToken = localStorage.getItem(ACCESS_TOKEN); + if (!accessToken) { + return url; + } + const parsedUrl = new URL(url); + parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken); + return parsedUrl.toString(); +} diff --git a/interface/src/components/SocketController.tsx b/interface/src/components/SocketController.tsx index 53f7ba8f..bf6a9db2 100644 --- a/interface/src/components/SocketController.tsx +++ b/interface/src/components/SocketController.tsx @@ -3,6 +3,7 @@ import Sockette from 'sockette'; import throttle from 'lodash/throttle'; import { withSnackbar, WithSnackbarProps } from 'notistack'; +import { addAccessTokenParameter } from '../authentication'; import { extractEventValue } from '.'; export interface SocketControllerProps extends WithSnackbarProps { @@ -47,7 +48,7 @@ export function socketController>(wsUrl: s constructor(props: Omit> & WithSnackbarProps) { super(props); this.state = { - ws: new Sockette(wsUrl, { + ws: new Sockette(addAccessTokenParameter(wsUrl), { onmessage: this.onMessage, onopen: this.onOpen, onclose: this.onClose, diff --git a/lib/framework/SecurityManager.h b/lib/framework/SecurityManager.h index c9e2681e..1f3fa117 100644 --- a/lib/framework/SecurityManager.h +++ b/lib/framework/SecurityManager.h @@ -8,6 +8,8 @@ #define DEFAULT_JWT_SECRET "esp8266-react" +#define ACCESS_TOKEN_PARAMATER "access_token" + #define AUTHORIZATION_HEADER "Authorization" #define AUTHORIZATION_HEADER_PREFIX "Bearer " #define AUTHORIZATION_HEADER_PREFIX_LEN 7 @@ -72,6 +74,11 @@ class SecurityManager { */ virtual String generateJWT(User* user) = 0; + /** + * Filter a request with the provided predicate, only returning true if the predicate matches. + */ + virtual ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate) = 0; + /** * Wrap the provided request to provide validation against an AuthenticationPredicate. */ diff --git a/lib/framework/SecuritySettingsService.cpp b/lib/framework/SecuritySettingsService.cpp index 099cac7a..cc2efa0d 100644 --- a/lib/framework/SecuritySettingsService.cpp +++ b/lib/framework/SecuritySettingsService.cpp @@ -22,6 +22,10 @@ Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerReques value = value.substring(AUTHORIZATION_HEADER_PREFIX_LEN); return authenticateJWT(value); } + } else if (request->hasParam(ACCESS_TOKEN_PARAMATER)) { + AsyncWebParameter* tokenParamater = request->getParam(ACCESS_TOKEN_PARAMATER); + String value = tokenParamater->value(); + return authenticateJWT(value); } return Authentication(); } @@ -73,6 +77,13 @@ String SecuritySettingsService::generateJWT(User* user) { return _jwtHandler.buildJWT(payload); } +ArRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate) { + return [this, predicate](AsyncWebServerRequest* request) { + Authentication authentication = authenticateRequest(request); + return predicate(authentication); + }; +} + ArRequestHandlerFunction SecuritySettingsService::wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate) { return [this, onRequest, predicate](AsyncWebServerRequest* request) { diff --git a/lib/framework/SecuritySettingsService.h b/lib/framework/SecuritySettingsService.h index 8998575b..b1eaf5ed 100644 --- a/lib/framework/SecuritySettingsService.h +++ b/lib/framework/SecuritySettingsService.h @@ -63,6 +63,7 @@ class SecuritySettingsService : public SettingsService, public Authentication authenticate(String& username, String& password); Authentication authenticateRequest(AsyncWebServerRequest* request); String generateJWT(User* user); + ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate); ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback, AuthenticationPredicate predicate); diff --git a/lib/framework/SettingsSocket.h b/lib/framework/SettingsSocket.h index a9128534..4b9c2957 100644 --- a/lib/framework/SettingsSocket.h +++ b/lib/framework/SettingsSocket.h @@ -14,8 +14,6 @@ /** * SettingsSocket is designed to provide WebSocket based communication for making and observing updates to settings. - * - * TODO - Security via a parameter, optional on construction to start! */ template class SettingsSocket { @@ -24,12 +22,16 @@ class SettingsSocket { SettingsDeserializer* settingsDeserializer, SettingsService* settingsService, AsyncWebServer* server, - char const* socketPath) : + char const* socketPath, + SecurityManager* securityManager, + AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : _settingsSerializer(settingsSerializer), _settingsDeserializer(settingsDeserializer), _settingsService(settingsService), _server(server), _webSocket(socketPath) { + _settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); + _webSocket.setFilter(securityManager->filterRequest(authenticationPredicate)); _webSocket.onEvent(std::bind(&SettingsSocket::onWSEvent, this, std::placeholders::_1, @@ -38,7 +40,29 @@ class SettingsSocket { std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + _server->addHandler(&_webSocket); + _server->on(socketPath, HTTP_GET, std::bind(&SettingsSocket::forbidden, this, std::placeholders::_1)); + } + + SettingsSocket(SettingsSerializer* settingsSerializer, + SettingsDeserializer* settingsDeserializer, + SettingsService* settingsService, + AsyncWebServer* server, + char const* socketPath) : + _settingsSerializer(settingsSerializer), + _settingsDeserializer(settingsDeserializer), + _settingsService(settingsService), + _server(server), + _webSocket(socketPath) { _settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); + _webSocket.onEvent(std::bind(&SettingsSocket::onWSEvent, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5, + std::placeholders::_6)); _server->addHandler(&_webSocket); } @@ -50,8 +74,15 @@ class SettingsSocket { AsyncWebSocket _webSocket; /** - * Responds to the WSEvent by sending the current settings to the clients when they connect and by applying the changes - * sent to the socket directly to the settings service. + * Renders a forbidden respnose to the client if they fail to connect. + */ + void forbidden(AsyncWebServerRequest* request) { + request->send(403); + } + + /** + * Responds to the WSEvent by sending the current settings to the clients when they connect and by applying the + * changes sent to the socket directly to the settings service. */ void onWSEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, diff --git a/src/LightSettingsService.cpp b/src/LightSettingsService.cpp index 7a5f992f..b9a7f2c8 100644 --- a/src/LightSettingsService.cpp +++ b/src/LightSettingsService.cpp @@ -12,7 +12,7 @@ LightSettingsService::LightSettingsService(AsyncWebServer* server, LightBrokerSettingsService* lightBrokerSettingsService) : _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, LIGHT_SETTINGS_ENDPOINT_PATH, securityManager), _settingsBroker(&HA_SERIALIZER, &HA_DESERIALIZER, this, mqttClient), - _settingsSocket(&SERIALIZER, &DESERIALIZER, this, server, LIGHT_SETTINGS_SOCKET_PATH), + _settingsSocket(&SERIALIZER, &DESERIALIZER, this, server, LIGHT_SETTINGS_SOCKET_PATH, securityManager), _mqttClient(mqttClient), _lightBrokerSettingsService(lightBrokerSettingsService) { // configure blink led to be output From 02ba38c831b51957c82cfe09403a952e3987a534 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Wed, 22 Apr 2020 15:10:59 +0100 Subject: [PATCH 06/35] more improvements to documentation fix demo led state on esp32 --- README.md | 52 ++++++++++++++++++++-------------- lib/framework/SettingsSocket.h | 2 +- platformio.ini | 5 ++-- src/LightSettingsService.h | 13 +++++++-- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4d3539dc..f0a2d9d0 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ platform = espressif32 board = node32s ``` -This is largley left as an exersise for the reader as everyone's requirements will vary. +This is left as an exersise for the reader as everyone's requirements will vary. ### Running the interface locally @@ -325,15 +325,15 @@ void loop() { ### Developing with the framework -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. +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 in this section and a comprenensive example is provided by the demo project. -The following diagram visualises how the framework's modular components fit together. They are described in detail below. +The following diagram visualises how the framework's modular components fit together, each feature is described in detail below. ![framework diagram](/media/framework.png?raw=true "framework diagram") #### Settings service -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: +The [SettingsService.h](lib/framework/SettingsService.h) class is a responsible for managing settings and interfacing with code which wants to change or respond to changes in them. You can define a data class to hold settings then build a SettingsService instance to manage them: ```cpp class LightSettings { @@ -346,7 +346,7 @@ class LightSettingsService : public SettingsService { }; ``` -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. +You may listen for changes to settings by registering an update handler callback. It is possible to remove an update handler later if required. ```cpp // register an update handler @@ -360,6 +360,14 @@ update_handler_id_t myUpdateHandler = lightSettingsService.addUpdateHandler( lightSettingsService.removeUpdateHandler(myUpdateHandler); ``` +An "originId" is passed to the update handler which may be used to identify the origin of the update. The default origin values the framework provides are: + +Origin | Description +----------------- | ----------- +endpoint | An update over REST (SettingsEndpoint) +broker | An update sent over MQTT (SettingsBroker) +socket:{clientId} | An update sent over WebSocket (SettingsSocket) + 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. ```cpp @@ -368,12 +376,12 @@ lightSettingsService.read([&](LightSettings& settings) { }); ``` -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. +SettingsService also exposes an update function which allows the caller to update the settings with a callback. This approach automatically calls the registered update handlers when complete. The example below turns on the lights using the arbitrary origin "timer": ```cpp lightSettingsService.update([&](LightSettings& settings) { settings.on = true; // turn on the lights! -}); +}, "timer"); ``` #### Serialization @@ -447,7 +455,7 @@ class LightSettingsService : public SettingsService { 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): +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 privileges. The built in AuthenticationPredicates can be found in [SecurityManager.h](lib/framework/SecurityManager.h) and are as follows: Predicate | Description -------------------- | ----------- @@ -480,32 +488,32 @@ getMQTTClient() | Provides direct access to the MQTT client instanc These can be used to observe changes to settings. They can also be used to fetch or update settings. ------- TODO ----- -Fix documentation, provide serialization examples - Inspect the current WiFi settings: ```cpp -WiFiSettings wifiSettings = esp8266React.getWiFiSettingsService()->fetch(); -Serial.print("The ssid is:"); -Serial.println(wifiSettings.ssid); +esp8266React.getWiFiSettingsService()->read([&](WiFiSettings& settings) { + Serial.print("The ssid is:"); + Serial.println(wifiSettings.ssid); +}); ``` -Configure the SSID and password: +Configure the WiFi SSID and password manually: ```cpp -WiFiSettings wifiSettings = esp8266React.getWiFiSettingsService()->fetch(); -wifiSettings.ssid = "MyNetworkSSID"; -wifiSettings.password = "MySuperSecretPassword"; -esp8266React.getWiFiSettingsService()->update(wifiSettings); +esp8266React.getWiFiSettingsService()->update([&](WiFiSettings& settings) { + wifiSettings.ssid = "MyNetworkSSID"; + wifiSettings.password = "MySuperSecretPassword"; +}, "myapp"); ``` Observe changes to the WiFiSettings: ```cpp -esp8266React.getWiFiSettingsService()->addUpdateHandler([]() { - Serial.println("The WiFi Settings were updated!"); -}); +esp8266React.getWiFiSettingsService()->addUpdateHandler( + [&](String originId) { + Serial.println("The WiFi Settings were updated!"); + } +); ``` ## Libraries Used diff --git a/lib/framework/SettingsSocket.h b/lib/framework/SettingsSocket.h index 4b9c2957..69e45eda 100644 --- a/lib/framework/SettingsSocket.h +++ b/lib/framework/SettingsSocket.h @@ -43,7 +43,7 @@ class SettingsSocket { _server->addHandler(&_webSocket); _server->on(socketPath, HTTP_GET, std::bind(&SettingsSocket::forbidden, this, std::placeholders::_1)); } - + SettingsSocket(SettingsSerializer* settingsSerializer, SettingsDeserializer* settingsDeserializer, SettingsService* settingsService, diff --git a/platformio.ini b/platformio.ini index de7f78da..39976155 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,7 +6,7 @@ default_envs = esp12e build_flags= -D NO_GLOBAL_ARDUINOOTA ; Uncomment ENABLE_CORS to enable Cross-Origin Resource Sharing (required for local React development) - ;-D ENABLE_CORS + -D ENABLE_CORS -D CORS_ORIGIN=\"http://localhost:3000\" ; Uncomment PROGMEM_WWW to enable the storage of the WWW data in PROGMEM ;-D PROGMEM_WWW @@ -24,7 +24,7 @@ framework = arduino monitor_speed = 115200 extra_scripts = - pre:scripts/build_interface.py + pre:scripts/build_interface.py lib_deps = ArduinoJson@>=6.0.0,<7.0.0 @@ -37,6 +37,7 @@ board = esp12e board_build.f_cpu = 160000000L [env:node32s] +; Uncomment the min_spiffs.csv setting if using PROGMEM_WWW with ESP32 ;board_build.partitions = min_spiffs.csv platform = espressif32 board = node32s diff --git a/src/LightSettingsService.h b/src/LightSettingsService.h index 693f2a39..5d73e0c9 100644 --- a/src/LightSettingsService.h +++ b/src/LightSettingsService.h @@ -13,8 +13,17 @@ #define DEFAULT_LED_STATE false #define OFF_STATE "OFF" #define ON_STATE "ON" -#define LED_ON 0x0 -#define LED_OFF 0x1 + +// Note that the built-in LED is on when the pin is low on most NodeMCU boards. +// This is because the anode is tied to VCC and the cathode to the GPIO 4 (Arduino pin 2). +#ifdef ESP32 + #define LED_ON 0x1 + #define LED_OFF 0x0 +#elif defined(ESP8266) + #define LED_ON 0x0 + #define LED_OFF 0x1 +#endif + #define LIGHT_SETTINGS_ENDPOINT_PATH "/rest/lightSettings" #define LIGHT_SETTINGS_SOCKET_PATH "/ws/lightSettings" From fdf676af1348ccc09af5b7671643a79e350f7353 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Wed, 22 Apr 2020 15:12:56 +0100 Subject: [PATCH 07/35] fix code examples in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f0a2d9d0..118cbfff 100644 --- a/README.md +++ b/README.md @@ -491,7 +491,7 @@ These can be used to observe changes to settings. They can also be used to fetch Inspect the current WiFi settings: ```cpp -esp8266React.getWiFiSettingsService()->read([&](WiFiSettings& settings) { +esp8266React.getWiFiSettingsService()->read([&](WiFiSettings& wifiSettings) { Serial.print("The ssid is:"); Serial.println(wifiSettings.ssid); }); @@ -500,7 +500,7 @@ esp8266React.getWiFiSettingsService()->read([&](WiFiSettings& settings) { Configure the WiFi SSID and password manually: ```cpp -esp8266React.getWiFiSettingsService()->update([&](WiFiSettings& settings) { +esp8266React.getWiFiSettingsService()->update([&](WiFiSettings& wifiSettings) { wifiSettings.ssid = "MyNetworkSSID"; wifiSettings.password = "MySuperSecretPassword"; }, "myapp"); From bb1c81f5475621ff7e55cddaebc35eb898d5ab82 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Wed, 22 Apr 2020 22:00:35 +0100 Subject: [PATCH 08/35] continue documenting new features --- README.md | 69 +++++++++++++++++++++++++---- lib/framework/SettingsBroker.h | 11 +++-- lib/framework/SettingsPersistence.h | 6 +-- platformio.ini | 2 +- src/LightBrokerSettingsService.cpp | 12 ++++- src/LightSettingsService.cpp | 16 ++++++- 6 files changed, 97 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 118cbfff..f913c6c3 100644 --- a/README.md +++ b/README.md @@ -415,12 +415,14 @@ static LightSettingsDeserializer DESERIALIZER; #### Endpoints -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: +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: ```cpp class LightSettingsService : public SettingsService { public: - LightSettingsService(AsyncWebServer* server, SecurityManager* securityManager) : + LightSettingsService(AsyncWebServer* server) : _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, "/rest/lightSettings") { } @@ -429,28 +431,79 @@ class LightSettingsService : public SettingsService { }; ``` -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. +Endpoint security is provided by authentication predicates which are [documented below](#security-features). The SecurityManager and authentication predicate may be provided if a secure endpoint is required. The demo project shows how endpoints can be secured. #### Persistence -[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. +[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 `disableUpdateHandler()` if manual control of persistence is required. -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: +The code below demonstrates how to extend the LightSettingsService class to provide persistence: ```cpp class LightSettingsService : public SettingsService { public: - LightSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, "/rest/lightSettings"), + LightSettingsService(FS* fs) : _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, "/config/lightSettings.json") { } private: - SettingsEndpoint _settingsEndpoint; SettingsPersistence _settingsPersistence; }; ``` +#### WebSockets + +[SettingsSocket.h](lib/framework/SettingsSocket.h) allows you to read and update settings over a WebSocket connection. SettingsSocket automatically pushes changes to all connected clients when settings are updated. + +The code below demonstrates how to extend the LightSettingsService class to provide an unsecured websocket: + +```cpp +class LightSettingsService : public SettingsService { + public: + LightSettingsService(AsyncWebServer* server) : + _settingsSocket(&SERIALIZER, &DESERIALIZER, this, server, "/ws/lightSettings"), { + } + + private: + SettingsSocket _settingsSocket; +}; +``` + +WebSocket security is provided by authentication predicates which are [documented below](#security-features). The SecurityManager and authentication predicate may be provided if a secure WebSocket is required. The demo project shows how WebSockets can be secured. + +#### MQTT + +The framework includes an MQTT client which can be configured via the UI. MQTT requirements will differ from project to project so the framework exposes the client for you to use as you see fit. The framework does however provide a utility to interface SettingsService to a pair of pub/sub (state/set) topics. This utility can be used to synchronize state with software such as Home Assistant. + +[SettingsBroker.h](lib/framework/SettingsBroker.h) allows you to read and update settings over a pair of MQTT topics. SettingsBroker automatically pushes changes to the pub topic and reads updates from the sub topic. + +The code below demonstrates how to extend the LightSettingsService class to interface with MQTT: + +```cpp +class LightSettingsService : public SettingsService { + public: + LightSettingsService(AsyncMqttClient* mqttClient) : + _settingsBroker(&SERIALIZER, + &DESERIALIZER, + this, + mqttClient, + "homeassistant/light/my_light/set", + "homeassistant/light/my_light/state") { + } + + private: + SettingsBroker _settingsBroker; +}; +``` + +You can also re-configure the pub/sub topics at runtime as required: + +```cpp +_settingsBroker.configureBroker("homeassistant/light/desk_lamp/set", "homeassistant/light/desk_lamp/state"); +``` + +The demo project allows the user to modify the MQTT topics via the UI so they can be changed without re-flashing the firmware. + ### Security features The framework has security features to prevent unauthorized use of the device. This is driven by [SecurityManager.h](lib/framework/SecurityManager.h). diff --git a/lib/framework/SettingsBroker.h b/lib/framework/SettingsBroker.h index 2ba3766d..4b33fea7 100644 --- a/lib/framework/SettingsBroker.h +++ b/lib/framework/SettingsBroker.h @@ -28,11 +28,15 @@ class SettingsBroker { SettingsBroker(SettingsSerializer* settingsSerializer, SettingsDeserializer* settingsDeserializer, SettingsService* settingsService, - AsyncMqttClient* mqttClient) : + AsyncMqttClient* mqttClient, + String setTopic = "", + String stateTopic = "") : _settingsSerializer(settingsSerializer), _settingsDeserializer(settingsDeserializer), _settingsService(settingsService), - _mqttClient(mqttClient) { + _mqttClient(mqttClient), + _setTopic(setTopic), + _stateTopic(stateTopic) { _settingsService->addUpdateHandler([&](String originId) { publish(); }, false); _mqttClient->onConnect(std::bind(&SettingsBroker::configureMQTT, this)); _mqttClient->onMessage(std::bind(&SettingsBroker::onMqttMessage, @@ -98,7 +102,8 @@ class SettingsBroker { DeserializationError error = deserializeJson(json, payload, len); if (!error && json.is()) { _settingsService->update( - [&](T& settings) { _settingsDeserializer->deserialize(settings, json.as()); }, SETTINGS_BROKER_ORIGIN_ID); + [&](T& settings) { _settingsDeserializer->deserialize(settings, json.as()); }, + SETTINGS_BROKER_ORIGIN_ID); } } }; diff --git a/lib/framework/SettingsPersistence.h b/lib/framework/SettingsPersistence.h index 5f81164d..636d964b 100644 --- a/lib/framework/SettingsPersistence.h +++ b/lib/framework/SettingsPersistence.h @@ -27,7 +27,7 @@ class SettingsPersistence { _settingsService(settingsManager), _fs(fs), _filePath(filePath) { - enableAutomatic(); + enableUpdateHandler(); } void readFromFS() { @@ -71,14 +71,14 @@ class SettingsPersistence { return true; } - void disableAutomatic() { + void disableUpdateHandler() { if (_updateHandlerId) { _settingsService->removeUpdateHandler(_updateHandlerId); _updateHandlerId = 0; } } - void enableAutomatic() { + void enableUpdateHandler() { if (!_updateHandlerId) { _updateHandlerId = _settingsService->addUpdateHandler([&](String originId) { writeToFS(); }); } diff --git a/platformio.ini b/platformio.ini index 39976155..38c91347 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,7 +6,7 @@ default_envs = esp12e build_flags= -D NO_GLOBAL_ARDUINOOTA ; Uncomment ENABLE_CORS to enable Cross-Origin Resource Sharing (required for local React development) - -D ENABLE_CORS + ;-D ENABLE_CORS -D CORS_ORIGIN=\"http://localhost:3000\" ; Uncomment PROGMEM_WWW to enable the storage of the WWW data in PROGMEM ;-D PROGMEM_WWW diff --git a/src/LightBrokerSettingsService.cpp b/src/LightBrokerSettingsService.cpp index 40835a32..97e13f07 100644 --- a/src/LightBrokerSettingsService.cpp +++ b/src/LightBrokerSettingsService.cpp @@ -3,8 +3,16 @@ static LightBrokerSettingsSerializer SERIALIZER; static LightBrokerSettingsDeserializer DESERIALIZER; -LightBrokerSettingsService::LightBrokerSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, LIGHT_BROKER_SETTINGS_PATH, securityManager), +LightBrokerSettingsService::LightBrokerSettingsService(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager) : + _settingsEndpoint(&SERIALIZER, + &DESERIALIZER, + this, + server, + LIGHT_BROKER_SETTINGS_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED), _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, LIGHT_BROKER_SETTINGS_FILE) { } diff --git a/src/LightSettingsService.cpp b/src/LightSettingsService.cpp index b9a7f2c8..79053d8f 100644 --- a/src/LightSettingsService.cpp +++ b/src/LightSettingsService.cpp @@ -10,9 +10,21 @@ LightSettingsService::LightSettingsService(AsyncWebServer* server, SecurityManager* securityManager, AsyncMqttClient* mqttClient, LightBrokerSettingsService* lightBrokerSettingsService) : - _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, LIGHT_SETTINGS_ENDPOINT_PATH, securityManager), + _settingsEndpoint(&SERIALIZER, + &DESERIALIZER, + this, + server, + LIGHT_SETTINGS_ENDPOINT_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED), _settingsBroker(&HA_SERIALIZER, &HA_DESERIALIZER, this, mqttClient), - _settingsSocket(&SERIALIZER, &DESERIALIZER, this, server, LIGHT_SETTINGS_SOCKET_PATH, securityManager), + _settingsSocket(&SERIALIZER, + &DESERIALIZER, + this, + server, + LIGHT_SETTINGS_SOCKET_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED), _mqttClient(mqttClient), _lightBrokerSettingsService(lightBrokerSettingsService) { // configure blink led to be output From 27e049b1518cb05a1845709f16248f65fa86ba81 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Wed, 22 Apr 2020 23:39:57 +0100 Subject: [PATCH 09/35] update documentation in the ui --- interface/src/project/DemoInformation.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/interface/src/project/DemoInformation.tsx b/interface/src/project/DemoInformation.tsx index fd3fde21..17cdc51f 100644 --- a/interface/src/project/DemoInformation.tsx +++ b/interface/src/project/DemoInformation.tsx @@ -65,10 +65,26 @@ class DemoInformation extends Component { - DemoController.tsx + LightSettingsRestController.tsx - The demo controller tab, to control the built-in LED. + A form which lets the user control the LED over a REST service. + + + + + LightSettingsSocketController.tsx + + + A form which lets the user control and monitor the status of the LED over WebSockets. + + + + + LightBrokerSettingsController.tsx + + + A form which lets the user change the MQTT settings for MQTT based control of the LED. From c33479ad4cdb54cad42054890e1348234d2305fc Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sun, 26 Apr 2020 23:22:12 +0100 Subject: [PATCH 10/35] add documentation --- README.md | 17 +++++++++++------ media/framework.png | Bin 112054 -> 61887 bytes 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f913c6c3..08a45a0f 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ You can configure the project to serve the interface from PROGMEM by uncommentin Be aware that this will consume ~150k of program space which can be especially problematic if you already have a large build artefact or if you have added large javascript dependencies to the interface. The ESP32 binaries are large already, so this will be a problem if you are using one of these devices and require this type of setup. -A method for working around this issue can be to reduce the amount of space allocated to SPIFFS by configuring the device to use a differnt strategy partitioning. If you don't require SPIFFS other than for storing config one approach might be to configure a minimal SPIFFS partition. +A method for working around this issue can be to reduce the amount of space allocated to SPIFFS by configuring the device to use differnt partitioning. If you don't require SPIFFS other than for storing config one approach might be to configure a minimal SPIFFS partition. For a ESP32 (4mb variant) there is a handy "min_spiffs.csv" partition table which can be enabled easily: @@ -113,7 +113,7 @@ This is left as an exersise for the reader as everyone's requirements will vary. ### Running the interface locally -You can run a local development server to allow you preview changes to the front end without the need to upload a file system image to the device after each change. +You can run a local development server to allow you preview changes to the front end without the need to upload a file system image to the device after each change. Change to the ['interface'](interface) directory with your bash shell (or Git Bash) and use the standard commands you would with any react app built with create-react-app: @@ -132,12 +132,17 @@ npm start #### Changing the endpoint root -The endpoint root path can be found in ['interface/.env.development'](interface/.env.development), defined as the environment variable 'REACT_APP_ENDPOINT_ROOT'. This needs to be the root URL of the device running the back end, for example: +The interface has a development environment which is enabled when running the development server using `npm start`. The environment file can be found in ['interface/.env.development'](interface/.env.development) and contains the HTTP root URL and the WebSocket root URL: -```js -REACT_APP_ENDPOINT_ROOT=http://192.168.0.6/rest/ +```properties +REACT_APP_HTTP_ROOT=http://192.168.0.99 +REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.99 ``` +The `REACT_APP_HTTP_ROOT` and `REACT_APP_WEB_SOCKET_ROOT` properties can be modified to point a ESP device running the back end firmware. + +> **Note**: You must restart the development server for changes to the environment to become effective. + #### Enabling CORS You can enable CORS on the back end by uncommenting the -D ENABLE_CORS build flag in ['platformio.ini'](platformio.ini) then re-building and uploading the firmware to the device. The default settings assume you will be accessing the development server on the default port on [http://localhost:3000](http://localhost:3000) this can also be changed if required: @@ -254,7 +259,7 @@ You can replace the app icon is located at ['interface/public/app/icon.png'](int The app name displayed on the login page and on the menu bar can be modified by editing the REACT_APP_NAME property in ['interface/.env'](interface/.env) -```js +```properties REACT_APP_NAME=Funky IoT Project ``` diff --git a/media/framework.png b/media/framework.png index 2e107662a329b593e13966886b932efd51faa6fb..badbf3adc98f13197ba1e8dffb3c760ea49a9f32 100644 GIT binary patch literal 61887 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 F{{| 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 Date: Sat, 2 May 2020 19:41:55 +0100 Subject: [PATCH 11/35] Fix typos in README.md Make allowRemove work as expected --- README.md | 14 +++++++------- lib/framework/SettingsEndpoint.h | 2 +- lib/framework/SettingsService.h | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 08a45a0f..61df464e 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ You can configure the project to serve the interface from PROGMEM by uncommentin Be aware that this will consume ~150k of program space which can be especially problematic if you already have a large build artefact or if you have added large javascript dependencies to the interface. The ESP32 binaries are large already, so this will be a problem if you are using one of these devices and require this type of setup. -A method for working around this issue can be to reduce the amount of space allocated to SPIFFS by configuring the device to use differnt partitioning. If you don't require SPIFFS other than for storing config one approach might be to configure a minimal SPIFFS partition. +A method for working around this issue can be to reduce the amount of space allocated to SPIFFS by configuring the device to use different partitioning. If you don't require SPIFFS other than for storing config one approach might be to configure a minimal SPIFFS partition. For a ESP32 (4mb variant) there is a handy "min_spiffs.csv" partition table which can be enabled easily: @@ -109,7 +109,7 @@ platform = espressif32 board = node32s ``` -This is left as an exersise for the reader as everyone's requirements will vary. +This is left as an exercise for the reader as everyone's requirements will vary. ### Running the interface locally @@ -147,7 +147,7 @@ The `REACT_APP_HTTP_ROOT` and `REACT_APP_WEB_SOCKET_ROOT` properties can be modi You can enable CORS on the back end by uncommenting the -D ENABLE_CORS build flag in ['platformio.ini'](platformio.ini) then re-building and uploading the firmware to the device. The default settings assume you will be accessing the development server on the default port on [http://localhost:3000](http://localhost:3000) this can also be changed if required: -``` +```properties -D ENABLE_CORS -D CORS_ORIGIN=\"http://localhost:3000\" ``` @@ -330,7 +330,7 @@ void loop() { ### Developing with the framework -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 in this section and a comprenensive example is provided by the demo project. +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 in this section and a comprehensive example is provided by the demo project. The following diagram visualises how the framework's modular components fit together, each feature is described in detail below. @@ -391,7 +391,7 @@ lightSettingsService.update([&](LightSettings& settings) { #### Serialization -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: +When transmitting settings over HTTP, WebSockets, or MQTT they must to be marshalled into a serializable 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 serialization of settings: ```cpp class LightSettingsSerializer : public SettingsSerializer { @@ -411,7 +411,7 @@ class LightSettingsDeserializer : public SettingsDeserializer { }; ``` -It is recommended you make create singletons for your serialzers and that they are stateless: +Unless you have more complicated requirements most serializers/deserializers are easiest to implement as stateless singletons: ```cpp static LightSettingsSerializer SERIALIZER; @@ -513,7 +513,7 @@ The demo project allows the user to modify the MQTT topics via the UI so they ca 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 privileges. The built in AuthenticationPredicates can be found in [SecurityManager.h](lib/framework/SecurityManager.h) and are as follows: +On successful authentication, the /rest/signIn endpoint issues a [JSON Web Token (JWT)](https://jwt.io/) which is then sent using Bearer Authentication. The framework come with built-in predicates for verifying a users access privileges. The built in AuthenticationPredicates can be found in [SecurityManager.h](lib/framework/SecurityManager.h) and are as follows: Predicate | Description -------------------- | ----------- diff --git a/lib/framework/SettingsEndpoint.h b/lib/framework/SettingsEndpoint.h index def7c039..249c83a7 100644 --- a/lib/framework/SettingsEndpoint.h +++ b/lib/framework/SettingsEndpoint.h @@ -79,7 +79,7 @@ class SettingsEndpoint { request->onDisconnect([this]() { _settingsService->callUpdateHandlers(SETTINGS_ENDPOINT_ORIGIN_ID); }); // update the settings, deferring the call to the update handlers to when the response is complete - _settingsService->updateWithoutPropogation([&](T& settings) { + _settingsService->updateWithoutPropagation([&](T& settings) { _settingsDeserializer->deserialize(settings, json.as()); _settingsSerializer->serialize(settings, response->getRoot().as()); }); diff --git a/lib/framework/SettingsService.h b/lib/framework/SettingsService.h index f8125336..ead713ec 100644 --- a/lib/framework/SettingsService.h +++ b/lib/framework/SettingsService.h @@ -41,7 +41,7 @@ class SettingsService { void removeUpdateHandler(update_handler_id_t id) { for (auto i = _settingsUpdateHandlers.begin(); i != _settingsUpdateHandlers.end();) { - if ((*i)._id == id) { + if ((*i)._allowRemove && (*i)._id == id) { i = _settingsUpdateHandlers.erase(i); } else { ++i; @@ -49,7 +49,7 @@ class SettingsService { } } - void updateWithoutPropogation(std::function callback) { + void updateWithoutPropagation(std::function callback) { read(callback); } From b00c6fb4a4fb33f38cf1fef16dd2c5ad64325e1a Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Thu, 7 May 2020 00:44:32 +0100 Subject: [PATCH 12/35] start conversion of serializers to static functions --- README.md | 33 ++++++++++------------- lib/framework/APSettingsService.cpp | 12 +++++---- lib/framework/APSettingsService.h | 10 ++----- lib/framework/MQTTSettingsService.cpp | 12 +++++---- lib/framework/MQTTSettingsService.h | 26 +++++++----------- lib/framework/NTPSettingsService.cpp | 12 +++++---- lib/framework/NTPSettingsService.h | 10 ++----- lib/framework/OTASettingsService.cpp | 7 ++--- lib/framework/OTASettingsService.h | 10 ++----- lib/framework/SecuritySettingsService.cpp | 12 +++++---- lib/framework/SecuritySettingsService.h | 10 ++----- lib/framework/SettingsBroker.h | 18 ++++++++----- lib/framework/SettingsDeserializer.h | 6 ++--- lib/framework/SettingsEndpoint.h | 24 ++++++++++------- lib/framework/SettingsPersistence.h | 16 ++++++----- lib/framework/SettingsSerializer.h | 5 +--- lib/framework/SettingsSocket.h | 19 +++++++------ lib/framework/WiFiSettingsService.cpp | 12 +++++---- lib/framework/WiFiSettingsService.h | 10 ++----- src/LightBrokerSettingsService.cpp | 13 ++++----- src/LightBrokerSettingsService.h | 20 +++++--------- src/LightSettingsService.cpp | 16 ++++------- src/LightSettingsService.h | 29 ++++++-------------- 23 files changed, 147 insertions(+), 195 deletions(-) diff --git a/README.md b/README.md index 61df464e..bcd3271f 100644 --- a/README.md +++ b/README.md @@ -391,33 +391,28 @@ lightSettingsService.update([&](LightSettings& settings) { #### Serialization -When transmitting settings over HTTP, WebSockets, or MQTT they must to be marshalled into a serializable 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 serialization of settings: +When transmitting settings over HTTP, WebSockets, or MQTT they must to be marshalled into a serializable form (JSON). The framework uses ArduinoJson for serialization and the functions defined in [SettingsSerializer.h](lib/framework/SettingsSerializer.h) and [SettingsDeserializer.h](lib/framework/SettingsDeserializer.h) facilitate this. + +The static functions below can be used to facilitate the serialization/deserialization of the example settings: ```cpp -class LightSettingsSerializer : public SettingsSerializer { +class LightSettings { public: - void serialize(LightSettings& settings, JsonObject root) { + bool on = false; + uint8_t brightness = 255; + + static void serialize(LightSettings& settings, JsonObject& root) { root["on"] = settings.on; root["brightness"] = settings.brightness; } -}; -class LightSettingsDeserializer : public SettingsDeserializer { - public: - void deserialize(LightSettings& settings, JsonObject root) { + static void deserialize(JsonObject& root, LightSettings& settings) { settings.on = root["on"] | false; settings.brightness = root["brightness"] | 255; } }; ``` -Unless you have more complicated requirements most serializers/deserializers are easiest to implement as stateless singletons: - -```cpp -static LightSettingsSerializer SERIALIZER; -static LightSettingsDeserializer DESERIALIZER; -``` - #### Endpoints 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. @@ -428,7 +423,7 @@ The code below demonstrates how to extend the LightSettingsService class to prov class LightSettingsService : public SettingsService { public: LightSettingsService(AsyncWebServer* server) : - _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, "/rest/lightSettings") { + _settingsEndpoint(LightSettings::serialize, LightSettings::deserialize, this, server, "/rest/lightSettings") { } private: @@ -448,7 +443,7 @@ The code below demonstrates how to extend the LightSettingsService class to prov class LightSettingsService : public SettingsService { public: LightSettingsService(FS* fs) : - _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, "/config/lightSettings.json") { + _settingsPersistence(LightSettings::serialize, LightSettings::deserialize, this, fs, "/config/lightSettings.json") { } private: @@ -466,7 +461,7 @@ The code below demonstrates how to extend the LightSettingsService class to prov class LightSettingsService : public SettingsService { public: LightSettingsService(AsyncWebServer* server) : - _settingsSocket(&SERIALIZER, &DESERIALIZER, this, server, "/ws/lightSettings"), { + _settingsSocket(LightSettings::serialize, LightSettings::deserialize, this, server, "/ws/lightSettings"), { } private: @@ -488,8 +483,8 @@ The code below demonstrates how to extend the LightSettingsService class to inte class LightSettingsService : public SettingsService { public: LightSettingsService(AsyncMqttClient* mqttClient) : - _settingsBroker(&SERIALIZER, - &DESERIALIZER, + _settingsBroker(LightSettings::serialize, + LightSettings::deserialize, this, mqttClient, "homeassistant/light/my_light/set", diff --git a/lib/framework/APSettingsService.cpp b/lib/framework/APSettingsService.cpp index c273f459..641ca972 100644 --- a/lib/framework/APSettingsService.cpp +++ b/lib/framework/APSettingsService.cpp @@ -1,11 +1,13 @@ #include -static APSettingsSerializer SERIALIZER; -static APSettingsDeserializer DESERIALIZER; - APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, AP_SETTINGS_SERVICE_PATH, securityManager), - _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, AP_SETTINGS_FILE) { + _settingsEndpoint(APSettings::serialize, + APSettings::deserialize, + this, + server, + AP_SETTINGS_SERVICE_PATH, + securityManager), + _settingsPersistence(APSettings::serialize, APSettings::deserialize, this, fs, AP_SETTINGS_FILE) { addUpdateHandler([&](String originId) { reconfigureAP(); }, false); } diff --git a/lib/framework/APSettingsService.h b/lib/framework/APSettingsService.h index a2f7d391..6c0905bd 100644 --- a/lib/framework/APSettingsService.h +++ b/lib/framework/APSettingsService.h @@ -26,20 +26,14 @@ class APSettings { uint8_t provisionMode; String ssid; String password; -}; -class APSettingsSerializer : public SettingsSerializer { - public: - void serialize(APSettings& settings, JsonObject root) { + static void serialize(APSettings& settings, JsonObject& root) { root["provision_mode"] = settings.provisionMode; root["ssid"] = settings.ssid; root["password"] = settings.password; } -}; -class APSettingsDeserializer : public SettingsDeserializer { - public: - void deserialize(APSettings& settings, JsonObject root) { + static void deserialize(JsonObject& root, APSettings& settings) { settings.provisionMode = root["provision_mode"] | AP_MODE_ALWAYS; switch (settings.provisionMode) { case AP_MODE_ALWAYS: diff --git a/lib/framework/MQTTSettingsService.cpp b/lib/framework/MQTTSettingsService.cpp index 85a2e197..224c2686 100644 --- a/lib/framework/MQTTSettingsService.cpp +++ b/lib/framework/MQTTSettingsService.cpp @@ -1,11 +1,13 @@ #include -static MQTTSettingsSerializer SERIALIZER; -static MQTTSettingsDeserializer DESERIALIZER; - MQTTSettingsService::MQTTSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, MQTT_SETTINGS_SERVICE_PATH, securityManager), - _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, MQTT_SETTINGS_FILE) { + _settingsEndpoint(MQTTSettings::serialize, + MQTTSettings::deserialize, + this, + server, + MQTT_SETTINGS_SERVICE_PATH, + securityManager), + _settingsPersistence(MQTTSettings::serialize, MQTTSettings::deserialize, this, fs, MQTT_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent( std::bind(&MQTTSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), diff --git a/lib/framework/MQTTSettingsService.h b/lib/framework/MQTTSettingsService.h index a08cea07..14e7c324 100644 --- a/lib/framework/MQTTSettingsService.h +++ b/lib/framework/MQTTSettingsService.h @@ -20,6 +20,14 @@ #define MQTT_SETTINGS_SERVICE_DEFAULT_CLEAN_SESSION true #define MQTT_SETTINGS_SERVICE_DEFAULT_MAX_TOPIC_LENGTH 128 +static String generateClientId() { +#ifdef ESP32 + return "esp32-" + String((unsigned long)ESP.getEfuseMac(), HEX); +#elif defined(ESP8266) + return "esp8266-" + String(ESP.getChipId(), HEX); +#endif +} + class MQTTSettings { public: // host and port - if enabled @@ -38,19 +46,8 @@ class MQTTSettings { uint16_t keepAlive; bool cleanSession; uint16_t maxTopicLength; -}; - -static String generateClientId() { -#ifdef ESP32 - return "esp32-" + String((unsigned long)ESP.getEfuseMac(), HEX); -#elif defined(ESP8266) - return "esp8266-" + String(ESP.getChipId(), HEX); -#endif -} -class MQTTSettingsSerializer : public SettingsSerializer { - public: - void serialize(MQTTSettings& settings, JsonObject root) { + static void serialize(MQTTSettings& settings, JsonObject& root) { root["enabled"] = settings.enabled; root["host"] = settings.host; root["port"] = settings.port; @@ -61,11 +58,8 @@ class MQTTSettingsSerializer : public SettingsSerializer { root["clean_session"] = settings.cleanSession; root["max_topic_length"] = settings.maxTopicLength; } -}; -class MQTTSettingsDeserializer : public SettingsDeserializer { - public: - void deserialize(MQTTSettings& settings, JsonObject root) { + static void deserialize(JsonObject& root, MQTTSettings& settings) { settings.enabled = root["enabled"] | MQTT_SETTINGS_SERVICE_DEFAULT_ENABLED; settings.host = root["host"] | MQTT_SETTINGS_SERVICE_DEFAULT_HOST; settings.port = root["port"] | MQTT_SETTINGS_SERVICE_DEFAULT_PORT; diff --git a/lib/framework/NTPSettingsService.cpp b/lib/framework/NTPSettingsService.cpp index e5d37ee8..c9f637b2 100644 --- a/lib/framework/NTPSettingsService.cpp +++ b/lib/framework/NTPSettingsService.cpp @@ -1,11 +1,13 @@ #include -static NTPSettingsSerializer SERIALIZER; -static NTPSettingsDeserializer DESERIALIZER; - NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager), - _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, NTP_SETTINGS_FILE) { + _settingsEndpoint(NTPSettings::serialize, + NTPSettings::deserialize, + this, + server, + NTP_SETTINGS_SERVICE_PATH, + securityManager), + _settingsPersistence(NTPSettings::serialize, NTPSettings::deserialize, this, fs, NTP_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent( std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), diff --git a/lib/framework/NTPSettingsService.h b/lib/framework/NTPSettingsService.h index ad355a27..38db3f72 100644 --- a/lib/framework/NTPSettingsService.h +++ b/lib/framework/NTPSettingsService.h @@ -26,21 +26,15 @@ class NTPSettings { String tzLabel; String tzFormat; String server; -}; -class NTPSettingsSerializer : public SettingsSerializer { - public: - void serialize(NTPSettings& settings, JsonObject root) { + static void serialize(NTPSettings& settings, JsonObject& root) { root["enabled"] = settings.enabled; root["server"] = settings.server; root["tz_label"] = settings.tzLabel; root["tz_format"] = settings.tzFormat; } -}; -class NTPSettingsDeserializer : public SettingsDeserializer { - public: - void deserialize(NTPSettings& settings, JsonObject root) { + static void deserialize(JsonObject& root, NTPSettings& settings) { settings.enabled = root["enabled"] | NTP_SETTINGS_SERVICE_DEFAULT_ENABLED; settings.server = root["server"] | NTP_SETTINGS_SERVICE_DEFAULT_SERVER; settings.tzLabel = root["tz_label"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_LABEL; diff --git a/lib/framework/OTASettingsService.cpp b/lib/framework/OTASettingsService.cpp index dc971881..78daf929 100644 --- a/lib/framework/OTASettingsService.cpp +++ b/lib/framework/OTASettingsService.cpp @@ -1,11 +1,8 @@ #include -static OTASettingsSerializer SERIALIZER; -static OTASettingsDeserializer DESERIALIZER; - OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, OTA_SETTINGS_SERVICE_PATH, securityManager), - _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, OTA_SETTINGS_FILE) { + _settingsEndpoint(OTASettings::serialize, OTASettings::deserialize, this, server, OTA_SETTINGS_SERVICE_PATH, securityManager), + _settingsPersistence(OTASettings::serialize, OTASettings::deserialize, this, fs, OTA_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); diff --git a/lib/framework/OTASettingsService.h b/lib/framework/OTASettingsService.h index ae76949d..ca2fff19 100644 --- a/lib/framework/OTASettingsService.h +++ b/lib/framework/OTASettingsService.h @@ -26,20 +26,14 @@ class OTASettings { bool enabled; int port; String password; -}; -class OTASettingsSerializer : public SettingsSerializer { - public: - void serialize(OTASettings& settings, JsonObject root) { + static void serialize(OTASettings& settings, JsonObject& root) { root["enabled"] = settings.enabled; root["port"] = settings.port; root["password"] = settings.password; } -}; -class OTASettingsDeserializer : public SettingsDeserializer { - public: - void deserialize(OTASettings& settings, JsonObject root) { + static void deserialize(JsonObject& root, OTASettings& settings) { settings.enabled = root["enabled"] | DEFAULT_OTA_ENABLED; settings.port = root["port"] | DEFAULT_OTA_PORT; settings.password = root["password"] | DEFAULT_OTA_PASSWORD; diff --git a/lib/framework/SecuritySettingsService.cpp b/lib/framework/SecuritySettingsService.cpp index cc2efa0d..e0be0c61 100644 --- a/lib/framework/SecuritySettingsService.cpp +++ b/lib/framework/SecuritySettingsService.cpp @@ -1,11 +1,13 @@ #include -static SecuritySettingsSerializer SERIALIZER; -static SecuritySettingsDeserializer DESERIALIZER; - SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : - _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, SECURITY_SETTINGS_PATH, this), - _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, SECURITY_SETTINGS_FILE) { + _settingsEndpoint(SecuritySettings::serialize, + SecuritySettings::deserialize, + this, + server, + SECURITY_SETTINGS_PATH, + this), + _settingsPersistence(SecuritySettings::serialize, SecuritySettings::deserialize, this, fs, SECURITY_SETTINGS_FILE) { addUpdateHandler([&](String originId) { configureJWTHandler(); }, false); } diff --git a/lib/framework/SecuritySettingsService.h b/lib/framework/SecuritySettingsService.h index b1eaf5ed..d4973577 100644 --- a/lib/framework/SecuritySettingsService.h +++ b/lib/framework/SecuritySettingsService.h @@ -15,11 +15,8 @@ class SecuritySettings { public: String jwtSecret; std::list users; -}; -class SecuritySettingsSerializer : public SettingsSerializer { - public: - void serialize(SecuritySettings& settings, JsonObject root) { + static void serialize(SecuritySettings& settings, JsonObject& root) { // secret root["jwt_secret"] = settings.jwtSecret; @@ -32,11 +29,8 @@ class SecuritySettingsSerializer : public SettingsSerializer { userRoot["admin"] = user.admin; } } -}; -class SecuritySettingsDeserializer : public SettingsDeserializer { - public: - void deserialize(SecuritySettings& settings, JsonObject root) { + static void deserialize(JsonObject& root, SecuritySettings& settings) { // secret settings.jwtSecret = root["jwt_secret"] | DEFAULT_JWT_SECRET; diff --git a/lib/framework/SettingsBroker.h b/lib/framework/SettingsBroker.h index 4b33fea7..bd4894d4 100644 --- a/lib/framework/SettingsBroker.h +++ b/lib/framework/SettingsBroker.h @@ -25,8 +25,8 @@ template class SettingsBroker { public: - SettingsBroker(SettingsSerializer* settingsSerializer, - SettingsDeserializer* settingsDeserializer, + SettingsBroker(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, SettingsService* settingsService, AsyncMqttClient* mqttClient, String setTopic = "", @@ -64,8 +64,8 @@ class SettingsBroker { } private: - SettingsSerializer* _settingsSerializer; - SettingsDeserializer* _settingsDeserializer; + SettingsSerializer _settingsSerializer; + SettingsDeserializer _settingsDeserializer; SettingsService* _settingsService; AsyncMqttClient* _mqttClient; String _setTopic; @@ -75,7 +75,10 @@ class SettingsBroker { if (_stateTopic.length() > 0 && _mqttClient->connected()) { // serialize to json doc DynamicJsonDocument json(MAX_MESSAGE_SIZE); - _settingsService->read([&](T& settings) { _settingsSerializer->serialize(settings, json.to()); }); + _settingsService->read([&](T& settings) { + JsonObject jsonObject = json.to(); + _settingsSerializer(settings, jsonObject); + }); // serialize to string String payload; @@ -102,7 +105,10 @@ class SettingsBroker { DeserializationError error = deserializeJson(json, payload, len); if (!error && json.is()) { _settingsService->update( - [&](T& settings) { _settingsDeserializer->deserialize(settings, json.as()); }, + [&](T& settings) { + JsonObject jsonObject = json.as(); + _settingsDeserializer(jsonObject, settings); + }, SETTINGS_BROKER_ORIGIN_ID); } } diff --git a/lib/framework/SettingsDeserializer.h b/lib/framework/SettingsDeserializer.h index 8ca40916..80876171 100644 --- a/lib/framework/SettingsDeserializer.h +++ b/lib/framework/SettingsDeserializer.h @@ -4,9 +4,7 @@ #include template -class SettingsDeserializer { - public: - virtual void deserialize(T& settings, JsonObject root) = 0; -}; +using SettingsDeserializer = void (*)(JsonObject& root, T& settings); + #endif // end SettingsDeserializer diff --git a/lib/framework/SettingsEndpoint.h b/lib/framework/SettingsEndpoint.h index 249c83a7..64c552c5 100644 --- a/lib/framework/SettingsEndpoint.h +++ b/lib/framework/SettingsEndpoint.h @@ -17,8 +17,8 @@ template class SettingsEndpoint { public: - SettingsEndpoint(SettingsSerializer* settingsSerializer, - SettingsDeserializer* settingsDeserializer, + SettingsEndpoint(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, SettingsService* settingsManager, AsyncWebServer* server, const String& servicePath, @@ -41,8 +41,8 @@ class SettingsEndpoint { server->addHandler(&_updateHandler); } - SettingsEndpoint(SettingsSerializer* settingsSerializer, - SettingsDeserializer* settingsDeserializer, + SettingsEndpoint(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, SettingsService* settingsManager, AsyncWebServer* server, const String& servicePath) : @@ -58,15 +58,17 @@ class SettingsEndpoint { } protected: - SettingsSerializer* _settingsSerializer; - SettingsDeserializer* _settingsDeserializer; + SettingsSerializer _settingsSerializer; + SettingsDeserializer _settingsDeserializer; SettingsService* _settingsService; AsyncCallbackJsonWebHandler _updateHandler; void fetchSettings(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); - _settingsService->read( - [&](T& settings) { _settingsSerializer->serialize(settings, response->getRoot().to()); }); + _settingsService->read([&](T& settings) { + JsonObject jsonObject = response->getRoot().to(); + _settingsSerializer(settings, jsonObject); + }); response->setLength(); request->send(response); } @@ -80,8 +82,10 @@ class SettingsEndpoint { // update the settings, deferring the call to the update handlers to when the response is complete _settingsService->updateWithoutPropagation([&](T& settings) { - _settingsDeserializer->deserialize(settings, json.as()); - _settingsSerializer->serialize(settings, response->getRoot().as()); + JsonObject jsonObject = json.as(); + _settingsDeserializer(jsonObject, settings); + jsonObject = response->getRoot().to(); + _settingsSerializer(settings, jsonObject); }); // write the response to the client diff --git a/lib/framework/SettingsPersistence.h b/lib/framework/SettingsPersistence.h index 636d964b..28ec18cb 100644 --- a/lib/framework/SettingsPersistence.h +++ b/lib/framework/SettingsPersistence.h @@ -17,8 +17,8 @@ template class SettingsPersistence { public: - SettingsPersistence(SettingsSerializer* settingsSerializer, - SettingsDeserializer* settingsDeserializer, + SettingsPersistence(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, SettingsService* settingsManager, FS* fs, char const* filePath) : @@ -54,8 +54,10 @@ class SettingsPersistence { bool writeToFS() { // create and populate a new json object DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); - _settingsService->read( - [&](T& settings) { _settingsSerializer->serialize(settings, jsonDocument.to()); }); + _settingsService->read([&](T& settings) { + JsonObject jsonObject = jsonDocument.to(); + _settingsSerializer(settings, jsonObject); + }); // serialize it to filesystem File settingsFile = _fs->open(_filePath, "w"); @@ -85,8 +87,8 @@ class SettingsPersistence { } private: - SettingsSerializer* _settingsSerializer; - SettingsDeserializer* _settingsDeserializer; + SettingsSerializer _settingsSerializer; + SettingsDeserializer _settingsDeserializer; SettingsService* _settingsService; FS* _fs; char const* _filePath; @@ -94,7 +96,7 @@ class SettingsPersistence { // read the settings, but do not call propogate void readSettings(JsonObject root) { - _settingsService->read([&](T& settings) { _settingsDeserializer->deserialize(settings, root); }); + _settingsService->read([&](T& settings) { _settingsDeserializer(root, settings); }); } protected: diff --git a/lib/framework/SettingsSerializer.h b/lib/framework/SettingsSerializer.h index 25dab3ea..1586a309 100644 --- a/lib/framework/SettingsSerializer.h +++ b/lib/framework/SettingsSerializer.h @@ -4,9 +4,6 @@ #include template -class SettingsSerializer { - public: - virtual void serialize(T& settings, JsonObject root) = 0; -}; +using SettingsSerializer = void (*)(T& settings, JsonObject& root); #endif // end SettingsSerializer diff --git a/lib/framework/SettingsSocket.h b/lib/framework/SettingsSocket.h index 69e45eda..a7008530 100644 --- a/lib/framework/SettingsSocket.h +++ b/lib/framework/SettingsSocket.h @@ -18,8 +18,8 @@ template class SettingsSocket { public: - SettingsSocket(SettingsSerializer* settingsSerializer, - SettingsDeserializer* settingsDeserializer, + SettingsSocket(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, SettingsService* settingsService, AsyncWebServer* server, char const* socketPath, @@ -44,8 +44,8 @@ class SettingsSocket { _server->on(socketPath, HTTP_GET, std::bind(&SettingsSocket::forbidden, this, std::placeholders::_1)); } - SettingsSocket(SettingsSerializer* settingsSerializer, - SettingsDeserializer* settingsDeserializer, + SettingsSocket(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, SettingsService* settingsService, AsyncWebServer* server, char const* socketPath) : @@ -67,8 +67,8 @@ class SettingsSocket { } private: - SettingsSerializer* _settingsSerializer; - SettingsDeserializer* _settingsDeserializer; + SettingsSerializer _settingsSerializer; + SettingsDeserializer _settingsDeserializer; SettingsService* _settingsService; AsyncWebServer* _server; AsyncWebSocket _webSocket; @@ -102,7 +102,10 @@ class SettingsSocket { DeserializationError error = deserializeJson(jsonDocument, (char*)data); if (!error && jsonDocument.is()) { _settingsService->update( - [&](T& settings) { _settingsDeserializer->deserialize(settings, jsonDocument.as()); }, + [&](T& settings) { + JsonObject jsonObject = jsonDocument.as(); + _settingsDeserializer(jsonObject, settings); + }, clientId(client)); } } @@ -140,7 +143,7 @@ class SettingsSocket { root["type"] = "payload"; root["origin_id"] = originId; JsonObject payload = root.createNestedObject("payload"); - _settingsService->read([&](T& settings) { _settingsSerializer->serialize(settings, payload); }); + _settingsService->read([&](T& settings) { _settingsSerializer(settings, payload); }); size_t len = measureJson(jsonDocument); AsyncWebSocketMessageBuffer* buffer = _webSocket.makeBuffer(len); diff --git a/lib/framework/WiFiSettingsService.cpp b/lib/framework/WiFiSettingsService.cpp index d463690e..96df66c1 100644 --- a/lib/framework/WiFiSettingsService.cpp +++ b/lib/framework/WiFiSettingsService.cpp @@ -1,11 +1,13 @@ #include -static WiFiSettingsSerializer SERIALIZER; -static WiFiSettingsDeserializer DESERIALIZER; - WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(&SERIALIZER, &DESERIALIZER, this, server, WIFI_SETTINGS_SERVICE_PATH, securityManager), - _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, WIFI_SETTINGS_FILE) { + _settingsEndpoint(WiFiSettings::serialize, + WiFiSettings::deserialize, + this, + server, + WIFI_SETTINGS_SERVICE_PATH, + securityManager), + _settingsPersistence(WiFiSettings::serialize, WiFiSettings::deserialize, this, fs, WIFI_SETTINGS_FILE) { // We want the device to come up in opmode=0 (WIFI_OFF), when erasing the flash this is not the default. // If needed, we save opmode=0 before disabling persistence so the device boots with WiFi disabled in the future. if (WiFi.getMode() != WIFI_OFF) { diff --git a/lib/framework/WiFiSettingsService.h b/lib/framework/WiFiSettingsService.h index e520c984..99690c2d 100644 --- a/lib/framework/WiFiSettingsService.h +++ b/lib/framework/WiFiSettingsService.h @@ -23,11 +23,8 @@ class WiFiSettings { IPAddress subnetMask; IPAddress dnsIP1; IPAddress dnsIP2; -}; -class WiFiSettingsSerializer : public SettingsSerializer { - public: - void serialize(WiFiSettings& settings, JsonObject root) { + static void serialize(WiFiSettings& settings, JsonObject& root) { // connection settings root["ssid"] = settings.ssid; root["password"] = settings.password; @@ -41,11 +38,8 @@ class WiFiSettingsSerializer : public SettingsSerializer { JsonUtils::writeIP(root, "dns_ip_1", settings.dnsIP1); JsonUtils::writeIP(root, "dns_ip_2", settings.dnsIP2); } -}; -class WiFiSettingsDeserializer : public SettingsDeserializer { - public: - void deserialize(WiFiSettings& settings, JsonObject root) { + static void deserialize(JsonObject& root, WiFiSettings& settings) { settings.ssid = root["ssid"] | ""; settings.password = root["password"] | ""; settings.hostname = root["hostname"] | ""; diff --git a/src/LightBrokerSettingsService.cpp b/src/LightBrokerSettingsService.cpp index 97e13f07..793b7446 100644 --- a/src/LightBrokerSettingsService.cpp +++ b/src/LightBrokerSettingsService.cpp @@ -1,19 +1,20 @@ #include -static LightBrokerSettingsSerializer SERIALIZER; -static LightBrokerSettingsDeserializer DESERIALIZER; - LightBrokerSettingsService::LightBrokerSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(&SERIALIZER, - &DESERIALIZER, + _settingsEndpoint(LightBrokerSettings::serialize, + LightBrokerSettings::deserialize, this, server, LIGHT_BROKER_SETTINGS_PATH, securityManager, AuthenticationPredicates::IS_AUTHENTICATED), - _settingsPersistence(&SERIALIZER, &DESERIALIZER, this, fs, LIGHT_BROKER_SETTINGS_FILE) { + _settingsPersistence(LightBrokerSettings::serialize, + LightBrokerSettings::deserialize, + this, + fs, + LIGHT_BROKER_SETTINGS_FILE) { } void LightBrokerSettingsService::begin() { diff --git a/src/LightBrokerSettingsService.h b/src/LightBrokerSettingsService.h index d18d49d9..9b0ddf39 100644 --- a/src/LightBrokerSettingsService.h +++ b/src/LightBrokerSettingsService.h @@ -9,13 +9,6 @@ #define LIGHT_BROKER_SETTINGS_FILE "/config/brokerSettings.json" #define LIGHT_BROKER_SETTINGS_PATH "/rest/brokerSettings" -class LightBrokerSettings { - public: - String mqttPath; - String name; - String uniqueId; -}; - static String defaultDeviceValue(String prefix = "") { #ifdef ESP32 return prefix + String((unsigned long)ESP.getEfuseMac(), HEX); @@ -24,18 +17,19 @@ static String defaultDeviceValue(String prefix = "") { #endif } -class LightBrokerSettingsSerializer : public SettingsSerializer { +class LightBrokerSettings { public: - void serialize(LightBrokerSettings& settings, JsonObject root) { + String mqttPath; + String name; + String uniqueId; + + static void serialize(LightBrokerSettings& settings, JsonObject& root) { root["mqtt_path"] = settings.mqttPath; root["name"] = settings.name; root["unique_id"] = settings.uniqueId; } -}; -class LightBrokerSettingsDeserializer : public SettingsDeserializer { - public: - void deserialize(LightBrokerSettings& settings, JsonObject root) { + static void deserialize(JsonObject& root, LightBrokerSettings& settings) { settings.mqttPath = root["mqtt_path"] | defaultDeviceValue("homeassistant/light/"); settings.name = root["name"] | defaultDeviceValue("light-"); settings.uniqueId = root["unique_id"] | defaultDeviceValue("light-"); diff --git a/src/LightSettingsService.cpp b/src/LightSettingsService.cpp index 79053d8f..90c95d42 100644 --- a/src/LightSettingsService.cpp +++ b/src/LightSettingsService.cpp @@ -1,25 +1,19 @@ #include -static LightSettingsSerializer SERIALIZER; -static LightSettingsDeserializer DESERIALIZER; - -static HomeAssistantSerializer HA_SERIALIZER; -static HomeAssistantDeserializer HA_DESERIALIZER; - LightSettingsService::LightSettingsService(AsyncWebServer* server, SecurityManager* securityManager, AsyncMqttClient* mqttClient, LightBrokerSettingsService* lightBrokerSettingsService) : - _settingsEndpoint(&SERIALIZER, - &DESERIALIZER, + _settingsEndpoint(LightSettings::serialize, + LightSettings::deserialize, this, server, LIGHT_SETTINGS_ENDPOINT_PATH, securityManager, AuthenticationPredicates::IS_AUTHENTICATED), - _settingsBroker(&HA_SERIALIZER, &HA_DESERIALIZER, this, mqttClient), - _settingsSocket(&SERIALIZER, - &DESERIALIZER, + _settingsBroker(LightSettings::haSerialize, LightSettings::haDeserialize, this, mqttClient), + _settingsSocket(LightSettings::serialize, + LightSettings::deserialize, this, server, LIGHT_SETTINGS_SOCKET_PATH, diff --git a/src/LightSettingsService.h b/src/LightSettingsService.h index 5d73e0c9..6646b37c 100644 --- a/src/LightSettingsService.h +++ b/src/LightSettingsService.h @@ -17,46 +17,33 @@ // Note that the built-in LED is on when the pin is low on most NodeMCU boards. // This is because the anode is tied to VCC and the cathode to the GPIO 4 (Arduino pin 2). #ifdef ESP32 - #define LED_ON 0x1 - #define LED_OFF 0x0 +#define LED_ON 0x1 +#define LED_OFF 0x0 #elif defined(ESP8266) - #define LED_ON 0x0 - #define LED_OFF 0x1 +#define LED_ON 0x0 +#define LED_OFF 0x1 #endif - #define LIGHT_SETTINGS_ENDPOINT_PATH "/rest/lightSettings" #define LIGHT_SETTINGS_SOCKET_PATH "/ws/lightSettings" class LightSettings { public: bool ledOn; -}; -class LightSettingsSerializer : public SettingsSerializer { - public: - void serialize(LightSettings& settings, JsonObject root) { + static void serialize(LightSettings& settings, JsonObject& root) { root["led_on"] = settings.ledOn; } -}; -class LightSettingsDeserializer : public SettingsDeserializer { - public: - void deserialize(LightSettings& settings, JsonObject root) { + static void deserialize(JsonObject& root, LightSettings& settings) { settings.ledOn = root["led_on"] | DEFAULT_LED_STATE; } -}; -class HomeAssistantSerializer : public SettingsSerializer { - public: - void serialize(LightSettings& settings, JsonObject root) { + static void haSerialize(LightSettings& settings, JsonObject& root) { root["state"] = settings.ledOn ? ON_STATE : OFF_STATE; } -}; -class HomeAssistantDeserializer : public SettingsDeserializer { - public: - void deserialize(LightSettings& settings, JsonObject root) { + static void haDeserialize(JsonObject& root, LightSettings& settings) { String state = root["state"]; settings.ledOn = strcmp(ON_STATE, state.c_str()) ? false : true; } From 134de74f3a03eb981a3e8d9a4f4669c5aa2095b4 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Fri, 8 May 2020 12:05:47 +0100 Subject: [PATCH 13/35] Allow SettingsService to forward constructor arguments to settings instance --- lib/framework/SettingsService.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/framework/SettingsService.h b/lib/framework/SettingsService.h index ead713ec..8b2c02c7 100644 --- a/lib/framework/SettingsService.h +++ b/lib/framework/SettingsService.h @@ -25,8 +25,13 @@ typedef struct SettingsUpdateHandlerInfo { template class SettingsService { public: + template #ifdef ESP32 - SettingsService() : _updateMutex(xSemaphoreCreateRecursiveMutex()) { + SettingsService(Args&&... args) : + _settings(std::forward(args)...), _updateMutex(xSemaphoreCreateRecursiveMutex()) { + } +#else + SettingsService(Args&&... args) : _settings(std::forward(args)...) { } #endif From 518313f61f6f81ca6e1729c08be679c14513783b Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Fri, 8 May 2020 14:11:15 +0100 Subject: [PATCH 14/35] Reduce use of callbacks with overloads for common use-case --- lib/framework/SettingsBroker.h | 14 +++------- lib/framework/SettingsEndpoint.h | 7 +++-- lib/framework/SettingsPersistence.h | 22 +++++++--------- lib/framework/SettingsService.h | 41 ++++++++++++++++++++++++++++- lib/framework/SettingsSocket.h | 10 +++---- 5 files changed, 60 insertions(+), 34 deletions(-) diff --git a/lib/framework/SettingsBroker.h b/lib/framework/SettingsBroker.h index bd4894d4..54b35710 100644 --- a/lib/framework/SettingsBroker.h +++ b/lib/framework/SettingsBroker.h @@ -75,10 +75,8 @@ class SettingsBroker { if (_stateTopic.length() > 0 && _mqttClient->connected()) { // serialize to json doc DynamicJsonDocument json(MAX_MESSAGE_SIZE); - _settingsService->read([&](T& settings) { - JsonObject jsonObject = json.to(); - _settingsSerializer(settings, jsonObject); - }); + JsonObject jsonObject = json.to(); + _settingsService->read(jsonObject, _settingsSerializer); // serialize to string String payload; @@ -104,12 +102,8 @@ class SettingsBroker { DynamicJsonDocument json(MAX_MESSAGE_SIZE); DeserializationError error = deserializeJson(json, payload, len); if (!error && json.is()) { - _settingsService->update( - [&](T& settings) { - JsonObject jsonObject = json.as(); - _settingsDeserializer(jsonObject, settings); - }, - SETTINGS_BROKER_ORIGIN_ID); + JsonObject jsonObject = json.as(); + _settingsService->update(jsonObject, _settingsDeserializer, SETTINGS_BROKER_ORIGIN_ID); } } }; diff --git a/lib/framework/SettingsEndpoint.h b/lib/framework/SettingsEndpoint.h index 64c552c5..44a0be74 100644 --- a/lib/framework/SettingsEndpoint.h +++ b/lib/framework/SettingsEndpoint.h @@ -65,10 +65,9 @@ class SettingsEndpoint { void fetchSettings(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); - _settingsService->read([&](T& settings) { - JsonObject jsonObject = response->getRoot().to(); - _settingsSerializer(settings, jsonObject); - }); + JsonObject jsonObject = response->getRoot().to(); + _settingsService->read(jsonObject, _settingsSerializer); + response->setLength(); request->send(response); } diff --git a/lib/framework/SettingsPersistence.h b/lib/framework/SettingsPersistence.h index 28ec18cb..0f414cd7 100644 --- a/lib/framework/SettingsPersistence.h +++ b/lib/framework/SettingsPersistence.h @@ -38,7 +38,7 @@ class SettingsPersistence { DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); DeserializationError error = deserializeJson(jsonDocument, settingsFile); if (error == DeserializationError::Ok && jsonDocument.is()) { - readSettings(jsonDocument.as()); + updateSettings(jsonDocument.as()); settingsFile.close(); return; } @@ -48,16 +48,14 @@ class SettingsPersistence { // If we reach here we have not been successful in loading the config, // hard-coded emergency defaults are now applied. - readDefaults(); + applyDefaults(); } bool writeToFS() { // create and populate a new json object DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); - _settingsService->read([&](T& settings) { - JsonObject jsonObject = jsonDocument.to(); - _settingsSerializer(settings, jsonObject); - }); + JsonObject jsonObject = jsonDocument.to(); + _settingsService->read(jsonObject, _settingsSerializer); // serialize it to filesystem File settingsFile = _fs->open(_filePath, "w"); @@ -94,17 +92,17 @@ class SettingsPersistence { char const* _filePath; update_handler_id_t _updateHandlerId = 0; - // read the settings, but do not call propogate - void readSettings(JsonObject root) { - _settingsService->read([&](T& settings) { _settingsDeserializer(root, settings); }); + // update the settings, but do not call propogate + void updateSettings(JsonObject root) { + _settingsService->updateWithoutPropagation(root, _settingsDeserializer); } protected: - // We assume the readFromJsonObject supplies sensible defaults if an empty object + // We assume the deserializer supplies sensible defaults if an empty object // is supplied, this virtual function allows that to be changed. - virtual void readDefaults() { + virtual void applyDefaults() { DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); - readSettings(jsonDocument.to()); + updateSettings(jsonDocument.to()); } }; diff --git a/lib/framework/SettingsService.h b/lib/framework/SettingsService.h index 8b2c02c7..111db425 100644 --- a/lib/framework/SettingsService.h +++ b/lib/framework/SettingsService.h @@ -2,6 +2,8 @@ #define SettingsService_h #include +#include +#include #include #include @@ -55,7 +57,23 @@ class SettingsService { } void updateWithoutPropagation(std::function callback) { - read(callback); +#ifdef ESP32 + xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); +#endif + callback(_settings); +#ifdef ESP32 + xSemaphoreGiveRecursive(_updateMutex); +#endif + } + + void updateWithoutPropagation(JsonObject& jsonObject, SettingsDeserializer deserializer) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); +#endif + deserializer(jsonObject, _settings); +#ifdef ESP32 + xSemaphoreGiveRecursive(_updateMutex); +#endif } void update(std::function callback, String originId) { @@ -69,6 +87,17 @@ class SettingsService { #endif } + void update(JsonObject& jsonObject, SettingsDeserializer deserializer, String originId) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); +#endif + deserializer(jsonObject, _settings); + callUpdateHandlers(originId); +#ifdef ESP32 + xSemaphoreGiveRecursive(_updateMutex); +#endif + } + void read(std::function callback) { #ifdef ESP32 xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); @@ -79,6 +108,16 @@ class SettingsService { #endif } + void read(JsonObject& jsonObject, SettingsSerializer serializer) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); +#endif + serializer(_settings, jsonObject); +#ifdef ESP32 + xSemaphoreGiveRecursive(_updateMutex); +#endif + } + void callUpdateHandlers(String originId) { for (const SettingsUpdateHandlerInfo_t& handler : _settingsUpdateHandlers) { handler._cb(originId); diff --git a/lib/framework/SettingsSocket.h b/lib/framework/SettingsSocket.h index a7008530..2a318b12 100644 --- a/lib/framework/SettingsSocket.h +++ b/lib/framework/SettingsSocket.h @@ -101,12 +101,8 @@ class SettingsSocket { DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SOCKET_MSG_SIZE); DeserializationError error = deserializeJson(jsonDocument, (char*)data); if (!error && jsonDocument.is()) { - _settingsService->update( - [&](T& settings) { - JsonObject jsonObject = jsonDocument.as(); - _settingsDeserializer(jsonObject, settings); - }, - clientId(client)); + JsonObject jsonObject = jsonDocument.as(); + _settingsService->update(jsonObject, _settingsDeserializer, clientId(client)); } } } @@ -143,7 +139,7 @@ class SettingsSocket { root["type"] = "payload"; root["origin_id"] = originId; JsonObject payload = root.createNestedObject("payload"); - _settingsService->read([&](T& settings) { _settingsSerializer(settings, payload); }); + _settingsService->read(payload, _settingsSerializer); size_t len = measureJson(jsonDocument); AsyncWebSocketMessageBuffer* buffer = _webSocket.makeBuffer(len); From 18d6adcf8f17532e58b2988b330c9205e5d1c7ac Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Fri, 8 May 2020 14:23:42 +0100 Subject: [PATCH 15/35] document new overloaded functions --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index bcd3271f..5136b083 100644 --- a/README.md +++ b/README.md @@ -413,6 +413,22 @@ class LightSettings { }; ``` +For convenience, the SettingsService class provides overloads of its `update` and `read` functions which utilize these functions. + +Copy the settings to a JsonObject using a serializer: + +```cpp +JsonObject jsonObject = jsonDocument.to(); +lightSettingsService->read(jsonObject, serializer); +``` + +Update the settings from a JsonObject using a deserializer: + +```cpp +JsonObject jsonObject = jsonDocument.as(); +lightSettingsService->update(jsonObject, deserializer, "timer"); +``` + #### Endpoints 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. From 94c5ef4d569d05f9d353217538a3328eff0616f6 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sat, 9 May 2020 15:12:03 +0100 Subject: [PATCH 16/35] fix argument names --- lib/framework/SettingsEndpoint.h | 8 ++++---- lib/framework/SettingsPersistence.h | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/framework/SettingsEndpoint.h b/lib/framework/SettingsEndpoint.h index 44a0be74..d54901f0 100644 --- a/lib/framework/SettingsEndpoint.h +++ b/lib/framework/SettingsEndpoint.h @@ -19,14 +19,14 @@ class SettingsEndpoint { public: SettingsEndpoint(SettingsSerializer settingsSerializer, SettingsDeserializer settingsDeserializer, - SettingsService* settingsManager, + SettingsService* settingsService, AsyncWebServer* server, const String& servicePath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : _settingsSerializer(settingsSerializer), _settingsDeserializer(settingsDeserializer), - _settingsService(settingsManager), + _settingsService(settingsService), _updateHandler( servicePath, securityManager->wrapCallback( @@ -43,12 +43,12 @@ class SettingsEndpoint { SettingsEndpoint(SettingsSerializer settingsSerializer, SettingsDeserializer settingsDeserializer, - SettingsService* settingsManager, + SettingsService* settingsService, AsyncWebServer* server, const String& servicePath) : _settingsSerializer(settingsSerializer), _settingsDeserializer(settingsDeserializer), - _settingsService(settingsManager), + _settingsService(settingsService), _updateHandler(servicePath, std::bind(&SettingsEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2)) { server->on(servicePath.c_str(), HTTP_GET, std::bind(&SettingsEndpoint::fetchSettings, this, std::placeholders::_1)); diff --git a/lib/framework/SettingsPersistence.h b/lib/framework/SettingsPersistence.h index 0f414cd7..43448c15 100644 --- a/lib/framework/SettingsPersistence.h +++ b/lib/framework/SettingsPersistence.h @@ -19,12 +19,12 @@ class SettingsPersistence { public: SettingsPersistence(SettingsSerializer settingsSerializer, SettingsDeserializer settingsDeserializer, - SettingsService* settingsManager, + SettingsService* settingsService, FS* fs, char const* filePath) : _settingsSerializer(settingsSerializer), _settingsDeserializer(settingsDeserializer), - _settingsService(settingsManager), + _settingsService(settingsService), _fs(fs), _filePath(filePath) { enableUpdateHandler(); From 314e6b19788c881c9d856102e3b2730a2fee3bb1 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sat, 9 May 2020 23:07:03 +0100 Subject: [PATCH 17/35] Introduce separate MQTT pub/sub, HTTP get/post and WebSocket rx/tx classes Experiment with alternative naming --- lib/framework/FSPersistence.h | 103 ++++++++++++ lib/framework/HttpEndpoint.h | 167 ++++++++++++++++++++ lib/framework/MqttPubSub.h | 161 +++++++++++++++++++ lib/framework/WebSocketTxRx.h | 242 +++++++++++++++++++++++++++++ src/LightBrokerSettingsService.cpp | 26 ++-- src/LightBrokerSettingsService.h | 9 +- src/LightSettingsService.cpp | 32 ++-- src/LightSettingsService.h | 13 +- 8 files changed, 713 insertions(+), 40 deletions(-) create mode 100644 lib/framework/FSPersistence.h create mode 100644 lib/framework/HttpEndpoint.h create mode 100644 lib/framework/MqttPubSub.h create mode 100644 lib/framework/WebSocketTxRx.h diff --git a/lib/framework/FSPersistence.h b/lib/framework/FSPersistence.h new file mode 100644 index 00000000..cefaa947 --- /dev/null +++ b/lib/framework/FSPersistence.h @@ -0,0 +1,103 @@ +#ifndef FSPersistence_h +#define FSPersistence_h + +#include +#include +#include +#include + +#define MAX_FILE_SIZE 1024 + +template +class FSPersistence { + public: + FSPersistence(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + FS* fs, + char const* filePath) : + _settingsSerializer(settingsSerializer), + _settingsDeserializer(settingsDeserializer), + _settingsService(settingsService), + _fs(fs), + _filePath(filePath) { + enableUpdateHandler(); + } + + void readFromFS() { + File settingsFile = _fs->open(_filePath, "r"); + + if (settingsFile) { + if (settingsFile.size() <= MAX_FILE_SIZE) { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); + DeserializationError error = deserializeJson(jsonDocument, settingsFile); + if (error == DeserializationError::Ok && jsonDocument.is()) { + updateSettings(jsonDocument.as()); + settingsFile.close(); + return; + } + } + settingsFile.close(); + } + + // If we reach here we have not been successful in loading the config, + // hard-coded emergency defaults are now applied. + applyDefaults(); + } + + bool writeToFS() { + // create and populate a new json object + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); + JsonObject jsonObject = jsonDocument.to(); + _settingsService->read(jsonObject, _settingsSerializer); + + // serialize it to filesystem + File settingsFile = _fs->open(_filePath, "w"); + + // failed to open file, return false + if (!settingsFile) { + return false; + } + + // serialize the data to the file + serializeJson(jsonDocument, settingsFile); + settingsFile.close(); + return true; + } + + void disableUpdateHandler() { + if (_updateHandlerId) { + _settingsService->removeUpdateHandler(_updateHandlerId); + _updateHandlerId = 0; + } + } + + void enableUpdateHandler() { + if (!_updateHandlerId) { + _updateHandlerId = _settingsService->addUpdateHandler([&](String originId) { writeToFS(); }); + } + } + + private: + SettingsSerializer _settingsSerializer; + SettingsDeserializer _settingsDeserializer; + SettingsService* _settingsService; + FS* _fs; + char const* _filePath; + update_handler_id_t _updateHandlerId = 0; + + // update the settings, but do not call propogate + void updateSettings(JsonObject root) { + _settingsService->updateWithoutPropagation(root, _settingsDeserializer); + } + + protected: + // We assume the deserializer supplies sensible defaults if an empty object + // is supplied, this virtual function allows that to be changed. + virtual void applyDefaults() { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); + updateSettings(jsonDocument.to()); + } +}; + +#endif // end FSPersistence diff --git a/lib/framework/HttpEndpoint.h b/lib/framework/HttpEndpoint.h new file mode 100644 index 00000000..3f5cf6fe --- /dev/null +++ b/lib/framework/HttpEndpoint.h @@ -0,0 +1,167 @@ +#ifndef HttpEndpoint_h +#define HttpEndpoint_h + +#include + +#include +#include + +#include +#include +#include +#include + +#define MAX_CONTENT_LENGTH 1024 +#define HTTP_ENDPOINT_ORIGIN_ID "http" + +template +class HttpGetEndpoint { + public: + HttpGetEndpoint(SettingsSerializer settingsSerializer, + SettingsService* settingsService, + AsyncWebServer* server, + const String& servicePath, + SecurityManager* securityManager, + AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : + _settingsSerializer(settingsSerializer), _settingsService(settingsService) { + server->on(servicePath.c_str(), + HTTP_GET, + securityManager->wrapRequest(std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1), + authenticationPredicate)); + } + + HttpGetEndpoint(SettingsSerializer settingsSerializer, + SettingsService* settingsService, + AsyncWebServer* server, + const String& servicePath) : + _settingsSerializer(settingsSerializer), _settingsService(settingsService) { + server->on(servicePath.c_str(), HTTP_GET, std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1)); + } + + protected: + SettingsSerializer _settingsSerializer; + SettingsService* _settingsService; + + void fetchSettings(AsyncWebServerRequest* request) { + AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); + JsonObject jsonObject = response->getRoot().to(); + _settingsService->read(jsonObject, _settingsSerializer); + + response->setLength(); + request->send(response); + } +}; + +template +class HttpPostEndpoint { + public: + HttpPostEndpoint(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + AsyncWebServer* server, + const String& servicePath, + SecurityManager* securityManager, + AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : + _settingsSerializer(settingsSerializer), + _settingsDeserializer(settingsDeserializer), + _settingsService(settingsService), + _updateHandler( + servicePath, + securityManager->wrapCallback( + std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2), + authenticationPredicate)) { + _updateHandler.setMethod(HTTP_POST); + _updateHandler.setMaxContentLength(MAX_CONTENT_LENGTH); + server->addHandler(&_updateHandler); + } + + HttpPostEndpoint(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + AsyncWebServer* server, + const String& servicePath) : + _settingsSerializer(settingsSerializer), + _settingsDeserializer(settingsDeserializer), + _settingsService(settingsService), + _updateHandler(servicePath, + std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2)) { + _updateHandler.setMethod(HTTP_POST); + _updateHandler.setMaxContentLength(MAX_CONTENT_LENGTH); + server->addHandler(&_updateHandler); + } + + protected: + SettingsSerializer _settingsSerializer; + SettingsDeserializer _settingsDeserializer; + SettingsService* _settingsService; + AsyncCallbackJsonWebHandler _updateHandler; + + void fetchSettings(AsyncWebServerRequest* request) { + AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); + JsonObject jsonObject = response->getRoot().to(); + _settingsService->read(jsonObject, _settingsSerializer); + + response->setLength(); + request->send(response); + } + + void updateSettings(AsyncWebServerRequest* request, JsonVariant& json) { + if (json.is()) { + AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); + + // use callback to update the settings once the response is complete + request->onDisconnect([this]() { _settingsService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); }); + + // update the settings, deferring the call to the update handlers to when the response is complete + _settingsService->updateWithoutPropagation([&](T& settings) { + JsonObject jsonObject = json.as(); + _settingsDeserializer(jsonObject, settings); + jsonObject = response->getRoot().to(); + _settingsSerializer(settings, jsonObject); + }); + + // write the response to the client + response->setLength(); + request->send(response); + } else { + request->send(400); + } + } +}; + +template +class HttpEndpoint : public HttpGetEndpoint, public HttpPostEndpoint { + public: + HttpEndpoint(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + AsyncWebServer* server, + const String& servicePath, + SecurityManager* securityManager, + AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : + HttpGetEndpoint(settingsSerializer, + settingsService, + server, + servicePath, + securityManager, + authenticationPredicate), + HttpPostEndpoint(settingsSerializer, + settingsDeserializer, + settingsService, + server, + servicePath, + securityManager, + authenticationPredicate) { + } + + HttpEndpoint(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + AsyncWebServer* server, + const String& servicePath) : + HttpGetEndpoint(settingsSerializer, settingsService, server, servicePath), + HttpPostEndpoint(settingsSerializer, settingsDeserializer, settingsService, server, servicePath) { + } +}; + +#endif // end HttpEndpoint diff --git a/lib/framework/MqttPubSub.h b/lib/framework/MqttPubSub.h new file mode 100644 index 00000000..d3eea7a7 --- /dev/null +++ b/lib/framework/MqttPubSub.h @@ -0,0 +1,161 @@ +#ifndef MqttPubSub_h +#define MqttPubSub_h + +#include +#include +#include +#include + +#define MAX_MESSAGE_SIZE 1024 +#define MQTT_ORIGIN_ID "mqtt" + +template +class MqttConnector { + protected: + SettingsService* _settingsService; + AsyncMqttClient* _mqttClient; + + MqttConnector(SettingsService* settingsService, AsyncMqttClient* mqttClient) : + _settingsService(settingsService), _mqttClient(mqttClient) { + _mqttClient->onConnect(std::bind(&MqttConnector::onConnect, this)); + } + + virtual void onConnect() = 0; +}; + +template +class MqttPub : virtual public MqttConnector { + public: + MqttPub(SettingsSerializer settingsSerializer, + SettingsService* settingsService, + AsyncMqttClient* mqttClient, + String pubTopic = "") : + MqttConnector(settingsService, mqttClient), _settingsSerializer(settingsSerializer), _pubTopic(pubTopic) { + MqttConnector::_settingsService->addUpdateHandler([&](String originId) { publish(); }, false); + } + + void setPubTopic(String pubTopic) { + _pubTopic = pubTopic; + publish(); + } + + protected: + virtual void onConnect() { + publish(); + } + + private: + SettingsSerializer _settingsSerializer; + String _pubTopic; + + void publish() { + if (_pubTopic.length() > 0 && MqttConnector::_mqttClient->connected()) { + // serialize to json doc + DynamicJsonDocument json(MAX_MESSAGE_SIZE); + JsonObject jsonObject = json.to(); + MqttConnector::_settingsService->read(jsonObject, _settingsSerializer); + + // serialize to string + String payload; + serializeJson(json, payload); + + // publish the payload + MqttConnector::_mqttClient->publish(_pubTopic.c_str(), 0, false, payload.c_str()); + } + } +}; + +template +class MqttSub : virtual public MqttConnector { + public: + MqttSub(SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + AsyncMqttClient* mqttClient, + String subTopic = "") : + MqttConnector(settingsService, mqttClient), _settingsDeserializer(settingsDeserializer), _subTopic(subTopic) { + MqttConnector::_mqttClient->onMessage(std::bind(&MqttSub::onMqttMessage, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5, + std::placeholders::_6)); + } + + void setSubTopic(String subTopic) { + if (!_subTopic.equals(subTopic)) { + // unsubscribe from the existing topic if one was set + if (_subTopic.length() > 0) { + MqttConnector::_mqttClient->unsubscribe(_subTopic.c_str()); + } + // set the new topic and re-configure the subscription + _subTopic = subTopic; + subscribe(); + } + } + + protected: + virtual void onConnect() { + subscribe(); + } + + private: + SettingsDeserializer _settingsDeserializer; + String _subTopic; + + void subscribe() { + if (_subTopic.length() > 0) { + MqttConnector::_mqttClient->subscribe(_subTopic.c_str(), 2); + } + } + + void onMqttMessage(char* topic, + char* payload, + AsyncMqttClientMessageProperties properties, + size_t len, + size_t index, + size_t total) { + // we only care about the topic we are watching in this class + if (strcmp(_subTopic.c_str(), topic)) { + return; + } + + // deserialize from string + DynamicJsonDocument json(MAX_MESSAGE_SIZE); + DeserializationError error = deserializeJson(json, payload, len); + if (!error && json.is()) { + JsonObject jsonObject = json.as(); + MqttConnector::_settingsService->update(jsonObject, _settingsDeserializer, MQTT_ORIGIN_ID); + } + } +}; + +template +class MqttPubSub : public MqttPub, public MqttSub { + public: + MqttPubSub(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + AsyncMqttClient* mqttClient, + String pubTopic = "", + String subTopic = "") : + MqttConnector(settingsService, mqttClient), + MqttPub(settingsSerializer, settingsService, mqttClient, pubTopic = ""), + MqttSub(settingsDeserializer, settingsService, mqttClient, subTopic = "") { + } + + public: + void configureTopics(String pubTopic, String subTopic) { + MqttSub::setSubTopic(subTopic); + MqttPub::setPubTopic(pubTopic); + } + + protected: + void onConnect() { + MqttSub::onConnect(); + MqttPub::onConnect(); + } +}; + +#endif // end MqttPubSub diff --git a/lib/framework/WebSocketTxRx.h b/lib/framework/WebSocketTxRx.h new file mode 100644 index 00000000..7e2e117f --- /dev/null +++ b/lib/framework/WebSocketTxRx.h @@ -0,0 +1,242 @@ +#ifndef WebSocketTxRx_h +#define WebSocketTxRx_h + +#include +#include +#include +#include + +#define WEB_SOCKET_MSG_SIZE 1024 +#define WEB_SOCKET_CLIENT_ID_MSG_SIZE 128 + +#define WEB_SOCKET_ORIGIN "websocket" +#define WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX "websocket:" + +template +class WebSocketConnector { + protected: + SettingsService* _settingsService; + AsyncWebServer* _server; + AsyncWebSocket _webSocket; + + WebSocketConnector(SettingsService* settingsService, + AsyncWebServer* server, + char const* socketPath, + SecurityManager* securityManager, + AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : + _settingsService(settingsService), _server(server), _webSocket(socketPath) { + _webSocket.setFilter(securityManager->filterRequest(authenticationPredicate)); + _webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5, + std::placeholders::_6)); + _server->addHandler(&_webSocket); + _server->on(socketPath, HTTP_GET, std::bind(&WebSocketConnector::forbidden, this, std::placeholders::_1)); + } + + WebSocketConnector(SettingsService* settingsService, AsyncWebServer* server, char const* socketPath) : + _settingsService(settingsService), _server(server), _webSocket(socketPath) { + _webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5, + std::placeholders::_6)); + _server->addHandler(&_webSocket); + } + + virtual void onWSEvent(AsyncWebSocket* server, + AsyncWebSocketClient* client, + AwsEventType type, + void* arg, + uint8_t* data, + size_t len) = 0; + + String clientId(AsyncWebSocketClient* client) { + return WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX + String(client->id()); + } + + private: + void forbidden(AsyncWebServerRequest* request) { + request->send(403); + } +}; + +template +class WebSocketTx : virtual public WebSocketConnector { + public: + WebSocketTx(SettingsSerializer settingsSerializer, + SettingsService* settingsService, + AsyncWebServer* server, + char const* socketPath, + SecurityManager* securityManager, + AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : + WebSocketConnector(settingsService, server, socketPath, securityManager, authenticationPredicate), + _settingsSerializer(settingsSerializer) { + WebSocketConnector::_settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, + false); + } + + WebSocketTx(SettingsSerializer settingsSerializer, + SettingsService* settingsService, + AsyncWebServer* server, + char const* socketPath) : + WebSocketConnector(settingsService, server, socketPath), _settingsSerializer(settingsSerializer) { + WebSocketConnector::_settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, + false); + } + + protected: + virtual void onWSEvent(AsyncWebSocket* server, + AsyncWebSocketClient* client, + AwsEventType type, + void* arg, + uint8_t* data, + size_t len) { + if (type == WS_EVT_CONNECT) { + // when a client connects, we transmit it's id and the current payload + transmitId(client); + transmitData(client, WEB_SOCKET_ORIGIN); + } + } + + private: + SettingsSerializer _settingsSerializer; + + void transmitId(AsyncWebSocketClient* client) { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_CLIENT_ID_MSG_SIZE); + JsonObject root = jsonDocument.to(); + root["type"] = "id"; + root["id"] = WebSocketConnector::clientId(client); + size_t len = measureJson(jsonDocument); + AsyncWebSocketMessageBuffer* buffer = WebSocketConnector::_webSocket.makeBuffer(len); + if (buffer) { + serializeJson(jsonDocument, (char*)buffer->get(), len + 1); + client->text(buffer); + } + } + + /** + * Broadcasts the payload to the destination, if provided. Otherwise broadcasts to all clients except the origin, if + * specified. + * + * Original implementation sent clients their own IDs so they could ignore updates they initiated. This approach + * simplifies the client and the server implementation but may not be sufficent for all use-cases. + */ + void transmitData(AsyncWebSocketClient* client, String originId) { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_MSG_SIZE); + JsonObject root = jsonDocument.to(); + root["type"] = "payload"; + root["origin_id"] = originId; + JsonObject payload = root.createNestedObject("payload"); + WebSocketConnector::_settingsService->read(payload, _settingsSerializer); + + size_t len = measureJson(jsonDocument); + AsyncWebSocketMessageBuffer* buffer = WebSocketConnector::_webSocket.makeBuffer(len); + if (buffer) { + serializeJson(jsonDocument, (char*)buffer->get(), len + 1); + if (client) { + client->text(buffer); + } else { + WebSocketConnector::_webSocket.textAll(buffer); + } + } + } +}; + +template +class WebSocketRx : virtual public WebSocketConnector { + public: + WebSocketRx(SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + AsyncWebServer* server, + char const* socketPath, + SecurityManager* securityManager, + AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : + WebSocketConnector(settingsService, server, socketPath, securityManager, authenticationPredicate), + _settingsDeserializer(settingsDeserializer) { + } + + WebSocketRx(SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + AsyncWebServer* server, + char const* socketPath) : + WebSocketConnector(settingsService, server, socketPath), _settingsDeserializer(settingsDeserializer) { + } + + protected: + virtual void onWSEvent(AsyncWebSocket* server, + AsyncWebSocketClient* client, + AwsEventType type, + void* arg, + uint8_t* data, + size_t len) { + if (type == WS_EVT_DATA) { + AwsFrameInfo* info = (AwsFrameInfo*)arg; + if (info->final && info->index == 0 && info->len == len) { + if (info->opcode == WS_TEXT) { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_MSG_SIZE); + DeserializationError error = deserializeJson(jsonDocument, (char*)data); + if (!error && jsonDocument.is()) { + JsonObject jsonObject = jsonDocument.as(); + WebSocketConnector::_settingsService->update( + jsonObject, _settingsDeserializer, WebSocketConnector::clientId(client)); + } + } + } + } + } + + private: + SettingsDeserializer _settingsDeserializer; +}; + +template +class WebSocketTxRx : public WebSocketTx, public WebSocketRx { + public: + WebSocketTxRx(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + AsyncWebServer* server, + char const* socketPath, + SecurityManager* securityManager, + AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : + WebSocketConnector(settingsService, server, socketPath, securityManager, authenticationPredicate), + WebSocketTx(settingsSerializer, settingsService, server, socketPath, securityManager, authenticationPredicate), + WebSocketRx(settingsDeserializer, + settingsService, + server, + socketPath, + securityManager, + authenticationPredicate) { + } + + WebSocketTxRx(SettingsSerializer settingsSerializer, + SettingsDeserializer settingsDeserializer, + SettingsService* settingsService, + AsyncWebServer* server, + char const* socketPath) : + WebSocketConnector(settingsService, server, socketPath), + WebSocketTx(settingsSerializer, settingsService, server, socketPath), + WebSocketRx(settingsDeserializer, settingsService, server, socketPath) { + } + + protected: + void onWSEvent(AsyncWebSocket* server, + AsyncWebSocketClient* client, + AwsEventType type, + void* arg, + uint8_t* data, + size_t len) { + WebSocketRx::onWSEvent(server, client, type, arg, data, len); + WebSocketTx::onWSEvent(server, client, type, arg, data, len); + } +}; + +#endif diff --git a/src/LightBrokerSettingsService.cpp b/src/LightBrokerSettingsService.cpp index 793b7446..cd46b726 100644 --- a/src/LightBrokerSettingsService.cpp +++ b/src/LightBrokerSettingsService.cpp @@ -3,20 +3,20 @@ LightBrokerSettingsService::LightBrokerSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(LightBrokerSettings::serialize, - LightBrokerSettings::deserialize, - this, - server, - LIGHT_BROKER_SETTINGS_PATH, - securityManager, - AuthenticationPredicates::IS_AUTHENTICATED), - _settingsPersistence(LightBrokerSettings::serialize, - LightBrokerSettings::deserialize, - this, - fs, - LIGHT_BROKER_SETTINGS_FILE) { + _httpEndpoint(LightBrokerSettings::serialize, + LightBrokerSettings::deserialize, + this, + server, + LIGHT_BROKER_SETTINGS_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED), + _fsPersistence(LightBrokerSettings::serialize, + LightBrokerSettings::deserialize, + this, + fs, + LIGHT_BROKER_SETTINGS_FILE) { } void LightBrokerSettingsService::begin() { - _settingsPersistence.readFromFS(); + _fsPersistence.readFromFS(); } diff --git a/src/LightBrokerSettingsService.h b/src/LightBrokerSettingsService.h index 9b0ddf39..4ce92795 100644 --- a/src/LightBrokerSettingsService.h +++ b/src/LightBrokerSettingsService.h @@ -1,9 +1,8 @@ #ifndef LightBrokerSettingsService_h #define LightBrokerSettingsService_h -#include -#include -#include +#include +#include #include #define LIGHT_BROKER_SETTINGS_FILE "/config/brokerSettings.json" @@ -42,8 +41,8 @@ class LightBrokerSettingsService : public SettingsService { void begin(); private: - SettingsEndpoint _settingsEndpoint; - SettingsPersistence _settingsPersistence; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; }; #endif // end LightBrokerSettingsService_h diff --git a/src/LightSettingsService.cpp b/src/LightSettingsService.cpp index 90c95d42..57fcd61a 100644 --- a/src/LightSettingsService.cpp +++ b/src/LightSettingsService.cpp @@ -4,21 +4,21 @@ LightSettingsService::LightSettingsService(AsyncWebServer* server, SecurityManager* securityManager, AsyncMqttClient* mqttClient, LightBrokerSettingsService* lightBrokerSettingsService) : - _settingsEndpoint(LightSettings::serialize, - LightSettings::deserialize, - this, - server, - LIGHT_SETTINGS_ENDPOINT_PATH, - securityManager, - AuthenticationPredicates::IS_AUTHENTICATED), - _settingsBroker(LightSettings::haSerialize, LightSettings::haDeserialize, this, mqttClient), - _settingsSocket(LightSettings::serialize, - LightSettings::deserialize, - this, - server, - LIGHT_SETTINGS_SOCKET_PATH, - securityManager, - AuthenticationPredicates::IS_AUTHENTICATED), + _httpEndpoint(LightSettings::serialize, + LightSettings::deserialize, + this, + server, + LIGHT_SETTINGS_ENDPOINT_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED), + _mqttPubSub(LightSettings::haSerialize, LightSettings::haDeserialize, this, mqttClient), + _webSocket(LightSettings::serialize, + LightSettings::deserialize, + this, + server, + LIGHT_SETTINGS_SOCKET_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED), _mqttClient(mqttClient), _lightBrokerSettingsService(lightBrokerSettingsService) { // configure blink led to be output @@ -69,5 +69,5 @@ void LightSettingsService::registerConfig() { serializeJson(doc, payload); _mqttClient->publish(configTopic.c_str(), 0, false, payload.c_str()); - _settingsBroker.configureBroker(setTopic, stateTopic); + _mqttPubSub.configureTopics(stateTopic, setTopic); } \ No newline at end of file diff --git a/src/LightSettingsService.h b/src/LightSettingsService.h index 6646b37c..3b1d5dc3 100644 --- a/src/LightSettingsService.h +++ b/src/LightSettingsService.h @@ -2,9 +2,10 @@ #define LightSettingsService_h #include -#include -#include -#include + +#include +#include +#include #include #define BLINK_LED 2 @@ -58,9 +59,9 @@ class LightSettingsService : public SettingsService { void begin(); private: - SettingsEndpoint _settingsEndpoint; - SettingsBroker _settingsBroker; - SettingsSocket _settingsSocket; + HttpEndpoint _httpEndpoint; + MqttPubSub _mqttPubSub; + WebSocketTxRx _webSocket; AsyncMqttClient* _mqttClient; LightBrokerSettingsService* _lightBrokerSettingsService; From 66ffb27a4a73c530a3a6b97732b92a4714dc76f1 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sun, 10 May 2020 15:05:30 +0100 Subject: [PATCH 18/35] fix component name and props name --- interface/src/mqtt/MQTT.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/mqtt/MQTT.tsx b/interface/src/mqtt/MQTT.tsx index ee9a7db5..50617a43 100644 --- a/interface/src/mqtt/MQTT.tsx +++ b/interface/src/mqtt/MQTT.tsx @@ -8,9 +8,9 @@ import { MenuAppBar } from '../components'; import MQTTStatusController from './MQTTStatusController'; import MQTTSettingsController from './MQTTSettingsController'; -type AccessPointProps = AuthenticatedContextProps & RouteComponentProps; +type MQTTProps = AuthenticatedContextProps & RouteComponentProps; -class AccessPoint extends Component { +class MQTT extends Component { handleTabChange = (event: React.ChangeEvent<{}>, path: string) => { this.props.history.push(path); @@ -34,4 +34,4 @@ class AccessPoint extends Component { } } -export default withAuthenticatedContext(AccessPoint); +export default withAuthenticatedContext(MQTT); From a511a526329caee869eacbf1bd56ddacb8944230 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sun, 10 May 2020 15:25:48 +0100 Subject: [PATCH 19/35] delegate to mqtt client connected function instead of tracking connected state internally --- lib/framework/MQTTSettingsService.cpp | 4 +--- lib/framework/MQTTSettingsService.h | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/framework/MQTTSettingsService.cpp b/lib/framework/MQTTSettingsService.cpp index 224c2686..44b130c8 100644 --- a/lib/framework/MQTTSettingsService.cpp +++ b/lib/framework/MQTTSettingsService.cpp @@ -48,7 +48,7 @@ bool MQTTSettingsService::isEnabled() { } bool MQTTSettingsService::isConnected() { - return _connected; + return _mqttClient.connected(); } const char* MQTTSettingsService::getClientId() { @@ -67,13 +67,11 @@ void MQTTSettingsService::onMqttConnect(bool sessionPresent) { Serial.print("Connected to MQTT, "); Serial.print(sessionPresent ? "with" : "without"); Serial.println(" persistent session"); - _connected = true; } void MQTTSettingsService::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { Serial.print("Disconnected from MQTT reason: "); Serial.println((uint8_t)reason); - _connected = false; _disconnectReason = reason; _disconnectedAt = millis(); } diff --git a/lib/framework/MQTTSettingsService.h b/lib/framework/MQTTSettingsService.h index 14e7c324..8938f159 100644 --- a/lib/framework/MQTTSettingsService.h +++ b/lib/framework/MQTTSettingsService.h @@ -97,7 +97,6 @@ class MQTTSettingsService : public SettingsService { unsigned long _disconnectedAt; // connection status - bool _connected; AsyncMqttClientDisconnectReason _disconnectReason; #ifdef ESP32 From 61802112527d28ea0fda34d1c71238c573c12b87 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sun, 10 May 2020 16:32:40 +0100 Subject: [PATCH 20/35] Retain copies of the cstr values we pass to AsyncMqttClient --- lib/framework/MQTTSettingsService.cpp | 32 ++++++++++++++++++++++----- lib/framework/MQTTSettingsService.h | 8 +++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/framework/MQTTSettingsService.cpp b/lib/framework/MQTTSettingsService.cpp index 44b130c8..ed21f8fa 100644 --- a/lib/framework/MQTTSettingsService.cpp +++ b/lib/framework/MQTTSettingsService.cpp @@ -1,5 +1,25 @@ #include +/** + * Retains a copy of the cstr provided in the pointer provided using dynamic allocation. + * + * Frees the pointer before allocation and leaves it as nullptr if cstr == nullptr. + */ +static char* retainCstr(const char* cstr, char** ptr) { + // free up previously retained value if exists + free(*ptr); + *ptr = nullptr; + + // dynamically allocate and copy cstr (if non null) + if (cstr != nullptr) { + *ptr = (char*)malloc(strlen(cstr) + 1); + strcpy(*ptr, cstr); + } + + // return reference to pointer for convenience + return *ptr; +} + MQTTSettingsService::MQTTSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : _settingsEndpoint(MQTTSettings::serialize, MQTTSettings::deserialize, @@ -118,15 +138,15 @@ void MQTTSettingsService::configureMQTT() { // only connect if WiFi is connected and MQTT is enabled if (_settings.enabled && WiFi.isConnected()) { Serial.println("Connecting to MQTT..."); - _mqttClient.setServer(_settings.host.c_str(), _settings.port); + _mqttClient.setServer(retainCstr(_settings.host.c_str(), &_retainedHost), _settings.port); if (_settings.username.length() > 0) { - const char* username = _settings.username.c_str(); - const char* password = _settings.password.length() > 0 ? _settings.password.c_str() : nullptr; - _mqttClient.setCredentials(username, password); + _mqttClient.setCredentials( + retainCstr(_settings.username.c_str(), &_retainedUsername), + retainCstr(_settings.password.length() > 0 ? _settings.password.c_str() : nullptr, &_retainedPassword)); } else { - _mqttClient.setCredentials(nullptr, nullptr); + _mqttClient.setCredentials(retainCstr(nullptr, &_retainedUsername), retainCstr(nullptr, &_retainedPassword)); } - _mqttClient.setClientId(_settings.clientId.c_str()); + _mqttClient.setClientId(retainCstr(_settings.clientId.c_str(), &_retainedClientId)); _mqttClient.setKeepAlive(_settings.keepAlive); _mqttClient.setCleanSession(_settings.cleanSession); _mqttClient.setMaxTopicLength(_settings.maxTopicLength); diff --git a/lib/framework/MQTTSettingsService.h b/lib/framework/MQTTSettingsService.h index 8938f159..44b848ae 100644 --- a/lib/framework/MQTTSettingsService.h +++ b/lib/framework/MQTTSettingsService.h @@ -92,6 +92,13 @@ class MQTTSettingsService : public SettingsService { SettingsEndpoint _settingsEndpoint; SettingsPersistence _settingsPersistence; + // Pointers to hold retained copies of the mqtt client connection strings. + // Required as AsyncMqttClient holds refrences to the supplied connection strings. + char* _retainedHost = nullptr; + char* _retainedClientId = nullptr; + char* _retainedUsername = nullptr; + char* _retainedPassword = nullptr; + AsyncMqttClient _mqttClient; bool _reconfigureMqtt; unsigned long _disconnectedAt; @@ -112,6 +119,7 @@ class MQTTSettingsService : public SettingsService { void onMqttConnect(bool sessionPresent); void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); void configureMQTT(); + }; #endif // end MQTTSettingsService_h From e125e46d595b7bce54a9eede0f4e12e253500511 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sun, 10 May 2020 16:41:25 +0100 Subject: [PATCH 21/35] Update comments, remove references to HomeAssistant. --- lib/framework/SettingsBroker.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/framework/SettingsBroker.h b/lib/framework/SettingsBroker.h index 54b35710..2b4a4dba 100644 --- a/lib/framework/SettingsBroker.h +++ b/lib/framework/SettingsBroker.h @@ -10,11 +10,9 @@ #define SETTINGS_BROKER_ORIGIN_ID "broker" /** - * SettingsBroker is designed to operate with Home Assistant and takes care of observing state change requests over MQTT - * and acting on them. + * SettingsBroker is designed to operate on a pair of pub/sub topics. * - * The broker listens to changes on a "set" topic and publish it's state on a "state" topic. It also has - * an optional config topic which can be used for home assistant's auto discovery feature if required. + * The broker listens to changes on a "set" topic and publish its state on a "state" topic. * * Settings are automatically published to the state topic when a connection to the broker is established or when * settings are updated. From 7af7fbab6c9761ad28d82c87db4e350957d86c18 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sun, 10 May 2020 19:43:38 +0100 Subject: [PATCH 22/35] Replace use of SettingsEndpoint, and SettingsPersistence. Rename SettingsDeserializer and SettingsSerializer Remove SettingsEndpoint, SettingsPersistence, SettingsSocket. --- README.md | 24 ++-- lib/framework/APSettingsService.cpp | 6 +- lib/framework/APSettingsService.h | 8 +- lib/framework/FSPersistence.h | 20 +-- lib/framework/HttpEndpoint.h | 60 ++++----- lib/framework/JsonDeserializer.h | 10 ++ lib/framework/JsonSerializer.h | 9 ++ lib/framework/MQTTSettingsService.cpp | 6 +- lib/framework/MQTTSettingsService.h | 8 +- lib/framework/MqttPubSub.h | 28 ++-- lib/framework/NTPSettingsService.cpp | 6 +- lib/framework/NTPSettingsService.h | 8 +- lib/framework/OTASettingsService.cpp | 6 +- lib/framework/OTASettingsService.h | 8 +- lib/framework/SecuritySettingsService.cpp | 6 +- lib/framework/SecuritySettingsService.h | 8 +- lib/framework/SettingsBroker.h | 109 --------------- lib/framework/SettingsDeserializer.h | 10 -- lib/framework/SettingsEndpoint.h | 99 -------------- lib/framework/SettingsPersistence.h | 109 --------------- lib/framework/SettingsSerializer.h | 9 -- lib/framework/SettingsService.h | 10 +- lib/framework/SettingsSocket.h | 156 ---------------------- lib/framework/WebSocketTxRx.h | 44 +++--- lib/framework/WiFiSettingsService.cpp | 6 +- lib/framework/WiFiSettingsService.h | 8 +- 26 files changed, 154 insertions(+), 627 deletions(-) create mode 100644 lib/framework/JsonDeserializer.h create mode 100644 lib/framework/JsonSerializer.h delete mode 100644 lib/framework/SettingsBroker.h delete mode 100644 lib/framework/SettingsDeserializer.h delete mode 100644 lib/framework/SettingsEndpoint.h delete mode 100644 lib/framework/SettingsPersistence.h delete mode 100644 lib/framework/SettingsSerializer.h delete mode 100644 lib/framework/SettingsSocket.h diff --git a/README.md b/README.md index 5136b083..f48660bf 100644 --- a/README.md +++ b/README.md @@ -367,11 +367,11 @@ lightSettingsService.removeUpdateHandler(myUpdateHandler); An "originId" is passed to the update handler which may be used to identify the origin of the update. The default origin values the framework provides are: -Origin | Description ------------------ | ----------- -endpoint | An update over REST (SettingsEndpoint) -broker | An update sent over MQTT (SettingsBroker) -socket:{clientId} | An update sent over WebSocket (SettingsSocket) +Origin | Description +--------------------- | ----------- +http | An update sent over REST (HttpEndpoint) +mqtt | An update sent over MQTT (MqttPubSub) +websocket:{clientId} | An update sent over WebSocket (WebSocketRxTx) 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. @@ -391,7 +391,7 @@ lightSettingsService.update([&](LightSettings& settings) { #### Serialization -When transmitting settings over HTTP, WebSockets, or MQTT they must to be marshalled into a serializable form (JSON). The framework uses ArduinoJson for serialization and the functions defined in [SettingsSerializer.h](lib/framework/SettingsSerializer.h) and [SettingsDeserializer.h](lib/framework/SettingsDeserializer.h) facilitate this. +When transmitting settings over HTTP, WebSockets, or MQTT they must to be marshalled into a serializable form (JSON). The framework uses ArduinoJson for serialization and the functions defined in [JsonSerializer.h](lib/framework/JsonSerializer.h) and [JsonDeserializer.h](lib/framework/JsonDeserializer.h) facilitate this. The static functions below can be used to facilitate the serialization/deserialization of the example settings: @@ -431,7 +431,7 @@ lightSettingsService->update(jsonObject, deserializer, "timer"); #### Endpoints -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 framework provides a [HttpEndpoint.h](lib/framework/HttpEndpoint.h) class which may be used to register GET and POST handlers to read and update the settings over HTTP. You may construct a HttpEndpoint 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: @@ -439,11 +439,11 @@ The code below demonstrates how to extend the LightSettingsService class to prov class LightSettingsService : public SettingsService { public: LightSettingsService(AsyncWebServer* server) : - _settingsEndpoint(LightSettings::serialize, LightSettings::deserialize, this, server, "/rest/lightSettings") { + _httpEndpoint(LightSettings::serialize, LightSettings::deserialize, this, server, "/rest/lightSettings") { } private: - SettingsEndpoint _settingsEndpoint; + HttpEndpoint _httpEndpoint; }; ``` @@ -451,7 +451,7 @@ Endpoint security is provided by authentication predicates which are [documented #### Persistence -[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 `disableUpdateHandler()` if manual control of persistence is required. +[FSPersistence.h](lib/framework/FSPersistence.h) allows you to save settings to the filesystem. FSPersistence automatically writes changes to the file system when settings are updated. This feature can be disabled by calling `disableUpdateHandler()` if manual control of persistence is required. The code below demonstrates how to extend the LightSettingsService class to provide persistence: @@ -459,11 +459,11 @@ The code below demonstrates how to extend the LightSettingsService class to prov class LightSettingsService : public SettingsService { public: LightSettingsService(FS* fs) : - _settingsPersistence(LightSettings::serialize, LightSettings::deserialize, this, fs, "/config/lightSettings.json") { + _fsPersistence(LightSettings::serialize, LightSettings::deserialize, this, fs, "/config/lightSettings.json") { } private: - SettingsPersistence _settingsPersistence; + FSPersistence _fsPersistence; }; ``` diff --git a/lib/framework/APSettingsService.cpp b/lib/framework/APSettingsService.cpp index 641ca972..3215a85b 100644 --- a/lib/framework/APSettingsService.cpp +++ b/lib/framework/APSettingsService.cpp @@ -1,18 +1,18 @@ #include APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(APSettings::serialize, + _httpEndpoint(APSettings::serialize, APSettings::deserialize, this, server, AP_SETTINGS_SERVICE_PATH, securityManager), - _settingsPersistence(APSettings::serialize, APSettings::deserialize, this, fs, AP_SETTINGS_FILE) { + _fsPersistence(APSettings::serialize, APSettings::deserialize, this, fs, AP_SETTINGS_FILE) { addUpdateHandler([&](String originId) { reconfigureAP(); }, false); } void APSettingsService::begin() { - _settingsPersistence.readFromFS(); + _fsPersistence.readFromFS(); reconfigureAP(); } diff --git a/lib/framework/APSettingsService.h b/lib/framework/APSettingsService.h index 6c0905bd..2c11eccc 100644 --- a/lib/framework/APSettingsService.h +++ b/lib/framework/APSettingsService.h @@ -1,8 +1,8 @@ #ifndef APSettingsConfig_h #define APSettingsConfig_h -#include -#include +#include +#include #include #include @@ -56,8 +56,8 @@ class APSettingsService : public SettingsService { void loop(); private: - SettingsEndpoint _settingsEndpoint; - SettingsPersistence _settingsPersistence; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; // for the mangement delay loop unsigned long _lastManaged; diff --git a/lib/framework/FSPersistence.h b/lib/framework/FSPersistence.h index cefaa947..c299b8be 100644 --- a/lib/framework/FSPersistence.h +++ b/lib/framework/FSPersistence.h @@ -2,8 +2,8 @@ #define FSPersistence_h #include -#include -#include +#include +#include #include #define MAX_FILE_SIZE 1024 @@ -11,13 +11,13 @@ template class FSPersistence { public: - FSPersistence(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, + FSPersistence(JsonSerializer jsonSerializer, + JsonDeserializer jsonDeserializer, SettingsService* settingsService, FS* fs, char const* filePath) : - _settingsSerializer(settingsSerializer), - _settingsDeserializer(settingsDeserializer), + _jsonSerializer(jsonSerializer), + _jsonDeserializer(jsonDeserializer), _settingsService(settingsService), _fs(fs), _filePath(filePath) { @@ -49,7 +49,7 @@ class FSPersistence { // create and populate a new json object DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); JsonObject jsonObject = jsonDocument.to(); - _settingsService->read(jsonObject, _settingsSerializer); + _settingsService->read(jsonObject, _jsonSerializer); // serialize it to filesystem File settingsFile = _fs->open(_filePath, "w"); @@ -79,8 +79,8 @@ class FSPersistence { } private: - SettingsSerializer _settingsSerializer; - SettingsDeserializer _settingsDeserializer; + JsonSerializer _jsonSerializer; + JsonDeserializer _jsonDeserializer; SettingsService* _settingsService; FS* _fs; char const* _filePath; @@ -88,7 +88,7 @@ class FSPersistence { // update the settings, but do not call propogate void updateSettings(JsonObject root) { - _settingsService->updateWithoutPropagation(root, _settingsDeserializer); + _settingsService->updateWithoutPropagation(root, _jsonDeserializer); } protected: diff --git a/lib/framework/HttpEndpoint.h b/lib/framework/HttpEndpoint.h index 3f5cf6fe..72d8aaa8 100644 --- a/lib/framework/HttpEndpoint.h +++ b/lib/framework/HttpEndpoint.h @@ -8,8 +8,8 @@ #include #include -#include -#include +#include +#include #define MAX_CONTENT_LENGTH 1024 #define HTTP_ENDPOINT_ORIGIN_ID "http" @@ -17,35 +17,35 @@ template class HttpGetEndpoint { public: - HttpGetEndpoint(SettingsSerializer settingsSerializer, + HttpGetEndpoint(JsonSerializer jsonSerializer, SettingsService* settingsService, AsyncWebServer* server, const String& servicePath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - _settingsSerializer(settingsSerializer), _settingsService(settingsService) { + _jsonSerializer(jsonSerializer), _settingsService(settingsService) { server->on(servicePath.c_str(), HTTP_GET, securityManager->wrapRequest(std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1), authenticationPredicate)); } - HttpGetEndpoint(SettingsSerializer settingsSerializer, + HttpGetEndpoint(JsonSerializer jsonSerializer, SettingsService* settingsService, AsyncWebServer* server, const String& servicePath) : - _settingsSerializer(settingsSerializer), _settingsService(settingsService) { + _jsonSerializer(jsonSerializer), _settingsService(settingsService) { server->on(servicePath.c_str(), HTTP_GET, std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1)); } protected: - SettingsSerializer _settingsSerializer; + JsonSerializer _jsonSerializer; SettingsService* _settingsService; void fetchSettings(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); JsonObject jsonObject = response->getRoot().to(); - _settingsService->read(jsonObject, _settingsSerializer); + _settingsService->read(jsonObject, _jsonSerializer); response->setLength(); request->send(response); @@ -55,15 +55,15 @@ class HttpGetEndpoint { template class HttpPostEndpoint { public: - HttpPostEndpoint(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, + HttpPostEndpoint(JsonSerializer jsonSerializer, + JsonDeserializer jsonDeserializer, SettingsService* settingsService, AsyncWebServer* server, const String& servicePath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - _settingsSerializer(settingsSerializer), - _settingsDeserializer(settingsDeserializer), + _jsonSerializer(jsonSerializer), + _jsonDeserializer(jsonDeserializer), _settingsService(settingsService), _updateHandler( servicePath, @@ -75,13 +75,13 @@ class HttpPostEndpoint { server->addHandler(&_updateHandler); } - HttpPostEndpoint(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, + HttpPostEndpoint(JsonSerializer jsonSerializer, + JsonDeserializer jsonDeserializer, SettingsService* settingsService, AsyncWebServer* server, const String& servicePath) : - _settingsSerializer(settingsSerializer), - _settingsDeserializer(settingsDeserializer), + _jsonSerializer(jsonSerializer), + _jsonDeserializer(jsonDeserializer), _settingsService(settingsService), _updateHandler(servicePath, std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2)) { @@ -91,15 +91,15 @@ class HttpPostEndpoint { } protected: - SettingsSerializer _settingsSerializer; - SettingsDeserializer _settingsDeserializer; + JsonSerializer _jsonSerializer; + JsonDeserializer _jsonDeserializer; SettingsService* _settingsService; AsyncCallbackJsonWebHandler _updateHandler; void fetchSettings(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); JsonObject jsonObject = response->getRoot().to(); - _settingsService->read(jsonObject, _settingsSerializer); + _settingsService->read(jsonObject, _jsonSerializer); response->setLength(); request->send(response); @@ -115,9 +115,9 @@ class HttpPostEndpoint { // update the settings, deferring the call to the update handlers to when the response is complete _settingsService->updateWithoutPropagation([&](T& settings) { JsonObject jsonObject = json.as(); - _settingsDeserializer(jsonObject, settings); + _jsonDeserializer(jsonObject, settings); jsonObject = response->getRoot().to(); - _settingsSerializer(settings, jsonObject); + _jsonSerializer(settings, jsonObject); }); // write the response to the client @@ -132,21 +132,21 @@ class HttpPostEndpoint { template class HttpEndpoint : public HttpGetEndpoint, public HttpPostEndpoint { public: - HttpEndpoint(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, + HttpEndpoint(JsonSerializer jsonSerializer, + JsonDeserializer jsonDeserializer, SettingsService* settingsService, AsyncWebServer* server, const String& servicePath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - HttpGetEndpoint(settingsSerializer, + HttpGetEndpoint(jsonSerializer, settingsService, server, servicePath, securityManager, authenticationPredicate), - HttpPostEndpoint(settingsSerializer, - settingsDeserializer, + HttpPostEndpoint(jsonSerializer, + jsonDeserializer, settingsService, server, servicePath, @@ -154,13 +154,13 @@ class HttpEndpoint : public HttpGetEndpoint, public HttpPostEndpoint { authenticationPredicate) { } - HttpEndpoint(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, + HttpEndpoint(JsonSerializer jsonSerializer, + JsonDeserializer jsonDeserializer, SettingsService* settingsService, AsyncWebServer* server, const String& servicePath) : - HttpGetEndpoint(settingsSerializer, settingsService, server, servicePath), - HttpPostEndpoint(settingsSerializer, settingsDeserializer, settingsService, server, servicePath) { + HttpGetEndpoint(jsonSerializer, settingsService, server, servicePath), + HttpPostEndpoint(jsonSerializer, jsonDeserializer, settingsService, server, servicePath) { } }; diff --git a/lib/framework/JsonDeserializer.h b/lib/framework/JsonDeserializer.h new file mode 100644 index 00000000..c342eabe --- /dev/null +++ b/lib/framework/JsonDeserializer.h @@ -0,0 +1,10 @@ +#ifndef JsonDeserializer_h +#define JsonDeserializer_h + +#include + +template +using JsonDeserializer = void (*)(JsonObject& root, T& settings); + + +#endif // end JsonDeserializer diff --git a/lib/framework/JsonSerializer.h b/lib/framework/JsonSerializer.h new file mode 100644 index 00000000..993d5aa0 --- /dev/null +++ b/lib/framework/JsonSerializer.h @@ -0,0 +1,9 @@ +#ifndef JsonSerializer_h +#define JsonSerializer_h + +#include + +template +using JsonSerializer = void (*)(T& settings, JsonObject& root); + +#endif // end JsonSerializer diff --git a/lib/framework/MQTTSettingsService.cpp b/lib/framework/MQTTSettingsService.cpp index ed21f8fa..8c169398 100644 --- a/lib/framework/MQTTSettingsService.cpp +++ b/lib/framework/MQTTSettingsService.cpp @@ -21,13 +21,13 @@ static char* retainCstr(const char* cstr, char** ptr) { } MQTTSettingsService::MQTTSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(MQTTSettings::serialize, + _httpEndpoint(MQTTSettings::serialize, MQTTSettings::deserialize, this, server, MQTT_SETTINGS_SERVICE_PATH, securityManager), - _settingsPersistence(MQTTSettings::serialize, MQTTSettings::deserialize, this, fs, MQTT_SETTINGS_FILE) { + _fsPersistence(MQTTSettings::serialize, MQTTSettings::deserialize, this, fs, MQTT_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent( std::bind(&MQTTSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), @@ -49,7 +49,7 @@ MQTTSettingsService::~MQTTSettingsService() { } void MQTTSettingsService::begin() { - _settingsPersistence.readFromFS(); + _fsPersistence.readFromFS(); } void MQTTSettingsService::loop() { diff --git a/lib/framework/MQTTSettingsService.h b/lib/framework/MQTTSettingsService.h index 44b848ae..dbcb45a5 100644 --- a/lib/framework/MQTTSettingsService.h +++ b/lib/framework/MQTTSettingsService.h @@ -1,8 +1,8 @@ #ifndef MQTTSettingsService_h #define MQTTSettingsService_h -#include -#include +#include +#include #include #define MQTT_RECONNECTION_DELAY 5000 @@ -89,8 +89,8 @@ class MQTTSettingsService : public SettingsService { void onConfigUpdated(); private: - SettingsEndpoint _settingsEndpoint; - SettingsPersistence _settingsPersistence; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; // Pointers to hold retained copies of the mqtt client connection strings. // Required as AsyncMqttClient holds refrences to the supplied connection strings. diff --git a/lib/framework/MqttPubSub.h b/lib/framework/MqttPubSub.h index d3eea7a7..bdb93d47 100644 --- a/lib/framework/MqttPubSub.h +++ b/lib/framework/MqttPubSub.h @@ -2,8 +2,8 @@ #define MqttPubSub_h #include -#include -#include +#include +#include #include #define MAX_MESSAGE_SIZE 1024 @@ -26,11 +26,11 @@ class MqttConnector { template class MqttPub : virtual public MqttConnector { public: - MqttPub(SettingsSerializer settingsSerializer, + MqttPub(JsonSerializer jsonSerializer, SettingsService* settingsService, AsyncMqttClient* mqttClient, String pubTopic = "") : - MqttConnector(settingsService, mqttClient), _settingsSerializer(settingsSerializer), _pubTopic(pubTopic) { + MqttConnector(settingsService, mqttClient), _jsonSerializer(jsonSerializer), _pubTopic(pubTopic) { MqttConnector::_settingsService->addUpdateHandler([&](String originId) { publish(); }, false); } @@ -45,7 +45,7 @@ class MqttPub : virtual public MqttConnector { } private: - SettingsSerializer _settingsSerializer; + JsonSerializer _jsonSerializer; String _pubTopic; void publish() { @@ -53,7 +53,7 @@ class MqttPub : virtual public MqttConnector { // serialize to json doc DynamicJsonDocument json(MAX_MESSAGE_SIZE); JsonObject jsonObject = json.to(); - MqttConnector::_settingsService->read(jsonObject, _settingsSerializer); + MqttConnector::_settingsService->read(jsonObject, _jsonSerializer); // serialize to string String payload; @@ -68,11 +68,11 @@ class MqttPub : virtual public MqttConnector { template class MqttSub : virtual public MqttConnector { public: - MqttSub(SettingsDeserializer settingsDeserializer, + MqttSub(JsonDeserializer jsonDeserializer, SettingsService* settingsService, AsyncMqttClient* mqttClient, String subTopic = "") : - MqttConnector(settingsService, mqttClient), _settingsDeserializer(settingsDeserializer), _subTopic(subTopic) { + MqttConnector(settingsService, mqttClient), _jsonDeserializer(jsonDeserializer), _subTopic(subTopic) { MqttConnector::_mqttClient->onMessage(std::bind(&MqttSub::onMqttMessage, this, std::placeholders::_1, @@ -101,7 +101,7 @@ class MqttSub : virtual public MqttConnector { } private: - SettingsDeserializer _settingsDeserializer; + JsonDeserializer _jsonDeserializer; String _subTopic; void subscribe() { @@ -126,7 +126,7 @@ class MqttSub : virtual public MqttConnector { DeserializationError error = deserializeJson(json, payload, len); if (!error && json.is()) { JsonObject jsonObject = json.as(); - MqttConnector::_settingsService->update(jsonObject, _settingsDeserializer, MQTT_ORIGIN_ID); + MqttConnector::_settingsService->update(jsonObject, _jsonDeserializer, MQTT_ORIGIN_ID); } } }; @@ -134,15 +134,15 @@ class MqttSub : virtual public MqttConnector { template class MqttPubSub : public MqttPub, public MqttSub { public: - MqttPubSub(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, + MqttPubSub(JsonSerializer jsonSerializer, + JsonDeserializer jsonDeserializer, SettingsService* settingsService, AsyncMqttClient* mqttClient, String pubTopic = "", String subTopic = "") : MqttConnector(settingsService, mqttClient), - MqttPub(settingsSerializer, settingsService, mqttClient, pubTopic = ""), - MqttSub(settingsDeserializer, settingsService, mqttClient, subTopic = "") { + MqttPub(jsonSerializer, settingsService, mqttClient, pubTopic = ""), + MqttSub(jsonDeserializer, settingsService, mqttClient, subTopic = "") { } public: diff --git a/lib/framework/NTPSettingsService.cpp b/lib/framework/NTPSettingsService.cpp index c9f637b2..db417865 100644 --- a/lib/framework/NTPSettingsService.cpp +++ b/lib/framework/NTPSettingsService.cpp @@ -1,13 +1,13 @@ #include NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(NTPSettings::serialize, + _httpEndpoint(NTPSettings::serialize, NTPSettings::deserialize, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager), - _settingsPersistence(NTPSettings::serialize, NTPSettings::deserialize, this, fs, NTP_SETTINGS_FILE) { + _fsPersistence(NTPSettings::serialize, NTPSettings::deserialize, this, fs, NTP_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent( std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), @@ -24,7 +24,7 @@ NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityM } void NTPSettingsService::begin() { - _settingsPersistence.readFromFS(); + _fsPersistence.readFromFS(); configureNTP(); } diff --git a/lib/framework/NTPSettingsService.h b/lib/framework/NTPSettingsService.h index 38db3f72..a8fc97df 100644 --- a/lib/framework/NTPSettingsService.h +++ b/lib/framework/NTPSettingsService.h @@ -1,8 +1,8 @@ #ifndef NTPSettingsService_h #define NTPSettingsService_h -#include -#include +#include +#include #include #ifdef ESP32 @@ -49,8 +49,8 @@ class NTPSettingsService : public SettingsService { void begin(); private: - SettingsEndpoint _settingsEndpoint; - SettingsPersistence _settingsPersistence; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; #ifdef ESP32 void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info); diff --git a/lib/framework/OTASettingsService.cpp b/lib/framework/OTASettingsService.cpp index 78daf929..40eec2d0 100644 --- a/lib/framework/OTASettingsService.cpp +++ b/lib/framework/OTASettingsService.cpp @@ -1,8 +1,8 @@ #include OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(OTASettings::serialize, OTASettings::deserialize, this, server, OTA_SETTINGS_SERVICE_PATH, securityManager), - _settingsPersistence(OTASettings::serialize, OTASettings::deserialize, this, fs, OTA_SETTINGS_FILE) { + _httpEndpoint(OTASettings::serialize, OTASettings::deserialize, this, server, OTA_SETTINGS_SERVICE_PATH, securityManager), + _fsPersistence(OTASettings::serialize, OTASettings::deserialize, this, fs, OTA_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); @@ -14,7 +14,7 @@ OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityM } void OTASettingsService::begin() { - _settingsPersistence.readFromFS(); + _fsPersistence.readFromFS(); configureArduinoOTA(); } diff --git a/lib/framework/OTASettingsService.h b/lib/framework/OTASettingsService.h index ca2fff19..3877ac53 100644 --- a/lib/framework/OTASettingsService.h +++ b/lib/framework/OTASettingsService.h @@ -1,8 +1,8 @@ #ifndef OTASettingsService_h #define OTASettingsService_h -#include -#include +#include +#include #ifdef ESP32 #include @@ -48,8 +48,8 @@ class OTASettingsService : public SettingsService { void loop(); private: - SettingsEndpoint _settingsEndpoint; - SettingsPersistence _settingsPersistence; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; ArduinoOTAClass* _arduinoOTA; void configureArduinoOTA(); diff --git a/lib/framework/SecuritySettingsService.cpp b/lib/framework/SecuritySettingsService.cpp index e0be0c61..fe952ddb 100644 --- a/lib/framework/SecuritySettingsService.cpp +++ b/lib/framework/SecuritySettingsService.cpp @@ -1,18 +1,18 @@ #include SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : - _settingsEndpoint(SecuritySettings::serialize, + _httpEndpoint(SecuritySettings::serialize, SecuritySettings::deserialize, this, server, SECURITY_SETTINGS_PATH, this), - _settingsPersistence(SecuritySettings::serialize, SecuritySettings::deserialize, this, fs, SECURITY_SETTINGS_FILE) { + _fsPersistence(SecuritySettings::serialize, SecuritySettings::deserialize, this, fs, SECURITY_SETTINGS_FILE) { addUpdateHandler([&](String originId) { configureJWTHandler(); }, false); } void SecuritySettingsService::begin() { - _settingsPersistence.readFromFS(); + _fsPersistence.readFromFS(); configureJWTHandler(); } diff --git a/lib/framework/SecuritySettingsService.h b/lib/framework/SecuritySettingsService.h index d4973577..94239357 100644 --- a/lib/framework/SecuritySettingsService.h +++ b/lib/framework/SecuritySettingsService.h @@ -2,8 +2,8 @@ #define SecuritySettingsService_h #include -#include -#include +#include +#include #define DEFAULT_ADMIN_USERNAME "admin" #define DEFAULT_GUEST_USERNAME "guest" @@ -62,8 +62,8 @@ class SecuritySettingsService : public SettingsService, public ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback, AuthenticationPredicate predicate); private: - SettingsEndpoint _settingsEndpoint; - SettingsPersistence _settingsPersistence; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; ArduinoJsonJWT _jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET); void configureJWTHandler(); diff --git a/lib/framework/SettingsBroker.h b/lib/framework/SettingsBroker.h deleted file mode 100644 index 2b4a4dba..00000000 --- a/lib/framework/SettingsBroker.h +++ /dev/null @@ -1,109 +0,0 @@ -#ifndef SettingsBroker_h -#define SettingsBroker_h - -#include -#include -#include -#include - -#define MAX_MESSAGE_SIZE 1024 -#define SETTINGS_BROKER_ORIGIN_ID "broker" - -/** - * SettingsBroker is designed to operate on a pair of pub/sub topics. - * - * The broker listens to changes on a "set" topic and publish its state on a "state" topic. - * - * Settings are automatically published to the state topic when a connection to the broker is established or when - * settings are updated. - * - * When a message is recieved on the set topic the settings are deserialized from the payload and applied. The state - * topic is then updated as normal. - */ -template -class SettingsBroker { - public: - SettingsBroker(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, - SettingsService* settingsService, - AsyncMqttClient* mqttClient, - String setTopic = "", - String stateTopic = "") : - _settingsSerializer(settingsSerializer), - _settingsDeserializer(settingsDeserializer), - _settingsService(settingsService), - _mqttClient(mqttClient), - _setTopic(setTopic), - _stateTopic(stateTopic) { - _settingsService->addUpdateHandler([&](String originId) { publish(); }, false); - _mqttClient->onConnect(std::bind(&SettingsBroker::configureMQTT, this)); - _mqttClient->onMessage(std::bind(&SettingsBroker::onMqttMessage, - this, - std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3, - std::placeholders::_4, - std::placeholders::_5, - std::placeholders::_6)); - } - - void configureBroker(String setTopic, String stateTopic) { - _setTopic = setTopic; - _stateTopic = stateTopic; - configureMQTT(); - } - - protected: - virtual void configureMQTT() { - if (_setTopic.length() > 0) { - _mqttClient->subscribe(_setTopic.c_str(), 2); - } - publish(); - } - - private: - SettingsSerializer _settingsSerializer; - SettingsDeserializer _settingsDeserializer; - SettingsService* _settingsService; - AsyncMqttClient* _mqttClient; - String _setTopic; - String _stateTopic; - - void publish() { - if (_stateTopic.length() > 0 && _mqttClient->connected()) { - // serialize to json doc - DynamicJsonDocument json(MAX_MESSAGE_SIZE); - JsonObject jsonObject = json.to(); - _settingsService->read(jsonObject, _settingsSerializer); - - // serialize to string - String payload; - serializeJson(json, payload); - - // publish the payload - _mqttClient->publish(_stateTopic.c_str(), 0, false, payload.c_str()); - } - } - - void onMqttMessage(char* topic, - char* payload, - AsyncMqttClientMessageProperties properties, - size_t len, - size_t index, - size_t total) { - // we only care about the topic we are watching in this class - if (strcmp(_setTopic.c_str(), topic)) { - return; - } - - // deserialize from string - DynamicJsonDocument json(MAX_MESSAGE_SIZE); - DeserializationError error = deserializeJson(json, payload, len); - if (!error && json.is()) { - JsonObject jsonObject = json.as(); - _settingsService->update(jsonObject, _settingsDeserializer, SETTINGS_BROKER_ORIGIN_ID); - } - } -}; - -#endif // end SettingsBroker_h diff --git a/lib/framework/SettingsDeserializer.h b/lib/framework/SettingsDeserializer.h deleted file mode 100644 index 80876171..00000000 --- a/lib/framework/SettingsDeserializer.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef SettingsDeserializer_h -#define SettingsDeserializer_h - -#include - -template -using SettingsDeserializer = void (*)(JsonObject& root, T& settings); - - -#endif // end SettingsDeserializer diff --git a/lib/framework/SettingsEndpoint.h b/lib/framework/SettingsEndpoint.h deleted file mode 100644 index d54901f0..00000000 --- a/lib/framework/SettingsEndpoint.h +++ /dev/null @@ -1,99 +0,0 @@ -#ifndef SettingsEndpoint_h -#define SettingsEndpoint_h - -#include - -#include -#include - -#include -#include -#include -#include - -#define MAX_CONTENT_LENGTH 1024 -#define SETTINGS_ENDPOINT_ORIGIN_ID "endpoint" - -template -class SettingsEndpoint { - public: - SettingsEndpoint(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, - SettingsService* settingsService, - AsyncWebServer* server, - const String& servicePath, - SecurityManager* securityManager, - AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - _settingsSerializer(settingsSerializer), - _settingsDeserializer(settingsDeserializer), - _settingsService(settingsService), - _updateHandler( - servicePath, - securityManager->wrapCallback( - std::bind(&SettingsEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2), - authenticationPredicate)) { - server->on(servicePath.c_str(), - HTTP_GET, - securityManager->wrapRequest(std::bind(&SettingsEndpoint::fetchSettings, this, std::placeholders::_1), - authenticationPredicate)); - _updateHandler.setMethod(HTTP_POST); - _updateHandler.setMaxContentLength(MAX_CONTENT_LENGTH); - server->addHandler(&_updateHandler); - } - - SettingsEndpoint(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, - SettingsService* settingsService, - AsyncWebServer* server, - const String& servicePath) : - _settingsSerializer(settingsSerializer), - _settingsDeserializer(settingsDeserializer), - _settingsService(settingsService), - _updateHandler(servicePath, - std::bind(&SettingsEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2)) { - server->on(servicePath.c_str(), HTTP_GET, std::bind(&SettingsEndpoint::fetchSettings, this, std::placeholders::_1)); - _updateHandler.setMethod(HTTP_POST); - _updateHandler.setMaxContentLength(MAX_CONTENT_LENGTH); - server->addHandler(&_updateHandler); - } - - protected: - SettingsSerializer _settingsSerializer; - SettingsDeserializer _settingsDeserializer; - SettingsService* _settingsService; - AsyncCallbackJsonWebHandler _updateHandler; - - void fetchSettings(AsyncWebServerRequest* request) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); - JsonObject jsonObject = response->getRoot().to(); - _settingsService->read(jsonObject, _settingsSerializer); - - response->setLength(); - request->send(response); - } - - void updateSettings(AsyncWebServerRequest* request, JsonVariant& json) { - if (json.is()) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); - - // use callback to update the settings once the response is complete - request->onDisconnect([this]() { _settingsService->callUpdateHandlers(SETTINGS_ENDPOINT_ORIGIN_ID); }); - - // update the settings, deferring the call to the update handlers to when the response is complete - _settingsService->updateWithoutPropagation([&](T& settings) { - JsonObject jsonObject = json.as(); - _settingsDeserializer(jsonObject, settings); - jsonObject = response->getRoot().to(); - _settingsSerializer(settings, jsonObject); - }); - - // write the response to the client - response->setLength(); - request->send(response); - } else { - request->send(400); - } - } -}; - -#endif // end SettingsEndpoint diff --git a/lib/framework/SettingsPersistence.h b/lib/framework/SettingsPersistence.h deleted file mode 100644 index 43448c15..00000000 --- a/lib/framework/SettingsPersistence.h +++ /dev/null @@ -1,109 +0,0 @@ -#ifndef SettingsPersistence_h -#define SettingsPersistence_h - -#include -#include -#include -#include - -#define MAX_FILE_SIZE 1024 - -/** - * SettingsPersistance takes care of loading and saving settings when they change. - * - * SettingsPersistence automatically registers writeToFS as an update handler with the settings manager - * when constructed, saving any updates to the file system. - */ -template -class SettingsPersistence { - public: - SettingsPersistence(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, - SettingsService* settingsService, - FS* fs, - char const* filePath) : - _settingsSerializer(settingsSerializer), - _settingsDeserializer(settingsDeserializer), - _settingsService(settingsService), - _fs(fs), - _filePath(filePath) { - enableUpdateHandler(); - } - - void readFromFS() { - File settingsFile = _fs->open(_filePath, "r"); - - if (settingsFile) { - if (settingsFile.size() <= MAX_FILE_SIZE) { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); - DeserializationError error = deserializeJson(jsonDocument, settingsFile); - if (error == DeserializationError::Ok && jsonDocument.is()) { - updateSettings(jsonDocument.as()); - settingsFile.close(); - return; - } - } - settingsFile.close(); - } - - // If we reach here we have not been successful in loading the config, - // hard-coded emergency defaults are now applied. - applyDefaults(); - } - - bool writeToFS() { - // create and populate a new json object - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); - JsonObject jsonObject = jsonDocument.to(); - _settingsService->read(jsonObject, _settingsSerializer); - - // serialize it to filesystem - File settingsFile = _fs->open(_filePath, "w"); - - // failed to open file, return false - if (!settingsFile) { - return false; - } - - // serialize the data to the file - serializeJson(jsonDocument, settingsFile); - settingsFile.close(); - return true; - } - - void disableUpdateHandler() { - if (_updateHandlerId) { - _settingsService->removeUpdateHandler(_updateHandlerId); - _updateHandlerId = 0; - } - } - - void enableUpdateHandler() { - if (!_updateHandlerId) { - _updateHandlerId = _settingsService->addUpdateHandler([&](String originId) { writeToFS(); }); - } - } - - private: - SettingsSerializer _settingsSerializer; - SettingsDeserializer _settingsDeserializer; - SettingsService* _settingsService; - FS* _fs; - char const* _filePath; - update_handler_id_t _updateHandlerId = 0; - - // update the settings, but do not call propogate - void updateSettings(JsonObject root) { - _settingsService->updateWithoutPropagation(root, _settingsDeserializer); - } - - protected: - // We assume the deserializer supplies sensible defaults if an empty object - // is supplied, this virtual function allows that to be changed. - virtual void applyDefaults() { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); - updateSettings(jsonDocument.to()); - } -}; - -#endif // end SettingsPersistence diff --git a/lib/framework/SettingsSerializer.h b/lib/framework/SettingsSerializer.h deleted file mode 100644 index 1586a309..00000000 --- a/lib/framework/SettingsSerializer.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef SettingsSerializer_h -#define SettingsSerializer_h - -#include - -template -using SettingsSerializer = void (*)(T& settings, JsonObject& root); - -#endif // end SettingsSerializer diff --git a/lib/framework/SettingsService.h b/lib/framework/SettingsService.h index 111db425..8a8bf796 100644 --- a/lib/framework/SettingsService.h +++ b/lib/framework/SettingsService.h @@ -2,8 +2,8 @@ #define SettingsService_h #include -#include -#include +#include +#include #include #include @@ -66,7 +66,7 @@ class SettingsService { #endif } - void updateWithoutPropagation(JsonObject& jsonObject, SettingsDeserializer deserializer) { + void updateWithoutPropagation(JsonObject& jsonObject, JsonDeserializer deserializer) { #ifdef ESP32 xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); #endif @@ -87,7 +87,7 @@ class SettingsService { #endif } - void update(JsonObject& jsonObject, SettingsDeserializer deserializer, String originId) { + void update(JsonObject& jsonObject, JsonDeserializer deserializer, String originId) { #ifdef ESP32 xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); #endif @@ -108,7 +108,7 @@ class SettingsService { #endif } - void read(JsonObject& jsonObject, SettingsSerializer serializer) { + void read(JsonObject& jsonObject, JsonSerializer serializer) { #ifdef ESP32 xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); #endif diff --git a/lib/framework/SettingsSocket.h b/lib/framework/SettingsSocket.h deleted file mode 100644 index 2a318b12..00000000 --- a/lib/framework/SettingsSocket.h +++ /dev/null @@ -1,156 +0,0 @@ -#ifndef SettingsSocket_h -#define SettingsSocket_h - -#include -#include -#include -#include - -#define MAX_SETTINGS_SOCKET_MSG_SIZE 1024 - -#define SETTINGS_SOCKET_CLIENT_ID_MSG_SIZE 128 -#define SETTINGS_SOCKET_ORIGIN "socket" -#define SETTINGS_SOCKET_CLIENT_ORIGIN_ID_PREFIX "socket:" - -/** - * SettingsSocket is designed to provide WebSocket based communication for making and observing updates to settings. - */ -template -class SettingsSocket { - public: - SettingsSocket(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, - SettingsService* settingsService, - AsyncWebServer* server, - char const* socketPath, - SecurityManager* securityManager, - AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - _settingsSerializer(settingsSerializer), - _settingsDeserializer(settingsDeserializer), - _settingsService(settingsService), - _server(server), - _webSocket(socketPath) { - _settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); - _webSocket.setFilter(securityManager->filterRequest(authenticationPredicate)); - _webSocket.onEvent(std::bind(&SettingsSocket::onWSEvent, - this, - std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3, - std::placeholders::_4, - std::placeholders::_5, - std::placeholders::_6)); - _server->addHandler(&_webSocket); - _server->on(socketPath, HTTP_GET, std::bind(&SettingsSocket::forbidden, this, std::placeholders::_1)); - } - - SettingsSocket(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, - SettingsService* settingsService, - AsyncWebServer* server, - char const* socketPath) : - _settingsSerializer(settingsSerializer), - _settingsDeserializer(settingsDeserializer), - _settingsService(settingsService), - _server(server), - _webSocket(socketPath) { - _settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); - _webSocket.onEvent(std::bind(&SettingsSocket::onWSEvent, - this, - std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3, - std::placeholders::_4, - std::placeholders::_5, - std::placeholders::_6)); - _server->addHandler(&_webSocket); - } - - private: - SettingsSerializer _settingsSerializer; - SettingsDeserializer _settingsDeserializer; - SettingsService* _settingsService; - AsyncWebServer* _server; - AsyncWebSocket _webSocket; - - /** - * Renders a forbidden respnose to the client if they fail to connect. - */ - void forbidden(AsyncWebServerRequest* request) { - request->send(403); - } - - /** - * Responds to the WSEvent by sending the current settings to the clients when they connect and by applying the - * changes sent to the socket directly to the settings service. - */ - void onWSEvent(AsyncWebSocket* server, - AsyncWebSocketClient* client, - AwsEventType type, - void* arg, - uint8_t* data, - size_t len) { - if (type == WS_EVT_CONNECT) { - // when a client connects, we transmit it's id and the current payload - transmitId(client); - transmitData(client, SETTINGS_SOCKET_ORIGIN); - } else if (type == WS_EVT_DATA) { - AwsFrameInfo* info = (AwsFrameInfo*)arg; - if (info->final && info->index == 0 && info->len == len) { - if (info->opcode == WS_TEXT) { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SOCKET_MSG_SIZE); - DeserializationError error = deserializeJson(jsonDocument, (char*)data); - if (!error && jsonDocument.is()) { - JsonObject jsonObject = jsonDocument.as(); - _settingsService->update(jsonObject, _settingsDeserializer, clientId(client)); - } - } - } - } - } - - void transmitId(AsyncWebSocketClient* client) { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(SETTINGS_SOCKET_CLIENT_ID_MSG_SIZE); - JsonObject root = jsonDocument.to(); - root["type"] = "id"; - root["id"] = clientId(client); - size_t len = measureJson(jsonDocument); - AsyncWebSocketMessageBuffer* buffer = _webSocket.makeBuffer(len); - if (buffer) { - serializeJson(jsonDocument, (char*)buffer->get(), len + 1); - client->text(buffer); - } - } - - String clientId(AsyncWebSocketClient* client) { - return SETTINGS_SOCKET_CLIENT_ORIGIN_ID_PREFIX + String(client->id()); - } - - /** - * Broadcasts the payload to the destination, if provided. Otherwise broadcasts to all clients except the origin, if - * specified. - * - * Original implementation sent clients their own IDs so they could ignore updates they initiated. This approach - * simplifies the client and the server implementation but may not be sufficent for all use-cases. - */ - void transmitData(AsyncWebSocketClient* client, String originId) { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SOCKET_MSG_SIZE); - JsonObject root = jsonDocument.to(); - root["type"] = "payload"; - root["origin_id"] = originId; - JsonObject payload = root.createNestedObject("payload"); - _settingsService->read(payload, _settingsSerializer); - - size_t len = measureJson(jsonDocument); - AsyncWebSocketMessageBuffer* buffer = _webSocket.makeBuffer(len); - if (buffer) { - serializeJson(jsonDocument, (char*)buffer->get(), len + 1); - if (client) { - client->text(buffer); - } else { - _webSocket.textAll(buffer); - } - } - } -}; -#endif // end SettingsSocket_h diff --git a/lib/framework/WebSocketTxRx.h b/lib/framework/WebSocketTxRx.h index 7e2e117f..48216d90 100644 --- a/lib/framework/WebSocketTxRx.h +++ b/lib/framework/WebSocketTxRx.h @@ -2,8 +2,8 @@ #define WebSocketTxRx_h #include -#include -#include +#include +#include #include #define WEB_SOCKET_MSG_SIZE 1024 @@ -71,23 +71,23 @@ class WebSocketConnector { template class WebSocketTx : virtual public WebSocketConnector { public: - WebSocketTx(SettingsSerializer settingsSerializer, + WebSocketTx(JsonSerializer jsonSerializer, SettingsService* settingsService, AsyncWebServer* server, char const* socketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : WebSocketConnector(settingsService, server, socketPath, securityManager, authenticationPredicate), - _settingsSerializer(settingsSerializer) { + _jsonSerializer(jsonSerializer) { WebSocketConnector::_settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); } - WebSocketTx(SettingsSerializer settingsSerializer, + WebSocketTx(JsonSerializer jsonSerializer, SettingsService* settingsService, AsyncWebServer* server, char const* socketPath) : - WebSocketConnector(settingsService, server, socketPath), _settingsSerializer(settingsSerializer) { + WebSocketConnector(settingsService, server, socketPath), _jsonSerializer(jsonSerializer) { WebSocketConnector::_settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); } @@ -107,7 +107,7 @@ class WebSocketTx : virtual public WebSocketConnector { } private: - SettingsSerializer _settingsSerializer; + JsonSerializer _jsonSerializer; void transmitId(AsyncWebSocketClient* client) { DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_CLIENT_ID_MSG_SIZE); @@ -135,7 +135,7 @@ class WebSocketTx : virtual public WebSocketConnector { root["type"] = "payload"; root["origin_id"] = originId; JsonObject payload = root.createNestedObject("payload"); - WebSocketConnector::_settingsService->read(payload, _settingsSerializer); + WebSocketConnector::_settingsService->read(payload, _jsonSerializer); size_t len = measureJson(jsonDocument); AsyncWebSocketMessageBuffer* buffer = WebSocketConnector::_webSocket.makeBuffer(len); @@ -153,21 +153,21 @@ class WebSocketTx : virtual public WebSocketConnector { template class WebSocketRx : virtual public WebSocketConnector { public: - WebSocketRx(SettingsDeserializer settingsDeserializer, + WebSocketRx(JsonDeserializer jsonDeserializer, SettingsService* settingsService, AsyncWebServer* server, char const* socketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : WebSocketConnector(settingsService, server, socketPath, securityManager, authenticationPredicate), - _settingsDeserializer(settingsDeserializer) { + _jsonDeserializer(jsonDeserializer) { } - WebSocketRx(SettingsDeserializer settingsDeserializer, + WebSocketRx(JsonDeserializer jsonDeserializer, SettingsService* settingsService, AsyncWebServer* server, char const* socketPath) : - WebSocketConnector(settingsService, server, socketPath), _settingsDeserializer(settingsDeserializer) { + WebSocketConnector(settingsService, server, socketPath), _jsonDeserializer(jsonDeserializer) { } protected: @@ -186,7 +186,7 @@ class WebSocketRx : virtual public WebSocketConnector { if (!error && jsonDocument.is()) { JsonObject jsonObject = jsonDocument.as(); WebSocketConnector::_settingsService->update( - jsonObject, _settingsDeserializer, WebSocketConnector::clientId(client)); + jsonObject, _jsonDeserializer, WebSocketConnector::clientId(client)); } } } @@ -194,22 +194,22 @@ class WebSocketRx : virtual public WebSocketConnector { } private: - SettingsDeserializer _settingsDeserializer; + JsonDeserializer _jsonDeserializer; }; template class WebSocketTxRx : public WebSocketTx, public WebSocketRx { public: - WebSocketTxRx(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, + WebSocketTxRx(JsonSerializer jsonSerializer, + JsonDeserializer jsonDeserializer, SettingsService* settingsService, AsyncWebServer* server, char const* socketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : WebSocketConnector(settingsService, server, socketPath, securityManager, authenticationPredicate), - WebSocketTx(settingsSerializer, settingsService, server, socketPath, securityManager, authenticationPredicate), - WebSocketRx(settingsDeserializer, + WebSocketTx(jsonSerializer, settingsService, server, socketPath, securityManager, authenticationPredicate), + WebSocketRx(jsonDeserializer, settingsService, server, socketPath, @@ -217,14 +217,14 @@ class WebSocketTxRx : public WebSocketTx, public WebSocketRx { authenticationPredicate) { } - WebSocketTxRx(SettingsSerializer settingsSerializer, - SettingsDeserializer settingsDeserializer, + WebSocketTxRx(JsonSerializer jsonSerializer, + JsonDeserializer jsonDeserializer, SettingsService* settingsService, AsyncWebServer* server, char const* socketPath) : WebSocketConnector(settingsService, server, socketPath), - WebSocketTx(settingsSerializer, settingsService, server, socketPath), - WebSocketRx(settingsDeserializer, settingsService, server, socketPath) { + WebSocketTx(jsonSerializer, settingsService, server, socketPath), + WebSocketRx(jsonDeserializer, settingsService, server, socketPath) { } protected: diff --git a/lib/framework/WiFiSettingsService.cpp b/lib/framework/WiFiSettingsService.cpp index 96df66c1..91b59cc7 100644 --- a/lib/framework/WiFiSettingsService.cpp +++ b/lib/framework/WiFiSettingsService.cpp @@ -1,13 +1,13 @@ #include WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _settingsEndpoint(WiFiSettings::serialize, + _httpEndpoint(WiFiSettings::serialize, WiFiSettings::deserialize, this, server, WIFI_SETTINGS_SERVICE_PATH, securityManager), - _settingsPersistence(WiFiSettings::serialize, WiFiSettings::deserialize, this, fs, WIFI_SETTINGS_FILE) { + _fsPersistence(WiFiSettings::serialize, WiFiSettings::deserialize, this, fs, WIFI_SETTINGS_FILE) { // We want the device to come up in opmode=0 (WIFI_OFF), when erasing the flash this is not the default. // If needed, we save opmode=0 before disabling persistence so the device boots with WiFi disabled in the future. if (WiFi.getMode() != WIFI_OFF) { @@ -35,7 +35,7 @@ WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, Securit } void WiFiSettingsService::begin() { - _settingsPersistence.readFromFS(); + _fsPersistence.readFromFS(); reconfigureWiFiConnection(); } diff --git a/lib/framework/WiFiSettingsService.h b/lib/framework/WiFiSettingsService.h index 99690c2d..4d3f5c4c 100644 --- a/lib/framework/WiFiSettingsService.h +++ b/lib/framework/WiFiSettingsService.h @@ -1,8 +1,8 @@ #ifndef WiFiSettingsService_h #define WiFiSettingsService_h -#include -#include +#include +#include #include #define WIFI_SETTINGS_FILE "/config/wifiSettings.json" @@ -76,8 +76,8 @@ class WiFiSettingsService : public SettingsService { void loop(); private: - SettingsEndpoint _settingsEndpoint; - SettingsPersistence _settingsPersistence; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; unsigned long _lastConnectionAttempt; #ifdef ESP32 From b54f076d316bdff687822187e553dc2b63f05f6e Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sun, 10 May 2020 20:34:35 +0100 Subject: [PATCH 23/35] rename SettingsService to StatefulService --- lib/framework/APSettingsService.cpp | 16 +-- lib/framework/APSettingsService.h | 2 +- lib/framework/ESP8266React.h | 12 +- lib/framework/FSPersistence.h | 22 ++-- lib/framework/HttpEndpoint.h | 42 +++---- lib/framework/JsonDeserializer.h | 1 - lib/framework/MQTTSettingsService.cpp | 38 +++--- lib/framework/MQTTSettingsService.h | 4 +- lib/framework/MQTTStatus.cpp | 2 +- lib/framework/MqttPubSub.h | 30 ++--- lib/framework/NTPSettingsService.cpp | 16 +-- lib/framework/NTPSettingsService.h | 2 +- lib/framework/OTASettingsService.cpp | 15 ++- lib/framework/OTASettingsService.h | 2 +- lib/framework/SecuritySettingsService.cpp | 16 +-- lib/framework/SecuritySettingsService.h | 2 +- lib/framework/SettingsService.h | 136 --------------------- lib/framework/StatefulService.h | 137 ++++++++++++++++++++++ lib/framework/WebSocketTxRx.h | 57 ++++----- lib/framework/WiFiSettingsService.cpp | 22 ++-- lib/framework/WiFiSettingsService.h | 3 +- src/LightBrokerSettingsService.h | 3 +- src/LightSettingsService.cpp | 4 +- src/LightSettingsService.h | 3 +- 24 files changed, 293 insertions(+), 294 deletions(-) delete mode 100644 lib/framework/SettingsService.h create mode 100644 lib/framework/StatefulService.h diff --git a/lib/framework/APSettingsService.cpp b/lib/framework/APSettingsService.cpp index 3215a85b..ec4f5d5d 100644 --- a/lib/framework/APSettingsService.cpp +++ b/lib/framework/APSettingsService.cpp @@ -2,11 +2,11 @@ APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : _httpEndpoint(APSettings::serialize, - APSettings::deserialize, - this, - server, - AP_SETTINGS_SERVICE_PATH, - securityManager), + APSettings::deserialize, + this, + server, + AP_SETTINGS_SERVICE_PATH, + securityManager), _fsPersistence(APSettings::serialize, APSettings::deserialize, this, fs, AP_SETTINGS_FILE) { addUpdateHandler([&](String originId) { reconfigureAP(); }, false); } @@ -32,8 +32,8 @@ void APSettingsService::loop() { void APSettingsService::manageAP() { WiFiMode_t currentWiFiMode = WiFi.getMode(); - if (_settings.provisionMode == AP_MODE_ALWAYS || - (_settings.provisionMode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED)) { + if (_state.provisionMode == AP_MODE_ALWAYS || + (_state.provisionMode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED)) { if (currentWiFiMode == WIFI_OFF || currentWiFiMode == WIFI_STA) { startAP(); } @@ -46,7 +46,7 @@ void APSettingsService::manageAP() { void APSettingsService::startAP() { Serial.println("Starting software access point"); - WiFi.softAP(_settings.ssid.c_str(), _settings.password.c_str()); + WiFi.softAP(_state.ssid.c_str(), _state.password.c_str()); if (!_dnsServer) { IPAddress apIp = WiFi.softAPIP(); Serial.print("Starting captive portal on "); diff --git a/lib/framework/APSettingsService.h b/lib/framework/APSettingsService.h index 2c11eccc..22c4e7bc 100644 --- a/lib/framework/APSettingsService.h +++ b/lib/framework/APSettingsService.h @@ -48,7 +48,7 @@ class APSettings { } }; -class APSettingsService : public SettingsService { +class APSettingsService : public StatefulService { public: APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); diff --git a/lib/framework/ESP8266React.h b/lib/framework/ESP8266React.h index 5b4b94e1..8af5d90d 100644 --- a/lib/framework/ESP8266React.h +++ b/lib/framework/ESP8266React.h @@ -43,27 +43,27 @@ class ESP8266React { return &_securitySettingsService; } - SettingsService* getSecuritySettingsService() { + StatefulService* getSecuritySettingsService() { return &_securitySettingsService; } - SettingsService* getWiFiSettingsService() { + StatefulService* getWiFiSettingsService() { return &_wifiSettingsService; } - SettingsService* getAPSettingsService() { + StatefulService* getAPSettingsService() { return &_apSettingsService; } - SettingsService* getNTPSettingsService() { + StatefulService* getNTPSettingsService() { return &_ntpSettingsService; } - SettingsService* getOTASettingsService() { + StatefulService* getOTASettingsService() { return &_otaSettingsService; } - SettingsService* getMQTTSettingsService() { + StatefulService* getMQTTSettingsService() { return &_mqttSettingsService; } diff --git a/lib/framework/FSPersistence.h b/lib/framework/FSPersistence.h index c299b8be..3d3daeb7 100644 --- a/lib/framework/FSPersistence.h +++ b/lib/framework/FSPersistence.h @@ -1,7 +1,7 @@ #ifndef FSPersistence_h #define FSPersistence_h -#include +#include #include #include #include @@ -12,13 +12,13 @@ template class FSPersistence { public: FSPersistence(JsonSerializer jsonSerializer, - JsonDeserializer jsonDeserializer, - SettingsService* settingsService, - FS* fs, - char const* filePath) : + JsonDeserializer jsonDeserializer, + StatefulService* statefulService, + FS* fs, + char const* filePath) : _jsonSerializer(jsonSerializer), _jsonDeserializer(jsonDeserializer), - _settingsService(settingsService), + _statefulService(statefulService), _fs(fs), _filePath(filePath) { enableUpdateHandler(); @@ -49,7 +49,7 @@ class FSPersistence { // create and populate a new json object DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); JsonObject jsonObject = jsonDocument.to(); - _settingsService->read(jsonObject, _jsonSerializer); + _statefulService->read(jsonObject, _jsonSerializer); // serialize it to filesystem File settingsFile = _fs->open(_filePath, "w"); @@ -67,28 +67,28 @@ class FSPersistence { void disableUpdateHandler() { if (_updateHandlerId) { - _settingsService->removeUpdateHandler(_updateHandlerId); + _statefulService->removeUpdateHandler(_updateHandlerId); _updateHandlerId = 0; } } void enableUpdateHandler() { if (!_updateHandlerId) { - _updateHandlerId = _settingsService->addUpdateHandler([&](String originId) { writeToFS(); }); + _updateHandlerId = _statefulService->addUpdateHandler([&](String originId) { writeToFS(); }); } } private: JsonSerializer _jsonSerializer; JsonDeserializer _jsonDeserializer; - SettingsService* _settingsService; + StatefulService* _statefulService; FS* _fs; char const* _filePath; update_handler_id_t _updateHandlerId = 0; // update the settings, but do not call propogate void updateSettings(JsonObject root) { - _settingsService->updateWithoutPropagation(root, _jsonDeserializer); + _statefulService->updateWithoutPropagation(root, _jsonDeserializer); } protected: diff --git a/lib/framework/HttpEndpoint.h b/lib/framework/HttpEndpoint.h index 72d8aaa8..68429630 100644 --- a/lib/framework/HttpEndpoint.h +++ b/lib/framework/HttpEndpoint.h @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include @@ -18,12 +18,12 @@ template class HttpGetEndpoint { public: HttpGetEndpoint(JsonSerializer jsonSerializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, const String& servicePath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - _jsonSerializer(jsonSerializer), _settingsService(settingsService) { + _jsonSerializer(jsonSerializer), _statefulService(statefulService) { server->on(servicePath.c_str(), HTTP_GET, securityManager->wrapRequest(std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1), @@ -31,21 +31,21 @@ class HttpGetEndpoint { } HttpGetEndpoint(JsonSerializer jsonSerializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, const String& servicePath) : - _jsonSerializer(jsonSerializer), _settingsService(settingsService) { + _jsonSerializer(jsonSerializer), _statefulService(statefulService) { server->on(servicePath.c_str(), HTTP_GET, std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1)); } protected: JsonSerializer _jsonSerializer; - SettingsService* _settingsService; + StatefulService* _statefulService; void fetchSettings(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); JsonObject jsonObject = response->getRoot().to(); - _settingsService->read(jsonObject, _jsonSerializer); + _statefulService->read(jsonObject, _jsonSerializer); response->setLength(); request->send(response); @@ -57,14 +57,14 @@ class HttpPostEndpoint { public: HttpPostEndpoint(JsonSerializer jsonSerializer, JsonDeserializer jsonDeserializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, const String& servicePath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : _jsonSerializer(jsonSerializer), _jsonDeserializer(jsonDeserializer), - _settingsService(settingsService), + _statefulService(statefulService), _updateHandler( servicePath, securityManager->wrapCallback( @@ -77,12 +77,12 @@ class HttpPostEndpoint { HttpPostEndpoint(JsonSerializer jsonSerializer, JsonDeserializer jsonDeserializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, const String& servicePath) : _jsonSerializer(jsonSerializer), _jsonDeserializer(jsonDeserializer), - _settingsService(settingsService), + _statefulService(statefulService), _updateHandler(servicePath, std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2)) { _updateHandler.setMethod(HTTP_POST); @@ -93,13 +93,13 @@ class HttpPostEndpoint { protected: JsonSerializer _jsonSerializer; JsonDeserializer _jsonDeserializer; - SettingsService* _settingsService; + StatefulService* _statefulService; AsyncCallbackJsonWebHandler _updateHandler; void fetchSettings(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); JsonObject jsonObject = response->getRoot().to(); - _settingsService->read(jsonObject, _jsonSerializer); + _statefulService->read(jsonObject, _jsonSerializer); response->setLength(); request->send(response); @@ -110,10 +110,10 @@ class HttpPostEndpoint { AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); // use callback to update the settings once the response is complete - request->onDisconnect([this]() { _settingsService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); }); + request->onDisconnect([this]() { _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); }); // update the settings, deferring the call to the update handlers to when the response is complete - _settingsService->updateWithoutPropagation([&](T& settings) { + _statefulService->updateWithoutPropagation([&](T& settings) { JsonObject jsonObject = json.as(); _jsonDeserializer(jsonObject, settings); jsonObject = response->getRoot().to(); @@ -134,20 +134,20 @@ class HttpEndpoint : public HttpGetEndpoint, public HttpPostEndpoint { public: HttpEndpoint(JsonSerializer jsonSerializer, JsonDeserializer jsonDeserializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, const String& servicePath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : HttpGetEndpoint(jsonSerializer, - settingsService, + statefulService, server, servicePath, securityManager, authenticationPredicate), HttpPostEndpoint(jsonSerializer, jsonDeserializer, - settingsService, + statefulService, server, servicePath, securityManager, @@ -156,11 +156,11 @@ class HttpEndpoint : public HttpGetEndpoint, public HttpPostEndpoint { HttpEndpoint(JsonSerializer jsonSerializer, JsonDeserializer jsonDeserializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, const String& servicePath) : - HttpGetEndpoint(jsonSerializer, settingsService, server, servicePath), - HttpPostEndpoint(jsonSerializer, jsonDeserializer, settingsService, server, servicePath) { + HttpGetEndpoint(jsonSerializer, statefulService, server, servicePath), + HttpPostEndpoint(jsonSerializer, jsonDeserializer, statefulService, server, servicePath) { } }; diff --git a/lib/framework/JsonDeserializer.h b/lib/framework/JsonDeserializer.h index c342eabe..630ba535 100644 --- a/lib/framework/JsonDeserializer.h +++ b/lib/framework/JsonDeserializer.h @@ -6,5 +6,4 @@ template using JsonDeserializer = void (*)(JsonObject& root, T& settings); - #endif // end JsonDeserializer diff --git a/lib/framework/MQTTSettingsService.cpp b/lib/framework/MQTTSettingsService.cpp index 8c169398..aaa4f15a 100644 --- a/lib/framework/MQTTSettingsService.cpp +++ b/lib/framework/MQTTSettingsService.cpp @@ -22,11 +22,11 @@ static char* retainCstr(const char* cstr, char** ptr) { MQTTSettingsService::MQTTSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : _httpEndpoint(MQTTSettings::serialize, - MQTTSettings::deserialize, - this, - server, - MQTT_SETTINGS_SERVICE_PATH, - securityManager), + MQTTSettings::deserialize, + this, + server, + MQTT_SETTINGS_SERVICE_PATH, + securityManager), _fsPersistence(MQTTSettings::serialize, MQTTSettings::deserialize, this, fs, MQTT_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent( @@ -64,7 +64,7 @@ void MQTTSettingsService::loop() { } bool MQTTSettingsService::isEnabled() { - return _settings.enabled; + return _state.enabled; } bool MQTTSettingsService::isConnected() { @@ -103,28 +103,28 @@ void MQTTSettingsService::onConfigUpdated() { #ifdef ESP32 void MQTTSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { - if (_settings.enabled) { + if (_state.enabled) { Serial.println("WiFi connection dropped, starting MQTT client."); onConfigUpdated(); } } void MQTTSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) { - if (_settings.enabled) { + if (_state.enabled) { Serial.println("WiFi connection dropped, stopping MQTT client."); onConfigUpdated(); } } #elif defined(ESP8266) void MQTTSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP& event) { - if (_settings.enabled) { + if (_state.enabled) { Serial.println("WiFi connection dropped, starting MQTT client."); onConfigUpdated(); } } void MQTTSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) { - if (_settings.enabled) { + if (_state.enabled) { Serial.println("WiFi connection dropped, stopping MQTT client."); onConfigUpdated(); } @@ -136,20 +136,20 @@ void MQTTSettingsService::configureMQTT() { _mqttClient.disconnect(); // only connect if WiFi is connected and MQTT is enabled - if (_settings.enabled && WiFi.isConnected()) { + if (_state.enabled && WiFi.isConnected()) { Serial.println("Connecting to MQTT..."); - _mqttClient.setServer(retainCstr(_settings.host.c_str(), &_retainedHost), _settings.port); - if (_settings.username.length() > 0) { + _mqttClient.setServer(retainCstr(_state.host.c_str(), &_retainedHost), _state.port); + if (_state.username.length() > 0) { _mqttClient.setCredentials( - retainCstr(_settings.username.c_str(), &_retainedUsername), - retainCstr(_settings.password.length() > 0 ? _settings.password.c_str() : nullptr, &_retainedPassword)); + retainCstr(_state.username.c_str(), &_retainedUsername), + retainCstr(_state.password.length() > 0 ? _state.password.c_str() : nullptr, &_retainedPassword)); } else { _mqttClient.setCredentials(retainCstr(nullptr, &_retainedUsername), retainCstr(nullptr, &_retainedPassword)); } - _mqttClient.setClientId(retainCstr(_settings.clientId.c_str(), &_retainedClientId)); - _mqttClient.setKeepAlive(_settings.keepAlive); - _mqttClient.setCleanSession(_settings.cleanSession); - _mqttClient.setMaxTopicLength(_settings.maxTopicLength); + _mqttClient.setClientId(retainCstr(_state.clientId.c_str(), &_retainedClientId)); + _mqttClient.setKeepAlive(_state.keepAlive); + _mqttClient.setCleanSession(_state.cleanSession); + _mqttClient.setMaxTopicLength(_state.maxTopicLength); _mqttClient.connect(); } } diff --git a/lib/framework/MQTTSettingsService.h b/lib/framework/MQTTSettingsService.h index dbcb45a5..744af526 100644 --- a/lib/framework/MQTTSettingsService.h +++ b/lib/framework/MQTTSettingsService.h @@ -1,6 +1,7 @@ #ifndef MQTTSettingsService_h #define MQTTSettingsService_h +#include #include #include #include @@ -72,7 +73,7 @@ class MQTTSettings { } }; -class MQTTSettingsService : public SettingsService { +class MQTTSettingsService : public StatefulService { public: MQTTSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); ~MQTTSettingsService(); @@ -119,7 +120,6 @@ class MQTTSettingsService : public SettingsService { void onMqttConnect(bool sessionPresent); void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); void configureMQTT(); - }; #endif // end MQTTSettingsService_h diff --git a/lib/framework/MQTTStatus.cpp b/lib/framework/MQTTStatus.cpp index 723f757c..65cc05f6 100644 --- a/lib/framework/MQTTStatus.cpp +++ b/lib/framework/MQTTStatus.cpp @@ -17,7 +17,7 @@ void MQTTStatus::mqttStatus(AsyncWebServerRequest* request) { root["enabled"] = _mqttSettingsService->isEnabled(); root["connected"] = _mqttSettingsService->isConnected(); root["client_id"] = _mqttSettingsService->getClientId(); - root["disconnect_reason"] = (uint8_t) _mqttSettingsService->getDisconnectReason(); + root["disconnect_reason"] = (uint8_t)_mqttSettingsService->getDisconnectReason(); response->setLength(); request->send(response); diff --git a/lib/framework/MqttPubSub.h b/lib/framework/MqttPubSub.h index bdb93d47..87490f34 100644 --- a/lib/framework/MqttPubSub.h +++ b/lib/framework/MqttPubSub.h @@ -1,7 +1,7 @@ #ifndef MqttPubSub_h #define MqttPubSub_h -#include +#include #include #include #include @@ -12,11 +12,11 @@ template class MqttConnector { protected: - SettingsService* _settingsService; + StatefulService* _statefulService; AsyncMqttClient* _mqttClient; - MqttConnector(SettingsService* settingsService, AsyncMqttClient* mqttClient) : - _settingsService(settingsService), _mqttClient(mqttClient) { + MqttConnector(StatefulService* statefulService, AsyncMqttClient* mqttClient) : + _statefulService(statefulService), _mqttClient(mqttClient) { _mqttClient->onConnect(std::bind(&MqttConnector::onConnect, this)); } @@ -27,11 +27,11 @@ template class MqttPub : virtual public MqttConnector { public: MqttPub(JsonSerializer jsonSerializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncMqttClient* mqttClient, String pubTopic = "") : - MqttConnector(settingsService, mqttClient), _jsonSerializer(jsonSerializer), _pubTopic(pubTopic) { - MqttConnector::_settingsService->addUpdateHandler([&](String originId) { publish(); }, false); + MqttConnector(statefulService, mqttClient), _jsonSerializer(jsonSerializer), _pubTopic(pubTopic) { + MqttConnector::_statefulService->addUpdateHandler([&](String originId) { publish(); }, false); } void setPubTopic(String pubTopic) { @@ -53,7 +53,7 @@ class MqttPub : virtual public MqttConnector { // serialize to json doc DynamicJsonDocument json(MAX_MESSAGE_SIZE); JsonObject jsonObject = json.to(); - MqttConnector::_settingsService->read(jsonObject, _jsonSerializer); + MqttConnector::_statefulService->read(jsonObject, _jsonSerializer); // serialize to string String payload; @@ -69,10 +69,10 @@ template class MqttSub : virtual public MqttConnector { public: MqttSub(JsonDeserializer jsonDeserializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncMqttClient* mqttClient, String subTopic = "") : - MqttConnector(settingsService, mqttClient), _jsonDeserializer(jsonDeserializer), _subTopic(subTopic) { + MqttConnector(statefulService, mqttClient), _jsonDeserializer(jsonDeserializer), _subTopic(subTopic) { MqttConnector::_mqttClient->onMessage(std::bind(&MqttSub::onMqttMessage, this, std::placeholders::_1, @@ -126,7 +126,7 @@ class MqttSub : virtual public MqttConnector { DeserializationError error = deserializeJson(json, payload, len); if (!error && json.is()) { JsonObject jsonObject = json.as(); - MqttConnector::_settingsService->update(jsonObject, _jsonDeserializer, MQTT_ORIGIN_ID); + MqttConnector::_statefulService->update(jsonObject, _jsonDeserializer, MQTT_ORIGIN_ID); } } }; @@ -136,13 +136,13 @@ class MqttPubSub : public MqttPub, public MqttSub { public: MqttPubSub(JsonSerializer jsonSerializer, JsonDeserializer jsonDeserializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncMqttClient* mqttClient, String pubTopic = "", String subTopic = "") : - MqttConnector(settingsService, mqttClient), - MqttPub(jsonSerializer, settingsService, mqttClient, pubTopic = ""), - MqttSub(jsonDeserializer, settingsService, mqttClient, subTopic = "") { + MqttConnector(statefulService, mqttClient), + MqttPub(jsonSerializer, statefulService, mqttClient, pubTopic = ""), + MqttSub(jsonDeserializer, statefulService, mqttClient, subTopic = "") { } public: diff --git a/lib/framework/NTPSettingsService.cpp b/lib/framework/NTPSettingsService.cpp index db417865..e6394f1d 100644 --- a/lib/framework/NTPSettingsService.cpp +++ b/lib/framework/NTPSettingsService.cpp @@ -2,11 +2,11 @@ NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : _httpEndpoint(NTPSettings::serialize, - NTPSettings::deserialize, - this, - server, - NTP_SETTINGS_SERVICE_PATH, - securityManager), + NTPSettings::deserialize, + this, + server, + NTP_SETTINGS_SERVICE_PATH, + securityManager), _fsPersistence(NTPSettings::serialize, NTPSettings::deserialize, this, fs, NTP_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent( @@ -51,12 +51,12 @@ void NTPSettingsService::onStationModeDisconnected(const WiFiEventStationModeDis #endif void NTPSettingsService::configureNTP() { - if (WiFi.isConnected() && _settings.enabled) { + if (WiFi.isConnected() && _state.enabled) { Serial.println("Starting NTP..."); #ifdef ESP32 - configTzTime(_settings.tzFormat.c_str(), _settings.server.c_str()); + configTzTime(_state.tzFormat.c_str(), _state.server.c_str()); #elif defined(ESP8266) - configTime(_settings.tzFormat.c_str(), _settings.server.c_str()); + configTime(_state.tzFormat.c_str(), _state.server.c_str()); #endif } else { sntp_stop(); diff --git a/lib/framework/NTPSettingsService.h b/lib/framework/NTPSettingsService.h index a8fc97df..1782fc79 100644 --- a/lib/framework/NTPSettingsService.h +++ b/lib/framework/NTPSettingsService.h @@ -42,7 +42,7 @@ class NTPSettings { } }; -class NTPSettingsService : public SettingsService { +class NTPSettingsService : public StatefulService { public: NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); diff --git a/lib/framework/OTASettingsService.cpp b/lib/framework/OTASettingsService.cpp index 40eec2d0..887a664c 100644 --- a/lib/framework/OTASettingsService.cpp +++ b/lib/framework/OTASettingsService.cpp @@ -1,7 +1,12 @@ #include OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _httpEndpoint(OTASettings::serialize, OTASettings::deserialize, this, server, OTA_SETTINGS_SERVICE_PATH, securityManager), + _httpEndpoint(OTASettings::serialize, + OTASettings::deserialize, + this, + server, + OTA_SETTINGS_SERVICE_PATH, + securityManager), _fsPersistence(OTASettings::serialize, OTASettings::deserialize, this, fs, OTA_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), @@ -19,7 +24,7 @@ void OTASettingsService::begin() { } void OTASettingsService::loop() { - if (_settings.enabled && _arduinoOTA) { + if (_state.enabled && _arduinoOTA) { _arduinoOTA->handle(); } } @@ -32,11 +37,11 @@ void OTASettingsService::configureArduinoOTA() { delete _arduinoOTA; _arduinoOTA = nullptr; } - if (_settings.enabled) { + if (_state.enabled) { Serial.println("Starting OTA Update Service..."); _arduinoOTA = new ArduinoOTAClass; - _arduinoOTA->setPort(_settings.port); - _arduinoOTA->setPassword(_settings.password.c_str()); + _arduinoOTA->setPort(_state.port); + _arduinoOTA->setPassword(_state.password.c_str()); _arduinoOTA->onStart([]() { Serial.println("Starting"); }); _arduinoOTA->onEnd([]() { Serial.println("\nEnd"); }); _arduinoOTA->onProgress([](unsigned int progress, unsigned int total) { diff --git a/lib/framework/OTASettingsService.h b/lib/framework/OTASettingsService.h index 3877ac53..1c7fbb85 100644 --- a/lib/framework/OTASettingsService.h +++ b/lib/framework/OTASettingsService.h @@ -40,7 +40,7 @@ class OTASettings { } }; -class OTASettingsService : public SettingsService { +class OTASettingsService : public StatefulService { public: OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); diff --git a/lib/framework/SecuritySettingsService.cpp b/lib/framework/SecuritySettingsService.cpp index fe952ddb..9fb68f08 100644 --- a/lib/framework/SecuritySettingsService.cpp +++ b/lib/framework/SecuritySettingsService.cpp @@ -2,11 +2,11 @@ SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : _httpEndpoint(SecuritySettings::serialize, - SecuritySettings::deserialize, - this, - server, - SECURITY_SETTINGS_PATH, - this), + SecuritySettings::deserialize, + this, + server, + SECURITY_SETTINGS_PATH, + this), _fsPersistence(SecuritySettings::serialize, SecuritySettings::deserialize, this, fs, SECURITY_SETTINGS_FILE) { addUpdateHandler([&](String originId) { configureJWTHandler(); }, false); } @@ -33,7 +33,7 @@ Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerReques } void SecuritySettingsService::configureJWTHandler() { - _jwtHandler.setSecret(_settings.jwtSecret); + _jwtHandler.setSecret(_state.jwtSecret); } Authentication SecuritySettingsService::authenticateJWT(String& jwt) { @@ -42,7 +42,7 @@ Authentication SecuritySettingsService::authenticateJWT(String& jwt) { if (payloadDocument.is()) { JsonObject parsedPayload = payloadDocument.as(); String username = parsedPayload["username"]; - for (User _user : _settings.users) { + for (User _user : _state.users) { if (_user.username == username && validatePayload(parsedPayload, &_user)) { return Authentication(_user); } @@ -52,7 +52,7 @@ Authentication SecuritySettingsService::authenticateJWT(String& jwt) { } Authentication SecuritySettingsService::authenticate(String& username, String& password) { - for (User _user : _settings.users) { + for (User _user : _state.users) { if (_user.username == username && _user.password == password) { return Authentication(_user); } diff --git a/lib/framework/SecuritySettingsService.h b/lib/framework/SecuritySettingsService.h index 94239357..02213db1 100644 --- a/lib/framework/SecuritySettingsService.h +++ b/lib/framework/SecuritySettingsService.h @@ -47,7 +47,7 @@ class SecuritySettings { } }; -class SecuritySettingsService : public SettingsService, public SecurityManager { +class SecuritySettingsService : public StatefulService, public SecurityManager { public: SecuritySettingsService(AsyncWebServer* server, FS* fs); diff --git a/lib/framework/SettingsService.h b/lib/framework/SettingsService.h deleted file mode 100644 index 8a8bf796..00000000 --- a/lib/framework/SettingsService.h +++ /dev/null @@ -1,136 +0,0 @@ -#ifndef SettingsService_h -#define SettingsService_h - -#include -#include -#include - -#include -#include -#ifdef ESP32 -#include -#include -#endif - -typedef size_t update_handler_id_t; -typedef std::function SettingsUpdateCallback; -static update_handler_id_t currentUpdatedHandlerId; - -typedef struct SettingsUpdateHandlerInfo { - update_handler_id_t _id; - SettingsUpdateCallback _cb; - bool _allowRemove; - SettingsUpdateHandlerInfo(SettingsUpdateCallback cb, bool allowRemove) : - _id(++currentUpdatedHandlerId), _cb(cb), _allowRemove(allowRemove){}; -} SettingsUpdateHandlerInfo_t; - -template -class SettingsService { - public: - template -#ifdef ESP32 - SettingsService(Args&&... args) : - _settings(std::forward(args)...), _updateMutex(xSemaphoreCreateRecursiveMutex()) { - } -#else - SettingsService(Args&&... args) : _settings(std::forward(args)...) { - } -#endif - - update_handler_id_t addUpdateHandler(SettingsUpdateCallback cb, bool allowRemove = true) { - if (!cb) { - return 0; - } - SettingsUpdateHandlerInfo_t updateHandler(cb, allowRemove); - _settingsUpdateHandlers.push_back(updateHandler); - return updateHandler._id; - } - - void removeUpdateHandler(update_handler_id_t id) { - for (auto i = _settingsUpdateHandlers.begin(); i != _settingsUpdateHandlers.end();) { - if ((*i)._allowRemove && (*i)._id == id) { - i = _settingsUpdateHandlers.erase(i); - } else { - ++i; - } - } - } - - void updateWithoutPropagation(std::function callback) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); -#endif - callback(_settings); -#ifdef ESP32 - xSemaphoreGiveRecursive(_updateMutex); -#endif - } - - void updateWithoutPropagation(JsonObject& jsonObject, JsonDeserializer deserializer) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); -#endif - deserializer(jsonObject, _settings); -#ifdef ESP32 - xSemaphoreGiveRecursive(_updateMutex); -#endif - } - - void update(std::function callback, String originId) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); -#endif - callback(_settings); - callUpdateHandlers(originId); -#ifdef ESP32 - xSemaphoreGiveRecursive(_updateMutex); -#endif - } - - void update(JsonObject& jsonObject, JsonDeserializer deserializer, String originId) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); -#endif - deserializer(jsonObject, _settings); - callUpdateHandlers(originId); -#ifdef ESP32 - xSemaphoreGiveRecursive(_updateMutex); -#endif - } - - void read(std::function callback) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); -#endif - callback(_settings); -#ifdef ESP32 - xSemaphoreGiveRecursive(_updateMutex); -#endif - } - - void read(JsonObject& jsonObject, JsonSerializer serializer) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_updateMutex, portMAX_DELAY); -#endif - serializer(_settings, jsonObject); -#ifdef ESP32 - xSemaphoreGiveRecursive(_updateMutex); -#endif - } - - void callUpdateHandlers(String originId) { - for (const SettingsUpdateHandlerInfo_t& handler : _settingsUpdateHandlers) { - handler._cb(originId); - } - } - - protected: - T _settings; -#ifdef ESP32 - SemaphoreHandle_t _updateMutex; -#endif - private: - std::list _settingsUpdateHandlers; -}; - -#endif // end SettingsService_h diff --git a/lib/framework/StatefulService.h b/lib/framework/StatefulService.h new file mode 100644 index 00000000..7a8f4742 --- /dev/null +++ b/lib/framework/StatefulService.h @@ -0,0 +1,137 @@ +#ifndef StatefulService_h +#define StatefulService_h + +#include +#include +#include + +#include +#include +#ifdef ESP32 +#include +#include +#endif + +typedef size_t update_handler_id_t; +typedef std::function StateUpdateCallback; +static update_handler_id_t currentUpdatedHandlerId; + +typedef struct StateUpdateHandlerInfo { + update_handler_id_t _id; + StateUpdateCallback _cb; + bool _allowRemove; + StateUpdateHandlerInfo(StateUpdateCallback cb, bool allowRemove) : + _id(++currentUpdatedHandlerId), _cb(cb), _allowRemove(allowRemove){}; +} StateUpdateHandlerInfo_t; + +template +class StatefulService { + public: + template +#ifdef ESP32 + StatefulService(Args&&... args) : + _state(std::forward(args)...), _accessMutex(xSemaphoreCreateRecursiveMutex()) { + } +#else + StatefulService(Args&&... args) : _state(std::forward(args)...) { + } +#endif + + update_handler_id_t addUpdateHandler(StateUpdateCallback cb, bool allowRemove = true) { + if (!cb) { + return 0; + } + StateUpdateHandlerInfo_t updateHandler(cb, allowRemove); + _updateHandlers.push_back(updateHandler); + return updateHandler._id; + } + + void removeUpdateHandler(update_handler_id_t id) { + for (auto i = _updateHandlers.begin(); i != _updateHandlers.end();) { + if ((*i)._allowRemove && (*i)._id == id) { + i = _updateHandlers.erase(i); + } else { + ++i; + } + } + } + + void updateWithoutPropagation(std::function callback) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); +#endif + callback(_state); +#ifdef ESP32 + xSemaphoreGiveRecursive(_accessMutex); +#endif + } + + void updateWithoutPropagation(JsonObject& jsonObject, JsonDeserializer deserializer) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); +#endif + deserializer(jsonObject, _state); +#ifdef ESP32 + xSemaphoreGiveRecursive(_accessMutex); +#endif + } + + void update(std::function callback, String originId) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); +#endif + callback(_state); + callUpdateHandlers(originId); +#ifdef ESP32 + xSemaphoreGiveRecursive(_accessMutex); +#endif + } + + void update(JsonObject& jsonObject, JsonDeserializer deserializer, String originId) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); +#endif + deserializer(jsonObject, _state); + callUpdateHandlers(originId); +#ifdef ESP32 + xSemaphoreGiveRecursive(_accessMutex); +#endif + } + + void read(std::function callback) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); +#endif + callback(_state); +#ifdef ESP32 + xSemaphoreGiveRecursive(_accessMutex); +#endif + } + + void read(JsonObject& jsonObject, JsonSerializer serializer) { +#ifdef ESP32 + xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); +#endif + serializer(_state, jsonObject); +#ifdef ESP32 + xSemaphoreGiveRecursive(_accessMutex); +#endif + } + + void callUpdateHandlers(String originId) { + for (const StateUpdateHandlerInfo_t& updateHandler : _updateHandlers) { + updateHandler._cb(originId); + } + } + + protected: + T _state; + + private: +#ifdef ESP32 + SemaphoreHandle_t _accessMutex; +#endif + std::list _updateHandlers; +}; + +#endif // end StatefulService_h diff --git a/lib/framework/WebSocketTxRx.h b/lib/framework/WebSocketTxRx.h index 48216d90..9bee4da2 100644 --- a/lib/framework/WebSocketTxRx.h +++ b/lib/framework/WebSocketTxRx.h @@ -1,7 +1,7 @@ #ifndef WebSocketTxRx_h #define WebSocketTxRx_h -#include +#include #include #include #include @@ -15,16 +15,16 @@ template class WebSocketConnector { protected: - SettingsService* _settingsService; + StatefulService* _statefulService; AsyncWebServer* _server; AsyncWebSocket _webSocket; - WebSocketConnector(SettingsService* settingsService, + WebSocketConnector(StatefulService* statefulService, AsyncWebServer* server, char const* socketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - _settingsService(settingsService), _server(server), _webSocket(socketPath) { + _statefulService(statefulService), _server(server), _webSocket(socketPath) { _webSocket.setFilter(securityManager->filterRequest(authenticationPredicate)); _webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent, this, @@ -38,8 +38,8 @@ class WebSocketConnector { _server->on(socketPath, HTTP_GET, std::bind(&WebSocketConnector::forbidden, this, std::placeholders::_1)); } - WebSocketConnector(SettingsService* settingsService, AsyncWebServer* server, char const* socketPath) : - _settingsService(settingsService), _server(server), _webSocket(socketPath) { + WebSocketConnector(StatefulService* statefulService, AsyncWebServer* server, char const* socketPath) : + _statefulService(statefulService), _server(server), _webSocket(socketPath) { _webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent, this, std::placeholders::_1, @@ -72,23 +72,23 @@ template class WebSocketTx : virtual public WebSocketConnector { public: WebSocketTx(JsonSerializer jsonSerializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, char const* socketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - WebSocketConnector(settingsService, server, socketPath, securityManager, authenticationPredicate), + WebSocketConnector(statefulService, server, socketPath, securityManager, authenticationPredicate), _jsonSerializer(jsonSerializer) { - WebSocketConnector::_settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, + WebSocketConnector::_statefulService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); } WebSocketTx(JsonSerializer jsonSerializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, char const* socketPath) : - WebSocketConnector(settingsService, server, socketPath), _jsonSerializer(jsonSerializer) { - WebSocketConnector::_settingsService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, + WebSocketConnector(statefulService, server, socketPath), _jsonSerializer(jsonSerializer) { + WebSocketConnector::_statefulService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); } @@ -135,7 +135,7 @@ class WebSocketTx : virtual public WebSocketConnector { root["type"] = "payload"; root["origin_id"] = originId; JsonObject payload = root.createNestedObject("payload"); - WebSocketConnector::_settingsService->read(payload, _jsonSerializer); + WebSocketConnector::_statefulService->read(payload, _jsonSerializer); size_t len = measureJson(jsonDocument); AsyncWebSocketMessageBuffer* buffer = WebSocketConnector::_webSocket.makeBuffer(len); @@ -154,20 +154,20 @@ template class WebSocketRx : virtual public WebSocketConnector { public: WebSocketRx(JsonDeserializer jsonDeserializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, char const* socketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - WebSocketConnector(settingsService, server, socketPath, securityManager, authenticationPredicate), + WebSocketConnector(statefulService, server, socketPath, securityManager, authenticationPredicate), _jsonDeserializer(jsonDeserializer) { } WebSocketRx(JsonDeserializer jsonDeserializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, char const* socketPath) : - WebSocketConnector(settingsService, server, socketPath), _jsonDeserializer(jsonDeserializer) { + WebSocketConnector(statefulService, server, socketPath), _jsonDeserializer(jsonDeserializer) { } protected: @@ -185,7 +185,7 @@ class WebSocketRx : virtual public WebSocketConnector { DeserializationError error = deserializeJson(jsonDocument, (char*)data); if (!error && jsonDocument.is()) { JsonObject jsonObject = jsonDocument.as(); - WebSocketConnector::_settingsService->update( + WebSocketConnector::_statefulService->update( jsonObject, _jsonDeserializer, WebSocketConnector::clientId(client)); } } @@ -202,29 +202,24 @@ class WebSocketTxRx : public WebSocketTx, public WebSocketRx { public: WebSocketTxRx(JsonSerializer jsonSerializer, JsonDeserializer jsonDeserializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, char const* socketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - WebSocketConnector(settingsService, server, socketPath, securityManager, authenticationPredicate), - WebSocketTx(jsonSerializer, settingsService, server, socketPath, securityManager, authenticationPredicate), - WebSocketRx(jsonDeserializer, - settingsService, - server, - socketPath, - securityManager, - authenticationPredicate) { + WebSocketConnector(statefulService, server, socketPath, securityManager, authenticationPredicate), + WebSocketTx(jsonSerializer, statefulService, server, socketPath, securityManager, authenticationPredicate), + WebSocketRx(jsonDeserializer, statefulService, server, socketPath, securityManager, authenticationPredicate) { } WebSocketTxRx(JsonSerializer jsonSerializer, JsonDeserializer jsonDeserializer, - SettingsService* settingsService, + StatefulService* statefulService, AsyncWebServer* server, char const* socketPath) : - WebSocketConnector(settingsService, server, socketPath), - WebSocketTx(jsonSerializer, settingsService, server, socketPath), - WebSocketRx(jsonDeserializer, settingsService, server, socketPath) { + WebSocketConnector(statefulService, server, socketPath), + WebSocketTx(jsonSerializer, statefulService, server, socketPath), + WebSocketRx(jsonDeserializer, statefulService, server, socketPath) { } protected: diff --git a/lib/framework/WiFiSettingsService.cpp b/lib/framework/WiFiSettingsService.cpp index 91b59cc7..98637cde 100644 --- a/lib/framework/WiFiSettingsService.cpp +++ b/lib/framework/WiFiSettingsService.cpp @@ -2,11 +2,11 @@ WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : _httpEndpoint(WiFiSettings::serialize, - WiFiSettings::deserialize, - this, - server, - WIFI_SETTINGS_SERVICE_PATH, - securityManager), + WiFiSettings::deserialize, + this, + server, + WIFI_SETTINGS_SERVICE_PATH, + securityManager), _fsPersistence(WiFiSettings::serialize, WiFiSettings::deserialize, this, fs, WIFI_SETTINGS_FILE) { // We want the device to come up in opmode=0 (WIFI_OFF), when erasing the flash this is not the default. // If needed, we save opmode=0 before disabling persistence so the device boots with WiFi disabled in the future. @@ -63,27 +63,27 @@ void WiFiSettingsService::loop() { void WiFiSettingsService::manageSTA() { // Abort if already connected, or if we have no SSID - if (WiFi.isConnected() || _settings.ssid.length() == 0) { + if (WiFi.isConnected() || _state.ssid.length() == 0) { return; } // Connect or reconnect as required if ((WiFi.getMode() & WIFI_STA) == 0) { Serial.println("Connecting to WiFi."); - if (_settings.staticIPConfig) { + if (_state.staticIPConfig) { // configure for static IP - WiFi.config(_settings.localIP, _settings.gatewayIP, _settings.subnetMask, _settings.dnsIP1, _settings.dnsIP2); + WiFi.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2); } else { // configure for DHCP #ifdef ESP32 WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); - WiFi.setHostname(_settings.hostname.c_str()); + WiFi.setHostname(_state.hostname.c_str()); #elif defined(ESP8266) WiFi.config(INADDR_ANY, INADDR_ANY, INADDR_ANY); - WiFi.hostname(_settings.hostname); + WiFi.hostname(_state.hostname); #endif } // attempt to connect to the network - WiFi.begin(_settings.ssid.c_str(), _settings.password.c_str()); + WiFi.begin(_state.ssid.c_str(), _state.password.c_str()); } } diff --git a/lib/framework/WiFiSettingsService.h b/lib/framework/WiFiSettingsService.h index 4d3f5c4c..6460ccda 100644 --- a/lib/framework/WiFiSettingsService.h +++ b/lib/framework/WiFiSettingsService.h @@ -1,6 +1,7 @@ #ifndef WiFiSettingsService_h #define WiFiSettingsService_h +#include #include #include #include @@ -68,7 +69,7 @@ class WiFiSettings { } }; -class WiFiSettingsService : public SettingsService { +class WiFiSettingsService : public StatefulService { public: WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); diff --git a/src/LightBrokerSettingsService.h b/src/LightBrokerSettingsService.h index 4ce92795..2b94c1a7 100644 --- a/src/LightBrokerSettingsService.h +++ b/src/LightBrokerSettingsService.h @@ -3,7 +3,6 @@ #include #include -#include #define LIGHT_BROKER_SETTINGS_FILE "/config/brokerSettings.json" #define LIGHT_BROKER_SETTINGS_PATH "/rest/brokerSettings" @@ -35,7 +34,7 @@ class LightBrokerSettings { } }; -class LightBrokerSettingsService : public SettingsService { +class LightBrokerSettingsService : public StatefulService { public: LightBrokerSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); void begin(); diff --git a/src/LightSettingsService.cpp b/src/LightSettingsService.cpp index 57fcd61a..6057858e 100644 --- a/src/LightSettingsService.cpp +++ b/src/LightSettingsService.cpp @@ -35,12 +35,12 @@ LightSettingsService::LightSettingsService(AsyncWebServer* server, } void LightSettingsService::begin() { - _settings.ledOn = DEFAULT_LED_STATE; + _state.ledOn = DEFAULT_LED_STATE; onConfigUpdated(); } void LightSettingsService::onConfigUpdated() { - digitalWrite(BLINK_LED, _settings.ledOn ? LED_ON : LED_OFF); + digitalWrite(BLINK_LED, _state.ledOn ? LED_ON : LED_OFF); } void LightSettingsService::registerConfig() { diff --git a/src/LightSettingsService.h b/src/LightSettingsService.h index 3b1d5dc3..3d348e50 100644 --- a/src/LightSettingsService.h +++ b/src/LightSettingsService.h @@ -6,7 +6,6 @@ #include #include #include -#include #define BLINK_LED 2 #define PRINT_DELAY 5000 @@ -50,7 +49,7 @@ class LightSettings { } }; -class LightSettingsService : public SettingsService { +class LightSettingsService : public StatefulService { public: LightSettingsService(AsyncWebServer* server, SecurityManager* securityManager, From e35ebb330c2aaae4088c8cfb5c22ec52f0c52f8c Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sun, 10 May 2020 22:11:52 +0100 Subject: [PATCH 24/35] introduce "core" and "util" directories --- lib/{framework => core}/FSPersistence.h | 0 lib/{framework => core}/HttpEndpoint.h | 0 lib/{framework => core}/JsonDeserializer.h | 0 lib/{framework => core}/JsonSerializer.h | 0 lib/{framework => core}/MqttPubSub.h | 0 lib/{framework => core}/StatefulService.h | 0 lib/{framework => core}/WebSocketTxRx.h | 0 lib/{framework => util}/ArduinoJsonJWT.cpp | 0 lib/{framework => util}/ArduinoJsonJWT.h | 0 lib/{framework => util}/JsonUtils.h | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename lib/{framework => core}/FSPersistence.h (100%) rename lib/{framework => core}/HttpEndpoint.h (100%) rename lib/{framework => core}/JsonDeserializer.h (100%) rename lib/{framework => core}/JsonSerializer.h (100%) rename lib/{framework => core}/MqttPubSub.h (100%) rename lib/{framework => core}/StatefulService.h (100%) rename lib/{framework => core}/WebSocketTxRx.h (100%) rename lib/{framework => util}/ArduinoJsonJWT.cpp (100%) rename lib/{framework => util}/ArduinoJsonJWT.h (100%) rename lib/{framework => util}/JsonUtils.h (100%) diff --git a/lib/framework/FSPersistence.h b/lib/core/FSPersistence.h similarity index 100% rename from lib/framework/FSPersistence.h rename to lib/core/FSPersistence.h diff --git a/lib/framework/HttpEndpoint.h b/lib/core/HttpEndpoint.h similarity index 100% rename from lib/framework/HttpEndpoint.h rename to lib/core/HttpEndpoint.h diff --git a/lib/framework/JsonDeserializer.h b/lib/core/JsonDeserializer.h similarity index 100% rename from lib/framework/JsonDeserializer.h rename to lib/core/JsonDeserializer.h diff --git a/lib/framework/JsonSerializer.h b/lib/core/JsonSerializer.h similarity index 100% rename from lib/framework/JsonSerializer.h rename to lib/core/JsonSerializer.h diff --git a/lib/framework/MqttPubSub.h b/lib/core/MqttPubSub.h similarity index 100% rename from lib/framework/MqttPubSub.h rename to lib/core/MqttPubSub.h diff --git a/lib/framework/StatefulService.h b/lib/core/StatefulService.h similarity index 100% rename from lib/framework/StatefulService.h rename to lib/core/StatefulService.h diff --git a/lib/framework/WebSocketTxRx.h b/lib/core/WebSocketTxRx.h similarity index 100% rename from lib/framework/WebSocketTxRx.h rename to lib/core/WebSocketTxRx.h diff --git a/lib/framework/ArduinoJsonJWT.cpp b/lib/util/ArduinoJsonJWT.cpp similarity index 100% rename from lib/framework/ArduinoJsonJWT.cpp rename to lib/util/ArduinoJsonJWT.cpp diff --git a/lib/framework/ArduinoJsonJWT.h b/lib/util/ArduinoJsonJWT.h similarity index 100% rename from lib/framework/ArduinoJsonJWT.h rename to lib/util/ArduinoJsonJWT.h diff --git a/lib/framework/JsonUtils.h b/lib/util/JsonUtils.h similarity index 100% rename from lib/framework/JsonUtils.h rename to lib/util/JsonUtils.h From 8adcf96f92ea3f6916423fccc930e85cd5d2d2f4 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Mon, 11 May 2020 23:19:54 +0100 Subject: [PATCH 25/35] move util and core back to framework for now --- lib/{util => framework}/ArduinoJsonJWT.cpp | 0 lib/{util => framework}/ArduinoJsonJWT.h | 0 lib/{core => framework}/FSPersistence.h | 0 lib/{core => framework}/HttpEndpoint.h | 0 lib/{core => framework}/JsonDeserializer.h | 0 lib/{core => framework}/JsonSerializer.h | 0 lib/{util => framework}/JsonUtils.h | 0 lib/{core => framework}/MqttPubSub.h | 0 lib/{core => framework}/StatefulService.h | 0 lib/{core => framework}/WebSocketTxRx.h | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename lib/{util => framework}/ArduinoJsonJWT.cpp (100%) rename lib/{util => framework}/ArduinoJsonJWT.h (100%) rename lib/{core => framework}/FSPersistence.h (100%) rename lib/{core => framework}/HttpEndpoint.h (100%) rename lib/{core => framework}/JsonDeserializer.h (100%) rename lib/{core => framework}/JsonSerializer.h (100%) rename lib/{util => framework}/JsonUtils.h (100%) rename lib/{core => framework}/MqttPubSub.h (100%) rename lib/{core => framework}/StatefulService.h (100%) rename lib/{core => framework}/WebSocketTxRx.h (100%) diff --git a/lib/util/ArduinoJsonJWT.cpp b/lib/framework/ArduinoJsonJWT.cpp similarity index 100% rename from lib/util/ArduinoJsonJWT.cpp rename to lib/framework/ArduinoJsonJWT.cpp diff --git a/lib/util/ArduinoJsonJWT.h b/lib/framework/ArduinoJsonJWT.h similarity index 100% rename from lib/util/ArduinoJsonJWT.h rename to lib/framework/ArduinoJsonJWT.h diff --git a/lib/core/FSPersistence.h b/lib/framework/FSPersistence.h similarity index 100% rename from lib/core/FSPersistence.h rename to lib/framework/FSPersistence.h diff --git a/lib/core/HttpEndpoint.h b/lib/framework/HttpEndpoint.h similarity index 100% rename from lib/core/HttpEndpoint.h rename to lib/framework/HttpEndpoint.h diff --git a/lib/core/JsonDeserializer.h b/lib/framework/JsonDeserializer.h similarity index 100% rename from lib/core/JsonDeserializer.h rename to lib/framework/JsonDeserializer.h diff --git a/lib/core/JsonSerializer.h b/lib/framework/JsonSerializer.h similarity index 100% rename from lib/core/JsonSerializer.h rename to lib/framework/JsonSerializer.h diff --git a/lib/util/JsonUtils.h b/lib/framework/JsonUtils.h similarity index 100% rename from lib/util/JsonUtils.h rename to lib/framework/JsonUtils.h diff --git a/lib/core/MqttPubSub.h b/lib/framework/MqttPubSub.h similarity index 100% rename from lib/core/MqttPubSub.h rename to lib/framework/MqttPubSub.h diff --git a/lib/core/StatefulService.h b/lib/framework/StatefulService.h similarity index 100% rename from lib/core/StatefulService.h rename to lib/framework/StatefulService.h diff --git a/lib/core/WebSocketTxRx.h b/lib/framework/WebSocketTxRx.h similarity index 100% rename from lib/core/WebSocketTxRx.h rename to lib/framework/WebSocketTxRx.h From 4f48c74bf59988c95ce98f89f32f31e4d502e4aa Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Tue, 12 May 2020 22:51:09 +0100 Subject: [PATCH 26/35] two stage rename for mqtt --- README.md | 2 +- .../src/mqtt/{MQTT.tsx => MQTT.tsx.rename} | 8 +-- ....tsx => MQTTSettingsController.tsx.rename} | 12 ++--- ...gsForm.tsx => MQTTSettingsForm.tsx.rename} | 8 +-- .../{MQTTStatus.ts => MQTTStatus.ts.rename} | 24 ++++----- ...er.tsx => MQTTStatusController.tsx.rename} | 12 ++--- ...atusForm.tsx => MQTTStatusForm.tsx.rename} | 10 ++-- interface/src/mqtt/types.ts | 8 +-- lib/framework/ESP8266React.h | 10 ++-- ...ice.cpp => MQTTSettingsService.cpp.rename} | 54 +++++++++---------- ...Service.h => MQTTSettingsService.h.rename} | 22 ++++---- .../{MQTTStatus.cpp => MQTTStatus.cpp.rename} | 10 ++-- .../{MQTTStatus.h => MQTTStatus.h.rename} | 14 ++--- 13 files changed, 97 insertions(+), 97 deletions(-) rename interface/src/mqtt/{MQTT.tsx => MQTT.tsx.rename} (86%) rename interface/src/mqtt/{MQTTSettingsController.tsx => MQTTSettingsController.tsx.rename} (57%) rename interface/src/mqtt/{MQTTSettingsForm.tsx => MQTTSettingsForm.tsx.rename} (95%) rename interface/src/mqtt/{MQTTStatus.ts => MQTTStatus.ts.rename} (51%) rename interface/src/mqtt/{MQTTStatusController.tsx => MQTTStatusController.tsx.rename} (56%) rename interface/src/mqtt/{MQTTStatusForm.tsx => MQTTStatusForm.tsx.rename} (90%) rename lib/framework/{MQTTSettingsService.cpp => MQTTSettingsService.cpp.rename} (72%) rename lib/framework/{MQTTSettingsService.h => MQTTSettingsService.h.rename} (88%) rename lib/framework/{MQTTStatus.cpp => MQTTStatus.cpp.rename} (74%) rename lib/framework/{MQTTStatus.h => MQTTStatus.h.rename} (66%) diff --git a/README.md b/README.md index f48660bf..547e4636 100644 --- a/README.md +++ b/README.md @@ -552,7 +552,7 @@ getWiFiSettingsService() | Configures and manages the WiFi network connectio getAPSettingsService() | Configures and manages the Access Point getNTPSettingsService() | Configures and manages the network time getOTASettingsService() | Configures and manages the Over-The-Air update feature -getMQTTSettingsService() | Configures and manages the MQTT connection +getMqttSettingsService() | Configures and manages the MQTT connection getMQTTClient() | Provides direct access to the MQTT client instance These can be used to observe changes to settings. They can also be used to fetch or update settings. diff --git a/interface/src/mqtt/MQTT.tsx b/interface/src/mqtt/MQTT.tsx.rename similarity index 86% rename from interface/src/mqtt/MQTT.tsx rename to interface/src/mqtt/MQTT.tsx.rename index 50617a43..14fc7277 100644 --- a/interface/src/mqtt/MQTT.tsx +++ b/interface/src/mqtt/MQTT.tsx.rename @@ -5,8 +5,8 @@ import { Tabs, Tab } from '@material-ui/core'; import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication'; import { MenuAppBar } from '../components'; -import MQTTStatusController from './MQTTStatusController'; -import MQTTSettingsController from './MQTTSettingsController'; +import MqttStatusController from './MqttStatusController'; +import MqttSettingsController from './MqttSettingsController'; type MQTTProps = AuthenticatedContextProps & RouteComponentProps; @@ -25,8 +25,8 @@ class MQTT extends Component { - - + + diff --git a/interface/src/mqtt/MQTTSettingsController.tsx b/interface/src/mqtt/MQTTSettingsController.tsx.rename similarity index 57% rename from interface/src/mqtt/MQTTSettingsController.tsx rename to interface/src/mqtt/MQTTSettingsController.tsx.rename index 06bb0503..8cc9d160 100644 --- a/interface/src/mqtt/MQTTSettingsController.tsx +++ b/interface/src/mqtt/MQTTSettingsController.tsx.rename @@ -3,12 +3,12 @@ import React, { Component } from 'react'; import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components'; import { MQTT_SETTINGS_ENDPOINT } from '../api'; -import MQTTSettingsForm from './MQTTSettingsForm'; -import { MQTTSettings } from './types'; +import MqttSettingsForm from './MqttSettingsForm'; +import { MqttSettings } from './types'; -type MQTTSettingsControllerProps = RestControllerProps; +type MqttSettingsControllerProps = RestControllerProps; -class MQTTSettingsController extends Component { +class MqttSettingsController extends Component { componentDidMount() { this.props.loadData(); @@ -19,7 +19,7 @@ class MQTTSettingsController extends Component { } + render={formProps => } /> ) @@ -27,4 +27,4 @@ class MQTTSettingsController extends Component { } -export default restController(MQTT_SETTINGS_ENDPOINT, MQTTSettingsController); +export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController); diff --git a/interface/src/mqtt/MQTTSettingsForm.tsx b/interface/src/mqtt/MQTTSettingsForm.tsx.rename similarity index 95% rename from interface/src/mqtt/MQTTSettingsForm.tsx rename to interface/src/mqtt/MQTTSettingsForm.tsx.rename index 2ea256f1..1f5bf21c 100644 --- a/interface/src/mqtt/MQTTSettingsForm.tsx +++ b/interface/src/mqtt/MQTTSettingsForm.tsx.rename @@ -7,11 +7,11 @@ import SaveIcon from '@material-ui/icons/Save'; import { RestFormProps, FormActions, FormButton, BlockFormControlLabel, PasswordValidator } from '../components'; import { isIP, isHostname, or } from '../validators'; -import { MQTTSettings } from './types'; +import { MqttSettings } from './types'; -type MQTTSettingsFormProps = RestFormProps; +type MqttSettingsFormProps = RestFormProps; -class MQTTSettingsForm extends React.Component { +class MqttSettingsForm extends React.Component { componentDidMount() { ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname)); @@ -128,4 +128,4 @@ class MQTTSettingsForm extends React.Component { } } -export default MQTTSettingsForm; +export default MqttSettingsForm; diff --git a/interface/src/mqtt/MQTTStatus.ts b/interface/src/mqtt/MQTTStatus.ts.rename similarity index 51% rename from interface/src/mqtt/MQTTStatus.ts rename to interface/src/mqtt/MQTTStatus.ts.rename index d25fd56f..b9bb80cb 100644 --- a/interface/src/mqtt/MQTTStatus.ts +++ b/interface/src/mqtt/MQTTStatus.ts.rename @@ -1,7 +1,7 @@ import { Theme } from "@material-ui/core"; -import { MQTTStatus, MQTTDisconnectReason } from "./types"; +import { MqttStatus, MqttDisconnectReason } from "./types"; -export const mqttStatusHighlight = ({ enabled, connected }: MQTTStatus, theme: Theme) => { +export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => { if (!enabled) { return theme.palette.info.main; } @@ -11,7 +11,7 @@ export const mqttStatusHighlight = ({ enabled, connected }: MQTTStatus, theme: T return theme.palette.error.main; } -export const mqttStatus = ({ enabled, connected }: MQTTStatus) => { +export const mqttStatus = ({ enabled, connected }: MqttStatus) => { if (!enabled) { return "Not enabled"; } @@ -21,23 +21,23 @@ export const mqttStatus = ({ enabled, connected }: MQTTStatus) => { return "Disconnected"; } -export const disconnectReason = ({ disconnect_reason }: MQTTStatus) => { +export const disconnectReason = ({ disconnect_reason }: MqttStatus) => { switch (disconnect_reason) { - case MQTTDisconnectReason.TCP_DISCONNECTED: + case MqttDisconnectReason.TCP_DISCONNECTED: return "TCP disconnected"; - case MQTTDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION: + case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION: return "Unacceptable protocol version"; - case MQTTDisconnectReason.MQTT_IDENTIFIER_REJECTED: + case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED: return "Client ID rejected"; - case MQTTDisconnectReason.MQTT_SERVER_UNAVAILABLE: + case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE: return "Server unavailable"; - case MQTTDisconnectReason.MQTT_MALFORMED_CREDENTIALS: + case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS: return "Malformed credentials"; - case MQTTDisconnectReason.MQTT_NOT_AUTHORIZED: + case MqttDisconnectReason.MQTT_NOT_AUTHORIZED: return "Not authorized"; - case MQTTDisconnectReason.ESP8266_NOT_ENOUGH_SPACE: + case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE: return "Device out of memory"; - case MQTTDisconnectReason.TLS_BAD_FINGERPRINT: + case MqttDisconnectReason.TLS_BAD_FINGERPRINT: return "Server fingerprint invalid"; default: return "Unknown" diff --git a/interface/src/mqtt/MQTTStatusController.tsx b/interface/src/mqtt/MQTTStatusController.tsx.rename similarity index 56% rename from interface/src/mqtt/MQTTStatusController.tsx rename to interface/src/mqtt/MQTTStatusController.tsx.rename index 28887985..4dd54097 100644 --- a/interface/src/mqtt/MQTTStatusController.tsx +++ b/interface/src/mqtt/MQTTStatusController.tsx.rename @@ -3,12 +3,12 @@ import React, { Component } from 'react'; import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components'; import { MQTT_STATUS_ENDPOINT } from '../api'; -import MQTTStatusForm from './MQTTStatusForm'; -import { MQTTStatus } from './types'; +import MqttStatusForm from './MqttStatusForm'; +import { MqttStatus } from './types'; -type MQTTStatusControllerProps = RestControllerProps; +type MqttStatusControllerProps = RestControllerProps; -class MQTTStatusController extends Component { +class MqttStatusController extends Component { componentDidMount() { this.props.loadData(); @@ -19,11 +19,11 @@ class MQTTStatusController extends Component { } + render={formProps => } /> ) } } -export default restController(MQTT_STATUS_ENDPOINT, MQTTStatusController); +export default restController(MQTT_STATUS_ENDPOINT, MqttStatusController); diff --git a/interface/src/mqtt/MQTTStatusForm.tsx b/interface/src/mqtt/MQTTStatusForm.tsx.rename similarity index 90% rename from interface/src/mqtt/MQTTStatusForm.tsx rename to interface/src/mqtt/MQTTStatusForm.tsx.rename index ebf3e802..5a80a416 100644 --- a/interface/src/mqtt/MQTTStatusForm.tsx +++ b/interface/src/mqtt/MQTTStatusForm.tsx.rename @@ -8,12 +8,12 @@ import RefreshIcon from '@material-ui/icons/Refresh'; import ReportIcon from '@material-ui/icons/Report'; import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components'; -import { mqttStatusHighlight, mqttStatus, disconnectReason } from './MQTTStatus'; -import { MQTTStatus } from './types'; +import { mqttStatusHighlight, mqttStatus, disconnectReason } from './MqttStatus'; +import { MqttStatus } from './types'; -type MQTTStatusFormProps = RestFormProps & WithTheme; +type MqttStatusFormProps = RestFormProps & WithTheme; -class MQTTStatusForm extends Component { +class MqttStatusForm extends Component { renderConnectionStatus() { const { data } = this.props @@ -80,4 +80,4 @@ class MQTTStatusForm extends Component { } -export default withTheme(MQTTStatusForm); +export default withTheme(MqttStatusForm); diff --git a/interface/src/mqtt/types.ts b/interface/src/mqtt/types.ts index e92ea5f4..04e20cac 100644 --- a/interface/src/mqtt/types.ts +++ b/interface/src/mqtt/types.ts @@ -1,4 +1,4 @@ -export enum MQTTDisconnectReason { +export enum MqttDisconnectReason { TCP_DISCONNECTED = 0, MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1, MQTT_IDENTIFIER_REJECTED = 2, @@ -9,14 +9,14 @@ export enum MQTTDisconnectReason { TLS_BAD_FINGERPRINT = 7 } -export interface MQTTStatus { +export interface MqttStatus { enabled: boolean; connected: boolean; client_id: string; - disconnect_reason: MQTTDisconnectReason; + disconnect_reason: MqttDisconnectReason; } -export interface MQTTSettings { +export interface MqttSettings { enabled: boolean; host: string; port: number; diff --git a/lib/framework/ESP8266React.h b/lib/framework/ESP8266React.h index 8af5d90d..7e7b1623 100644 --- a/lib/framework/ESP8266React.h +++ b/lib/framework/ESP8266React.h @@ -16,8 +16,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -63,7 +63,7 @@ class ESP8266React { return &_otaSettingsService; } - StatefulService* getMQTTSettingsService() { + StatefulService* getMqttSettingsService() { return &_mqttSettingsService; } @@ -77,7 +77,7 @@ class ESP8266React { APSettingsService _apSettingsService; NTPSettingsService _ntpSettingsService; OTASettingsService _otaSettingsService; - MQTTSettingsService _mqttSettingsService; + MqttSettingsService _mqttSettingsService; RestartService _restartService; AuthenticationService _authenticationService; @@ -86,7 +86,7 @@ class ESP8266React { WiFiStatus _wifiStatus; NTPStatus _ntpStatus; APStatus _apStatus; - MQTTStatus _mqttStatus; + MqttStatus _mqttStatus; SystemStatus _systemStatus; }; diff --git a/lib/framework/MQTTSettingsService.cpp b/lib/framework/MQTTSettingsService.cpp.rename similarity index 72% rename from lib/framework/MQTTSettingsService.cpp rename to lib/framework/MQTTSettingsService.cpp.rename index aaa4f15a..d3c8ed09 100644 --- a/lib/framework/MQTTSettingsService.cpp +++ b/lib/framework/MQTTSettingsService.cpp.rename @@ -1,4 +1,4 @@ -#include +#include /** * Retains a copy of the cstr provided in the pointer provided using dynamic allocation. @@ -20,39 +20,39 @@ static char* retainCstr(const char* cstr, char** ptr) { return *ptr; } -MQTTSettingsService::MQTTSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _httpEndpoint(MQTTSettings::serialize, - MQTTSettings::deserialize, +MqttSettingsService::MqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : + _httpEndpoint(MqttSettings::serialize, + MqttSettings::deserialize, this, server, MQTT_SETTINGS_SERVICE_PATH, securityManager), - _fsPersistence(MQTTSettings::serialize, MQTTSettings::deserialize, this, fs, MQTT_SETTINGS_FILE) { + _fsPersistence(MqttSettings::serialize, MqttSettings::deserialize, this, fs, MQTT_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent( - std::bind(&MQTTSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&MqttSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); - WiFi.onEvent(std::bind(&MQTTSettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), + WiFi.onEvent(std::bind(&MqttSettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); #elif defined(ESP8266) _onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected( - std::bind(&MQTTSettingsService::onStationModeDisconnected, this, std::placeholders::_1)); + std::bind(&MqttSettingsService::onStationModeDisconnected, this, std::placeholders::_1)); _onStationModeGotIPHandler = - WiFi.onStationModeGotIP(std::bind(&MQTTSettingsService::onStationModeGotIP, this, std::placeholders::_1)); + WiFi.onStationModeGotIP(std::bind(&MqttSettingsService::onStationModeGotIP, this, std::placeholders::_1)); #endif - _mqttClient.onConnect(std::bind(&MQTTSettingsService::onMqttConnect, this, std::placeholders::_1)); - _mqttClient.onDisconnect(std::bind(&MQTTSettingsService::onMqttDisconnect, this, std::placeholders::_1)); + _mqttClient.onConnect(std::bind(&MqttSettingsService::onMqttConnect, this, std::placeholders::_1)); + _mqttClient.onDisconnect(std::bind(&MqttSettingsService::onMqttDisconnect, this, std::placeholders::_1)); addUpdateHandler([&](String originId) { onConfigUpdated(); }, false); } -MQTTSettingsService::~MQTTSettingsService() { +MqttSettingsService::~MqttSettingsService() { } -void MQTTSettingsService::begin() { +void MqttSettingsService::begin() { _fsPersistence.readFromFS(); } -void MQTTSettingsService::loop() { +void MqttSettingsService::loop() { if (_reconfigureMqtt || (_disconnectedAt && (unsigned long)(millis() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) { // reconfigure MQTT client configureMQTT(); @@ -63,67 +63,67 @@ void MQTTSettingsService::loop() { } } -bool MQTTSettingsService::isEnabled() { +bool MqttSettingsService::isEnabled() { return _state.enabled; } -bool MQTTSettingsService::isConnected() { +bool MqttSettingsService::isConnected() { return _mqttClient.connected(); } -const char* MQTTSettingsService::getClientId() { +const char* MqttSettingsService::getClientId() { return _mqttClient.getClientId(); } -AsyncMqttClientDisconnectReason MQTTSettingsService::getDisconnectReason() { +AsyncMqttClientDisconnectReason MqttSettingsService::getDisconnectReason() { return _disconnectReason; } -AsyncMqttClient* MQTTSettingsService::getMqttClient() { +AsyncMqttClient* MqttSettingsService::getMqttClient() { return &_mqttClient; } -void MQTTSettingsService::onMqttConnect(bool sessionPresent) { +void MqttSettingsService::onMqttConnect(bool sessionPresent) { Serial.print("Connected to MQTT, "); Serial.print(sessionPresent ? "with" : "without"); Serial.println(" persistent session"); } -void MQTTSettingsService::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { +void MqttSettingsService::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { Serial.print("Disconnected from MQTT reason: "); Serial.println((uint8_t)reason); _disconnectReason = reason; _disconnectedAt = millis(); } -void MQTTSettingsService::onConfigUpdated() { +void MqttSettingsService::onConfigUpdated() { _reconfigureMqtt = true; _disconnectedAt = 0; } #ifdef ESP32 -void MQTTSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { +void MqttSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { if (_state.enabled) { Serial.println("WiFi connection dropped, starting MQTT client."); onConfigUpdated(); } } -void MQTTSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) { +void MqttSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) { if (_state.enabled) { Serial.println("WiFi connection dropped, stopping MQTT client."); onConfigUpdated(); } } #elif defined(ESP8266) -void MQTTSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP& event) { +void MqttSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP& event) { if (_state.enabled) { Serial.println("WiFi connection dropped, starting MQTT client."); onConfigUpdated(); } } -void MQTTSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) { +void MqttSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) { if (_state.enabled) { Serial.println("WiFi connection dropped, stopping MQTT client."); onConfigUpdated(); @@ -131,7 +131,7 @@ void MQTTSettingsService::onStationModeDisconnected(const WiFiEventStationModeDi } #endif -void MQTTSettingsService::configureMQTT() { +void MqttSettingsService::configureMQTT() { // disconnect if currently connected _mqttClient.disconnect(); diff --git a/lib/framework/MQTTSettingsService.h b/lib/framework/MQTTSettingsService.h.rename similarity index 88% rename from lib/framework/MQTTSettingsService.h rename to lib/framework/MQTTSettingsService.h.rename index 744af526..0dae0cac 100644 --- a/lib/framework/MQTTSettingsService.h +++ b/lib/framework/MQTTSettingsService.h.rename @@ -1,5 +1,5 @@ -#ifndef MQTTSettingsService_h -#define MQTTSettingsService_h +#ifndef MqttSettingsService_h +#define MqttSettingsService_h #include #include @@ -29,7 +29,7 @@ static String generateClientId() { #endif } -class MQTTSettings { +class MqttSettings { public: // host and port - if enabled bool enabled; @@ -48,7 +48,7 @@ class MQTTSettings { bool cleanSession; uint16_t maxTopicLength; - static void serialize(MQTTSettings& settings, JsonObject& root) { + static void serialize(MqttSettings& settings, JsonObject& root) { root["enabled"] = settings.enabled; root["host"] = settings.host; root["port"] = settings.port; @@ -60,7 +60,7 @@ class MQTTSettings { root["max_topic_length"] = settings.maxTopicLength; } - static void deserialize(JsonObject& root, MQTTSettings& settings) { + static void deserialize(JsonObject& root, MqttSettings& settings) { settings.enabled = root["enabled"] | MQTT_SETTINGS_SERVICE_DEFAULT_ENABLED; settings.host = root["host"] | MQTT_SETTINGS_SERVICE_DEFAULT_HOST; settings.port = root["port"] | MQTT_SETTINGS_SERVICE_DEFAULT_PORT; @@ -73,10 +73,10 @@ class MQTTSettings { } }; -class MQTTSettingsService : public StatefulService { +class MqttSettingsService : public StatefulService { public: - MQTTSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); - ~MQTTSettingsService(); + MqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); + ~MqttSettingsService(); void begin(); void loop(); @@ -90,8 +90,8 @@ class MQTTSettingsService : public StatefulService { void onConfigUpdated(); private: - HttpEndpoint _httpEndpoint; - FSPersistence _fsPersistence; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; // Pointers to hold retained copies of the mqtt client connection strings. // Required as AsyncMqttClient holds refrences to the supplied connection strings. @@ -122,4 +122,4 @@ class MQTTSettingsService : public StatefulService { void configureMQTT(); }; -#endif // end MQTTSettingsService_h +#endif // end MqttSettingsService_h diff --git a/lib/framework/MQTTStatus.cpp b/lib/framework/MQTTStatus.cpp.rename similarity index 74% rename from lib/framework/MQTTStatus.cpp rename to lib/framework/MQTTStatus.cpp.rename index 65cc05f6..8f5bac34 100644 --- a/lib/framework/MQTTStatus.cpp +++ b/lib/framework/MQTTStatus.cpp.rename @@ -1,16 +1,16 @@ -#include +#include -MQTTStatus::MQTTStatus(AsyncWebServer* server, - MQTTSettingsService* mqttSettingsService, +MqttStatus::MqttStatus(AsyncWebServer* server, + MqttSettingsService* mqttSettingsService, SecurityManager* securityManager) : _mqttSettingsService(mqttSettingsService) { server->on(MQTT_STATUS_SERVICE_PATH, HTTP_GET, - securityManager->wrapRequest(std::bind(&MQTTStatus::mqttStatus, this, std::placeholders::_1), + securityManager->wrapRequest(std::bind(&MqttStatus::mqttStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)); } -void MQTTStatus::mqttStatus(AsyncWebServerRequest* request) { +void MqttStatus::mqttStatus(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_MQTT_STATUS_SIZE); JsonObject root = response->getRoot(); diff --git a/lib/framework/MQTTStatus.h b/lib/framework/MQTTStatus.h.rename similarity index 66% rename from lib/framework/MQTTStatus.h rename to lib/framework/MQTTStatus.h.rename index 438e35cf..a726d3b5 100644 --- a/lib/framework/MQTTStatus.h +++ b/lib/framework/MQTTStatus.h.rename @@ -1,5 +1,5 @@ -#ifndef MQTTStatus_h -#define MQTTStatus_h +#ifndef MqttStatus_h +#define MqttStatus_h #ifdef ESP32 #include @@ -9,7 +9,7 @@ #include #endif -#include +#include #include #include #include @@ -18,14 +18,14 @@ #define MAX_MQTT_STATUS_SIZE 1024 #define MQTT_STATUS_SERVICE_PATH "/rest/mqttStatus" -class MQTTStatus { +class MqttStatus { public: - MQTTStatus(AsyncWebServer* server, MQTTSettingsService* mqttSettingsService, SecurityManager* securityManager); + MqttStatus(AsyncWebServer* server, MqttSettingsService* mqttSettingsService, SecurityManager* securityManager); private: - MQTTSettingsService* _mqttSettingsService; + MqttSettingsService* _mqttSettingsService; void mqttStatus(AsyncWebServerRequest* request); }; -#endif // end MQTTStatus_h +#endif // end MqttStatus_h From 189476ffe563bad13c1ff36aa0e3c9260e6debb1 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Tue, 12 May 2020 23:32:11 +0100 Subject: [PATCH 27/35] Complete two-part rename of MQTT front-end and back-end classes --- README.md | 2 +- interface/src/AppRouting.tsx | 4 ++-- interface/src/mqtt/{MQTT.tsx.rename => Mqtt.tsx} | 6 +++--- ...ingsController.tsx.rename => MqttSettingsController.tsx} | 0 .../{MQTTSettingsForm.tsx.rename => MqttSettingsForm.tsx} | 0 interface/src/mqtt/{MQTTStatus.ts.rename => MqttStatus.ts} | 0 ...StatusController.tsx.rename => MqttStatusController.tsx} | 0 .../mqtt/{MQTTStatusForm.tsx.rename => MqttStatusForm.tsx} | 0 lib/framework/ESP8266React.h | 2 +- ...TTSettingsService.cpp.rename => MQTTSettingsService.cpp} | 4 ++-- .../{MQTTSettingsService.h.rename => MQTTSettingsService.h} | 2 +- lib/framework/{MQTTStatus.cpp.rename => MQTTStatus.cpp} | 0 lib/framework/{MQTTStatus.h.rename => MQTTStatus.h} | 0 src/main.cpp | 2 +- 14 files changed, 11 insertions(+), 11 deletions(-) rename interface/src/mqtt/{MQTT.tsx.rename => Mqtt.tsx} (89%) rename interface/src/mqtt/{MQTTSettingsController.tsx.rename => MqttSettingsController.tsx} (100%) rename interface/src/mqtt/{MQTTSettingsForm.tsx.rename => MqttSettingsForm.tsx} (100%) rename interface/src/mqtt/{MQTTStatus.ts.rename => MqttStatus.ts} (100%) rename interface/src/mqtt/{MQTTStatusController.tsx.rename => MqttStatusController.tsx} (100%) rename interface/src/mqtt/{MQTTStatusForm.tsx.rename => MqttStatusForm.tsx} (100%) rename lib/framework/{MQTTSettingsService.cpp.rename => MQTTSettingsService.cpp} (98%) rename lib/framework/{MQTTSettingsService.h.rename => MQTTSettingsService.h} (99%) rename lib/framework/{MQTTStatus.cpp.rename => MQTTStatus.cpp} (100%) rename lib/framework/{MQTTStatus.h.rename => MQTTStatus.h} (100%) diff --git a/README.md b/README.md index 547e4636..fc76a03a 100644 --- a/README.md +++ b/README.md @@ -553,7 +553,7 @@ getAPSettingsService() | Configures and manages the Access Point getNTPSettingsService() | Configures and manages the network time getOTASettingsService() | Configures and manages the Over-The-Air update feature getMqttSettingsService() | Configures and manages the MQTT connection -getMQTTClient() | Provides direct access to the MQTT client instance +getMqttClient() | Provides direct access to the MQTT client instance These can be used to observe changes to settings. They can also be used to fetch or update settings. diff --git a/interface/src/AppRouting.tsx b/interface/src/AppRouting.tsx index f8d7874d..48eac829 100644 --- a/interface/src/AppRouting.tsx +++ b/interface/src/AppRouting.tsx @@ -15,7 +15,7 @@ import Security from './security/Security'; import System from './system/System'; import { PROJECT_PATH } from './api'; -import MQTT from './mqtt/MQTT'; +import Mqtt from './mqtt/Mqtt'; class AppRouting extends Component { @@ -32,7 +32,7 @@ class AppRouting extends Component { - + diff --git a/interface/src/mqtt/MQTT.tsx.rename b/interface/src/mqtt/Mqtt.tsx similarity index 89% rename from interface/src/mqtt/MQTT.tsx.rename rename to interface/src/mqtt/Mqtt.tsx index 14fc7277..8daca772 100644 --- a/interface/src/mqtt/MQTT.tsx.rename +++ b/interface/src/mqtt/Mqtt.tsx @@ -8,9 +8,9 @@ import { MenuAppBar } from '../components'; import MqttStatusController from './MqttStatusController'; import MqttSettingsController from './MqttSettingsController'; -type MQTTProps = AuthenticatedContextProps & RouteComponentProps; +type MqttProps = AuthenticatedContextProps & RouteComponentProps; -class MQTT extends Component { +class Mqtt extends Component { handleTabChange = (event: React.ChangeEvent<{}>, path: string) => { this.props.history.push(path); @@ -34,4 +34,4 @@ class MQTT extends Component { } } -export default withAuthenticatedContext(MQTT); +export default withAuthenticatedContext(Mqtt); diff --git a/interface/src/mqtt/MQTTSettingsController.tsx.rename b/interface/src/mqtt/MqttSettingsController.tsx similarity index 100% rename from interface/src/mqtt/MQTTSettingsController.tsx.rename rename to interface/src/mqtt/MqttSettingsController.tsx diff --git a/interface/src/mqtt/MQTTSettingsForm.tsx.rename b/interface/src/mqtt/MqttSettingsForm.tsx similarity index 100% rename from interface/src/mqtt/MQTTSettingsForm.tsx.rename rename to interface/src/mqtt/MqttSettingsForm.tsx diff --git a/interface/src/mqtt/MQTTStatus.ts.rename b/interface/src/mqtt/MqttStatus.ts similarity index 100% rename from interface/src/mqtt/MQTTStatus.ts.rename rename to interface/src/mqtt/MqttStatus.ts diff --git a/interface/src/mqtt/MQTTStatusController.tsx.rename b/interface/src/mqtt/MqttStatusController.tsx similarity index 100% rename from interface/src/mqtt/MQTTStatusController.tsx.rename rename to interface/src/mqtt/MqttStatusController.tsx diff --git a/interface/src/mqtt/MQTTStatusForm.tsx.rename b/interface/src/mqtt/MqttStatusForm.tsx similarity index 100% rename from interface/src/mqtt/MQTTStatusForm.tsx.rename rename to interface/src/mqtt/MqttStatusForm.tsx diff --git a/lib/framework/ESP8266React.h b/lib/framework/ESP8266React.h index 7e7b1623..3603d8b9 100644 --- a/lib/framework/ESP8266React.h +++ b/lib/framework/ESP8266React.h @@ -67,7 +67,7 @@ class ESP8266React { return &_mqttSettingsService; } - AsyncMqttClient* getMQTTClient() { + AsyncMqttClient* getMqttClient() { return _mqttSettingsService.getMqttClient(); } diff --git a/lib/framework/MQTTSettingsService.cpp.rename b/lib/framework/MQTTSettingsService.cpp similarity index 98% rename from lib/framework/MQTTSettingsService.cpp.rename rename to lib/framework/MQTTSettingsService.cpp index d3c8ed09..a6ae82b4 100644 --- a/lib/framework/MQTTSettingsService.cpp.rename +++ b/lib/framework/MQTTSettingsService.cpp @@ -55,7 +55,7 @@ void MqttSettingsService::begin() { void MqttSettingsService::loop() { if (_reconfigureMqtt || (_disconnectedAt && (unsigned long)(millis() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) { // reconfigure MQTT client - configureMQTT(); + configureMqtt(); // clear the reconnection flags _reconfigureMqtt = false; @@ -131,7 +131,7 @@ void MqttSettingsService::onStationModeDisconnected(const WiFiEventStationModeDi } #endif -void MqttSettingsService::configureMQTT() { +void MqttSettingsService::configureMqtt() { // disconnect if currently connected _mqttClient.disconnect(); diff --git a/lib/framework/MQTTSettingsService.h.rename b/lib/framework/MQTTSettingsService.h similarity index 99% rename from lib/framework/MQTTSettingsService.h.rename rename to lib/framework/MQTTSettingsService.h index 0dae0cac..552b1ec7 100644 --- a/lib/framework/MQTTSettingsService.h.rename +++ b/lib/framework/MQTTSettingsService.h @@ -119,7 +119,7 @@ class MqttSettingsService : public StatefulService { void onMqttConnect(bool sessionPresent); void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); - void configureMQTT(); + void configureMqtt(); }; #endif // end MqttSettingsService_h diff --git a/lib/framework/MQTTStatus.cpp.rename b/lib/framework/MQTTStatus.cpp similarity index 100% rename from lib/framework/MQTTStatus.cpp.rename rename to lib/framework/MQTTStatus.cpp diff --git a/lib/framework/MQTTStatus.h.rename b/lib/framework/MQTTStatus.h similarity index 100% rename from lib/framework/MQTTStatus.h.rename rename to lib/framework/MQTTStatus.h diff --git a/src/main.cpp b/src/main.cpp index ea9c980e..c5c833e5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,7 +11,7 @@ LightBrokerSettingsService lightBrokerSettingsService = LightBrokerSettingsService(&server, &SPIFFS, esp8266React.getSecurityManager()); LightSettingsService lightSettingsService = LightSettingsService(&server, esp8266React.getSecurityManager(), - esp8266React.getMQTTClient(), + esp8266React.getMqttClient(), &lightBrokerSettingsService); void setup() { From 8dc87f014afdd82907de37e056383a146eae4914 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Tue, 12 May 2020 23:34:27 +0100 Subject: [PATCH 28/35] fix broken rename --- .../{MQTTSettingsService.cpp => MQTTSettingsService.cpp.rename} | 0 .../{MQTTSettingsService.h => MQTTSettingsService.h.rename} | 0 lib/framework/{MQTTStatus.cpp => MQTTStatus.cpp.rename} | 0 lib/framework/{MQTTStatus.h => MQTTStatus.h.rename} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename lib/framework/{MQTTSettingsService.cpp => MQTTSettingsService.cpp.rename} (100%) rename lib/framework/{MQTTSettingsService.h => MQTTSettingsService.h.rename} (100%) rename lib/framework/{MQTTStatus.cpp => MQTTStatus.cpp.rename} (100%) rename lib/framework/{MQTTStatus.h => MQTTStatus.h.rename} (100%) diff --git a/lib/framework/MQTTSettingsService.cpp b/lib/framework/MQTTSettingsService.cpp.rename similarity index 100% rename from lib/framework/MQTTSettingsService.cpp rename to lib/framework/MQTTSettingsService.cpp.rename diff --git a/lib/framework/MQTTSettingsService.h b/lib/framework/MQTTSettingsService.h.rename similarity index 100% rename from lib/framework/MQTTSettingsService.h rename to lib/framework/MQTTSettingsService.h.rename diff --git a/lib/framework/MQTTStatus.cpp b/lib/framework/MQTTStatus.cpp.rename similarity index 100% rename from lib/framework/MQTTStatus.cpp rename to lib/framework/MQTTStatus.cpp.rename diff --git a/lib/framework/MQTTStatus.h b/lib/framework/MQTTStatus.h.rename similarity index 100% rename from lib/framework/MQTTStatus.h rename to lib/framework/MQTTStatus.h.rename From de330c80b99a6ac68e5fe4ef942756b484b2f95a Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Tue, 12 May 2020 23:35:05 +0100 Subject: [PATCH 29/35] fix broken rename --- .../{MQTTSettingsService.cpp.rename => MqttSettingsService.cpp} | 0 .../{MQTTSettingsService.h.rename => MqttSettingsService.h} | 0 lib/framework/{MQTTStatus.cpp.rename => MqttStatus.cpp} | 0 lib/framework/{MQTTStatus.h.rename => MqttStatus.h} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename lib/framework/{MQTTSettingsService.cpp.rename => MqttSettingsService.cpp} (100%) rename lib/framework/{MQTTSettingsService.h.rename => MqttSettingsService.h} (100%) rename lib/framework/{MQTTStatus.cpp.rename => MqttStatus.cpp} (100%) rename lib/framework/{MQTTStatus.h.rename => MqttStatus.h} (100%) diff --git a/lib/framework/MQTTSettingsService.cpp.rename b/lib/framework/MqttSettingsService.cpp similarity index 100% rename from lib/framework/MQTTSettingsService.cpp.rename rename to lib/framework/MqttSettingsService.cpp diff --git a/lib/framework/MQTTSettingsService.h.rename b/lib/framework/MqttSettingsService.h similarity index 100% rename from lib/framework/MQTTSettingsService.h.rename rename to lib/framework/MqttSettingsService.h diff --git a/lib/framework/MQTTStatus.cpp.rename b/lib/framework/MqttStatus.cpp similarity index 100% rename from lib/framework/MQTTStatus.cpp.rename rename to lib/framework/MqttStatus.cpp diff --git a/lib/framework/MQTTStatus.h.rename b/lib/framework/MqttStatus.h similarity index 100% rename from lib/framework/MQTTStatus.h.rename rename to lib/framework/MqttStatus.h From 437cac72169247b2611646d40b0f142711cf5305 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Tue, 12 May 2020 23:52:07 +0100 Subject: [PATCH 30/35] rename example project in keeping with removal of "Settings" concept from framework --- README.md | 64 +++++++++---------- interface/src/project/DemoInformation.tsx | 6 +- interface/src/project/DemoProject.tsx | 12 ++-- ...er.tsx => LightMqttSettingsController.tsx} | 14 ++-- ...oller.tsx => LightStateRestController.tsx} | 14 ++-- ...ler.tsx => LightStateSocketController.tsx} | 14 ++-- interface/src/project/types.ts | 4 +- src/LightBrokerSettingsService.cpp | 22 ------- src/LightMqttSettingsService.cpp | 16 +++++ ...gsService.h => LightMqttSettingsService.h} | 20 +++--- ...tingsService.cpp => LightStateService.cpp} | 34 +++++----- ...tSettingsService.h => LightStateService.h} | 34 +++++----- src/main.cpp | 20 +++--- 13 files changed, 134 insertions(+), 140 deletions(-) rename interface/src/project/{LightBrokerSettingsController.tsx => LightMqttSettingsController.tsx} (83%) rename interface/src/project/{LightSettingsRestController.tsx => LightStateRestController.tsx} (76%) rename interface/src/project/{LightSettingsSocketController.tsx => LightStateSocketController.tsx} (76%) delete mode 100644 src/LightBrokerSettingsService.cpp create mode 100644 src/LightMqttSettingsService.cpp rename src/{LightBrokerSettingsService.h => LightMqttSettingsService.h} (59%) rename src/{LightSettingsService.cpp => LightStateService.cpp} (58%) rename src/{LightSettingsService.h => LightStateService.h} (53%) diff --git a/README.md b/README.md index fc76a03a..1be1ab2d 100644 --- a/README.md +++ b/README.md @@ -341,13 +341,13 @@ The following diagram visualises how the framework's modular components fit toge The [SettingsService.h](lib/framework/SettingsService.h) class is a responsible for managing settings and interfacing with code which wants to change or respond to changes in them. You can define a data class to hold settings then build a SettingsService instance to manage them: ```cpp -class LightSettings { +class LightState { public: bool on = false; uint8_t brightness = 255; }; -class LightSettingsService : public SettingsService { +class LightStateService : public SettingsService { }; ``` @@ -355,14 +355,14 @@ You may listen for changes to settings by registering an update handler callback ```cpp // register an update handler -update_handler_id_t myUpdateHandler = lightSettingsService.addUpdateHandler( +update_handler_id_t myUpdateHandler = lightStateService.addUpdateHandler( [&](String originId) { Serial.println("The light settings have been updated"); } ); // remove the update handler -lightSettingsService.removeUpdateHandler(myUpdateHandler); +lightStateService.removeUpdateHandler(myUpdateHandler); ``` An "originId" is passed to the update handler which may be used to identify the origin of the update. The default origin values the framework provides are: @@ -376,7 +376,7 @@ websocket:{clientId} | An update sent over WebSocket (WebSocketRxTx) 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. ```cpp -lightSettingsService.read([&](LightSettings& settings) { +lightStateService.read([&](LightState& settings) { digitalWrite(LED_PIN, settings.on ? HIGH : LOW) }); ``` @@ -384,7 +384,7 @@ lightSettingsService.read([&](LightSettings& settings) { SettingsService also exposes an update function which allows the caller to update the settings with a callback. This approach automatically calls the registered update handlers when complete. The example below turns on the lights using the arbitrary origin "timer": ```cpp -lightSettingsService.update([&](LightSettings& settings) { +lightStateService.update([&](LightState& settings) { settings.on = true; // turn on the lights! }, "timer"); ``` @@ -396,17 +396,17 @@ When transmitting settings over HTTP, WebSockets, or MQTT they must to be marsha The static functions below can be used to facilitate the serialization/deserialization of the example settings: ```cpp -class LightSettings { +class LightState { public: bool on = false; uint8_t brightness = 255; - static void serialize(LightSettings& settings, JsonObject& root) { + static void serialize(LightState& settings, JsonObject& root) { root["on"] = settings.on; root["brightness"] = settings.brightness; } - static void deserialize(JsonObject& root, LightSettings& settings) { + static void deserialize(JsonObject& root, LightState& settings) { settings.on = root["on"] | false; settings.brightness = root["brightness"] | 255; } @@ -419,31 +419,31 @@ Copy the settings to a JsonObject using a serializer: ```cpp JsonObject jsonObject = jsonDocument.to(); -lightSettingsService->read(jsonObject, serializer); +lightStateService->read(jsonObject, serializer); ``` Update the settings from a JsonObject using a deserializer: ```cpp JsonObject jsonObject = jsonDocument.as(); -lightSettingsService->update(jsonObject, deserializer, "timer"); +lightStateService->update(jsonObject, deserializer, "timer"); ``` #### Endpoints The framework provides a [HttpEndpoint.h](lib/framework/HttpEndpoint.h) class which may be used to register GET and POST handlers to read and update the settings over HTTP. You may construct a HttpEndpoint 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: +The code below demonstrates how to extend the LightStateService class to provide an unsecured endpoint: ```cpp -class LightSettingsService : public SettingsService { +class LightStateService : public SettingsService { public: - LightSettingsService(AsyncWebServer* server) : - _httpEndpoint(LightSettings::serialize, LightSettings::deserialize, this, server, "/rest/lightSettings") { + LightStateService(AsyncWebServer* server) : + _httpEndpoint(LightState::serialize, LightState::deserialize, this, server, "/rest/lightSettings") { } private: - HttpEndpoint _httpEndpoint; + HttpEndpoint _httpEndpoint; }; ``` @@ -453,17 +453,17 @@ Endpoint security is provided by authentication predicates which are [documented [FSPersistence.h](lib/framework/FSPersistence.h) allows you to save settings to the filesystem. FSPersistence automatically writes changes to the file system when settings are updated. This feature can be disabled by calling `disableUpdateHandler()` if manual control of persistence is required. -The code below demonstrates how to extend the LightSettingsService class to provide persistence: +The code below demonstrates how to extend the LightStateService class to provide persistence: ```cpp -class LightSettingsService : public SettingsService { +class LightStateService : public SettingsService { public: - LightSettingsService(FS* fs) : - _fsPersistence(LightSettings::serialize, LightSettings::deserialize, this, fs, "/config/lightSettings.json") { + LightStateService(FS* fs) : + _fsPersistence(LightState::serialize, LightState::deserialize, this, fs, "/config/lightSettings.json") { } private: - FSPersistence _fsPersistence; + FSPersistence _fsPersistence; }; ``` @@ -471,17 +471,17 @@ class LightSettingsService : public SettingsService { [SettingsSocket.h](lib/framework/SettingsSocket.h) allows you to read and update settings over a WebSocket connection. SettingsSocket automatically pushes changes to all connected clients when settings are updated. -The code below demonstrates how to extend the LightSettingsService class to provide an unsecured websocket: +The code below demonstrates how to extend the LightStateService class to provide an unsecured websocket: ```cpp -class LightSettingsService : public SettingsService { +class LightStateService : public SettingsService { public: - LightSettingsService(AsyncWebServer* server) : - _settingsSocket(LightSettings::serialize, LightSettings::deserialize, this, server, "/ws/lightSettings"), { + LightStateService(AsyncWebServer* server) : + _settingsSocket(LightState::serialize, LightState::deserialize, this, server, "/ws/lightSettings"), { } private: - SettingsSocket _settingsSocket; + SettingsSocket _settingsSocket; }; ``` @@ -493,14 +493,14 @@ The framework includes an MQTT client which can be configured via the UI. MQTT r [SettingsBroker.h](lib/framework/SettingsBroker.h) allows you to read and update settings over a pair of MQTT topics. SettingsBroker automatically pushes changes to the pub topic and reads updates from the sub topic. -The code below demonstrates how to extend the LightSettingsService class to interface with MQTT: +The code below demonstrates how to extend the LightStateService class to interface with MQTT: ```cpp -class LightSettingsService : public SettingsService { +class LightStateService : public SettingsService { public: - LightSettingsService(AsyncMqttClient* mqttClient) : - _settingsBroker(LightSettings::serialize, - LightSettings::deserialize, + LightStateService(AsyncMqttClient* mqttClient) : + _settingsBroker(LightState::serialize, + LightState::deserialize, this, mqttClient, "homeassistant/light/my_light/set", @@ -508,7 +508,7 @@ class LightSettingsService : public SettingsService { } private: - SettingsBroker _settingsBroker; + SettingsBroker _settingsBroker; }; ``` diff --git a/interface/src/project/DemoInformation.tsx b/interface/src/project/DemoInformation.tsx index 17cdc51f..8a5d8cc0 100644 --- a/interface/src/project/DemoInformation.tsx +++ b/interface/src/project/DemoInformation.tsx @@ -65,7 +65,7 @@ class DemoInformation extends Component { - LightSettingsRestController.tsx + LightStateRestController.tsx A form which lets the user control the LED over a REST service. @@ -73,7 +73,7 @@ class DemoInformation extends Component { - LightSettingsSocketController.tsx + LightStateSocketController.tsx A form which lets the user control and monitor the status of the LED over WebSockets. @@ -81,7 +81,7 @@ class DemoInformation extends Component { - LightBrokerSettingsController.tsx + LightMqttSettingsController.tsx A form which lets the user change the MQTT settings for MQTT based control of the LED. diff --git a/interface/src/project/DemoProject.tsx b/interface/src/project/DemoProject.tsx index bdfa2d70..daffa426 100644 --- a/interface/src/project/DemoProject.tsx +++ b/interface/src/project/DemoProject.tsx @@ -8,9 +8,9 @@ import { MenuAppBar } from '../components'; import { AuthenticatedRoute } from '../authentication'; import DemoInformation from './DemoInformation'; -import LightSettingsRestController from './LightSettingsRestController'; -import LightSettingsSocketController from './LightSettingsSocketController'; -import LightBrokerSettingsController from './LightBrokerSettingsController'; +import LightStateRestController from './LightStateRestController'; +import LightStateSocketController from './LightStateSocketController'; +import LightMqttSettingsController from './LightMqttSettingsController'; class DemoProject extends Component { @@ -29,9 +29,9 @@ class DemoProject extends Component { - - - + + + diff --git a/interface/src/project/LightBrokerSettingsController.tsx b/interface/src/project/LightMqttSettingsController.tsx similarity index 83% rename from interface/src/project/LightBrokerSettingsController.tsx rename to interface/src/project/LightMqttSettingsController.tsx index c2b26ee2..7378490b 100644 --- a/interface/src/project/LightBrokerSettingsController.tsx +++ b/interface/src/project/LightMqttSettingsController.tsx @@ -7,13 +7,13 @@ import SaveIcon from '@material-ui/icons/Save'; import { ENDPOINT_ROOT } from '../api'; import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent } from '../components'; -import { LightBrokerSettings } from './types'; +import { LightMqttSettings } from './types'; export const LIGHT_BROKER_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "brokerSettings"; -type LightBrokerSettingsControllerProps = RestControllerProps; +type LightMqttSettingsControllerProps = RestControllerProps; -class LightBrokerSettingsController extends Component { +class LightMqttSettingsController extends Component { componentDidMount() { this.props.loadData(); @@ -25,7 +25,7 @@ class LightBrokerSettingsController extends Component ( - + )} /> @@ -34,11 +34,11 @@ class LightBrokerSettingsController extends Component; +type LightMqttSettingsControllerFormProps = RestFormProps; -function LightBrokerSettingsControllerForm(props: LightBrokerSettingsControllerFormProps) { +function LightMqttSettingsControllerForm(props: LightMqttSettingsControllerFormProps) { const { data, saveData, loadData, handleValueChange } = props; return ( diff --git a/interface/src/project/LightSettingsRestController.tsx b/interface/src/project/LightStateRestController.tsx similarity index 76% rename from interface/src/project/LightSettingsRestController.tsx rename to interface/src/project/LightStateRestController.tsx index a9ba3823..7e415660 100644 --- a/interface/src/project/LightSettingsRestController.tsx +++ b/interface/src/project/LightStateRestController.tsx @@ -7,13 +7,13 @@ import SaveIcon from '@material-ui/icons/Save'; import { ENDPOINT_ROOT } from '../api'; import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent, BlockFormControlLabel } from '../components'; -import { LightSettings } from './types'; +import { LightState } from './types'; export const LIGHT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "lightSettings"; -type LightSettingsRestControllerProps = RestControllerProps; +type LightStateRestControllerProps = RestControllerProps; -class LightSettingsRestController extends Component { +class LightStateRestController extends Component { componentDidMount() { this.props.loadData(); @@ -25,7 +25,7 @@ class LightSettingsRestController extends Component ( - + )} /> @@ -34,11 +34,11 @@ class LightSettingsRestController extends Component; +type LightStateRestControllerFormProps = RestFormProps; -function LightSettingsRestControllerForm(props: LightSettingsRestControllerFormProps) { +function LightStateRestControllerForm(props: LightStateRestControllerFormProps) { const { data, saveData, loadData, handleValueChange } = props; return ( diff --git a/interface/src/project/LightSettingsSocketController.tsx b/interface/src/project/LightStateSocketController.tsx similarity index 76% rename from interface/src/project/LightSettingsSocketController.tsx rename to interface/src/project/LightStateSocketController.tsx index 25de5be7..be29dc66 100644 --- a/interface/src/project/LightSettingsSocketController.tsx +++ b/interface/src/project/LightStateSocketController.tsx @@ -6,13 +6,13 @@ import { WEB_SOCKET_ROOT } from '../api'; import { SocketControllerProps, SocketFormLoader, SocketFormProps, socketController } from '../components'; import { SectionContent, BlockFormControlLabel } from '../components'; -import { LightSettings } from './types'; +import { LightState } from './types'; export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "lightSettings"; -type LightSettingsSocketControllerProps = SocketControllerProps; +type LightStateSocketControllerProps = SocketControllerProps; -class LightSettingsSocketController extends Component { +class LightStateSocketController extends Component { render() { return ( @@ -20,7 +20,7 @@ class LightSettingsSocketController extends Component ( - + )} /> @@ -29,11 +29,11 @@ class LightSettingsSocketController extends Component; +type LightStateSocketControllerFormProps = SocketFormProps; -function LightSettingsSocketControllerForm(props: LightSettingsSocketControllerFormProps) { +function LightStateSocketControllerForm(props: LightStateSocketControllerFormProps) { const { data, saveData, setData } = props; const changeLedOn = (event: React.ChangeEvent) => { diff --git a/interface/src/project/types.ts b/interface/src/project/types.ts index ec1ac0ba..32212557 100644 --- a/interface/src/project/types.ts +++ b/interface/src/project/types.ts @@ -1,8 +1,8 @@ -export interface LightSettings { +export interface LightState { led_on: boolean; } -export interface LightBrokerSettings { +export interface LightMqttSettings { unique_id : string; name: string; mqtt_path : string; diff --git a/src/LightBrokerSettingsService.cpp b/src/LightBrokerSettingsService.cpp deleted file mode 100644 index cd46b726..00000000 --- a/src/LightBrokerSettingsService.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include - -LightBrokerSettingsService::LightBrokerSettingsService(AsyncWebServer* server, - FS* fs, - SecurityManager* securityManager) : - _httpEndpoint(LightBrokerSettings::serialize, - LightBrokerSettings::deserialize, - this, - server, - LIGHT_BROKER_SETTINGS_PATH, - securityManager, - AuthenticationPredicates::IS_AUTHENTICATED), - _fsPersistence(LightBrokerSettings::serialize, - LightBrokerSettings::deserialize, - this, - fs, - LIGHT_BROKER_SETTINGS_FILE) { -} - -void LightBrokerSettingsService::begin() { - _fsPersistence.readFromFS(); -} diff --git a/src/LightMqttSettingsService.cpp b/src/LightMqttSettingsService.cpp new file mode 100644 index 00000000..96e25ebd --- /dev/null +++ b/src/LightMqttSettingsService.cpp @@ -0,0 +1,16 @@ +#include + +LightMqttSettingsService::LightMqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : + _httpEndpoint(LightMqttSettings::serialize, + LightMqttSettings::deserialize, + this, + server, + LIGHT_BROKER_SETTINGS_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED), + _fsPersistence(LightMqttSettings::serialize, LightMqttSettings::deserialize, this, fs, LIGHT_BROKER_SETTINGS_FILE) { +} + +void LightMqttSettingsService::begin() { + _fsPersistence.readFromFS(); +} diff --git a/src/LightBrokerSettingsService.h b/src/LightMqttSettingsService.h similarity index 59% rename from src/LightBrokerSettingsService.h rename to src/LightMqttSettingsService.h index 2b94c1a7..a21b2458 100644 --- a/src/LightBrokerSettingsService.h +++ b/src/LightMqttSettingsService.h @@ -1,5 +1,5 @@ -#ifndef LightBrokerSettingsService_h -#define LightBrokerSettingsService_h +#ifndef LightMqttSettingsService_h +#define LightMqttSettingsService_h #include #include @@ -15,33 +15,33 @@ static String defaultDeviceValue(String prefix = "") { #endif } -class LightBrokerSettings { +class LightMqttSettings { public: String mqttPath; String name; String uniqueId; - static void serialize(LightBrokerSettings& settings, JsonObject& root) { + static void serialize(LightMqttSettings& settings, JsonObject& root) { root["mqtt_path"] = settings.mqttPath; root["name"] = settings.name; root["unique_id"] = settings.uniqueId; } - static void deserialize(JsonObject& root, LightBrokerSettings& settings) { + static void deserialize(JsonObject& root, LightMqttSettings& settings) { settings.mqttPath = root["mqtt_path"] | defaultDeviceValue("homeassistant/light/"); settings.name = root["name"] | defaultDeviceValue("light-"); settings.uniqueId = root["unique_id"] | defaultDeviceValue("light-"); } }; -class LightBrokerSettingsService : public StatefulService { +class LightMqttSettingsService : public StatefulService { public: - LightBrokerSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); + LightMqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); void begin(); private: - HttpEndpoint _httpEndpoint; - FSPersistence _fsPersistence; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; }; -#endif // end LightBrokerSettingsService_h +#endif // end LightMqttSettingsService_h diff --git a/src/LightSettingsService.cpp b/src/LightStateService.cpp similarity index 58% rename from src/LightSettingsService.cpp rename to src/LightStateService.cpp index 6057858e..58535250 100644 --- a/src/LightSettingsService.cpp +++ b/src/LightStateService.cpp @@ -1,49 +1,49 @@ -#include +#include -LightSettingsService::LightSettingsService(AsyncWebServer* server, - SecurityManager* securityManager, - AsyncMqttClient* mqttClient, - LightBrokerSettingsService* lightBrokerSettingsService) : - _httpEndpoint(LightSettings::serialize, - LightSettings::deserialize, +LightStateService::LightStateService(AsyncWebServer* server, + SecurityManager* securityManager, + AsyncMqttClient* mqttClient, + LightMqttSettingsService* lightMqttSettingsService) : + _httpEndpoint(LightState::serialize, + LightState::deserialize, this, server, LIGHT_SETTINGS_ENDPOINT_PATH, securityManager, AuthenticationPredicates::IS_AUTHENTICATED), - _mqttPubSub(LightSettings::haSerialize, LightSettings::haDeserialize, this, mqttClient), - _webSocket(LightSettings::serialize, - LightSettings::deserialize, + _mqttPubSub(LightState::haSerialize, LightState::haDeserialize, this, mqttClient), + _webSocket(LightState::serialize, + LightState::deserialize, this, server, LIGHT_SETTINGS_SOCKET_PATH, securityManager, AuthenticationPredicates::IS_AUTHENTICATED), _mqttClient(mqttClient), - _lightBrokerSettingsService(lightBrokerSettingsService) { + _lightMqttSettingsService(lightMqttSettingsService) { // configure blink led to be output pinMode(BLINK_LED, OUTPUT); // configure MQTT callback - _mqttClient->onConnect(std::bind(&LightSettingsService::registerConfig, this)); + _mqttClient->onConnect(std::bind(&LightStateService::registerConfig, this)); // configure update handler for when the light settings change - _lightBrokerSettingsService->addUpdateHandler([&](String originId) { registerConfig(); }, false); + _lightMqttSettingsService->addUpdateHandler([&](String originId) { registerConfig(); }, false); // configure settings service update handler to update LED state addUpdateHandler([&](String originId) { onConfigUpdated(); }, false); } -void LightSettingsService::begin() { +void LightStateService::begin() { _state.ledOn = DEFAULT_LED_STATE; onConfigUpdated(); } -void LightSettingsService::onConfigUpdated() { +void LightStateService::onConfigUpdated() { digitalWrite(BLINK_LED, _state.ledOn ? LED_ON : LED_OFF); } -void LightSettingsService::registerConfig() { +void LightStateService::registerConfig() { if (!_mqttClient->connected()) { return; } @@ -52,7 +52,7 @@ void LightSettingsService::registerConfig() { String stateTopic; DynamicJsonDocument doc(256); - _lightBrokerSettingsService->read([&](LightBrokerSettings& settings) { + _lightMqttSettingsService->read([&](LightMqttSettings& settings) { configTopic = settings.mqttPath + "/config"; setTopic = settings.mqttPath + "/set"; stateTopic = settings.mqttPath + "/state"; diff --git a/src/LightSettingsService.h b/src/LightStateService.h similarity index 53% rename from src/LightSettingsService.h rename to src/LightStateService.h index 3d348e50..6cb9387e 100644 --- a/src/LightSettingsService.h +++ b/src/LightStateService.h @@ -1,7 +1,7 @@ -#ifndef LightSettingsService_h -#define LightSettingsService_h +#ifndef LightStateService_h +#define LightStateService_h -#include +#include #include #include @@ -27,42 +27,42 @@ #define LIGHT_SETTINGS_ENDPOINT_PATH "/rest/lightSettings" #define LIGHT_SETTINGS_SOCKET_PATH "/ws/lightSettings" -class LightSettings { +class LightState { public: bool ledOn; - static void serialize(LightSettings& settings, JsonObject& root) { + static void serialize(LightState& settings, JsonObject& root) { root["led_on"] = settings.ledOn; } - static void deserialize(JsonObject& root, LightSettings& settings) { + static void deserialize(JsonObject& root, LightState& settings) { settings.ledOn = root["led_on"] | DEFAULT_LED_STATE; } - static void haSerialize(LightSettings& settings, JsonObject& root) { + static void haSerialize(LightState& settings, JsonObject& root) { root["state"] = settings.ledOn ? ON_STATE : OFF_STATE; } - static void haDeserialize(JsonObject& root, LightSettings& settings) { + static void haDeserialize(JsonObject& root, LightState& settings) { String state = root["state"]; settings.ledOn = strcmp(ON_STATE, state.c_str()) ? false : true; } }; -class LightSettingsService : public StatefulService { +class LightStateService : public StatefulService { public: - LightSettingsService(AsyncWebServer* server, - SecurityManager* securityManager, - AsyncMqttClient* mqttClient, - LightBrokerSettingsService* lightBrokerSettingsService); + LightStateService(AsyncWebServer* server, + SecurityManager* securityManager, + AsyncMqttClient* mqttClient, + LightMqttSettingsService* lightMqttSettingsService); void begin(); private: - HttpEndpoint _httpEndpoint; - MqttPubSub _mqttPubSub; - WebSocketTxRx _webSocket; + HttpEndpoint _httpEndpoint; + MqttPubSub _mqttPubSub; + WebSocketTxRx _webSocket; AsyncMqttClient* _mqttClient; - LightBrokerSettingsService* _lightBrokerSettingsService; + LightMqttSettingsService* _lightMqttSettingsService; void registerConfig(); void onConfigUpdated(); diff --git a/src/main.cpp b/src/main.cpp index c5c833e5..978d74bd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,18 +1,18 @@ #include -#include -#include +#include +#include #include #define SERIAL_BAUD_RATE 115200 AsyncWebServer server(80); ESP8266React esp8266React(&server, &SPIFFS); -LightBrokerSettingsService lightBrokerSettingsService = - LightBrokerSettingsService(&server, &SPIFFS, esp8266React.getSecurityManager()); -LightSettingsService lightSettingsService = LightSettingsService(&server, - esp8266React.getSecurityManager(), - esp8266React.getMqttClient(), - &lightBrokerSettingsService); +LightMqttSettingsService lightMqttSettingsService = + LightMqttSettingsService(&server, &SPIFFS, esp8266React.getSecurityManager()); +LightStateService lightStateService = LightStateService(&server, + esp8266React.getSecurityManager(), + esp8266React.getMqttClient(), + &lightMqttSettingsService); void setup() { // start serial and filesystem @@ -29,10 +29,10 @@ void setup() { esp8266React.begin(); // load the initial light settings - lightSettingsService.begin(); + lightStateService.begin(); // start the light service - lightBrokerSettingsService.begin(); + lightMqttSettingsService.begin(); // start the server server.begin(); From d705c1b2b0f1568571f19255ec03bf806e0c9a1b Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Wed, 13 May 2020 00:05:24 +0100 Subject: [PATCH 31/35] rename Socket to WebSocket in front-end rename socketPath to webSocketPath in back-end --- ...Controller.tsx => WebSocketController.tsx} | 40 ++++++++-------- ...FormLoader.tsx => WebSocketFormLoader.tsx} | 10 ++-- interface/src/components/index.ts | 6 +-- interface/src/project/DemoInformation.tsx | 2 +- interface/src/project/DemoProject.tsx | 4 +- ....tsx => LightStateWebSocketController.tsx} | 16 +++---- lib/framework/WebSocketTxRx.h | 47 ++++++++++--------- 7 files changed, 65 insertions(+), 60 deletions(-) rename interface/src/components/{SocketController.tsx => WebSocketController.tsx} (67%) rename interface/src/components/{SocketFormLoader.tsx => WebSocketFormLoader.tsx} (70%) rename interface/src/project/{LightStateSocketController.tsx => LightStateWebSocketController.tsx} (67%) diff --git a/interface/src/components/SocketController.tsx b/interface/src/components/WebSocketController.tsx similarity index 67% rename from interface/src/components/SocketController.tsx rename to interface/src/components/WebSocketController.tsx index bf6a9db2..6713e12f 100644 --- a/interface/src/components/SocketController.tsx +++ b/interface/src/components/WebSocketController.tsx @@ -6,7 +6,7 @@ import { withSnackbar, WithSnackbarProps } from 'notistack'; import { addAccessTokenParameter } from '../authentication'; import { extractEventValue } from '.'; -export interface SocketControllerProps extends WithSnackbarProps { +export interface WebSocketControllerProps extends WithSnackbarProps { handleValueChange: (name: keyof D) => (event: React.ChangeEvent) => void; setData: (data: D, callback?: () => void) => void; @@ -17,35 +17,35 @@ export interface SocketControllerProps extends WithSnackbarProps { data?: D; } -interface SocketControllerState { +interface WebSocketControllerState { ws: Sockette; connected: boolean; clientId?: string; data?: D; } -enum SocketMessageType { +enum WebSocketMessageType { ID = "id", PAYLOAD = "payload" } -interface SocketIdMessage { - type: typeof SocketMessageType.ID; +interface WebSocketIdMessage { + type: typeof WebSocketMessageType.ID; id: string; } -interface SocketPayloadMessage { - type: typeof SocketMessageType.PAYLOAD; +interface WebSocketPayloadMessage { + type: typeof WebSocketMessageType.PAYLOAD; origin_id: string; payload: D; } -export type SocketMessage = SocketIdMessage | SocketPayloadMessage; +export type WebSocketMessage = WebSocketIdMessage | WebSocketPayloadMessage; -export function socketController>(wsUrl: string, wsThrottle: number, SocketController: React.ComponentType

>) { +export function webSocketController>(wsUrl: string, wsThrottle: number, WebSocketController: React.ComponentType

>) { return withSnackbar( - class extends React.Component> & WithSnackbarProps, SocketControllerState> { - constructor(props: Omit> & WithSnackbarProps) { + class extends React.Component> & WithSnackbarProps, WebSocketControllerState> { + constructor(props: Omit> & WithSnackbarProps) { super(props); this.state = { ws: new Sockette(addAccessTokenParameter(wsUrl), { @@ -64,20 +64,20 @@ export function socketController>(wsUrl: s onMessage = (event: MessageEvent) => { const rawData = event.data; if (typeof rawData === 'string' || rawData instanceof String) { - this.handleMessage(JSON.parse(rawData as string) as SocketMessage); + this.handleMessage(JSON.parse(rawData as string) as WebSocketMessage); } } - handleMessage = (socketMessage: SocketMessage) => { - switch (socketMessage.type) { - case SocketMessageType.ID: - this.setState({ clientId: socketMessage.id }); + handleMessage = (message: WebSocketMessage) => { + switch (message.type) { + case WebSocketMessageType.ID: + this.setState({ clientId: message.id }); break; - case SocketMessageType.PAYLOAD: + case WebSocketMessageType.PAYLOAD: const { clientId, data } = this.state; - if (clientId && (!data || clientId !== socketMessage.origin_id)) { + if (clientId && (!data || clientId !== message.origin_id)) { this.setState( - { data: socketMessage.payload } + { data: message.payload } ); } break; @@ -118,7 +118,7 @@ export function socketController>(wsUrl: s } render() { - return createStyles({ @@ -17,13 +17,13 @@ const useStyles = makeStyles((theme: Theme) => }) ); -export type SocketFormProps = Omit, "connected"> & { data: D }; +export type WebSocketFormProps = Omit, "connected"> & { data: D }; -interface SocketFormLoaderProps extends SocketControllerProps { - render: (props: SocketFormProps) => JSX.Element; +interface WebSocketFormLoaderProps extends WebSocketControllerProps { + render: (props: WebSocketFormProps) => JSX.Element; } -export default function SocketFormLoader(props: SocketFormLoaderProps) { +export default function WebSocketFormLoader(props: WebSocketFormLoaderProps) { const { connected, render, data, ...rest } = props; const classes = useStyles(); if (!connected || !data) { diff --git a/interface/src/components/index.ts b/interface/src/components/index.ts index 24963b48..e4e490e7 100644 --- a/interface/src/components/index.ts +++ b/interface/src/components/index.ts @@ -6,10 +6,10 @@ export { default as MenuAppBar } from './MenuAppBar'; export { default as PasswordValidator } from './PasswordValidator'; export { default as RestFormLoader } from './RestFormLoader'; export { default as SectionContent } from './SectionContent'; -export { default as SocketFormLoader } from './SocketFormLoader'; +export { default as WebSocketFormLoader } from './WebSocketFormLoader'; export * from './RestFormLoader'; export * from './RestController'; -export * from './SocketFormLoader'; -export * from './SocketController'; +export * from './WebSocketFormLoader'; +export * from './WebSocketController'; diff --git a/interface/src/project/DemoInformation.tsx b/interface/src/project/DemoInformation.tsx index 8a5d8cc0..0573799b 100644 --- a/interface/src/project/DemoInformation.tsx +++ b/interface/src/project/DemoInformation.tsx @@ -73,7 +73,7 @@ class DemoInformation extends Component { - LightStateSocketController.tsx + LightStateWebSocketController.tsx A form which lets the user control and monitor the status of the LED over WebSockets. diff --git a/interface/src/project/DemoProject.tsx b/interface/src/project/DemoProject.tsx index daffa426..74f25e53 100644 --- a/interface/src/project/DemoProject.tsx +++ b/interface/src/project/DemoProject.tsx @@ -9,7 +9,7 @@ import { AuthenticatedRoute } from '../authentication'; import DemoInformation from './DemoInformation'; import LightStateRestController from './LightStateRestController'; -import LightStateSocketController from './LightStateSocketController'; +import LightStateWebSocketController from './LightStateWebSocketController'; import LightMqttSettingsController from './LightMqttSettingsController'; class DemoProject extends Component { @@ -30,7 +30,7 @@ class DemoProject extends Component { - + diff --git a/interface/src/project/LightStateSocketController.tsx b/interface/src/project/LightStateWebSocketController.tsx similarity index 67% rename from interface/src/project/LightStateSocketController.tsx rename to interface/src/project/LightStateWebSocketController.tsx index be29dc66..0b7a9064 100644 --- a/interface/src/project/LightStateSocketController.tsx +++ b/interface/src/project/LightStateWebSocketController.tsx @@ -3,24 +3,24 @@ import { ValidatorForm } from 'react-material-ui-form-validator'; import { Typography, Box, Switch } from '@material-ui/core'; import { WEB_SOCKET_ROOT } from '../api'; -import { SocketControllerProps, SocketFormLoader, SocketFormProps, socketController } from '../components'; +import { WebSocketControllerProps, WebSocketFormLoader, WebSocketFormProps, webSocketController } from '../components'; import { SectionContent, BlockFormControlLabel } from '../components'; import { LightState } from './types'; export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "lightSettings"; -type LightStateSocketControllerProps = SocketControllerProps; +type LightStateWebSocketControllerProps = WebSocketControllerProps; -class LightStateSocketController extends Component { +class LightStateWebSocketController extends Component { render() { return ( - ( - + )} /> @@ -29,11 +29,11 @@ class LightStateSocketController extends Component; +type LightStateWebSocketControllerFormProps = WebSocketFormProps; -function LightStateSocketControllerForm(props: LightStateSocketControllerFormProps) { +function LightStateWebSocketControllerForm(props: LightStateWebSocketControllerFormProps) { const { data, saveData, setData } = props; const changeLedOn = (event: React.ChangeEvent) => { diff --git a/lib/framework/WebSocketTxRx.h b/lib/framework/WebSocketTxRx.h index 9bee4da2..7f5faa45 100644 --- a/lib/framework/WebSocketTxRx.h +++ b/lib/framework/WebSocketTxRx.h @@ -21,10 +21,10 @@ class WebSocketConnector { WebSocketConnector(StatefulService* statefulService, AsyncWebServer* server, - char const* socketPath, + char const* webSocketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - _statefulService(statefulService), _server(server), _webSocket(socketPath) { + _statefulService(statefulService), _server(server), _webSocket(webSocketPath) { _webSocket.setFilter(securityManager->filterRequest(authenticationPredicate)); _webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent, this, @@ -35,11 +35,11 @@ class WebSocketConnector { std::placeholders::_5, std::placeholders::_6)); _server->addHandler(&_webSocket); - _server->on(socketPath, HTTP_GET, std::bind(&WebSocketConnector::forbidden, this, std::placeholders::_1)); + _server->on(webSocketPath, HTTP_GET, std::bind(&WebSocketConnector::forbidden, this, std::placeholders::_1)); } - WebSocketConnector(StatefulService* statefulService, AsyncWebServer* server, char const* socketPath) : - _statefulService(statefulService), _server(server), _webSocket(socketPath) { + WebSocketConnector(StatefulService* statefulService, AsyncWebServer* server, char const* webSocketPath) : + _statefulService(statefulService), _server(server), _webSocket(webSocketPath) { _webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent, this, std::placeholders::_1, @@ -74,10 +74,10 @@ class WebSocketTx : virtual public WebSocketConnector { WebSocketTx(JsonSerializer jsonSerializer, StatefulService* statefulService, AsyncWebServer* server, - char const* socketPath, + char const* webSocketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - WebSocketConnector(statefulService, server, socketPath, securityManager, authenticationPredicate), + WebSocketConnector(statefulService, server, webSocketPath, securityManager, authenticationPredicate), _jsonSerializer(jsonSerializer) { WebSocketConnector::_statefulService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); @@ -86,8 +86,8 @@ class WebSocketTx : virtual public WebSocketConnector { WebSocketTx(JsonSerializer jsonSerializer, StatefulService* statefulService, AsyncWebServer* server, - char const* socketPath) : - WebSocketConnector(statefulService, server, socketPath), _jsonSerializer(jsonSerializer) { + char const* webSocketPath) : + WebSocketConnector(statefulService, server, webSocketPath), _jsonSerializer(jsonSerializer) { WebSocketConnector::_statefulService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, false); } @@ -156,18 +156,18 @@ class WebSocketRx : virtual public WebSocketConnector { WebSocketRx(JsonDeserializer jsonDeserializer, StatefulService* statefulService, AsyncWebServer* server, - char const* socketPath, + char const* webSocketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - WebSocketConnector(statefulService, server, socketPath, securityManager, authenticationPredicate), + WebSocketConnector(statefulService, server, webSocketPath, securityManager, authenticationPredicate), _jsonDeserializer(jsonDeserializer) { } WebSocketRx(JsonDeserializer jsonDeserializer, StatefulService* statefulService, AsyncWebServer* server, - char const* socketPath) : - WebSocketConnector(statefulService, server, socketPath), _jsonDeserializer(jsonDeserializer) { + char const* webSocketPath) : + WebSocketConnector(statefulService, server, webSocketPath), _jsonDeserializer(jsonDeserializer) { } protected: @@ -204,22 +204,27 @@ class WebSocketTxRx : public WebSocketTx, public WebSocketRx { JsonDeserializer jsonDeserializer, StatefulService* statefulService, AsyncWebServer* server, - char const* socketPath, + char const* webSocketPath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : - WebSocketConnector(statefulService, server, socketPath, securityManager, authenticationPredicate), - WebSocketTx(jsonSerializer, statefulService, server, socketPath, securityManager, authenticationPredicate), - WebSocketRx(jsonDeserializer, statefulService, server, socketPath, securityManager, authenticationPredicate) { + WebSocketConnector(statefulService, server, webSocketPath, securityManager, authenticationPredicate), + WebSocketTx(jsonSerializer, statefulService, server, webSocketPath, securityManager, authenticationPredicate), + WebSocketRx(jsonDeserializer, + statefulService, + server, + webSocketPath, + securityManager, + authenticationPredicate) { } WebSocketTxRx(JsonSerializer jsonSerializer, JsonDeserializer jsonDeserializer, StatefulService* statefulService, AsyncWebServer* server, - char const* socketPath) : - WebSocketConnector(statefulService, server, socketPath), - WebSocketTx(jsonSerializer, statefulService, server, socketPath), - WebSocketRx(jsonDeserializer, statefulService, server, socketPath) { + char const* webSocketPath) : + WebSocketConnector(statefulService, server, webSocketPath), + WebSocketTx(jsonSerializer, statefulService, server, webSocketPath), + WebSocketRx(jsonDeserializer, statefulService, server, webSocketPath) { } protected: From eb3dd4aa2fc99cf7c48722c4681b6f5bbb0702a1 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Wed, 13 May 2020 23:38:56 +0100 Subject: [PATCH 32/35] Use PROGMEM_WWW as default Update README documenting PROGMEM_WWW as default --- README.md | 67 +++++++++++++++++++++++++------------------------- platformio.ini | 6 ++--- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 1be1ab2d..9e751e66 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Provides many of the features required for IoT projects: * Remote Firmware Updates - Enable secured OTA updates * Security - Protected RESTful endpoints and a secured user interface -The back end is provided by a set of RESTful endpoints and the React based front end is responsive and scales well to various screen sizes. +The back end is provided by a set of RESTful endpoints and the responsive React based front end is built using [Material-UI](https://material-ui.com/). The front end has the prerequisite manifest file and icon, so it can be added to the home screen of a mobile device if required. @@ -38,13 +38,14 @@ Pull the project and open it in PlatformIO. PlatformIO should download the ESP82 The project structure is as follows: -Resource | Description ----- | ----------- -[data/](data) | The file system image directory -[interface/](interface) | React based front end -[src/](src) | The main.cpp and demo project to get you started +Resource | Description +-------------------------------- | ---------------------------------------------------------------------- +[data/](data) | The file system image directory +[interface/](interface) | React based front end +[lib/framework/](lib/framework) | C++ back end for the ESP8266/ESP32 device +[src/](src) | The main.cpp and demo project to get you started +[scripts/](scripts) | Scripts that build the React interface as part of the platformio build [platformio.ini](platformio.ini) | PlatformIO project configuration file -[lib/framework/](lib/framework) | C++ back end for the ESP8266 device ### Building the firmware @@ -76,31 +77,18 @@ platformio run -t upload ### Building & uploading the interface -The interface has been configured with create-react-app and react-app-rewired so the build can customized for the target device. The large artefacts are gzipped and source maps and service worker are excluded from the production build. This reduces the production build to around ~200k, which easily fits on the device. - -The interface will be automatically built by PlatformIO before it builds the firmware. The project can be configured to serve the interface from either SPIFFS or PROGMEM as your project requires. The default configuration is to serve the content from SPIFFS which requires an additional upload step which is documented below. - -#### Uploading the file system image - -If service content from SPIFFS (default), build the project first. Then the compiled interface may be uploaded to the device by pressing the "Upload File System image" button: - -![uploadfs](/media/uploadfs.png?raw=true "uploadfs") - -Alternatively run the 'uploadfs' target: +The interface has been configured with create-react-app and react-app-rewired so the build can customized for the target device. The large artefacts are gzipped and source maps and service worker are excluded from the production build. This reduces the production build to around ~150k, which easily fits on the device. -```bash -platformio run -t uploadfs -``` +The interface will be automatically built by PlatformIO before it builds the firmware. The project can be configured to serve the interface from either PROGMEM or SPIFFS as your project requires. The default configuration is to serve the content from PROGMEM, serving from SPIFFS requires an additional upload step which is documented below. #### Serving the interface from PROGMEM -You can configure the project to serve the interface from PROGMEM by uncommenting the -D PROGMEM_WWW build flag in ['platformio.ini'](platformio.ini) then re-building and uploading the firmware to the device. +By default, the project is configured to serve the interface from PROGMEM. This can be disabled by removing the -D PROGMEM_WWW build flag in ['platformio.ini'](platformio.ini) and re-building the firmware. If this your desired approach you must manually [upload the file system image](#uploading-the-file-system-image) to the device. -Be aware that this will consume ~150k of program space which can be especially problematic if you already have a large build artefact or if you have added large javascript dependencies to the interface. The ESP32 binaries are large already, so this will be a problem if you are using one of these devices and require this type of setup. +The interface will consume ~150k of program space which can be problematic if you already have a large binary artefact or if you have added large dependencies to the interface. The ESP32 binaries are fairly large in there simplest form so the addition of the interface resources requires us to use special partitioning for the ESP32. -A method for working around this issue can be to reduce the amount of space allocated to SPIFFS by configuring the device to use different partitioning. If you don't require SPIFFS other than for storing config one approach might be to configure a minimal SPIFFS partition. +When building using the "node32s" profile, the project uses the custom [min_spiffs.csv](https://github.com/espressif/arduino-esp32/blob/master/tools/partitions/min_spiffs.csv) partitioning mode. You may want to disable this if you are manually uploading the file system image: -For a ESP32 (4mb variant) there is a handy "min_spiffs.csv" partition table which can be enabled easily: ```yaml [env:node32s] @@ -109,11 +97,21 @@ platform = espressif32 board = node32s ``` -This is left as an exercise for the reader as everyone's requirements will vary. +#### Uploading the file system image + +If service content from SPIFFS, disable the PROGMEM_WWW build flag and build the project. The compiled interface will be copied to [data/](data) by the build process and may now be uploaded to the device by pressing the "Upload File System image" button: + +![uploadfs](/media/uploadfs.png?raw=true "uploadfs") + +Alternatively run the 'uploadfs' target: + +```bash +platformio run -t uploadfs +``` ### Running the interface locally -You can run a local development server to allow you preview changes to the front end without the need to upload a file system image to the device after each change. +You can run a development server locally to allow you preview changes to the front end without the need to upload a file system image to the device after each change. Change to the ['interface'](interface) directory with your bash shell (or Git Bash) and use the standard commands you would with any react app built with create-react-app: @@ -127,8 +125,7 @@ Install the npm dependencies, if required and start the development server: npm install npm start ``` - -> **Note**: To run the interface locally you may need to modify the endpoint root path and enable CORS. +> **Tip**: You can (optionally) speed up the build by commenting out the call to build_interface.py under "extra scripts" during local development. This will prevent the npm process from building the production release every time the firmware is compiled significantly decreasing the build time. #### Changing the endpoint root @@ -141,7 +138,7 @@ REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.99 The `REACT_APP_HTTP_ROOT` and `REACT_APP_WEB_SOCKET_ROOT` properties can be modified to point a ESP device running the back end firmware. -> **Note**: You must restart the development server for changes to the environment to become effective. +> **Tip**: You must restart the development server for changes to the environment file to come into effect. #### Enabling CORS @@ -152,7 +149,7 @@ You can enable CORS on the back end by uncommenting the -D ENABLE_CORS build fla -D CORS_ORIGIN=\"http://localhost:3000\" ``` -## Device Configuration +## Device configuration & default settings The SPIFFS image (in the ['data'](data) folder) contains a JSON settings file for each of the configurable features. @@ -167,7 +164,9 @@ File | Description [securitySettings.json](data/config/securitySettings.json) | Security settings and user credentials [wifiSettings.json](data/config/wifiSettings.json) | WiFi connection settings -### Access point settings +These files can be pre-loaded with default configuration and [uploaded to the device](#uploading-the-file-system-image) if required. There are sensible defaults provided by the firmware, so this is optional. + +### Default access point settings The default settings configure the device to bring up an access point on start up which can be used to configure the device: @@ -183,7 +182,7 @@ Username | Password admin | admin guest | guest -It is recommended that you change the JWT secret and user credentials from their defaults protect your device. You can do this in the user interface, or by modifying [securitySettings.json](data/config/securitySettings.json) before uploading the file system image. +It is recommended that you change the JWT secret and user credentials from their defaults protect your device. You can do this in the user interface, or by modifying [securitySettings.json](data/config/securitySettings.json) before [uploading the file system image](#uploading-the-file-system-image). ## Building for different devices @@ -588,7 +587,7 @@ esp8266React.getWiFiSettingsService()->addUpdateHandler( ## Libraries Used * [React](https://reactjs.org/) -* [Material-UI](https://material-ui-next.com/) +* [Material-UI](https://material-ui.com/) * [notistack](https://github.com/iamhosseindhv/notistack) * [ArduinoJson](https://github.com/bblanchon/ArduinoJson) * [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) diff --git a/platformio.ini b/platformio.ini index 38c91347..d6bcba10 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,7 +9,7 @@ build_flags= ;-D ENABLE_CORS -D CORS_ORIGIN=\"http://localhost:3000\" ; Uncomment PROGMEM_WWW to enable the storage of the WWW data in PROGMEM - ;-D PROGMEM_WWW + -D PROGMEM_WWW ; ensure transitive dependencies are included for correct platforms only lib_compat_mode = strict @@ -37,7 +37,7 @@ board = esp12e board_build.f_cpu = 160000000L [env:node32s] -; Uncomment the min_spiffs.csv setting if using PROGMEM_WWW with ESP32 -;board_build.partitions = min_spiffs.csv +; Comment out min_spiffs.csv setting if disabling PROGMEM_WWW with ESP32 +board_build.partitions = min_spiffs.csv platform = espressif32 board = node32s From 33b08a54c09acb7e4e9da012c316d50fe07ff29c Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Wed, 13 May 2020 23:58:40 +0100 Subject: [PATCH 33/35] update light state endpoint paths --- interface/src/project/LightStateRestController.tsx | 2 +- interface/src/project/LightStateWebSocketController.tsx | 2 +- src/LightStateService.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/src/project/LightStateRestController.tsx b/interface/src/project/LightStateRestController.tsx index 7e415660..48cd0e6c 100644 --- a/interface/src/project/LightStateRestController.tsx +++ b/interface/src/project/LightStateRestController.tsx @@ -9,7 +9,7 @@ import { restController, RestControllerProps, RestFormLoader, RestFormProps, For import { LightState } from './types'; -export const LIGHT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "lightSettings"; +export const LIGHT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "lightState"; type LightStateRestControllerProps = RestControllerProps; diff --git a/interface/src/project/LightStateWebSocketController.tsx b/interface/src/project/LightStateWebSocketController.tsx index 0b7a9064..a0b99a2e 100644 --- a/interface/src/project/LightStateWebSocketController.tsx +++ b/interface/src/project/LightStateWebSocketController.tsx @@ -8,7 +8,7 @@ import { SectionContent, BlockFormControlLabel } from '../components'; import { LightState } from './types'; -export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "lightSettings"; +export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "lightState"; type LightStateWebSocketControllerProps = WebSocketControllerProps; diff --git a/src/LightStateService.h b/src/LightStateService.h index 6cb9387e..bbf3ed84 100644 --- a/src/LightStateService.h +++ b/src/LightStateService.h @@ -24,8 +24,8 @@ #define LED_OFF 0x1 #endif -#define LIGHT_SETTINGS_ENDPOINT_PATH "/rest/lightSettings" -#define LIGHT_SETTINGS_SOCKET_PATH "/ws/lightSettings" +#define LIGHT_SETTINGS_ENDPOINT_PATH "/rest/lightState" +#define LIGHT_SETTINGS_SOCKET_PATH "/ws/lightState" class LightState { public: From 4dce60bda7898166e26ee3a0c7f412d810ed4cbb Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Thu, 14 May 2020 00:01:42 +0100 Subject: [PATCH 34/35] Update README with API changes --- README.md | 84 +++++++++++++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 9e751e66..0b452259 100644 --- a/README.md +++ b/README.md @@ -335,9 +335,9 @@ The following diagram visualises how the framework's modular components fit toge ![framework diagram](/media/framework.png?raw=true "framework diagram") -#### Settings service +#### Stateful service -The [SettingsService.h](lib/framework/SettingsService.h) class is a responsible for managing settings and interfacing with code which wants to change or respond to changes in them. You can define a data class to hold settings then build a SettingsService instance to manage them: +The [StatefulService.h](lib/framework/StatefulService.h) class is a responsible for managing state and interfacing with code which wants to change or respond to changes in that state. You can define a data class to hold some state, then build a StatefulService class to manage its state: ```cpp class LightState { @@ -346,17 +346,17 @@ class LightState { uint8_t brightness = 255; }; -class LightStateService : public SettingsService { +class LightStateService : public StatefulService { }; ``` -You may listen for changes to settings by registering an update handler callback. It is possible to remove an update handler later if required. +You may listen for changes to state by registering an update handler callback. It is possible to remove an update handler later if required. ```cpp // register an update handler update_handler_id_t myUpdateHandler = lightStateService.addUpdateHandler( [&](String originId) { - Serial.println("The light settings have been updated"); + Serial.println("The light's state has been updated"); } ); @@ -372,27 +372,27 @@ http | An update sent over REST (HttpEndpoint) mqtt | An update sent over MQTT (MqttPubSub) websocket:{clientId} | An update sent over WebSocket (WebSocketRxTx) -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. +StatefulService exposes a read function which you may use to safely read the state. This function takes care of protecting against parallel access to the state in multi-core enviornments such as the ESP32. ```cpp -lightStateService.read([&](LightState& settings) { - digitalWrite(LED_PIN, settings.on ? HIGH : LOW) +lightStateService.read([&](LightState& state) { + digitalWrite(LED_PIN, state.on ? HIGH : LOW); // apply the state update to the LED_PIN }); ``` -SettingsService also exposes an update function which allows the caller to update the settings with a callback. This approach automatically calls the registered update handlers when complete. The example below turns on the lights using the arbitrary origin "timer": +StatefulService also exposes an update function which allows the caller to update the state with a callback. This approach automatically calls the registered update handlers when complete. The example below turns on the lights using the arbitrary origin "timer": ```cpp -lightStateService.update([&](LightState& settings) { - settings.on = true; // turn on the lights! +lightStateService.update([&](LightState& state) { + state.on = true; // turn on the lights! }, "timer"); ``` #### Serialization -When transmitting settings over HTTP, WebSockets, or MQTT they must to be marshalled into a serializable form (JSON). The framework uses ArduinoJson for serialization and the functions defined in [JsonSerializer.h](lib/framework/JsonSerializer.h) and [JsonDeserializer.h](lib/framework/JsonDeserializer.h) facilitate this. +When transmitting state over HTTP, WebSockets, or MQTT it must to be marshalled into a serializable form (JSON). The framework uses ArduinoJson for serialization and the functions defined in [JsonSerializer.h](lib/framework/JsonSerializer.h) and [JsonDeserializer.h](lib/framework/JsonDeserializer.h) facilitate this. -The static functions below can be used to facilitate the serialization/deserialization of the example settings: +The static functions below can be used to facilitate the serialization/deserialization of the light state: ```cpp class LightState { @@ -400,28 +400,28 @@ class LightState { bool on = false; uint8_t brightness = 255; - static void serialize(LightState& settings, JsonObject& root) { - root["on"] = settings.on; - root["brightness"] = settings.brightness; + static void serialize(LightState& state, JsonObject& root) { + root["on"] = state.on; + root["brightness"] = state.brightness; } - static void deserialize(JsonObject& root, LightState& settings) { - settings.on = root["on"] | false; - settings.brightness = root["brightness"] | 255; + static void deserialize(JsonObject& root, LightState& state) { + state.on = root["on"] | false; + state.brightness = root["brightness"] | 255; } }; ``` -For convenience, the SettingsService class provides overloads of its `update` and `read` functions which utilize these functions. +For convenience, the StatefulService class provides overloads of its `update` and `read` functions which utilize these functions. -Copy the settings to a JsonObject using a serializer: +Copy the state to a JsonObject using a serializer: ```cpp JsonObject jsonObject = jsonDocument.to(); lightStateService->read(jsonObject, serializer); ``` -Update the settings from a JsonObject using a deserializer: +Update the state from a JsonObject using a deserializer: ```cpp JsonObject jsonObject = jsonDocument.as(); @@ -430,15 +430,15 @@ lightStateService->update(jsonObject, deserializer, "timer"); #### Endpoints -The framework provides a [HttpEndpoint.h](lib/framework/HttpEndpoint.h) class which may be used to register GET and POST handlers to read and update the settings over HTTP. You may construct a HttpEndpoint as a part of the SettingsService or separately if you prefer. +The framework provides an [HttpEndpoint.h](lib/framework/HttpEndpoint.h) class which may be used to register GET and POST handlers to read and update the state over HTTP. You may construct an HttpEndpoint as a part of the StatefulService or separately if you prefer. The code below demonstrates how to extend the LightStateService class to provide an unsecured endpoint: ```cpp -class LightStateService : public SettingsService { +class LightStateService : public StatefulService { public: LightStateService(AsyncWebServer* server) : - _httpEndpoint(LightState::serialize, LightState::deserialize, this, server, "/rest/lightSettings") { + _httpEndpoint(LightState::serialize, LightState::deserialize, this, server, "/rest/lightState") { } private: @@ -450,15 +450,15 @@ Endpoint security is provided by authentication predicates which are [documented #### Persistence -[FSPersistence.h](lib/framework/FSPersistence.h) allows you to save settings to the filesystem. FSPersistence automatically writes changes to the file system when settings are updated. This feature can be disabled by calling `disableUpdateHandler()` if manual control of persistence is required. +[FSPersistence.h](lib/framework/FSPersistence.h) allows you to save state to the filesystem. FSPersistence automatically writes changes to the file system when state is updated. This feature can be disabled by calling `disableUpdateHandler()` if manual control of persistence is required. The code below demonstrates how to extend the LightStateService class to provide persistence: ```cpp -class LightStateService : public SettingsService { +class LightStateService : public StatefulService { public: LightStateService(FS* fs) : - _fsPersistence(LightState::serialize, LightState::deserialize, this, fs, "/config/lightSettings.json") { + _fsPersistence(LightState::serialize, LightState::deserialize, this, fs, "/config/lightState.json") { } private: @@ -468,19 +468,19 @@ class LightStateService : public SettingsService { #### WebSockets -[SettingsSocket.h](lib/framework/SettingsSocket.h) allows you to read and update settings over a WebSocket connection. SettingsSocket automatically pushes changes to all connected clients when settings are updated. +[WebSocketTxRx.h](lib/framework/WebSocketTxRx.h) allows you to read and update state over a WebSocket connection. WebSocketTxRx automatically pushes changes to all connected clients when state is updated. -The code below demonstrates how to extend the LightStateService class to provide an unsecured websocket: +The code below demonstrates how to extend the LightStateService class to provide an unsecured WebSocket: ```cpp -class LightStateService : public SettingsService { +class LightStateService : public StatefulService { public: LightStateService(AsyncWebServer* server) : - _settingsSocket(LightState::serialize, LightState::deserialize, this, server, "/ws/lightSettings"), { + _webSocket(LightState::serialize, LightState::deserialize, this, server, "/ws/lightState"), { } private: - SettingsSocket _settingsSocket; + WebSocketTxRx _webSocket; }; ``` @@ -488,17 +488,17 @@ WebSocket security is provided by authentication predicates which are [documente #### MQTT -The framework includes an MQTT client which can be configured via the UI. MQTT requirements will differ from project to project so the framework exposes the client for you to use as you see fit. The framework does however provide a utility to interface SettingsService to a pair of pub/sub (state/set) topics. This utility can be used to synchronize state with software such as Home Assistant. +The framework includes an MQTT client which can be configured via the UI. MQTT requirements will differ from project to project so the framework exposes the client for you to use as you see fit. The framework does however provide a utility to interface StatefulService to a pair of pub/sub (state/set) topics. This utility can be used to synchronize state with software such as Home Assistant. -[SettingsBroker.h](lib/framework/SettingsBroker.h) allows you to read and update settings over a pair of MQTT topics. SettingsBroker automatically pushes changes to the pub topic and reads updates from the sub topic. +[MqttPubSub.h](lib/framework/MqttPubSub.h) allows you to publish and subscribe to synchronize state over a pair of MQTT topics. MqttPubSub automatically pushes changes to the "pub" topic and reads updates from the "sub" topic. The code below demonstrates how to extend the LightStateService class to interface with MQTT: ```cpp -class LightStateService : public SettingsService { +class LightStateService : public StatefulService { public: LightStateService(AsyncMqttClient* mqttClient) : - _settingsBroker(LightState::serialize, + _mqttPubSub(LightState::serialize, LightState::deserialize, this, mqttClient, @@ -507,14 +507,14 @@ class LightStateService : public SettingsService { } private: - SettingsBroker _settingsBroker; + MqttPubSub _mqttPubSub; }; ``` -You can also re-configure the pub/sub topics at runtime as required: +You can re-configure the pub/sub topics at runtime as required: ```cpp -_settingsBroker.configureBroker("homeassistant/light/desk_lamp/set", "homeassistant/light/desk_lamp/state"); +_mqttPubSub.configureBroker("homeassistant/light/desk_lamp/set", "homeassistant/light/desk_lamp/state"); ``` The demo project allows the user to modify the MQTT topics via the UI so they can be changed without re-flashing the firmware. @@ -543,7 +543,7 @@ server->on("/rest/someService", HTTP_GET, The framework supplies access to various features via getter functions: -SettingsService | Description +SettingsService | Description ---------------------------- | ---------------------------------------------- getSecurityManager() | The security manager - detailed above getSecuritySettingsService() | Configures the users and other security settings @@ -554,7 +554,7 @@ getOTASettingsService() | Configures and manages the Over-The-Air update fe getMqttSettingsService() | Configures and manages the MQTT connection getMqttClient() | Provides direct access to the MQTT client instance -These can be used to observe changes to settings. They can also be used to fetch or update settings. +The core features use the [StatefulService.h](lib/framework/StatefulService.h) class and can therefore you can change settings or observe changes to settings through the read/update API. Inspect the current WiFi settings: From 2332671a027e729e2408a40f38f24edd41e3f3e1 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Thu, 14 May 2020 00:19:13 +0100 Subject: [PATCH 35/35] update diagram --- media/framework.png | Bin 61887 -> 58594 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/media/framework.png b/media/framework.png index badbf3adc98f13197ba1e8dffb3c760ea49a9f32..e157dda6d65320dd1b10ae6e35b0c85537dbb44e 100644 GIT binary patch literal 58594 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

oHGZ6>iJeRH8|2!=IYW!Q01%mJr*#5A{ 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 F{{