Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 58 additions & 16 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Alert,
Stack,
Flex,
HStack,
} from '@chakra-ui/react';
import FileUpload, { FileUploadHandle } from '@/components/FileUpload';
import Steps from '@/components/Steps';
Expand All @@ -20,7 +21,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;
Expand All @@ -32,15 +34,42 @@ export default function Home() {
const appPartitionFileInput = useRef<FileUploadHandle>(null);

useEffect(() => {
getOfficialFirmwareVersions().then((versions) =>
setOfficialFirmwareVersions(versions),
);
let cancelled = false;
setOfficialFirmwareVersions(null);
getOfficialFirmwareVersions(deviceModel).then((versions) => {
if (!cancelled) {
setOfficialFirmwareVersions(versions);
}
});

return () => {
cancelled = true;
};
}, [deviceModel]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

useEffect(() => {
getCommunityFirmwareRemoteData().then(setCommunityFirmwareVersions);
}, []);

return (
<Flex direction="column" gap="20px">
<Stack gap={3} as="section">
<Heading size="xl">Device model</Heading>
<HStack gap={3}>
{(['x4', 'x3'] as const).map((model) => (
<Button
key={model}
variant={deviceModel === model ? 'solid' : 'outline'}
aria-pressed={deviceModel === model}
onClick={() => setDeviceModel(model)}
disabled={isRunning}
>
Xteink {model.toUpperCase()}
</Button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
))}
</HStack>
</Stack>
Comment on lines +56 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't expose the CrossPoint flash path in X3 mode yet.

This selector makes X3 look fully supported, but the community firmware path is still model-agnostic in src/remote/firmwareFetcher.ts. Users can now switch to X3 and still trigger a CrossPoint flash with no model scoping. Please disable that button for X3 until community firmware resolution is model-aware.

🧭 Minimal UI guard
           <Button
             variant="subtle"
             onClick={actions.flashCrossPointFirmware}
-            disabled={isRunning || !communityFirmwareVersions}
+            disabled={
+              isRunning || !communityFirmwareVersions || deviceModel === 'x3'
+            }
             loading={!communityFirmwareVersions}
           >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/page.tsx` around lines 58 - 73, The X3 selector currently allows
switching to 'x3' which can trigger model-agnostic CrossPoint flashing; update
the Device model buttons in the component that renders deviceModel (the map over
(['x4','x3'] as const) that calls setDeviceModel) to disable the X3 option until
community firmware resolution is model-aware: modify the Button disabled prop to
include model === 'x3' (e.g., disabled={isRunning || model === 'x3'}) and also
ensure aria-pressed/aria-disabled reflect the disabled state so X3 cannot be
selected while firmwareFetcher (src/remote/firmwareFetcher.ts) lacks model
scoping.

<Separator />
<Alert.Root status="warning">
<Alert.Indicator />
<Alert.Content>
Expand Down Expand Up @@ -136,23 +165,25 @@ export default function Home() {
disabled={isRunning || !officialFirmwareVersions}
loading={!officialFirmwareVersions}
>
Flash English firmware ({officialFirmwareVersions?.en ?? '...'})
Flash English firmware for {deviceModel.toUpperCase()} (
{officialFirmwareVersions?.en ?? '...'})
</Button>
<Button
variant="subtle"
onClick={actions.flashChineseFirmware}
disabled={isRunning || !officialFirmwareVersions}
loading={!officialFirmwareVersions}
>
Flash Chinese firmware ({officialFirmwareVersions?.ch ?? '...'})
Flash Chinese firmware for {deviceModel.toUpperCase()} (
{officialFirmwareVersions?.ch ?? '...'})
</Button>
<Button
variant="subtle"
onClick={actions.flashCrossPointFirmware}
disabled={isRunning || !communityFirmwareVersions}
loading={!communityFirmwareVersions}
>
Flash CrossPoint firmware (
Flash CrossPoint firmware for {deviceModel.toUpperCase()} (
{communityFirmwareVersions?.crossPoint.version}) -{' '}
{communityFirmwareVersions?.crossPoint.releaseDate}
</Button>
Expand All @@ -170,7 +201,7 @@ export default function Home() {
}
disabled={isRunning}
>
Flash firmware from file
Flash firmware from file for {deviceModel.toUpperCase()}
</Button>
</Stack>
{process.env.NODE_ENV === 'development' && (
Expand Down Expand Up @@ -208,10 +239,10 @@ export default function Home() {
<Alert.Title>Change device language</Alert.Title>
<Alert.Description>
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
shown. Otherwise, the language will still be Chinese after flashing
and you may not notice changes.
language to English. To do this, select &ldquo;Settings&rdquo; icon,
then click &ldquo;OK / Confirm&rdquo; button and &ldquo;OK /
Confirm&rdquo; again until English is shown. Otherwise, the language
will still be Chinese after flashing and you may not notice changes.
</Alert.Description>
</Alert.Content>
</Alert.Root>
Expand All @@ -220,10 +251,21 @@ export default function Home() {
<Alert.Content>
<Alert.Title>Device restart instructions</Alert.Title>
<Alert.Description>
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.
<Stack>
<p>
Once you complete a write operation, you will need to restart
your device by pressing and releasing the small
&ldquo;Reset&rdquo; button near the bottom right, followed
quickly by pressing and holding of the main power button for
about 3 seconds.
</p>
{deviceModel === 'x3' && (
<p>
For CrossPoint firmware, disconnect the USB cable and connect
it again instead.
</p>
)}
</Stack>
</Alert.Description>
</Alert.Content>
</Alert.Root>
Expand Down
65 changes: 55 additions & 10 deletions src/esp/EspController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ const PARTITION_TYPES: Record<number, Record<number, string>> = {
},
};

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)) {
Expand All @@ -54,15 +72,23 @@ 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;

constructor(device: SerialPort) {
private layout: PartitionLayout;

constructor(
device: SerialPort,
partitionLayout: PartitionLayout = X4_PARTITION_LAYOUT,
) {
const transport = new Transport(device, false);
this.layout = partitionLayout;
this.espLoader = new ESPLoader({
transport,
baudrate: 115200,
Expand All @@ -71,12 +97,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();
}

Expand Down Expand Up @@ -180,8 +210,15 @@ 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(
Expand All @@ -206,7 +243,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,
Expand All @@ -224,16 +264,21 @@ 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(
`Data seems too small, are you sure this is the right file?`,
);
}

const offset = partitionLabel === 'app0' ? 0x10000 : 0x650000;
const offset =
partitionLabel === 'app0'
? this.layout.app0Offset
: this.layout.app1Offset;

await this.writeData(data, offset, reportProgress);
}
Expand Down
Loading