Skip to content

Serial feature #309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4b372f3
Add Streamserver
mjkl-gh Jan 2, 2021
21e092c
add Streamserver to main
mjkl-gh Jan 2, 2021
f6fd0bd
Add EventConsole
mjkl-gh Dec 25, 2021
b8145c6
Merge branch 'master' of https://github.com/rjwats/esp8266-react into…
mjkl-gh Dec 25, 2021
5e3b6e2
Merge branch 'serial' of https://github.com/mjkl-gh/esp-dsmr into ser…
mjkl-gh Dec 25, 2021
d99de58
Add SerialService
mjkl-gh Feb 7, 2021
ebbf9f4
Comment out LogEventController
mjkl-gh Dec 25, 2021
5a69d33
Merge ser2net
mjkl-gh Dec 25, 2021
39a8d2d
Rename ser2net to serial
mjkl-gh Dec 25, 2021
c72cbc0
add baud to SerialStatus
mjkl-gh Feb 7, 2021
dc7b28c
WIP Serial page
mjkl-gh Feb 7, 2021
2b3996a
Merge fixes from main branch
mjkl-gh Dec 25, 2021
0c9d454
Merge remote-tracking branch 'refs/remotes/Upstream/master'
mjkl-gh Dec 25, 2021
9249d1e
WIP update serial feature to new format
mjkl-gh Jun 6, 2022
5888bdc
Merge remote-tracking branch 'Upstream/master' into SerialFeature-Fix
mjkl-gh Jun 6, 2022
cddf41e
fix Tab moved to @mui/material from @material-ui/core
mjkl-gh Jun 6, 2022
11fb837
styleguide fixes
mjkl-gh Jun 8, 2022
6725888
Fix typo in Serial status title
mjkl-gh Jun 16, 2022
70ec5fc
Set sensible defaults for serial pins for both platforms
mjkl-gh Jun 16, 2022
8710636
WIP logevent console
mjkl-gh Jun 16, 2022
a7148b4
Fix endpoint.ts and env.ts names
mjkl-gh Jun 16, 2022
0760a5e
Fix double dependency
mjkl-gh Jun 16, 2022
956aacc
Simplify serial feature
mjkl-gh Jun 19, 2022
436a00e
add numbervalue to serialsettingsform
mjkl-gh Jun 19, 2022
44b5c7e
Fix settings forms
mjkl-gh Jun 20, 2022
35f37f3
Remove unnecessary this
mjkl-gh Jun 20, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions factory_settings.ini
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions features.ini
Original file line number Diff line number Diff line change
@@ -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
204 changes: 199 additions & 5 deletions interface/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion interface/package.json
Original file line number Diff line number Diff line change
@@ -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",
4 changes: 4 additions & 0 deletions interface/src/AuthenticatedRouting.tsx
Original file line number Diff line number Diff line change
@@ -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 && (
<Route path="/mqtt/*" element={<Mqtt />} />
)}
{features.serial && (
<Route path="/serial/*" element={<Serial />} />
)}
{features.security && (
<Route
path="/security/*"
23 changes: 23 additions & 0 deletions interface/src/api/env.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,25 @@
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME || 'ESP8266 React';
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH || 'project';

export const ENDPOINT_ROOT = calculateEndpointRoot("/rest/");
export const EVENT_SOURCE_ROOT = calculateEndpointRoot("/es/");
export const WEB_SOCKET_ROOT = calculateWebSocketRoot("/ws/");

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) {
return webSocketRoot + webSocketPath;
}
const location = window.location;
const webProtocol = location.protocol === "https:" ? "wss:" : "ws:";
return webProtocol + "//" + location.host + webSocketPath;
}
16 changes: 16 additions & 0 deletions interface/src/api/serial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AxiosPromise } from "axios";

import { SerialSettings, SerialStatus } from "../types";
import { AXIOS } from "./endpoints";

export function readSerialStatus(): AxiosPromise<SerialStatus> {
return AXIOS.get('/serialStatus');
}

export function readSerialSettings(): AxiosPromise<SerialSettings> {
return AXIOS.get('/serialSettings');
}

export function updateSerialSettings(serialSettings: SerialSettings): AxiosPromise<SerialSettings> {
return AXIOS.post('/serialSettings', serialSettings);
}
14 changes: 14 additions & 0 deletions interface/src/components/WindowSize.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions interface/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -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';

4 changes: 4 additions & 0 deletions interface/src/components/layout/LayoutMenu.tsx
Original file line number Diff line number Diff line change
@@ -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 && (
<LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />
)}
{features.serial && (
<LayoutMenuItem icon={CableIcon} label="Serial" to="/serial" />
)}
{features.security && (
<LayoutMenuItem icon={LockIcon} label="Security" to="/security" disabled={!authenticatedContext.me.admin} />
)}
41 changes: 41 additions & 0 deletions interface/src/framework/serial/Serial.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="Serial Status" />
{/* <Tab value="log" label="Remote Log" /> */}
<Tab value="settings" label="Serial Settings" disabled={!authenticatedContext.me.admin} />
</RouterTabs>
<Routes>
<Route path="status" element={<SerialStatusForm />} />
<Route
path="settings"
element={
<RequireAdmin>
<SerialSettingsForm />
</RequireAdmin>
}
/>
<Route path="/*" element={<Navigate replace to="status" />} />
</Routes>
</>
);
};

export default Serial;
130 changes: 130 additions & 0 deletions interface/src/framework/serial/SerialSettingsForm.tsx
Original file line number Diff line number Diff line change
@@ -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<ValidateFieldsError>();
const {
loadData, saving, data, setData, saveData, errorMessage
} = useRest<SerialSettings>({ read: SerialApi.readSerialSettings, update: SerialApi.updateSerialSettings });

const updateFormValue = updateValue(setData);

const content = () => {
if (!data) {
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
}

const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(SERIAL_SETTINGS_VALIDATOR, data);
saveData();
} catch (errors: any) {
setFieldErrors(errors);
}
};

return (
<>
<BlockFormControlLabel
control={
<Checkbox
name="enabled"
checked={data.enabled}
onChange={updateFormValue}
/>
}
label="Enable Serial"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="rxpin"
label="rx pin"
fullWidth
variant="outlined"
value={numberValue(data.rxpin)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="txpin"
label="tx pin"
fullWidth
variant="outlined"
value={numberValue(data.txpin)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="baud"
label="Baud rate"
fullWidth
variant="outlined"
value={numberValue(data.baud)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="config"
label="Config"
fullWidth
variant="outlined"
value={numberValue(data.config)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
name="invert"
checked={data.invert}
onChange={updateFormValue}
/>
}
label="Inverted signal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="port"
label="Port"
fullWidth
variant="outlined"
value={numberValue(data.port)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
<ButtonRow mt={1}>
<Button startIcon={<SaveIcon />} disabled={saving} variant="contained" color="primary" type="submit" onClick={validateAndSubmit}>
Save
</Button>
</ButtonRow>
</>
);
};

return (
<SectionContent title='Serial Settings' titleGutter>
{content()}
</SectionContent>
);

};

export default SerialSettingsForm;
39 changes: 39 additions & 0 deletions interface/src/framework/serial/SerialStatus.ts
Original file line number Diff line number Diff line change
@@ -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"
// }
// }
81 changes: 81 additions & 0 deletions interface/src/framework/serial/SerialStatusForm.tsx
Original file line number Diff line number Diff line change
@@ -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<SerialStatus>({ read: SerialApi.readSerialStatus });

const theme = useTheme();

const content = () => {
if (!data) {
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
}

return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: serialStatusHighlight(data, theme) }}>
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={serialStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>#</Avatar>
</ListItemAvatar>
<ListItemText primary="Baud rate" secondary={data.baud} />
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>#</Avatar>
</ListItemAvatar>
<ListItemText primary="Config" secondary={data.config} />
</ListItem>
<Divider variant="inset" component="li" />
</List>
<ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={loadData}>
Refresh
</Button>
</ButtonRow>
</>
);
};

return (
<SectionContent title='Serial Status' titleGutter>
{content()}
</SectionContent>
);

};

export default SerialStatusForm;
1 change: 1 addition & 0 deletions interface/src/types/features.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ export interface Features {
project: boolean;
security: boolean;
mqtt: boolean;
serial: boolean;
ntp: boolean;
ota: boolean;
upload_firmware: boolean;
2 changes: 2 additions & 0 deletions interface/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -7,3 +7,5 @@ export * from './security';
export * from './signin';
export * from './system';
export * from './wifi';
export * from './serial';

42 changes: 42 additions & 0 deletions interface/src/types/serial.ts
Original file line number Diff line number Diff line change
@@ -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;
}
15 changes: 15 additions & 0 deletions interface/src/types/system.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions interface/src/utils/time.ts
Original file line number Diff line number Diff line change
@@ -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 : ''} `;
1 change: 1 addition & 0 deletions interface/src/validators/index.ts
Original file line number Diff line number Diff line change
@@ -6,3 +6,4 @@ export * from './security';
export * from './shared';
export * from './system';
export * from './wifi';
export * from './serial';
4 changes: 2 additions & 2 deletions interface/src/validators/mqtt.ts
Original file line number Diff line number Diff line change
@@ -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" },
27 changes: 27 additions & 0 deletions interface/src/validators/serial.ts
Original file line number Diff line number Diff line change
@@ -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
],
});
23 changes: 22 additions & 1 deletion interface/src/validators/shared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Schema, { InternalRuleItem, ValidateOption } from "async-validator";
import Schema, {RuleItem, InternalRuleItem, ValidateOption } from "async-validator";

export const validate = <T extends object>(validator: Schema, source: Partial<T>, options?: ValidateOption): Promise<T> => {
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"
};
21 changes: 20 additions & 1 deletion lib/framework/ESP8266React.cpp
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 17 additions & 0 deletions lib/framework/ESP8266React.h
Original file line number Diff line number Diff line change
@@ -20,15 +20,20 @@
#include <MqttStatus.h>
#include <NTPSettingsService.h>
#include <NTPStatus.h>
#include <SerialStatus.h>
#include <SerialSettingsService.h>
#include <OTASettingsService.h>
#include <UploadFirmwareService.h>
#include <RestartService.h>
#include <SecuritySettingsService.h>
#include <SerialSettingsService.h>
#include <SerialStatus.h>
#include <SystemStatus.h>
#include <WiFiScanner.h>
#include <WiFiSettingsService.h>
#include <WiFiStatus.h>
#include <ESPFS.h>
#include <WebSocketLogHandler.h>

#ifdef PROGMEM_WWW
#include <WWWData.h>
@@ -73,6 +78,12 @@ class ESP8266React {
}
#endif

#if FT_ENABLED(FT_SERIAL)
StatefulService<SerialSettings>* getSerialSettingsService() {
return &_serialSettingsService;
}
#endif

#if FT_ENABLED(FT_OTA)
StatefulService<OTASettings>* 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
29 changes: 29 additions & 0 deletions lib/framework/ESPUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#ifndef ESPUtils_h
#define ESPUtils_h

#include <Arduino.h>

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
5 changes: 5 additions & 0 deletions lib/framework/Features.h
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions lib/framework/FeaturesService.cpp
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lib/framework/MqttSettingsService.h
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
#include <FSPersistence.h>
#include <AsyncMqttClient.h>
#include <SettingValue.h>
#include <ESPUtils.h>

#ifndef FACTORY_MQTT_ENABLED
#define FACTORY_MQTT_ENABLED false
2 changes: 2 additions & 0 deletions lib/framework/NTPStatus.h
Original file line number Diff line number Diff line change
@@ -28,4 +28,6 @@ class NTPStatus {
void ntpStatus(AsyncWebServerRequest* request);
};

String toISOString(tm* time, bool incOffset);

#endif // end NTPStatus_h
41 changes: 41 additions & 0 deletions lib/framework/SerialSettingsService.cpp
Original file line number Diff line number Diff line change
@@ -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();
}
}

84 changes: 84 additions & 0 deletions lib/framework/SerialSettingsService.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#ifndef SerialSettingsService_h
#define SerialSettingsService_h

#include <HttpEndpoint.h>
#include <FSPersistence.h>
#include <memory>
#include <string>
#include <vector>
#include <Stream.h>

#ifdef ESP32
#include <HardwareSerial.h>
#define HARDWARE_SERIAL_NUMBER 2
#elif defined(ESP8266)
#include <SoftwareSerial.h>
#endif

#include <StreamServer.h>

#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<SerialSettings> {
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<SerialSettings> httpEndpoint;
FSPersistence<SerialSettings> fsPersistence;

#ifdef ESP32
HardwareSerial _serial{HARDWARE_SERIAL_NUMBER};
#elif defined(ESP8266)
SoftwareSerial _serial;
#endif

StreamServer _tcpServer{nullptr};

void configureSerial();
};

#endif // end SerialSettingsService_h
23 changes: 23 additions & 0 deletions lib/framework/SerialStatus.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <SerialStatus.h>

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);
}
31 changes: 31 additions & 0 deletions lib/framework/SerialStatus.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#ifndef SerialStatus_h
#define SerialStatus_h

#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif

#include <SerialSettingsService.h>
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>

#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
100 changes: 100 additions & 0 deletions lib/framework/StreamServer.cpp
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

#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<Client>(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> &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<uint8_t> &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<uint8_t *>(data);
recv_buf.insert(recv_buf.end(), buf, buf + len);
}, nullptr);
}

StreamServer::Client::~Client() {
delete this->tcp_client;
}
63 changes: 63 additions & 0 deletions lib/framework/StreamServer.h
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

#pragma once

#include <memory>
#include <string>
#include <vector>
#include <Stream.h>

#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#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<uint8_t> &recv_buf);
~Client();

AsyncClient *tcp_client{nullptr};
std::string identifier{};
bool disconnected{false};
};

Stream *stream_{};
AsyncServer server_{0};
uint16_t port_{6638};
std::vector<uint8_t> recv_buf_{};
std::vector<std::unique_ptr<Client>> clients_{};
};
104 changes: 104 additions & 0 deletions lib/framework/WebSocketLogHandler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#ifndef WebSocketLogHandler_h
#define WebSocketLogHandler_h

#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
//#include <Logger.h>
#include <ESPUtils.h>
#include <time.h>

#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<uint8_t>();
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>();
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
4 changes: 2 additions & 2 deletions platformio.ini
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#include <ESP8266React.h>
#include <StreamServer.h>
#include <LightMqttSettingsService.h>
#include <LightStateService.h>

#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();
}