-
Notifications
You must be signed in to change notification settings - Fork 6
Scope serial and flash manager to component lifetime #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b96635a
04a75f8
2fdaed9
0fdcac5
f56e1f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { isSerialSupported } from '$lib/utils/compatibility'; | ||
| import { onMount } from 'svelte'; | ||
|
|
||
| export class SerialContext { | ||
| #ports = $state<SerialPort[]>([]); | ||
| #onConnect: (e: Event) => void; | ||
| #onDisconnect: (e: Event) => void; | ||
|
|
||
| constructor() { | ||
| this.#onConnect = (e) => this.#addPort(e.target as SerialPort); | ||
| this.#onDisconnect = (e) => this.#removePort(e.target as SerialPort); | ||
|
|
||
| if (isSerialSupported) { | ||
| navigator.serial.addEventListener('connect', this.#onConnect); | ||
| navigator.serial.addEventListener('disconnect', this.#onDisconnect); | ||
|
|
||
| navigator.serial | ||
| .getPorts() | ||
| .then((existing) => { | ||
| for (const p of existing) { | ||
| this.#addPort(p); | ||
| } | ||
| }) | ||
| .catch((error) => { | ||
| console.error('Failed to get serial ports', error); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| get ports(): readonly SerialPort[] { | ||
| return this.#ports; | ||
| } | ||
|
|
||
| async requestPort(options: SerialPortRequestOptions): Promise<SerialPort | null> { | ||
| if (!isSerialSupported) return null; | ||
|
|
||
| const port = await navigator.serial.requestPort(options); | ||
| this.#addPort(port); | ||
| return port; | ||
| } | ||
|
|
||
| destroy() { | ||
| if (isSerialSupported) { | ||
| navigator.serial.removeEventListener('connect', this.#onConnect); | ||
| navigator.serial.removeEventListener('disconnect', this.#onDisconnect); | ||
| } | ||
| for (const port of this.#ports) { | ||
| port.close().catch(() => {}); | ||
| } | ||
| this.#ports.length = 0; | ||
| } | ||
|
|
||
| #addPort(port: SerialPort) { | ||
| if (!this.#ports.includes(port)) { | ||
| this.#ports.push(port); | ||
| } | ||
| } | ||
|
|
||
| #removePort(port: SerialPort) { | ||
| const idx = this.#ports.indexOf(port); | ||
| if (idx !== -1) { | ||
| this.#ports.splice(idx, 1); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates a SerialContext scoped to the current component's lifetime. | ||
| * Must be called during component initialization (top-level script). | ||
| * Automatically cleans up event listeners on unmount. | ||
| */ | ||
| export function useSerial(): SerialContext { | ||
| const ctx = new SerialContext(); | ||
| onMount(() => () => ctx.destroy()); | ||
| return ctx; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,17 +15,17 @@ | |
| import { Label } from '$lib/components/ui/label'; | ||
| import { Progress } from '$lib/components/ui/progress'; | ||
| import { Sheet, SheetContent, SheetHeader, SheetTitle } from '$lib/components/ui/sheet'; | ||
| import { useSerial } from '$lib/utils/serial-context.svelte'; | ||
| import { getBrowserName, isSerialSupported } from '$lib/utils/compatibility'; | ||
| import FirmwareBoardSelector from './FirmwareBoardSelector.svelte'; | ||
| import FirmwareFlasher from './FirmwareFlasher.svelte'; | ||
| import FlashManager from './FlashManager'; | ||
| import { useFlashManager } from './flash-context.svelte'; | ||
| import HelpDialog from './HelpDialog.svelte'; | ||
| import SerialPortSelector from './SerialPortSelector.svelte'; | ||
|
|
||
| const serial = useSerial(); | ||
|
|
||
| let port = $state<SerialPort | null>(null); | ||
| let manager = $state<FlashManager | null>(null); | ||
| let connectFailed = $state<boolean>(false); | ||
| let isFlashing = $state<boolean>(false); | ||
|
|
||
| let terminalOpen = $state<boolean>(false); | ||
| let terminalText = $state<string>(''); | ||
|
|
@@ -43,16 +43,13 @@ | |
| }, | ||
| }; | ||
|
|
||
| const flash = useFlashManager(terminal); | ||
|
|
||
| $effect(() => { | ||
| if (port && !manager) { | ||
| connectFailed = false; | ||
| FlashManager.Connect(port, terminal).then((m) => { | ||
| manager = m; | ||
| connectFailed = !manager; | ||
| }); | ||
| } else if (!port && manager) { | ||
| manager.disconnect(); | ||
| manager = null; | ||
| if (port && !flash.manager) { | ||
| flash.connect(port); | ||
| } else if (!port && flash.manager) { | ||
| flash.disconnect(); | ||
| } | ||
| }); | ||
|
|
||
|
|
@@ -64,52 +61,52 @@ | |
| let eraseBeforeFlash = $state<boolean>(false); | ||
|
|
||
| async function AppModeDevice() { | ||
| if (isFlashing) return; | ||
| if (flash.isFlashing) return; | ||
| try { | ||
| isFlashing = true; | ||
| flash.isFlashing = true; | ||
|
|
||
| if (!manager) { | ||
| if (!flash.manager) { | ||
| terminal.writeLine(`Host-side error during reset: no device!`); | ||
| return; | ||
| } | ||
|
|
||
| await manager.ensureApplication(true); | ||
| await flash.manager.ensureApplication(true); | ||
| } catch (e) { | ||
|
Comment on lines
+68
to
74
|
||
| terminal.writeLine(`Host-side error during reset: ${e}`); | ||
| } finally { | ||
| isFlashing = false; | ||
| flash.isFlashing = false; | ||
| } | ||
| } | ||
|
|
||
| async function RunCommand() { | ||
| if (isFlashing) return; | ||
| if (flash.isFlashing) return; | ||
| try { | ||
| isFlashing = true; | ||
| flash.isFlashing = true; | ||
|
|
||
| if (!manager) { | ||
| if (!flash.manager) { | ||
| terminal.writeLine(`Couldn't send: no device!`); | ||
| return; | ||
| } | ||
|
|
||
| await manager.ensureApplication(); | ||
| await manager.sendApplicationCommand(terminalCommand); | ||
| await flash.manager.ensureApplication(); | ||
| await flash.manager.sendApplicationCommand(terminalCommand); | ||
| } catch (e) { | ||
|
Comment on lines
+86
to
93
|
||
| terminal.writeLine(`Couldn't send: ${e}`); | ||
| } finally { | ||
| isFlashing = false; | ||
| flash.isFlashing = false; | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| {#snippet mainContent()} | ||
| <SerialPortSelector bind:port disabled={isFlashing} /> | ||
| <SerialPortSelector {serial} bind:port disabled={flash.isFlashing} /> | ||
|
|
||
| {#if manager} | ||
| {#if flash.manager} | ||
| <h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">Select Channel</h3> | ||
| <FirmwareChannelSelector bind:channel bind:version disabled={isFlashing} /> | ||
| <FirmwareChannelSelector bind:channel bind:version disabled={flash.isFlashing} /> | ||
|
|
||
| <h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">Select Board</h3> | ||
| <FirmwareBoardSelector {version} bind:selectedBoard={board} disabled={isFlashing} /> | ||
| <FirmwareBoardSelector {version} bind:selectedBoard={board} disabled={flash.isFlashing} /> | ||
|
|
||
| <div class="items-top flex space-x-2"> | ||
| <Checkbox id="erase-before-flash" bind:checked={eraseBeforeFlash} /> | ||
|
|
@@ -131,21 +128,20 @@ | |
| <FirmwareFlasher | ||
| {version} | ||
| {board} | ||
| {manager} | ||
| {flash} | ||
| {eraseBeforeFlash} | ||
| showNonStableWarning={channel !== 'stable'} | ||
| bind:isFlashing | ||
| /> | ||
| {/if} | ||
| {:else if port && !connectFailed} | ||
| {:else if port && !flash.connectFailed} | ||
| <div class="flex flex-col items-center gap-2"> | ||
| <span class="text-center text-2xl"> Connecting... </span> | ||
| <Progress /> | ||
| <!-- TODO: Make this a loading animation --> | ||
| </div> | ||
| {/if} | ||
|
|
||
| {#if port && connectFailed} | ||
| {#if port && flash.connectFailed} | ||
| <div class="flex flex-col items-start gap-2"> | ||
| <span class="bold text-center text-2xl text-red-500"> Device connection failed </span> | ||
| <span class="text-center"> | ||
|
|
@@ -227,7 +223,7 @@ | |
| <div class="flex-1"></div> | ||
| <Button class="m-2" onclick={() => (terminalText = '')}>Clear</Button> | ||
| <!-- Reset & start application --> | ||
| <Button onclick={AppModeDevice} disabled={!manager || isFlashing}>Reset</Button> | ||
| <Button onclick={AppModeDevice} disabled={!flash.manager || flash.isFlashing}>Reset</Button> | ||
| </SheetTitle> | ||
| </SheetHeader> | ||
| <div | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export const ssr = false; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,34 +3,26 @@ | |||||
| import { DownloadAndVerifyBoardBinary } from '$lib/api/firmwareCDN'; | ||||||
| import { Button } from '$lib/components/ui/button'; | ||||||
| import { Progress } from '$lib/components/ui/progress'; | ||||||
| import FlashManager from './FlashManager'; | ||||||
| import type { FlashContext } from './flash-context.svelte'; | ||||||
|
||||||
| import type { FlashContext } from './flash-context.svelte'; | |
| import type { FlashContext } from './flash-context.svelte.ts'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import paths likely won't resolve:
serial-context.svelte.tsandflash-context.svelte.tsare imported as.../serial-context.svelteand./flash-context.svelte. In this repo,.svelte.tsmodules are imported with the full extension (e.g.src/lib/components/dialog-manager/dialog-manager.svelte:3). Update these imports to target the actual file names to avoid build failures.