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
|
- 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`
|
- 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
|
- `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
|
- 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`)
|
- optional `ccache` wrapping when it is installed (`scripts/enable_ccache.py`) to speed up CI
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"etag": "^1.8.1",
|
"etag": "^1.8.1",
|
||||||
"mime-types": "^3.0.2",
|
"mime-types": "^3.0.2",
|
||||||
"prettier": "^3.9.9",
|
"prettier": "^3.9.10",
|
||||||
"rollup-plugin-visualizer": "^7.1.1",
|
"rollup-plugin-visualizer": "^7.1.1",
|
||||||
"terser": "^5.51.2",
|
"terser": "^5.51.2",
|
||||||
"typescript": "^6.0.3",
|
"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))
|
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':
|
'@trivago/prettier-plugin-sort-imports':
|
||||||
specifier: ^6.0.2
|
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':
|
'@types/node':
|
||||||
specifier: ^26.6.5
|
specifier: ^26.6.5
|
||||||
version: 26.6.5
|
version: 26.6.5
|
||||||
@@ -243,8 +243,8 @@ importers:
|
|||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.0.2
|
version: 3.0.2
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.9.9
|
specifier: ^3.9.10
|
||||||
version: 3.9.9
|
version: 3.9.10
|
||||||
rollup-plugin-visualizer:
|
rollup-plugin-visualizer:
|
||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.1(rolldown@1.2.13)
|
version: 7.1.1(rolldown@1.2.13)
|
||||||
@@ -2554,8 +2554,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
|
resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
prettier@3.9.9:
|
prettier@3.9.10:
|
||||||
resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==}
|
resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
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-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)
|
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:
|
dependencies:
|
||||||
'@babel/generator': 7.29.8
|
'@babel/generator': 7.29.8
|
||||||
'@babel/parser': 7.29.9
|
'@babel/parser': 7.29.9
|
||||||
@@ -3718,7 +3718,7 @@ snapshots:
|
|||||||
lodash-es: 4.18.1
|
lodash-es: 4.18.1
|
||||||
minimatch: 9.0.9
|
minimatch: 9.0.9
|
||||||
parse-imports-exports: 0.2.4
|
parse-imports-exports: 0.2.4
|
||||||
prettier: 3.9.9
|
prettier: 3.9.10
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -5514,7 +5514,7 @@ snapshots:
|
|||||||
|
|
||||||
prepend-http@2.0.0: {}
|
prepend-http@2.0.0: {}
|
||||||
|
|
||||||
prettier@3.9.9: {}
|
prettier@3.9.10: {}
|
||||||
|
|
||||||
process-nextick-args@2.0.1: {}
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { Link } from 'react-router';
|
|||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import DescriptionIcon from '@mui/icons-material/Description';
|
||||||
import CheckIcon from '@mui/icons-material/Done';
|
import CheckIcon from '@mui/icons-material/Done';
|
||||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
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 SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||||
|
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -51,6 +54,8 @@ const STABLE_RELNOTES_URL =
|
|||||||
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
||||||
const DEV_RELNOTES_URL =
|
const DEV_RELNOTES_URL =
|
||||||
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
'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
|
const MAX_VERSION_POLLS = 5; // approx 15 seconds of waiting
|
||||||
|
|
||||||
// Types for better type safety
|
// Types for better type safety
|
||||||
@@ -75,6 +80,96 @@ interface VersionData {
|
|||||||
disable_reset: boolean;
|
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
|
// Memoized components for better performance
|
||||||
const VersionInfoDialog = memo(
|
const VersionInfoDialog = memo(
|
||||||
({
|
({
|
||||||
@@ -235,6 +330,7 @@ const VersionInfoDialog = memo(
|
|||||||
<DialogActions>
|
<DialogActions>
|
||||||
{!isPartition && (
|
{!isPartition && (
|
||||||
<Button
|
<Button
|
||||||
|
startIcon={<DescriptionIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
component="a"
|
component="a"
|
||||||
href={relNotesUrl}
|
href={relNotesUrl}
|
||||||
@@ -244,7 +340,12 @@ const VersionInfoDialog = memo(
|
|||||||
Changelog
|
Changelog
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button variant="outlined" onClick={onClose} color="secondary">
|
<Button
|
||||||
|
startIcon={<CloseIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onClose}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CLOSE()}
|
{LL.CLOSE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -372,9 +473,7 @@ const InstallPartitionDialog = memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
|
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
|
||||||
<DialogTitle>
|
<DialogTitle>{isRecovery ? LL.RECOVERY_APP() : LL.INSTALL()}</DialogTitle>
|
||||||
{isRecovery ? LL.RECOVERY() : `${LL.INSTALL()} ${LL.STORED_VERSIONS()}`}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography sx={{ mb: 2 }}>
|
<Typography sx={{ mb: 2 }}>
|
||||||
{isRecovery
|
{isRecovery
|
||||||
@@ -394,7 +493,7 @@ const InstallPartitionDialog = memo(
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
startIcon={
|
startIcon={
|
||||||
isRecovery ? <PowerSettingsNewIcon /> : <WarningIcon color="warning" />
|
isRecovery ? <RestartAltIcon /> : <WarningIcon color="warning" />
|
||||||
}
|
}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => onInstall(partition)}
|
onClick={() => onInstall(partition)}
|
||||||
@@ -418,6 +517,11 @@ const Version = () => {
|
|||||||
const { me, versions, refreshVersions } = useContext(AuthenticatedContext);
|
const { me, versions, refreshVersions } = useContext(AuthenticatedContext);
|
||||||
|
|
||||||
const [restarting, setRestarting] = useState<boolean>(false);
|
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 [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
|
||||||
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
||||||
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
|
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
|
||||||
@@ -499,9 +603,6 @@ const Version = () => {
|
|||||||
|
|
||||||
const platform = data ? getPlatform(data) : '';
|
const platform = data ? getPlatform(data) : '';
|
||||||
|
|
||||||
const otherPartitions =
|
|
||||||
data?.partitions.filter((p) => p.partition !== data.partition) ?? [];
|
|
||||||
|
|
||||||
const setPartitionVersionInfo = (partition: string) => {
|
const setPartitionVersionInfo = (partition: string) => {
|
||||||
setShowVersionInfo(3);
|
setShowVersionInfo(3);
|
||||||
const partitionData = data?.partitions.find((p) => p.partition === partition);
|
const partitionData = data?.partitions.find((p) => p.partition === partition);
|
||||||
@@ -544,6 +645,49 @@ const Version = () => {
|
|||||||
await doRestart();
|
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) => {
|
const installPartitionFirmware = async (partition: string) => {
|
||||||
await sendSetPartition(partition).catch((error: Error) => {
|
await sendSetPartition(partition).catch((error: Error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
@@ -607,8 +751,9 @@ const Version = () => {
|
|||||||
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
|
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
sx={{ ml: 1 }}
|
sx={{ ml: 3 }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => showFirmwareDialog(showingDev)}
|
onClick={() => showFirmwareDialog(showingDev)}
|
||||||
>
|
>
|
||||||
@@ -624,9 +769,10 @@ const Version = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
sx={{ ml: 1 }}
|
sx={{ ml: 3 }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color={isUpdateAvailable ? 'success' : 'warning'}
|
color={isUpdateAvailable ? 'success' : 'warning'}
|
||||||
|
startIcon={!isUpdateAvailable ? <SwapHorizIcon /> : undefined}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => showFirmwareDialog(showingDev)}
|
onClick={() => showFirmwareDialog(showingDev)}
|
||||||
>
|
>
|
||||||
@@ -639,7 +785,7 @@ const Version = () => {
|
|||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
ml: 1,
|
ml: 3,
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
backgroundColor: '#ffeb3b',
|
backgroundColor: '#ffeb3b',
|
||||||
@@ -655,6 +801,20 @@ const Version = () => {
|
|||||||
return <SystemMonitor />;
|
return <SystemMonitor />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (installingRecovery) {
|
||||||
|
return (
|
||||||
|
<SystemMonitor
|
||||||
|
onDone={(success) => {
|
||||||
|
setInstallingRecovery(false);
|
||||||
|
if (success) {
|
||||||
|
toast.success(LL.RECOVERY_INSTALLED());
|
||||||
|
}
|
||||||
|
void loadData();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
@@ -674,9 +834,13 @@ const Version = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'auto 1fr',
|
gridTemplateColumns: 'auto 1fr',
|
||||||
|
gridAutoRows: 'minmax(32px, auto)', // same height for every row
|
||||||
columnGap: 3,
|
columnGap: 3,
|
||||||
rowGap: 0.75,
|
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>
|
<Typography color="secondary">{LL.VERSION()}</Typography>
|
||||||
@@ -732,6 +896,51 @@ const Version = () => {
|
|||||||
PSRAM)
|
PSRAM)
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</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>
|
</Box>
|
||||||
|
|
||||||
{internetLive ? (
|
{internetLive ? (
|
||||||
@@ -749,96 +958,6 @@ const Version = () => {
|
|||||||
alignItems: 'center'
|
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>
|
<Typography color="secondary">{LL.STABLE()}</Typography>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -880,6 +999,52 @@ const Version = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
{showButtons(true)}
|
{showButtons(true)}
|
||||||
</Box>
|
</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>
|
</Box>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -978,7 +1143,7 @@ const Version = () => {
|
|||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<PowerSettingsNewIcon />}
|
startIcon={<RestartAltIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={doRestart}
|
onClick={doRestart}
|
||||||
color="success"
|
color="success"
|
||||||
@@ -1010,7 +1175,7 @@ const Version = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
startIcon={<PowerSettingsNewIcon />}
|
startIcon={<RestartAltIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handleRestartClick}
|
onClick={handleRestartClick}
|
||||||
color="success"
|
color="success"
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ import { useInterval } from 'utils';
|
|||||||
|
|
||||||
import { LinearProgressWithLabel } from '../../components/upload/LinearProgressWithLabel';
|
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 [errorMessage, setErrorMessage] = useState<string>();
|
||||||
const hasInitialized = useRef(false);
|
const hasInitialized = useRef(false);
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
@@ -42,7 +47,11 @@ const SystemMonitor = () => {
|
|||||||
event.data.status === SystemStatusCodes.SYSTEM_STATUS_NORMAL ||
|
event.data.status === SystemStatusCodes.SYSTEM_STATUS_NORMAL ||
|
||||||
event.data.status === undefined
|
event.data.status === undefined
|
||||||
) {
|
) {
|
||||||
document.location.href = '/';
|
if (onDone) {
|
||||||
|
onDone(true);
|
||||||
|
} else {
|
||||||
|
document.location.href = '/';
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
event.data.status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
|
event.data.status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
|
||||||
) {
|
) {
|
||||||
@@ -80,7 +89,11 @@ const SystemMonitor = () => {
|
|||||||
const onCancel = async () => {
|
const onCancel = async () => {
|
||||||
setErrorMessage(undefined);
|
setErrorMessage(undefined);
|
||||||
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
|
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
|
||||||
document.location.href = '/';
|
if (onDone) {
|
||||||
|
onDone(false);
|
||||||
|
} else {
|
||||||
|
document.location.href = '/';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const cs: Translation = {
|
|||||||
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi',
|
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi',
|
||||||
FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru',
|
FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru',
|
||||||
USER_PROFILE: 'Uživatelský profil',
|
USER_PROFILE: 'Uživatelský profil',
|
||||||
STORED_VERSIONS: 'Uložené verze',
|
|
||||||
ONLINE_HELP: 'online nápověda',
|
ONLINE_HELP: 'online nápověda',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovat důležité zprávy',
|
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.',
|
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_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_RECEIVED: 'Digest přijat, nyní nahrajte firmware .bin',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 souhlasí, firmware se instaluje...',
|
UPLOAD_MD5_MATCHED: 'MD5 souhlasí, firmware se instaluje...',
|
||||||
RECOVERY: 'Obnova',
|
RECOVERY_APP: 'Aplikace pro obnovu',
|
||||||
RECOVERY_NOT_INSTALLED: 'Nenainstalováno',
|
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;
|
export default cs;
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const de: Translation = {
|
|||||||
SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln',
|
SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln',
|
||||||
FIRMWARE_VERSION_INFO: 'Firmware-Versionsinformation',
|
FIRMWARE_VERSION_INFO: 'Firmware-Versionsinformation',
|
||||||
USER_PROFILE: 'Benutzerprofil',
|
USER_PROFILE: 'Benutzerprofil',
|
||||||
STORED_VERSIONS: 'Gespeicherte Versionen',
|
|
||||||
ONLINE_HELP: 'Online-Hilfe',
|
ONLINE_HELP: 'Online-Hilfe',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Wichtige Nachrichten aktualisieren',
|
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.',
|
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_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_RECEIVED: 'Prüfsumme empfangen, jetzt die Firmware-Datei (.bin) hochladen',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 stimmt überein, Firmware wird installiert...',
|
UPLOAD_MD5_MATCHED: 'MD5 stimmt überein, Firmware wird installiert...',
|
||||||
RECOVERY: 'Wiederherstellung',
|
RECOVERY_APP: 'Wiederherstellungs-App',
|
||||||
RECOVERY_NOT_INSTALLED: 'Nicht installiert',
|
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;
|
export default de;
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const en: Translation = {
|
|||||||
SWITCH_RELEASE_TYPE: 'Switch to {0} release',
|
SWITCH_RELEASE_TYPE: 'Switch to {0} release',
|
||||||
FIRMWARE_VERSION_INFO: 'Firmware Version Information',
|
FIRMWARE_VERSION_INFO: 'Firmware Version Information',
|
||||||
USER_PROFILE: 'User Profile',
|
USER_PROFILE: 'User Profile',
|
||||||
STORED_VERSIONS: 'Stored Versions',
|
|
||||||
ONLINE_HELP: 'online help',
|
ONLINE_HELP: 'online help',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Important Messages',
|
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.',
|
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_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_RECEIVED: 'Digest received, now upload the firmware .bin file',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 matches, installing firmware...',
|
UPLOAD_MD5_MATCHED: 'MD5 matches, installing firmware...',
|
||||||
RECOVERY: 'Recovery',
|
RECOVERY_APP: 'Recovery App',
|
||||||
RECOVERY_NOT_INSTALLED: 'Not installed',
|
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;
|
export default en;
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const fr: Translation = {
|
|||||||
SWITCH_RELEASE_TYPE: 'Passer à la version {0}',
|
SWITCH_RELEASE_TYPE: 'Passer à la version {0}',
|
||||||
FIRMWARE_VERSION_INFO: 'Informations sur la version du firmware',
|
FIRMWARE_VERSION_INFO: 'Informations sur la version du firmware',
|
||||||
USER_PROFILE: 'Profil utilisateur',
|
USER_PROFILE: 'Profil utilisateur',
|
||||||
STORED_VERSIONS: 'Versions stockées',
|
|
||||||
ONLINE_HELP: 'aide en ligne',
|
ONLINE_HELP: 'aide en ligne',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Mettre à jour les messages importants',
|
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.',
|
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_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_RECEIVED: 'Empreinte reçue, téléversez maintenant le fichier firmware .bin',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 concordant, installation du firmware...',
|
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_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;
|
export default fr;
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const it: Translation = {
|
|||||||
SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio',
|
SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio',
|
||||||
FIRMWARE_VERSION_INFO: 'Informazioni sulla versione del firmware',
|
FIRMWARE_VERSION_INFO: 'Informazioni sulla versione del firmware',
|
||||||
USER_PROFILE: 'Profilo utente',
|
USER_PROFILE: 'Profilo utente',
|
||||||
STORED_VERSIONS: 'Versioni memorizzate',
|
|
||||||
ONLINE_HELP: 'aiuto online',
|
ONLINE_HELP: 'aiuto online',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Aggiorna Messaggi Importanti',
|
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.',
|
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_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_RECEIVED: 'Digest ricevuto, ora caricare il file firmware .bin',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 corrispondente, installazione firmware...',
|
UPLOAD_MD5_MATCHED: 'MD5 corrispondente, installazione firmware...',
|
||||||
RECOVERY: 'Ripristino',
|
RECOVERY_APP: 'App di ripristino',
|
||||||
RECOVERY_NOT_INSTALLED: 'Non installato',
|
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;
|
export default it;
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const nl: Translation = {
|
|||||||
SWITCH_RELEASE_TYPE: 'Switch naar {0} release',
|
SWITCH_RELEASE_TYPE: 'Switch naar {0} release',
|
||||||
FIRMWARE_VERSION_INFO: 'Informatie over firmwareversie',
|
FIRMWARE_VERSION_INFO: 'Informatie over firmwareversie',
|
||||||
USER_PROFILE: 'Gebruikersprofiel',
|
USER_PROFILE: 'Gebruikersprofiel',
|
||||||
STORED_VERSIONS: 'Opgeslagen versies',
|
|
||||||
ONLINE_HELP: 'online help',
|
ONLINE_HELP: 'online help',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Belangrijke Berichten',
|
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.',
|
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_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_RECEIVED: 'Digest ontvangen, upload nu het firmware .bin-bestand',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 komt overeen, firmware wordt geïnstalleerd...',
|
UPLOAD_MD5_MATCHED: 'MD5 komt overeen, firmware wordt geïnstalleerd...',
|
||||||
RECOVERY: 'Herstel',
|
RECOVERY_APP: 'Herstel-app',
|
||||||
RECOVERY_NOT_INSTALLED: 'Niet geïnstalleerd',
|
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;
|
export default nl;
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const no: Translation = {
|
|||||||
SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse',
|
SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse',
|
||||||
FIRMWARE_VERSION_INFO: 'Informasjon om firmwareversjon',
|
FIRMWARE_VERSION_INFO: 'Informasjon om firmwareversjon',
|
||||||
USER_PROFILE: 'Brukerprofil',
|
USER_PROFILE: 'Brukerprofil',
|
||||||
STORED_VERSIONS: 'Lagret versjoner',
|
|
||||||
ONLINE_HELP: 'online hjelp',
|
ONLINE_HELP: 'online hjelp',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Oppdater viktige meldinger',
|
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.',
|
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_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_RECEIVED: 'MD5 mottatt, last nå opp firmware .bin-filen',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 stemmer, firmware installeres...',
|
UPLOAD_MD5_MATCHED: 'MD5 stemmer, firmware installeres...',
|
||||||
RECOVERY: 'Gjenoppretting',
|
RECOVERY_APP: 'Gjenopprettingsapp',
|
||||||
RECOVERY_NOT_INSTALLED: 'Ikke installert',
|
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;
|
export default no;
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const pl: BaseTranslation = {
|
|||||||
SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie',
|
SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie',
|
||||||
FIRMWARE_VERSION_INFO: 'Informacje o wersji firmware',
|
FIRMWARE_VERSION_INFO: 'Informacje o wersji firmware',
|
||||||
USER_PROFILE: 'Profil użytkownika',
|
USER_PROFILE: 'Profil użytkownika',
|
||||||
STORED_VERSIONS: 'Zapisane wersje',
|
|
||||||
ONLINE_HELP: 'pomoc online',
|
ONLINE_HELP: 'pomoc online',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizuj ważne wiadomości',
|
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.',
|
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_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_RECEIVED: 'Suma MD5 odebrana, teraz wgraj plik firmware .bin',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 zgodne, instalowanie firmware...',
|
UPLOAD_MD5_MATCHED: 'MD5 zgodne, instalowanie firmware...',
|
||||||
RECOVERY: 'Odzyskiwanie',
|
RECOVERY_APP: 'Aplikacja odzyskiwania',
|
||||||
RECOVERY_NOT_INSTALLED: 'Nie zainstalowano',
|
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;
|
export default pl;
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const sk: Translation = {
|
|||||||
SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu',
|
SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu',
|
||||||
FIRMWARE_VERSION_INFO: 'Informácie o verzii firmware',
|
FIRMWARE_VERSION_INFO: 'Informácie o verzii firmware',
|
||||||
USER_PROFILE: 'Profil používateľa',
|
USER_PROFILE: 'Profil používateľa',
|
||||||
STORED_VERSIONS: 'Uložené verzie',
|
|
||||||
ONLINE_HELP: 'online pomoc',
|
ONLINE_HELP: 'online pomoc',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovať dôležité správy',
|
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.',
|
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_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_RECEIVED: 'Suma MD5 prijatá, teraz nahrajte firmware .bin',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 súhlasí, firmvér sa inštaluje...',
|
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_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;
|
export default sk;
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const sv: Translation = {
|
|||||||
SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva',
|
SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva',
|
||||||
FIRMWARE_VERSION_INFO: 'Information om firmwareversion',
|
FIRMWARE_VERSION_INFO: 'Information om firmwareversion',
|
||||||
USER_PROFILE: 'Användarprofil',
|
USER_PROFILE: 'Användarprofil',
|
||||||
STORED_VERSIONS: 'Lagrad versioner',
|
|
||||||
ONLINE_HELP: 'online hjälp',
|
ONLINE_HELP: 'online hjälp',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Uppdatera viktiga meddelanden',
|
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.',
|
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_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_RECEIVED: 'MD5 mottagen, ladda nu upp firmware .bin-filen',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 stämmer, firmware installeras...',
|
UPLOAD_MD5_MATCHED: 'MD5 stämmer, firmware installeras...',
|
||||||
RECOVERY: 'Återställning',
|
RECOVERY_APP: 'Återställningsapp',
|
||||||
RECOVERY_NOT_INSTALLED: 'Inte installerad',
|
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;
|
export default sv;
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ const tr: Translation = {
|
|||||||
SWITCH_RELEASE_TYPE: '{0} sürümüne geç',
|
SWITCH_RELEASE_TYPE: '{0} sürümüne geç',
|
||||||
FIRMWARE_VERSION_INFO: 'Firmware Sürüm Bilgisi',
|
FIRMWARE_VERSION_INFO: 'Firmware Sürüm Bilgisi',
|
||||||
USER_PROFILE: 'Kullanıcı Profili',
|
USER_PROFILE: 'Kullanıcı Profili',
|
||||||
STORED_VERSIONS: 'Kaydedilmiş Sürümler',
|
|
||||||
ONLINE_HELP: 'online yardım',
|
ONLINE_HELP: 'online yardım',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Önemli Mesajları Güncelle',
|
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.',
|
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_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_RECEIVED: 'Özet alındı, şimdi firmware .bin dosyasını yükleyin',
|
||||||
UPLOAD_MD5_MATCHED: 'MD5 eşleşti, firmware yükleniyor...',
|
UPLOAD_MD5_MATCHED: 'MD5 eşleşti, firmware yükleniyor...',
|
||||||
RECOVERY: 'Kurtarma',
|
RECOVERY_APP: 'Kurtarma Uygulaması',
|
||||||
RECOVERY_NOT_INSTALLED: 'Yüklü değil',
|
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;
|
export default tr;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { type ReactNode, useState } from 'react';
|
|||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import HealthAndSafetyIcon from '@mui/icons-material/HealthAndSafety';
|
import HealthAndSafetyIcon from '@mui/icons-material/HealthAndSafety';
|
||||||
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
import LogoutIcon from '@mui/icons-material/Logout';
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
import MemoryIcon from '@mui/icons-material/Memory';
|
import MemoryIcon from '@mui/icons-material/Memory';
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
@@ -17,11 +18,16 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Divider,
|
Divider,
|
||||||
|
IconButton,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
@@ -38,6 +44,9 @@ import type { RecoveryPartition, RecoveryStatus } from './types';
|
|||||||
|
|
||||||
const formatMB = (bytes: number) => `${(bytes / 1048576).toFixed(2)} MB`;
|
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 formatUptime = (seconds: number) => {
|
||||||
const h = Math.floor(seconds / 3600);
|
const h = Math.floor(seconds / 3600);
|
||||||
const m = Math.floor((seconds % 3600) / 60);
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
@@ -81,13 +90,27 @@ const SectionTitle = ({ children }: { children: ReactNode }) => (
|
|||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// same look as the version info dialog in the EMS-ESP app
|
||||||
const InfoRow = ({ label, value }: { label: string; value: ReactNode }) => (
|
const InfoRow = ({ label, value }: { label: string; value: ReactNode }) => (
|
||||||
<Box sx={{ display: 'flex', py: 0.5 }}>
|
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||||
<Typography sx={{ width: 160, flexShrink: 0 }} color="text.secondary">
|
<TableCell
|
||||||
|
component="th"
|
||||||
|
scope="row"
|
||||||
|
sx={{
|
||||||
|
color: 'lightblue',
|
||||||
|
borderBottom: 'none',
|
||||||
|
pr: 1,
|
||||||
|
py: 0.5,
|
||||||
|
fontSize: 13,
|
||||||
|
width: 140
|
||||||
|
}}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</Typography>
|
</TableCell>
|
||||||
<Typography>{value}</Typography>
|
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||||
</Box>
|
{value}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
@@ -98,6 +121,7 @@ interface DashboardProps {
|
|||||||
|
|
||||||
const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
||||||
const [confirmBoot, setConfirmBoot] = useState<RecoveryPartition>();
|
const [confirmBoot, setConfirmBoot] = useState<RecoveryPartition>();
|
||||||
|
const [infoPartition, setInfoPartition] = useState<RecoveryPartition>();
|
||||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState(false);
|
const [confirmFactoryReset, setConfirmFactoryReset] = useState(false);
|
||||||
const [pickedTarget, setPickedTarget] = useState<string>();
|
const [pickedTarget, setPickedTarget] = useState<string>();
|
||||||
const uploadTarget = pickedTarget ?? status.upload_target;
|
const uploadTarget = pickedTarget ?? status.upload_target;
|
||||||
@@ -240,6 +264,16 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
|||||||
label="unconfirmed"
|
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>
|
</Box>
|
||||||
}
|
}
|
||||||
secondary={
|
secondary={
|
||||||
@@ -287,46 +321,53 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
|||||||
|
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
<SectionTitle>System</SectionTitle>
|
<SectionTitle>System</SectionTitle>
|
||||||
<InfoRow label="Recovery version" value={'v' + status.recovery_version} />
|
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
|
||||||
<InfoRow
|
<TableBody>
|
||||||
label="Chip"
|
<InfoRow
|
||||||
value={`${status.chip}, ${formatMB(status.flash_size)} flash${status.psram_size ? ', ' + formatMB(status.psram_size) + ' PSRAM' : ''}`}
|
label="Recovery version"
|
||||||
/>
|
value={'v' + status.recovery_version}
|
||||||
<InfoRow
|
/>
|
||||||
label="Free heap"
|
<InfoRow
|
||||||
value={`${Math.round(status.free_heap / 1024)} KB`}
|
label="Chip"
|
||||||
/>
|
value={`${status.chip}, ${formatMB(status.flash_size)} flash${status.psram_size ? ', ' + formatMB(status.psram_size) + ' PSRAM' : ''}`}
|
||||||
<InfoRow label="Uptime" value={formatUptime(status.uptime)} />
|
/>
|
||||||
<InfoRow label="MAC address" value={status.mac} />
|
<InfoRow
|
||||||
<InfoRow
|
label="Free heap"
|
||||||
label="Access point"
|
value={`${Math.round(status.free_heap / 1024)} KB`}
|
||||||
value={`${status.ap_ssid} on ${status.ap_ip} (${status.ap_clients} connected)`}
|
/>
|
||||||
/>
|
<InfoRow label="Uptime" value={formatUptime(status.uptime)} />
|
||||||
{status.eth_state && (
|
<InfoRow label="MAC address" value={status.mac} />
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="Ethernet"
|
label="Access point"
|
||||||
value={
|
value={`${status.ap_ssid} on ${status.ap_ip} (${status.ap_clients} connected)`}
|
||||||
status.eth_state === 'connected'
|
/>
|
||||||
? `connected, ${status.eth_ip ?? ''}${status.static_ip ? ' (static IP)' : ''}`
|
{status.eth_state && (
|
||||||
: ETH_STATE_TEXT[status.eth_state]
|
<InfoRow
|
||||||
}
|
label="Ethernet"
|
||||||
/>
|
value={
|
||||||
)}
|
status.eth_state === 'connected'
|
||||||
<InfoRow
|
? `connected, ${status.eth_ip ?? ''}${status.static_ip ? ' (static IP)' : ''}`
|
||||||
label="WiFi"
|
: ETH_STATE_TEXT[status.eth_state]
|
||||||
value={
|
}
|
||||||
!status.wifi_ssid
|
/>
|
||||||
? 'not configured'
|
)}
|
||||||
: status.wifi_connected
|
<InfoRow
|
||||||
? `${status.wifi_ssid}, ${status.wifi_ip ?? ''} (${status.wifi_rssi ?? 0} dBm)${status.static_ip ? ', static IP' : ''}`
|
label="WiFi"
|
||||||
: !status.wifi_started
|
value={
|
||||||
? status.eth_state === 'connected'
|
!status.wifi_ssid
|
||||||
? 'not used, Ethernet is connected'
|
? 'not configured'
|
||||||
: 'waiting for Ethernet'
|
: status.wifi_connected
|
||||||
: `connecting to ${status.wifi_ssid}`
|
? `${status.wifi_ssid}, ${status.wifi_ip ?? ''} (${status.wifi_rssi ?? 0} dBm)${status.static_ip ? ', static IP' : ''}`
|
||||||
}
|
: !status.wifi_started
|
||||||
/>
|
? status.eth_state === 'connected'
|
||||||
<InfoRow label="Hostname" value={status.hostname} />
|
? 'not used, Ethernet is connected'
|
||||||
|
: 'waiting for Ethernet'
|
||||||
|
: `connecting to ${status.wifi_ssid}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InfoRow label="Hostname" value={status.hostname} />
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
{!status.disable_reset && (
|
{!status.disable_reset && (
|
||||||
@@ -342,7 +383,7 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
|||||||
<Button
|
<Button
|
||||||
startIcon={<RestartAltIcon />}
|
startIcon={<RestartAltIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="error"
|
color="success"
|
||||||
onClick={() => void doRestart(status.boot)}
|
onClick={() => void doRestart(status.boot)}
|
||||||
>
|
>
|
||||||
Restart{status.boot !== status.running ? ` (boots ${status.boot})` : ''}
|
Restart{status.boot !== status.running ? ` (boots ${status.boot})` : ''}
|
||||||
@@ -351,6 +392,7 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
|||||||
startIcon={<LogoutIcon />}
|
startIcon={<LogoutIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
sx={{ float: 'right' }}
|
||||||
onClick={onSignOut}
|
onClick={onSignOut}
|
||||||
>
|
>
|
||||||
Sign Out
|
Sign Out
|
||||||
@@ -424,6 +466,50 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</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": {
|
"devDependencies": {
|
||||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||||
"prettier": "^3.9.9"
|
"prettier": "^3.9.10"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@12.11.2"
|
"packageManager": "pnpm@12.11.2"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+8
-8
@@ -175,10 +175,10 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@trivago/prettier-plugin-sort-imports':
|
'@trivago/prettier-plugin-sort-imports':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2(prettier@3.9.9)
|
version: 6.0.2(prettier@3.9.10)
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.9.9
|
specifier: ^3.9.10
|
||||||
version: 3.9.9
|
version: 3.9.10
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -303,8 +303,8 @@ packages:
|
|||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
prettier@3.9.9:
|
prettier@3.9.10:
|
||||||
resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==}
|
resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -373,7 +373,7 @@ snapshots:
|
|||||||
|
|
||||||
'@msgpack/msgpack@3.1.3': {}
|
'@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:
|
dependencies:
|
||||||
'@babel/generator': 7.29.8
|
'@babel/generator': 7.29.8
|
||||||
'@babel/parser': 7.29.9
|
'@babel/parser': 7.29.9
|
||||||
@@ -383,7 +383,7 @@ snapshots:
|
|||||||
lodash-es: 4.18.1
|
lodash-es: 4.18.1
|
||||||
minimatch: 9.0.9
|
minimatch: 9.0.9
|
||||||
parse-imports-exports: 0.2.4
|
parse-imports-exports: 0.2.4
|
||||||
prettier: 3.9.9
|
prettier: 3.9.10
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -421,4 +421,4 @@ snapshots:
|
|||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
prettier@3.9.9: {}
|
prettier@3.9.10: {}
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ let system_status = {
|
|||||||
psram: true,
|
psram: true,
|
||||||
psram_size: 8189,
|
psram_size: 8189,
|
||||||
free_psram: 8166,
|
free_psram: 8166,
|
||||||
// recovery_version: '', // upload an EMS-ESP-Recovery-*.bin to set it
|
recovery_version: '', // upload an EMS-ESP-Recovery-*.bin to set it
|
||||||
recovery_version: '1.0.0',
|
// recovery_version: '1.0.0',
|
||||||
has_partition: true,
|
has_partition: true,
|
||||||
partitions: [
|
partitions: [
|
||||||
{
|
{
|
||||||
@@ -5447,6 +5447,10 @@ router
|
|||||||
} else if (action === 'uploadURL') {
|
} else if (action === 'uploadURL') {
|
||||||
// upload URL
|
// upload URL
|
||||||
console.log('upload File from URL', content.param);
|
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);
|
return status(200);
|
||||||
} else if (action === 'resetMQTT') {
|
} else if (action === 'resetMQTT') {
|
||||||
// reset MQTT
|
// reset MQTT
|
||||||
|
|||||||
+51
-22
@@ -2,36 +2,46 @@
|
|||||||
|
|
||||||
[common]
|
[common]
|
||||||
; custom build flags to use in my_build_flags
|
; custom build flags to use in my_build_flags
|
||||||
; -DEMSESP_UART_DEBUG ; debugging UART
|
; -D EMSESP_UART_DEBUG ; debugging UART
|
||||||
; -DEMSESP_DEBUG ; enables DEBUG to the log. Will generate a lot of extra traffic on Console and Syslog
|
; -D EMSESP_DEBUG ; enables DEBUG to the log. Will generate a lot of extra traffic on Console and Syslog
|
||||||
; -DEMSESP_DEFAULT_BOARD_PROFILE=\"NODEMCU\" ; hard code the default board name
|
; -D EMSESP_DEFAULT_BOARD_PROFILE=\"NODEMCU\" ; hard code the default board name
|
||||||
; -DEMSESP_DEBUG_SENSOR ; additional debug for the sensors
|
; -D EMSESP_DEBUG_SENSOR ; additional debug for the sensors
|
||||||
; -DCORE_DEBUG_LEVEL=5 ; verbose core level debugging 5=verbose, 4=debug, 3=info
|
; -D CORE_DEBUG_LEVEL=5 ; verbose core level debugging 5=verbose, 4=debug, 3=info
|
||||||
; -DEMSESP_TEST ; enable the tests. EN language only
|
; -D EMSESP_TEST ; enable the tests. EN language only
|
||||||
; -DEMSESP_DE_ONLY ; only DE translated entity names
|
; -D EMSESP_DE_ONLY ; only DE translated entity names
|
||||||
; -DEMSESP_EN_ONLY ; only EN translated entity names
|
; -D EMSESP_EN_ONLY ; only EN translated entity names
|
||||||
; -DEMSESP_PINGTEST ; send log message every 1/2 second
|
; -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_TEST -DEMSESP_DEBUG
|
||||||
; my_build_flags = -DEMSESP_DEBUG -DEMSESP_TEST -DEMSESP_PINGTEST
|
; my_build_flags = -DEMSESP_DEBUG -DEMSESP_TEST -DEMSESP_PINGTEST
|
||||||
; my_build_flags = -DEMSESP_DEBUG -DCORE_DEBUG_LEVEL=5
|
; 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 = -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]
|
[platformio]
|
||||||
default_envs = s_16M_P ; BBQKees E32V2
|
default_envs = s_16M_P, recovery_s_16M_P, build-recovery, build-webUI
|
||||||
; default_envs = build-webUI ; build the web interface only
|
; default_envs = s_16M_P ; BBQKees E32V2
|
||||||
; default_envs = s3_16M_P ; BBQKees S3
|
; default_envs = s_16M_P_custom ; BBQKees E32V2 with custom Arduino/IDF libs
|
||||||
; default_envs = s_4M ; BBQKees older S32, 4MB no psram
|
; default_envs = recovery_s_16M_P ; recovery firmware
|
||||||
; default_envs = s_16M ; BBQKees newer S32 V2, 16MB no psram
|
; default_envs = crashtest_s_16M_P ; crash test
|
||||||
; default_envs = c6 ; XIAO ESP32C
|
; default_envs = build-webUI ; build the main ems-esp webUI only
|
||||||
; default_envs = debug
|
; 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]
|
[env]
|
||||||
|
|
||||||
|
; uncomment if you want to upload the firmware via OTA (must have upload_protocol = custom)
|
||||||
extra_scripts =
|
extra_scripts =
|
||||||
pre:scripts/build_interface.py ; builds the WebUI
|
post:scripts/rename_fw.py ; renames the firmware .bin file
|
||||||
scripts/rename_fw.py ; renames the firmware .bin file - comment out if not needed
|
|
||||||
scripts/upload.py ; optionally upload the firmware via OTA (must have upload_protocol = custom)
|
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)
|
; set the username and password for the admin account (default password is admin)
|
||||||
@@ -40,13 +50,33 @@ custom_password = admin
|
|||||||
|
|
||||||
; ** upload settings **
|
; ** upload settings **
|
||||||
; for direct USB, which is the default you can set an optional port for example:
|
; for direct USB, which is the default you can set an optional port for example:
|
||||||
; upload_port = /dev/ttyUSB0 or COM3
|
; upload_port = /dev/ttyUSB0
|
||||||
; upload_speed = 921600
|
; 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:
|
; for direct via HTTP (OTA) set:
|
||||||
; upload_protocol = custom
|
; upload_protocol = custom
|
||||||
; custom_emsesp_ip = <ip address> or ems-esp.local
|
; 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 **
|
; ** debug settings **
|
||||||
; to be used with an ESP32-S3 which has an onboard JTAG, connect the OTG USB port to the PC
|
; 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
|
; 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
|
upload_speed = 2000000
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
extra_scripts =
|
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
|
; Exclude all source files so the C code doesn't build
|
||||||
build_src_filter = -<*>
|
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
|
; Builds for different board types
|
||||||
; If you're building for a single target environment, we recommend creating a pio_local.ini (see example file)
|
; If you're building for a single target environment, we recommend creating a pio_local.ini (see example file)
|
||||||
|
|||||||
+87
-26
@@ -28,6 +28,7 @@
|
|||||||
#include <esp_partition.h>
|
#include <esp_partition.h>
|
||||||
#include <esp_random.h>
|
#include <esp_random.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "WWWData.h" // generated by `pnpm build-recovery` in ./interface
|
#include "WWWData.h" // generated by `pnpm build-recovery` in ./interface
|
||||||
@@ -37,18 +38,18 @@
|
|||||||
#define FACTORY_AP_CHANNEL 1
|
#define FACTORY_AP_CHANNEL 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static constexpr size_t MIN_FIRMWARE_SIZE = 512 * 1024;
|
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;
|
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 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_NAMESPACE[] = "ems-esp";
|
||||||
static constexpr char NVS_FRESH_FW[] = "fresh_firmware"; // EMSESP_NVS_BOOT_NEW_FIRMWARE in emsesp.h
|
static constexpr char NVS_FRESH_FW[] = "fresh_firmware"; // EMSESP_NVS_BOOT_NEW_FIRMWARE in emsesp.h
|
||||||
static constexpr char NVS_REASON[] = "rec_reason"; // EMSESP_NVS_RECOVERY_REASON in emsesp.h
|
static constexpr char NVS_REASON[] = "rec_reason"; // EMSESP_NVS_RECOVERY_REASON in emsesp.h
|
||||||
|
|
||||||
#define LOG(...) \
|
#define LOG(...) \
|
||||||
do { \
|
do { \
|
||||||
Serial.print("[recovery] "); \
|
Serial.print("[recovery] "); \
|
||||||
Serial.printf(__VA_ARGS__); \
|
Serial.printf(__VA_ARGS__); \
|
||||||
Serial.println(); \
|
Serial.println(); \
|
||||||
} while (0)
|
} while (0)
|
||||||
|
|
||||||
struct AdminUser {
|
struct AdminUser {
|
||||||
@@ -102,7 +103,7 @@ static std::vector<PartitionInfo> partitions;
|
|||||||
static uint32_t restart_at = 0;
|
static uint32_t restart_at = 0;
|
||||||
static String session_token;
|
static String session_token;
|
||||||
static String ap_ssid;
|
static String ap_ssid;
|
||||||
static String recovery_reason; // set by EMS-ESP when it switched to the recovery
|
static String recovery_reason; // set by EMS-ESP when it switched to the recovery
|
||||||
static const char * eth_state = ""; // "", "disabled", "failed" or "started"
|
static const char * eth_state = ""; // "", "disabled", "failed" or "started"
|
||||||
static uint32_t wifi_join_at = 0; // when to try WiFi if Ethernet has no address, 0 if not waiting
|
static uint32_t wifi_join_at = 0; // when to try WiFi if Ethernet has no address, 0 if not waiting
|
||||||
static bool wifi_started = false;
|
static bool wifi_started = false;
|
||||||
@@ -113,6 +114,8 @@ static bool ethConnected();
|
|||||||
static int upload_error = 0;
|
static int upload_error = 0;
|
||||||
static bool upload_done = false;
|
static bool upload_done = false;
|
||||||
static String upload_target;
|
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)
|
// settings (read-only)
|
||||||
@@ -256,6 +259,7 @@ static void loadRecoveryReason() {
|
|||||||
if (nvs.isKey(NVS_REASON)) {
|
if (nvs.isKey(NVS_REASON)) {
|
||||||
recovery_reason = nvs.getString(NVS_REASON);
|
recovery_reason = nvs.getString(NVS_REASON);
|
||||||
nvs.remove(NVS_REASON);
|
nvs.remove(NVS_REASON);
|
||||||
|
LOG("EMS-ESP switched to the recovery: %s", recovery_reason.c_str());
|
||||||
}
|
}
|
||||||
nvs.end();
|
nvs.end();
|
||||||
}
|
}
|
||||||
@@ -272,8 +276,8 @@ static void loadPartitions() {
|
|||||||
const esp_partition_t * part = esp_partition_get(it);
|
const esp_partition_t * part = esp_partition_get(it);
|
||||||
|
|
||||||
PartitionInfo info;
|
PartitionInfo info;
|
||||||
info.label = part->label;
|
info.label = part->label;
|
||||||
info.factory = part->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY;
|
info.factory = part->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY;
|
||||||
info.address = part->address;
|
info.address = part->address;
|
||||||
info.size = part->size;
|
info.size = part->size;
|
||||||
info.image_size = 0;
|
info.image_size = 0;
|
||||||
@@ -293,6 +297,10 @@ static void loadPartitions() {
|
|||||||
|
|
||||||
if (part == running) {
|
if (part == running) {
|
||||||
info.version = RECOVERY_VERSION;
|
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) {
|
} else if (info.valid) {
|
||||||
// EMS-ESP stores the version and install date of each partition in NVS, keyed by the label
|
// EMS-ESP stores the version and install date of each partition in NVS, keyed by the label
|
||||||
if (has_nvs && nvs.isKey(part->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);
|
partitions.push_back(info);
|
||||||
}
|
}
|
||||||
esp_partition_iterator_release(it);
|
esp_partition_iterator_release(it);
|
||||||
@@ -353,6 +372,7 @@ static const esp_partition_t * uploadTarget(AsyncWebServerRequest * request) {
|
|||||||
static void markFreshFirmware(const String & label) {
|
static void markFreshFirmware(const String & label) {
|
||||||
Preferences nvs;
|
Preferences nvs;
|
||||||
if (!openNvs(nvs, false)) {
|
if (!openNvs(nvs, false)) {
|
||||||
|
LOG("Cannot open NVS, EMS-ESP won't know it's a new firmware");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
nvs.putBool(NVS_FRESH_FW, true);
|
nvs.putBool(NVS_FRESH_FW, true);
|
||||||
@@ -381,10 +401,11 @@ static String generateToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool isAuthorized(AsyncWebServerRequest * request) {
|
static bool isAuthorized(AsyncWebServerRequest * request) {
|
||||||
if (session_token.isEmpty() || !request->hasHeader("Authorization")) {
|
const bool ok = !session_token.isEmpty() && request->hasHeader("Authorization") && request->header("Authorization") == "Bearer " + session_token;
|
||||||
return false;
|
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 String label = json["partition"] | "";
|
||||||
const esp_partition_t * part = findAppPartition(label.c_str());
|
const esp_partition_t * part = findAppPartition(label.c_str());
|
||||||
if (part == nullptr) {
|
if (part == nullptr) {
|
||||||
|
LOG("Cannot boot from %s: no such partition", label.c_str());
|
||||||
request->send(404);
|
request->send(404);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -569,6 +591,7 @@ static void handleFactoryReset(AsyncWebServerRequest * request) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOG("Factory reset requested");
|
||||||
const bool ok = LittleFS.format(); // the only place the recovery writes to the filesystem
|
const bool ok = LittleFS.format(); // the only place the recovery writes to the filesystem
|
||||||
LOG("Factory reset %s", ok ? "done" : "failed");
|
LOG("Factory reset %s", ok ? "done" : "failed");
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@@ -580,12 +603,12 @@ static void handleFactoryReset(AsyncWebServerRequest * request) {
|
|||||||
restart_at = millis() + RESTART_DELAY_MS;
|
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) {
|
if (upload_error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
upload_error = code;
|
upload_error = code;
|
||||||
LOG("Upload error %d", code);
|
LOG("Upload error %d: %s", code, reason);
|
||||||
request->send(code);
|
request->send(code);
|
||||||
if (code == 406) {
|
if (code == 406) {
|
||||||
request->client()->close();
|
request->client()->close();
|
||||||
@@ -596,10 +619,12 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
|
|||||||
if (!index) {
|
if (!index) {
|
||||||
upload_error = 0;
|
upload_error = 0;
|
||||||
upload_done = false;
|
upload_done = false;
|
||||||
|
upload_size = 0;
|
||||||
|
upload_step = 0;
|
||||||
upload_target.clear();
|
upload_target.clear();
|
||||||
|
|
||||||
if (!isAuthorized(request)) {
|
if (!isAuthorized(request)) {
|
||||||
uploadFail(request, 403);
|
uploadFail(request, 403, "not signed in");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,41 +636,51 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
|
|||||||
if (ota.setMd5(data, len)) {
|
if (ota.setMd5(data, len)) {
|
||||||
LOG("MD5 digest received (%s)", ota.md5());
|
LOG("MD5 digest received (%s)", ota.md5());
|
||||||
} else {
|
} else {
|
||||||
uploadFail(request, 406);
|
uploadFail(request, 406, "invalid MD5 digest");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
// the recovery app only flashes firmware, it never touches the settings
|
// the recovery app only flashes firmware, it never touches the settings
|
||||||
ota.clearMd5();
|
ota.clearMd5();
|
||||||
LOG("Unsupported file %s (%u bytes)", filename.c_str(), static_cast<unsigned>(filesize));
|
LOG("Unsupported file %s (%u bytes)", filename.c_str(), static_cast<unsigned>(filesize));
|
||||||
uploadFail(request, 406);
|
uploadFail(request, 406, "unsupported file");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!OtaUpdater::isCompatibleFirmware(data, len)) {
|
if (!OtaUpdater::isCompatibleFirmware(data, len)) {
|
||||||
uploadFail(request, 503);
|
uploadFail(request, 503, "not a firmware image for this chip");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (OtaUpdater::isRecoveryFirmware(data, len)) {
|
if (OtaUpdater::isRecoveryFirmware(data, len)) {
|
||||||
uploadFail(request, 409);
|
uploadFail(request, 409, "the recovery firmware can't be installed from the recovery");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const esp_partition_t * target = uploadTarget(request);
|
const esp_partition_t * target = uploadTarget(request);
|
||||||
if (target == nullptr) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!ota.beginFirmware(filesize, target)) {
|
if (!ota.beginFirmware(filesize, target)) {
|
||||||
LOG("Cannot write to %s: %s", target->label, ota.errorString());
|
LOG("Cannot write to %s: %s", target->label, ota.errorString());
|
||||||
uploadFail(request, 507);
|
uploadFail(request, 507, "cannot start the update");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
upload_target = target->label;
|
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([] {
|
request->onDisconnect([] {
|
||||||
if (!upload_done && !upload_target.isEmpty()) {
|
if (!upload_done && !upload_target.isEmpty()) {
|
||||||
|
LOG("Upload aborted at %uKB, the client disconnected", static_cast<unsigned>(ota.progress() / 1024));
|
||||||
ota.abort();
|
ota.abort();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -658,17 +693,27 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
|
|||||||
if (!ota.write(data, len)) {
|
if (!ota.write(data, len)) {
|
||||||
LOG("Write failed at %u: %s", static_cast<unsigned>(ota.progress()), ota.errorString());
|
LOG("Write failed at %u: %s", static_cast<unsigned>(ota.progress()), ota.errorString());
|
||||||
ota.abort();
|
ota.abort();
|
||||||
uploadFail(request, 500);
|
uploadFail(request, 500, "write failed");
|
||||||
return;
|
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) {
|
if (final) {
|
||||||
// on success this also makes the new partition the boot partition
|
// on success this also makes the new partition the boot partition
|
||||||
if (!ota.end()) {
|
if (!ota.end()) {
|
||||||
LOG("Upload failed: %s", ota.errorString());
|
LOG("Upload failed: %s", ota.errorString());
|
||||||
uploadFail(request, 500);
|
uploadFail(request, 500, "image check failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
LOG("Upload finished, the image is verified");
|
||||||
upload_done = true;
|
upload_done = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -785,6 +830,17 @@ static bool ethConnected() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void startNetwork() {
|
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.persistent(false);
|
||||||
WiFi.setHostname(settings.hostname.c_str()); // also the default for the Ethernet interface
|
WiFi.setHostname(settings.hostname.c_str()); // also the default for the Ethernet interface
|
||||||
WiFi.mode(WIFI_AP);
|
WiFi.mode(WIFI_AP);
|
||||||
@@ -826,6 +882,9 @@ void setup() {
|
|||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
LOG("EMS-ESP Recovery v%s", RECOVERY_VERSION);
|
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();
|
loadSettings();
|
||||||
loadRecoveryReason();
|
loadRecoveryReason();
|
||||||
loadPartitions();
|
loadPartitions();
|
||||||
@@ -841,6 +900,8 @@ void loop() {
|
|||||||
was_connected = WiFi.isConnected();
|
was_connected = WiFi.isConnected();
|
||||||
if (was_connected) {
|
if (was_connected) {
|
||||||
LOG("WiFi connected, IP %s", WiFi.localIP().toString().c_str());
|
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)
|
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
|
# Create custom target that only runs the script and then exits, without continuing with the pio workflow
|
||||||
env.AddCustomTarget(
|
env.AddCustomTarget(
|
||||||
name="build",
|
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
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -52,7 +53,29 @@ def build():
|
|||||||
env.Exit(1)
|
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...")
|
print("Building Recovery WebUI...")
|
||||||
build()
|
build()
|
||||||
else:
|
else:
|
||||||
|
|||||||
+90
-12
@@ -568,7 +568,8 @@ void System::get_partition_info() {
|
|||||||
|
|
||||||
void System::recovery_installed() {
|
void System::recovery_installed() {
|
||||||
#ifdef EMSESP_HAS_RECOVERY
|
#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);
|
const esp_partition_t * factory = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, nullptr);
|
||||||
if (factory != nullptr) {
|
if (factory != nullptr) {
|
||||||
char c[20];
|
char c[20];
|
||||||
@@ -576,8 +577,11 @@ void System::recovery_installed() {
|
|||||||
if (EMSESP::nvs_.isKey(factory->label)) {
|
if (EMSESP::nvs_.isKey(factory->label)) {
|
||||||
EMSESP::nvs_.remove(factory->label);
|
EMSESP::nvs_.remove(factory->label);
|
||||||
}
|
}
|
||||||
if (EMSESP::nvs_.isKey(c)) {
|
const time_t now = time(nullptr);
|
||||||
EMSESP::nvs_.remove(c);
|
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
|
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
|
#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 {
|
std::string System::recovery_version() const {
|
||||||
#ifdef EMSESP_HAS_RECOVERY
|
#ifdef EMSESP_HAS_RECOVERY
|
||||||
esp_app_desc_t desc;
|
esp_app_desc_t desc;
|
||||||
@@ -3511,21 +3528,44 @@ bool System::uploadFirmwareURL(const char * url) {
|
|||||||
break;
|
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
|
// 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");
|
LOG_ERROR("Firmware upload failed - invalid size");
|
||||||
return false; // error
|
return false; // error
|
||||||
}
|
}
|
||||||
|
|
||||||
OtaUpdater::confirmRunningApp();
|
OtaUpdater::confirmRunningApp();
|
||||||
|
|
||||||
// check we have enough space for the upload in the ota partition
|
if (is_recovery) {
|
||||||
if (!Update.begin(firmware_size)) {
|
#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
|
||||||
LOG_ERROR("Firmware upload failed - no space");
|
LOG_ERROR("Firmware upload failed - no space");
|
||||||
return false; // error
|
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
|
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
|
// via the systemStatus action, which runs on the AsyncTCP task while we're blocked here
|
||||||
if (EMSESP::system_.systemStatus() < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) {
|
if (EMSESP::system_.systemStatus() < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) {
|
||||||
LOG_WARNING("Firmware upload cancelled at %u of %d bytes", (unsigned)total_read, firmware_size);
|
LOG_WARNING("Firmware upload cancelled at %u of %d bytes", (unsigned)total_read, firmware_size);
|
||||||
Update.abort(); // release the OTA partition handle so a later attempt can start cleanly
|
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
|
ssl_client.stop(); // drop the connection
|
||||||
saved_url.clear(); // prevent it from downloading again
|
saved_url.clear(); // prevent it from downloading again
|
||||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
|
||||||
@@ -3573,6 +3617,9 @@ bool System::uploadFirmwareURL(const char * url) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
LOG_ERROR("Firmware upload failed - read stalled at %u of %d bytes", (unsigned)total_read, firmware_size);
|
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);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3585,6 +3632,9 @@ bool System::uploadFirmwareURL(const char * url) {
|
|||||||
size_t n = stream->readBytes(buf, want);
|
size_t n = stream->readBytes(buf, want);
|
||||||
if (n == 0) {
|
if (n == 0) {
|
||||||
LOG_ERROR("Firmware upload failed - read returned 0 at %u of %d bytes", (unsigned)total_read, firmware_size);
|
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);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3594,14 +3644,33 @@ bool System::uploadFirmwareURL(const char * url) {
|
|||||||
if (!magic_ok) {
|
if (!magic_ok) {
|
||||||
if (buf[0] != 0xE9) {
|
if (buf[0] != 0xE9) {
|
||||||
LOG_ERROR("Firmware upload failed - bad magic byte 0x%02X (expected 0xE9, not an ESP32 firmware image?)", buf[0]);
|
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);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
magic_ok = true;
|
magic_ok = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Update.write(buf, n) != n) {
|
const bool written = is_recovery ? ota.write(buf, n) : (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());
|
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);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3618,13 +3687,22 @@ bool System::uploadFirmwareURL(const char * url) {
|
|||||||
yield();
|
yield();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Update.end(true)) {
|
if (!(is_recovery ? ota.end() : Update.end(true))) {
|
||||||
LOG_ERROR("Firmware upload failed - %s", Update.errorString());
|
LOG_ERROR("Firmware upload failed - %s", is_recovery ? ota.errorString() : Update.errorString());
|
||||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||||
return false; // error
|
return false; // error
|
||||||
}
|
}
|
||||||
|
|
||||||
saved_url.clear(); // prevent from downloading again
|
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...");
|
LOG_INFO("Firmware uploaded successfully. Restarting...");
|
||||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);
|
||||||
#endif
|
#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
|
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(); // 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
|
std::string recovery_version() const; // empty if there's no recovery firmware
|
||||||
|
|
||||||
// commands
|
// commands
|
||||||
@@ -391,6 +393,8 @@ class System {
|
|||||||
|
|
||||||
void set_partition_install_date();
|
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
|
// 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
|
static constexpr uint32_t FIRMWARE_HEALTHY_UPTIME = 120; // seconds
|
||||||
// consecutive crashes before falling back to the recovery firmware in the factory partition
|
// consecutive crashes before falling back to the recovery firmware in the factory partition
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ WebStatusService::WebStatusService(AsyncWebServer * server, SecurityManager * se
|
|||||||
// This contains both system & hardware Status to avoid having multiple costly endpoints
|
// This contains both system & hardware Status to avoid having multiple costly endpoints
|
||||||
// This is also used for polling during the SystemMonitor to see if EMS-ESP is alive
|
// This is also used for polling during the SystemMonitor to see if EMS-ESP is alive
|
||||||
void WebStatusService::systemStatus(AsyncWebServerRequest * request) {
|
void WebStatusService::systemStatus(AsyncWebServerRequest * request) {
|
||||||
EMSESP::system_.refreshHeapMem(); // refresh free heap and max alloc heap
|
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);
|
auto * response = new PsramAsyncJsonResponse(false);
|
||||||
JsonObject root = response->getRoot();
|
JsonObject root = response->getRoot();
|
||||||
|
|||||||
Reference in New Issue
Block a user