mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-10-28 11:14:58 +00:00
show recovery app in Version page
This commit is contained in:
+2
-2
@@ -26,8 +26,8 @@ This release is based on the latest Espressif/Arduino core version 3. It brings
|
||||
- the Version page shows the installed Recovery version on 16MB boards
|
||||
- crash test builds to try out the rollback and recovery, see `[env:crashtest_s3_16M_P]` in `platformio.ini`
|
||||
- `s_16M_P_custom` PlatformIO env that HybridCompiles Arduino/IDF with a trimmed sdkconfig (no Bluetooth, PPP/modem, IP forward/NAPT, ULP, SPI Ethernet, GDBSTUB, FreeRTOS trace, mDNS browse/CLI) and places the UART ISR in IRAM
|
||||
- 16MB builds use EMS-ESP's own Arduino/IDF libs from [esp32-arduino-lib-builder](https://github.com/emsesp/esp32-arduino-lib-builder) (`0910-1604-5.5.5`) instead of Tasmota's prebuilt cores
|
||||
- optional `ccache` wrapping when it is installed (`scripts/enable_ccache.py`)
|
||||
- 16MB builds use EMS-ESP's own Arduino/IDF libs from [esp32-arduino-lib-builder](https://github.com/emsesp/esp32-arduino-lib-builder) instead of Tasmota's prebuilt cores
|
||||
- optional `ccache` wrapping when it is installed (`scripts/enable_ccache.py`) to speed up CI
|
||||
|
||||
## Fixed
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"etag": "^1.8.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"prettier": "^3.9.9",
|
||||
"prettier": "^3.9.10",
|
||||
"rollup-plugin-visualizer": "^7.1.1",
|
||||
"terser": "^5.51.2",
|
||||
"typescript": "^6.0.3",
|
||||
|
||||
Generated
+8
-8
@@ -217,7 +217,7 @@ importers:
|
||||
version: 2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@11.0.1)(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.5)(terser@5.51.2))
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2(prettier@3.9.9)(supports-color@10.2.2)
|
||||
version: 6.0.2(prettier@3.9.10)(supports-color@10.2.2)
|
||||
'@types/node':
|
||||
specifier: ^26.6.5
|
||||
version: 26.6.5
|
||||
@@ -243,8 +243,8 @@ importers:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
prettier:
|
||||
specifier: ^3.9.9
|
||||
version: 3.9.9
|
||||
specifier: ^3.9.10
|
||||
version: 3.9.10
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.1(rolldown@1.2.13)
|
||||
@@ -2554,8 +2554,8 @@ packages:
|
||||
resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
prettier@3.9.9:
|
||||
resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==}
|
||||
prettier@3.9.10:
|
||||
resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
@@ -3708,7 +3708,7 @@ snapshots:
|
||||
react-virtualized-auto-sizer: 1.0.26(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
react-window: 1.8.11(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.9)(supports-color@10.2.2)':
|
||||
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.10)(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.29.8
|
||||
'@babel/parser': 7.29.9
|
||||
@@ -3718,7 +3718,7 @@ snapshots:
|
||||
lodash-es: 4.18.1
|
||||
minimatch: 9.0.9
|
||||
parse-imports-exports: 0.2.4
|
||||
prettier: 3.9.9
|
||||
prettier: 3.9.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -5514,7 +5514,7 @@ snapshots:
|
||||
|
||||
prepend-http@2.0.0: {}
|
||||
|
||||
prettier@3.9.9: {}
|
||||
prettier@3.9.10: {}
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@ import { Link } from 'react-router';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import CheckIcon from '@mui/icons-material/Done';
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {
|
||||
Box,
|
||||
@@ -51,6 +54,8 @@ const STABLE_RELNOTES_URL =
|
||||
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
||||
const DEV_RELNOTES_URL =
|
||||
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
||||
const RELEASES_API_URL =
|
||||
'https://api.github.com/repos/emsesp/EMS-ESP32/releases/tags/';
|
||||
const MAX_VERSION_POLLS = 5; // approx 15 seconds of waiting
|
||||
|
||||
// Types for better type safety
|
||||
@@ -75,6 +80,96 @@ interface VersionData {
|
||||
disable_reset: boolean;
|
||||
}
|
||||
|
||||
interface RecoveryRelease {
|
||||
url: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
// finds the recovery firmware for this board in the GitHub releases, trying each release tag in turn.
|
||||
// The version is in the filename, e.g. EMS-ESP-Recovery-1_0_0-ESP32S3-16MB+.bin is 1.0.0
|
||||
const findRecovery = async (
|
||||
tags: string[],
|
||||
platform: string
|
||||
): Promise<RecoveryRelease | undefined> => {
|
||||
const prefix = 'ems-esp-recovery-';
|
||||
const suffix = `-${platform}.bin`.toLowerCase();
|
||||
for (const tag of tags) {
|
||||
try {
|
||||
const response = await fetch(`${RELEASES_API_URL}${tag}`);
|
||||
if (!response.ok) {
|
||||
continue;
|
||||
}
|
||||
const release = (await response.json()) as {
|
||||
assets?: { name: string; browser_download_url: string }[];
|
||||
};
|
||||
const asset = release.assets?.find((a) => {
|
||||
const name = a.name.toLowerCase();
|
||||
return name.startsWith(prefix) && name.endsWith(suffix);
|
||||
});
|
||||
if (asset) {
|
||||
return {
|
||||
url: asset.browser_download_url,
|
||||
version: asset.name
|
||||
.slice(prefix.length, asset.name.length - suffix.length)
|
||||
.replaceAll('_', '.')
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// try the next one
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// the GitHub API only allows 60 requests an hour, so only look once per page load for each board and channel
|
||||
const recoveryLookups = new Map<string, Promise<RecoveryRelease | undefined>>();
|
||||
const findRecoveryCached = (tags: string[], platform: string) => {
|
||||
const key = `${tags.join(',')}|${platform}`;
|
||||
let lookup = recoveryLookups.get(key);
|
||||
if (!lookup) {
|
||||
lookup = findRecovery(tags, platform);
|
||||
recoveryLookups.set(key, lookup);
|
||||
void lookup.then((release) => {
|
||||
if (!release) {
|
||||
recoveryLookups.delete(key); // try again next time
|
||||
}
|
||||
});
|
||||
}
|
||||
return lookup;
|
||||
};
|
||||
|
||||
// true if version a is newer than b, like 1.0.1 and 1.0.0. A prerelease, like 1.1.0-dev.1, is older than the release
|
||||
const isNewerVersion = (a: string, b: string) => {
|
||||
const parse = (v: string) => {
|
||||
const dash = v.indexOf('-');
|
||||
const numbers = (text: string) =>
|
||||
text.split('.').map((n) => Number.parseInt(n, 10) || 0);
|
||||
return {
|
||||
main: numbers(dash < 0 ? v : v.slice(0, dash)),
|
||||
pre: dash < 0 ? [] : numbers(v.slice(dash + 1)),
|
||||
isPre: dash >= 0
|
||||
};
|
||||
};
|
||||
const x = parse(a);
|
||||
const y = parse(b);
|
||||
for (let i = 0; i < Math.max(x.main.length, y.main.length); i++) {
|
||||
const diff = (x.main[i] ?? 0) - (y.main[i] ?? 0);
|
||||
if (diff !== 0) {
|
||||
return diff > 0;
|
||||
}
|
||||
}
|
||||
if (x.isPre !== y.isPre) {
|
||||
return !x.isPre;
|
||||
}
|
||||
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
|
||||
const diff = (x.pre[i] ?? 0) - (y.pre[i] ?? 0);
|
||||
if (diff !== 0) {
|
||||
return diff > 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Memoized components for better performance
|
||||
const VersionInfoDialog = memo(
|
||||
({
|
||||
@@ -235,6 +330,7 @@ const VersionInfoDialog = memo(
|
||||
<DialogActions>
|
||||
{!isPartition && (
|
||||
<Button
|
||||
startIcon={<DescriptionIcon />}
|
||||
variant="outlined"
|
||||
component="a"
|
||||
href={relNotesUrl}
|
||||
@@ -244,7 +340,12 @@ const VersionInfoDialog = memo(
|
||||
Changelog
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outlined" onClick={onClose} color="secondary">
|
||||
<Button
|
||||
startIcon={<CloseIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CLOSE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -372,9 +473,7 @@ const InstallPartitionDialog = memo(
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
|
||||
<DialogTitle>
|
||||
{isRecovery ? LL.RECOVERY() : `${LL.INSTALL()} ${LL.STORED_VERSIONS()}`}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{isRecovery ? LL.RECOVERY_APP() : LL.INSTALL()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography sx={{ mb: 2 }}>
|
||||
{isRecovery
|
||||
@@ -394,7 +493,7 @@ const InstallPartitionDialog = memo(
|
||||
|
||||
<Button
|
||||
startIcon={
|
||||
isRecovery ? <PowerSettingsNewIcon /> : <WarningIcon color="warning" />
|
||||
isRecovery ? <RestartAltIcon /> : <WarningIcon color="warning" />
|
||||
}
|
||||
variant="outlined"
|
||||
onClick={() => onInstall(partition)}
|
||||
@@ -418,6 +517,11 @@ const Version = () => {
|
||||
const { me, versions, refreshVersions } = useContext(AuthenticatedContext);
|
||||
|
||||
const [restarting, setRestarting] = useState<boolean>(false);
|
||||
const [findingRecovery, setFindingRecovery] = useState<boolean>(false);
|
||||
const [installingRecovery, setInstallingRecovery] = useState<boolean>(false);
|
||||
const [latestRecovery, setLatestRecovery] = useState<RecoveryRelease | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
|
||||
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
||||
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
|
||||
@@ -499,9 +603,6 @@ const Version = () => {
|
||||
|
||||
const platform = data ? getPlatform(data) : '';
|
||||
|
||||
const otherPartitions =
|
||||
data?.partitions.filter((p) => p.partition !== data.partition) ?? [];
|
||||
|
||||
const setPartitionVersionInfo = (partition: string) => {
|
||||
setShowVersionInfo(3);
|
||||
const partitionData = data?.partitions.find((p) => p.partition === partition);
|
||||
@@ -544,6 +645,49 @@ const Version = () => {
|
||||
await doRestart();
|
||||
};
|
||||
|
||||
// the release of the channel we're on first, it may not have a recovery app yet so try the other one too
|
||||
const stableTag = latestVersion ? `v${latestVersion.version}` : '';
|
||||
const recoveryTags = (
|
||||
usingDevVersion ? ['latest', stableTag] : [stableTag, 'latest']
|
||||
).filter(Boolean);
|
||||
const recoveryTagsKey = recoveryTags.join(',');
|
||||
const hasRecoverySlot = data?.recovery_version !== undefined;
|
||||
|
||||
// look for the latest recovery app, to show if there is a newer one
|
||||
useEffect(() => {
|
||||
if (!hasRecoverySlot || !internetLive || !platform) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void findRecoveryCached(recoveryTags, platform).then((release) => {
|
||||
if (!cancelled) {
|
||||
setLatestRecovery(release);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [hasRecoverySlot, internetLive, platform, recoveryTagsKey]);
|
||||
|
||||
// downloads the recovery app from GitHub into the factory partition, EMS-ESP doesn't restart
|
||||
const installRecovery = async () => {
|
||||
setFindingRecovery(true);
|
||||
const release =
|
||||
latestRecovery ?? (await findRecoveryCached(recoveryTags, platform));
|
||||
setFindingRecovery(false);
|
||||
|
||||
if (!release) {
|
||||
toast.error(LL.RECOVERY_NOT_FOUND());
|
||||
return;
|
||||
}
|
||||
|
||||
await sendUploadURL(release.url)
|
||||
.then(() => setInstallingRecovery(true))
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const installPartitionFirmware = async (partition: string) => {
|
||||
await sendSetPartition(partition).catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
@@ -607,8 +751,9 @@ const Version = () => {
|
||||
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
|
||||
</span>
|
||||
<Button
|
||||
sx={{ ml: 1 }}
|
||||
sx={{ ml: 3 }}
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
size="small"
|
||||
onClick={() => showFirmwareDialog(showingDev)}
|
||||
>
|
||||
@@ -624,9 +769,10 @@ const Version = () => {
|
||||
|
||||
return (
|
||||
<Button
|
||||
sx={{ ml: 1 }}
|
||||
sx={{ ml: 3 }}
|
||||
variant="outlined"
|
||||
color={isUpdateAvailable ? 'success' : 'warning'}
|
||||
startIcon={!isUpdateAvailable ? <SwapHorizIcon /> : undefined}
|
||||
size="small"
|
||||
onClick={() => showFirmwareDialog(showingDev)}
|
||||
>
|
||||
@@ -639,7 +785,7 @@ const Version = () => {
|
||||
display: 'inline-block',
|
||||
width: 8,
|
||||
height: 8,
|
||||
ml: 1,
|
||||
ml: 3,
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#ffeb3b',
|
||||
@@ -655,6 +801,20 @@ const Version = () => {
|
||||
return <SystemMonitor />;
|
||||
}
|
||||
|
||||
if (installingRecovery) {
|
||||
return (
|
||||
<SystemMonitor
|
||||
onDone={(success) => {
|
||||
setInstallingRecovery(false);
|
||||
if (success) {
|
||||
toast.success(LL.RECOVERY_INSTALLED());
|
||||
}
|
||||
void loadData();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<SectionContent>
|
||||
@@ -674,9 +834,13 @@ const Version = () => {
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'auto 1fr',
|
||||
gridAutoRows: 'minmax(32px, auto)', // same height for every row
|
||||
columnGap: 3,
|
||||
rowGap: 0.75,
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
// whole pixel line height so the labels and values are centered on the same pixel
|
||||
'& .MuiTypography-root': { lineHeight: '24px' },
|
||||
'& .MuiTypography-caption': { lineHeight: 1 }
|
||||
}}
|
||||
>
|
||||
<Typography color="secondary">{LL.VERSION()}</Typography>
|
||||
@@ -732,6 +896,51 @@ const Version = () => {
|
||||
PSRAM)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{data.recovery_version !== undefined && (
|
||||
<>
|
||||
<Typography color="secondary">{LL.RECOVERY_APP()}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minWidth: 0
|
||||
}}
|
||||
>
|
||||
<Typography component="span">
|
||||
{data.recovery_version || LL.RECOVERY_NOT_INSTALLED()}
|
||||
</Typography>
|
||||
{!data.recovery_version && me.admin && internetLive && (
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
sx={{ ml: 3 }}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
disabled={findingRecovery}
|
||||
onClick={() => void installRecovery()}
|
||||
>
|
||||
{LL.INSTALL()}
|
||||
</Button>
|
||||
)}
|
||||
{data.recovery_version && me.admin && (
|
||||
<Button
|
||||
startIcon={<RestartAltIcon />}
|
||||
sx={{ ml: 3 }}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const version = data.recovery_version;
|
||||
if (version) {
|
||||
showPartitionDialog(version, 'boot', '');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{LL.RESTART_INTO_RECOVERY()}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{internetLive ? (
|
||||
@@ -749,96 +958,6 @@ const Version = () => {
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{data.developer_mode && (
|
||||
<>
|
||||
{otherPartitions.length > 0 && (
|
||||
<>
|
||||
<Typography
|
||||
color="secondary"
|
||||
sx={{ alignSelf: 'start', pt: 0.5 }}
|
||||
>
|
||||
{LL.STORED_VERSIONS()}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}
|
||||
>
|
||||
{otherPartitions.map((partition) => (
|
||||
<Box
|
||||
key={partition.partition}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
<Typography component="span">
|
||||
{partition.version}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setPartitionVersionInfo(partition.partition)
|
||||
}
|
||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
<InfoOutlinedIcon
|
||||
color="primary"
|
||||
sx={{ fontSize: 18 }}
|
||||
/>
|
||||
</IconButton>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
showPartitionDialog(
|
||||
partition.version,
|
||||
partition.partition,
|
||||
partition.install_date ?? ''
|
||||
)
|
||||
}
|
||||
>
|
||||
{LL.INSTALL()}
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{data.recovery_version !== undefined && (
|
||||
<>
|
||||
<Typography color="secondary">{LL.RECOVERY()}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
<Typography component="span">
|
||||
{data.recovery_version || LL.RECOVERY_NOT_INSTALLED()}
|
||||
</Typography>
|
||||
{data.recovery_version && me.admin && (
|
||||
<Button
|
||||
sx={{ ml: 1 }}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const version = data.recovery_version;
|
||||
if (version) {
|
||||
showPartitionDialog(version, 'boot', '');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Boot
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ gridColumn: '1 / -1', height: 12 }} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Typography color="secondary">{LL.STABLE()}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -880,6 +999,52 @@ const Version = () => {
|
||||
</IconButton>
|
||||
{showButtons(true)}
|
||||
</Box>
|
||||
|
||||
{data.recovery_version && latestRecovery && (
|
||||
<>
|
||||
<Typography color="secondary">{LL.RECOVERY_APP()}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
minWidth: 0
|
||||
}}
|
||||
>
|
||||
<Typography component="span">
|
||||
{latestRecovery.version}
|
||||
</Typography>
|
||||
{isNewerVersion(
|
||||
latestRecovery.version,
|
||||
data.recovery_version
|
||||
) ? (
|
||||
me.admin && (
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
sx={{ ml: 3 }}
|
||||
variant="outlined"
|
||||
color="success"
|
||||
size="small"
|
||||
disabled={findingRecovery}
|
||||
onClick={() => void installRecovery()}
|
||||
>
|
||||
{LL.INSTALL()}
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<CheckIcon
|
||||
color="success"
|
||||
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
|
||||
/>
|
||||
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
|
||||
{LL.LATEST_VERSION(LL.RECOVERY_APP())}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
@@ -978,7 +1143,7 @@ const Version = () => {
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
startIcon={<RestartAltIcon />}
|
||||
variant="outlined"
|
||||
onClick={doRestart}
|
||||
color="success"
|
||||
@@ -1010,7 +1175,7 @@ const Version = () => {
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
startIcon={<RestartAltIcon />}
|
||||
variant="outlined"
|
||||
onClick={handleRestartClick}
|
||||
color="success"
|
||||
|
||||
@@ -14,7 +14,12 @@ import { useInterval } from 'utils';
|
||||
|
||||
import { LinearProgressWithLabel } from '../../components/upload/LinearProgressWithLabel';
|
||||
|
||||
const SystemMonitor = () => {
|
||||
interface SystemMonitorProps {
|
||||
// when set the page isn't reloaded when done, e.g. when the firmware was installed without a restart
|
||||
onDone?: (success: boolean) => void;
|
||||
}
|
||||
|
||||
const SystemMonitor = ({ onDone }: SystemMonitorProps) => {
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const hasInitialized = useRef(false);
|
||||
const { LL } = useI18nContext();
|
||||
@@ -42,7 +47,11 @@ const SystemMonitor = () => {
|
||||
event.data.status === SystemStatusCodes.SYSTEM_STATUS_NORMAL ||
|
||||
event.data.status === undefined
|
||||
) {
|
||||
if (onDone) {
|
||||
onDone(true);
|
||||
} else {
|
||||
document.location.href = '/';
|
||||
}
|
||||
} else if (
|
||||
event.data.status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
|
||||
) {
|
||||
@@ -80,7 +89,11 @@ const SystemMonitor = () => {
|
||||
const onCancel = async () => {
|
||||
setErrorMessage(undefined);
|
||||
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
|
||||
if (onDone) {
|
||||
onDone(false);
|
||||
} else {
|
||||
document.location.href = '/';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -336,7 +336,6 @@ const cs: Translation = {
|
||||
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi',
|
||||
FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru',
|
||||
USER_PROFILE: 'Uživatelský profil',
|
||||
STORED_VERSIONS: 'Uložené verze',
|
||||
ONLINE_HELP: 'online nápověda',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovat důležité zprávy',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že nejprve stáhnete systémovou zálohu před pokračováním a poté nahrajte tento soubor po instalaci nové verze.',
|
||||
@@ -353,9 +352,11 @@ const cs: Translation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Klikněte, abyste vybrali, nebo drop soubor firmware .bin/.md5 sem',
|
||||
UPLOAD_MD5_RECEIVED: 'Digest přijat, nyní nahrajte firmware .bin',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 souhlasí, firmware se instaluje...',
|
||||
RECOVERY: 'Obnova',
|
||||
RECOVERY_APP: 'Aplikace pro obnovu',
|
||||
RECOVERY_NOT_INSTALLED: 'Nenainstalováno',
|
||||
RECOVERY_INSTALLED: 'Firmware pro obnovu nainstalován'
|
||||
RECOVERY_INSTALLED: 'Firmware pro obnovu nainstalován',
|
||||
RESTART_INTO_RECOVERY: 'Restartovat do aplikace pro obnovu',
|
||||
RECOVERY_NOT_FOUND: 'Aplikace pro obnovu nebyla online nalezena'
|
||||
};
|
||||
|
||||
export default cs;
|
||||
|
||||
@@ -336,7 +336,6 @@ const de: Translation = {
|
||||
SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln',
|
||||
FIRMWARE_VERSION_INFO: 'Firmware-Versionsinformation',
|
||||
USER_PROFILE: 'Benutzerprofil',
|
||||
STORED_VERSIONS: 'Gespeicherte Versionen',
|
||||
ONLINE_HELP: 'Online-Hilfe',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Wichtige Nachrichten aktualisieren',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Für diese Aktualisierung ist ein Werksreset erforderlich. Stellen Sie sicher, dass Sie zuerst eine Systemsicherung herunterladen, bevor Sie fortfahren, und laden Sie diese Datei dann nach der Installation der neuen Version hoch.',
|
||||
@@ -353,9 +352,11 @@ const de: Translation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Klicken Sie, um eine Firmware-Datei (.bin/.md5) auszuwählen, oder drop ein',
|
||||
UPLOAD_MD5_RECEIVED: 'Prüfsumme empfangen, jetzt die Firmware-Datei (.bin) hochladen',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 stimmt überein, Firmware wird installiert...',
|
||||
RECOVERY: 'Wiederherstellung',
|
||||
RECOVERY_APP: 'Wiederherstellungs-App',
|
||||
RECOVERY_NOT_INSTALLED: 'Nicht installiert',
|
||||
RECOVERY_INSTALLED: 'Wiederherstellungs-Firmware installiert'
|
||||
RECOVERY_INSTALLED: 'Wiederherstellungs-Firmware installiert',
|
||||
RESTART_INTO_RECOVERY: 'Neustart in die Wiederherstellungs-App',
|
||||
RECOVERY_NOT_FOUND: 'Wiederherstellungs-App online nicht gefunden'
|
||||
};
|
||||
|
||||
export default de;
|
||||
|
||||
@@ -336,7 +336,6 @@ const en: Translation = {
|
||||
SWITCH_RELEASE_TYPE: 'Switch to {0} release',
|
||||
FIRMWARE_VERSION_INFO: 'Firmware Version Information',
|
||||
USER_PROFILE: 'User Profile',
|
||||
STORED_VERSIONS: 'Stored Versions',
|
||||
ONLINE_HELP: 'online help',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Important Messages',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you first download a System Backup before continuing, and then upload this file after the new version is installed.',
|
||||
@@ -353,9 +352,11 @@ const en: Translation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Click to select, or drop a firmware .bin/.md5 file here',
|
||||
UPLOAD_MD5_RECEIVED: 'Digest received, now upload the firmware .bin file',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 matches, installing firmware...',
|
||||
RECOVERY: 'Recovery',
|
||||
RECOVERY_APP: 'Recovery App',
|
||||
RECOVERY_NOT_INSTALLED: 'Not installed',
|
||||
RECOVERY_INSTALLED: 'Recovery firmware installed'
|
||||
RECOVERY_INSTALLED: 'Recovery firmware installed',
|
||||
RESTART_INTO_RECOVERY: 'Restart into Recovery App',
|
||||
RECOVERY_NOT_FOUND: 'Recovery App not found online'
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
||||
@@ -336,7 +336,6 @@ const fr: Translation = {
|
||||
SWITCH_RELEASE_TYPE: 'Passer à la version {0}',
|
||||
FIRMWARE_VERSION_INFO: 'Informations sur la version du firmware',
|
||||
USER_PROFILE: 'Profil utilisateur',
|
||||
STORED_VERSIONS: 'Versions stockées',
|
||||
ONLINE_HELP: 'aide en ligne',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Mettre à jour les messages importants',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous de télécharger une sauvegarde système avant de continuer, et de la charger après l\'installation de la nouvelle version.',
|
||||
@@ -353,9 +352,11 @@ const fr: Translation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Cliquez pour sélectionner, ou drop un fichier firmware .bin/.md5 ici',
|
||||
UPLOAD_MD5_RECEIVED: 'Empreinte reçue, téléversez maintenant le fichier firmware .bin',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 concordant, installation du firmware...',
|
||||
RECOVERY: 'Récupération',
|
||||
RECOVERY_APP: 'Application de récupération',
|
||||
RECOVERY_NOT_INSTALLED: 'Non installé',
|
||||
RECOVERY_INSTALLED: 'Firmware de récupération installé'
|
||||
RECOVERY_INSTALLED: 'Firmware de récupération installé',
|
||||
RESTART_INTO_RECOVERY: 'Redémarrer dans l\'application de récupération',
|
||||
RECOVERY_NOT_FOUND: 'Application de récupération introuvable en ligne'
|
||||
};
|
||||
|
||||
export default fr;
|
||||
|
||||
@@ -336,7 +336,6 @@ const it: Translation = {
|
||||
SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio',
|
||||
FIRMWARE_VERSION_INFO: 'Informazioni sulla versione del firmware',
|
||||
USER_PROFILE: 'Profilo utente',
|
||||
STORED_VERSIONS: 'Versioni memorizzate',
|
||||
ONLINE_HELP: 'aiuto online',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Aggiorna Messaggi Importanti',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di prima scaricare un backup del sistema prima di continuare, e poi caricare questo file dopo l\'installazione della nuova versione.',
|
||||
@@ -353,9 +352,11 @@ const it: Translation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Klik per selezionare, o drop un file firmware .bin/.md5 qui',
|
||||
UPLOAD_MD5_RECEIVED: 'Digest ricevuto, ora caricare il file firmware .bin',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 corrispondente, installazione firmware...',
|
||||
RECOVERY: 'Ripristino',
|
||||
RECOVERY_APP: 'App di ripristino',
|
||||
RECOVERY_NOT_INSTALLED: 'Non installato',
|
||||
RECOVERY_INSTALLED: 'Firmware di ripristino installato'
|
||||
RECOVERY_INSTALLED: 'Firmware di ripristino installato',
|
||||
RESTART_INTO_RECOVERY: 'Riavvia nell\'app di ripristino',
|
||||
RECOVERY_NOT_FOUND: 'App di ripristino non trovata online'
|
||||
};
|
||||
|
||||
export default it;
|
||||
|
||||
@@ -336,7 +336,6 @@ const nl: Translation = {
|
||||
SWITCH_RELEASE_TYPE: 'Switch naar {0} release',
|
||||
FIRMWARE_VERSION_INFO: 'Informatie over firmwareversie',
|
||||
USER_PROFILE: 'Gebruikersprofiel',
|
||||
STORED_VERSIONS: 'Opgeslagen versies',
|
||||
ONLINE_HELP: 'online help',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Belangrijke Berichten',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u eerst een Systeem Backup download voordat u doorgaat, en upload deze file na de installatie van de nieuwe versie.',
|
||||
@@ -353,9 +352,11 @@ const nl: Translation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Klik voor selecteren, of drop een firmware .bin/.md5 bestand hier',
|
||||
UPLOAD_MD5_RECEIVED: 'Digest ontvangen, upload nu het firmware .bin-bestand',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 komt overeen, firmware wordt geïnstalleerd...',
|
||||
RECOVERY: 'Herstel',
|
||||
RECOVERY_APP: 'Herstel-app',
|
||||
RECOVERY_NOT_INSTALLED: 'Niet geïnstalleerd',
|
||||
RECOVERY_INSTALLED: 'Herstelfirmware geïnstalleerd'
|
||||
RECOVERY_INSTALLED: 'Herstelfirmware geïnstalleerd',
|
||||
RESTART_INTO_RECOVERY: 'Herstart naar de herstel-app',
|
||||
RECOVERY_NOT_FOUND: 'Herstel-app niet online gevonden'
|
||||
};
|
||||
|
||||
export default nl;
|
||||
|
||||
@@ -336,7 +336,6 @@ const no: Translation = {
|
||||
SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse',
|
||||
FIRMWARE_VERSION_INFO: 'Informasjon om firmwareversjon',
|
||||
USER_PROFILE: 'Brukerprofil',
|
||||
STORED_VERSIONS: 'Lagret versjoner',
|
||||
ONLINE_HELP: 'online hjelp',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Oppdater viktige meldinger',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du først lastet ned en System Backup før du fortsetter, og last denne filen etter at den nye versjonen er installert.',
|
||||
@@ -353,9 +352,11 @@ const no: Translation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Klikk for å velge, eller drop en firmware .bin/.md5 her',
|
||||
UPLOAD_MD5_RECEIVED: 'MD5 mottatt, last nå opp firmware .bin-filen',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 stemmer, firmware installeres...',
|
||||
RECOVERY: 'Gjenoppretting',
|
||||
RECOVERY_APP: 'Gjenopprettingsapp',
|
||||
RECOVERY_NOT_INSTALLED: 'Ikke installert',
|
||||
RECOVERY_INSTALLED: 'Gjenopprettingsfirmware installert'
|
||||
RECOVERY_INSTALLED: 'Gjenopprettingsfirmware installert',
|
||||
RESTART_INTO_RECOVERY: 'Start på nytt i gjenopprettingsappen',
|
||||
RECOVERY_NOT_FOUND: 'Fant ikke gjenopprettingsappen på nettet'
|
||||
};
|
||||
|
||||
export default no;
|
||||
|
||||
@@ -336,7 +336,6 @@ const pl: BaseTranslation = {
|
||||
SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie',
|
||||
FIRMWARE_VERSION_INFO: 'Informacje o wersji firmware',
|
||||
USER_PROFILE: 'Profil użytkownika',
|
||||
STORED_VERSIONS: 'Zapisane wersje',
|
||||
ONLINE_HELP: 'pomoc online',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizuj ważne wiadomości',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że najpierw pobierzesz kopię zapasową systemu przed kontynuowaniem, a następnie przesuń tę plik po zainstalowaniu nowej wersji.',
|
||||
@@ -353,9 +352,11 @@ const pl: BaseTranslation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Kliknij, aby wybrać, lub drop plik firmware .bin/.md5 tutaj',
|
||||
UPLOAD_MD5_RECEIVED: 'Suma MD5 odebrana, teraz wgraj plik firmware .bin',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 zgodne, instalowanie firmware...',
|
||||
RECOVERY: 'Odzyskiwanie',
|
||||
RECOVERY_APP: 'Aplikacja odzyskiwania',
|
||||
RECOVERY_NOT_INSTALLED: 'Nie zainstalowano',
|
||||
RECOVERY_INSTALLED: 'Zainstalowano firmware odzyskiwania'
|
||||
RECOVERY_INSTALLED: 'Zainstalowano firmware odzyskiwania',
|
||||
RESTART_INTO_RECOVERY: 'Uruchom ponownie w aplikacji odzyskiwania',
|
||||
RECOVERY_NOT_FOUND: 'Nie znaleziono aplikacji odzyskiwania online'
|
||||
};
|
||||
|
||||
export default pl;
|
||||
|
||||
@@ -336,7 +336,6 @@ const sk: Translation = {
|
||||
SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu',
|
||||
FIRMWARE_VERSION_INFO: 'Informácie o verzii firmware',
|
||||
USER_PROFILE: 'Profil používateľa',
|
||||
STORED_VERSIONS: 'Uložené verzie',
|
||||
ONLINE_HELP: 'online pomoc',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovať dôležité správy',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že najprv stiahnete systémovú zálohu pred pokračovaním, a potom nahrajte tento súbor po instalácii novej verzie.',
|
||||
@@ -353,9 +352,11 @@ const sk: Translation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Kliknite, aby ste vybrali, alebo drop súbor firmware .bin/.md5 sem',
|
||||
UPLOAD_MD5_RECEIVED: 'Suma MD5 prijatá, teraz nahrajte firmware .bin',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 súhlasí, firmvér sa inštaluje...',
|
||||
RECOVERY: 'Obnova',
|
||||
RECOVERY_APP: 'Aplikácia na obnovu',
|
||||
RECOVERY_NOT_INSTALLED: 'Nenainštalované',
|
||||
RECOVERY_INSTALLED: 'Firmvér obnovy nainštalovaný'
|
||||
RECOVERY_INSTALLED: 'Firmvér obnovy nainštalovaný',
|
||||
RESTART_INTO_RECOVERY: 'Reštartovať do aplikácie na obnovu',
|
||||
RECOVERY_NOT_FOUND: 'Aplikácia na obnovu sa online nenašla'
|
||||
};
|
||||
|
||||
export default sk;
|
||||
|
||||
@@ -336,7 +336,6 @@ const sv: Translation = {
|
||||
SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva',
|
||||
FIRMWARE_VERSION_INFO: 'Information om firmwareversion',
|
||||
USER_PROFILE: 'Användarprofil',
|
||||
STORED_VERSIONS: 'Lagrad versioner',
|
||||
ONLINE_HELP: 'online hjälp',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Uppdatera viktiga meddelanden',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du först laddar ned en System Backup innan du fortsätter, och ladda upp denna fil efter att den nya versionen är installerad.',
|
||||
@@ -353,9 +352,11 @@ const sv: Translation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Klicka för att välja, eller drop en firmware .bin/.md5 här',
|
||||
UPLOAD_MD5_RECEIVED: 'MD5 mottagen, ladda nu upp firmware .bin-filen',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 stämmer, firmware installeras...',
|
||||
RECOVERY: 'Återställning',
|
||||
RECOVERY_APP: 'Återställningsapp',
|
||||
RECOVERY_NOT_INSTALLED: 'Inte installerad',
|
||||
RECOVERY_INSTALLED: 'Återställningsfirmware installerad'
|
||||
RECOVERY_INSTALLED: 'Återställningsfirmware installerad',
|
||||
RESTART_INTO_RECOVERY: 'Starta om i återställningsappen',
|
||||
RECOVERY_NOT_FOUND: 'Återställningsappen hittades inte online'
|
||||
};
|
||||
|
||||
export default sv;
|
||||
|
||||
@@ -336,7 +336,6 @@ const tr: Translation = {
|
||||
SWITCH_RELEASE_TYPE: '{0} sürümüne geç',
|
||||
FIRMWARE_VERSION_INFO: 'Firmware Sürüm Bilgisi',
|
||||
USER_PROFILE: 'Kullanıcı Profili',
|
||||
STORED_VERSIONS: 'Kaydedilmiş Sürümler',
|
||||
ONLINE_HELP: 'online yardım',
|
||||
UPGRADE_IMPORTANT_MESSAGES: 'Önemli Mesajları Güncelle',
|
||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Bu güncelleme továrnı ayarlarını gerektirir. Yapılandırmanızı ve ayarlarınızı önce yedekleyin ve ardından yeni sürüm yüklendikten sonra yükleyin.',
|
||||
@@ -353,9 +352,11 @@ const tr: Translation = {
|
||||
UPLOAD_FIRMWARE_TEXT: 'Bir firmware .bin dosyası, buraya drop yapın',
|
||||
UPLOAD_MD5_RECEIVED: 'Özet alındı, şimdi firmware .bin dosyasını yükleyin',
|
||||
UPLOAD_MD5_MATCHED: 'MD5 eşleşti, firmware yükleniyor...',
|
||||
RECOVERY: 'Kurtarma',
|
||||
RECOVERY_APP: 'Kurtarma Uygulaması',
|
||||
RECOVERY_NOT_INSTALLED: 'Yüklü değil',
|
||||
RECOVERY_INSTALLED: 'Kurtarma firmware yüklendi'
|
||||
RECOVERY_INSTALLED: 'Kurtarma firmware yüklendi',
|
||||
RESTART_INTO_RECOVERY: 'Kurtarma uygulamasına yeniden başlat',
|
||||
RECOVERY_NOT_FOUND: 'Kurtarma uygulaması çevrimiçi bulunamadı'
|
||||
};
|
||||
|
||||
export default tr;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type ReactNode, useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import HealthAndSafetyIcon from '@mui/icons-material/HealthAndSafety';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import MemoryIcon from '@mui/icons-material/Memory';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
@@ -17,11 +18,16 @@ import {
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
@@ -38,6 +44,9 @@ import type { RecoveryPartition, RecoveryStatus } from './types';
|
||||
|
||||
const formatMB = (bytes: number) => `${(bytes / 1048576).toFixed(2)} MB`;
|
||||
|
||||
const formatInstallDate = (epoch: number) =>
|
||||
epoch > 0 ? new Date(epoch * 1000).toLocaleString() : 'unknown';
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
@@ -81,13 +90,27 @@ const SectionTitle = ({ children }: { children: ReactNode }) => (
|
||||
</Typography>
|
||||
);
|
||||
|
||||
// same look as the version info dialog in the EMS-ESP app
|
||||
const InfoRow = ({ label, value }: { label: string; value: ReactNode }) => (
|
||||
<Box sx={{ display: 'flex', py: 0.5 }}>
|
||||
<Typography sx={{ width: 160, flexShrink: 0 }} color="text.secondary">
|
||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{
|
||||
color: 'lightblue',
|
||||
borderBottom: 'none',
|
||||
pr: 1,
|
||||
py: 0.5,
|
||||
fontSize: 13,
|
||||
width: 140
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography>{value}</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||
{value}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
interface DashboardProps {
|
||||
@@ -98,6 +121,7 @@ interface DashboardProps {
|
||||
|
||||
const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
||||
const [confirmBoot, setConfirmBoot] = useState<RecoveryPartition>();
|
||||
const [infoPartition, setInfoPartition] = useState<RecoveryPartition>();
|
||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState(false);
|
||||
const [pickedTarget, setPickedTarget] = useState<string>();
|
||||
const uploadTarget = pickedTarget ?? status.upload_target;
|
||||
@@ -240,6 +264,16 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
||||
label="unconfirmed"
|
||||
/>
|
||||
)}
|
||||
{p.valid && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setInfoPartition(p)}
|
||||
aria-label="Firmware version info"
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
@@ -287,7 +321,12 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
||||
|
||||
<SectionContent>
|
||||
<SectionTitle>System</SectionTitle>
|
||||
<InfoRow label="Recovery version" value={'v' + status.recovery_version} />
|
||||
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
|
||||
<TableBody>
|
||||
<InfoRow
|
||||
label="Recovery version"
|
||||
value={'v' + status.recovery_version}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Chip"
|
||||
value={`${status.chip}, ${formatMB(status.flash_size)} flash${status.psram_size ? ', ' + formatMB(status.psram_size) + ' PSRAM' : ''}`}
|
||||
@@ -327,6 +366,8 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
||||
}
|
||||
/>
|
||||
<InfoRow label="Hostname" value={status.hostname} />
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<ButtonRow>
|
||||
{!status.disable_reset && (
|
||||
@@ -342,7 +383,7 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
||||
<Button
|
||||
startIcon={<RestartAltIcon />}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
color="success"
|
||||
onClick={() => void doRestart(status.boot)}
|
||||
>
|
||||
Restart{status.boot !== status.running ? ` (boots ${status.boot})` : ''}
|
||||
@@ -351,6 +392,7 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
||||
startIcon={<LogoutIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
sx={{ float: 'right' }}
|
||||
onClick={onSignOut}
|
||||
>
|
||||
Sign Out
|
||||
@@ -424,6 +466,50 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={!!infoPartition}
|
||||
onClose={() => setInfoPartition(undefined)}
|
||||
>
|
||||
<DialogTitle>Firmware Version Info</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{infoPartition && (
|
||||
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
|
||||
<TableBody>
|
||||
<InfoRow
|
||||
label="Version"
|
||||
value={
|
||||
infoPartition.version ? 'v' + infoPartition.version : 'unknown'
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Type"
|
||||
value={infoPartition.factory ? 'Recovery App' : 'EMS-ESP Firmware'}
|
||||
/>
|
||||
<InfoRow label="Partition" value={infoPartition.label} />
|
||||
<InfoRow
|
||||
label="Size"
|
||||
value={`${formatMB(infoPartition.image_size)} of ${formatMB(infoPartition.size)}`}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Install Date"
|
||||
value={formatInstallDate(infoPartition.install_date)}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => setInfoPartition(undefined)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||
"prettier": "^3.9.9"
|
||||
"prettier": "^3.9.10"
|
||||
},
|
||||
"packageManager": "pnpm@12.11.2"
|
||||
}
|
||||
|
||||
Generated
+8
-8
@@ -175,10 +175,10 @@ importers:
|
||||
devDependencies:
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2(prettier@3.9.9)
|
||||
version: 6.0.2(prettier@3.9.10)
|
||||
prettier:
|
||||
specifier: ^3.9.9
|
||||
version: 3.9.9
|
||||
specifier: ^3.9.10
|
||||
version: 3.9.10
|
||||
|
||||
packages:
|
||||
|
||||
@@ -303,8 +303,8 @@ packages:
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
prettier@3.9.9:
|
||||
resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==}
|
||||
prettier@3.9.10:
|
||||
resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
@@ -373,7 +373,7 @@ snapshots:
|
||||
|
||||
'@msgpack/msgpack@3.1.3': {}
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.9)':
|
||||
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.10)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.29.8
|
||||
'@babel/parser': 7.29.9
|
||||
@@ -383,7 +383,7 @@ snapshots:
|
||||
lodash-es: 4.18.1
|
||||
minimatch: 9.0.9
|
||||
parse-imports-exports: 0.2.4
|
||||
prettier: 3.9.9
|
||||
prettier: 3.9.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -421,4 +421,4 @@ snapshots:
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
prettier@3.9.9: {}
|
||||
prettier@3.9.10: {}
|
||||
|
||||
@@ -116,8 +116,8 @@ let system_status = {
|
||||
psram: true,
|
||||
psram_size: 8189,
|
||||
free_psram: 8166,
|
||||
// recovery_version: '', // upload an EMS-ESP-Recovery-*.bin to set it
|
||||
recovery_version: '1.0.0',
|
||||
recovery_version: '', // upload an EMS-ESP-Recovery-*.bin to set it
|
||||
// recovery_version: '1.0.0',
|
||||
has_partition: true,
|
||||
partitions: [
|
||||
{
|
||||
@@ -5447,6 +5447,10 @@ router
|
||||
} else if (action === 'uploadURL') {
|
||||
// upload URL
|
||||
console.log('upload File from URL', content.param);
|
||||
if (String(content.param).toLowerCase().includes('recovery')) {
|
||||
// the recovery app is installed without a restart
|
||||
system_status.recovery_version = '1.0.0';
|
||||
}
|
||||
return status(200);
|
||||
} else if (action === 'resetMQTT') {
|
||||
// reset MQTT
|
||||
|
||||
+42
-13
@@ -11,27 +11,37 @@
|
||||
; -D EMSESP_DE_ONLY ; only DE translated entity names
|
||||
; -D EMSESP_EN_ONLY ; only EN translated entity names
|
||||
; -D EMSESP_PINGTEST ; send log message every 1/2 second
|
||||
; -D EMSESP_GATEWAY_TX_HOLD ; hold Tx while a K30RF/KM200 gateway is syncing, see issue #3094
|
||||
; -D EMSESP_TX_PRE_DELAY=1000 ; wait this many us before each Tx so we don't overlap the master's break, see issue #3094
|
||||
|
||||
; my_build_flags = -DEMSESP_TEST -DEMSESP_DEBUG
|
||||
; my_build_flags = -DEMSESP_DEBUG -DEMSESP_TEST -DEMSESP_PINGTEST
|
||||
; my_build_flags = -DEMSESP_DEBUG -DCORE_DEBUG_LEVEL=5
|
||||
; my_build_flags = -DEMSESP_EN_ONLY -DEMSESP_TEST -DCORE_DEBUG_LEVEL=5 -DARDUINO_USB_CDC_ON_BOOT=1
|
||||
my_build_flags =
|
||||
; my_build_flags = -DEMSESP_EN_ONLY
|
||||
; my_build_flags = -D EMSESP_GATEWAY_TX_HOLD -D EMSESP_TX_PRE_DELAY=1000
|
||||
my_build_flags = -DEMSESP_DEBUG
|
||||
; my_build_flags =
|
||||
|
||||
[platformio]
|
||||
default_envs = s_16M_P ; BBQKees E32V2
|
||||
; default_envs = build-webUI ; build the web interface only
|
||||
; default_envs = s3_16M_P ; BBQKees S3
|
||||
; default_envs = s_4M ; BBQKees older S32, 4MB no psram
|
||||
; default_envs = s_16M ; BBQKees newer S32 V2, 16MB no psram
|
||||
; default_envs = c6 ; XIAO ESP32C
|
||||
; default_envs = debug
|
||||
default_envs = s_16M_P, recovery_s_16M_P, build-recovery, build-webUI
|
||||
; default_envs = s_16M_P ; BBQKees E32V2
|
||||
; default_envs = s_16M_P_custom ; BBQKees E32V2 with custom Arduino/IDF libs
|
||||
; default_envs = recovery_s_16M_P ; recovery firmware
|
||||
; default_envs = crashtest_s_16M_P ; crash test
|
||||
; default_envs = build-webUI ; build the main ems-esp webUI only
|
||||
; default_envs = build-recovery ; build the recovery WebUI only
|
||||
; default_envs = s3_16M_P ; BBQKees S3 firmware
|
||||
; default_envs = s_4M ; firmware forBBQKees older S32, 4MB no psram
|
||||
; default_envs = s_16M ; firmware for BBQKees newer S32 V2, 16MB no psram
|
||||
; default_envs = c6 ; firmware for XIAO ESP32C
|
||||
; default_envs = debug ; see below
|
||||
|
||||
[env]
|
||||
|
||||
; uncomment if you want to upload the firmware via OTA (must have upload_protocol = custom)
|
||||
extra_scripts =
|
||||
pre:scripts/build_interface.py ; builds the WebUI
|
||||
scripts/rename_fw.py ; renames the firmware .bin file - comment out if not needed
|
||||
post:scripts/rename_fw.py ; renames the firmware .bin file
|
||||
scripts/upload.py ; optionally upload the firmware via OTA (must have upload_protocol = custom)
|
||||
|
||||
; set the username and password for the admin account (default password is admin)
|
||||
@@ -40,13 +50,33 @@ custom_password = admin
|
||||
|
||||
; ** upload settings **
|
||||
; for direct USB, which is the default you can set an optional port for example:
|
||||
; upload_port = /dev/ttyUSB0 or COM3
|
||||
; upload_speed = 921600
|
||||
; upload_port = /dev/ttyUSB0
|
||||
; upload_port = /dev/ttyACM0
|
||||
|
||||
; CH340G bridge tops out here reliably; drop back to 460800 if uploads fail
|
||||
; 2000000 is the max speed for the CH340G bridge
|
||||
upload_speed = 921600
|
||||
|
||||
; for direct via HTTP (OTA) set:
|
||||
; upload_protocol = custom
|
||||
; custom_emsesp_ip = <ip address> or ems-esp.local
|
||||
|
||||
upload_protocol = custom
|
||||
|
||||
custom_emsesp_ip = ems-esp.local ; E32V2.2 (test)
|
||||
; custom_emsesp_ip = ems-espP.local ; E32V2.2 (prod)
|
||||
; custom_emsesp_ip = ems-esps32.local ; S32, 4MB no psramS32
|
||||
; custom_emsesp_ip = ems-espS3.local ; ems-espS3
|
||||
|
||||
; lib_deps =
|
||||
; bblanchon/ArduinoJson @ 7.4.3
|
||||
; ESP32Async/AsyncTCP @ 3.4.10
|
||||
; ESP32Async/ESPAsyncWebServer @ 3.11.0
|
||||
; https://github.com/mobizt/ReadyMail.git @ 0.4.0
|
||||
; https://github.com/mobizt/ESP_SSLClient.git @ 3.1.3
|
||||
; https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8
|
||||
; ; file://${PROJECT_DIR}/../modules/EMS-ESP-Modules
|
||||
|
||||
; ** debug settings **
|
||||
; to be used with an ESP32-S3 which has an onboard JTAG, connect the OTG USB port to the PC
|
||||
; if using an external JTAG board like the ESP-PROG set debug_tool = esp-prog, and use zadig to set the correct driver for the USB port
|
||||
@@ -69,4 +99,3 @@ debug_init_break =
|
||||
upload_speed = 2000000
|
||||
monitor_speed = 115200
|
||||
extra_scripts =
|
||||
; debug_build_flags = -Og -g2 -ggdb2
|
||||
@@ -116,6 +116,14 @@ extra_scripts = pre:scripts/build_interface.py
|
||||
; Exclude all source files so the C code doesn't build
|
||||
build_src_filter = -<*>
|
||||
|
||||
; builds the recovery web interface only (recovery/WWWData.h), not the recovery firmware
|
||||
[env:build-recovery]
|
||||
platform = native
|
||||
targets = build
|
||||
extra_scripts = pre:scripts/build_recovery_interface.py
|
||||
; Exclude all source files so the C code doesn't build
|
||||
build_src_filter = -<*>
|
||||
|
||||
;
|
||||
; Builds for different board types
|
||||
; If you're building for a single target environment, we recommend creating a pio_local.ini (see example file)
|
||||
|
||||
+78
-17
@@ -28,6 +28,7 @@
|
||||
#include <esp_partition.h>
|
||||
#include <esp_random.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "WWWData.h" // generated by `pnpm build-recovery` in ./interface
|
||||
@@ -37,8 +38,8 @@
|
||||
#define FACTORY_AP_CHANNEL 1
|
||||
#endif
|
||||
|
||||
static constexpr size_t MIN_FIRMWARE_SIZE = 512 * 1024;
|
||||
static constexpr uint32_t RESTART_DELAY_MS = 500;
|
||||
static constexpr size_t MIN_FIRMWARE_SIZE = 512 * 1024; // smallest .bin accepted by the upload, anything smaller is rejected
|
||||
static constexpr uint32_t RESTART_DELAY_MS = 500; // delay before restarting, so the HTTP response is sent first
|
||||
static constexpr uint32_t ETH_WAIT_MS = 15000; // how long Ethernet gets to obtain an address before WiFi is tried
|
||||
static constexpr char NVS_NAMESPACE[] = "ems-esp";
|
||||
static constexpr char NVS_FRESH_FW[] = "fresh_firmware"; // EMSESP_NVS_BOOT_NEW_FIRMWARE in emsesp.h
|
||||
@@ -113,6 +114,8 @@ static bool ethConnected();
|
||||
static int upload_error = 0;
|
||||
static bool upload_done = false;
|
||||
static String upload_target;
|
||||
static size_t upload_size = 0; // content length, for the progress
|
||||
static size_t upload_step = 0; // last progress step logged, in tens of percent
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// settings (read-only)
|
||||
@@ -256,6 +259,7 @@ static void loadRecoveryReason() {
|
||||
if (nvs.isKey(NVS_REASON)) {
|
||||
recovery_reason = nvs.getString(NVS_REASON);
|
||||
nvs.remove(NVS_REASON);
|
||||
LOG("EMS-ESP switched to the recovery: %s", recovery_reason.c_str());
|
||||
}
|
||||
nvs.end();
|
||||
}
|
||||
@@ -293,6 +297,10 @@ static void loadPartitions() {
|
||||
|
||||
if (part == running) {
|
||||
info.version = RECOVERY_VERSION;
|
||||
// EMS-ESP stores the install date when it installs the recovery. Not set if it was flashed over serial
|
||||
if (has_nvs) {
|
||||
info.install_date = nvs.getULong(("d_" + info.label).c_str(), 0);
|
||||
}
|
||||
} else if (info.valid) {
|
||||
// EMS-ESP stores the version and install date of each partition in NVS, keyed by the label
|
||||
if (has_nvs && nvs.isKey(part->label)) {
|
||||
@@ -305,6 +313,17 @@ static void loadPartitions() {
|
||||
}
|
||||
}
|
||||
|
||||
LOG("Partition %s: %s%s, %uKB at 0x%x%s%s%s%s",
|
||||
info.label.c_str(),
|
||||
info.factory ? "factory, " : "",
|
||||
info.valid ? "valid" : "empty",
|
||||
static_cast<unsigned>(info.size / 1024),
|
||||
static_cast<unsigned>(info.address),
|
||||
info.version.isEmpty() ? "" : ", v",
|
||||
info.version.c_str(),
|
||||
info.ota_state.isEmpty() ? "" : ", ota state ",
|
||||
info.ota_state.c_str());
|
||||
|
||||
partitions.push_back(info);
|
||||
}
|
||||
esp_partition_iterator_release(it);
|
||||
@@ -353,6 +372,7 @@ static const esp_partition_t * uploadTarget(AsyncWebServerRequest * request) {
|
||||
static void markFreshFirmware(const String & label) {
|
||||
Preferences nvs;
|
||||
if (!openNvs(nvs, false)) {
|
||||
LOG("Cannot open NVS, EMS-ESP won't know it's a new firmware");
|
||||
return;
|
||||
}
|
||||
nvs.putBool(NVS_FRESH_FW, true);
|
||||
@@ -381,10 +401,11 @@ static String generateToken() {
|
||||
}
|
||||
|
||||
static bool isAuthorized(AsyncWebServerRequest * request) {
|
||||
if (session_token.isEmpty() || !request->hasHeader("Authorization")) {
|
||||
return false;
|
||||
const bool ok = !session_token.isEmpty() && request->hasHeader("Authorization") && request->header("Authorization") == "Bearer " + session_token;
|
||||
if (!ok) {
|
||||
LOG("Unauthorized request to %s", request->url().c_str());
|
||||
}
|
||||
return request->header("Authorization") == "Bearer " + session_token;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -529,6 +550,7 @@ static void handleSetBoot(AsyncWebServerRequest * request, JsonVariant & json) {
|
||||
const String label = json["partition"] | "";
|
||||
const esp_partition_t * part = findAppPartition(label.c_str());
|
||||
if (part == nullptr) {
|
||||
LOG("Cannot boot from %s: no such partition", label.c_str());
|
||||
request->send(404);
|
||||
return;
|
||||
}
|
||||
@@ -569,6 +591,7 @@ static void handleFactoryReset(AsyncWebServerRequest * request) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG("Factory reset requested");
|
||||
const bool ok = LittleFS.format(); // the only place the recovery writes to the filesystem
|
||||
LOG("Factory reset %s", ok ? "done" : "failed");
|
||||
if (!ok) {
|
||||
@@ -580,12 +603,12 @@ static void handleFactoryReset(AsyncWebServerRequest * request) {
|
||||
restart_at = millis() + RESTART_DELAY_MS;
|
||||
}
|
||||
|
||||
static void uploadFail(AsyncWebServerRequest * request, int code) {
|
||||
static void uploadFail(AsyncWebServerRequest * request, int code, const char * reason) {
|
||||
if (upload_error) {
|
||||
return;
|
||||
}
|
||||
upload_error = code;
|
||||
LOG("Upload error %d", code);
|
||||
LOG("Upload error %d: %s", code, reason);
|
||||
request->send(code);
|
||||
if (code == 406) {
|
||||
request->client()->close();
|
||||
@@ -596,10 +619,12 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
|
||||
if (!index) {
|
||||
upload_error = 0;
|
||||
upload_done = false;
|
||||
upload_size = 0;
|
||||
upload_step = 0;
|
||||
upload_target.clear();
|
||||
|
||||
if (!isAuthorized(request)) {
|
||||
uploadFail(request, 403);
|
||||
uploadFail(request, 403, "not signed in");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -611,41 +636,51 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
|
||||
if (ota.setMd5(data, len)) {
|
||||
LOG("MD5 digest received (%s)", ota.md5());
|
||||
} else {
|
||||
uploadFail(request, 406);
|
||||
uploadFail(request, 406, "invalid MD5 digest");
|
||||
}
|
||||
return;
|
||||
default:
|
||||
// the recovery app only flashes firmware, it never touches the settings
|
||||
ota.clearMd5();
|
||||
LOG("Unsupported file %s (%u bytes)", filename.c_str(), static_cast<unsigned>(filesize));
|
||||
uploadFail(request, 406);
|
||||
uploadFail(request, 406, "unsupported file");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!OtaUpdater::isCompatibleFirmware(data, len)) {
|
||||
uploadFail(request, 503);
|
||||
uploadFail(request, 503, "not a firmware image for this chip");
|
||||
return;
|
||||
}
|
||||
if (OtaUpdater::isRecoveryFirmware(data, len)) {
|
||||
uploadFail(request, 409);
|
||||
uploadFail(request, 409, "the recovery firmware can't be installed from the recovery");
|
||||
return;
|
||||
}
|
||||
|
||||
const esp_partition_t * target = uploadTarget(request);
|
||||
if (target == nullptr) {
|
||||
uploadFail(request, 400);
|
||||
if (request->hasParam("partition")) {
|
||||
LOG("Invalid upload target %s", request->getParam("partition")->value().c_str());
|
||||
}
|
||||
uploadFail(request, 400, "no valid target partition");
|
||||
return;
|
||||
}
|
||||
if (!ota.beginFirmware(filesize, target)) {
|
||||
LOG("Cannot write to %s: %s", target->label, ota.errorString());
|
||||
uploadFail(request, 507);
|
||||
uploadFail(request, 507, "cannot start the update");
|
||||
return;
|
||||
}
|
||||
upload_target = target->label;
|
||||
LOG("Uploading %s (%uKB) to %s%s", filename.c_str(), static_cast<unsigned>(filesize / 1024), target->label, ota.md5Applied() ? ", MD5 check enabled" : "");
|
||||
upload_size = filesize;
|
||||
LOG("Uploading %s (%uKB) to %s (%s)%s",
|
||||
filename.c_str(),
|
||||
static_cast<unsigned>(filesize / 1024),
|
||||
target->label,
|
||||
request->hasParam("partition") ? "selected" : "default target",
|
||||
ota.md5Applied() ? ", MD5 check enabled" : "");
|
||||
|
||||
request->onDisconnect([] {
|
||||
if (!upload_done && !upload_target.isEmpty()) {
|
||||
LOG("Upload aborted at %uKB, the client disconnected", static_cast<unsigned>(ota.progress() / 1024));
|
||||
ota.abort();
|
||||
}
|
||||
});
|
||||
@@ -658,17 +693,27 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
|
||||
if (!ota.write(data, len)) {
|
||||
LOG("Write failed at %u: %s", static_cast<unsigned>(ota.progress()), ota.errorString());
|
||||
ota.abort();
|
||||
uploadFail(request, 500);
|
||||
uploadFail(request, 500, "write failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// log the progress every 10%, the content length is a little more than the image so it may stop just short of 100%
|
||||
if (upload_size > 0) {
|
||||
const size_t step = std::min<size_t>(ota.progress() * 10 / upload_size, 10);
|
||||
if (step > upload_step) {
|
||||
upload_step = step;
|
||||
LOG("Uploading to %s, %u%%", upload_target.c_str(), static_cast<unsigned>(step * 10));
|
||||
}
|
||||
}
|
||||
|
||||
if (final) {
|
||||
// on success this also makes the new partition the boot partition
|
||||
if (!ota.end()) {
|
||||
LOG("Upload failed: %s", ota.errorString());
|
||||
uploadFail(request, 500);
|
||||
uploadFail(request, 500, "image check failed");
|
||||
return;
|
||||
}
|
||||
LOG("Upload finished, the image is verified");
|
||||
upload_done = true;
|
||||
}
|
||||
}
|
||||
@@ -785,6 +830,17 @@ static bool ethConnected() {
|
||||
}
|
||||
|
||||
static void startNetwork() {
|
||||
// the reason is logged once per change, since a missing network disconnects again every few seconds
|
||||
WiFi.onEvent(
|
||||
[](arduino_event_id_t, arduino_event_info_t info) {
|
||||
static uint8_t last_reason = 0;
|
||||
if (info.wifi_sta_disconnected.reason != last_reason) {
|
||||
last_reason = info.wifi_sta_disconnected.reason;
|
||||
LOG("WiFi disconnected, reason %d", last_reason);
|
||||
}
|
||||
},
|
||||
ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
|
||||
|
||||
WiFi.persistent(false);
|
||||
WiFi.setHostname(settings.hostname.c_str()); // also the default for the Ethernet interface
|
||||
WiFi.mode(WIFI_AP);
|
||||
@@ -826,6 +882,9 @@ void setup() {
|
||||
Serial.begin(115200);
|
||||
LOG("EMS-ESP Recovery v%s", RECOVERY_VERSION);
|
||||
|
||||
const esp_partition_t * boot = esp_ota_get_boot_partition();
|
||||
LOG("Reset reason: %s, running %s, next boot %s", resetReasonName(esp_reset_reason()), esp_ota_get_running_partition()->label, boot ? boot->label : "unknown");
|
||||
|
||||
loadSettings();
|
||||
loadRecoveryReason();
|
||||
loadPartitions();
|
||||
@@ -841,6 +900,8 @@ void loop() {
|
||||
was_connected = WiFi.isConnected();
|
||||
if (was_connected) {
|
||||
LOG("WiFi connected, IP %s", WiFi.localIP().toString().c_str());
|
||||
} else {
|
||||
LOG("WiFi connection lost");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,12 @@ def build_webUI(*args, **kwargs):
|
||||
env.Exit(0)
|
||||
|
||||
|
||||
# The web interface is only built by the "build" target. Skip any other, e.g. `pio run -t upload` also runs this
|
||||
# environment when it's one of the default_envs, and it has nothing to upload
|
||||
if "build" not in [t.strip() for t in COMMAND_LINE_TARGETS]:
|
||||
print("Skipping the web interface build, the target is not 'build'")
|
||||
env.Exit(0)
|
||||
|
||||
# Create custom target that only runs the script and then exits, without continuing with the pio workflow
|
||||
env.AddCustomTarget(
|
||||
name="build",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Builds the Recovery WebUI into recovery/WWWData.h when it's missing or older than its sources
|
||||
# Builds the Recovery WebUI into recovery/WWWData.h when it's missing or older than its sources.
|
||||
# In the build-recovery environment it only builds the WebUI, always.
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
@@ -52,7 +53,29 @@ def build():
|
||||
env.Exit(1)
|
||||
|
||||
|
||||
if is_stale():
|
||||
if env.get("PIOENV") == "build-recovery":
|
||||
# the WebUI is only built by the "build" target. Skip any other, e.g. `pio run -t upload` also runs this environment
|
||||
# when it's one of the default_envs, and it has nothing to upload
|
||||
if "build" not in [t.strip() for t in COMMAND_LINE_TARGETS]:
|
||||
print("Skipping the Recovery WebUI build, the target is not 'build'")
|
||||
env.Exit(0)
|
||||
|
||||
# `pio run -e build-recovery` only builds the WebUI, always, and exits without building any firmware
|
||||
def build_recovery_webUI(*args, **kwargs):
|
||||
print("Building Recovery WebUI...")
|
||||
build()
|
||||
print("Recovery WebUI build completed successfully!")
|
||||
env.Exit(0)
|
||||
|
||||
env.AddCustomTarget(
|
||||
name="build",
|
||||
dependencies=None,
|
||||
actions=[build_recovery_webUI],
|
||||
title="build recovery web interface",
|
||||
description="installs pnpm packages and builds the recovery web UI into recovery/WWWData.h",
|
||||
always_build=True,
|
||||
)
|
||||
elif is_stale():
|
||||
print("Building Recovery WebUI...")
|
||||
build()
|
||||
else:
|
||||
|
||||
+88
-10
@@ -568,7 +568,8 @@ void System::get_partition_info() {
|
||||
|
||||
void System::recovery_installed() {
|
||||
#ifdef EMSESP_HAS_RECOVERY
|
||||
// drop the version info of an EMS-ESP firmware that was in the factory partition before
|
||||
// drop the version info of an EMS-ESP firmware that was in the factory partition before. The recovery reports its
|
||||
// own version, but the install date is only known here, as it's not set when flashed over serial
|
||||
const esp_partition_t * factory = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, nullptr);
|
||||
if (factory != nullptr) {
|
||||
char c[20];
|
||||
@@ -576,8 +577,11 @@ void System::recovery_installed() {
|
||||
if (EMSESP::nvs_.isKey(factory->label)) {
|
||||
EMSESP::nvs_.remove(factory->label);
|
||||
}
|
||||
if (EMSESP::nvs_.isKey(c)) {
|
||||
EMSESP::nvs_.remove(c);
|
||||
const time_t now = time(nullptr);
|
||||
if (now > 1500000000L) {
|
||||
EMSESP::nvs_.putULong(c, now); // UTC epoch, read by the recovery app. Only if the clock is set (NTP)
|
||||
} else if (EMSESP::nvs_.isKey(c)) {
|
||||
EMSESP::nvs_.remove(c); // clock not set, the date of the previous firmware is wrong
|
||||
}
|
||||
partition_info_.erase(factory->label); // on the AsyncTCP task, same as the web handlers reading it
|
||||
}
|
||||
@@ -585,6 +589,19 @@ void System::recovery_installed() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// the partition info is read by the web handlers on the AsyncTCP task, so the update after a recovery firmware
|
||||
// download (which runs on the main loop) is deferred to be done there, on the next systemStatus request
|
||||
void System::recovery_installed_deferred() {
|
||||
recovery_installed_pending_ = true;
|
||||
}
|
||||
|
||||
void System::recovery_installed_apply() {
|
||||
if (recovery_installed_pending_) {
|
||||
recovery_installed_pending_ = false;
|
||||
recovery_installed();
|
||||
}
|
||||
}
|
||||
|
||||
std::string System::recovery_version() const {
|
||||
#ifdef EMSESP_HAS_RECOVERY
|
||||
esp_app_desc_t desc;
|
||||
@@ -3511,21 +3528,44 @@ bool System::uploadFirmwareURL(const char * url) {
|
||||
break;
|
||||
}
|
||||
|
||||
// the recovery firmware is recognized by its filename. It's written to the factory partition and
|
||||
// EMS-ESP keeps running, there is no restart
|
||||
#ifdef EMSESP_HAS_RECOVERY
|
||||
String file_name = saved_url.substring(saved_url.lastIndexOf('/') + 1);
|
||||
file_name.toLowerCase();
|
||||
const bool is_recovery = file_name.indexOf("recovery") >= 0;
|
||||
#else
|
||||
const bool is_recovery = false;
|
||||
#endif
|
||||
OtaUpdater ota; // only used for the recovery firmware
|
||||
|
||||
// check we have a valid size
|
||||
if (firmware_size < (int)MIN_FIRMWARE_SIZE) {
|
||||
if (firmware_size < (int)(is_recovery ? OtaUpdater::MIN_RECOVERY_FIRMWARE_SIZE : MIN_FIRMWARE_SIZE)) {
|
||||
LOG_ERROR("Firmware upload failed - invalid size");
|
||||
return false; // error
|
||||
}
|
||||
|
||||
OtaUpdater::confirmRunningApp();
|
||||
|
||||
if (is_recovery) {
|
||||
#ifdef EMSESP_HAS_RECOVERY
|
||||
const esp_partition_t * factory = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, nullptr);
|
||||
if (factory == nullptr || factory == esp_ota_get_running_partition()) {
|
||||
LOG_ERROR("The recovery firmware can't be installed on this board");
|
||||
return false; // error
|
||||
}
|
||||
if (!ota.beginFirmware(firmware_size, factory, false)) {
|
||||
LOG_ERROR("Recovery firmware upload failed - %s", ota.errorString());
|
||||
return false; // error
|
||||
}
|
||||
#endif
|
||||
} else if (!Update.begin(firmware_size)) {
|
||||
// check we have enough space for the upload in the ota partition
|
||||
if (!Update.begin(firmware_size)) {
|
||||
LOG_ERROR("Firmware upload failed - no space");
|
||||
return false; // error
|
||||
}
|
||||
|
||||
LOG_INFO("Firmware uploading (size: %dKB) over %s. Please wait...", firmware_size / 1024, is_https ? "HTTPS" : "HTTP");
|
||||
LOG_INFO("%s uploading (size: %dKB) over %s. Please wait...", is_recovery ? "Recovery firmware" : "Firmware", firmware_size / 1024, is_https ? "HTTPS" : "HTTP");
|
||||
|
||||
Shell::loop_all(); // flush log buffers so latest messages are shown in console
|
||||
|
||||
@@ -3543,7 +3583,11 @@ bool System::uploadFirmwareURL(const char * url) {
|
||||
// via the systemStatus action, which runs on the AsyncTCP task while we're blocked here
|
||||
if (EMSESP::system_.systemStatus() < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) {
|
||||
LOG_WARNING("Firmware upload cancelled at %u of %d bytes", (unsigned)total_read, firmware_size);
|
||||
if (is_recovery) {
|
||||
ota.abort();
|
||||
} else {
|
||||
Update.abort(); // release the OTA partition handle so a later attempt can start cleanly
|
||||
}
|
||||
ssl_client.stop(); // drop the connection
|
||||
saved_url.clear(); // prevent it from downloading again
|
||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
|
||||
@@ -3573,6 +3617,9 @@ bool System::uploadFirmwareURL(const char * url) {
|
||||
continue;
|
||||
}
|
||||
LOG_ERROR("Firmware upload failed - read stalled at %u of %d bytes", (unsigned)total_read, firmware_size);
|
||||
if (is_recovery) {
|
||||
ota.abort();
|
||||
}
|
||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||
return false;
|
||||
}
|
||||
@@ -3585,6 +3632,9 @@ bool System::uploadFirmwareURL(const char * url) {
|
||||
size_t n = stream->readBytes(buf, want);
|
||||
if (n == 0) {
|
||||
LOG_ERROR("Firmware upload failed - read returned 0 at %u of %d bytes", (unsigned)total_read, firmware_size);
|
||||
if (is_recovery) {
|
||||
ota.abort();
|
||||
}
|
||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||
return false;
|
||||
}
|
||||
@@ -3594,14 +3644,33 @@ bool System::uploadFirmwareURL(const char * url) {
|
||||
if (!magic_ok) {
|
||||
if (buf[0] != 0xE9) {
|
||||
LOG_ERROR("Firmware upload failed - bad magic byte 0x%02X (expected 0xE9, not an ESP32 firmware image?)", buf[0]);
|
||||
if (is_recovery) {
|
||||
ota.abort();
|
||||
}
|
||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||
return false;
|
||||
}
|
||||
// the recovery firmware must be for this chip, and really be the recovery
|
||||
if (is_recovery && !(OtaUpdater::isCompatibleFirmware(buf, n) && OtaUpdater::isRecoveryFirmware(buf, n))) {
|
||||
LOG_ERROR("Recovery firmware upload failed - not a recovery firmware image for this chip");
|
||||
if (is_recovery) {
|
||||
ota.abort();
|
||||
}
|
||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||
return false;
|
||||
}
|
||||
magic_ok = true;
|
||||
}
|
||||
|
||||
if (Update.write(buf, n) != n) {
|
||||
LOG_ERROR("Firmware upload failed - flash write error at %u of %d bytes: %s", (unsigned)total_read, firmware_size, Update.errorString());
|
||||
const bool written = is_recovery ? ota.write(buf, n) : (Update.write(buf, n) == n);
|
||||
if (!written) {
|
||||
LOG_ERROR("Firmware upload failed - flash write error at %u of %d bytes: %s",
|
||||
(unsigned)total_read,
|
||||
firmware_size,
|
||||
is_recovery ? ota.errorString() : Update.errorString());
|
||||
if (is_recovery) {
|
||||
ota.abort();
|
||||
}
|
||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||
return false;
|
||||
}
|
||||
@@ -3618,13 +3687,22 @@ bool System::uploadFirmwareURL(const char * url) {
|
||||
yield();
|
||||
}
|
||||
|
||||
if (!Update.end(true)) {
|
||||
LOG_ERROR("Firmware upload failed - %s", Update.errorString());
|
||||
if (!(is_recovery ? ota.end() : Update.end(true))) {
|
||||
LOG_ERROR("Firmware upload failed - %s", is_recovery ? ota.errorString() : Update.errorString());
|
||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||
return false; // error
|
||||
}
|
||||
|
||||
saved_url.clear(); // prevent from downloading again
|
||||
|
||||
if (is_recovery) {
|
||||
// no restart. The partition info is updated later on the AsyncTCP task, see recovery_installed_apply()
|
||||
EMSESP::system_.recovery_installed_deferred();
|
||||
LOG_INFO("Recovery firmware uploaded successfully");
|
||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_INFO("Firmware uploaded successfully. Restarting...");
|
||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);
|
||||
#endif
|
||||
|
||||
@@ -84,6 +84,8 @@ class System {
|
||||
static void check_crash_loop(); // call at boot once NVS is open, may restart into the factory partition
|
||||
|
||||
void recovery_installed(); // called on the AsyncTCP task after the recovery firmware was written
|
||||
void recovery_installed_deferred(); // called from the main loop after the recovery firmware was downloaded from a URL
|
||||
void recovery_installed_apply(); // called on the AsyncTCP task, handles the deferred call
|
||||
std::string recovery_version() const; // empty if there's no recovery firmware
|
||||
|
||||
// commands
|
||||
@@ -391,6 +393,8 @@ class System {
|
||||
|
||||
void set_partition_install_date();
|
||||
|
||||
volatile bool recovery_installed_pending_ = false; // set by the main loop, handled on the AsyncTCP task
|
||||
|
||||
// firmware is confirmed to the bootloader and the crash counter cleared once it has been up this long
|
||||
static constexpr uint32_t FIRMWARE_HEALTHY_UPTIME = 120; // seconds
|
||||
// consecutive crashes before falling back to the recovery firmware in the factory partition
|
||||
|
||||
@@ -46,6 +46,7 @@ WebStatusService::WebStatusService(AsyncWebServer * server, SecurityManager * se
|
||||
// This is also used for polling during the SystemMonitor to see if EMS-ESP is alive
|
||||
void WebStatusService::systemStatus(AsyncWebServerRequest * request) {
|
||||
EMSESP::system_.refreshHeapMem(); // refresh free heap and max alloc heap
|
||||
EMSESP::system_.recovery_installed_apply(); // update the partition info if the recovery firmware was just downloaded
|
||||
|
||||
auto * response = new PsramAsyncJsonResponse(false);
|
||||
JsonObject root = response->getRoot();
|
||||
|
||||
Reference in New Issue
Block a user