Skip to content
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
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: CI
'on':
push:
branches: [master]
pull_request:

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .tool-versions
cache: yarn

- name: Install Packages
run: yarn --immutable

- name: Run linter
run: yarn lint
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs 24
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"lint:fix": "yarn eslint --max-warnings 0 --cache --fix && yarn prettier --write .",
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"prepare": "husky"
},
"dependencies": {
"@chakra-ui/react": "^3.31.0",
Expand Down Expand Up @@ -44,9 +45,17 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.5.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"prettier": "^3.8.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.0"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"yarn eslint --cache --fix --max-warnings 0 --no-warn-ignored",
"yarn prettier --write"
]
},
"packageManager": "yarn@4.11.0"
}
15 changes: 7 additions & 8 deletions src/app/debug/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import OtaPartition, { OtaPartitionDetails } from '@/esp/OtaPartition';
import HexSpan from '@/components/HexSpan';
import HexViewer from '@/components/HexViewer';
import { downloadData } from '@/utils/download';
import FileUpload, { FileUploadHandle } from '@/components/FileUpload';
import { FirmwareInfo } from '@/utils/firmwareIdentifier';

function OtadataDebug({ otaPartition }: { otaPartition: OtaPartition }) {
Expand Down Expand Up @@ -220,6 +219,7 @@ function FirmwareIdentificationDebug({
case 'crosspoint':
return 'blue';
case 'unknown':
default:
return 'orange';
}
};
Expand Down Expand Up @@ -272,9 +272,8 @@ function FirmwareIdentificationDebug({
}

export default function Debug() {
const { actions, debugActions, stepData, isRunning } = useEspOperations();
const { debugActions, stepData, isRunning } = useEspOperations();
const [debugOutputNode, setDebugOutputNode] = useState<ReactNode>(null);
const appPartitionFileInput = React.useRef<FileUploadHandle>(null);

return (
<Flex direction="column" gap="20px">
Expand All @@ -284,8 +283,8 @@ export default function Debug() {
<Stack gap={1} color="grey" textStyle="sm">
<p>
These are few tools to help debugging / administering your Xtink
device. They&apos;re designed to be used by those who are
intentionally messing around with their device.
device. Theyre designed to be used by those who are intentionally
messing around with their device.
</p>
<p>
<b>Read otadata partition</b> will read the raw data out of the{' '}
Expand All @@ -303,8 +302,8 @@ export default function Debug() {
<Em>otadata</Em> to switch the boot partition.
</p>
<p>
<b>Identify firmware in both partitions</b> will read both app0 and
app1 partitions and automatically identify which firmware is
<b>Identify firmware in both partitions</b> will read both app0
and app1 partitions and automatically identify which firmware is
installed on each (Official English, Official Chinese, CrossPoint
Community, or Custom).
</p>
Expand Down Expand Up @@ -414,4 +413,4 @@ export default function Debug() {
) : null}
</Flex>
);
}
}
32 changes: 16 additions & 16 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ export default function Home() {
<Alert.Description>
<Stack>
<p>
I&apos;ve tried to make this foolproof and while the likelihood
of unrecoverable things going wrong is extremely low, it&apos;s
never zero. So proceed with care and make sure to grab a backup
using <b>Save full flash</b> before flashing your device.
Ive tried to make this foolproof and while the likelihood of
unrecoverable things going wrong is extremely low, it’s never
zero. So proceed with care and make sure to grab a backup using{' '}
<b>Save full flash</b> before flashing your device.
</p>
<p>
Once you start <b>Write flash from file</b> or{' '}
Expand All @@ -75,10 +75,10 @@ export default function Home() {
goes wrong.
</p>
<p>
<b>Save full flash</b> will read your device&apos;s flash and save
it as <Em>flash.bin</Em>. This will take around 25 minutes to
complete. You can use that file (or someone else&apos;s) with{' '}
<b>Write full flash from file</b> to overwrite your device&apos;s
<b>Save full flash</b> will read your devices flash and save it
as <Em>flash.bin</Em>. This will take around 25 minutes to
complete. You can use that file (or someone elses) with{' '}
<b>Write full flash from file</b> to overwrite your devices
entire flash.
</p>
</Stack>
Expand Down Expand Up @@ -116,8 +116,8 @@ export default function Home() {
<Heading size="xl">OTA fast flash controls</Heading>
<Stack gap={1} color="grey" textStyle="sm">
<p>
Before using this, I&apos;d strongly recommend taking a backup of
your device using <b>Save full flash</b> above.
Before using this, Id strongly recommend taking a backup of your
device using <b>Save full flash</b> above.
</p>
<p>
<b>Flash English/Chinese firmware</b> will download the firmware,
Expand Down Expand Up @@ -208,9 +208,9 @@ 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
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.
</Alert.Description>
</Alert.Content>
Expand All @@ -221,9 +221,9 @@ export default function Home() {
<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.
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.
</Alert.Description>
</Alert.Content>
</Alert.Root>
Expand Down
6 changes: 3 additions & 3 deletions src/esp/EspController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default class EspController {
return this.espLoader.readFlash(offset, 0x640000, onPacketReceived);
}

async readAppPartitionForIdentification(
async readAppPartitionForIdentification(
partitionLabel: 'app0' | 'app1',
{
readSize = 0x6400, // Default to 25KB (0x6400) for fast identification
Expand All @@ -123,7 +123,7 @@ export default class EspController {
// Optimized read for firmware identification with flexible read size and offset:
// - Default (25KB / 0x6400): Fast path, covers 99% of cases
// - Additional chunks: Specify offset multiples of 25KB until identification succeeds
// In testing, most firmwares are identified within the first 25KB read, so reading the entire
// 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;
Expand Down Expand Up @@ -183,4 +183,4 @@ export default class EspController {
reportProgress,
});
}
}
}
19 changes: 13 additions & 6 deletions src/esp/useEspOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export function useEspOperations() {

const wrapWithRunning =
<Args extends unknown[], T>(fn: (...a: Args) => Promise<T>) =>
async (...a: Args) => {
setIsRunning(true);
return fn(...a).finally(() => setIsRunning(false));
};
async (...a: Args) => {
setIsRunning(true);
return fn(...a).finally(() => setIsRunning(false));
};

const flashRemoteFirmware = async (
getFirmware: () => Promise<Uint8Array>,
Expand Down Expand Up @@ -416,6 +416,7 @@ export function useEspOperations() {
let info: FirmwareInfo | undefined;

for (let offset = 0; offset < maxReadSize; offset += chunkSize) {
// eslint-disable-next-line no-await-in-loop
const chunk = await espController.readAppPartitionForIdentification(
partitionLabel,
{
Expand All @@ -441,7 +442,13 @@ export function useEspOperations() {
}
}

return info ?? { type: 'unknown', version: 'unknown', displayName: 'Custom/Unknown Firmware' }; // Return the last identification result if not found
return (
info ?? {
type: 'unknown',
version: 'unknown',
displayName: 'Custom/Unknown Firmware',
}
); // Return the last identification result if not found
};

const app0Info = await runStep('Read app0 partition', () =>
Expand Down Expand Up @@ -486,4 +493,4 @@ export function useEspOperations() {
readAndIdentifyAllFirmware: wrapWithRunning(readAndIdentifyAllFirmware),
},
};
}
}
24 changes: 14 additions & 10 deletions src/utils/firmwareIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ function findString(
return -1;
}

for (let i = startOffset; i <= data.length - searchBytes.length; i++) {
for (let i = startOffset; i <= data.length - searchBytes.length; i += 1) {
let match = true;
for (let j = 0; j < searchBytes.length; j++) {
for (let j = 0; j < searchBytes.length; j += 1) {
if (data[i + j] !== searchBytes[j]) {
match = false;
break;
Expand Down Expand Up @@ -73,7 +73,7 @@ function extractVersion(data: Uint8Array, searchLimit = 25000): string {

// Try to find V-pattern versions (official firmware)
// Pattern: [any byte]<V3.1.1 or similar
for (let i = 0; i < searchArea.length - 8; i++) {
for (let i = 0; i < searchArea.length - 8; i += 1) {
if (searchArea[i] === 0x56) {
// 'V' character
const chunk = decoder.decode(
Expand All @@ -91,12 +91,16 @@ function extractVersion(data: Uint8Array, searchLimit = 25000): string {
const fullString = decoder.decode(searchArea);

// Check for CrossPoint-ESP32-x.x.x pattern
const crossPointMatch = fullString.match(/CrossPoint-ESP32-(\d+\.\d+\.\d+)/);
const crossPointMatch = fullString.match(
/CrossPoint-ESP32-(\d+\.\d+\.\d+)/,
);
if (crossPointMatch) {
return crossPointMatch[1]!;
}

// eslint-disable-next-line no-control-regex
const lines = fullString.split(/[\x00\n]/);
// eslint-disable-next-line no-restricted-syntax
for (const line of lines) {
const match = line.match(/^\d+\.\d+\.\d+$/);
if (match) {
Expand All @@ -107,7 +111,7 @@ function extractVersion(data: Uint8Array, searchLimit = 25000): string {
// Also search for version in common patterns
const versionMatch = fullString.match(/(?:Version[:\s]*)(\d+\.\d+\.\d+)/i);
if (versionMatch?.[1]) {
return versionMatch[1] as string;
return versionMatch[1];
}
} catch {
// Decoding failed, continue
Expand Down Expand Up @@ -162,7 +166,7 @@ export function identifyFirmware(firmwareData: Uint8Array): FirmwareInfo {
if (findString(areaAfterVersion, 'XTOS') !== -1) {
return {
type: 'official-chinese',
version: version,
version,
displayName: 'Official Chinese',
};
}
Expand All @@ -171,7 +175,7 @@ export function identifyFirmware(firmwareData: Uint8Array): FirmwareInfo {
// If we have a V-pattern version, valid ESP32 image, but no XTOS → English
return {
type: 'official-english',
version: version,
version,
displayName: 'Official English',
};
}
Expand All @@ -183,15 +187,15 @@ export function identifyFirmware(firmwareData: Uint8Array): FirmwareInfo {
) {
return {
type: 'crosspoint',
version: version,
version,
displayName: 'CrossPoint Community Reader',
};
}

// Unknown firmware
return {
type: 'unknown',
version: version,
version,
displayName: 'Custom/Unknown Firmware',
};
}
Expand All @@ -205,4 +209,4 @@ export function identifyFirmware(firmwareData: Uint8Array): FirmwareInfo {
*/
export function isIdentificationSuccessful(info: FirmwareInfo): boolean {
return info.type !== 'unknown';
}
}
Loading