refactored restart and format services to be non-blocking

This commit is contained in:
proddy
2024-08-31 16:12:30 +02:00
parent 382c46622d
commit 931827c526
19 changed files with 243 additions and 202 deletions

View File

@@ -10,12 +10,6 @@ export const readHardwareStatus = () =>
export const readSystemStatus = () => export const readSystemStatus = () =>
alovaInstance.Get<SystemStatus>('/rest/systemStatus'); alovaInstance.Get<SystemStatus>('/rest/systemStatus');
// commands
export const restart = () => alovaInstance.Post('/rest/restart');
export const partition = () => alovaInstance.Post('/rest/partition');
export const factoryPartition = () => alovaInstance.Post('/rest/factoryPartition');
export const factoryReset = () => alovaInstance.Post('/rest/factoryReset');
// SystemLog // SystemLog
export const readLogSettings = () => export const readLogSettings = () =>
alovaInstance.Get<LogSettings>(`/rest/logSettings`); alovaInstance.Get<LogSettings>(`/rest/logSettings`);

View File

@@ -26,8 +26,6 @@ import {
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2'; import Grid from '@mui/material/Grid2';
import { restart } from 'api/system';
import { import {
Body, Body,
Cell, Cell,
@@ -51,6 +49,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { import {
API,
readDeviceEntities, readDeviceEntities,
readDevices, readDevices,
resetCustomizations, resetCustomizations,
@@ -61,7 +60,7 @@ import SettingsCustomizationsDialog from './CustomizationsDialog';
import EntityMaskToggle from './EntityMaskToggle'; import EntityMaskToggle from './EntityMaskToggle';
import OptionIcon from './OptionIcon'; import OptionIcon from './OptionIcon';
import { DeviceEntityMask } from './types'; import { DeviceEntityMask } from './types';
import type { DeviceEntity, DeviceShort } from './types'; import type { APIcall, DeviceEntity, DeviceShort } from './types';
export const APIURL = window.location.origin + '/api/'; export const APIURL = window.location.origin + '/api/';
@@ -85,6 +84,10 @@ const Customizations = () => {
// fetch devices first // fetch devices first
const { data: devices, send: fetchDevices } = useRequest(readDevices); const { data: devices, send: fetchDevices } = useRequest(readDevices);
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const [selectedDevice, setSelectedDevice] = useState<number>( const [selectedDevice, setSelectedDevice] = useState<number>(
Number(useLocation().state) || -1 Number(useLocation().state) || -1
); );
@@ -132,9 +135,14 @@ const Customizations = () => {
); );
}; };
const { send: sendRestart } = useRequest(restart(), { const doRestart = async () => {
immediate: false setRestarting(true);
}); await sendAPI({ device: 'system', cmd: 'restart', id: -1 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
};
const entities_theme = useTheme({ const entities_theme = useTheme({
Table: ` Table: `
@@ -247,13 +255,6 @@ const Customizations = () => {
} }
}, [devices, selectedDevice]); }, [devices, selectedDevice]);
const doRestart = async () => {
await sendRestart().catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
};
function formatValue(value: unknown) { function formatValue(value: unknown) {
if (typeof value === 'number') { if (typeof value === 'number') {
return new Intl.NumberFormat().format(value); return new Intl.NumberFormat().format(value);
@@ -509,7 +510,7 @@ const Customizations = () => {
container container
mb={1} mb={1}
mt={0} mt={0}
spacing={1} spacing={2}
direction="row" direction="row"
justifyContent="flex-start" justifyContent="flex-start"
alignItems="center" alignItems="center"

View File

@@ -28,7 +28,7 @@ const Help = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.HELP()); useLayoutTitle(LL.HELP());
const { send: getAPI } = useRequest((data: APIcall) => API(data), { const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false immediate: false
}).onSuccess((event) => { }).onSuccess((event) => {
const anchor = document.createElement('a'); const anchor = document.createElement('a');
@@ -45,8 +45,8 @@ const Help = () => {
toast.info(LL.DOWNLOAD_SUCCESSFUL()); toast.info(LL.DOWNLOAD_SUCCESSFUL());
}); });
const callAPI = async (device: string, entity: string) => { const callAPI = async (device: string, cmd: string) => {
await getAPI({ device, entity, id: 0 }).catch((error: Error) => { await sendAPI({ device, cmd, id: 0 }).catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
}; };
@@ -113,7 +113,7 @@ const Help = () => {
color="primary" color="primary"
onClick={() => callAPI('system', 'allvalues')} onClick={() => callAPI('system', 'allvalues')}
> >
{LL.ALLVALUES(0)} {LL.ALLVALUES()}
</Button> </Button>
<Box border={1} p={1} mt={4}> <Box border={1} p={1} mt={4}>

View File

@@ -272,8 +272,8 @@ export interface BoardProfile {
export interface APIcall { export interface APIcall {
device: string; device: string;
entity: string; cmd: string;
id: unknown; id: number;
} }
export interface WriteAnalogSensor { export interface WriteAnalogSensor {
id: number; id: number;

View File

@@ -16,7 +16,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2'; import Grid from '@mui/material/Grid2';
import { readHardwareStatus, restart } from 'api/system'; import { readHardwareStatus } from 'api/system';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
import RestartMonitor from 'app/status/RestartMonitor'; import RestartMonitor from 'app/status/RestartMonitor';
@@ -35,9 +35,9 @@ import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValueDirty, useRest } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { getBoardProfile, readSettings, writeSettings } from '../../api/app'; import { API, getBoardProfile, readSettings, writeSettings } from '../../api/app';
import { BOARD_PROFILES } from '../main/types'; import { BOARD_PROFILES } from '../main/types';
import type { Settings } from '../main/types'; import type { APIcall, Settings } from '../main/types';
import { createSettingsValidator } from '../main/validators'; import { createSettingsValidator } from '../main/validators';
export function boardProfileSelectItems() { export function boardProfileSelectItems() {
@@ -80,6 +80,10 @@ const ApplicationSettings = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const { loading: processingBoard, send: readBoardProfile } = useRequest( const { loading: processingBoard, send: readBoardProfile } = useRequest(
(boardProfile: string) => getBoardProfile(boardProfile), (boardProfile: string) => getBoardProfile(boardProfile),
{ {
@@ -102,9 +106,14 @@ const ApplicationSettings = () => {
}); });
}); });
const { send: restartCommand } = useRequest(restart(), { const doRestart = async () => {
immediate: false setRestarting(true);
}); await sendAPI({ device: 'system', cmd: 'restart', id: -1 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
};
const updateBoardProfile = async (board_profile: string) => { const updateBoardProfile = async (board_profile: string) => {
await readBoardProfile(board_profile).catch((error: Error) => { await readBoardProfile(board_profile).catch((error: Error) => {
@@ -158,10 +167,7 @@ const ApplicationSettings = () => {
const restart = async () => { const restart = async () => {
await validateAndSubmit(); await validateAndSubmit();
await restartCommand().catch((error: Error) => { await doRestart();
toast.error(error.message);
});
setRestarting(true);
}; };
return ( return (
@@ -204,7 +210,7 @@ const ApplicationSettings = () => {
label={LL.ENABLE_MODBUS()} label={LL.ENABLE_MODBUS()}
/> />
{data.modbus_enabled && ( {data.modbus_enabled && (
<Grid container spacing={1} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors}
@@ -258,7 +264,7 @@ const ApplicationSettings = () => {
label={LL.ENABLE_SYSLOG()} label={LL.ENABLE_SYSLOG()}
/> />
{data.syslog_enabled && ( {data.syslog_enabled && (
<Grid container spacing={1} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors}
@@ -351,7 +357,7 @@ const ApplicationSettings = () => {
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary"> <Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.FORMATTING_OPTIONS()} {LL.FORMATTING_OPTIONS()}
</Typography> </Typography>
<Grid container spacing={1}> <Grid container spacing={2}>
<Grid size={3}> <Grid size={3}>
<TextField <TextField
name="locale" name="locale"
@@ -469,7 +475,7 @@ const ApplicationSettings = () => {
</TextField> </TextField>
{data.board_profile === 'CUSTOM' && ( {data.board_profile === 'CUSTOM' && (
<> <>
<Grid container spacing={1} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors}
@@ -555,7 +561,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
</Grid> </Grid>
{data.phy_type !== 0 && ( {data.phy_type !== 0 && (
<Grid container spacing={1} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<TextField <TextField
name="eth_power" name="eth_power"
@@ -601,7 +607,7 @@ const ApplicationSettings = () => {
)} )}
</> </>
)} )}
<Grid container spacing={1} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<TextField <TextField
name="tx_mode" name="tx_mode"
@@ -717,7 +723,7 @@ const ApplicationSettings = () => {
/> />
</Box> </Box>
)} )}
<Grid container spacing={1} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<BlockFormControlLabel <BlockFormControlLabel
control={ control={
<Checkbox <Checkbox
@@ -740,7 +746,7 @@ const ApplicationSettings = () => {
disabled={!data.shower_timer} disabled={!data.shower_timer}
/> />
</Grid> </Grid>
<Grid container spacing={1} rowSpacing={2} sx={{ pt: 2 }}> <Grid container spacing={2} sx={{ pt: 2 }}>
{data.shower_timer && ( {data.shower_timer && (
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField

View File

@@ -28,7 +28,6 @@ import {
checkUpgrade, checkUpgrade,
getDevVersion, getDevVersion,
getStableVersion, getStableVersion,
restart,
uploadURL uploadURL
} from 'api/system'; } from 'api/system';
@@ -76,7 +75,11 @@ const DownloadUpload = () => {
saveFile(event.data, 'schedule.json'); saveFile(event.data, 'schedule.json');
}); });
const { send: getAPI } = useRequest((data: APIcall) => API(data), { const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const { send: sendAPIandSave } = useRequest((data: APIcall) => API(data), {
immediate: false immediate: false
}).onSuccess((event) => { }).onSuccess((event) => {
saveFile( saveFile(
@@ -98,15 +101,13 @@ const DownloadUpload = () => {
} }
); );
const { send: restartCommand } = useRequest(restart(), { const doRestart = async () => {
immediate: false
});
const callRestart = async () => {
setRestarting(true); setRestarting(true);
await restartCommand().catch((error: Error) => { await sendAPI({ device: 'system', cmd: 'restart', id: -1 }).catch(
(error: Error) => {
toast.error(error.message); toast.error(error.message);
}); }
);
}; };
const { send: sendCheckUpgrade } = useRequest(checkUpgrade, { const { send: sendCheckUpgrade } = useRequest(checkUpgrade, {
@@ -200,8 +201,8 @@ const DownloadUpload = () => {
}); });
}; };
const callAPI = async (device: string, entity: string) => { const callAPIandSave = async (device: string, cmd: string) => {
await getAPI({ device, entity, id: 0 }).catch((error: Error) => { await sendAPIandSave({ device, cmd, id: 0 }).catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
}; };
@@ -291,7 +292,7 @@ const DownloadUpload = () => {
startIcon={<DownloadIcon />} startIcon={<DownloadIcon />}
variant="outlined" variant="outlined"
color="primary" color="primary"
onClick={() => callAPI('system', 'info')} onClick={() => callAPIandSave('system', 'info')}
> >
{LL.SUPPORT_INFORMATION(0)} {LL.SUPPORT_INFORMATION(0)}
</Button> </Button>
@@ -300,7 +301,7 @@ const DownloadUpload = () => {
startIcon={<DownloadIcon />} startIcon={<DownloadIcon />}
variant="outlined" variant="outlined"
color="primary" color="primary"
onClick={() => callAPI('system', 'allvalues')} onClick={() => callAPIandSave('system', 'allvalues')}
> >
{LL.ALLVALUES()} {LL.ALLVALUES()}
</Button> </Button>
@@ -420,15 +421,13 @@ const DownloadUpload = () => {
<Typography variant="body2">{LL.UPLOAD_TEXT()}</Typography> <Typography variant="body2">{LL.UPLOAD_TEXT()}</Typography>
</Box> </Box>
<SingleUpload callRestart={callRestart} /> <SingleUpload doRestart={doRestart} />
</> </>
); );
}; };
return ( return (
<SectionContent> <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
{restarting ? <RestartMonitor message={LL.WAIT_FIRMWARE()} /> : content()}
</SectionContent>
); );
}; };

View File

@@ -21,9 +21,11 @@ import {
} from '@mui/material'; } from '@mui/material';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { API } from 'api/app';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types';
import { SectionContent, useLayoutTitle } from 'components'; import { SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem'; import ListMenuItem from 'components/layout/ListMenuItem';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -34,13 +36,14 @@ const Settings = () => {
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false); const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), { const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false immediate: false
}); });
const factoryReset = async () => { const doFormat = async () => {
await factoryResetCommand(); await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
setConfirmFactoryReset(false); setConfirmFactoryReset(false);
});
}; };
const renderFactoryResetDialog = () => ( const renderFactoryResetDialog = () => (
@@ -63,7 +66,7 @@ const Settings = () => {
<Button <Button
startIcon={<SettingsBackupRestoreIcon />} startIcon={<SettingsBackupRestoreIcon />}
variant="outlined" variant="outlined"
onClick={factoryReset} onClick={doFormat}
color="error" color="error"
> >
{LL.FACTORY_RESET()} {LL.FACTORY_RESET()}

View File

@@ -22,9 +22,10 @@ import {
} from '@mui/material'; } from '@mui/material';
import * as NetworkApi from 'api/network'; import * as NetworkApi from 'api/network';
import * as SystemApi from 'api/system'; import { API } from 'api/app';
import { updateState, useRequest } from 'alova/client'; import { updateState, useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import { import {
BlockFormControlLabel, BlockFormControlLabel,
@@ -71,7 +72,7 @@ const NetworkSettings = () => {
update: NetworkApi.updateNetworkSettings update: NetworkApi.updateNetworkSettings
}); });
const { send: restartCommand } = useRequest(SystemApi.restart(), { const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false immediate: false
}); });
@@ -131,11 +132,13 @@ const NetworkSettings = () => {
await loadData(); await loadData();
}; };
const restart = async () => { const doRestart = async () => {
await restartCommand().catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true); setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: -1 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
}; };
return ( return (
@@ -358,7 +361,7 @@ const NetworkSettings = () => {
startIcon={<PowerSettingsNewIcon />} startIcon={<PowerSettingsNewIcon />}
variant="contained" variant="contained"
color="error" color="error"
onClick={restart} onClick={doRestart}
> >
{LL.RESTART()} {LL.RESTART()}
</Button> </Button>

View File

@@ -1,52 +1,80 @@
import { type FC, useEffect, useRef, useState } from 'react'; import { useState } from 'react';
import {
Box,
CircularProgress,
Dialog,
DialogContent,
Typography
} from '@mui/material';
import { readHardwareStatus } from 'api/system'; import { readHardwareStatus } from 'api/system';
import { useRequest } from 'alova/client'; import { dialogStyle } from 'CustomTheme';
import { FormLoader } from 'components'; import { useAutoRequest } from 'alova/client';
import MessageBox from 'components/MessageBox';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const RESTART_TIMEOUT = 2 * 60 * 1000; // 2 minutes const RestartMonitor = () => {
const POLL_INTERVAL = 2000; // every 2 seconds const [errorMessage, setErrorMessage] = useState<string>();
export interface RestartMonitorProps {
message?: string;
}
const RestartMonitor: FC<RestartMonitorProps> = ({ message }) => {
const [failed, setFailed] = useState<boolean>(false);
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT); let count = 0;
const { send } = useRequest(readHardwareStatus, { immediate: false }); const { data } = useAutoRequest(readHardwareStatus, {
pollingTime: 1000,
const poll = useRef(async () => { force: true,
try { initialData: { status: 'Getting ready...' },
await send(); async middleware(_, next) {
if (count++ >= 1) {
// skip first request (1 seconds) to allow AsyncWS to send its response
await next();
}
}
})
.onSuccess((event) => {
console.log(event.data.status); // TODO remove
if (event.data.status === 'ready' || event.data.status === undefined) {
document.location.href = '/'; document.location.href = '/';
} catch {
if (new Date().getTime() < timeoutAt.current) {
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
} else {
setFailed(true);
}
} }
})
.onError((error, _method) => {
setErrorMessage(error.message);
}); });
useEffect(() => {
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
}, []);
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
return ( return (
<FormLoader <Dialog fullWidth={true} sx={dialogStyle} open={true}>
message={message ? message : LL.APPLICATION_RESTARTING() + '...'} <DialogContent dividers>
errorMessage={failed ? 'Timed out' : undefined} <Box m={2} py={2} display="flex" alignItems="center" flexDirection="column">
/> <Typography
color="secondary"
variant="h6"
fontWeight={400}
textAlign="center"
>
{data?.status === 'uploading'
? LL.WAIT_FIRMWARE()
: data?.status === 'restarting'
? LL.APPLICATION_RESTARTING()
: data?.status === 'ready'
? 'Reloading'
: 'Preparing'}
</Typography>
<Typography mt={2} variant="h6" fontWeight={400} textAlign="center">
{LL.PLEASE_WAIT()}&hellip;
</Typography>
{errorMessage ? (
<MessageBox my={2} level="error" message={errorMessage} />
) : (
<Box py={2}>
<CircularProgress size={48} />
</Box>
)}
</Box>
</DialogContent>
</Dialog>
); );
}; };

View File

@@ -30,10 +30,11 @@ import {
} from '@mui/material'; } from '@mui/material';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { API } from 'api/app';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useAutoRequest, useRequest } from 'alova/client'; import { useAutoRequest, useRequest } from 'alova/client';
import { busConnectionStatus } from 'app/main/types'; import { type APIcall, busConnectionStatus } from 'app/main/types';
import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem'; import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
@@ -54,7 +55,7 @@ const SystemStatus = () => {
const [confirmRestart, setConfirmRestart] = useState<boolean>(false); const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
const [restarting, setRestarting] = useState<boolean>(); const [restarting, setRestarting] = useState<boolean>();
const { send: restartCommand } = useRequest(SystemApi.restart(), { const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false immediate: false
}); });
@@ -64,7 +65,12 @@ const SystemStatus = () => {
error error
} = useAutoRequest(SystemApi.readSystemStatus, { } = useAutoRequest(SystemApi.readSystemStatus, {
initialData: [], initialData: [],
pollingTime: 5000 pollingTime: 5000,
async middleware(_, next) {
if (!restarting) {
await next();
}
}
}); });
const theme = useTheme(); const theme = useTheme();
@@ -195,17 +201,14 @@ const SystemStatus = () => {
const activeHighlight = (value: boolean) => const activeHighlight = (value: boolean) =>
value ? theme.palette.success.main : theme.palette.info.main; value ? theme.palette.success.main : theme.palette.info.main;
const restart = async () => { const doRestart = async () => {
await restartCommand()
.then(() => {
setRestarting(true);
})
.catch((error: Error) => {
toast.error(error.message);
})
.finally(() => {
setConfirmRestart(false); setConfirmRestart(false);
}); setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: -1 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
}; };
const renderRestartDialog = () => ( const renderRestartDialog = () => (
@@ -228,7 +231,7 @@ const SystemStatus = () => {
<Button <Button
startIcon={<PowerSettingsNewIcon />} startIcon={<PowerSettingsNewIcon />}
variant="outlined" variant="outlined"
onClick={restart} onClick={doRestart}
color="error" color="error"
> >
{LL.RESTART()} {LL.RESTART()}

View File

@@ -16,7 +16,7 @@ UploadFileService::UploadFileService(AsyncWebServer * server, SecurityManager *
: _securityManager(securityManager) : _securityManager(securityManager)
, _is_firmware(false) , _is_firmware(false)
, _md5() { , _md5() {
// end-points // upload a file via a form
server->on( server->on(
UPLOAD_FILE_PATH, UPLOAD_FILE_PATH,
HTTP_POST, HTTP_POST,
@@ -25,6 +25,7 @@ UploadFileService::UploadFileService(AsyncWebServer * server, SecurityManager *
handleUpload(request, filename, index, data, len, final); handleUpload(request, filename, index, data, len, final);
}); });
// upload from a URL
server->on(UPLOAD_URL_PATH, server->on(UPLOAD_URL_PATH,
securityManager->wrapCallback([this](AsyncWebServerRequest * request, JsonVariant json) { uploadURL(request, json); }, securityManager->wrapCallback([this](AsyncWebServerRequest * request, JsonVariant json) { uploadURL(request, json); },
AuthenticationPredicates::IS_AUTHENTICATED)); AuthenticationPredicates::IS_AUTHENTICATED));
@@ -123,7 +124,7 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
request->_tempFile.close(); // close the file handle as the upload is now done request->_tempFile.close(); // close the file handle as the upload is now done
AsyncWebServerResponse * response = request->beginResponse(200); AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response); request->send(response);
request->onDisconnect(RestartService::restartNow); emsesp::EMSESP::system_.restart_pending(true); // will be handled by the main loop
return; return;
} }
@@ -132,7 +133,7 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
if (_is_firmware && !request->_tempObject) { if (_is_firmware && !request->_tempObject) {
AsyncWebServerResponse * response = request->beginResponse(200); AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response); request->send(response);
request->onDisconnect(RestartService::restartNow); emsesp::EMSESP::system_.restart_pending(true); // will be handled by the main loop
return; return;
} }

View File

@@ -215,20 +215,6 @@ static void setup_commands(std::shared_ptr<Commands> & commands) {
string_vector{F_(wifi), F_(reconnect)}, string_vector{F_(wifi), F_(reconnect)},
[](Shell & shell, const std::vector<std::string> & arguments) { to_app(shell).system_.wifi_reconnect(); }); [](Shell & shell, const std::vector<std::string> & arguments) { to_app(shell).system_.wifi_reconnect(); });
commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, string_vector{F_(format)}, [](Shell & shell, const std::vector<std::string> & arguments) {
shell.enter_password(F_(password_prompt), [=](Shell & shell, bool completed, const std::string & password) {
if (completed) {
to_app(shell).esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) {
if (securitySettings.jwtSecret.equals(password.c_str())) {
to_app(shell).system_.format(shell);
} else {
shell.println("incorrect password");
}
});
}
});
});
// //
// SET commands // SET commands
// //
@@ -651,11 +637,7 @@ void EMSESPShell::stopped() {
void EMSESPShell::display_banner() { void EMSESPShell::display_banner() {
println(); println();
printfln("┌───────────────────────────────────────┐"); printfln("┌───────────────────────────────────────┐");
#ifndef EMSESP_DEBUG
printfln("│ %sEMS-ESP version %-20s%s │", COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_BOLD_OFF); printfln("│ %sEMS-ESP version %-20s%s │", COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_BOLD_OFF);
#else
printfln("│ %sEMS-ESP version %s%-8s%s │", COLOR_BOLD_ON, EMSESP_APP_VERSION, " (D)", COLOR_BOLD_OFF);
#endif
printfln("│ │"); printfln("│ │");
printfln("│ %shelp%s to show available commands │", COLOR_UNDERLINE, COLOR_RESET); printfln("│ %shelp%s to show available commands │", COLOR_UNDERLINE, COLOR_RESET);
printfln("│ %ssu%s to access admin commands │", COLOR_UNDERLINE, COLOR_RESET); printfln("│ %ssu%s to access admin commands │", COLOR_UNDERLINE, COLOR_RESET);

View File

@@ -1569,7 +1569,7 @@ void EMSESP::start() {
// do a quick scan of the filesystem to see if we have a /config folder // do a quick scan of the filesystem to see if we have a /config folder
// so we know if this is a new install or not // so we know if this is a new install or not
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
File root = LittleFS.open("/config"); File root = LittleFS.open(EMSESP_FS_CONFIG_DIRECTORY);
bool factory_settings = !root; bool factory_settings = !root;
if (!root) { if (!root) {
LOG_INFO("No config found, assuming factory settings"); LOG_INFO("No config found, assuming factory settings");
@@ -1589,9 +1589,9 @@ void EMSESP::start() {
LOG_DEBUG("NVS device information: %s", system_.getBBQKeesGatewayDetails().c_str()); LOG_DEBUG("NVS device information: %s", system_.getBBQKeesGatewayDetails().c_str());
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
LOG_INFO("Starting EMS-ESP version %s from %s partition", EMSESP_APP_VERSION, esp_ota_get_running_partition()->label); // welcome message LOG_INFO("Booting EMS-ESP version %s from %s partition", EMSESP_APP_VERSION, esp_ota_get_running_partition()->label); // welcome message
#else #else
LOG_INFO("Starting EMS-ESP version %s", EMSESP_APP_VERSION); // welcome message LOG_INFO("Booting EMS-ESP version %s", EMSESP_APP_VERSION); // welcome message
#endif #endif
LOG_DEBUG("System is running in Debug mode"); LOG_DEBUG("System is running in Debug mode");
LOG_INFO("Last system reset reason Core0: %s, Core1: %s", system_.reset_reason(0).c_str(), system_.reset_reason(1).c_str()); LOG_INFO("Last system reset reason Core0: %s, Core1: %s", system_.reset_reason(0).c_str(), system_.reset_reason(1).c_str());
@@ -1610,6 +1610,12 @@ void EMSESP::start() {
system_.system_restart(); system_.system_restart();
}; };
// Load our library of known devices into stack mem. Names are stored in Flash memory
device_library_ = {
#include "device_library.h"
};
LOG_INFO("Loaded EMS device library (%d)", device_library_.size());
system_.reload_settings(); // ... and store some of the settings locally system_.reload_settings(); // ... and store some of the settings locally
webCustomizationService.begin(); // load the customizations webCustomizationService.begin(); // load the customizations
@@ -1627,31 +1633,22 @@ void EMSESP::start() {
#endif #endif
} }
// start services
if (system_.modbus_enabled()) { if (system_.modbus_enabled()) {
modbus_ = new Modbus; modbus_ = new Modbus;
modbus_->start(1, system_.modbus_port(), system_.modbus_max_clients(), system_.modbus_timeout()); modbus_->start(1, system_.modbus_port(), system_.modbus_max_clients(), system_.modbus_timeout());
} }
mqtt_.start(); // mqtt init mqtt_.start(); // mqtt init
system_.start(); // starts commands, led, adc, button, network (sets hostname), syslog & uart system_.start(); // starts commands, led, adc, button, network (sets hostname), syslog & uart
shower_.start(); // initialize shower timer and shower alert shower_.start(); // initialize shower timer and shower alert
temperaturesensor_.start(); // Temperature external sensors temperaturesensor_.start(); // Temperature external sensors
analogsensor_.start(); // Analog external sensors analogsensor_.start(); // Analog external sensors
// start web services
webLogService.start(); // apply settings to weblog service webLogService.start(); // apply settings to weblog service
webModulesService.begin(); // setup the external library modules webModulesService.begin(); // setup the external library modules
// Load our library of known devices into stack mem. Names are stored in Flash memory
device_library_ = {
#include "device_library.h"
};
LOG_INFO("Loaded EMS device library (%d records)", device_library_.size());
#if defined(EMSESP_STANDALONE)
Mqtt::on_connect(); // simulate an MQTT connection
#endif
webServer.begin(); // start the web server webServer.begin(); // start the web server
LOG_INFO("Starting Web Server");
} }
// main loop calling all services // main loop calling all services

View File

@@ -67,6 +67,7 @@ MAKE_WORD_TRANSLATION(setiovalue_cmd, "set io value", "Setze Wertevorgabe", "ins
MAKE_WORD_TRANSLATION(changeloglevel_cmd, "change log level", "Ändere Sysloglevel", "aanpassen log niveau", "", "zmień poziom log-u", "endre loggnivå", "", "Kayıt seviyesini değiştir", "cambia livello registrazione", "") // TODO translate MAKE_WORD_TRANSLATION(changeloglevel_cmd, "change log level", "Ändere Sysloglevel", "aanpassen log niveau", "", "zmień poziom log-u", "endre loggnivå", "", "Kayıt seviyesini değiştir", "cambia livello registrazione", "") // TODO translate
MAKE_WORD_TRANSLATION(fetch_cmd, "refresh all EMS values", "Lese alle EMS-Werte neu", "Verversen alle EMS waardes", "", "odśwież wszystkie wartości EMS", "oppfrisk alle EMS verdier", "", "Bütün EMS değerlerini yenile", "aggiornare tutti i valori EMS", "obnoviť všetky hodnoty EMS") // TODO translate MAKE_WORD_TRANSLATION(fetch_cmd, "refresh all EMS values", "Lese alle EMS-Werte neu", "Verversen alle EMS waardes", "", "odśwież wszystkie wartości EMS", "oppfrisk alle EMS verdier", "", "Bütün EMS değerlerini yenile", "aggiornare tutti i valori EMS", "obnoviť všetky hodnoty EMS") // TODO translate
MAKE_WORD_TRANSLATION(restart_cmd, "restart EMS-ESP", "Neustart", "opnieuw opstarten", "", "uruchom ponownie EMS-ESP", "restart EMS-ESP", "redémarrer EMS-ESP", "EMS-ESPyi yeniden başlat", "riavvia EMS-ESP", "reštart EMS-ESP") // TODO translate MAKE_WORD_TRANSLATION(restart_cmd, "restart EMS-ESP", "Neustart", "opnieuw opstarten", "", "uruchom ponownie EMS-ESP", "restart EMS-ESP", "redémarrer EMS-ESP", "EMS-ESPyi yeniden başlat", "riavvia EMS-ESP", "reštart EMS-ESP") // TODO translate
MAKE_WORD_TRANSLATION(format_cmd, "factory reset EMS-ESP", "", "", "", "", "", "", "", "", "") // TODO translate
MAKE_WORD_TRANSLATION(watch_cmd, "watch incoming telegrams", "Watch auf eingehende Telegramme", "inkomende telegrammen bekijken", "", "obserwuj przyczodzące telegramy", "se innkommende telegrammer", "", "Gelen telegramları", "guardare i telegrammi in arrivo", "sledovať prichádzajúce telegramy") // TODO translate MAKE_WORD_TRANSLATION(watch_cmd, "watch incoming telegrams", "Watch auf eingehende Telegramme", "inkomende telegrammen bekijken", "", "obserwuj przyczodzące telegramy", "se innkommende telegrammer", "", "Gelen telegramları", "guardare i telegrammi in arrivo", "sledovať prichádzajúce telegramy") // TODO translate
MAKE_WORD_TRANSLATION(publish_cmd, "publish all to MQTT", "Publiziere MQTT", "publiceer alles naar MQTT", "", "opublikuj wszystko na MQTT", "Publiser alt til MQTT", "", "Hepsini MQTTye gönder", "pubblica tutto su MQTT", "zverejniť všetko na MQTT") // TODO translate MAKE_WORD_TRANSLATION(publish_cmd, "publish all to MQTT", "Publiziere MQTT", "publiceer alles naar MQTT", "", "opublikuj wszystko na MQTT", "Publiser alt til MQTT", "", "Hepsini MQTTye gönder", "pubblica tutto su MQTT", "zverejniť všetko na MQTT") // TODO translate
MAKE_WORD_TRANSLATION(system_info_cmd, "show system info", "Zeige System-Status", "toon systeemstatus", "", "pokaż status systemu", "vis system status", "", "Sistem Durumunu Göster", "visualizza stati di sistema", "zobraziť stav systému") // TODO translate MAKE_WORD_TRANSLATION(system_info_cmd, "show system info", "Zeige System-Status", "toon systeemstatus", "", "pokaż status systemu", "vis system status", "", "Sistem Durumunu Göster", "visualizza stati di sistema", "zobraziť stav systému") // TODO translate

View File

@@ -78,6 +78,7 @@ uuid::log::Logger System::logger_{F_(system), uuid::log::Facility::KERN};
// init statics // init statics
PButton System::myPButton_; PButton System::myPButton_;
bool System::restart_requested_ = false; bool System::restart_requested_ = false;
bool System::restart_pending_ = false;
bool System::test_set_all_active_ = false; bool System::test_set_all_active_ = false;
uint32_t System::max_alloc_mem_; uint32_t System::max_alloc_mem_;
uint32_t System::heap_mem_; uint32_t System::heap_mem_;
@@ -328,7 +329,10 @@ void System::system_restart(const char * partitionname) {
LOG_INFO("Restarting EMS-ESP..."); LOG_INFO("Restarting EMS-ESP...");
} }
restart_requested(false); // make sure it's not repeated // make sure it's only executed once
restart_requested(false);
restart_pending(false);
store_nvs_values(); // save any NVS values store_nvs_values(); // save any NVS values
Shell::loop_all(); // flush log to output Shell::loop_all(); // flush log to output
delay(1000); // wait 1 second delay(1000); // wait 1 second
@@ -346,19 +350,6 @@ void System::wifi_reconnect() {
EMSESP::esp8266React.getNetworkSettingsService()->callUpdateHandlers(); // in case we've changed ssid or password EMSESP::esp8266React.getNetworkSettingsService()->callUpdateHandlers(); // in case we've changed ssid or password
} }
// format the FS. Wipes everything.
void System::format(uuid::console::Shell & shell) {
auto msg = ("Formatting file system. This will reset all settings to their defaults");
shell.logger().warning(msg);
EMSuart::stop();
#ifndef EMSESP_STANDALONE
LittleFS.format();
#endif
System::system_restart();
}
void System::syslog_init() { void System::syslog_init() {
EMSESP::webSettingsService.read([&](WebSettings & settings) { EMSESP::webSettingsService.read([&](WebSettings & settings) {
syslog_enabled_ = settings.syslog_enabled; syslog_enabled_ = settings.syslog_enabled;
@@ -538,12 +529,9 @@ void System::button_OnLongPress(PButton & b) {
EMSESP::system_.system_restart("boot"); EMSESP::system_.system_restart("boot");
} }
// button indefinite press // button indefinite press - do nothing for now
void System::button_OnVLongPress(PButton & b) { void System::button_OnVLongPress(PButton & b) {
LOG_NOTICE("Button pressed - very long press - factory reset"); LOG_NOTICE("Button pressed - very long press");
#ifndef EMSESP_STANDALONE
EMSESP::esp8266React.factoryReset();
#endif
} }
// push button // push button
@@ -860,9 +848,8 @@ void System::system_check() {
void System::commands_init() { void System::commands_init() {
Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, FL_(send_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, FL_(send_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, FL_(fetch_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, FL_(fetch_cmd), CommandFlag::ADMIN_ONLY);
// restart, watch, message (and test) are also exposed as Console commands
Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(format), System::command_format, FL_(format_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, FL_(watch_cmd)); Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, FL_(watch_cmd));
Command::add(EMSdevice::DeviceType::SYSTEM, F_(message), System::command_message, FL_(message_cmd)); Command::add(EMSdevice::DeviceType::SYSTEM, F_(message), System::command_message, FL_(message_cmd));
#if defined(EMSESP_TEST) #if defined(EMSESP_TEST)
@@ -1748,13 +1735,35 @@ bool System::load_board_profile(std::vector<int8_t> & data, const std::string &
return true; return true;
} }
// format command - factory reset, removing all config files
bool System::command_format(const char * value, const int8_t id) {
LOG_INFO("Removing all config files");
#ifndef EMSESP_STANDALONE
// TODO To replaced with fs.rmdir(FS_CONFIG_DIRECTORY) now we're using IDF 4.2+
File root = LittleFS.open(EMSESP_FS_CONFIG_DIRECTORY);
File file;
while ((file = root.openNextFile())) {
String path = file.path();
file.close();
LittleFS.remove(path);
}
#endif
EMSESP::system_.restart_requested(true); // will be handled by the main loop
return true;
}
// restart command - perform a hard reset // restart command - perform a hard reset
bool System::command_restart(const char * value, const int8_t id) { bool System::command_restart(const char * value, const int8_t id) {
if (value != nullptr && value[0] != '\0') { if (id != 0) {
EMSESP::system_.system_restart(value); // if it has an id then it's a web call and we need to queue the restart
} else { LOG_INFO("Preparing to restart system");
EMSESP::system_.system_restart(); EMSESP::system_.restart_pending(true);
return true;
} }
LOG_INFO("Restarting system");
EMSESP::system_.restart_requested(true); // will be handled by the main loop
return true; return true;
} }
@@ -1908,7 +1917,7 @@ bool System::uploadFirmwareURL(const char * url) {
LOG_INFO("Firmware uploaded successfully. Restarting..."); LOG_INFO("Firmware uploaded successfully. Restarting...");
restart_requested(true); restart_pending(true);
#endif #endif

View File

@@ -41,6 +41,8 @@
using uuid::console::Shell; using uuid::console::Shell;
#define EMSESP_FS_CONFIG_DIRECTORY "/config"
namespace emsesp { namespace emsesp {
enum PHY_type : uint8_t { PHY_TYPE_NONE = 0, PHY_TYPE_LAN8720, PHY_TYPE_TLK110 }; enum PHY_type : uint8_t { PHY_TYPE_NONE = 0, PHY_TYPE_LAN8720, PHY_TYPE_TLK110 };
@@ -55,12 +57,14 @@ class System {
static bool command_publish(const char * value, const int8_t id); static bool command_publish(const char * value, const int8_t id);
static bool command_fetch(const char * value, const int8_t id); static bool command_fetch(const char * value, const int8_t id);
static bool command_restart(const char * value, const int8_t id); static bool command_restart(const char * value, const int8_t id);
static bool command_syslog_level(const char * value, const int8_t id); static bool command_format(const char * value, const int8_t id);
// static bool command_syslog_level(const char * value, const int8_t id);
static bool command_watch(const char * value, const int8_t id); static bool command_watch(const char * value, const int8_t id);
static bool command_message(const char * value, const int8_t id); static bool command_message(const char * value, const int8_t id);
static bool command_info(const char * value, const int8_t id, JsonObject output); static bool command_info(const char * value, const int8_t id, JsonObject output);
static bool command_response(const char * value, const int8_t id, JsonObject output); static bool command_response(const char * value, const int8_t id, JsonObject output);
static bool command_allvalues(const char * value, const int8_t id, JsonObject output); static bool command_allvalues(const char * value, const int8_t id, JsonObject output);
static bool get_value_info(JsonObject root, const char * cmd); static bool get_value_info(JsonObject root, const char * cmd);
static void get_value_json(JsonObject output, const std::string & circuit, const std::string & name, JsonVariant val); static void get_value_json(JsonObject output, const std::string & circuit, const std::string & name, JsonVariant val);
@@ -72,7 +76,6 @@ class System {
void store_nvs_values(); void store_nvs_values();
void system_restart(const char * partition = nullptr); void system_restart(const char * partition = nullptr);
void format(uuid::console::Shell & shell);
void upload_status(bool in_progress); void upload_status(bool in_progress);
bool upload_status(); bool upload_status();
void show_mem(const char * note); void show_mem(const char * note);
@@ -116,11 +119,17 @@ class System {
static void restart_requested(bool restart_requested) { static void restart_requested(bool restart_requested) {
restart_requested_ = restart_requested; restart_requested_ = restart_requested;
} }
static bool restart_requested() { static bool restart_requested() {
return restart_requested_; return restart_requested_;
} }
static void restart_pending(bool restart_pending) {
restart_pending_ = restart_pending;
}
static bool restart_pending() {
return restart_pending_;
}
bool telnet_enabled() { bool telnet_enabled() {
return telnet_enabled_; return telnet_enabled_;
} }
@@ -291,6 +300,7 @@ class System {
private: private:
static uuid::log::Logger logger_; static uuid::log::Logger logger_;
static bool restart_requested_; static bool restart_requested_;
static bool restart_pending_; // used in 2-stage process to call restart from Web API
static bool test_set_all_active_; // force all entities in a device to have a value static bool test_set_all_active_; // force all entities in a device to have a value
static uint32_t max_alloc_mem_; static uint32_t max_alloc_mem_;
static uint32_t heap_mem_; static uint32_t heap_mem_;

View File

@@ -156,7 +156,7 @@ void WebCustomizationService::reset_customization(AsyncWebServerRequest * reques
if (LittleFS.remove(EMSESP_CUSTOMIZATION_FILE)) { if (LittleFS.remove(EMSESP_CUSTOMIZATION_FILE)) {
AsyncWebServerResponse * response = request->beginResponse(205); // restart needed AsyncWebServerResponse * response = request->beginResponse(205); // restart needed
request->send(response); request->send(response);
EMSESP::system_.restart_requested(true); EMSESP::system_.restart_pending(true);
return; return;
} }

View File

@@ -156,7 +156,8 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
} else { } else {
EMSESP::nvs_.putString("boot", "S32"); EMSESP::nvs_.putString("boot", "S32");
} }
ESP.restart(); // ESP.restart();
EMSESP::system_.restart_requested(true);
#elif CONFIG_IDF_TARGET_ESP32C3 #elif CONFIG_IDF_TARGET_ESP32C3
settings.board_profile = "C3MINI"; settings.board_profile = "C3MINI";
#elif CONFIG_IDF_TARGET_ESP32S2 #elif CONFIG_IDF_TARGET_ESP32S2

View File

@@ -34,12 +34,6 @@ WebStatusService::WebStatusService(AsyncWebServer * server, SecurityManager * se
// /rest/systemStatus // /rest/systemStatus
void WebStatusService::systemStatus(AsyncWebServerRequest * request) { void WebStatusService::systemStatus(AsyncWebServerRequest * request) {
// This is a litle trick for the OTA upload. We don't want the React RestartService to think we're finished
// with the upload so we fake it and pretent the /rest/systemStatus is not available. That way the spinner keeps spinning.
if (EMSESP::system_.upload_status()) {
return; // ignore endpoint
}
EMSESP::system_.refreshHeapMem(); // refresh free heap and max alloc heap EMSESP::system_.refreshHeapMem(); // refresh free heap and max alloc heap
auto * response = new AsyncJsonResponse(false); auto * response = new AsyncJsonResponse(false);
@@ -85,6 +79,7 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) {
} }
// /rest/hardwareStatus // /rest/hardwareStatus
// This is also used for polling
void WebStatusService::hardwareStatus(AsyncWebServerRequest * request) { void WebStatusService::hardwareStatus(AsyncWebServerRequest * request) {
EMSESP::system_.refreshHeapMem(); // refresh free heap and max alloc heap EMSESP::system_.refreshHeapMem(); // refresh free heap and max alloc heap
@@ -144,6 +139,14 @@ void WebStatusService::hardwareStatus(AsyncWebServerRequest * request) {
root["has_partition"] = false; root["has_partition"] = false;
} }
// Matches RestartMonitor.tsx
if (EMSESP::system_.restart_pending()) {
root["status"] = "restarting";
EMSESP::system_.restart_requested(true); // tell emsesp loop to start restart
} else {
root["status"] = EMSESP::system_.upload_status() ? "uploading" : "ready";
}
#endif #endif
response->setLength(); response->setLength();