From 8cc925d86488e8cb00ec904145852ec491762d64 Mon Sep 17 00:00:00 2001 From: Abubakr Eirabie Date: Wed, 7 Oct 2026 20:06:49 -0400 Subject: [PATCH] Unify USB connection before installation or recovery --- .impeccable/design.json | 8 +- DESIGN.md | 3 +- PRODUCT.md | 2 +- README.md | 4 +- installer/README.md | 10 +- installer/real-service.ts | 37 ++++---- installer/service.ts | 5 +- installer/session.ts | 4 +- installer/simulator.ts | 24 +++-- installer/ui.ts | 67 +++++-------- site/docs/getting-started.md | 2 + site/pages/docs/getting-started.astro | 34 ++++--- site/pages/docs/troubleshooting.astro | 20 ++-- site/pages/install/index.astro | 5 +- site/pages/preview/[page].astro | 4 +- site/styles.css | 37 +------- tests/browser/fixtures/serial-device.ts | 14 ++- tests/browser/installer.spec.ts | 72 +++++++++----- tests/browser/recovery-status.spec.ts | 54 ++++++++++- tests/browser/release-selection.spec.ts | 15 +-- tests/unit/recovery.test.ts | 120 ++++++++++++------------ 21 files changed, 303 insertions(+), 238 deletions(-) diff --git a/.impeccable/design.json b/.impeccable/design.json index 6486519..08d0c93 100644 --- a/.impeccable/design.json +++ b/.impeccable/design.json @@ -1,6 +1,6 @@ { "schemaVersion": 2, - "generatedAt": "2026-09-27T23:44:35.712408Z", + "generatedAt": "2026-10-08T00:06:48.945959Z", "title": "Design System: OpenAthan", "extensions": { "colorMeta": { @@ -261,7 +261,7 @@ "name": "Primary action", "kind": "button", "description": "The main action in the current task.", - "html": "", + "html": "", "css": ".ds-primary { font-family: 'Mukta Malar', sans-serif; display: inline-flex; align-items: center; justify-content: center; min-height: 48px; padding: 10px 30px; border: 1px solid var(--accent, #355e83); border-radius: 4px; background: var(--accent, #355e83); color: var(--paper, #ffffff); font-size: 1.35rem; font-weight: 700; line-height: 1.35; text-decoration: none; text-align: center; cursor: pointer; } .ds-primary:hover:not(:disabled) { background: var(--accent-hover, #254764); border-color: var(--accent-hover, #254764); color: var(--paper, #ffffff); } .ds-primary:focus-visible { outline: 3px solid var(--accent, #355e83); outline-offset: 4px; } @media (max-width: 600px) { .ds-primary { font-size: 1.15rem; padding-inline: 22px; } }", "refersTo": "button-primary" }, @@ -308,7 +308,7 @@ "name": "Availability notice", "kind": "custom", "description": "An amber notice that explains availability or a caution.", - "html": "", + "html": "", "css": ".ds-notice { font-family: 'Mukta Malar', sans-serif; margin: 20px 0; padding: 16px 20px; border-radius: 4px; background: var(--warning-bg, #fff5e6); color: var(--warning-text, #805719); font-size: 20px; line-height: 1.45; } .ds-notice strong { font-weight: 700; } .ds-notice p { margin: 4px 0 0; color: var(--text, #38434f); } @media (max-width: 600px) { .ds-notice { font-size: 18px; } }", "refersTo": "notice" }, @@ -323,7 +323,7 @@ "name": "Uncertain feedback", "kind": "custom", "description": "A polite status message explaining that the result needs checking.", - "html": "
The device\u2019s current status could not be confirmed. Reconnect through recovery to check it before making another change.
", + "html": "
The device\u2019s current status could not be confirmed. Reconnect through USB setup to check it before making another change.
", "css": ".ds-uncertain { font-family: 'Mukta Malar', sans-serif; margin: 20px 0; padding: 14px 18px; border-radius: 4px; background: var(--warning-bg, #fff5e6); color: var(--warning-text, #805719); font-size: 20px; line-height: 1.45; overflow-wrap: anywhere; } @media (max-width: 600px) { .ds-uncertain { font-size: 18px; } }", "refersTo": "feedback-uncertain" }, diff --git a/DESIGN.md b/DESIGN.md index c6b04e0..b734651 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -209,7 +209,6 @@ Spacing centers on the frontmatter scale, with 24px section padding, 16px paragr | ------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Homepage | Main information and supporting panels use a 1.9:1 grid with a 24px column gap. | Grid ratio becomes 1.6:1 with a 20px gap and smaller section type. | Single column: features, hardware guidance, then contributions. Hardware guidance returns to white; contributions retain a pale panel. The primary action fills the width. | | Documentation index | Reading column plus a 225px contents panel, separated by 36px. | Single reading column; the duplicate contents navigation is hidden. Section anchors remain. | Smaller headings and introductions; paired cable diagrams become one column. | -| USB choices | Task choices plus a 320px help column, separated by 36px. | Help flows below the choices. | Shared narrow-screen gutters and control typography apply. | | USB flow | A single task panel capped at 880px; forms are capped at 760px. | Width follows the available container. | Actions wrap; fields remain full width and long addresses break safely. | Documentation articles use a single reading column capped at 76ch. There is no sticky header, floating navigation or animated layout transition. Reduced-motion preferences explicitly disable transitions and smooth scrolling. @@ -248,7 +247,7 @@ Help and contribution panels use pale blue with 24px padding. The documentation Each page has one visible h1. Documentation uses h2 sections and h3 subsections; its link-row titles remain link text rather than false section headings. Section anchors retain a 24px scroll margin. -The USB entry screen keeps the page heading as its visible title. A visually hidden h2 names the choices region and serves as a focus target. Inside a flow, a visible h2 identifies USB recovery or new installation and gives its preservation/setup context; the changing step title is an h3. Unsupported-browser guidance uses an h2 beneath the page title. Preserve these relationships when adding installer states. +The USB page has a Set up OpenAthan h1 and a visible Connect speaker h2 beside its cable instructions and single connection action. After discovery, a visible h2 identifies Connected speaker or New installation and gives its preservation/setup context; the changing step title is an h3. Unrecognized firmware opens installation confirmation with retry, cancel and troubleshooting. Recognized firmware with unreadable status cannot install. Unsupported-browser guidance uses an h2 beneath the page title. Preserve these relationships when adding installer states. After the initial render, installer navigation focuses the new step heading. The initial page load does not move focus. Focusable step headings have `tabindex="-1"`; keyboard-visible heading focus uses a 3px slate outline with a 5px offset. Other keyboard-focusable elements use a 3px outline with a 4px offset. The skip link becomes visible on focus and leads to the main content. Keep native links, buttons, labels and heading semantics. diff --git a/PRODUCT.md b/PRODUCT.md index c7b5817..6665eac 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -26,7 +26,7 @@ The public website explains the project, provides documentation and performs sup - Public fresh installation is available for the selected approved stable reference release. Preserve artifact verification, publication approval and explicit erase confirmation. - The website adopts stable latest after validation, with manual pin/disable policies for rollback. This never installs updates on existing speakers; owners use the authenticated device page or a preserving USB transition. -- Recovery is separate from new installation and preserves prayer settings and history. +- USB setup begins with one connection action, then checks the speaker. Existing-device controls and recovery preserve prayer settings and history. Unrecognized firmware requires explicit hardware/full-erasure confirmation for installation; recognized firmware with unreadable status cannot install. - Keep hardware model numbers and connection diagrams in setup documentation rather than homepage focal areas. - Preserve existing routes, anchors, protocol behavior and simulator isolation. - Firmware and the device-local UI belong in the firmware repository. The public website consumes published release artifacts and does not compile firmware. diff --git a/README.md b/README.md index a424556..5c22591 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ The website and USB installer for OpenAthan, a standalone speaker that automatic Visit [openathan.com](https://openathan.com) for supported hardware, installation, setup, and recovery instructions. -- [Install OpenAthan](https://openathan.com/install/) +- [Connect your speaker](https://openathan.com/install/) - [Getting started and supported hardware](https://openathan.com/docs/getting-started/) - [Troubleshooting and recovery](https://openathan.com/docs/troubleshooting/) ## Installation and updates -The browser installer supports fresh installation on **AtomS3R C126 + Pyramid A167** and Wi-Fi/password recovery for existing devices. **Fresh installation erases saved data.** Existing owners should [update from the device page or follow the preserving USB transition](https://openathan.com/docs/getting-started/#firmware-updates). +Connect once through USB setup: the website checks the speaker and shows existing-device controls or installation confirmation. The browser installer supports fresh installation on **AtomS3R C126 + Pyramid A167** and Wi-Fi/password recovery for existing devices. **Fresh installation erases saved data.** Existing owners should [update from the device page or follow the preserving USB transition](https://openathan.com/docs/getting-started/#firmware-updates). The installer uses the latest approved stable OpenAthan release. The website is scheduled to check for releases twice hourly and deploys only after artifact verification and website tests pass. See the [deployment guide](deployment/README.md) for timing limitations, release selection, and rollback. diff --git a/installer/README.md b/installer/README.md index 8ecfc36..d770623 100644 --- a/installer/README.md +++ b/installer/README.md @@ -1,8 +1,8 @@ # OpenAthan browser installer -The `/install/` wizard supports two explicit paths: **Install a new device** and **Fix Wi-Fi or password**. Supported reference hardware is M5Stack AtomS3R C126 + Pyramid A167, with an unlocked ESP32-S3 and 8 MiB flash. USB setup targets desktop Chrome and Edge with Web Serial in a secure context. The documentation at `/docs/` remains available in other browsers. +The `/install/` wizard has one **Connect speaker** entry with a **Choose USB device** action. Read-only discovery then opens existing-speaker controls or new-installation confirmation. Installation and credential recovery remain separate operations. Supported reference hardware is M5Stack AtomS3R C126 + Pyramid A167, with an unlocked ESP32-S3 and 8 MiB flash. USB setup targets desktop Chrome and Edge with Web Serial in a secure context. The documentation at `/docs/` remains available in other browsers. -The website adopts the firmware repository’s latest approved stable release after verification and website tests. The committed pin is a local-preview default and a manual rollback option, not a version label that needs changing for every release. Recovery remains a separate command-only path. +The website adopts the firmware repository’s latest approved stable release after verification and website tests. The committed pin is a local-preview default and a manual rollback option, not a version label that needs changing for every release. Credential recovery remains a command-only operation after discovery. Review `/preview/installer/` on the development server to exercise the complete wizard without hardware. Its injected simulator transport cannot reach Web Serial or the flasher; its route and code are excluded from the static build. Both example addresses are clickable and open `/preview/device/` in a new tab, a clearly labelled local handoff preview. They never navigate to the example hostname or LAN IP. Actual device addresses open the reported local device URL. Synthetic firmware and recording fixtures are never served. @@ -21,7 +21,7 @@ The implementation follows the firmware [provisioning handoff](https://github.co Credentials are held only in temporary browser memory, removed from forms after submission and never logged, stored, added to URLs or sent to analytics. JavaScript strings cannot be guaranteed to be physically zeroed; transmitted byte buffers are cleared after writing. Avoid browser tracing with real credentials. The site opens only validated device-reported local links chosen by the user; it does not call the device's HTTP API across origins. -Recovery never imports or invokes the flashing adapter, erase operations or settings/history commands. Unrecognized firmware receives troubleshooting guidance. Recognized OpenAthan firmware on the installation path is directed to recovery, including when its status is unreadable. A failed recognition probe is not permission to erase: new installation requires a separate explicit hardware/full-erasure confirmation. Existing-device upgrades happen on the authenticated device page or through a preserving USB transition, not this browser installer. See the [owner guidance](https://openathan.com/docs/getting-started/#firmware-updates). +Recovery never imports or invokes the flashing adapter, erase operations or settings/history commands. Unrecognized firmware receives troubleshooting guidance. Recognized OpenAthan firmware opens the connected-speaker menu. If identity succeeds but status cannot be read, installation is blocked and the user receives reconnect/troubleshooting guidance. An unrecognized result goes directly to installation confirmation, with retry, cancel and troubleshooting available. It does not identify a blank device or authorize erasure: new installation requires a separate explicit hardware/full-erasure confirmation. Discovery disconnects cannot become installation candidates. Existing-device upgrades happen on the authenticated device page or through a preserving USB transition, not this browser installer. See the [owner guidance](https://openathan.com/docs/getting-started/#firmware-updates). ## Approved release discovery and import @@ -29,7 +29,7 @@ The website consumes stable published releases only from `OpenAthan-Project/open `catalog.json` retains schema 1 with an optional `automatic` boolean (omitted means `false`). With automatic mode enabled, publishing a qualified release as GitHub’s **latest** authorizes website adoption. Maintainers must finish media rights/content review, appropriate physical qualification and publication approval first. Generated `mediaReviewed` and `hardwareQualified` fields record that publication policy; software checks cannot establish those human approvals or new physical results. -The committed release remains a verified manual pin. Set `automatic` to `false` to hold or roll back to it through a reviewed PR. Set `release` to `null` to disable fresh installation before the USB picker; null takes precedence over automatic discovery and keeps recovery available. Automatic mode must remain off for a persistent manual rollback. +The committed release remains a verified manual pin. Set `automatic` to `false` to hold or roll back to it through a reviewed PR. Set `release` to `null` to disable fresh installation; null takes precedence over automatic release discovery. The USB picker remains available for existing-device controls and recovery, while unrecognized firmware receives an unavailable-installation screen without erase controls. Automatic mode must remain off for a persistent manual rollback. `npm run release:import` discovers a selection once, verifies the published identity, exact asset names/URLs, source commit, bounded sizes, hardware/layout and hashes, then imports only its manifest and two binaries under ignored `public/releases//`. Old generated imports are removed only after the candidate verifies; null removes all generated firmware imports without querying the firmware service. CI first runs `release:select`, then `release:import -- --snapshot` to retain its frozen selection. @@ -93,6 +93,8 @@ The flashing transport releases its output lock even when a USB write rejects. I ## Validation boundaries and public-launch prerequisites +Development preview scenarios select new, existing, unrecognized or unreadable device state independently of the connection action. Simulator routes stay excluded from production. + Unit tests exercise Python-compatible framing, fragmentation, checksums, malformed data, timeouts, command serialization, uncertain writes and release rejection. Fake-programmer tests require confirmation, compatible hardware and successful flash readback before completion. Recovery tests assert that the flasher is never called. Browser tests cover complete simulated installation/recovery, denied/busy ports, disconnects, wrong Wi-Fi credentials, password validation, storage faults, uncertain acknowledgements, unsupported browsers, focus and narrow layouts. These simulations establish software behavior only. They do not establish USB reliability, bootloader entry, physical writes, power-cycle behavior, audible playback or durable preservation. Release maintainers must retain source-bound physical qualification evidence for the applicable behaviors below. Reuse accepted evidence where applicable, state untested changes, and coordinate new hardware testing when required; automatic website adoption does not repeat or establish these results: diff --git a/installer/real-service.ts b/installer/real-service.ts index 9c9aa17..8fded41 100644 --- a/installer/real-service.ts +++ b/installer/real-service.ts @@ -1,15 +1,16 @@ import { BrowserTransport, requestDevicePort } from './browser-transport'; import { ProvisioningSession } from './session'; import { loadRelease, type ReleasePin } from './release'; -import type { InstallerService, Path, Connected } from './service'; +import type { InstallerService, Connected } from './service'; export class RealService implements InstallerService { readonly simulated = false; readonly installAvailable: boolean; private port?: SerialPort; private session?: ProvisioningSession; - private newDeviceConfirmedPath = false; + private installationCandidate = false; private reloadRequired = false; + private connectionLost = false; onDisconnect?: () => void; constructor(private pin: ReleasePin | null) { this.installAvailable = pin !== null; @@ -17,14 +18,18 @@ export class RealService implements InstallerService { private async open(): Promise { if (!this.port) throw new Error('Choose the USB device first.'); const session = new ProvisioningSession(await BrowserTransport.open(this.port)); - session.onDisconnect = () => this.onDisconnect?.(); + this.connectionLost = false; + session.onDisconnect = () => { + this.connectionLost = true; + this.installationCandidate = false; + this.onDisconnect?.(); + }; this.session = session; return session; } - async connect(path: Path): Promise { + async connect(): Promise { if (this.reloadRequired) throw new Error('Reload this page before reconnecting after an interrupted installation.'); - if (path === 'install' && !this.pin) throw new Error('Installation release not available yet.'); await this.close(); this.port = await requestDevicePort(); const session = await this.open(); @@ -42,29 +47,29 @@ export class RealService implements InstallerService { if (remaining <= 0) throw new Error('Device discovery timed out.'); const status = await session.status(remaining); recognized = true; - this.newDeviceConfirmedPath = false; + this.installationCandidate = false; return { kind: 'existing', status }; } catch { - if (recognized || path === 'recovery') { + if (recognized || this.connectionLost) { await this.close(); throw new Error( - recognized - ? 'OpenAthan was recognized, but its status could not be read. Reconnect and use recovery; do not reinstall.' - : 'OpenAthan firmware was not recognized. Check the cable, close other USB tools and reconnect. Recovery will not install or erase firmware.', + this.connectionLost + ? 'Device disconnected. Reconnect the USB data cable.' + : 'OpenAthan was recognized, but its status could not be read. Reconnect your speaker; do not reinstall.', ); } // Detection failure is not installation authorization. The next screen requires // a separate explicit confirmation that this hardware may be fully replaced. await session.close(); this.session = undefined; - this.newDeviceConfirmedPath = true; - return { kind: 'new' }; + this.installationCandidate = this.installAvailable; + return { kind: 'unrecognized' }; } } async install(confirmed: boolean, progress: (percent: number) => void) { - if (!confirmed || !this.newDeviceConfirmedPath || !this.port || !this.pin || this.session) + if (!confirmed || !this.installationCandidate || !this.port || !this.pin || this.session) throw new Error('A confirmed new-device connection is required.'); - this.newDeviceConfirmedPath = false; + this.installationCandidate = false; const bundle = await loadRelease(this.pin); const { flashBundle, serialProgrammer } = await import('./flasher'); try { @@ -87,7 +92,7 @@ export class RealService implements InstallerService { } } throw new Error( - 'Installation was verified, but setup could not reconnect. Reconnect the cable and choose Fix Wi-Fi or password. Do not reinstall.', + 'Installation was verified, but setup could not reconnect. Reconnect through USB setup to check the speaker. Do not reinstall.', ); } private current() { @@ -113,7 +118,7 @@ export class RealService implements InstallerService { } async close() { this.port = undefined; - this.newDeviceConfirmedPath = false; + this.installationCandidate = false; await this.releaseSession(); } } diff --git a/installer/service.ts b/installer/service.ts index 85abec3..d21573a 100644 --- a/installer/service.ts +++ b/installer/service.ts @@ -1,11 +1,10 @@ import type { DeviceStatus } from './protocol'; -export type Path = 'install' | 'recovery'; -export type Connected = { kind: 'new' } | { kind: 'existing'; status: DeviceStatus }; +export type Connected = { kind: 'unrecognized' } | { kind: 'existing'; status: DeviceStatus }; export interface InstallerService { readonly simulated: boolean; readonly installAvailable: boolean; onDisconnect?: () => void; - connect(path: Path): Promise; + connect(): Promise; install(confirmed: boolean, progress: (percent: number) => void): Promise; status(): Promise; scan(): Promise<{ ssid: string; signal: number; secured: boolean }[]>; diff --git a/installer/session.ts b/installer/session.ts index 16ad369..fa659a0 100644 --- a/installer/session.ts +++ b/installer/session.ts @@ -159,9 +159,7 @@ export class ProvisioningSession { async identify(timeoutMs?: number): Promise { const [fields] = await this.command(false, 3, [], { timeoutMs }); if (fields?.length !== 4 || fields[0] !== 'OpenAthan' || fields[2] !== 'ESP32-S3') { - throw new Error( - 'OpenAthan firmware was not recognized. Recovery cannot install or erase firmware.', - ); + throw new Error('OpenAthan firmware was not recognized.'); } } async status(timeoutMs?: number): Promise { diff --git a/installer/simulator.ts b/installer/simulator.ts index feb22ec..ad1ae78 100644 --- a/installer/simulator.ts +++ b/installer/simulator.ts @@ -1,9 +1,11 @@ /** Deliberately has no dependency on Web Serial, release files or the flasher. */ import { decodeFields, encodeFrame, FrameDecoder, rpcPayload } from './protocol'; import { ProvisioningSession, type ByteTransport } from './session'; -import type { Connected, InstallerService, Path } from './service'; +import type { Connected, InstallerService } from './service'; export const scenarios = [ 'success', + 'new-device', + 'unreadable', 'denied', 'busy', 'unknown', @@ -121,7 +123,7 @@ export class SimulatorService implements InstallerService { readonly simulated = true; readonly installAvailable = true; private session?: ProvisioningSession; - private path?: Path; + private installationCandidate = false; onDisconnect?: () => void; constructor(private scenario: Scenario = 'success') {} private open(existing: boolean) { @@ -129,19 +131,28 @@ export class SimulatorService implements InstallerService { this.session.onDisconnect = () => this.onDisconnect?.(); return this.session; } - async connect(path: Path): Promise { + async connect(): Promise { await this.close(); - this.path = path; if (this.scenario === 'denied') throw new DOMException('Permission dismissed', 'NotFoundError'); if (this.scenario === 'busy') throw new DOMException('Port busy', 'InvalidStateError'); - if (path === 'install') return { kind: 'new' }; + if (['new-device', 'unknown', 'flash-failed'].includes(this.scenario)) { + this.installationCandidate = true; + return { kind: 'unrecognized' }; + } const session = this.open(true); await session.identify(); + if (this.scenario === 'unreadable') { + await this.close(); + throw new Error( + 'OpenAthan was recognized, but its status could not be read. Reconnect your speaker; do not reinstall.', + ); + } return { kind: 'existing', status: await session.status() }; } async install(confirmed: boolean, progress: (percent: number) => void) { - if (!confirmed || this.path !== 'install') + if (!confirmed || !this.installationCandidate) throw new Error('Confirm the simulated installation first.'); + this.installationCandidate = false; for (const percent of [15, 40, 75, 100]) { await new Promise((resolve) => setTimeout(resolve, 100)); progress(percent); @@ -169,6 +180,7 @@ export class SimulatorService implements InstallerService { return this.current().password(password); } async close() { + this.installationCandidate = false; const session = this.session; this.session = undefined; await session?.close(); diff --git a/installer/ui.ts b/installer/ui.ts index fdf9e2e..878c3d0 100644 --- a/installer/ui.ts +++ b/installer/ui.ts @@ -1,6 +1,6 @@ import { validatePassword, validateWifi, type DeviceStatus } from './protocol'; import { DeviceError, UncertainOutcome } from './session'; -import type { InstallerService, Path } from './service'; +import type { InstallerService } from './service'; const escape = (value: string) => value.replace( @@ -20,7 +20,7 @@ export function mountInstaller( const panel = root.querySelector('[data-panel]')!; const message = root.querySelector('[data-message]')!; const context = root.querySelector('[data-context]')!; - let path: Path = 'install', + let path: 'install' | 'recovery' = 'recovery', status: DeviceStatus | undefined, busy = false, current = 0; @@ -37,17 +37,18 @@ export function mountInstaller( current = step; root.dataset.view = 'flow'; context.hidden = entry; - context.innerHTML = `

${path === 'recovery' ? 'USB recovery' : 'New installation'}${service.simulated ? ' · simulated device' : ''}

${path === 'recovery' ? 'Prayer settings and playback history are preserved.' : 'Set up Wi-Fi and a password before opening your device’s settings.'}

`; + context.innerHTML = `

${path === 'recovery' ? 'Connected speaker' : 'New installation'}${service.simulated ? ' · simulated device' : ''}

${path === 'recovery' ? 'Prayer settings and playback history are preserved.' : 'Set up Wi-Fi and a password before opening your device’s settings.'}

`; const heading = entry ? 'h2' : 'h3'; panel.innerHTML = `<${heading} tabindex="-1" class="step-title">${title}${body}`; if (initialized) panel.querySelector('.step-title')?.focus(); } - function choices() { + function connectionScreen() { status = undefined; + path = 'recovery'; mutationBlocked = false; notify(''); const unavailable = - '
Installation release not available yet.

First-time installation will open when a tested firmware release and approved recordings are ready.

'; + '
New installation is unavailable.

You can still connect an existing OpenAthan to open its settings or fix Wi-Fi and its device password.

'; if (unsupported) { render( 'Use a computer for USB setup', @@ -57,34 +58,20 @@ export function mountInstaller( ); } else { render( - 'Choose a setup option', - `

Fix Wi-Fi or password

Reconnect an existing OpenAthan. Your prayer settings and playback history stay on the device.

Install a new device

${!service.installAvailable ? unavailable : '

Install OpenAthan on an AtomS3R C126 with Pyramid A167.

'}
`, + 'Connect speaker', + `

Use a USB data cable to connect your computer to the Atom’s USB-C port. Leave the Pyramid’s bottom power cable unplugged.

ComputerAtom USB-C

Use desktop Chrome or Edge. Close other serial tools first. Choose the USB JTAG/serial device in your browser’s connection window.

We’ll check for OpenAthan, then show settings and recovery options or ask you to confirm a new installation. Connecting does not install or erase firmware.

${!service.installAvailable ? unavailable : ''}
${button(service.simulated ? 'Connect simulated device' : 'Choose USB device', 'connect')}Setup instructionsTroubleshooting
`, 0, true, ); - // The page already names this entry screen; keep its focus target accessible. - panel.querySelector('.step-title')!.classList.add('sr-only'); } - context.hidden = true; - root.dataset.view = 'choices'; - } - - function connect() { - const blocked = - unsupported || - (path === 'install' && !service.installAvailable - ? 'Installation release not available yet.' - : null); - render( - 'Connect your device', - `

Use a USB data cable to connect your computer to the Atom’s USB-C port. Leave the Pyramid’s bottom power cable unplugged.

ComputerAtom USB-C

Close other serial tools first. Choose the USB JTAG/serial device in your browser’s connection window.

${blocked ? `
${escape(blocked)}

The documentation remains available without connecting hardware.

` : ''}
${button('Back', 'start', true)}
`, - 0, - ); + root.dataset.view = 'connection'; } function confirmation() { + path = 'install'; + const available = service.installAvailable; render( - 'Confirm a new installation', - `

${service.simulated ? 'This preview will simulate replacing a new device.' : 'OpenAthan was not recognized. Continue only if this is the supported reference hardware and you intend to replace everything on it.'}

This erases the device.

Existing firmware, saved Wi-Fi, prayer settings and prayer history will be replaced. This is not an update or a password recovery.

${button('Cancel', 'start', true)}
`, + available ? 'Confirm a new installation' : 'New installation is unavailable', + `

OpenAthan was not recognized. This does not prove the device is new or empty. If you expected an existing OpenAthan, check the cable and retry before considering installation.

${available ? '
This erases the device.

Existing firmware, saved Wi-Fi, prayer settings and prayer history will be replaced. This is not an update or a password recovery.

' : '

No installation release is available. Check the connection or follow the troubleshooting instructions.

'}
${available ? '' : ''}${button('Retry connection', 'start', true)}${button('Cancel', 'start', true)}Troubleshooting
`, 0, ); } @@ -116,7 +103,7 @@ export function mountInstaller( } catch (error) { render( 'Check the device status', - '

The device’s current status could not be confirmed. Reconnect through recovery before making another change.

' + + '

The device’s current status could not be confirmed. Reconnect through USB setup before making another change.

' + button('Disconnect and start again', 'start') + 'Troubleshooting
', current, @@ -131,7 +118,7 @@ export function mountInstaller( if (storageFault()) return; render( 'Your OpenAthan is connected', - `

${escape(status?.hostname ?? '')}

Choose what to repair. Your prayer settings and playback history will stay on the device.

${status?.wifi !== '4' ? '

Wi-Fi is not connected. If the speaker is reconnecting, wait a moment and select Refresh status.

' : ''}
${button('Change Wi-Fi', 'wifi-screen')}${button(status?.password === 'absent' ? 'Create device password' : 'Reset device password', 'password-screen', true)}
${status?.wifi === '4' && status.password === 'ready' ? button('Open device settings', 'finish', true) : ''}${button('Refresh status', 'refresh-status', true)}${button('Disconnect', 'start', true)}
`, + `

${escape(status?.hostname ?? '')}

Open your speaker’s settings or choose a change. Your prayer settings and playback history will stay on the device.

${status?.wifi !== '4' ? '

Wi-Fi is not connected. If the speaker is reconnecting, wait a moment and select Refresh status.

' : ''}
${status?.wifi === '4' && status.password === 'ready' ? button('Open device settings', 'finish') : ''}${button('Change Wi-Fi', 'wifi-screen', status?.wifi === '4' && status.password === 'ready')}${button(status?.password === 'absent' ? 'Create device password' : 'Reset device password', 'password-screen', true)}
${button('Refresh status', 'refresh-status', true)}${button('Disconnect', 'start', true)}
`, 1, ); } @@ -166,7 +153,7 @@ export function mountInstaller( const urls = status.urls; render( 'Ready for device setup', - `

${path === 'install' ? 'Wi-Fi and your device password are saved. Finish your prayer settings on the device.' : 'Your device is ready to open. Its prayer settings and history were not reset.'}

  1. Unplug the Atom’s USB cable.
  2. Connect only the Pyramid’s bottom USB-C power.
  3. Wait for it to reconnect, then open its settings on the same home network.
${urls.length && !service.simulated ? '

Device links open in a new tab.

' : ''}

Sign in as admin with the password you chose. Review your location, timezone and timetable, then select Finish setup. Athan playback waits until the device has synchronized its clock.

${!urls.length ? '

A device address was not returned. Reconnect through USB recovery to read it again.

' : ''}
${button('Done', 'start', true)}Setup instructions
`, + `

${path === 'install' ? 'Wi-Fi and your device password are saved. Finish your prayer settings on the device.' : 'Your device is ready to open. Its prayer settings and history were not reset.'}

  1. Unplug the Atom’s USB cable.
  2. Connect only the Pyramid’s bottom USB-C power.
  3. Wait for it to reconnect, then open its settings on the same home network.
${urls.length && !service.simulated ? '

Device links open in a new tab.

' : ''}

Sign in as admin with the password you chose. Review your location, timezone and timetable, then select Finish setup. Athan playback waits until the device has synchronized its clock.

${!urls.length ? '

A device address was not returned. Reconnect through USB setup to read it again.

' : ''}
${button('Done', 'start', true)}Setup instructions
`, 3, ); } @@ -260,26 +247,22 @@ export function mountInstaller( const action = (event.target as HTMLElement).closest('[data-action]')?.dataset .action; if (!action || busy) return; - if (action === 'choose-install' || action === 'choose-recovery') { - if (unsupported || (action === 'choose-install' && !service.installAvailable)) return; - path = action === 'choose-install' ? 'install' : 'recovery'; - notify(''); - connect(); - return; - } void attempt(async () => { if (action === 'start') { await service.close().catch(() => undefined); - choices(); + connectionScreen(); } else if (action === 'connect') { notify( service.simulated ? 'Connecting to the simulated device…' : 'Reading the device. This may take a moment…', ); - const result = await service.connect(path); + if (unsupported) return; + status = undefined; + mutationBlocked = false; + const result = await service.connect(); notify(''); - if (result.kind === 'new') confirmation(); + if (result.kind === 'unrecognized') confirmation(); else { status = result.status; path = 'recovery'; @@ -303,7 +286,7 @@ export function mountInstaller( } catch (error) { render( 'Installation needs attention', - '

Review the result below before any further installation. If writing was verified but setup could not reconnect, use USB recovery.

' + + '

Review the result below before any further installation. If writing was verified but setup could not reconnect, reconnect through USB setup; do not reinstall.

' + button('Disconnect and start again', 'start') + 'Troubleshooting
', 0, @@ -395,7 +378,7 @@ export function mountInstaller( mutationBlocked = true; render( 'Check the device status', - '

The save was acknowledged, but the device’s current status could not be confirmed. The change was not sent again. Reconnect through recovery to check the device before making another change.

' + + '

The save was acknowledged, but the device’s current status could not be confirmed. The change was not sent again. Reconnect through USB setup to check the device before making another change.

' + button('Disconnect and start again', 'start'), current, ); @@ -409,6 +392,6 @@ export function mountInstaller( window.addEventListener('pagehide', () => { void service.close().catch(() => undefined); }); - choices(); + connectionScreen(); initialized = true; } diff --git a/site/docs/getting-started.md b/site/docs/getting-started.md index ddd2ae8..f9a1f99 100644 --- a/site/docs/getting-started.md +++ b/site/docs/getting-started.md @@ -20,3 +20,5 @@ The website never compiles firmware or automatically updates connected speakers. Use the [development instructions](../../README.md) for local preview. The development simulator lets reviewers explore the flow without hardware and is excluded from the production build. + +USB setup has one connection entry. Choose the USB device, then read-only discovery opens existing-speaker controls or installation confirmation. An unrecognized result is not proof of a blank device. Installation requires explicit hardware and full-erasure confirmation; recognized OpenAthan with unreadable status receives troubleshooting, never installation. diff --git a/site/pages/docs/getting-started.astro b/site/pages/docs/getting-started.astro index 7d8414c..0208837 100644 --- a/site/pages/docs/getting-started.astro +++ b/site/pages/docs/getting-started.astro @@ -31,8 +31,8 @@ const { release } = await siteSelection(); ) } - Already have OpenAthan installed? Choose Fix Wi-Fi or password to recover - access while keeping your prayer settings and playback history. + Already have OpenAthan installed? Connect through USB setup to open its settings or recover + Wi-Fi and password access while keeping your prayer settings and playback history.

What you’ll need

Purchase an M5Stack AtomS3R C126{' '} @@ -50,16 +50,28 @@ const { release } = await siteSelection();

Close other serial tools and browser tabs using the device. On the installer page, choose your path and select the USB JTAG/serial device when prompted. -

2. Install, or recover an existing device

- A new installation replaces the device’s existing firmware and data. It is not a - settings-preserving update. If OpenAthan is already installed, choose Fix Wi-Fi or password. Recovery preserves your prayer settings and playback history. + >, select Choose USB device, then select the USB JTAG/serial device when + prompted. +

2. Check the connected speaker

+ The website checks for OpenAthan before showing your next steps. An existing OpenAthan opens + its settings and recovery options. Choose Open device settings, + Change Wi-Fi, or create/reset its device password as needed. Recovery + preserves your prayer settings and playback history. +

+ If OpenAthan is not recognized and a release is available, the website shows installation + confirmation. Detection alone does not prove the device is new or empty. If you expected an + existing OpenAthan, retry the connection or follow troubleshooting. Continue with a new installation only on supported hardware you intend to erase: confirm + the hardware and full erasure, then select Install OpenAthan. Installation + replaces existing firmware and saved data; it is not a preserving update. +

+ If OpenAthan is recognized but its status cannot be read, reconnect and troubleshoot. + Installation is blocked to protect the existing device.

3. Save Wi-Fi and a device password

- Select your home network or enter its name for a hidden network, then enter its Wi-Fi - password. Next, create a separate, long device password. Your browser sends these - credentials directly over USB; the website does not store them. + After a new installation, select your home network or enter its name for a hidden network, + then enter its Wi-Fi password. Next, create a separate, long device password. Your browser + sends these credentials directly over USB; the website does not store them.

Remember the device password. You will use the username admin to sign into the device’s settings. diff --git a/site/pages/docs/troubleshooting.astro b/site/pages/docs/troubleshooting.astro index a4db0ff..711a5b7 100644 --- a/site/pages/docs/troubleshooting.astro +++ b/site/pages/docs/troubleshooting.astro @@ -20,9 +20,10 @@ import Layout from '../../layouts/Layout.astro';

  • Unplug the Pyramid’s bottom power cable.
  • Connect your computer to the Atom’s USB-C with a data cable.
  • - Open device setup in desktop Chrome or Edge and choose Fix Wi-Fi or password. + Open USB setup in desktop Chrome or Edge, select Choose USB device, and select the USB JTAG/serial device. The website checks the speaker and opens its + settings and recovery options.
  • Then choose the change you need:

    @@ -31,6 +32,13 @@ import Layout from '../../layouts/Layout.astro'; Reset your password +

    OpenAthan is not recognized

    + Detection does not prove the speaker is new or empty. If you expected an existing OpenAthan, + select Retry connection, check the data cable, close other serial tools, + and choose the USB device again. Do not confirm erasure to fix Wi-Fi or a password. If + OpenAthan is recognized but its status cannot be read, installation is blocked; reconnect + and check the speaker before making changes. +

    Change Wi-Fi

    Choose Change Wi-Fi. Select your network or enter its name, enter the Wi-Fi @@ -96,15 +104,15 @@ import Layout from '../../layouts/Layout.astro';

    Installation was interrupted

    If USB disconnects during installation, unplug the cable, reload the installer page, then reconnect and deliberately start again. If it says the write was verified but setup could - not reconnect, use USB recovery rather than reinstalling. Do not restore an old full-flash - backup as routine recovery: it can restore old prayer history. + not reconnect, reconnect through USB setup to check the speaker; do not reinstall. Do not + restore an old full-flash backup as routine recovery: it can restore old prayer history.

    The browser installer does not perform firmware upgrades on existing devices. Use the firmware update instructions for the device-page update path and preserving USB transitions.

    diff --git a/site/pages/install/index.astro b/site/pages/install/index.astro index d9c0c6d..ab3aeea 100644 --- a/site/pages/install/index.astro +++ b/site/pages/install/index.astro @@ -6,12 +6,13 @@ const selection = await siteSelection(); ---

    Set up OpenAthan

    - Install OpenAthan or fix Wi-Fi and your device password over USB. + Connect over USB to open your speaker’s settings, fix Wi-Fi or its password, or install + OpenAthan.

    diff --git a/site/pages/preview/[page].astro b/site/pages/preview/[page].astro index 84aadd2..74f547d 100644 --- a/site/pages/preview/[page].astro +++ b/site/pages/preview/[page].astro @@ -32,7 +32,9 @@ const isInstaller = Astro.params.page === 'installer';