diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 94faaaca..11775f9d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -17,10 +17,14 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout code uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: latest - # Node + pnpm (with built-in cache) - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -28,12 +32,7 @@ jobs: cache: pnpm cache-dependency-path: '**/pnpm-lock.yaml' - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: latest - - - name: Install deps (root) + - name: Install dependencies (root) run: pnpm install --frozen-lockfile - name: Run tests diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5e1c27cb..495a1b18 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -36,7 +36,7 @@ jobs: cache: pnpm cache-dependency-path: '**/pnpm-lock.yaml' - - name: Install dependencies (root) + - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run linter diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 63e59f21..2719d397 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -18,18 +18,20 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - # Node + pnpm (with cache) - - name: Setup Node + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm cache-dependency-path: '**/pnpm-lock.yaml' - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: latest + - name: Install dependencies + run: pnpm install --frozen-lockfile - name: Configure npm auth env: @@ -38,9 +40,6 @@ jobs: pnpm config set //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} pnpm config set registry https://registry.npmjs.org/ - - name: Install deps (root) - run: pnpm install - - name: Resolve package list id: pkgs shell: bash diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index e8f14687..8edd528b 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -62,7 +62,11 @@ jobs: echo "push_latest=$PUSH_LATEST" >> "$GITHUB_OUTPUT" echo "Resolved tag: $TAG (push_latest=$PUSH_LATEST)" - # --- Setup Node.js and pnpm (with cache) --- + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -70,14 +74,17 @@ jobs: cache: pnpm cache-dependency-path: '**/pnpm-lock.yaml' - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: latest - - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Build web package + working-directory: packages/web + run: pnpm run build + + - name: Build web package + working-directory: packages/web + run: pnpm run build + - name: Create Web App Release Archive working-directory: packages/web run: pnpm run package diff --git a/package.json b/package.json index ea88d006..db43d9fc 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@types/node": "^22.16.4", - "biome": "^0.3.3", + "@biomejs/biome": "2.0.6", "tsdown": "^0.13.4", "typescript": "^5.8.3", "vitest": "^3.2.4" diff --git a/packages/transport-deno/scripts/build_npm.ts b/packages/transport-deno/scripts/build_npm.ts index 2b98e947..5c88dbd6 100644 --- a/packages/transport-deno/scripts/build_npm.ts +++ b/packages/transport-deno/scripts/build_npm.ts @@ -13,7 +13,8 @@ await build({ // package.json properties name: "@meshtastic/transport-deno", version: Deno.args[0], - description: "A Deno transport layer for your project, enabling seamless integration with npm.", + description: + "A Deno transport layer for your project, enabling seamless integration with npm.", license: "GPL-3.0-only", repository: { type: "git", diff --git a/packages/transport-node-serial/src/transport.ts b/packages/transport-node-serial/src/transport.ts index 02f439e2..56947bb6 100644 --- a/packages/transport-node-serial/src/transport.ts +++ b/packages/transport-node-serial/src/transport.ts @@ -14,7 +14,10 @@ export class TransportNodeSerial implements Types.Transport { * @param baudRate - The port number for the TCP connection (defaults to 4403). * @returns A promise that resolves with a connected TransportNode instance. */ - public static create(path: string, baudRate = 115200): Promise { + public static create( + path: string, + baudRate = 115200, + ): Promise { return new Promise((resolve, reject) => { const port = new SerialPort({ path, @@ -45,9 +48,7 @@ export class TransportNodeSerial implements Types.Transport { console.error("Serial port connection error:", err); }); - const fromDeviceSource = Readable.toWeb( - port, - ) as ReadableStream; + const fromDeviceSource = Readable.toWeb(port) as ReadableStream; this._fromDevice = fromDeviceSource.pipeThrough(Utils.fromDeviceStream()); // Stream for data going FROM the application TO the Meshtastic device. diff --git a/packages/transport-web-bluetooth/src/transport.ts b/packages/transport-web-bluetooth/src/transport.ts index 717ec277..05cf094c 100644 --- a/packages/transport-web-bluetooth/src/transport.ts +++ b/packages/transport-web-bluetooth/src/transport.ts @@ -95,6 +95,7 @@ export class TransportWebBluetooth implements Types.Transport { this._isFirstWrite = false; setTimeout(() => { + // biome-ignore lint/style/noNonNullAssertion: we know this will be set this.readFromRadio(this._fromDeviceController!); }, 50); } diff --git a/packages/transport-web-serial/package.json b/packages/transport-web-serial/package.json index f615b8d0..efc9d230 100644 --- a/packages/transport-web-serial/package.json +++ b/packages/transport-web-serial/package.json @@ -1,6 +1,6 @@ { "name": "@meshtastic/transport-web-serial", - "version": "0.2.3-1", + "version": "0.2.4-0", "description": "A transport layer for Meshtastic applications using Web Serial API.", "exports": { ".": "./mod.ts" diff --git a/packages/transport-web-serial/src/transport.ts b/packages/transport-web-serial/src/transport.ts index 47d98586..69aa35a1 100644 --- a/packages/transport-web-serial/src/transport.ts +++ b/packages/transport-web-serial/src/transport.ts @@ -5,6 +5,8 @@ export class TransportWebSerial implements Types.Transport { private _toDevice: WritableStream; private _fromDevice: ReadableStream; private connection: SerialPort; + private pipePromise: Promise | null = null; + private abortController: AbortController; public static async create(baudRate?: number): Promise { const port = await navigator.serial.requestPort(); @@ -26,8 +28,13 @@ export class TransportWebSerial implements Types.Transport { } this.connection = connection; + this.abortController = new AbortController(); - Utils.toDeviceStream.readable.pipeTo(connection.writable); + // Set up the pipe with abort signal for clean cancellation + this.pipePromise = Utils.toDeviceStream.readable.pipeTo( + connection.writable, + { signal: this.abortController.signal }, + ); this._toDevice = Utils.toDeviceStream.writable; this._fromDevice = connection.readable.pipeThrough( @@ -43,7 +50,53 @@ export class TransportWebSerial implements Types.Transport { return this._fromDevice; } - disconnect() { - return this.connection.close(); + /** + * Safely disconnects the serial port, following best practices from + * https://github.com/WICG/serial/. Cancels any active pipe + * operations and only closes the port after streams are unlocked. + */ + async disconnect() { + try { + this.abortController.abort(); + + if (this.pipePromise) { + try { + await this.pipePromise; + } catch (error) { + if (error instanceof Error && error.name !== "AbortError") { + throw error; + } + } + } + + // Cancel any remaining streams + if (this._fromDevice?.locked) { + try { + await this._fromDevice.cancel(); + } catch { + // Stream cancellation might fail if already cancelled + } + } + + await this.connection.close(); + } catch (error) { + // If we can't close cleanly, let the browser handle cleanup + console.warn("Could not cleanly disconnect serial port:", error); + } + } + + /** + * Reconnects the transport by creating a new AbortController and re-establishing + * the pipe connection. Only call this after disconnect() or if the connection failed. + */ + async reconnect() { + // Create a new AbortController for the new connection + this.abortController = new AbortController(); + + // Re-establish the pipe connection + this.pipePromise = Utils.toDeviceStream.readable.pipeTo( + this.connection.writable, + { signal: this.abortController.signal }, + ); } } diff --git a/packages/web/package.json b/packages/web/package.json index c29040e1..15f27c0b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "meshtastic-web", - "version": "2.7.0-0", + "version": "2.6.6-0", "type": "module", "description": "Meshtastic web client", "license": "GPL-3.0-only", @@ -86,7 +86,6 @@ "zustand": "5.0.6" }, "devDependencies": { - "@biomejs/biome": "2.0.6", "@tanstack/router-plugin": "^1.127.9", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index 809278ef..152bccda 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -12,6 +12,8 @@ import { Outlet } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { ErrorBoundary } from "react-error-boundary"; import { MapProvider } from "react-map-gl/maplibre"; +// Import feature flags and dev overrides +import "@core/services/dev-overrides.ts"; export function App() { const { getDevice } = useDeviceStore(); @@ -31,7 +33,6 @@ export function App() { setConnectDialogOpen(open); }} /> - {/* */}
{ label: t("contextual.command.dfuMode"), icon: HardDriveUpload, action() { - connection?.enterDfuMode() + connection?.enterDfuMode(); }, }, { diff --git a/packages/web/src/components/UI/Footer.tsx b/packages/web/src/components/UI/Footer.tsx index 552e6c49..4bd3c829 100644 --- a/packages/web/src/components/UI/Footer.tsx +++ b/packages/web/src/components/UI/Footer.tsx @@ -1,4 +1,5 @@ import { cn } from "@core/utils/cn.ts"; +import React from "react"; import { Trans } from "react-i18next"; type FooterProps = { @@ -6,6 +7,18 @@ type FooterProps = { }; const Footer = ({ className, ...props }: FooterProps) => { + const version = React.useMemo( + () => String(import.meta.env.VITE_VERSION)?.toUpperCase() || "", + [], + ); + const commitHash = React.useMemo( + () => + String(import.meta.env.VITE_COMMIT_HASH) + ?.toUpperCase() + .slice(0, 7) || "unk", + [], + ); + return (