diff --git a/interface/package.json b/interface/package.json index b32094173..a62086556 100644 --- a/interface/package.json +++ b/interface/package.json @@ -62,10 +62,10 @@ "prettier": "^3.7.4", "rollup-plugin-visualizer": "^6.0.5", "terser": "^5.44.1", - "typescript-eslint": "^8.50.0", + "typescript-eslint": "^8.50.1", "vite": "^7.3.0", "vite-plugin-imagemin": "^0.6.1", "vite-tsconfig-paths": "^6.0.3" }, - "packageManager": "pnpm@10.26.1+sha512.664074abc367d2c9324fdc18037097ce0a8f126034160f709928e9e9f95d98714347044e5c3164d65bd5da6c59c6be362b107546292a8eecb7999196e5ce58fa" + "packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6" } diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index d255316e1..571d3c740 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -118,8 +118,8 @@ importers: specifier: ^5.44.1 version: 5.44.1 typescript-eslint: - specifier: ^8.50.0 - version: 8.50.0(eslint@9.39.2)(typescript@5.9.3) + specifier: ^8.50.1 + version: 8.50.1(eslint@9.39.2)(typescript@5.9.3) vite: specifier: ^7.3.0 version: 7.3.0(@types/node@25.0.3)(terser@5.44.1) @@ -888,63 +888,63 @@ packages: '@types/svgo@2.6.4': resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==} - '@typescript-eslint/eslint-plugin@8.50.0': - resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} + '@typescript-eslint/eslint-plugin@8.50.1': + resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.50.0 + '@typescript-eslint/parser': ^8.50.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.50.0': - resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==} + '@typescript-eslint/parser@8.50.1': + resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.50.0': - resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} + '@typescript-eslint/project-service@8.50.1': + resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.50.0': - resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} + '@typescript-eslint/scope-manager@8.50.1': + resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.50.0': - resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==} + '@typescript-eslint/tsconfig-utils@8.50.1': + resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.50.0': - resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==} + '@typescript-eslint/type-utils@8.50.1': + resolution: {integrity: sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.50.0': - resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} + '@typescript-eslint/types@8.50.1': + resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.50.0': - resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==} + '@typescript-eslint/typescript-estree@8.50.1': + resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.50.0': - resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==} + '@typescript-eslint/utils@8.50.1': + resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.50.0': - resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} + '@typescript-eslint/visitor-keys@8.50.1': + resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -1614,8 +1614,8 @@ packages: resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} hasBin: true - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -2924,8 +2924,8 @@ packages: peerDependencies: typescript: '>=3.5.1' - typescript-eslint@8.50.0: - resolution: {integrity: sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==} + typescript-eslint@8.50.1: + resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3583,7 +3583,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + fastq: 1.20.1 '@paralleldrive/cuid2@2.3.1': dependencies: @@ -3803,14 +3803,14 @@ snapshots: dependencies: '@types/node': 25.0.3 - '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/type-utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 eslint: 9.39.2 ignore: 7.0.5 natural-compare: 1.4.0 @@ -3819,41 +3819,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 debug: 4.4.3 eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.50.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) - '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.50.0': + '@typescript-eslint/scope-manager@8.50.1': dependencies: - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 - '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.50.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.50.1(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.2 ts-api-utils: 2.1.0(typescript@5.9.3) @@ -3861,14 +3861,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.50.0': {} + '@typescript-eslint/types@8.50.1': {} - '@typescript-eslint/typescript-estree@8.50.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.50.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/project-service': 8.50.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 @@ -3878,20 +3878,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.50.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/utils@8.50.1(eslint@9.39.2)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.50.0': + '@typescript-eslint/visitor-keys@8.50.1': dependencies: - '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/types': 8.50.1 eslint-visitor-keys: 4.2.1 acorn-jsx@5.3.2(acorn@8.15.0): @@ -4662,7 +4662,7 @@ snapshots: dependencies: strnum: 1.1.2 - fastq@1.19.1: + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -5912,12 +5912,12 @@ snapshots: dependencies: typescript: 5.9.3 - typescript-eslint@8.50.0(eslint@9.39.2)(typescript@5.9.3): + typescript-eslint@8.50.1(eslint@9.39.2)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: diff --git a/interface/src/app/status/Version.tsx b/interface/src/app/status/Version.tsx index f033224e4..363a090fa 100644 --- a/interface/src/app/status/Version.tsx +++ b/interface/src/app/status/Version.tsx @@ -269,6 +269,52 @@ const InstallDialog = memo( } ); +const InstallPreviousDialog = memo( + ({ + openInstallPreviousDialog, + version, + partition, + LL, + onClose, + onInstall + }: { + openInstallPreviousDialog: boolean; + version: string; + partition: string; + LL: TranslationFunctions; + onClose: () => void; + onInstall: (partition: string) => void; + }) => { + return ( + + Rollback Firmware + + {LL.INSTALL_VERSION(LL.INSTALL(), version)} + + + + + + + + ); + } +); + // Helper function moved outside component const getPlatform = (data: VersionData): string => { return `${data.esp_platform}-${data.flash_chip_size >= 16384 ? '16MB' : '4MB'}${data.psram ? '+' : ''}`; @@ -281,6 +327,12 @@ const Version = () => { // State management const [restarting, setRestarting] = useState(false); const [openInstallDialog, setOpenInstallDialog] = useState(false); + + const [previousVersion, setPreviousVersion] = useState(''); + const [previousPartition, setPreviousPartition] = useState(''); + const [openInstallPreviousDialog, setOpenInstallPreviousDialog] = + useState(false); + const [usingDevVersion, setUsingDevVersion] = useState(false); const [fetchDevVersion, setFetchDevVersion] = useState(false); const [devUpgradeAvailable, setDevUpgradeAvailable] = useState(false); @@ -299,6 +351,11 @@ const Version = () => { setStableUpgradeAvailable(data.stable_upgradeable); }); + const { send: sendSetPartition } = useRequest( + (partition: string) => callAction({ action: 'setPartition', param: partition }), + { immediate: false } + ); + const { data, send: loadData, @@ -331,12 +388,12 @@ const Version = () => { ); const doRestart = useCallback(async () => { - setRestarting(true); await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch( (error: Error) => { toast.error(error.message); } ); + setRestarting(true); }, [sendAPI]); const installFirmwareURL = useCallback( @@ -344,11 +401,27 @@ const Version = () => { await sendUploadURL(url).catch((error: Error) => { toast.error(error.message); }); - setRestarting(true); + doRestart(); }, [sendUploadURL] ); + const installPreviousFirmware = useCallback( + async (partition: string) => { + await sendSetPartition(partition).catch((error: Error) => { + toast.error(error.message); + }); + setRestarting(true); + }, + [sendSetPartition] + ); + + const showPreviousDialog = useCallback((version: string, partition: string) => { + setOpenInstallPreviousDialog(true); + setPreviousVersion(version); + setPreviousPartition(partition); + }, []); + const showFirmwareDialog = useCallback((useDevVersion: boolean) => { setFetchDevVersion(useDevVersion); setOpenInstallDialog(true); @@ -358,6 +431,10 @@ const Version = () => { setOpenInstallDialog(false); }, []); + const closeInstallPreviousDialog = useCallback(() => { + setOpenInstallPreviousDialog(false); + }, []); + const handleVersionInfoClose = useCallback(() => { setShowVersionInfo(0); }, []); @@ -531,6 +608,28 @@ const Version = () => { alignItems: 'baseline' }} > + + {LL.PREVIOUS_VERSIONS()} + + + {data.partitions.map((partition) => ( + + v{partition.version} ({partition.partition}: {partition.size} + {' KB'}) + + + ))} + + {LL.STABLE()} @@ -591,6 +690,14 @@ const Version = () => { onClose={closeInstallDialog} onInstall={installFirmwareURL} /> + {LL.UPLOAD()} diff --git a/interface/src/i18n/cz/index.ts b/interface/src/i18n/cz/index.ts index 704ba83cc..3e7e28c41 100644 --- a/interface/src/i18n/cz/index.ts +++ b/interface/src/i18n/cz/index.ts @@ -355,7 +355,8 @@ const cz: Translation = { SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi', FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru', NO_DATA: 'Žádná data', - USER_PROFILE: 'Uživatelský profil' + USER_PROFILE: 'Uživatelský profil', + PREVIOUS_VERSIONS: 'Předchozí verze' }; export default cz; diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index 1e578fc24..8cbed793f 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -355,7 +355,8 @@ const de: Translation = { SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln', FIRMWARE_VERSION_INFO: 'Firmware-Versionsinformation', NO_DATA: 'Keine Daten', - USER_PROFILE: 'Benutzerprofil' + USER_PROFILE: 'Benutzerprofil', + PREVIOUS_VERSIONS: 'Vorherige Versionen' }; export default de; diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index 38a6e3185..de2419099 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -355,7 +355,8 @@ const en: Translation = { SWITCH_RELEASE_TYPE: 'Switch to {0} release', FIRMWARE_VERSION_INFO: 'Firmware Version Information', NO_DATA: 'No data', - USER_PROFILE: 'User Profile' + USER_PROFILE: 'User Profile', + PREVIOUS_VERSIONS: 'Previous Versions' }; export default en; diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index cfbbaa81d..9c69ff857 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -355,7 +355,8 @@ const fr: Translation = { SWITCH_RELEASE_TYPE: 'Passer à la version {0}', FIRMWARE_VERSION_INFO: 'Informations sur la version du firmware', NO_DATA: 'Aucune donnée', - USER_PROFILE: 'Profil utilisateur' + USER_PROFILE: 'Profil utilisateur', + PREVIOUS_VERSIONS: 'Versions précédentes' }; export default fr; diff --git a/interface/src/i18n/it/index.ts b/interface/src/i18n/it/index.ts index 3dba434f6..04b6072c2 100644 --- a/interface/src/i18n/it/index.ts +++ b/interface/src/i18n/it/index.ts @@ -355,7 +355,8 @@ const it: Translation = { SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio', FIRMWARE_VERSION_INFO: 'Informazioni sulla versione del firmware', NO_DATA: 'Nessun dato', - USER_PROFILE: 'Profilo utente' + USER_PROFILE: 'Profilo utente', + PREVIOUS_VERSIONS: 'Versioni precedenti' }; export default it; diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index 579202eb6..1ec6d8d07 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -355,7 +355,8 @@ const nl: Translation = { SWITCH_RELEASE_TYPE: 'Switch naar {0} release', FIRMWARE_VERSION_INFO: 'Informatie over firmwareversie', NO_DATA: 'Geen data', - USER_PROFILE: 'Gebruikersprofiel' + USER_PROFILE: 'Gebruikersprofiel', + PREVIOUS_VERSIONS: 'Vorige versies' }; export default nl; \ No newline at end of file diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index a2fbbaa6c..4cd78a5bf 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -355,7 +355,8 @@ const no: Translation = { SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse', FIRMWARE_VERSION_INFO: 'Informasjon om firmwareversjon', NO_DATA: 'Ingen data', - USER_PROFILE: 'Brukerprofil' + USER_PROFILE: 'Brukerprofil', + PREVIOUS_VERSIONS: 'Tidligere versjoner' }; export default no; diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index d50d6cefa..9895ec0de 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -355,7 +355,8 @@ const pl: BaseTranslation = { SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie', FIRMWARE_VERSION_INFO: 'Informacje o wersji firmware', NO_DATA: 'Brak danych', - USER_PROFILE: 'Profil użytkownika' + USER_PROFILE: 'Profil użytkownika', + PREVIOUS_VERSIONS: 'Poprzednie wersje' }; export default pl; diff --git a/interface/src/i18n/sk/index.ts b/interface/src/i18n/sk/index.ts index 52e6f9c75..df5359000 100644 --- a/interface/src/i18n/sk/index.ts +++ b/interface/src/i18n/sk/index.ts @@ -355,7 +355,8 @@ const sk: Translation = { SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu', FIRMWARE_VERSION_INFO: 'Informácie o verzii firmware', NO_DATA: 'Žiadne dáta', - USER_PROFILE: 'Profil používateľa' + USER_PROFILE: 'Profil používateľa', + PREVIOUS_VERSIONS: 'Predchádzajúce verzie' }; export default sk; diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index 644484269..001a8d0b6 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -355,7 +355,8 @@ const sv: Translation = { SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva', FIRMWARE_VERSION_INFO: 'Information om firmwareversion', NO_DATA: 'Ingen data', - USER_PROFILE: 'Användarprofil' + USER_PROFILE: 'Användarprofil', + PREVIOUS_VERSIONS: 'Tidigare versioner' }; export default sv; diff --git a/interface/src/i18n/tr/index.ts b/interface/src/i18n/tr/index.ts index 79824decd..3933401b8 100644 --- a/interface/src/i18n/tr/index.ts +++ b/interface/src/i18n/tr/index.ts @@ -355,7 +355,8 @@ const tr: Translation = { SWITCH_RELEASE_TYPE: '{0} sürümüne geç', FIRMWARE_VERSION_INFO: 'Firmware Sürüm Bilgisi', NO_DATA: 'Hiçbir veri yok', - USER_PROFILE: 'Kullanıcı Profili' + USER_PROFILE: 'Kullanıcı Profili', + PREVIOUS_VERSIONS: 'Önceki Sürümler' }; export default tr; diff --git a/interface/src/types/system.ts b/interface/src/types/system.ts index ace517688..f5d6602f7 100644 --- a/interface/src/types/system.ts +++ b/interface/src/types/system.ts @@ -52,7 +52,13 @@ export interface SystemStatus { model: string; has_loader: boolean; has_partition: boolean; + partitions: { + partition: string; + version: string; + size: number; + }[]; status: number; // System Status Codes which matches SYSTEM_STATUS in System.h + developer_mode: boolean; temperature?: number; } diff --git a/mock-api/restServer.ts b/mock-api/restServer.ts index 5ee3e539c..3710f4443 100644 --- a/mock-api/restServer.ts +++ b/mock-api/restServer.ts @@ -106,6 +106,20 @@ let system_status = { psram_size: 8189, free_psram: 8166, has_loader: true, + has_partition: true, + partitions: [ + { + partition: 'app1', + version: '3.7.3-dev.41', + size: 4672 + }, + { + partition: 'factory', + version: '3.7.3-dev.39', + size: 4672 + }, + ], + developer_mode: true, model: '', // model: 'BBQKees Electronics EMS Gateway E32 V2 (E32 V2.0 P3/2024011)', // status: 0, @@ -276,10 +290,10 @@ function updateMask(entity: any, de: any, dd: any) { const old_custom_name = dd.nodes[dd_objIndex].cn; console.log( 'comparing names, old (' + - old_custom_name + - ') with new (' + - new_custom_name + - ')' + old_custom_name + + ') with new (' + + new_custom_name + + ')' ); if (old_custom_name !== new_custom_name) { changed = true; @@ -375,15 +389,15 @@ function check_upgrade(version: string) { console.log( 'Upgrade this version (' + - THIS_VERSION + - ') to dev (' + - dev_version + - ') is ' + - (DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') + - ' and to stable (' + - stable_version + - ') is ' + - (STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') + THIS_VERSION + + ') to dev (' + + dev_version + + ') is ' + + (DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') + + ' and to stable (' + + stable_version + + ') is ' + + (STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') ); data = { emsesp_version: THIS_VERSION, @@ -5135,6 +5149,10 @@ router // reset MQTT console.log('resetting MQTT...'); return status(200); + } else if (action === 'setPartition') { + // set partition + console.log('setting partition to', content.param); + return status(200); } } return status(404); // cmd not found diff --git a/src/core/system.cpp b/src/core/system.cpp index ae0e862d0..c0a1e15f9 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -21,9 +21,11 @@ #ifndef EMSESP_STANDALONE #include "esp_ota_ops.h" +#include "esp_partition.h" #endif #include +#include #include @@ -293,6 +295,100 @@ void System::store_nvs_values() { EMSESP::nvs_.end(); } +// Build up a list of all partitions and their version info +void System::get_partition_info() { + partition_info_.clear(); // clear existing data + +#ifdef EMSESP_STANDALONE + // dummy data for standalone mode + partition_info_["app0"] = {EMSESP_APP_VERSION, 0}; + partition_info_["app1"] = {"", 0}; + partition_info_["factory"] = {"", 0}; + partition_info_["boot"] = {"", 0}; +#else + + auto current_partition = (const char *)esp_ota_get_running_partition()->label; + + // update the current version and partition name in NVS, if needed (to save on flash wearing) + if (EMSESP::nvs_.isKey(current_partition)) { + if (EMSESP::nvs_.getString(current_partition) != EMSESP_APP_VERSION) { + EMSESP::nvs_.putString(current_partition, EMSESP_APP_VERSION); + } + } else { + EMSESP::nvs_.putString(current_partition, EMSESP_APP_VERSION); // create new entry + } + + // Loop through all available partitions and update map with the version info pulled from NVS + // Partitions can be app0, app1, factory, boot + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr); + uint64_t buffer; + bool is_valid; + + while (it != nullptr) { + is_valid = true; + const esp_partition_t * part = esp_partition_get(it); + + if (part->label != nullptr && part->label[0] != '\0') { + // check if part is valid and not empty + esp_partition_read(part, 0, &buffer, 8); + if (buffer == 0xFFFFFFFFFFFFFFFF) { + // skip this partition + is_valid = false; + } + } + + // get the version from the NVS store, and add to map + if (is_valid) { + PartitionInfo info; + info.size = part->size / 1024; // in KB + if (EMSESP::nvs_.isKey(part->label)) { + info.version = EMSESP::nvs_.getString(part->label).c_str(); + } else { + info.version = ""; // no version, empty string + } + partition_info_[part->label] = info; + } + + it = esp_partition_next(it); + } + esp_partition_iterator_release(it); +#endif +} + +// sets the partition to use on the next restart +bool System::set_partition(const char * partitionname) { +#ifdef EMSESP_STANDALONE + return true; +#else + if (partitionname == nullptr) { + return false; + } + + // Find the partition by label + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, partitionname); + if (it == nullptr) { + return false; // partition not found + } + + const esp_partition_t * partition = esp_partition_get(it); + esp_partition_iterator_release(it); + + if (partition == nullptr) { + return false; + } + + // Set the boot partition + esp_err_t err = esp_ota_set_boot_partition(partition); + if (err != ESP_OK) { + return false; + } + + // initiate the restart + EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED); + return true; +#endif +} + // restart EMS-ESP // app0 or app1 // on 16MB we have the additional boot and factory partitions @@ -302,24 +398,25 @@ void System::system_restart(const char * partitionname) { if (partitionname != nullptr) { // Factory partition - label will be "factory" const esp_partition_t * partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL); - if (partition && strcmp(partition->label, partitionname) == 0) { + if (partition && !strcmp(partition->label, partitionname)) { esp_ota_set_boot_partition(partition); } else // try and find the partition by name - if (strcmp(esp_ota_get_running_partition()->label, partitionname) != 0) { + if (strcmp(esp_ota_get_running_partition()->label, partitionname)) { + // not found, get next one in cycle partition = esp_ota_get_next_update_partition(nullptr); if (!partition) { LOG_ERROR("Partition '%s' not found", partitionname); return; } - if (strcmp(partition->label, partitionname) != 0 && strcmp(partitionname, "boot") != 0) { + if (strcmp(partition->label, partitionname) && strcmp(partitionname, "boot") != 0) { partition = esp_ota_get_next_update_partition(partition); if (!partition || strcmp(partition->label, partitionname)) { LOG_ERROR("Partition '%s' not found", partitionname); return; } } - // check if partition is empty + // error if partition is empty uint64_t buffer; esp_partition_read(partition, 0, &buffer, 8); if (buffer == 0xFFFFFFFFFFFFFFFF) { @@ -456,6 +553,8 @@ void System::store_settings(WebSettings & settings) { // Starts up the UART Serial bridge void System::start() { + get_partition_info(); // get the partition info + #ifndef EMSESP_STANDALONE // disable bluetooth module // periph_module_disable(PERIPH_BT_MODULE); @@ -916,7 +1015,7 @@ void System::led_monitor() { // handle the step events (on odd numbers 3,5,7,etc). see if we need to turn on a LED // 1 flash is the EMS bus is not connected // 2 flashes if the network (wifi or ethernet) is not connected - // 3 flashes is both the bus and the network are not connected. Then you know you're truly f*cked. + // 3 flashes is both the bus and the network are not connected if (led_type_) { if (led_flash_step_ == 3) { if ((healthcheck_ & HEALTHCHECK_NO_NETWORK) == HEALTHCHECK_NO_NETWORK) { @@ -1027,7 +1126,6 @@ void System::show_system(uuid::console::Shell & shell) { #ifndef EMSESP_STANDALONE shell.printfln(" Platform: %s (%s)", EMSESP_PLATFORM, ESP.getChipModel()); shell.printfln(" Model: %s", getBBQKeesGatewayDetails().c_str()); - shell.printfln(" Partition: %s", esp_ota_get_running_partition()->label); #endif shell.printfln(" Language: %s", locale().c_str()); shell.printfln(" Board profile: %s", board_profile().c_str()); @@ -1055,21 +1153,33 @@ void System::show_system(uuid::console::Shell & shell) { shell.printfln(" PSRAM: not available"); } // GPIOs - shell.printf(" GPIO in use (%d):", used_gpios_.size()); + shell.println(" GPIOs:"); + shell.printf(" in use:"); for (const auto & gpio : used_gpios_) { shell.printf(" %d", gpio); } - shell.println(); + shell.printfln(" (total %d)", used_gpios_.size()); auto available = available_gpios(); - shell.printf(" GPIO available (%d):", available.size()); + shell.printf(" available:"); for (const auto & gpio : available) { shell.printf(" %d", gpio); } - shell.println(); - shell.println(); + shell.printfln(" (total %d)", available.size()); + // List all partitions and their version info + shell.println(" Partitions:"); + for (const auto & partition : partition_info_) { + if (partition.second.version.empty()) { + continue; // no version, empty string + } + shell.printfln(" %s: v%s (%d KB) %s", + partition.first.c_str(), + partition.second.version.c_str(), + partition.second.size, + (esp_ota_get_running_partition()->label == partition.first) ? " ** active **" : ""); + } + shell.println(); shell.println("Network:"); - switch (WiFi.status()) { case WL_IDLE_STATUS: shell.printfln(" Status: Idle"); diff --git a/src/core/system.h b/src/core/system.h index 23d3c5b6e..154e4ee3e 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -74,6 +74,11 @@ enum SYSTEM_STATUS : uint8_t { enum FUSE_VALUE : uint8_t { ALL = 0, MFG = 1, MODEL = 2, BOARD = 3, REV = 4, BATCH = 5, FUSE = 6 }; +struct PartitionInfo { + std::string version; + size_t size; +}; + class System { public: void start(); @@ -104,6 +109,7 @@ class System { void store_nvs_values(); void system_restart(const char * partition = nullptr); + void get_partition_info(); void show_mem(const char * note); void store_settings(class WebSettings & settings); @@ -363,6 +369,10 @@ class System { static void remove_gpio(uint8_t pin, bool also_system = false); // remove a gpio from both valid (optional) and used lists + // Partition info map: partition name -> {version, size} + std::map, AllocatorPSRAM>> partition_info_; + static bool set_partition(const char * partitionname); + private: static uuid::log::Logger logger_; @@ -404,6 +414,7 @@ class System { static std::vector> string_range_to_vector(const std::string & range); + // GPIOs static std::vector> valid_system_gpios_; // list of valid GPIOs for the ESP32 board that can be used static std::vector> used_gpios_; // list of GPIOs used by the application static std::vector> snapshot_used_gpios_; // snapshot of the used GPIOs diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp index e8d2a565b..1f604822d 100644 --- a/src/web/WebStatusService.cpp +++ b/src/web/WebStatusService.cpp @@ -146,6 +146,22 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) { root["has_partition"] = false; } + // get the partition info + EMSESP::system_.get_partition_info(); + JsonArray partitions = root["partitions"].to(); + for (const auto & partition : EMSESP::system_.partition_info_) { + // Skip partition if it's the running one, version is empty, or size is 0 + if (partition.first == (const char *)esp_ota_get_running_partition()->label || partition.second.version.empty() || partition.second.size == 0) { + continue; + } + JsonObject part = partitions.add(); + part["partition"] = partition.first; + part["version"] = partition.second.version; + part["size"] = partition.second.size; + } + + root["developer_mode"] = EMSESP::system_.developer_mode(); + // Also used in SystemMonitor.tsx root["status"] = EMSESP::system_.systemStatus(); // send the status. See System.h for status codes if (EMSESP::system_.systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART) { @@ -185,6 +201,8 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json) if (action == "checkUpgrade") { ok = checkUpgrade(root, param); // param could be empty, if so only send back version + } else if (action == "setPartition") { + ok = EMSESP::system_.set_partition(param.c_str()); } else if (action == "export") { if (has_param) { ok = exportData(root, param);