From 221037eb1983f11a45f0ca1d21f7a4eee4fc371f Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Fri, 3 Apr 2026 05:04:28 -0400 Subject: [PATCH 1/5] Adds initial support for the x3 device Note: The flash crosspoint/english/chinese firmware buttons have not been updated to support x3 yet --- src/app/page.tsx | 45 +++++++-- src/esp/EspController.ts | 57 ++++++++++-- src/esp/useEspOperations.ts | 177 ++++++++++++++++++++++++++---------- 3 files changed, 214 insertions(+), 65 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index bedc09c..2defe32 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,7 +10,10 @@ import { Alert, Stack, Flex, + HStack, + Text, } from '@chakra-ui/react'; +import type { DeviceModel } from '@/esp/useEspOperations'; import FileUpload, { FileUploadHandle } from '@/components/FileUpload'; import Steps from '@/components/Steps'; import { useEspOperations } from '@/esp/useEspOperations'; @@ -20,7 +23,8 @@ import { } from '@/remote/firmwareFetcher'; export default function Home() { - const { actions, stepData, isRunning } = useEspOperations(); + const { actions, stepData, isRunning, deviceModel, setDeviceModel } = + useEspOperations(); const [officialFirmwareVersions, setOfficialFirmwareVersions] = useState<{ en: string; ch: string; @@ -41,6 +45,22 @@ export default function Home() { return ( + + Device model + + {(['x4', 'x3'] as const).map((model) => ( + + ))} + + + @@ -208,8 +228,8 @@ export default function Home() { Change device language Before starting the process, it is recommended to change the device - language to English. To do this, select “Settings” icon, then click - “OK / Confirm” button and “OK / Confirm” again until English is + language to English. To do this, select “Settings" icon, then click + “OK / Confirm" button and “OK / Confirm" again until English is shown. Otherwise, the language will still be Chinese after flashing and you may not notice changes. @@ -220,13 +240,22 @@ export default function Home() { Device restart instructions - Once you complete a write operation, you will need to restart your - device by pressing and releasing the small “Reset” button near the - bottom right, followed quickly by pressing and holding of the main - power button for about 3 seconds. + {deviceModel === 'x3' ? ( +

+ Once you complete a write operation, disconnect the USB cable and + connect it again. +

+ ) : ( +

+ Once you complete a write operation, you will need to restart + your device by pressing and releasing the small “Reset” button + near the bottom right, followed quickly by pressing and holding + of the main power button for about 3 seconds. +

+ )}
); -} +} \ No newline at end of file diff --git a/src/esp/EspController.ts b/src/esp/EspController.ts index c472016..611c679 100644 --- a/src/esp/EspController.ts +++ b/src/esp/EspController.ts @@ -41,6 +41,24 @@ const PARTITION_TYPES: Record> = { }, }; +export interface PartitionLayout { + app0Offset: number; + app1Offset: number; + appSize: number; +} + +export const X4_PARTITION_LAYOUT: PartitionLayout = { + app0Offset: 0x10000, + app1Offset: 0x650000, + appSize: 0x640000, +}; + +export const X3_PARTITION_LAYOUT: PartitionLayout = { + app0Offset: 0x10000, + app1Offset: 0x780000, + appSize: 0x770000, +}; + export default class EspController { static async requestDevice() { if (!('serial' in navigator && navigator.serial)) { @@ -54,15 +72,19 @@ export default class EspController { }); } - static async fromRequestedDevice() { + static async fromRequestedDevice( + partitionLayout: PartitionLayout = X4_PARTITION_LAYOUT, + ) { const device = await this.requestDevice(); - return new EspController(device); + return new EspController(device, partitionLayout); } private espLoader; + private layout: PartitionLayout; - constructor(device: SerialPort) { + constructor(device: SerialPort, partitionLayout: PartitionLayout = X4_PARTITION_LAYOUT) { const transport = new Transport(device, false); + this.layout = partitionLayout; this.espLoader = new ESPLoader({ transport, baudrate: 115200, @@ -71,12 +93,16 @@ export default class EspController { }); } + setPartitionLayout(layout: PartitionLayout) { + this.layout = layout; + } + async connect() { await this.espLoader.main(); } async disconnect({ skipReset = false }: { skipReset?: boolean } = {}) { - await this.espLoader.after(skipReset ? 'no_reset' : 'hard_reset'); + await this.espLoader.after(skipReset ? 'no_reset_stub' : 'hard_reset'); await this.espLoader.transport.disconnect(); } @@ -180,8 +206,11 @@ export default class EspController { totalSize: number, ) => void, ) { - const offset = partitionLabel === 'app0' ? 0x10000 : 0x650000; - return this.espLoader.readFlash(offset, 0x640000, onPacketReceived); + const offset = + partitionLabel === 'app0' + ? this.layout.app0Offset + : this.layout.app1Offset; + return this.espLoader.readFlash(offset, this.layout.appSize, onPacketReceived); } async readAppPartitionForIdentification( @@ -206,7 +235,10 @@ export default class EspController { // In testing, most firmwares are identified within the first 25KB read, so reading the entire // partition is unnecessary in the majority of cases. - const baseOffset = partitionLabel === 'app0' ? 0x10000 : 0x650000; + const baseOffset = + partitionLabel === 'app0' + ? this.layout.app0Offset + : this.layout.app1Offset; return this.espLoader.readFlash( baseOffset + offset, @@ -224,8 +256,10 @@ export default class EspController { total: number, ) => void, ) { - if (data.length > 0x640000) { - throw new Error(`Data cannot be larger than 0x640000`); + if (data.length > this.layout.appSize) { + throw new Error( + `Data cannot be larger than 0x${this.layout.appSize.toString(16)}`, + ); } if (data.length < 0xf0000) { throw new Error( @@ -233,7 +267,10 @@ export default class EspController { ); } - const offset = partitionLabel === 'app0' ? 0x10000 : 0x650000; + const offset = + partitionLabel === 'app0' + ? this.layout.app0Offset + : this.layout.app1Offset; await this.writeData(data, offset, reportProgress); } diff --git a/src/esp/useEspOperations.ts b/src/esp/useEspOperations.ts index 57fcba8..774ed4d 100644 --- a/src/esp/useEspOperations.ts +++ b/src/esp/useEspOperations.ts @@ -14,21 +14,58 @@ import { } from '@/utils/firmwareIdentifier'; import OtaPartition, { OtaPartitionDetails } from './OtaPartition'; import useStepRunner from './useStepRunner'; -import EspController from './EspController'; - -const expectedPartitionTable = [ - { type: 'data-nvs', offset: 36864, size: 20480 }, - { type: 'data-ota', offset: 57344, size: 8192 }, - { type: 'app-ota_0', offset: 65536, size: 6553600 }, - { type: 'app-ota_1', offset: 6619136, size: 6553600 }, - { type: 'data-spiffs', offset: 13172736, size: 3538944 }, - { type: 'data-coredump', offset: 16711680, size: 65536 }, +import EspController, { + X3_PARTITION_LAYOUT, + X4_PARTITION_LAYOUT, +} from './EspController'; + +const x4PartitionTable = [ + { type: 'data-nvs', offset: 0x9000, size: 0x5000 }, + { type: 'data-ota', offset: 0xe000, size: 0x2000 }, + { type: 'app-ota_0', offset: 0x10000, size: 0x640000 }, + { type: 'app-ota_1', offset: 0x650000, size: 0x640000 }, + { type: 'data-spiffs', offset: 0xc90000, size: 0x360000 }, + { type: 'data-coredump', offset: 0xff0000, size: 0x10000 }, ]; +const x3PartitionTable = [ + { type: 'data-nvs', offset: 0x9000, size: 0x5000 }, + { type: 'data-ota', offset: 0xe000, size: 0x2000 }, + { type: 'app-ota_0', offset: 0x10000, size: 0x770000 }, + { type: 'app-ota_1', offset: 0x780000, size: 0x770000 }, + { type: 'data-spiffs', offset: 0xef0000, size: 0x100000 }, + { type: 'data-coredump', offset: 0xff0000, size: 0x10000 }, +]; + +type PartitionEntry = { type: string; offset: number; size: number }; + +function matchesPartitionTable( + actual: PartitionEntry[], + expected: PartitionEntry[], +) { + return ( + actual.length === expected.length && + expected.every( + (exp, i) => + actual[i]!.type === exp.type && + actual[i]!.offset === exp.offset && + actual[i]!.size === exp.size, + ) + ); +} + +export type DeviceModel = 'x4' | 'x3'; + export function useEspOperations() { const { stepData, initializeSteps, updateStepData, runStep } = useStepRunner(); const [isRunning, setIsRunning] = useState(false); + const [deviceModel, setDeviceModel] = useState('x4'); + + const resetStepName = + deviceModel === 'x3' + ? 'Disconnect (unplug and replug USB to restart)' + : 'Reset device'; const wrapWithRunning = (fn: (...a: Args) => Promise) => @@ -47,34 +84,46 @@ export function useEspOperations() { 'Read otadata partition', 'Flash app partition', 'Flash otadata partition', - 'Reset device', + resetStepName, ]); const espController = await runStep('Connect to device', async () => { - const c = await EspController.fromRequestedDevice(); + const c = await EspController.fromRequestedDevice( + deviceModel === 'x3' ? X3_PARTITION_LAYOUT : X4_PARTITION_LAYOUT, + ); await c.connect(); return c; }); await runStep('Validate partition table', async () => { const partitionTable = await espController.readPartitionTable(); - if ( - partitionTable.length !== expectedPartitionTable.length || - expectedPartitionTable.some( - (expected, index) => - partitionTable[index]!.type !== expected.type || - partitionTable[index]!.offset !== expected.offset || - partitionTable[index]!.size !== expected.size, - ) - ) { + + // X3 devices may have stock or CrossPoint partition layouts + const validTables = + deviceModel === 'x3' + ? [x3PartitionTable, x4PartitionTable] + : [x4PartitionTable]; + + const matched = validTables.find((t) => + matchesPartitionTable(partitionTable, t), + ); + + if (!matched) { throw new Error( - `Unexpected partition configuration. You can only use OTA fast flash controls on devices running CrossPoint or official firmware with the default partition table.\nGot ${JSON.stringify( + `Unexpected partition configuration for ${deviceModel.toUpperCase()}. Make sure you've selected the correct device model.\nGot ${JSON.stringify( partitionTable, null, 2, )}`, ); } + + // Update controller to use the detected layout + espController.setPartitionLayout( + matchesPartitionTable(partitionTable, x3PartitionTable) + ? X3_PARTITION_LAYOUT + : X4_PARTITION_LAYOUT, + ); }); const firmwareFile = await runStep('Download firmware', getFirmware); @@ -117,7 +166,9 @@ export function useEspOperations() { ); }); - await runStep('Reset device', () => espController.disconnect()); + await runStep(resetStepName, () => + espController.disconnect({ skipReset: deviceModel === 'x3' }), + ); }; const flashEnglishFirmware = async () => @@ -135,7 +186,7 @@ export function useEspOperations() { 'Read otadata partition', 'Flash app partition', 'Flash otadata partition', - 'Reset device', + resetStepName, ]); const fileData = await runStep('Read file', async () => { @@ -147,30 +198,42 @@ export function useEspOperations() { }); const espController = await runStep('Connect to device', async () => { - const c = await EspController.fromRequestedDevice(); + const c = await EspController.fromRequestedDevice( + deviceModel === 'x3' ? X3_PARTITION_LAYOUT : X4_PARTITION_LAYOUT, + ); await c.connect(); return c; }); await runStep('Validate partition table', async () => { const partitionTable = await espController.readPartitionTable(); - if ( - partitionTable.length !== expectedPartitionTable.length || - expectedPartitionTable.some( - (expected, index) => - partitionTable[index]!.type !== expected.type || - partitionTable[index]!.offset !== expected.offset || - partitionTable[index]!.size !== expected.size, - ) - ) { + + // X3 devices may have stock or CrossPoint partition layouts + const validTables = + deviceModel === 'x3' + ? [x3PartitionTable, x4PartitionTable] + : [x4PartitionTable]; + + const matched = validTables.find((t) => + matchesPartitionTable(partitionTable, t), + ); + + if (!matched) { throw new Error( - `Unexpected partition configuration. You can only use OTA fast flash controls on devices running CrossPoint or official firmware with the default partition table.\nGot ${JSON.stringify( + `Unexpected partition configuration for ${deviceModel.toUpperCase()}. Make sure you've selected the correct device model.\nGot ${JSON.stringify( partitionTable, null, 2, )}`, ); } + + // Update controller to use the detected layout + espController.setPartitionLayout( + matchesPartitionTable(partitionTable, x3PartitionTable) + ? X3_PARTITION_LAYOUT + : X4_PARTITION_LAYOUT, + ); }); const [otaPartition, backupPartitionLabel] = await runStep( @@ -211,7 +274,9 @@ export function useEspOperations() { ); }); - await runStep('Reset device', () => espController.disconnect()); + await runStep(resetStepName, () => + espController.disconnect({ skipReset: deviceModel === 'x3' }), + ); }; const saveFullFlash = async () => { @@ -222,7 +287,9 @@ export function useEspOperations() { ]); const espController = await runStep('Connect to device', async () => { - const c = await EspController.fromRequestedDevice(); + const c = await EspController.fromRequestedDevice( + deviceModel === 'x3' ? X3_PARTITION_LAYOUT : X4_PARTITION_LAYOUT, + ); await c.connect(); return c; }); @@ -248,7 +315,7 @@ export function useEspOperations() { 'Read file', 'Connect to device', 'Write flash', - 'Reset device', + resetStepName, ]); const fileData = await runStep('Read file', async () => { @@ -260,7 +327,9 @@ export function useEspOperations() { }); const espController = await runStep('Connect to device', async () => { - const c = await EspController.fromRequestedDevice(); + const c = await EspController.fromRequestedDevice( + deviceModel === 'x3' ? X3_PARTITION_LAYOUT : X4_PARTITION_LAYOUT, + ); await c.connect(); return c; }); @@ -271,7 +340,9 @@ export function useEspOperations() { ), ); - await runStep('Reset device', () => espController.disconnect()); + await runStep(resetStepName, () => + espController.disconnect({ skipReset: deviceModel === 'x3' }), + ); }; const readDebugOtadata = async () => { @@ -282,7 +353,9 @@ export function useEspOperations() { ]); const espController = await runStep('Connect to device', async () => { - const c = await EspController.fromRequestedDevice(); + const c = await EspController.fromRequestedDevice( + deviceModel === 'x3' ? X3_PARTITION_LAYOUT : X4_PARTITION_LAYOUT, + ); await c.connect(); return c; }); @@ -310,7 +383,9 @@ export function useEspOperations() { ]); const espController = await runStep('Connect to device', async () => { - const c = await EspController.fromRequestedDevice(); + const c = await EspController.fromRequestedDevice( + deviceModel === 'x3' ? X3_PARTITION_LAYOUT : X4_PARTITION_LAYOUT, + ); await c.connect(); return c; }); @@ -335,11 +410,13 @@ export function useEspOperations() { 'Connect to device', 'Read otadata partition', 'Flash otadata partition', - 'Reset device', + resetStepName, ]); const espController = await runStep('Connect to device', async () => { - const c = await EspController.fromRequestedDevice(); + const c = await EspController.fromRequestedDevice( + deviceModel === 'x3' ? X3_PARTITION_LAYOUT : X4_PARTITION_LAYOUT, + ); await c.connect(); return c; }); @@ -368,7 +445,9 @@ export function useEspOperations() { ), ); - await runStep('Reset device', () => espController.disconnect()); + await runStep(resetStepName, () => + espController.disconnect({ skipReset: deviceModel === 'x3' }), + ); return otaPartition; }; @@ -378,7 +457,7 @@ export function useEspOperations() { 'Read file', 'Connect to device', 'Write flash', - 'Reset device', + resetStepName, ]); await runStep( @@ -424,7 +503,7 @@ export function useEspOperations() { ); await runStep( - 'Reset device', + resetStepName, () => new Promise((resolve) => { setTimeout(resolve, 500); @@ -447,7 +526,9 @@ export function useEspOperations() { ]); const espController = await runStep('Connect to device', async () => { - const c = await EspController.fromRequestedDevice(); + const c = await EspController.fromRequestedDevice( + deviceModel === 'x3' ? X3_PARTITION_LAYOUT : X4_PARTITION_LAYOUT, + ); await c.connect(); return c; }); @@ -530,6 +611,8 @@ export function useEspOperations() { return { stepData, isRunning, + deviceModel, + setDeviceModel, actions: { flashEnglishFirmware: wrapWithRunning(flashEnglishFirmware), flashChineseFirmware: wrapWithRunning(flashChineseFirmware), From 625f7b40836d1a0dcd6de60e0c79de78a6df45d1 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Fri, 3 Apr 2026 18:18:19 -0400 Subject: [PATCH 2/5] Add X3 device model support to stock firmware fetcher Extends firmware fetching to support both X4 and X3 device models. X3 uses different API endpoints and has distinct fallback versions for Chinese and English firmware. The cache key is now scoped per device model to prevent conflicts. --- src/app/page.tsx | 5 ++- src/esp/useEspOperations.ts | 4 +- src/remote/firmwareFetcher.ts | 71 +++++++++++++++++++++++++++++------ 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 2defe32..dda2d3b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -36,12 +36,13 @@ export default function Home() { const appPartitionFileInput = useRef(null); useEffect(() => { - getOfficialFirmwareVersions().then((versions) => + setOfficialFirmwareVersions(null); + getOfficialFirmwareVersions(deviceModel).then((versions) => setOfficialFirmwareVersions(versions), ); getCommunityFirmwareRemoteData().then(setCommunityFirmwareVersions); - }, []); + }, [deviceModel]); return ( diff --git a/src/esp/useEspOperations.ts b/src/esp/useEspOperations.ts index 774ed4d..3aed9b2 100644 --- a/src/esp/useEspOperations.ts +++ b/src/esp/useEspOperations.ts @@ -172,9 +172,9 @@ export function useEspOperations() { }; const flashEnglishFirmware = async () => - flashRemoteFirmware(() => getOfficialFirmware('en')); + flashRemoteFirmware(() => getOfficialFirmware('en', deviceModel)); const flashChineseFirmware = async () => - flashRemoteFirmware(() => getOfficialFirmware('ch')); + flashRemoteFirmware(() => getOfficialFirmware('ch', deviceModel)); const flashCrossPointFirmware = async () => flashRemoteFirmware(() => getCommunityFirmware('CrossPoint')); diff --git a/src/remote/firmwareFetcher.ts b/src/remote/firmwareFetcher.ts index 7fecb86..af4cec9 100644 --- a/src/remote/firmwareFetcher.ts +++ b/src/remote/firmwareFetcher.ts @@ -2,6 +2,8 @@ import { getCache } from '@vercel/functions'; +type DeviceModel = 'x4' | 'x3'; + interface OfficialFirmwareData { change_log: string; download_url: string; @@ -21,7 +23,7 @@ interface CommunityFirmwareVersions { }; } -const firmwareVersionFallback: OfficialFirmwareVersions = { +const x4FirmwareVersionFallback: OfficialFirmwareVersions = { en: { change_log: '1. Optimize EPUB/TXT \r\n2. Optimize JPG speed \r\n3. Optimize Wi-Fi connection \r\n4. Optimize EPUB covers', @@ -38,23 +40,65 @@ const firmwareVersionFallback: OfficialFirmwareVersions = { }, }; -const chineseFirmwareCheckUrl = +const x3FirmwareVersionFallback: OfficialFirmwareVersions = { + en: { + change_log: '', + download_url: + 'http://8.216.34.42:5001/api/v1/download/ESP32C3_X3/V5.1.6/V5.1.6-X3-EN-PROD-0304_.bin?choose=1&lang=en', + version: 'V5.1.6', + }, + ch: { + change_log: '', + download_url: + 'https://domestic-upload-file-api.oss-cn-hangzhou.aliyuncs.com/admin_uploads/firmware/202603/26/751e134f-22b1-4a00-bbfa-0942593ef867/V5.2.13-X3-CH-PROD-0326_173844.bin', + version: 'V5.2.13', + }, +}; + +const x4ChineseFirmwareCheckUrl = 'http://47.122.74.33:5000/api/check-update?current_version=V3.0.1&device_type=ESP32C3'; -const englishFirmwareCheckUrl = +const x4EnglishFirmwareCheckUrl = 'http://gotaserver.xteink.com/api/check-update?current_version=V3.0.1&device_type=ESP32C3&device_id=1234'; +const x3ChineseFirmwareCheckUrl = + 'https://api-prod.xteink.cn/api/v1/check-update?current_version=V5.1.3&device_type=ESP32C3_X3&device_id=1052463&choose=1&lang=en'; -export async function getOfficialFirmwareRemoteData(): Promise { +export async function getOfficialFirmwareRemoteData( + deviceModel: DeviceModel, +): Promise { const cache = getCache(); - const cacheKey = 'firmware-versions.official.v1'; + const cacheKey = `firmware-versions.official.${deviceModel}.v1`; + const fallback = + deviceModel === 'x3' + ? x3FirmwareVersionFallback + : x4FirmwareVersionFallback; const value = (await cache.get(cacheKey)) as OfficialFirmwareVersions | null; if (value) { return value; } + if (deviceModel === 'x3') { + // X3: Chinese has a check-update API, English only has a static download URL + return fetch(x3ChineseFirmwareCheckUrl) + .then((res) => res.json()) + .then(async (chData) => { + const data: OfficialFirmwareVersions = { + en: fallback.en, + ch: chData.data, + }; + + await cache.set(cacheKey, data, { + ttl: 60 * 60 * 24, // 24 hours + }); + + return data; + }) + .catch(() => fallback); + } + return Promise.all([ - fetch(chineseFirmwareCheckUrl), - fetch(englishFirmwareCheckUrl), + fetch(x4ChineseFirmwareCheckUrl), + fetch(x4EnglishFirmwareCheckUrl), ]) .then(([chRes, enRes]) => Promise.all([chRes.json(), enRes.json()])) .then(async ([chData, enData]) => { @@ -69,11 +113,11 @@ export async function getOfficialFirmwareRemoteData(): Promise firmwareVersionFallback); + .catch(() => fallback); } -export async function getOfficialFirmwareVersions() { - const data = await getOfficialFirmwareRemoteData(); +export async function getOfficialFirmwareVersions(deviceModel: DeviceModel) { + const data = await getOfficialFirmwareRemoteData(deviceModel); return { en: data.en.version, @@ -118,8 +162,11 @@ export async function getCommunityFirmwareRemoteData(): Promise data[region].download_url, ); const response = await fetch(url); From 5bd31ad58611e9ecc2167ab360fcd994450c9caf Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Fri, 3 Apr 2026 18:21:07 -0400 Subject: [PATCH 3/5] Replace straight quotes with curly quotes in UI text Use proper HTML entities (“ and ”) for quotation marks in the settings instruction text to improve typography. --- src/app/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index dda2d3b..0fcaeeb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -229,8 +229,8 @@ export default function Home() { Change device language Before starting the process, it is recommended to change the device - language to English. To do this, select “Settings" icon, then click - “OK / Confirm" button and “OK / Confirm" again until English is + language to English. To do this, select “Settings” icon, then click + “OK / Confirm” button and “OK / Confirm” again until English is shown. Otherwise, the language will still be Chinese after flashing and you may not notice changes. From 5632b10ab8503686c288b0c52bb5ec82ee081110 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Fri, 3 Apr 2026 18:51:36 -0400 Subject: [PATCH 4/5] Fix race condition in firmware version fetching Add cleanup function to cancel state updates when component unmounts, preventing setState calls on unmounted components. Also split community firmware fetching into separate effect that only runs once on mount. --- src/app/page.tsx | 20 +++++-- src/esp/useEspOperations.ts | 107 +++++++++++++++------------------- src/remote/firmwareFetcher.ts | 2 +- 3 files changed, 63 insertions(+), 66 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 0fcaeeb..f0b6ba4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -36,14 +36,23 @@ export default function Home() { const appPartitionFileInput = useRef(null); useEffect(() => { + let cancelled = false; setOfficialFirmwareVersions(null); - getOfficialFirmwareVersions(deviceModel).then((versions) => - setOfficialFirmwareVersions(versions), - ); + getOfficialFirmwareVersions(deviceModel).then((versions) => { + if (!cancelled) { + setOfficialFirmwareVersions(versions); + } + }); - getCommunityFirmwareRemoteData().then(setCommunityFirmwareVersions); + return () => { + cancelled = true; + }; }, [deviceModel]); + useEffect(() => { + getCommunityFirmwareRemoteData().then(setCommunityFirmwareVersions); + }, []); + return ( @@ -53,6 +62,7 @@ export default function Home() {