diff --git a/README.md b/README.md
index ac0bed70..e96b609a 100644
--- a/README.md
+++ b/README.md
@@ -156,12 +156,13 @@ Many of the framework's built in features may be enabled or disabled as required
Customize the settings as you see fit. A value of 0 will disable the specified feature:
```ini
- -D FT_PROJECT=1
- -D FT_SECURITY=1
- -D FT_MQTT=1
- -D FT_NTP=1
- -D FT_OTA=1
- -D FT_UPLOAD_FIRMWARE=1
+ -D FT_PROJECT=1
+ -D FT_SECURITY=1
+ -D FT_MQTT=1
+ -D FT_NTP=1
+ -D FT_SERIAL=1
+ -D FT_OTA=1
+ -D FT_UPLOAD_FIRMWARE=1
```
Flag | Description
diff --git a/factory_settings.ini b/factory_settings.ini
index b9fbd60a..483227d4 100644
--- a/factory_settings.ini
+++ b/factory_settings.ini
@@ -50,5 +50,14 @@ build_flags =
-D FACTORY_MQTT_CLEAN_SESSION=true
-D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
+ ; Serial settings
+ -D FACTORY_SERIAL_ENABLED=true
+ -D FACTORY_SERIAL_RXPIN=14
+ -D FACTORY_SERIAL_TXPIN=15
+ -D FACTORY_SERIAL_BAUD=0
+ -D FACTORY_SERIAL_CONFIG=0x800001c
+ -D FACTORY_SERIAL_INVERTED=false
+ -D FACTORY_TCP_PORT=1963
+
; JWT Secret
-D FACTORY_JWT_SECRET=\"#{random}-#{random}\" ; supports placeholders
diff --git a/features.ini b/features.ini
index ffb890d9..bee9cd8a 100644
--- a/features.ini
+++ b/features.ini
@@ -5,4 +5,5 @@ build_flags =
-D FT_MQTT=1
-D FT_NTP=1
-D FT_OTA=1
+ -D FT_SERIAL=1
-D FT_UPLOAD_FIRMWARE=1
diff --git a/interface/package-lock.json b/interface/package-lock.json
index 46205e11..4426c15a 100644
--- a/interface/package-lock.json
+++ b/interface/package-lock.json
@@ -22012,6 +22012,66 @@
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
+ }
+ }
+ },
+ "eslint-plugin-react-hooks": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz",
+ "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ=="
+ },
+ "eslint-plugin-testing-library": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-3.10.1.tgz",
+ "integrity": "sha512-nQIFe2muIFv2oR2zIuXE4vTbcFNx8hZKRzgHZqJg8rfopIWwoTwtlbCCNELT/jXzVe1uZF68ALGYoDXjLczKiQ==",
+ "requires": {
+ "@typescript-eslint/experimental-utils": "^3.10.1"
+ },
+ "dependencies": {
+ "@typescript-eslint/experimental-utils": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz",
+ "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==",
+ "requires": {
+ "@types/json-schema": "^7.0.3",
+ "@typescript-eslint/types": "3.10.1",
+ "@typescript-eslint/typescript-estree": "3.10.1",
+ "eslint-scope": "^5.0.0",
+ "eslint-utils": "^2.0.0"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz",
+ "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ=="
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz",
+ "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==",
+ "requires": {
+ "@typescript-eslint/types": "3.10.1",
+ "@typescript-eslint/visitor-keys": "3.10.1",
+ "debug": "^4.1.1",
+ "glob": "^7.1.6",
+ "is-glob": "^4.0.1",
+ "lodash": "^4.17.15",
+ "semver": "^7.3.2",
+ "tsutils": "^3.17.1"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz",
+ "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==",
+ "requires": {
+ "eslint-visitor-keys": "^1.1.0"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ=="
},
"has-flag": {
"version": "4.0.0",
@@ -24831,6 +24891,15 @@
"tslib": "^2.0.3"
}
},
+ "param-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
+ "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
+ "requires": {
+ "dot-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -25998,6 +26067,11 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
},
+ "react-refresh": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
+ "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg=="
+ },
"react-router": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
@@ -26851,6 +26925,11 @@
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
},
+ "strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
+ },
"strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -26989,6 +27068,11 @@
}
}
},
+ "symbol-observable": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
+ },
"symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -27069,12 +27153,21 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
- "supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "terser": {
+ "version": "5.5.1",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.5.1.tgz",
+ "integrity": "sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==",
"requires": {
- "has-flag": "^4.0.0"
+ "commander": "^2.20.0",
+ "source-map": "~0.7.2",
+ "source-map-support": "~0.5.19"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
+ "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
+ }
}
}
}
@@ -27322,6 +27415,14 @@
"is-typedarray": "^1.0.0"
}
},
+ "typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "requires": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
"typescript": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
@@ -27370,6 +27471,14 @@
"crypto-random-string": "^2.0.0"
}
},
+ "unique-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz",
+ "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=",
+ "requires": {
+ "crypto-random-string": "^1.0.0"
+ }
+ },
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -27412,6 +27521,26 @@
"es-abstract": "^1.17.2",
"has-symbols": "^1.0.1",
"object.getownpropertydescriptors": "^2.1.0"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
+ "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ }
+ }
}
},
"utila": {
@@ -27683,6 +27812,40 @@
"ansi-regex": "^6.0.1"
}
},
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
+ },
+ "wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
"ws": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
@@ -27713,6 +27876,19 @@
"source-list-map": "^2.0.1",
"source-map": "^0.6.1"
}
+ },
+ "jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
}
}
},
@@ -27927,6 +28103,24 @@
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
+ },
+ "jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "source-map": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
+ "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
+ },
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
}
}
},
diff --git a/interface/package.json b/interface/package.json
index 5de682aa..b0169c43 100644
--- a/interface/package.json
+++ b/interface/package.json
@@ -2,7 +2,7 @@
"name": "esp8266-react",
"version": "0.1.0",
"private": true,
- "proxy": "http://192.168.0.23",
+ "proxy": "http://192.168.4.1",
"dependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
diff --git a/interface/src/AuthenticatedRouting.tsx b/interface/src/AuthenticatedRouting.tsx
index 4c32930c..36186e20 100644
--- a/interface/src/AuthenticatedRouting.tsx
+++ b/interface/src/AuthenticatedRouting.tsx
@@ -14,6 +14,7 @@ import WiFiConnection from './framework/wifi/WiFiConnection';
import AccessPoint from './framework/ap/AccessPoint';
import NetworkTime from './framework/ntp/NetworkTime';
import Mqtt from './framework/mqtt/Mqtt';
+import Serial from './framework/serial/Serial';
import System from './framework/system/System';
import Security from './framework/security/Security';
@@ -49,6 +50,9 @@ const AuthenticatedRouting: FC = () => {
{features.mqtt && (
} />
)}
+ {features.serial && (
+ } />
+ )}
{features.security && (
{
+ return AXIOS.get('/serialStatus');
+}
+
+export function readSerialSettings(): AxiosPromise {
+ return AXIOS.get('/serialSettings');
+}
+
+export function updateSerialSettings(serialSettings: SerialSettings): AxiosPromise {
+ return AXIOS.post('/serialSettings', serialSettings);
+}
diff --git a/interface/src/components/WindowSize.tsx b/interface/src/components/WindowSize.tsx
new file mode 100644
index 00000000..d69405ae
--- /dev/null
+++ b/interface/src/components/WindowSize.tsx
@@ -0,0 +1,14 @@
+import { useLayoutEffect, useState } from 'react';
+
+export function useWindowSize() {
+ const [size, setSize] = useState([0, 0]);
+ useLayoutEffect(() => {
+ function updateSize() {
+ setSize([window.innerWidth, window.innerHeight]);
+ }
+ window.addEventListener('resize', updateSize);
+ updateSize();
+ return () => window.removeEventListener('resize', updateSize);
+ }, []);
+ return size;
+}
diff --git a/interface/src/components/index.ts b/interface/src/components/index.ts
index 5d5c89e6..55e0c5ab 100644
--- a/interface/src/components/index.ts
+++ b/interface/src/components/index.ts
@@ -6,3 +6,5 @@ export * from './upload';
export { default as SectionContent } from './SectionContent';
export { default as ButtonRow } from './ButtonRow';
export { default as MessageBox } from './MessageBox';
+export * from './WindowSize';
+
diff --git a/interface/src/components/layout/LayoutMenu.tsx b/interface/src/components/layout/LayoutMenu.tsx
index abb07c93..131b33fe 100644
--- a/interface/src/components/layout/LayoutMenu.tsx
+++ b/interface/src/components/layout/LayoutMenu.tsx
@@ -8,6 +8,7 @@ import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import SettingsIcon from '@mui/icons-material/Settings';
import LockIcon from '@mui/icons-material/Lock';
import WifiIcon from '@mui/icons-material/Wifi';
+import CableIcon from '@mui/icons-material/Cable';
import { FeaturesContext } from '../../contexts/features';
import ProjectMenu from '../../project/ProjectMenu';
@@ -35,6 +36,9 @@ const LayoutMenu: FC = () => {
{features.mqtt && (
)}
+ {features.serial && (
+
+ )}
{features.security && (
)}
diff --git a/interface/src/framework/serial/Serial.tsx b/interface/src/framework/serial/Serial.tsx
new file mode 100644
index 00000000..4b6d8639
--- /dev/null
+++ b/interface/src/framework/serial/Serial.tsx
@@ -0,0 +1,41 @@
+import React, { FC, useContext } from 'react';
+import { Navigate, Route, Routes } from 'react-router-dom';
+
+import { Tab } from '@mui/material';
+
+import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
+import { AuthenticatedContext } from '../../contexts/authentication';
+
+import SerialStatusForm from './SerialStatusForm';
+import SerialSettingsForm from './SerialSettingsForm';
+
+const Serial: FC= () => {
+ useLayoutTitle("Serial");
+
+ const authenticatedContext = useContext(AuthenticatedContext);
+ const { routerTab } = useRouterTab();
+
+ return (
+ <>
+
+
+ {/* */}
+
+
+
+ } />
+
+
+
+ }
+ />
+ } />
+
+ >
+ );
+};
+
+export default Serial;
diff --git a/interface/src/framework/serial/SerialSettingsForm.tsx b/interface/src/framework/serial/SerialSettingsForm.tsx
new file mode 100644
index 00000000..3c3ec04c
--- /dev/null
+++ b/interface/src/framework/serial/SerialSettingsForm.tsx
@@ -0,0 +1,130 @@
+import { FC, useState } from 'react';
+import { ValidateFieldsError } from 'async-validator';
+
+import { Button, Checkbox} from '@mui/material';
+import SaveIcon from '@mui/icons-material/Save';
+
+import * as SerialApi from "../../api/serial";
+import { SerialSettings } from '../../types';
+import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedTextField } from '../../components';
+import { validate, SERIAL_SETTINGS_VALIDATOR } from '../../validators';
+import { numberValue, updateValue, useRest } from '../../utils';
+
+const SerialSettingsForm: FC = () => {
+ const [fieldErrors, setFieldErrors] = useState();
+ const {
+ loadData, saving, data, setData, saveData, errorMessage
+ } = useRest({ read: SerialApi.readSerialSettings, update: SerialApi.updateSerialSettings });
+
+ const updateFormValue = updateValue(setData);
+
+ const content = () => {
+ if (!data) {
+ return ();
+ }
+
+ const validateAndSubmit = async () => {
+ try {
+ setFieldErrors(undefined);
+ await validate(SERIAL_SETTINGS_VALIDATOR, data);
+ saveData();
+ } catch (errors: any) {
+ setFieldErrors(errors);
+ }
+ };
+
+ return (
+ <>
+
+ }
+ label="Enable Serial"
+ />
+
+
+
+
+
+ }
+ label="Inverted signal"
+ />
+
+
+ } disabled={saving} variant="contained" color="primary" type="submit" onClick={validateAndSubmit}>
+ Save
+
+
+ >
+ );
+ };
+
+ return (
+
+ {content()}
+
+ );
+
+};
+
+export default SerialSettingsForm;
diff --git a/interface/src/framework/serial/SerialStatus.ts b/interface/src/framework/serial/SerialStatus.ts
new file mode 100644
index 00000000..88b78ea4
--- /dev/null
+++ b/interface/src/framework/serial/SerialStatus.ts
@@ -0,0 +1,39 @@
+import { Theme } from "@mui/material";
+import { SerialStatus } from "../../types";
+
+export const serialStatusHighlight = ({ enabled }: SerialStatus, theme: Theme) => {
+ if (!enabled) {
+ return theme.palette.info.main;
+ }
+ return theme.palette.success.main;
+}
+
+export const serialStatus = ({ enabled }: SerialStatus) => {
+ if (!enabled) {
+ return "Not enabled";
+ }
+ return "Enabled";
+}
+
+// export const disconnectReason = ({ disconnect_reason }: SerialStatus) => {
+// switch (disconnect_reason) {
+// case SerialDisconnectReason.TCP_DISCONNECTED:
+// return "TCP disconnected";
+// case SerialDisconnectReason.SERIAL_UNACCEPTABLE_PROTOCOL_VERSION:
+// return "Unacceptable protocol version";
+// case SerialDisconnectReason.SERIAL_IDENTIFIER_REJECTED:
+// return "Client ID rejected";
+// case SerialDisconnectReason.SERIAL_SERVER_UNAVAILABLE:
+// return "Server unavailable";
+// case SerialDisconnectReason.SERIAL_MALFORMED_CREDENTIALS:
+// return "Malformed credentials";
+// case SerialDisconnectReason.SERIAL_NOT_AUTHORIZED:
+// return "Not authorized";
+// case SerialDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
+// return "Device out of memory";
+// case SerialDisconnectReason.TLS_BAD_FINGERPRINT:
+// return "Server fingerprint invalid";
+// default:
+// return "Unknown"
+// }
+// }
diff --git a/interface/src/framework/serial/SerialStatusForm.tsx b/interface/src/framework/serial/SerialStatusForm.tsx
new file mode 100644
index 00000000..b6714463
--- /dev/null
+++ b/interface/src/framework/serial/SerialStatusForm.tsx
@@ -0,0 +1,81 @@
+import { FC } from "react";
+
+import {
+ Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme
+} from "@mui/material";
+import DeviceHubIcon from '@mui/icons-material/DeviceHub';
+import RefreshIcon from '@mui/icons-material/Refresh';
+
+import * as SerialApi from "../../api/serial";
+import { SerialStatus } from "../../types";
+import { ButtonRow, FormLoader, SectionContent } from "../../components";
+import { useRest } from "../../utils";
+
+export const serialStatusHighlight = ({ enabled }: SerialStatus, theme: Theme) => {
+ if (!enabled) {
+ return theme.palette.info.main;
+ }
+ return theme.palette.success.main;
+};
+
+export const serialStatus = ({ enabled }: SerialStatus) => {
+ if (!enabled) {
+ return "Not enabled";
+ }
+ return "Enabled";
+};
+
+const SerialStatusForm: FC = () => {
+ const { loadData, data, errorMessage } = useRest({ read: SerialApi.readSerialStatus });
+
+ const theme = useTheme();
+
+ const content = () => {
+ if (!data) {
+ return ();
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ #
+
+
+
+
+
+ #
+
+
+
+
+
+
+ } variant="contained" color="secondary" onClick={loadData}>
+ Refresh
+
+
+ >
+ );
+ };
+
+return (
+
+ {content()}
+
+ );
+
+};
+
+export default SerialStatusForm;
diff --git a/interface/src/types/features.ts b/interface/src/types/features.ts
index 1753d9ab..263364ce 100644
--- a/interface/src/types/features.ts
+++ b/interface/src/types/features.ts
@@ -2,6 +2,7 @@ export interface Features {
project: boolean;
security: boolean;
mqtt: boolean;
+ serial: boolean;
ntp: boolean;
ota: boolean;
upload_firmware: boolean;
diff --git a/interface/src/types/index.ts b/interface/src/types/index.ts
index 3eb9b445..790c7d76 100644
--- a/interface/src/types/index.ts
+++ b/interface/src/types/index.ts
@@ -7,3 +7,5 @@ export * from './security';
export * from './signin';
export * from './system';
export * from './wifi';
+export * from './serial';
+
diff --git a/interface/src/types/serial.ts b/interface/src/types/serial.ts
new file mode 100644
index 00000000..b8ed4139
--- /dev/null
+++ b/interface/src/types/serial.ts
@@ -0,0 +1,42 @@
+export enum Config {
+ SERIAL_5N1 = 0x8000010,
+ SERIAL_6N1 = 0x8000014,
+ SERIAL_7N1 = 0x8000018,
+ SERIAL_8N1 = 0x800001c,
+ SERIAL_5N2 = 0x8000030,
+ SERIAL_6N2 = 0x8000034,
+ SERIAL_7N2 = 0x8000038,
+ SERIAL_8N2 = 0x800003c,
+ SERIAL_5E1 = 0x8000012,
+ SERIAL_6E1 = 0x8000016,
+ SERIAL_7E1 = 0x800001a,
+ SERIAL_8E1 = 0x800001e,
+ SERIAL_5E2 = 0x8000032,
+ SERIAL_6E2 = 0x8000036,
+ SERIAL_7E2 = 0x800003a,
+ SERIAL_8E2 = 0x800003e,
+ SERIAL_5O1 = 0x8000013,
+ SERIAL_6O1 = 0x8000017,
+ SERIAL_7O1 = 0x800001b,
+ SERIAL_8O1 = 0x800001f,
+ SERIAL_5O2 = 0x8000033,
+ SERIAL_6O2 = 0x8000037,
+ SERIAL_7O2 = 0x800003b,
+ SERIAL_8O2 = 0x800003f
+}
+
+export interface SerialStatus {
+ enabled: boolean;
+ baud: number;
+ config: Config;
+}
+
+export interface SerialSettings {
+ enabled: boolean;
+ baud: number;
+ rxpin: number;
+ txpin: number;
+ config: Config;
+ invert: boolean;
+ port: number;
+}
diff --git a/interface/src/types/system.ts b/interface/src/types/system.ts
index 67b15e22..b068c64f 100644
--- a/interface/src/types/system.ts
+++ b/interface/src/types/system.ts
@@ -35,3 +35,18 @@ export interface OTASettings {
port: number;
password: string;
}
+
+export enum LogLevel {
+ DEBUG = 0,
+ INFO = 1,
+ WARNING = 2,
+ ERROR = 3
+}
+
+export interface LogEvent {
+ time: string;
+ level: LogLevel;
+ file: string;
+ line: number;
+ message: string;
+}
diff --git a/interface/src/utils/time.ts b/interface/src/utils/time.ts
index 95080943..094be96f 100644
--- a/interface/src/utils/time.ts
+++ b/interface/src/utils/time.ts
@@ -42,4 +42,8 @@ export const formatDuration = (duration: number) => {
return formatted;
};
+export const formatIsoDateTimeToHr = (dateTime: string) => {
+ return LOCALE_FORMAT.format(new Date(dateTime));
+};
+
const pluralize = (count: number, noun: string, suffix: string = 's') => ` ${count} ${noun}${count !== 1 ? suffix : ''} `;
diff --git a/interface/src/validators/index.ts b/interface/src/validators/index.ts
index 424f3fa3..d93e5ae0 100644
--- a/interface/src/validators/index.ts
+++ b/interface/src/validators/index.ts
@@ -6,3 +6,4 @@ export * from './security';
export * from './shared';
export * from './system';
export * from './wifi';
+export * from './serial';
diff --git a/interface/src/validators/mqtt.ts b/interface/src/validators/mqtt.ts
index 9f9691ff..3fc0d1ac 100644
--- a/interface/src/validators/mqtt.ts
+++ b/interface/src/validators/mqtt.ts
@@ -1,6 +1,6 @@
import Schema from "async-validator";
-import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
+import { IP_OR_HOSTNAME_VALIDATOR, TCP_PORT_VALIDATOR } from './shared';
export const MQTT_SETTINGS_VALIDATOR = new Schema({
host: [
@@ -9,7 +9,7 @@ export const MQTT_SETTINGS_VALIDATOR = new Schema({
],
port: [
{ required: true, message: "Port is required" },
- { type: "number", min: 0, max: 65535, message: "Port must be between 0 and 65535" }
+ TCP_PORT_VALIDATOR
],
keep_alive: [
{ required: true, message: "Keep alive is required" },
diff --git a/interface/src/validators/serial.ts b/interface/src/validators/serial.ts
new file mode 100644
index 00000000..106a2bc0
--- /dev/null
+++ b/interface/src/validators/serial.ts
@@ -0,0 +1,27 @@
+import Schema from 'async-validator';
+
+//TODO Determine what esp we are dealing with
+import { ESP32_PIN_VALIDATOR, TCP_PORT_VALIDATOR } from './shared';
+
+export const SERIAL_SETTINGS_VALIDATOR = new Schema({
+ rxpin: [
+ { required: true, message: "Rx pin is required" },
+ ESP32_PIN_VALIDATOR
+ ],
+ txpin: [
+ { required: true, message: "Tx pin is required" },
+ ESP32_PIN_VALIDATOR
+ ],
+ baud: [
+ {required: true, message: "Baud rate is required"},
+ { type: "number", min: 0, max: 115200, message: "Baud rate must be between 1 and 115200 (0 for automatic)" }
+ ],
+ config : [
+ {required: true, message: "Config is required"},
+ { type: "number", min: 134217744, max: 134217791, message: "Config must be between 134217744 and 134217791" }
+ ],
+ port: [
+ { required: true, message: "Port is required" },
+ TCP_PORT_VALIDATOR
+ ],
+});
diff --git a/interface/src/validators/shared.ts b/interface/src/validators/shared.ts
index e66b31e4..1edf554d 100644
--- a/interface/src/validators/shared.ts
+++ b/interface/src/validators/shared.ts
@@ -1,4 +1,4 @@
-import Schema, { InternalRuleItem, ValidateOption } from "async-validator";
+import Schema, {RuleItem, InternalRuleItem, ValidateOption } from "async-validator";
export const validate = (validator: Schema, source: Partial, options?: ValidateOption): Promise => {
return new Promise(
@@ -53,3 +53,24 @@ export const IP_OR_HOSTNAME_VALIDATOR = {
}
}
};
+
+export const ESP32_PIN_VALIDATOR: RuleItem = {
+ type: "number",
+ min: 0,
+ max: 42,
+ message: "ESP32 pin must be between 0 and 36"
+};
+
+export const ESP8266_PIN_VALIDATOR: RuleItem = {
+ type: "number",
+ min: 0,
+ max: 16,
+ message: "ESP8266 pin must be between 0 and 16"
+};
+
+export const TCP_PORT_VALIDATOR: RuleItem = {
+ type: "number",
+ min: 0,
+ max: 65535,
+ message: "Port must be between 0 and 65535"
+};
diff --git a/lib/framework/ESP8266React.cpp b/lib/framework/ESP8266React.cpp
index 6e8969f6..1c678264 100644
--- a/lib/framework/ESP8266React.cpp
+++ b/lib/framework/ESP8266React.cpp
@@ -12,6 +12,10 @@ ESP8266React::ESP8266React(AsyncWebServer* server) :
_ntpSettingsService(server, &ESPFS, &_securitySettingsService),
_ntpStatus(server, &_securitySettingsService),
#endif
+#if FT_ENABLED(FT_SERIAL)
+ _serialSettingsService(server, &ESPFS, &_securitySettingsService),
+ _serialStatus(server, &_serialSettingsService, &_securitySettingsService),
+#endif
#if FT_ENABLED(FT_OTA)
_otaSettingsService(server, &ESPFS, &_securitySettingsService),
#endif
@@ -27,7 +31,8 @@ ESP8266React::ESP8266React(AsyncWebServer* server) :
#endif
_restartService(server, &_securitySettingsService),
_factoryResetService(server, &ESPFS, &_securitySettingsService),
- _systemStatus(server, &_securitySettingsService) {
+ _systemStatus(server, &_securitySettingsService),
+ _webSocketLogHandler(server, &_securitySettingsService) {
#ifdef PROGMEM_WWW
// Serve static resources from PROGMEM
WWWData::registerRoutes(
@@ -86,6 +91,11 @@ void ESP8266React::begin() {
#elif defined(ESP8266)
ESPFS.begin();
#endif
+ // Begin logging
+ //Logger::begin(_fs);
+ //Logger::getInstance()->addEventHandler(SerialLogHandler::logEvent);
+ _webSocketLogHandler.begin();
+
_wifiSettingsService.begin();
_apSettingsService.begin();
#if FT_ENABLED(FT_NTP)
@@ -100,9 +110,15 @@ void ESP8266React::begin() {
#if FT_ENABLED(FT_SECURITY)
_securitySettingsService.begin();
#endif
+#if FT_ENABLED(FT_SERIAL)
+ _serialSettingsService.begin();
+#endif
}
void ESP8266React::loop() {
+ //Logger::loop();
+ _webSocketLogHandler.loop();
+
_wifiSettingsService.loop();
_apSettingsService.loop();
#if FT_ENABLED(FT_OTA)
@@ -111,4 +127,7 @@ void ESP8266React::loop() {
#if FT_ENABLED(FT_MQTT)
_mqttSettingsService.loop();
#endif
+#if FT_ENABLED(FT_SERIAL)
+ _serialSettingsService.loop();
+#endif
}
diff --git a/lib/framework/ESP8266React.h b/lib/framework/ESP8266React.h
index 42e3f7a8..f83cb9b6 100644
--- a/lib/framework/ESP8266React.h
+++ b/lib/framework/ESP8266React.h
@@ -20,15 +20,20 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
#include
+#include
+#include
#include
#include
#include
#include
#include
+#include
#ifdef PROGMEM_WWW
#include
@@ -73,6 +78,12 @@ class ESP8266React {
}
#endif
+#if FT_ENABLED(FT_SERIAL)
+ StatefulService* getSerialSettingsService() {
+ return &_serialSettingsService;
+ }
+#endif
+
#if FT_ENABLED(FT_OTA)
StatefulService* getOTASettingsService() {
return &_otaSettingsService;
@@ -94,6 +105,7 @@ class ESP8266React {
}
private:
+ FS* _fs;
FeaturesService _featureService;
SecuritySettingsService _securitySettingsService;
WiFiSettingsService _wifiSettingsService;
@@ -105,6 +117,10 @@ class ESP8266React {
NTPSettingsService _ntpSettingsService;
NTPStatus _ntpStatus;
#endif
+#if FT_ENABLED(FT_SERIAL)
+ SerialSettingsService _serialSettingsService;
+ SerialStatus _serialStatus;
+#endif
#if FT_ENABLED(FT_OTA)
OTASettingsService _otaSettingsService;
#endif
@@ -121,6 +137,7 @@ class ESP8266React {
RestartService _restartService;
FactoryResetService _factoryResetService;
SystemStatus _systemStatus;
+ WebSocketLogHandler _webSocketLogHandler;
};
#endif
diff --git a/lib/framework/ESPUtils.h b/lib/framework/ESPUtils.h
new file mode 100644
index 00000000..5ef61a33
--- /dev/null
+++ b/lib/framework/ESPUtils.h
@@ -0,0 +1,29 @@
+#ifndef ESPUtils_h
+#define ESPUtils_h
+
+#include
+
+class ESPUtils {
+ public:
+ static String defaultDeviceValue(const String prefix = "") {
+#ifdef ESP32
+ return prefix + String((unsigned long)ESP.getEfuseMac(), HEX);
+#elif defined(ESP8266)
+ return prefix + String(ESP.getChipId(), HEX);
+#endif
+ }
+
+ static String toISOString(const tm* time, bool incOffset) {
+ char time_string[25];
+ strftime(time_string, 25, incOffset ? "%FT%T%z" : "%FT%TZ", time);
+ return String(time_string);
+ }
+
+ static String toHrString(const tm* time) {
+ char time_string[25];
+ strftime(time_string, 25, "%F %T%z", time);
+ return time_string;
+ }
+};
+
+#endif // end ESPUtils
diff --git a/lib/framework/Features.h b/lib/framework/Features.h
index 2de82c51..231b4380 100644
--- a/lib/framework/Features.h
+++ b/lib/framework/Features.h
@@ -18,6 +18,11 @@
#define FT_MQTT 1
#endif
+// serial feature on by default
+#ifndef FT_SERIAL
+#define FT_SERIAL 1
+#endif
+
// ntp feature on by default
#ifndef FT_NTP
#define FT_NTP 1
diff --git a/lib/framework/FeaturesService.cpp b/lib/framework/FeaturesService.cpp
index 095f1f2d..39c4f837 100644
--- a/lib/framework/FeaturesService.cpp
+++ b/lib/framework/FeaturesService.cpp
@@ -22,6 +22,11 @@ void FeaturesService::features(AsyncWebServerRequest* request) {
#else
root["mqtt"] = false;
#endif
+#if FT_ENABLED(FT_SERIAL)
+ root["serial"] = true;
+#else
+ root["serial"] = false;
+#endif
#if FT_ENABLED(FT_NTP)
root["ntp"] = true;
#else
diff --git a/lib/framework/MqttSettingsService.h b/lib/framework/MqttSettingsService.h
index a0117438..0862fe16 100644
--- a/lib/framework/MqttSettingsService.h
+++ b/lib/framework/MqttSettingsService.h
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
#ifndef FACTORY_MQTT_ENABLED
#define FACTORY_MQTT_ENABLED false
diff --git a/lib/framework/NTPStatus.h b/lib/framework/NTPStatus.h
index 7bb91805..a384fa57 100644
--- a/lib/framework/NTPStatus.h
+++ b/lib/framework/NTPStatus.h
@@ -28,4 +28,6 @@ class NTPStatus {
void ntpStatus(AsyncWebServerRequest* request);
};
+String toISOString(tm* time, bool incOffset);
+
#endif // end NTPStatus_h
diff --git a/lib/framework/SerialSettingsService.cpp b/lib/framework/SerialSettingsService.cpp
new file mode 100644
index 00000000..8b9d7db2
--- /dev/null
+++ b/lib/framework/SerialSettingsService.cpp
@@ -0,0 +1,41 @@
+#include "SerialSettingsService.h"
+
+SerialSettingsService::SerialSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
+ httpEndpoint(SerialSettings::read, SerialSettings::update, this, server, SERIAL_SETTINGS_SERVICE_PATH, securityManager),
+ fsPersistence(SerialSettings::read, SerialSettings::update, this, fs, SERIAL_SETTINGS_FILE) {
+ addUpdateHandler([&](const String& originId) { configureSerial(); }, false);
+}
+
+void SerialSettingsService::begin() {
+ fsPersistence.readFromFS();
+ configureSerial();
+ Serial.println("Stopping ser2net server");
+ _tcpServer = StreamServer{&_serial};
+ _tcpServer.setup();
+}
+
+void SerialSettingsService::loop() {
+ if(_state.enabled) {
+ _tcpServer.loop();
+ }
+}
+
+void SerialSettingsService::end() {
+ Serial.println("Stopping ser2net server");
+ _tcpServer.end();
+ _serial.end();
+}
+
+void SerialSettingsService::configureSerial() {
+ // disconnect if currently connected
+ end();
+ // only connect if Serial is enabled
+ if (_state.enabled) {
+ Serial.printf("Starting serial with rx pin %u and tx pin %u at %u baud\n",_state.rxPin, _state.txPin, _state.baud);
+ _serial.begin(_state.baud, _state.config, _state.rxPin, _state.txPin, _state.invert);
+ Serial.printf("Starting tcp server on port %u\n", _state.tCPPort);
+ _tcpServer.set_port(_state.tCPPort);
+ _tcpServer.setup();
+ }
+}
+
diff --git a/lib/framework/SerialSettingsService.h b/lib/framework/SerialSettingsService.h
new file mode 100644
index 00000000..0b2769a3
--- /dev/null
+++ b/lib/framework/SerialSettingsService.h
@@ -0,0 +1,84 @@
+#ifndef SerialSettingsService_h
+#define SerialSettingsService_h
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef ESP32
+#include
+#define HARDWARE_SERIAL_NUMBER 2
+#elif defined(ESP8266)
+#include
+#endif
+
+#include
+
+#define SERIAL_SETTINGS_FILE "/config/serialSettings.json"
+#define SERIAL_SETTINGS_SERVICE_PATH "/rest/serialSettings"
+
+class SerialSettings {
+ public:
+ bool enabled;
+ uint8_t rxPin;
+ uint8_t txPin;
+ uint32_t baud;
+ uint32_t config;
+ bool invert;
+ uint16_t tCPPort;
+
+ static void read(SerialSettings& settings, JsonObject& root) {
+ root["enabled"] = settings.enabled;
+ root["rxpin"] = settings.rxPin;
+ root["txpin"] = settings.txPin;
+ root["baud"] = settings.baud;
+ root["config"] = settings.config;
+ root["invert"] = settings.invert;
+ root["port"] = settings.tCPPort;
+ }
+
+ static StateUpdateResult update(JsonObject& root, SerialSettings& settings) {
+ settings.enabled = root["enabled"] | FACTORY_SERIAL_ENABLED;
+ settings.rxPin = root["rxpin"] | FACTORY_SERIAL_RXPIN;
+ settings.txPin = root["txpin"] | FACTORY_SERIAL_TXPIN;
+ settings.baud = root["baud"] | FACTORY_SERIAL_BAUD;
+ settings.config = root["config"] | FACTORY_SERIAL_CONFIG;
+ settings.invert = root["invert"] | FACTORY_SERIAL_INVERTED;
+ settings.tCPPort = root["port"] | FACTORY_TCP_PORT;
+ serializeJsonPretty(root, Serial);
+ return StateUpdateResult::CHANGED;
+ }
+};
+class SerialSettingsService : public StatefulService {
+public:
+ SerialSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
+
+ void setup();
+ void begin();
+ void loop();
+ void end();
+ void dump_config() ;
+ bool isEnabled() { return _state.enabled; };
+ uint32_t baud() { return _state.baud; };
+ uint32_t returnConfig() { return _state.config; };
+ void onConfigUpdate() { configureSerial(); } ;
+
+private:
+ HttpEndpoint httpEndpoint;
+ FSPersistence fsPersistence;
+
+ #ifdef ESP32
+ HardwareSerial _serial{HARDWARE_SERIAL_NUMBER};
+ #elif defined(ESP8266)
+ SoftwareSerial _serial;
+ #endif
+
+ StreamServer _tcpServer{nullptr};
+
+ void configureSerial();
+};
+
+#endif // end SerialSettingsService_h
\ No newline at end of file
diff --git a/lib/framework/SerialStatus.cpp b/lib/framework/SerialStatus.cpp
new file mode 100644
index 00000000..455f6dfb
--- /dev/null
+++ b/lib/framework/SerialStatus.cpp
@@ -0,0 +1,23 @@
+#include
+
+SerialStatus::SerialStatus(AsyncWebServer* server,
+ SerialSettingsService* serialSettingsService,
+ SecurityManager* securityManager) :
+ _serialSettingsService(serialSettingsService) {
+ server->on(SERIAL_STATUS_SERVICE_PATH,
+ HTTP_GET,
+ securityManager->wrapRequest(std::bind(&SerialStatus::serialStatus, this, std::placeholders::_1),
+ AuthenticationPredicates::IS_AUTHENTICATED));
+}
+
+void SerialStatus::serialStatus(AsyncWebServerRequest* request) {
+ AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SERIAL_STATUS_SIZE);
+ JsonObject root = response->getRoot();
+
+ root["enabled"] = _serialSettingsService->isEnabled();
+ root["baud"] = _serialSettingsService->baud();
+ root["config"] = _serialSettingsService->returnConfig();
+
+ response->setLength();
+ request->send(response);
+}
diff --git a/lib/framework/SerialStatus.h b/lib/framework/SerialStatus.h
new file mode 100644
index 00000000..c801038b
--- /dev/null
+++ b/lib/framework/SerialStatus.h
@@ -0,0 +1,31 @@
+#ifndef SerialStatus_h
+#define SerialStatus_h
+
+#ifdef ESP32
+#include
+#include
+#elif defined(ESP8266)
+#include
+#include
+#endif
+
+#include
+#include
+#include
+#include
+#include
+
+#define MAX_SERIAL_STATUS_SIZE 1024
+#define SERIAL_STATUS_SERVICE_PATH "/rest/serialStatus"
+
+class SerialStatus {
+ public:
+ SerialStatus(AsyncWebServer* server, SerialSettingsService* serialSettingsService, SecurityManager* securityManager);
+
+ private:
+ SerialSettingsService* _serialSettingsService;
+
+ void serialStatus(AsyncWebServerRequest* request);
+};
+
+#endif // end SerialStatus_h
diff --git a/lib/framework/StreamServer.cpp b/lib/framework/StreamServer.cpp
new file mode 100644
index 00000000..8cd95244
--- /dev/null
+++ b/lib/framework/StreamServer.cpp
@@ -0,0 +1,100 @@
+/* Copyright (C) 2020 Oxan van Leeuwen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "StreamServer.h"
+
+void StreamServer::setup() {
+ Serial.println("Setting up stream server...");
+ this->recv_buf_.reserve(128);
+
+ this->server_ = AsyncServer(this->port_);
+ this->server_.begin();
+ this->server_.onClient([this](void *h, AsyncClient *tcpClient) {
+ if(tcpClient == nullptr)
+ return;
+
+ this->clients_.push_back(std::unique_ptr(new Client(tcpClient, this->recv_buf_)));
+ }, this);
+}
+
+void StreamServer::loop() {
+ this->cleanup();
+ this->read();
+ this->write();
+}
+
+void StreamServer::cleanup() {
+ auto discriminator = [](std::unique_ptr &client) { return !client->disconnected; };
+ auto last_client = std::partition(this->clients_.begin(), this->clients_.end(), discriminator);
+ for (auto it = last_client; it != this->clients_.end(); it++)
+ Serial.printf("Client %s disconnected\n", (*it)->identifier.c_str());
+
+ this->clients_.erase(last_client, this->clients_.end());
+}
+
+void StreamServer::read() {
+ int len;
+ while ((len = this->stream_->available()) > 0) {
+ char buf[128];
+ size_t read = this->stream_->readBytes(buf, min(len, 128));
+ for (auto const& client : this->clients_)
+ client->tcp_client->write(buf, read);
+ }
+}
+
+void StreamServer::write() {
+ size_t len;
+ while ((len = this->recv_buf_.size()) > 0) {
+ this->stream_->write(this->recv_buf_.data(), len);
+ this->recv_buf_.erase(this->recv_buf_.begin(), this->recv_buf_.begin() + len);
+ }
+}
+
+void StreamServer::dump_config() {
+ // Serial.println(TAG, "Stream Server:");
+ // Serial.println(TAG, " Address: %s:%u", network_get_address().c_str(), this->port_);
+}
+
+void StreamServer::on_shutdown() {
+ for (auto &client : this->clients_)
+ client->tcp_client->close(true);
+}
+
+void StreamServer::end() {
+ this->on_shutdown();
+ this->server_.end();
+}
+
+StreamServer::Client::Client(AsyncClient *client, std::vector &recv_buf) :
+ tcp_client{client}, identifier{client->remoteIP().toString().c_str()}, disconnected{false} {
+ Serial.printf("New client connected from %s\n",this->identifier.c_str());
+
+ this->tcp_client->onError( [this](void *h, AsyncClient *client, int8_t error) { this->disconnected = true; });
+ this->tcp_client->onDisconnect([this](void *h, AsyncClient *client) { this->disconnected = true; });
+ this->tcp_client->onTimeout( [this](void *h, AsyncClient *client, uint32_t time) { this->disconnected = true; });
+
+ this->tcp_client->onData([&](void *h, AsyncClient *client, void *data, size_t len) {
+ if (len == 0 || data == nullptr)
+ return;
+
+ auto buf = static_cast(data);
+ recv_buf.insert(recv_buf.end(), buf, buf + len);
+ }, nullptr);
+}
+
+StreamServer::Client::~Client() {
+ delete this->tcp_client;
+}
\ No newline at end of file
diff --git a/lib/framework/StreamServer.h b/lib/framework/StreamServer.h
new file mode 100644
index 00000000..1fe08c8d
--- /dev/null
+++ b/lib/framework/StreamServer.h
@@ -0,0 +1,63 @@
+/* Copyright (C) 2020 Oxan van Leeuwen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#ifdef ESP32
+#include
+#include
+#elif defined(ESP8266)
+#include
+#include
+#endif
+
+class StreamServer {
+public:
+ StreamServer(Stream *stream) : stream_{stream} {}
+
+ void setup();
+ void loop() ;
+ void dump_config() ;
+ void on_shutdown() ;
+ void end() ;
+
+ void set_port(uint16_t port) { this->port_ = port; }
+
+protected:
+ void cleanup();
+ void read();
+ void write();
+
+ struct Client {
+ Client(AsyncClient *client, std::vector &recv_buf);
+ ~Client();
+
+ AsyncClient *tcp_client{nullptr};
+ std::string identifier{};
+ bool disconnected{false};
+ };
+
+ Stream *stream_{};
+ AsyncServer server_{0};
+ uint16_t port_{6638};
+ std::vector recv_buf_{};
+ std::vector> clients_{};
+};
\ No newline at end of file
diff --git a/lib/framework/WebSocketLogHandler.h b/lib/framework/WebSocketLogHandler.h
new file mode 100644
index 00000000..9f063dca
--- /dev/null
+++ b/lib/framework/WebSocketLogHandler.h
@@ -0,0 +1,104 @@
+#ifndef WebSocketLogHandler_h
+#define WebSocketLogHandler_h
+
+#include
+#include
+//#include
+#include
+#include
+
+#define WEB_SOCKET_LOG_PATH "/ws/log"
+#define WEB_SOCKET_LOG_BUFFER 512
+
+enum LogLevel { DEBUG = 0, INFO = 1, WARNING = 2, ERROR = 3 };
+
+class LogEvent {
+ public:
+ uint32_t id;
+ time_t time;
+ LogLevel level;
+ String file;
+ uint16_t line;
+ String message;
+
+ static void serialize(LogEvent& logEvent, JsonObject& root) {
+ root["time"] = logEvent.time;
+ root["level"] = (uint8_t)logEvent.level;
+ root["file"] = logEvent.file;
+ root["line"] = logEvent.line;
+ root["message"] = logEvent.message;
+ }
+
+ static void deserialize(JsonObject& root, LogEvent& logEvent) {
+ logEvent.time = root["time"];
+ logEvent.level = (LogLevel)root["level"].as();
+ logEvent.file = root["file"] | "";
+ logEvent.line = root["line"];
+ logEvent.message = root["message"] | "";
+ }
+};
+class WebSocketLogHandler {
+ public:
+ WebSocketLogHandler(AsyncWebServer* server, SecurityManager* securityManager) : _webSocket(WEB_SOCKET_LOG_PATH) {
+ _webSocket.setFilter(securityManager->filterRequest(AuthenticationPredicates::IS_ADMIN));
+ server->addHandler(&_webSocket);
+ server->on(WEB_SOCKET_LOG_PATH, HTTP_GET, std::bind(&WebSocketLogHandler::forbidden, this, std::placeholders::_1));
+ }
+
+ void begin() {
+ //Logger::getInstance()->addEventHandler(std::bind(&WebSocketLogHandler::logEvent, this, std::placeholders::_1));
+ }
+
+ void loop() {
+ unsigned long currentMillis = millis();
+
+ if(currentMillis - previousMillis > 1000) {
+ // save the last time you blinked the LED
+ previousMillis = currentMillis;
+ helloWorld();
+ }
+ }
+ private:
+ AsyncWebSocket _webSocket;
+ long previousMillis;
+
+ void forbidden(AsyncWebServerRequest* request) {
+ request->send(403);
+ }
+
+ void helloWorld() {
+ LogEvent helloWorldEvent;
+ helloWorldEvent.message = "Hello world";
+ logEvent(helloWorldEvent);
+ }
+
+ boolean logEvent(LogEvent& logEvent) {
+ // if there are no clients, don't bother doing anything
+ if (!_webSocket.getClients().length()) {
+ return true;
+ }
+ if (!_webSocket.availableForWriteAll()) {
+ return false;
+ }
+
+ // create JsonObject to hold log event
+ DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_LOG_BUFFER);
+ JsonObject jsonObject = jsonDocument.to();
+ jsonObject["time"] = ESPUtils::toISOString(localtime(&logEvent.time), true);
+ jsonObject["level"] = logEvent.level;
+ jsonObject["file"] = logEvent.file;
+ jsonObject["line"] = logEvent.line;
+ jsonObject["message"] = logEvent.message;
+
+ // transmit log event to all clients
+ size_t len = measureJson(jsonDocument);
+ AsyncWebSocketMessageBuffer* buffer = _webSocket.makeBuffer(len);
+ if (buffer) {
+ serializeJson(jsonDocument, (char*)buffer->get(), len + 1);
+ _webSocket.textAll(buffer);
+ }
+ return true;
+ }
+};
+
+#endif
diff --git a/platformio.ini b/platformio.ini
index f83cf677..094673cb 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -40,13 +40,13 @@ lib_deps =
;ESP Async WebServer@>=1.2.0,<2.0.0
AsyncMqttClient@>=0.9.0,<1.0.0
-[env:esp12e]
+[env:esp12e_common]
platform = espressif8266
board = esp12e
board_build.f_cpu = 160000000L
board_build.filesystem = littlefs
-[env:node32s]
+[env:node32s_common]
; Comment out min_spiffs.csv setting if disabling PROGMEM_WWW with ESP32
board_build.partitions = min_spiffs.csv
platform = espressif32
diff --git a/src/main.cpp b/src/main.cpp
index 0b1081a4..521480f3 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,9 +1,11 @@
#include
+#include
#include
#include
#define SERIAL_BAUD_RATE 115200
+//StreamServer serialServer;
AsyncWebServer server(80);
ESP8266React esp8266React(&server);
LightMqttSettingsService lightMqttSettingsService =
@@ -20,6 +22,9 @@ void setup() {
// start the framework and demo project
esp8266React.begin();
+ // // start the ser2net server
+ // serialServer.setup();
+
// load the initial light settings
lightStateService.begin();
@@ -33,4 +38,5 @@ void setup() {
void loop() {
// run the framework's loop function
esp8266React.loop();
+ // serialServer.loop();
}