intercom tab
This commit is contained in:
parent
324d97c0ce
commit
48edd149de
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
data/www/js/179.e21a.js.gz
Normal file
BIN
data/www/js/179.e21a.js.gz
Normal file
Binary file not shown.
Binary file not shown.
@ -11,7 +11,7 @@ import { Layout, RequireAdmin } from './components';
|
||||
import ProjectRouting from './project/ProjectRouting';
|
||||
|
||||
import NetworkConnection from './framework/network/NetworkConnection';
|
||||
import AccessPoint from './framework/ap/AccessPoint';
|
||||
import Intercom from './framework/intercom/Intercom';
|
||||
import NetworkTime from './framework/ntp/NetworkTime';
|
||||
import Mqtt from './framework/mqtt/Mqtt';
|
||||
import System from './framework/system/System';
|
||||
@ -42,7 +42,7 @@ const AuthenticatedRouting: FC = () => {
|
||||
<Route path={`/${PROJECT_PATH}/*`} element={<ProjectRouting />} />
|
||||
)}
|
||||
<Route path="/network/*" element={<NetworkConnection />} />
|
||||
<Route path="/intercom/*" element={<AccessPoint />} />
|
||||
<Route path="/intercom/*" element={<Intercom />} />
|
||||
{features.ntp && (
|
||||
<Route path="/ntp/*" element={<NetworkTime />} />
|
||||
)}
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
|
||||
import { APSettings, APStatus } from "../types";
|
||||
import { AXIOS } from "./endpoints";
|
||||
|
||||
export function readAPStatus(): AxiosPromise<APStatus> {
|
||||
return AXIOS.get('/intercomStatus');
|
||||
}
|
||||
|
||||
export function readAPSettings(): AxiosPromise<APSettings> {
|
||||
return AXIOS.get('/intercomSettings');
|
||||
}
|
||||
|
||||
export function updateAPSettings(apSettings: APSettings): AxiosPromise<APSettings> {
|
||||
return AXIOS.post('/intercomSettings', apSettings);
|
||||
}
|
24
interface/src/api/intercom.ts
Normal file
24
interface/src/api/intercom.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
|
||||
import { IntercomJournal, IntercomSettings, IntercomStatus, SwitchDoorDTO } from "../types";
|
||||
import { AXIOS } from "./endpoints";
|
||||
|
||||
export function readIntercomStatus(): AxiosPromise<IntercomStatus> {
|
||||
return AXIOS.get('/intercomStatus');
|
||||
}
|
||||
|
||||
export function readIntercomJournal(): AxiosPromise<IntercomJournal> {
|
||||
return AXIOS.get('/intercomJournal');
|
||||
}
|
||||
|
||||
export function readIntercomSettings(): AxiosPromise<IntercomSettings> {
|
||||
return AXIOS.get('/intercomSettings');
|
||||
}
|
||||
|
||||
export function switchDoor(switchDoorDTO: SwitchDoorDTO): AxiosPromise<IntercomSettings> {
|
||||
return AXIOS.post('/switchDoor', switchDoorDTO);
|
||||
}
|
||||
|
||||
export function updateIntercomSettings(intercomSettings: IntercomSettings): AxiosPromise<IntercomSettings> {
|
||||
return AXIOS.post('/intercomSettings', intercomSettings);
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { ValidateFieldsError } from 'async-validator';
|
||||
import { range } from 'lodash';
|
||||
|
||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
|
||||
import * as APApi from "../../api/ap";
|
||||
import { APProvisionMode, APSettings } from '../../types';
|
||||
import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedPasswordField, ValidatedTextField } from '../../components';
|
||||
import { createAPSettingsValidator, validate } from '../../validators';
|
||||
import { numberValue, updateValue, useRest } from '../../utils';
|
||||
|
||||
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
||||
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||
};
|
||||
|
||||
const APSettingsForm: FC = () => {
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const {
|
||||
loadData, saving, data, setData, saveData, errorMessage
|
||||
} = useRest<APSettings>({ read: APApi.readAPSettings, update: APApi.updateAPSettings });
|
||||
|
||||
const updateFormValue = updateValue(setData);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
|
||||
}
|
||||
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createAPSettingsValidator(data), data);
|
||||
saveData();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="provision_mode"
|
||||
label="Provide Access Point…"
|
||||
value={data.provision_mode}
|
||||
fullWidth
|
||||
select
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When WiFi Disconnected</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
||||
</ValidatedTextField>
|
||||
{
|
||||
isAPEnabled(data) &&
|
||||
<>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="ssid"
|
||||
label="Access Point SSID"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.ssid}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
name="password"
|
||||
label="Access Point Password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="channel"
|
||||
label="Preferred Channel"
|
||||
value={numberValue(data.channel)}
|
||||
fullWidth
|
||||
select
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
>
|
||||
{
|
||||
range(1, 14).map((i) => <MenuItem key={i} value={i}>{i}</MenuItem>)
|
||||
}
|
||||
</ValidatedTextField>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="ssid_hidden"
|
||||
checked={data.ssid_hidden}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label="Hide SSID?"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="max_clients"
|
||||
label="Max Clients"
|
||||
value={numberValue(data.max_clients)}
|
||||
fullWidth
|
||||
select
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
>
|
||||
{
|
||||
range(1, 9).map((i) => <MenuItem key={i} value={i}>{i}</MenuItem>)
|
||||
}
|
||||
</ValidatedTextField>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="local_ip"
|
||||
label="Local IP"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.local_ip}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="gateway_ip"
|
||||
label="Gateway"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.gateway_ip}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="subnet_mask"
|
||||
label="Subnet"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.subnet_mask}
|
||||
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='Access Point Settings' titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default APSettingsForm;
|
@ -1,105 +0,0 @@
|
||||
import { FC } from "react";
|
||||
|
||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from "@mui/material";
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import ComputerIcon from '@mui/icons-material/Computer';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
|
||||
import * as APApi from "../../api/ap";
|
||||
import { APNetworkStatus, APStatus } from "../../types";
|
||||
import { ButtonRow, FormLoader, SectionContent } from "../../components";
|
||||
import { useRest } from "../../utils";
|
||||
|
||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return theme.palette.success.main;
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return theme.palette.info.main;
|
||||
case APNetworkStatus.LINGERING:
|
||||
return theme.palette.warning.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
export const apStatus = ({ status }: APStatus) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return "Active";
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return "Inactive";
|
||||
case APNetworkStatus.LINGERING:
|
||||
return "Lingering until idle";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const APStatusForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<APStatus>({ read: APApi.readAPStatus });
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: apStatusHighlight(data, theme) }}>
|
||||
<SettingsInputAntennaIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Status" secondary={apStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>IP</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="IP Address" secondary={data.ip_address} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="MAC Address" secondary={data.mac_address} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<ComputerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="AP Clients" secondary={data.station_num} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
<ButtonRow pt={1}>
|
||||
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={loadData}>
|
||||
Refresh
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title='Access Point Status' titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default APStatusForm;
|
@ -6,10 +6,11 @@ import { Tab } from '@mui/material';
|
||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
|
||||
import APStatusForm from './APStatusForm';
|
||||
import APSettingsForm from './APSettingsForm';
|
||||
import IntercomStatusForm from './IntercomStatusForm';
|
||||
import IntercomJournalForm from './IntercomJournalForm';
|
||||
import IntercomSettingsForm from './IntercomSettingsForm';
|
||||
|
||||
const AccessPoint: FC = () => {
|
||||
const Intercom: FC = () => {
|
||||
useLayoutTitle("Access Point");
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
@ -19,15 +20,17 @@ const AccessPoint: FC = () => {
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="status" label="Intercom Status" />
|
||||
<Tab value="journal" label="Intercom Journal" />
|
||||
<Tab value="settings" label="Intercom Settings" disabled={!authenticatedContext.me.admin} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<APStatusForm />} />
|
||||
<Route path="status" element={<IntercomStatusForm />} />
|
||||
<Route path="journal" element={<IntercomJournalForm />} />
|
||||
<Route
|
||||
path="settings"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<APSettingsForm />
|
||||
<IntercomSettingsForm />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
@ -38,4 +41,4 @@ const AccessPoint: FC = () => {
|
||||
|
||||
};
|
||||
|
||||
export default AccessPoint;
|
||||
export default Intercom;
|
63
interface/src/framework/intercom/IntercomJournalForm.tsx
Normal file
63
interface/src/framework/intercom/IntercomJournalForm.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { FC } from "react";
|
||||
|
||||
import { Button, Divider, List, Paper,
|
||||
Table, TableBody, TableCell, TableContainer,
|
||||
TableHead, TableRow, useTheme } from "@mui/material";
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
|
||||
import * as IntercomApi from "../../api/intercom";
|
||||
import { IntercomJournal } from "../../types";
|
||||
import { ButtonRow, FormLoader, SectionContent } from "../../components";
|
||||
import { useRest } from "../../utils";
|
||||
|
||||
const IntercomJournalForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<IntercomJournal>({ read: IntercomApi.readIntercomJournal });
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Date</TableCell>
|
||||
<TableCell>Apartment</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.notes.map((note, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{note.createdAt}</TableCell>
|
||||
<TableCell>{note.appartment}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
<ButtonRow pt={1}>
|
||||
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={loadData}>
|
||||
Refresh
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title='Intercom Journal' titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default IntercomJournalForm;
|
83
interface/src/framework/intercom/IntercomSettingsForm.tsx
Normal file
83
interface/src/framework/intercom/IntercomSettingsForm.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { ValidateFieldsError } from 'async-validator';
|
||||
|
||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
|
||||
import * as IntercomApi from "../../api/intercom";
|
||||
import { IntercomSettings } from '../../types';
|
||||
import { ButtonRow, FormLoader, SectionContent, ValidatedTextField } from '../../components';
|
||||
import { createIntercomSettingsValidator, validate } from '../../validators';
|
||||
import { updateValue, useRest } from '../../utils';
|
||||
|
||||
const IntercomSettingsForm: FC = () => {
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const {
|
||||
loadData, saving, data, setData, saveData, errorMessage
|
||||
} = useRest<IntercomSettings>({ read: IntercomApi.readIntercomSettings, update: IntercomApi.updateIntercomSettings });
|
||||
|
||||
const updateFormValue = updateValue(setData);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
|
||||
}
|
||||
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createIntercomSettingsValidator(data), data);
|
||||
saveData();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="kmnModel"
|
||||
label="KMN Model"
|
||||
value={data.kmnModel}
|
||||
fullWidth
|
||||
select
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
>
|
||||
{
|
||||
data.kmnModelList.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
</ValidatedTextField>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="firstAppartment"
|
||||
label="First Appartment"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.firstAppartment}
|
||||
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='Intercom Settings' titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntercomSettingsForm;
|
165
interface/src/framework/intercom/IntercomStatusForm.tsx
Normal file
165
interface/src/framework/intercom/IntercomStatusForm.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import { FC } from "react";
|
||||
|
||||
import { Avatar, Box, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from "@mui/material";
|
||||
import PowerIcon from '@mui/icons-material/Power';
|
||||
import PowerOffIcon from '@mui/icons-material/PowerOff';
|
||||
import UpdateIcon from '@mui/icons-material/Update';
|
||||
import DoorFrontIcon from '@mui/icons-material/DoorFront';
|
||||
import MeetingRoomIcon from '@mui/icons-material/MeetingRoom';
|
||||
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
|
||||
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
|
||||
import * as IntercomApi from "../../api/intercom";
|
||||
import { DoorStatus, IntercomConnectionStatus, IntercomStatus, SwitchDoorType } from "../../types";
|
||||
import { ButtonRow, FormLoader, SectionContent } from "../../components";
|
||||
import { useRest } from "../../utils";
|
||||
import {useSnackbar} from "notistack";
|
||||
|
||||
export const intercomStatusHighlight = ({ status }: IntercomStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
case IntercomConnectionStatus.CONNECTED:
|
||||
return theme.palette.success.main;
|
||||
case IntercomConnectionStatus.NOT_CONNECTED:
|
||||
return theme.palette.info.main;
|
||||
case IntercomConnectionStatus.RECEIVING_DATA:
|
||||
return theme.palette.success.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
export const intercomStatusIcon = (status: IntercomConnectionStatus) => {
|
||||
switch (status) {
|
||||
case IntercomConnectionStatus.CONNECTED:
|
||||
return <PowerIcon />;
|
||||
case IntercomConnectionStatus.NOT_CONNECTED:
|
||||
return <PowerOffIcon />;
|
||||
case IntercomConnectionStatus.RECEIVING_DATA:
|
||||
return <UpdateIcon />;
|
||||
default:
|
||||
return <QuestionMarkIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
export const doorStatusIcon = (status: DoorStatus) => {
|
||||
switch (status) {
|
||||
case DoorStatus.CLOSED:
|
||||
return <DoorFrontIcon />;
|
||||
case DoorStatus.OPENED:
|
||||
return <MeetingRoomIcon />;
|
||||
default:
|
||||
return <QuestionMarkIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
export const intercomStatus = ({ status }: IntercomStatus) => {
|
||||
switch (status) {
|
||||
case IntercomConnectionStatus.CONNECTED:
|
||||
return "Active";
|
||||
case IntercomConnectionStatus.NOT_CONNECTED:
|
||||
return "Inactive";
|
||||
case IntercomConnectionStatus.RECEIVING_DATA:
|
||||
return "Receiving data";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
export const doorStatus = (status: DoorStatus) => {
|
||||
switch (status) {
|
||||
case DoorStatus.OPENED:
|
||||
return "Opened";
|
||||
case DoorStatus.CLOSED:
|
||||
return "Closed";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
export const openDoor = () => {
|
||||
IntercomApi.switchDoor({
|
||||
type: SwitchDoorType.JOGGING,
|
||||
status: DoorStatus.OPENED,
|
||||
time: 1
|
||||
})
|
||||
.then(() => {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
enqueueSnackbar("Update successful", { variant: 'success' });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
const IntercomStatusForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<IntercomStatus>({ read: IntercomApi.readIntercomStatus });
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: intercomStatusHighlight(data, theme) }}>
|
||||
{intercomStatusIcon(data.status)}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Status" secondary={intercomStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<NotificationsIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Last Called Number" secondary={data.lastCalledNumber} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
{doorStatusIcon(data.doorStatus)}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Door Status" secondary={doorStatus(data.doorStatus)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
<ButtonRow mt={1}>
|
||||
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={loadData}>
|
||||
Refresh
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow mt={1}>
|
||||
<Button startIcon={<MeetingRoomIcon />} variant="contained" color="info" onClick={openDoor}>
|
||||
Open door
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title='Intercom Status' titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default IntercomStatusForm;
|
@ -1,18 +1,15 @@
|
||||
import { FC, useContext, useEffect, useState } from 'react';
|
||||
import { ValidateFieldsError } from 'async-validator';
|
||||
|
||||
import { Avatar, Button, Checkbox, IconButton, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText } from '@mui/material';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { Button, Checkbox } from '@mui/material';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
|
||||
import * as NetworkApi from "../../api/network";
|
||||
import { NetworkSettings } from '../../types';
|
||||
import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedPasswordField, ValidatedTextField } from '../../components';
|
||||
import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedTextField } from '../../components';
|
||||
import { validate, createNetworkSettingsValidator } from '../../validators';
|
||||
import { updateValue, useRest } from '../../utils';
|
||||
|
||||
import { isNetworkOpen, networkSecurityMode } from './NetworkSelector';
|
||||
import { NetworkConnectionContext } from './NetworkConnectionContext';
|
||||
|
||||
const NetworkSettingsForm: FC = () => {
|
||||
|
@ -1,30 +0,0 @@
|
||||
export enum APProvisionMode {
|
||||
AP_MODE_ALWAYS = 0,
|
||||
AP_MODE_DISCONNECTED = 1,
|
||||
AP_NEVER = 2
|
||||
}
|
||||
|
||||
export enum APNetworkStatus {
|
||||
ACTIVE = 0,
|
||||
INACTIVE = 1,
|
||||
LINGERING = 2
|
||||
}
|
||||
|
||||
export interface APStatus {
|
||||
status: APNetworkStatus;
|
||||
ip_address: string;
|
||||
mac_address: string;
|
||||
station_num: number;
|
||||
}
|
||||
|
||||
export interface APSettings {
|
||||
provision_mode: APProvisionMode;
|
||||
ssid: string;
|
||||
password: string;
|
||||
channel: number;
|
||||
ssid_hidden: boolean;
|
||||
max_clients: number;
|
||||
local_ip: string;
|
||||
gateway_ip: string;
|
||||
subnet_mask: string;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export * from './ap';
|
||||
export * from './intercom';
|
||||
export * from './features';
|
||||
export * from './me';
|
||||
export * from './mqtt';
|
||||
|
43
interface/src/types/intercom.ts
Normal file
43
interface/src/types/intercom.ts
Normal file
@ -0,0 +1,43 @@
|
||||
export enum IntercomConnectionStatus {
|
||||
CONNECTED = 0,
|
||||
NOT_CONNECTED = 1,
|
||||
RECEIVING_DATA = 2
|
||||
}
|
||||
|
||||
export enum DoorStatus {
|
||||
OPENED = 0,
|
||||
CLOSED = 1,
|
||||
}
|
||||
|
||||
export enum SwitchDoorType {
|
||||
ON_OFF = 0,
|
||||
JOGGING = 1,
|
||||
DELAY = 2,
|
||||
}
|
||||
|
||||
export interface IntercomStatus {
|
||||
status: IntercomConnectionStatus;
|
||||
lastCalledNumber: number;
|
||||
doorStatus: DoorStatus;
|
||||
}
|
||||
|
||||
export interface IntercomJournalNote {
|
||||
createdAt: string;
|
||||
appartment: number;
|
||||
}
|
||||
|
||||
export interface IntercomJournal {
|
||||
notes: IntercomJournalNote[];
|
||||
}
|
||||
|
||||
export interface IntercomSettings {
|
||||
kmnModel: string;
|
||||
kmnModelList: string[];
|
||||
firstAppartment: number;
|
||||
}
|
||||
|
||||
export interface SwitchDoorDTO {
|
||||
type: SwitchDoorType;
|
||||
status: DoorStatus;
|
||||
time: number;
|
||||
}
|
@ -17,6 +17,7 @@ export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
|
||||
export const updateValue = <S>(updateEntity: UpdateEntity<S>) => (
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
console.log(event.target.name);
|
||||
updateEntity((prevState) => ({
|
||||
...prevState,
|
||||
[event.target.name]: extractEventValue(event)
|
||||
|
@ -1,4 +1,4 @@
|
||||
export * from './ap';
|
||||
export * from './intercom';
|
||||
export * from './authentication';
|
||||
export * from './mqtt';
|
||||
export * from './ntp';
|
||||
|
@ -1,13 +1,12 @@
|
||||
import Schema from 'async-validator';
|
||||
|
||||
import { APSettings } from '../types';
|
||||
import { isAPEnabled } from '../framework/ap/APSettingsForm';
|
||||
import { IntercomSettings } from '../types';
|
||||
|
||||
import { IP_ADDRESS_VALIDATOR } from './shared';
|
||||
|
||||
export const createAPSettingsValidator = (apSettings: APSettings) => new Schema({
|
||||
export const createIntercomSettingsValidator = (intercomSettings: IntercomSettings) => new Schema({
|
||||
provision_mode: { required: true, message: "Please provide a provision mode" },
|
||||
...(isAPEnabled(apSettings) && {
|
||||
...({
|
||||
ssid: [
|
||||
{ required: true, message: "Please provide an SSID" },
|
||||
{ type: "string", max: 32, message: "SSID must be 32 characters or less" }
|
20457
lib/framework/WWWData.h
20457
lib/framework/WWWData.h
File diff suppressed because it is too large
Load Diff
@ -23,3 +23,4 @@ lib_deps =
|
||||
ottowinter/AsyncMqttClient-esphome@^0.8.6
|
||||
ottowinter/ESPAsyncWebServer-esphome@^3.1.0
|
||||
bblanchon/ArduinoJson@^6.21.3
|
||||
paulstoffregen/Time@^1.6.1
|
||||
|
@ -1,13 +1,15 @@
|
||||
#include "app/routes.h"
|
||||
#include "config/config.h"
|
||||
#include "utils/print.h"
|
||||
#include "utils/settings.h"
|
||||
#include "utils/utils.h"
|
||||
#include "infra/eth.h"
|
||||
#include "infra/httpServer.h"
|
||||
#include "infra/mqtt.h"
|
||||
#include "infra/relay.h"
|
||||
#include "infra/fs.h"
|
||||
#include "config/config.h"
|
||||
#include "utils/print.h"
|
||||
#include "utils/settings.h"
|
||||
#include "utils/utils.h"
|
||||
#include "infra/intercom.h"
|
||||
#include "domain/intercomJournal.h"
|
||||
|
||||
void handleDoorOpen(AsyncWebServerRequest *request) {
|
||||
relayTurnOn();
|
||||
@ -37,15 +39,103 @@ void intercomStatus(AsyncWebServerRequest* request) {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_INTERCOM_STATUS_SIZE);
|
||||
JsonObject root = response->getRoot();
|
||||
|
||||
root["status"] = 0;
|
||||
root["ip_address"] = ETH.localIP().toString();
|
||||
root["mac_address"] = ETH.macAddress();
|
||||
root["station_num"] = WiFi.softAPgetStationNum();
|
||||
root["status"] = (int) getIntercomStatus();
|
||||
root["lastCalledNumber"] = getLastCalledNumber();
|
||||
root["doorStatus"] = false;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
static void intercomSettingsRead(AsyncWebServerRequest* request) {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_INTERCOM_SETTINGS_SIZE);
|
||||
JsonObject root = response->getRoot();
|
||||
|
||||
const size_t CAPACITY = JSON_ARRAY_SIZE(3);
|
||||
StaticJsonDocument<CAPACITY> modelsDoc;
|
||||
|
||||
JsonArray models = modelsDoc.to<JsonArray>();
|
||||
models.add("Vizit");
|
||||
models.add("Cyfral");
|
||||
|
||||
root["kmnModel"] = "Vizit";
|
||||
root["firstAppartment"] = 1;
|
||||
root["kmnModelList"] = models;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
static void intercomJournalRead(AsyncWebServerRequest* request) {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_INTERCOM_JOURNAL_SIZE);
|
||||
JsonObject root = response->getRoot();
|
||||
|
||||
const size_t CAPACITY = JSON_ARRAY_SIZE(5*INTERCOM_JOURNAL_FLATS_NUMBER);
|
||||
StaticJsonDocument<CAPACITY> flatsDoc;
|
||||
|
||||
JsonArray flats = flatsDoc.to<JsonArray>();
|
||||
getIntercomJournalAsJson(flats);
|
||||
|
||||
root["notes"] = flats;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
static void intercomSettingsUpdate(AsyncWebServerRequest *request,
|
||||
uint8_t *data, size_t len, size_t index, size_t total) {
|
||||
DynamicJsonDocument jsonDoc(MAX_INTERCOM_SETTINGS_SIZE);
|
||||
String jsonStr = requestDataToStr(data, len);
|
||||
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
|
||||
|
||||
if (!jsonDoc.is<JsonVariant>()) {
|
||||
request->send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonVariant root = jsonDoc.as<JsonVariant>();
|
||||
|
||||
bool success = writeJsonVariantToFile(INTERCOM_SETTINGS_PATH, root);
|
||||
|
||||
if (success) {
|
||||
String jsonString;
|
||||
serializeJson(root, jsonString);
|
||||
request->send(200, "application/json", jsonString.c_str());
|
||||
}
|
||||
else
|
||||
request->send(500, "text/plain", "Intercom settings not updated");
|
||||
|
||||
configureIntercom(
|
||||
root["kmnModel"].as<String>(),
|
||||
root["firstAppartment"].as<int>()
|
||||
);
|
||||
}
|
||||
|
||||
static void switchDoor(AsyncWebServerRequest *request,
|
||||
uint8_t *data, size_t len, size_t index, size_t total) {
|
||||
DynamicJsonDocument jsonDoc(MAX_INTERCOM_SWITCH_DOOR_SIZE);
|
||||
String jsonStr = requestDataToStr(data, len);
|
||||
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
|
||||
|
||||
if (!jsonDoc.is<JsonVariant>()) {
|
||||
request->send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonVariant root = jsonDoc.as<JsonVariant>();
|
||||
|
||||
SwitchDoorType switchDoorType = static_cast<SwitchDoorType>(root["type"].as<int>());
|
||||
DoorStatus doorStatus = static_cast<DoorStatus>(root["status"].as<int>());
|
||||
|
||||
switchRelay(
|
||||
switchDoorType,
|
||||
doorStatus,
|
||||
root["time"]
|
||||
);
|
||||
|
||||
request->send(200, "application/json");
|
||||
}
|
||||
|
||||
void networkStatus(AsyncWebServerRequest* request) {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_NETWORK_STATUS_SIZE);
|
||||
JsonObject root = response->getRoot();
|
||||
@ -244,8 +334,10 @@ void initRoutes() {
|
||||
server.on("/api/v1/networkSettings", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, networkSettingsUpdate);
|
||||
|
||||
server.on("/api/v1/intercomStatus", intercomStatus);
|
||||
//server.on("/api/v1/intercomSettings", HTTP_GET, intercomSettingsRead);
|
||||
//server.on("/api/v1/intercomSettings", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, intercomSettingsUpdate);
|
||||
server.on("/api/v1/intercomJournal", intercomJournalRead);
|
||||
server.on("/api/v1/intercomSettings", HTTP_GET, intercomSettingsRead);
|
||||
server.on("/api/v1/intercomSettings", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, intercomSettingsUpdate);
|
||||
server.on("/api/v1/switchDoor", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, switchDoor);
|
||||
|
||||
server.on("/api/v1/mqttStatus", mqttStatus);
|
||||
server.on("/api/v1/mqttSettings", HTTP_GET, mqttSettingsRead);
|
||||
|
@ -26,6 +26,8 @@
|
||||
#define FLAT_NUMBER_MQTT_TOPIC "flat_number"
|
||||
#define STATE_MQTT_TOPIC "state"
|
||||
|
||||
#define INTERCOM_JOURNAL_FLATS_NUMBER 100
|
||||
|
||||
#define LED_PIN 32
|
||||
#define DRY_CONT_PIN 15
|
||||
#define DOOR_SENS_PIN 114
|
||||
@ -40,10 +42,14 @@
|
||||
#define MAX_NETWORK_STATUS_SIZE 1024
|
||||
#define MAX_NETWORK_SETTINGS_SIZE 1024
|
||||
#define MAX_INTERCOM_STATUS_SIZE 1024
|
||||
#define MAX_INTERCOM_JOURNAL_SIZE 1024
|
||||
#define MAX_INTERCOM_SETTINGS_SIZE 1024
|
||||
#define MAX_INTERCOM_SWITCH_DOOR_SIZE 1024
|
||||
#define MAX_MQTT_STATUS_SIZE 1024
|
||||
#define MAX_MQTT_SETTINGS_SIZE 1024
|
||||
#define MAX_ESP_STATUS_SIZE 1024
|
||||
|
||||
#define FS_CONFIG_DIRECTORY "/config"
|
||||
#define NETWORK_SETTINGS_PATH "/config/networkSettings.json"
|
||||
#define INTERCOM_SETTINGS_PATH "/config/intercomSettings.json"
|
||||
#define MQTT_SETTINGS_PATH "/config/mqttSettings.json"
|
31
src/domain/intercomJournal.cpp
Normal file
31
src/domain/intercomJournal.cpp
Normal file
@ -0,0 +1,31 @@
|
||||
#include "domain/intercomJournal.h"
|
||||
#include "utils/print.h"
|
||||
|
||||
IntercomJournal _intercomJournal;
|
||||
|
||||
void addFlatToIntercomJournal(unsigned long createdAt, int appartment) {
|
||||
for (int i = INTERCOM_JOURNAL_FLATS_NUMBER - 1; i > 0; --i) {
|
||||
_intercomJournal.notes[i] = _intercomJournal.notes[i - 1];
|
||||
}
|
||||
|
||||
_intercomJournal.notes[0].createdAt = createdAt;
|
||||
_intercomJournal.notes[0].appartment = appartment;
|
||||
}
|
||||
|
||||
JsonArray getIntercomJournalAsJson(JsonArray& flats) {
|
||||
for (int i = 0; i < INTERCOM_JOURNAL_FLATS_NUMBER; ++i) {
|
||||
unsigned long createdAt = _intercomJournal.notes[i].createdAt;
|
||||
int& appartment = _intercomJournal.notes[i].appartment;
|
||||
|
||||
if (createdAt && appartment) {
|
||||
unsigned long timeDifference = timeModule.getTimeDifference(createdAt);
|
||||
String formatedTimeDifference = "\n(" + timeModule.getFormattedTimeAgo(timeDifference) + ")";
|
||||
|
||||
JsonObject flat = flats.createNestedObject();
|
||||
flat["createdAt"] = timeModule.getFormattedTime(createdAt) + formatedTimeDifference;
|
||||
flat["appartment"] = appartment;
|
||||
}
|
||||
}
|
||||
|
||||
return flats;
|
||||
}
|
19
src/domain/intercomJournal.h
Normal file
19
src/domain/intercomJournal.h
Normal file
@ -0,0 +1,19 @@
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "utils/time.h"
|
||||
|
||||
extern Time& timeModule;
|
||||
|
||||
struct Flat {
|
||||
unsigned long createdAt;
|
||||
int appartment;
|
||||
};
|
||||
|
||||
struct IntercomJournal {
|
||||
Flat notes[INTERCOM_JOURNAL_FLATS_NUMBER];
|
||||
};
|
||||
|
||||
void addFlatToIntercomJournal(unsigned long createdAt, int appartment);
|
||||
JsonArray getIntercomJournalAsJson(JsonArray& flats);
|
@ -1,179 +0,0 @@
|
||||
#include "utils/print.h"
|
||||
#include "config/config.h"
|
||||
#include "infra/mqtt.h"
|
||||
#include "infra/led.h"
|
||||
#include "domain/stateMachine.h"
|
||||
|
||||
State currentState = NOT_CONNECTED;
|
||||
int countZeros = 0;
|
||||
int countOnes = 0;
|
||||
|
||||
int previousData = 0;
|
||||
int dataLength = 0;
|
||||
int signalDuration = 0;
|
||||
|
||||
int flat = 0;
|
||||
|
||||
void resetCounters() {
|
||||
countZeros = 0;
|
||||
countOnes = 0;
|
||||
|
||||
previousData = 0;
|
||||
dataLength = 1;
|
||||
signalDuration = 0;
|
||||
}
|
||||
|
||||
void writeState(char* message) {
|
||||
println(message);
|
||||
publishToMQTT(STATE_MQTT_TOPIC, message);
|
||||
}
|
||||
|
||||
void receiveDigit(int data) {
|
||||
if (data != previousData) {
|
||||
if (previousData == HIGH) {
|
||||
//println("AAAA ", dataLength, " ", signalDuration);
|
||||
dataLength++;
|
||||
}
|
||||
signalDuration = 0;
|
||||
} else {
|
||||
signalDuration++;
|
||||
}
|
||||
previousData = data;
|
||||
}
|
||||
|
||||
void changeState(State state, bool resetCountersFlag=true) {
|
||||
currentState = state;
|
||||
|
||||
switch (state) {
|
||||
case NOT_CONNECTED:
|
||||
ledTurnOff();
|
||||
writeState("not connected");
|
||||
break;
|
||||
case CONNECTED:
|
||||
ledTurnOn();
|
||||
writeState("connected");
|
||||
break;
|
||||
case RECEIVING_FIRST_DIGIT:
|
||||
writeState("receiving data");
|
||||
break;
|
||||
}
|
||||
|
||||
if (resetCountersFlag)
|
||||
resetCounters();
|
||||
}
|
||||
|
||||
void firstDigitReceived() {
|
||||
println("| 1 data length: ", dataLength);
|
||||
flat = dataLength*10;
|
||||
}
|
||||
|
||||
void secondDigitReceived() {
|
||||
if (dataLength == 11) {
|
||||
dataLength = 1;
|
||||
}
|
||||
|
||||
flat += dataLength;
|
||||
flat -= 1;
|
||||
|
||||
if (flat > 100 && flat < 110) {
|
||||
flat -= 100;
|
||||
}
|
||||
|
||||
println("| 2 data length: ", dataLength);
|
||||
println("| flat: ", flat);
|
||||
publishToMQTT(FLAT_NUMBER_MQTT_TOPIC, flat);
|
||||
}
|
||||
|
||||
void updateStateMachine(int data) {
|
||||
switch (currentState) {
|
||||
case NOT_CONNECTED:
|
||||
if (data == LOW) {}
|
||||
else if (data == HIGH) {
|
||||
changeState(CONNECTED);
|
||||
}
|
||||
break;
|
||||
|
||||
case CONNECTED:
|
||||
if (data == LOW) {
|
||||
countZeros++;
|
||||
if (countZeros >= NOT_CONNECTED_THRESHOLD) {
|
||||
changeState(NOT_CONNECTED);
|
||||
}
|
||||
} else if (data == HIGH) {
|
||||
if (countZeros >= INITIALIZING_CALL_THRESHOLD) {
|
||||
changeState(RECEIVING_FIRST_DIGIT);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case RECEIVING_FIRST_DIGIT:
|
||||
receiveDigit(data);
|
||||
|
||||
if (data == LOW) {
|
||||
countOnes = 0;
|
||||
countZeros++;
|
||||
} else if (data == HIGH) {
|
||||
countZeros = 0;
|
||||
countOnes++;
|
||||
if (countOnes >= DATA_RECEIVED_THESHOLD) {
|
||||
firstDigitReceived();
|
||||
changeState(RECEIVING_SECOND_DIGIT);
|
||||
}
|
||||
if (countOnes >= CONNECTED_THRESHOLD) {
|
||||
changeState(CONNECTED);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case RECEIVING_SECOND_DIGIT:
|
||||
receiveDigit(data);
|
||||
|
||||
if (data == LOW) {
|
||||
countOnes = 0;
|
||||
countZeros++;
|
||||
} else if (data == HIGH) {
|
||||
countZeros = 0;
|
||||
countOnes++;
|
||||
if (countOnes >= DATA_RECEIVED_THESHOLD) {
|
||||
secondDigitReceived();
|
||||
changeState(DATA_RECEIVED);
|
||||
}
|
||||
if (countOnes >= CONNECTED_THRESHOLD) {
|
||||
changeState(CONNECTED);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DATA_RECEIVED:
|
||||
if (data == LOW) {
|
||||
countZeros++;
|
||||
if (countZeros >= CALL_ENDED_THRESHOLD) {
|
||||
changeState(CALL_ENDED);
|
||||
}
|
||||
} else if (data == HIGH) {
|
||||
countOnes++;
|
||||
if (countOnes >= CONNECTED_THRESHOLD) {
|
||||
changeState(CONNECTED);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CALL_ENDED:
|
||||
if (data == LOW) {
|
||||
countZeros++;
|
||||
if (countZeros >= NOT_CONNECTED_THRESHOLD) {
|
||||
changeState(NOT_CONNECTED);
|
||||
}
|
||||
} else if (data == HIGH) {
|
||||
countOnes++;
|
||||
if (countOnes >= CONNECTED_THRESHOLD) {
|
||||
changeState(CONNECTED);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void initStateMachine() {
|
||||
changeState(CONNECTED);
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
#ifndef STATE_MACHINE_H
|
||||
#define STATE_MACHINE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#define CONNECTED_THRESHOLD 50000
|
||||
#define NOT_CONNECTED_THRESHOLD 50000
|
||||
#define INITIALIZING_CALL_THRESHOLD 45
|
||||
#define DATA_RECEIVED_THESHOLD 5000
|
||||
#define CALL_ENDED_THRESHOLD 10000
|
||||
|
||||
enum State {
|
||||
NOT_CONNECTED,
|
||||
CONNECTED,
|
||||
RECEIVING_FIRST_DIGIT,
|
||||
RECEIVING_SECOND_DIGIT,
|
||||
DATA_RECEIVED,
|
||||
CALL_ENDED
|
||||
};
|
||||
|
||||
void resetCounters();
|
||||
void updateStateMachine(int data);
|
||||
void initStateMachine();
|
||||
|
||||
#endif // STATE_MACHINE_H
|
@ -1,148 +0,0 @@
|
||||
#include "utils/print.h"
|
||||
#include "config/config.h"
|
||||
#include "infra/mqtt.h"
|
||||
#include "infra/led.h"
|
||||
#include "domain/stateMachine.h"
|
||||
|
||||
State currentState = NOT_CONNECTED;
|
||||
int countZeros = 0;
|
||||
int countOnes = 0;
|
||||
|
||||
int previousData = 0;
|
||||
int dataLength = 0;
|
||||
int signalDuration = 0;
|
||||
|
||||
int flat = 0;
|
||||
|
||||
void resetCounters() {
|
||||
countZeros = 0;
|
||||
countOnes = 0;
|
||||
|
||||
previousData = 0;
|
||||
dataLength = 0;
|
||||
signalDuration = 0;
|
||||
}
|
||||
|
||||
void writeState(char* message) {
|
||||
println(message);
|
||||
publishToMQTT(STATE_MQTT_TOPIC, message);
|
||||
}
|
||||
|
||||
void receiveData(int data) {
|
||||
if (data != previousData) {
|
||||
if (previousData == HIGH) {
|
||||
dataLength++;
|
||||
}
|
||||
signalDuration = 0;
|
||||
} else {
|
||||
signalDuration++;
|
||||
}
|
||||
previousData = data;
|
||||
}
|
||||
|
||||
void changeState(State state, bool resetCountersFlag=true) {
|
||||
currentState = state;
|
||||
|
||||
switch (state) {
|
||||
case NOT_CONNECTED:
|
||||
ledTurnOff();
|
||||
writeState("not connected");
|
||||
break;
|
||||
case CONNECTED:
|
||||
ledTurnOn();
|
||||
writeState("connected");
|
||||
break;
|
||||
case RECEIVING_DATA:
|
||||
writeState("receiving data");
|
||||
break;
|
||||
}
|
||||
|
||||
if (resetCountersFlag)
|
||||
resetCounters();
|
||||
}
|
||||
|
||||
void flatReceived() {
|
||||
int flat = dataLength/2;
|
||||
|
||||
if (flat < 1)
|
||||
return;
|
||||
|
||||
println("| data length: ", dataLength);
|
||||
println("| flat: ", flat);
|
||||
publishToMQTT(FLAT_NUMBER_MQTT_TOPIC, flat);
|
||||
}
|
||||
|
||||
void updateStateMachine(int data) {
|
||||
switch (currentState) {
|
||||
case NOT_CONNECTED:
|
||||
if (data == LOW) {}
|
||||
else if (data == HIGH) {
|
||||
changeState(CONNECTED);
|
||||
}
|
||||
break;
|
||||
|
||||
case CONNECTED:
|
||||
if (data == LOW) {
|
||||
countZeros++;
|
||||
if (countZeros >= NOT_CONNECTED_THRESHOLD) {
|
||||
changeState(NOT_CONNECTED);
|
||||
}
|
||||
} else if (data == HIGH) {
|
||||
if (countZeros >= INITIALIZING_CALL_THRESHOLD) {
|
||||
changeState(RECEIVING_DATA);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case RECEIVING_DATA:
|
||||
receiveData(data);
|
||||
|
||||
if (data == LOW) {
|
||||
countOnes = 0;
|
||||
countZeros++;
|
||||
if (countZeros >= DATA_RECEIVED_THESHOLD) {
|
||||
flatReceived();
|
||||
changeState(DATA_RECEIVED);
|
||||
}
|
||||
} else if (data == HIGH) {
|
||||
countZeros = 0;
|
||||
countOnes++;
|
||||
if (countOnes >= CONNECTED_THRESHOLD) {
|
||||
changeState(CONNECTED);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DATA_RECEIVED:
|
||||
if (data == LOW) {
|
||||
countZeros++;
|
||||
if (countZeros >= CALL_ENDED_THRESHOLD) {
|
||||
changeState(CALL_ENDED);
|
||||
}
|
||||
} else if (data == HIGH) {
|
||||
countOnes++;
|
||||
if (countOnes >= CONNECTED_THRESHOLD) {
|
||||
changeState(CONNECTED);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CALL_ENDED:
|
||||
if (data == LOW) {
|
||||
countZeros++;
|
||||
if (countZeros >= NOT_CONNECTED_THRESHOLD) {
|
||||
changeState(NOT_CONNECTED);
|
||||
}
|
||||
} else if (data == HIGH) {
|
||||
countOnes++;
|
||||
if (countOnes >= CONNECTED_THRESHOLD) {
|
||||
changeState(CONNECTED);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void initStateMachine() {
|
||||
changeState(CONNECTED);
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
#ifndef STATE_MACHINE_H
|
||||
#define STATE_MACHINE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#define CONNECTED_THRESHOLD 50000
|
||||
#define NOT_CONNECTED_THRESHOLD 50000
|
||||
#define INITIALIZING_CALL_THRESHOLD 15000
|
||||
#define DATA_RECEIVED_THESHOLD 30000
|
||||
#define CALL_ENDED_THRESHOLD 10000
|
||||
|
||||
enum State {
|
||||
NOT_CONNECTED,
|
||||
CONNECTED,
|
||||
RECEIVING_DATA,
|
||||
DATA_RECEIVED,
|
||||
CALL_ENDED
|
||||
};
|
||||
|
||||
void resetCounters();
|
||||
void updateStateMachine(int data);
|
||||
void initStateMachine();
|
||||
|
||||
#endif // STATE_MACHINE_H
|
@ -1,5 +1,8 @@
|
||||
#include "domain/stateMachineController.h"
|
||||
|
||||
StateMachineController StateMachineController::instance;
|
||||
StateMachineController& stateMachineController = StateMachineController::getInstance();
|
||||
|
||||
StateMachineController::StateMachineController() : _strategy(nullptr) {}
|
||||
|
||||
void StateMachineController::setStrategy(StateMachineStrategy* newStrategy) {
|
||||
@ -20,8 +23,23 @@ void StateMachineController::updateStateMachine(int data) {
|
||||
}
|
||||
|
||||
StateMachineController::~StateMachineController() {
|
||||
// Cleanup the strategy in the destructor
|
||||
if (_strategy) {
|
||||
delete _strategy;
|
||||
}
|
||||
}
|
||||
|
||||
StateMachineController& StateMachineController::getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
IntercomConnectionStatus StateMachineController::getStatus() {
|
||||
return _strategy->getStatus();
|
||||
}
|
||||
|
||||
int StateMachineController::getLastCalledNumber() {
|
||||
return _strategy->getFlat();
|
||||
}
|
||||
|
||||
DoorStatus getDoorStatus() {
|
||||
return DoorStatus::CLOSED;
|
||||
}
|
@ -1,14 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "infra/relay.h"
|
||||
#include "domain/stateMachineStrategy.h"
|
||||
|
||||
class StateMachineController {
|
||||
private:
|
||||
static StateMachineController instance;
|
||||
StateMachineStrategy* _strategy;
|
||||
|
||||
public:
|
||||
StateMachineController();
|
||||
StateMachineController(const StateMachineController&) = delete;
|
||||
StateMachineController& operator=(const StateMachineController&) = delete;
|
||||
~StateMachineController();
|
||||
|
||||
public:
|
||||
void setStrategy(StateMachineStrategy* newStrategy);
|
||||
void updateStateMachine(int data);
|
||||
~StateMachineController();
|
||||
static StateMachineController& getInstance();
|
||||
|
||||
IntercomConnectionStatus getStatus();
|
||||
int getLastCalledNumber();
|
||||
DoorStatus getDoorStatus();
|
||||
};
|
@ -1,8 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
enum class IntercomConnectionStatus { CONNECTED, NOT_CONNECTED, RECEIVING_DATA };
|
||||
|
||||
class StateMachineStrategy {
|
||||
protected:
|
||||
int _flat = 0;
|
||||
|
||||
public:
|
||||
IntercomConnectionStatus _status = IntercomConnectionStatus::NOT_CONNECTED;
|
||||
|
||||
virtual void setup() = 0;
|
||||
virtual void updateStateMachine(int data) = 0;
|
||||
virtual ~StateMachineStrategy() {}
|
||||
|
||||
int getFlat() {
|
||||
return _flat;
|
||||
}
|
||||
|
||||
IntercomConnectionStatus getStatus() {
|
||||
return _status;
|
||||
}
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
#include "domain/strategies/cyfralStrategy.h"
|
||||
#include "domain/intercomJournal.h"
|
||||
|
||||
CyfralStrategy::CyfralStrategy() {}
|
||||
|
||||
@ -111,13 +112,16 @@ void CyfralStrategy::_changeState(State state, bool resetCountersFlag) {
|
||||
case NOT_CONNECTED:
|
||||
ledTurnOff();
|
||||
_writeState("not connected");
|
||||
_status = IntercomConnectionStatus::NOT_CONNECTED;
|
||||
break;
|
||||
case CONNECTED:
|
||||
ledTurnOn();
|
||||
_writeState("connected");
|
||||
_status = IntercomConnectionStatus::CONNECTED;
|
||||
break;
|
||||
case RECEIVING_DATA:
|
||||
_writeState("receiving data");
|
||||
_status = IntercomConnectionStatus::RECEIVING_DATA;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -126,14 +130,17 @@ void CyfralStrategy::_changeState(State state, bool resetCountersFlag) {
|
||||
}
|
||||
|
||||
void CyfralStrategy::_flatReceived() {
|
||||
int flat = _dataLength/2;
|
||||
_flat = _dataLength/2;
|
||||
|
||||
if (flat < 1)
|
||||
if (_flat < 1)
|
||||
return;
|
||||
|
||||
println("| data length: ", _dataLength);
|
||||
println("| flat: ", flat);
|
||||
publishToMQTT(FLAT_NUMBER_MQTT_TOPIC, flat);
|
||||
println("| flat: ", _flat);
|
||||
publishToMQTT(FLAT_NUMBER_MQTT_TOPIC, _flat);
|
||||
|
||||
unsigned long createdAt = timeModule.getCurrentTime();
|
||||
addFlatToIntercomJournal(createdAt, _flat);
|
||||
}
|
||||
|
||||
void CyfralStrategy::_initStateMachine() {
|
||||
|
@ -1,9 +1,12 @@
|
||||
#include "utils/print.h"
|
||||
#include "utils/time.h"
|
||||
#include "config/config.h"
|
||||
#include "infra/mqtt.h"
|
||||
#include "infra/led.h"
|
||||
#include "domain/stateMachineStrategy.h"
|
||||
|
||||
extern Time& timeModule;
|
||||
|
||||
class CyfralStrategy : public StateMachineStrategy {
|
||||
private:
|
||||
enum State {
|
||||
|
@ -1,4 +1,5 @@
|
||||
#include "domain/strategies/vizitStrategy.h"
|
||||
#include "domain/intercomJournal.h"
|
||||
|
||||
VizitStrategy::VizitStrategy() {}
|
||||
|
||||
@ -130,13 +131,16 @@ void VizitStrategy::_changeState(State state, bool resetCountersFlag) {
|
||||
case NOT_CONNECTED:
|
||||
ledTurnOff();
|
||||
_writeState("not connected");
|
||||
_status = IntercomConnectionStatus::NOT_CONNECTED;
|
||||
break;
|
||||
case CONNECTED:
|
||||
ledTurnOn();
|
||||
_writeState("connected");
|
||||
_status = IntercomConnectionStatus::CONNECTED;
|
||||
break;
|
||||
case RECEIVING_FIRST_DIGIT:
|
||||
_writeState("receiving data");
|
||||
_status = IntercomConnectionStatus::RECEIVING_DATA;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -164,6 +168,9 @@ void VizitStrategy::_secondDigitReceived() {
|
||||
println("| 2 data length: ", _dataLength);
|
||||
println("| flat: ", _flat);
|
||||
publishToMQTT(FLAT_NUMBER_MQTT_TOPIC, _flat);
|
||||
|
||||
unsigned long createdAt = timeModule.getCurrentTime();
|
||||
addFlatToIntercomJournal(createdAt, _flat);
|
||||
}
|
||||
|
||||
void VizitStrategy::_initStateMachine() {
|
||||
|
@ -1,9 +1,12 @@
|
||||
#include "utils/print.h"
|
||||
#include "utils/time.h"
|
||||
#include "config/config.h"
|
||||
#include "infra/mqtt.h"
|
||||
#include "infra/led.h"
|
||||
#include "domain/stateMachineStrategy.h"
|
||||
|
||||
extern Time& timeModule;
|
||||
|
||||
class VizitStrategy : public StateMachineStrategy {
|
||||
private:
|
||||
enum State {
|
||||
@ -29,8 +32,6 @@ private:
|
||||
int _dataLength = 0;
|
||||
int _signalDuration = 0;
|
||||
|
||||
int _flat = 0;
|
||||
|
||||
void _resetCounters();
|
||||
void _writeState(char* message);
|
||||
void _receiveDigit(int data);
|
||||
|
@ -0,0 +1,15 @@
|
||||
#include "infra/intercom.h"
|
||||
|
||||
extern StateMachineController& stateMachineController;
|
||||
|
||||
void configureIntercom(String kmnModel, int firstAppartment) {
|
||||
|
||||
}
|
||||
|
||||
IntercomConnectionStatus getIntercomStatus() {
|
||||
return stateMachineController.getStatus();
|
||||
}
|
||||
|
||||
int getLastCalledNumber() {
|
||||
return stateMachineController.getLastCalledNumber();
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include "domain/stateMachineController.h"
|
||||
|
||||
void configureIntercom(String kmnModel, int firstAppartment);
|
||||
IntercomConnectionStatus getIntercomStatus();
|
||||
int getLastCalledNumber();
|
@ -1,4 +1,5 @@
|
||||
#include "infra/relay.h"
|
||||
#include "utils/utils.h"
|
||||
|
||||
void relayTurnOn() {
|
||||
digitalWrite(DRY_CONT_PIN, LOW);
|
||||
@ -7,3 +8,32 @@ void relayTurnOn() {
|
||||
void relayTurnOff() {
|
||||
digitalWrite(DRY_CONT_PIN, HIGH);
|
||||
}
|
||||
|
||||
void switchRelay(SwitchDoorType type, DoorStatus status, int time) {
|
||||
switch (type) {
|
||||
case SwitchDoorType::ON_OFF:
|
||||
if (status == DoorStatus::OPENED) {
|
||||
relayTurnOn();
|
||||
} else {
|
||||
relayTurnOff();
|
||||
}
|
||||
break;
|
||||
|
||||
case SwitchDoorType::JOGGING:
|
||||
if (status == DoorStatus::OPENED) {
|
||||
relayTurnOn();
|
||||
delay(secondsToMilliseconds(time));
|
||||
relayTurnOff();
|
||||
} else {
|
||||
relayTurnOff();
|
||||
delay(secondsToMilliseconds(time));
|
||||
relayTurnOn();
|
||||
}
|
||||
break;
|
||||
|
||||
case SwitchDoorType::DELAY:
|
||||
delay(secondsToMilliseconds(time));
|
||||
relayTurnOn();
|
||||
break;
|
||||
}
|
||||
}
|
@ -2,5 +2,9 @@
|
||||
#include <Arduino.h>
|
||||
#include "config/config.h"
|
||||
|
||||
enum class SwitchDoorType { ON_OFF, JOGGING, DELAY };
|
||||
enum class DoorStatus { OPENED, CLOSED };
|
||||
|
||||
void relayTurnOn();
|
||||
void relayTurnOff();
|
||||
void switchRelay(SwitchDoorType type, DoorStatus status, int time);
|
@ -30,7 +30,7 @@ bool flag = false;
|
||||
int zeros = 0;
|
||||
int ones = 0;
|
||||
|
||||
StateMachineController controller;
|
||||
extern StateMachineController& stateMachineController;
|
||||
CyfralStrategy strategy;
|
||||
|
||||
void IRAM_ATTR one() {
|
||||
@ -72,13 +72,13 @@ void setup() {
|
||||
initRoutes();
|
||||
initHttpServer();
|
||||
|
||||
controller.setStrategy(&strategy);
|
||||
stateMachineController.setStrategy(&strategy);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (lastMicros < micros()) {
|
||||
data = digitalRead(DATA_PIN);
|
||||
controller.updateStateMachine(data);
|
||||
stateMachineController.updateStateMachine(data);
|
||||
|
||||
if (PRINT_RAW_SIGNAL_FLAG)
|
||||
printf("{}", data);
|
||||
|
54
src/utils/time.cpp
Normal file
54
src/utils/time.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
#include <TimeLib.h>
|
||||
#include "utils/time.h"
|
||||
|
||||
Time& timeModule = Time::getInstance();
|
||||
|
||||
Time::Time() {
|
||||
initialTime = 0;
|
||||
}
|
||||
|
||||
Time& Time::getInstance() {
|
||||
static Time instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void Time::setCurrentTime(unsigned long currentTimeMillis) {
|
||||
initialTime = currentTimeMillis;
|
||||
}
|
||||
|
||||
unsigned long Time::getCurrentTime() const {
|
||||
return millis() + initialTime;
|
||||
}
|
||||
|
||||
unsigned long Time::getTimeDifference(unsigned long otherTimeMillis) const {
|
||||
return millis() - otherTimeMillis;
|
||||
}
|
||||
|
||||
String Time::getFormattedTime(unsigned long timeMillis) const {
|
||||
time_t currentTime = timeMillis / 1000;
|
||||
|
||||
struct tm *timeStruct = gmtime(¤tTime);
|
||||
|
||||
char buffer[20];
|
||||
snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
|
||||
timeStruct->tm_year + 1900, timeStruct->tm_mon + 1, timeStruct->tm_mday,
|
||||
timeStruct->tm_hour, timeStruct->tm_min, timeStruct->tm_sec);
|
||||
|
||||
return String(buffer);
|
||||
}
|
||||
|
||||
String Time::getFormattedTimeAgo(unsigned long timeMillis) const {
|
||||
time_t currentTime = timeMillis / 1000;
|
||||
|
||||
struct tm *timeStruct = gmtime(¤tTime);
|
||||
|
||||
if (timeStruct->tm_yday >= 0) {
|
||||
return String(timeStruct->tm_yday) + " days ago";
|
||||
} else if (timeStruct->tm_hour > 0) {
|
||||
return String(timeStruct->tm_hour) + " hours ago";
|
||||
} else if (timeStruct->tm_min > 0) {
|
||||
return String(timeStruct->tm_min) + " minutes ago";
|
||||
} else {
|
||||
return String(timeStruct->tm_sec) + " seconds ago";
|
||||
}
|
||||
}
|
17
src/utils/time.h
Normal file
17
src/utils/time.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
class Time {
|
||||
public:
|
||||
static Time& getInstance();
|
||||
void setCurrentTime(unsigned long currentTimeMillis);
|
||||
unsigned long getCurrentTime() const;
|
||||
unsigned long getTimeDifference(unsigned long otherTimeMillis) const;
|
||||
String getFormattedTime(unsigned long timeMillis) const;
|
||||
String getFormattedTimeAgo(unsigned long timeMillis) const;
|
||||
|
||||
private:
|
||||
Time();
|
||||
unsigned long initialTime;
|
||||
};
|
@ -9,3 +9,7 @@ String requestDataToStr(uint8_t *data, size_t len) {
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
unsigned long secondsToMilliseconds(int seconds) {
|
||||
return static_cast<unsigned long>(seconds) * 1000;
|
||||
}
|
@ -3,3 +3,4 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
String requestDataToStr(uint8_t *data, size_t len);
|
||||
unsigned long secondsToMilliseconds(int seconds);
|
Loading…
x
Reference in New Issue
Block a user