Cleanup linting and repo

This commit is contained in:
Dave Allie
2025-11-20 18:47:30 +11:00
parent f063e5c533
commit 2f1143bbcd
17 changed files with 1181 additions and 730 deletions
+1
View File
@@ -41,3 +41,4 @@ yarn-error.log*
next-env.d.ts
.idea
.eslintcache
+5
View File
@@ -0,0 +1,5 @@
build
out
node_modules
public
.yarn
+3
View File
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
-4
View File
@@ -1,7 +1,3 @@
compressionLevel: mixed
enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.11.0.cjs
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Dave Allie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+85 -18
View File
@@ -1,21 +1,88 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import eslintConfigPrettier from "eslint-config-prettier/flat";
/**
* THIS FILE WAS AUTO-GENERATED.
* PLEASE DO NOT EDIT IT MANUALLY.
* ===============================
* IF YOU'RE COPYING THIS INTO AN ESLINT CONFIG, REMOVE THIS COMMENT BLOCK.
*/
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
eslintConfigPrettier,
import path from 'node:path';
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import { configs, plugins } from 'eslint-config-airbnb-extended';
import { rules as prettierConfigRules } from 'eslint-config-prettier';
import prettierPlugin from 'eslint-plugin-prettier';
export default eslintConfig;
const gitignorePath = path.resolve('.', '.gitignore');
const jsConfig = [
// ESLint Recommended Rules
{
name: 'js/config',
...js.configs.recommended,
},
// Stylistic Plugin
plugins.stylistic,
// Import X Plugin
plugins.importX,
// Airbnb Base Recommended Config
...configs.base.recommended,
];
const reactConfig = [
// React Plugin
plugins.react,
// React Hooks Plugin
plugins.reactHooks,
// React JSX A11y Plugin
plugins.reactA11y,
// Airbnb React Recommended Config
...configs.react.recommended,
];
const typescriptConfig = [
// TypeScript ESLint Plugin
plugins.typescriptEslint,
// Airbnb Base TypeScript Config
...configs.base.typescript,
// Airbnb React TypeScript Config
...configs.react.typescript,
];
const prettierConfig = [
// Prettier Plugin
{
name: 'prettier/plugin/config',
plugins: {
prettier: prettierPlugin,
},
},
// Prettier Config
{
name: 'prettier/config',
rules: {
...prettierConfigRules,
'prettier/prettier': 'error',
},
},
];
export default [
// Ignore .gitignore files/folder in eslint
includeIgnoreFile(gitignorePath),
// Javascript Config
...jsConfig,
// React Config
...reactConfig,
// TypeScript Config
...typescriptConfig,
// Prettier Config
...prettierConfig,
// Overrides
{
rules: {
'import-x/prefer-default-export': 'off',
'react/require-default-props': 'off',
},
},
];
+1 -1
View File
@@ -1,4 +1,4 @@
import type { NextConfig } from "next";
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
/* config options here */
+24 -10
View File
@@ -3,27 +3,41 @@
"version": "0.1.0",
"private": true,
"scripts": {
"lint": "yarn eslint --max-warnings 0 --cache && yarn prettier --check . && yarn tsc",
"lint:fix": "yarn eslint --max-warnings 0 --cache --fix && yarn prettier --write .",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
"start": "next start"
},
"dependencies": {
"esptool-js": "^0.5.7",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0"
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/js": "^9.39.1",
"@stylistic/eslint-plugin": "^5.6.1",
"@types/dom-serial": "^1.0.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.3",
"@types/eslint-plugin-jsx-a11y": "^6.10.1",
"@types/jest": "^30.0.0",
"@types/node": "^24.10.1",
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.1",
"eslint-config-airbnb-extended": "^2.3.2",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.5.0",
"prettier": "^3.6.2",
"typescript": "^5"
"typescript": "^5.9.3",
"typescript-eslint": "^8.47.0"
},
"packageManager": "yarn@4.11.0"
}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"automerge": true
}
],
"postUpdateOptions": ["yarnDedupeHighest"]
}
+11 -11
View File
@@ -1,14 +1,14 @@
"use client";
'use client';
import { ESPLoader, Transport } from "esptool-js";
import { ESPLoader, Transport } from 'esptool-js';
export default class EspController {
static async requestDevice() {
if (!("serial" in navigator && navigator.serial)) {
throw new Error("WebSerial is not supported in this browser");
if (!('serial' in navigator && navigator.serial)) {
throw new Error('WebSerial is not supported in this browser');
}
return await navigator.serial.requestPort({
return navigator.serial.requestPort({
filters: [{ usbVendorId: 12346, usbProductId: 4097 }],
});
}
@@ -58,7 +58,7 @@ export default class EspController {
) {
if (data.length !== 0x1000000) {
throw new Error(
`Data length must be 0x1000000, but got 0x${data.length.toString().padStart(7, "0")}`,
`Data length must be 0x1000000, but got 0x${data.length.toString().padStart(7, '0')}`,
);
}
@@ -75,7 +75,7 @@ export default class EspController {
) {
if (data.length !== 0x2000) {
throw new Error(
`Data length must be 0x2000, but got 0x${data.length.toString().padStart(4, "0")}`,
`Data length must be 0x2000, but got 0x${data.length.toString().padStart(4, '0')}`,
);
}
@@ -90,7 +90,7 @@ export default class EspController {
) => void,
) {
const u8Array = new Uint8Array(0x2000);
for (let i = 0; i < 0x2000; i++) {
for (let i = 0; i < 0x2000; i += 1) {
u8Array[i] = 255;
}
@@ -133,9 +133,9 @@ export default class EspController {
address,
},
],
flashSize: "keep",
flashMode: "keep",
flashFreq: "keep",
flashSize: 'keep',
flashMode: 'keep',
flashFreq: 'keep',
eraseAll: false,
compress: true,
+4 -4
View File
@@ -1,15 +1,15 @@
"use server";
'use server';
const urls: Record<string, string> = {
"3.0.8":
"http://gotaserver.xteink.com/api/download/ESP32C3/V3.0.8/V3.0.8-EN.bin",
'3.0.8':
'http://gotaserver.xteink.com/api/download/ESP32C3/V3.0.8/V3.0.8-EN.bin',
};
export async function getFirmware(version: string) {
const url = urls[version];
if (!url) {
throw new Error("Unknown firmware version");
throw new Error('Unknown firmware version');
}
const response = await fetch(url);
+10 -9
View File
@@ -1,20 +1,21 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import React from 'react';
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
variable: '--font-geist-mono',
subsets: ['latin'],
});
export const metadata: Metadata = {
title: "Xteink English Firmware Flasher",
description: "Web based tool to help flash the Xteink device",
title: 'Xteink English Firmware Flasher',
description: 'Web based tool to help flash the Xteink device',
};
export default function RootLayout({
+77 -75
View File
@@ -1,11 +1,10 @@
"use client";
'use client';
import { useRef, useState } from "react";
import EspController from "@/app/EspController";
import Step from "@/components/Step/Step";
import styles from "./page.module.css";
import { getFirmware } from "@/app/firmwareFetcher";
import React, { useRef, useState } from 'react';
import EspController from '@/app/EspController';
import Step from '@/components/Step/Step';
import { getFirmware } from '@/app/firmwareFetcher';
import styles from './page.module.css';
const downloadData = (data: Uint8Array, fileName: string, mimeType: string) => {
// @ts-expect-error types say no, but browser says yes
@@ -14,25 +13,23 @@ const downloadData = (data: Uint8Array, fileName: string, mimeType: string) => {
});
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.style = "display: none";
a.style = 'display: none';
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () {
return window.URL.revokeObjectURL(url);
}, 1000);
setTimeout(() => window.URL.revokeObjectURL(url), 1000);
};
type StepData = Array<{
type StepData = {
name: string;
status: "pending" | "running" | "success" | "failed";
status: 'pending' | 'running' | 'success' | 'failed';
progress: number;
errorMessage?: string;
}>;
}[];
export default function Home() {
const fileInput = useRef<HTMLInputElement>(null);
@@ -46,9 +43,8 @@ export default function Home() {
oldStepData.map((oldData) => {
if (oldData.name === step) {
return { ...oldData, ...data };
} else {
return oldData;
}
return oldData;
}),
);
};
@@ -57,164 +53,164 @@ export default function Home() {
const flashEnglishFirmware = async () => {
clearSteps();
appendStep({ name: "Connect device", status: "running", progress: -1 });
appendStep({ name: 'Connect device', status: 'running', progress: -1 });
const espController = await EspController.fromRequestedDevice().catch(
(e) => {
updateStepData("Connect device", {
status: "failed",
updateStepData('Connect device', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
},
);
await espController.connect().catch((e) => {
updateStepData("Connect device", {
status: "failed",
updateStepData('Connect device', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
});
updateStepData("Connect device", { status: "success" });
updateStepData('Connect device', { status: 'success' });
appendStep({ name: "Download firmware", status: "running", progress: -1 });
const firmwareFile = await getFirmware("3.0.8").catch((e) => {
updateStepData("Download firmware", {
status: "failed",
appendStep({ name: 'Download firmware', status: 'running', progress: -1 });
const firmwareFile = await getFirmware('3.0.8').catch((e) => {
updateStepData('Download firmware', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
});
updateStepData("Download firmware", { status: "success" });
updateStepData('Download firmware', { status: 'success' });
appendStep({ name: "Flash OTA partition", status: "running", progress: 0 });
appendStep({ name: 'Flash OTA partition', status: 'running', progress: 0 });
await espController
.writeEmptyOtaPartition((_, p, t) =>
updateStepData("Flash OTA partition", { progress: p / t }),
updateStepData('Flash OTA partition', { progress: p / t }),
)
.catch((e) => {
updateStepData("Flash OTA partition", {
status: "failed",
updateStepData('Flash OTA partition', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
});
updateStepData("Flash OTA partition", { status: "success" });
updateStepData('Flash OTA partition', { status: 'success' });
appendStep({
name: "Flash OTA_0 partition",
status: "running",
name: 'Flash OTA_0 partition',
status: 'running',
progress: 0,
});
await espController
.writeOta0(firmwareFile, (_, p, t) =>
updateStepData("Flash OTA_0 partition", { progress: p / t }),
updateStepData('Flash OTA_0 partition', { progress: p / t }),
)
.catch((e) => {
updateStepData("Flash OTA_0 partition", {
status: "failed",
updateStepData('Flash OTA_0 partition', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
});
updateStepData("Flash OTA_0 partition", { status: "success" });
updateStepData('Flash OTA_0 partition', { status: 'success' });
appendStep({ name: "Reset device", status: "running", progress: -0 });
appendStep({ name: 'Reset device', status: 'running', progress: -0 });
await espController.disconnect().catch((e) => {
updateStepData("Reset device", {
status: "failed",
updateStepData('Reset device', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
});
updateStepData("Reset device", { status: "success" });
updateStepData('Reset device', { status: 'success' });
};
const saveFullFlash = async () => {
clearSteps();
appendStep({ name: "Connect device", status: "running", progress: -1 });
appendStep({ name: 'Connect device', status: 'running', progress: -1 });
const espController = await EspController.fromRequestedDevice().catch(
(e) => {
updateStepData("Connect device", {
status: "failed",
updateStepData('Connect device', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
},
);
await espController.connect().catch((e) => {
updateStepData("Connect device", {
status: "failed",
updateStepData('Connect device', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
});
updateStepData("Connect device", { status: "success" });
updateStepData('Connect device', { status: 'success' });
appendStep({ name: "Read flash", status: "running", progress: 0 });
appendStep({ name: 'Read flash', status: 'running', progress: 0 });
const firmwareFile = await espController
.readFullFlash((_, p, t) =>
updateStepData("Read flash", { progress: p / t }),
updateStepData('Read flash', { progress: p / t }),
)
.catch((e) => {
updateStepData("Read flash", {
status: "failed",
updateStepData('Read flash', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
});
updateStepData("Read flash", { status: "success" });
updateStepData('Read flash', { status: 'success' });
downloadData(firmwareFile, "flash.bin", "application/octet-stream");
downloadData(firmwareFile, 'flash.bin', 'application/octet-stream');
};
const writeFullFlash = async () => {
clearSteps();
appendStep({ name: "Read file", status: "running", progress: -1 });
appendStep({ name: 'Read file', status: 'running', progress: -1 });
const file = fileInput.current?.files?.[0];
if (!file) {
updateStepData("Read file", {
status: "failed",
errorMessage: "File could not be found",
updateStepData('Read file', {
status: 'failed',
errorMessage: 'File could not be found',
});
return;
}
const fileData = new Uint8Array(await file.arrayBuffer());
updateStepData("Read file", { status: "success" });
updateStepData('Read file', { status: 'success' });
appendStep({ name: "Connect device", status: "running", progress: -1 });
appendStep({ name: 'Connect device', status: 'running', progress: -1 });
const espController = await EspController.fromRequestedDevice().catch(
(e) => {
updateStepData("Connect device", {
status: "failed",
updateStepData('Connect device', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
},
);
await espController.connect().catch((e) => {
updateStepData("Connect device", {
status: "failed",
updateStepData('Connect device', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
});
updateStepData("Connect device", { status: "success" });
updateStepData('Connect device', { status: 'success' });
appendStep({ name: "Write flash", status: "running", progress: 0 });
appendStep({ name: 'Write flash', status: 'running', progress: 0 });
await espController
.writeFullFlash(fileData, (_, p, t) =>
updateStepData("Write flash", { progress: p / t }),
updateStepData('Write flash', { progress: p / t }),
)
.catch((e) => {
updateStepData("Write flash", {
status: "failed",
updateStepData('Write flash', {
status: 'failed',
errorMessage: e.toString(),
});
throw e;
});
updateStepData("Write flash", { status: "success" });
updateStepData('Write flash', { status: 'success' });
};
return (
@@ -222,17 +218,23 @@ export default function Home() {
<h1>Xteink English Firmware Flasher</h1>
<section className={styles.section}>
<h2>Full flash controls</h2>
<button onClick={saveFullFlash}>Save full flash</button>
<div style={{ display: "flex" }}>
<button type="button" onClick={saveFullFlash}>
Save full flash
</button>
<div style={{ display: 'flex' }}>
<input ref={fileInput} type="file" />
<button style={{ flexGrow: 1 }} onClick={writeFullFlash}>
<button
type="button"
style={{ flexGrow: 1 }}
onClick={writeFullFlash}
>
Write full flash from file
</button>
</div>
</section>
<section className={styles.section}>
<h2>OTA fast flash controls</h2>
<button onClick={flashEnglishFirmware}>
<button type="button" onClick={flashEnglishFirmware}>
Flash English firmware (3.0.8) via OTA
</button>
</section>
+40 -16
View File
@@ -1,50 +1,74 @@
import React from 'react';
import styles from './styles.module.css';
type StepProps = { name: string, status: 'pending' | 'running' | 'success' | 'failed', progress: number, errorMessage?: string};
interface StepProps {
name: string;
status: 'pending' | 'running' | 'success' | 'failed';
progress: number;
errorMessage?: string;
}
function ProgressBar({ progress }: { progress: number }) {
return (
<div style={{ width: 100, background: 'lightgrey', height: 14 }}>
<div style={{width: `${Math.round(progress * 10000) / 100}%`, transition: 'width', height: '100%', backgroundColor: 'green' }}></div>
</div>
)
<div style={{ width: 100, background: 'lightgrey', height: 14 }}>
<div
style={{
width: `${Math.round(progress * 10000) / 100}%`,
transition: 'width',
height: '100%',
backgroundColor: 'green',
}}
/>
</div>
);
}
function StepStatus({ status, progress }: Pick<StepProps, 'status' | 'progress'>) {
function StepStatus({
status,
progress,
}: Pick<StepProps, 'status' | 'progress'>) {
let statusIcon;
switch (status) {
case 'pending':
statusIcon = (<span>P</span>)
statusIcon = <span>P</span>;
break;
case 'running':
statusIcon = (<span>Running</span>)
statusIcon = <span>Running</span>;
break;
case 'success':
statusIcon = (<span>Success</span>)
statusIcon = <span>Success</span>;
break;
case 'failed':
statusIcon = (<span>Failed</span>)
statusIcon = <span>Failed</span>;
break;
default:
statusIcon = (<span>Unknown status</span>)
statusIcon = <span>Unknown status</span>;
break;
}
return (
<>
{statusIcon}
{status === 'running' && progress !== -1 && (<ProgressBar progress={progress} />)}
{status === 'running' && progress !== -1 && (
<ProgressBar progress={progress} />
)}
</>
)
);
}
export default function Step({name, status, progress, errorMessage}: StepProps) {
export default function Step({
name,
status,
progress,
errorMessage,
}: StepProps) {
return (
<div className={styles.container}>
<span>{name}:</span>
<StepStatus status={status} progress={progress} />
{errorMessage && (<span className={styles.errorMessage}>{errorMessage}</span>)}
{errorMessage && (
<span className={styles.errorMessage}>{errorMessage}</span>
)}
</div>
);
}
+6 -2
View File
@@ -4,16 +4,20 @@
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"incremental": true,
"noUncheckedIndexedAccess": true,
"noImplicitAny": true,
"plugins": [
{
"name": "next"
+5
View File
@@ -0,0 +1,5 @@
{
"github": {
"silent": true
}
}
+876 -580
View File
File diff suppressed because it is too large Load Diff