diff --git a/installer/README.md b/installer/README.md
index 4d54097..b4869d5 100644
--- a/installer/README.md
+++ b/installer/README.md
@@ -131,3 +131,26 @@ These simulations establish software behavior only. They do not establish USB re
6. Before enabling USB updates: capable confirmed firmware and approved rollback bootloader; desktop Chrome/Edge streaming and exact version confirmation; Atom-only transfer then bottom-power startup; preservation readback; disconnect/power cuts before and after selection; failed startup rollback; invalid/truncated/out-of-order traffic; competing Wi-Fi/USB/settings actions; runtime heap, fragmentation and prayer/audio responsiveness. Prior fresh-install or Wi-Fi update evidence does not establish this new transport.
Use the firmware repository's current hardware runbooks and fresh private backups for coordinated acceptance. Historical full-flash images can roll back prayer history and are not routine credential recovery. Keep private diagnostics, credentials and recovery images outside this repository. Published recordings are imported only through the reviewed release manifest; never commit generated binaries.
+
+## Waveshare Box V2 release preparation
+
+The installer supports hardware-specific schema-1 bundles when a published stable
+release provides them. Atom retains its existing release fields and filenames.
+`hardwareReleases` adds a Waveshare pin under
+`waveshare-esp32-s3-touch-lcd-1_85c-box-v2`; automatic selection adopts the prefixed
+manifest only when present and source-matched. Legacy Atom-only releases continue
+to work. Additional pins must use the same release tag; manual pins and disabling
+installation retain their existing policy. Static metadata and freshness checks
+include all selected hardware manifests.
+
+Recognized OpenAthan reports hardware through USB status; older responses retain
+Atom compatibility. Factory/blank devices require explicit model selection,
+followed by full-erasure confirmation. Changing models clears that confirmation.
+The flasher checks unlocked ESP32-S3 and matching 8/16 MiB capacity; capacity alone
+cannot identify the board or distinguish Waveshare V1 from V2. V1 is unsupported.
+
+Waveshare bundles use `waveshare-box-v2.` names except for shared
+`athan-audio.bin`. Signed USB updates verify the selected hardware, and recovery
+continues to preserve settings/history. Public USB update writes remain disabled
+by the existing policy until physical qualification. New simulator scenarios
+`waveshare` and `waveshare-new` exercise existing and fresh-install flows.
diff --git a/installer/entry.ts b/installer/entry.ts
index c78b92a..c5c1c0f 100644
--- a/installer/entry.ts
+++ b/installer/entry.ts
@@ -1,4 +1,4 @@
-import { parsePin } from './release';
+import { parsePin, parseHardwareReleases } from './release';
import { usbSupport } from './browser-transport';
import { RealService } from './real-service';
import { mountInstaller } from './ui';
@@ -11,6 +11,9 @@ if (root) {
new RealService(
parsePin(JSON.parse(root.dataset.releaseSelection ?? 'null')),
JSON.parse(root.dataset.releaseSelection ?? 'null')?.usbUpdateEnabled === true,
+ parseHardwareReleases(
+ JSON.parse(root.dataset.releaseSelection ?? 'null')?.hardwareReleases,
+ ),
),
usbSupport(),
);
diff --git a/installer/flasher.ts b/installer/flasher.ts
index 2d7a31f..645ee0c 100644
--- a/installer/flasher.ts
+++ b/installer/flasher.ts
@@ -2,7 +2,7 @@ import { ESPLoader } from 'esptool-js';
import { md5 } from '@noble/hashes/legacy.js';
import { bytesToHex } from '@noble/hashes/utils.js';
import { FlashTransport } from './flash-transport';
-import { FLASH_BYTES, parseManifest, verifyBundle, type ReleaseBundle } from './release';
+import { HARDWARE_PROFILES, parseManifest, verifyBundle, type ReleaseBundle } from './release';
export interface Programmer {
inspect(): Promise<{ chip: string; flashBytes: number; secured: boolean }>;
@@ -22,6 +22,7 @@ export async function flashBundle(
try {
if (!confirmed) throw new Error('Confirm a new installation before continuing.');
const manifest = parseManifest(input.manifest, {
+ hardware: input.manifest.hardware,
tag: input.manifest.tag,
manifestSha256: '0'.repeat(64),
mediaReviewed: true,
@@ -32,9 +33,13 @@ export async function flashBundle(
new Map(input.parts.map((part) => [part.metadata.file, part.bytes])),
);
const device = await programmer.inspect();
- if (device.chip !== 'ESP32-S3' || device.flashBytes !== FLASH_BYTES || device.secured)
+ if (
+ device.chip !== 'ESP32-S3' ||
+ device.flashBytes !== HARDWARE_PROFILES[manifest.hardware].flashBytes ||
+ device.secured
+ )
throw new Error(
- 'This device does not match the supported unlocked 8 MB ESP32-S3 hardware. Nothing was installed.',
+ 'This device does not match the selected unlocked ESP32-S3 hardware. Nothing was installed.',
);
await programmer.write(bundle, progress);
for (const part of bundle.parts) {
@@ -76,7 +81,7 @@ export function serialProgrammer(port: SerialPort): Programmer {
const size = await loader.detectFlashSize();
return {
chip: loader.chip.CHIP_NAME,
- flashBytes: size === '8MB' ? FLASH_BYTES : 0,
+ flashBytes: size === '8MB' ? 0x800000 : size === '16MB' ? 0x1000000 : 0,
secured:
loader.secureDownloadMode ||
info.parsedFlags.SECURE_BOOT_EN ||
diff --git a/installer/protocol.ts b/installer/protocol.ts
index fa61329..5ba4b08 100644
--- a/installer/protocol.ts
+++ b/installer/protocol.ts
@@ -1,3 +1,4 @@
+import { HARDWARE, isHardware, type Hardware } from './release';
/** Improv Serial v1 and the OpenAthan v1 extension. No transport or browser imports. */
const encoder = new TextEncoder();
const decoder = new TextDecoder('utf-8', { fatal: true });
@@ -124,6 +125,7 @@ export function validatePassword(password: string): void {
}
export interface DeviceStatus {
+ hardware?: Hardware;
wifi: '2' | '3' | '4';
password: 'absent' | 'ready' | 'fault';
setup: 'incomplete' | 'active' | 'storage_fault';
@@ -166,6 +168,10 @@ export function localDeviceUrls(urls: string[], hostname: string): string[] {
export function parseStatus(fields: string[]): DeviceStatus {
const [version, wifi, password, setup, revision, hostname, storage, ...urls] = fields;
+ const hardwareFields = urls.filter((value) => value.startsWith('hardware='));
+ const hardware = hardwareFields[0]?.slice(9) ?? HARDWARE;
+ if (hardwareFields.length > 1 || !isHardware(hardware))
+ throw new Error('Unsupported connected hardware.');
if (
version !== '1' ||
!['2', '3', '4'].includes(wifi ?? '') ||
@@ -179,6 +185,7 @@ export function parseStatus(fields: string[]): DeviceStatus {
throw new Error('This device returned an unsupported OpenAthan status.');
}
return {
+ hardware,
wifi,
password,
setup,
diff --git a/installer/real-service.ts b/installer/real-service.ts
index 995a424..9905518 100644
--- a/installer/real-service.ts
+++ b/installer/real-service.ts
@@ -10,12 +10,20 @@ import {
type UpgradeOffer,
type UpgradeCheck,
} from './upgrade';
-import { loadRelease, type ReleasePin } from './release';
+import {
+ loadRelease,
+ HARDWARE,
+ type Hardware,
+ type HardwareReleases,
+ type ReleasePin,
+} from './release';
import type { InstallerService, Connected } from './service';
export class RealService implements InstallerService {
readonly simulated = false;
readonly installAvailable: boolean;
+ readonly installHardware: Hardware[];
+ private hardware: Hardware = HARDWARE;
private port?: SerialPort;
private session?: ProvisioningSession;
private installationCandidate = false;
@@ -26,8 +34,10 @@ export class RealService implements InstallerService {
constructor(
private pin: ReleasePin | null,
private usbUpdateEnabled = false,
+ private hardwareReleases: HardwareReleases = {},
) {
this.installAvailable = pin !== null;
+ this.installHardware = pin ? [HARDWARE, ...(Object.keys(hardwareReleases) as Hardware[])] : [];
}
private async open(): Promise ${escape(unsupported)} Setup instructions · Troubleshooting USB recovery can fix Wi-Fi or your device password without resetting prayer settings or history. Supported hardware: AtomS3R C126 + Pyramid A167. Use a USB data cable. ${escape(unsupported)} Setup instructions · Troubleshooting USB recovery can fix Wi-Fi or your device password without resetting prayer settings or history. Supported hardware: AtomS3R C126 + Pyramid A167 and Waveshare Box V2. Use a USB data cable. Use a USB data cable to connect your computer to the Atom’s USB-C port. Leave the Pyramid’s bottom power cable unplugged. 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. Use a USB data cable to connect your computer to the speaker’s USB-C port. For Atom, use its USB-C port and leave the Pyramid’s bottom power cable unplugged. For Waveshare Box V2, use the rear USB-C port. 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. Check the model printed on your device. Waveshare V1 is not supported. 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. 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. 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. 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. ${escape(status?.hostname ?? '')} ${wifiMissing ? 'Wi-Fi is not connected.' : 'Wi-Fi is connected.'} ${passwordMissing ? 'A device password is needed.' : 'Your device password is set.'} ${escape(status?.hostname ?? '')} ${HARDWARE_PROFILES[hardware].label} ${wifiMissing ? 'Wi-Fi is not connected.' : 'Wi-Fi is connected.'} ${passwordMissing ? 'A device password is needed.' : 'Your device password is set.'} Compatible firmware updates preserve settings, prayer history and recordings. Recovery changes only the Wi-Fi or password you choose. Install ${escape(offered.version)} over USB. Your Wi-Fi, device password, prayer settings, playback history and recordings stay on the speaker. The current application is retained for startup rollback. Keep Atom USB connected during transfer. After verification, you’ll switch to Pyramid bottom power. Install ${escape(offered.version)} over USB. Your Wi-Fi, device password, prayer settings, playback history and recordings stay on the speaker. The current application is retained for startup rollback. ${powerInstructions} Your device password still needs to be created through USB setup. The speaker may reconnect using its saved Wi-Fi settings. If it stays offline, check or change Wi-Fi through USB setup. For USB recovery after startup: After restarting, connect the rear USB-C port to your computer and reconnect through USB setup. Check firmware status before changing Wi-Fi or the device password. Keep your saved prayer settings and history. Your device password still needs to be created through USB setup. The speaker may reconnect using its saved Wi-Fi settings. If it stays offline, check or change Wi-Fi through USB setup. For USB recovery after startup: ${updateWritten ? 'The signed application was verified and selected. Switch to normal speaker power so it can start and complete its health checks.' : 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.'} Device links open in a new tab. ${needsRecovery ? '' : 'Sign in as admin with the password you chose. '}${path === 'install' && !updateWritten ? 'Review your location, timezone and timetable, then select Finish setup.' : updateWritten ? 'Check the firmware version and update result on the device’s settings page. Startup success is confirmed there; an interrupted connection alone cannot establish success or rollback.' : 'Continue with your saved prayer settings.'} Athan playback waits until the device has synchronized its clock. A device address was not returned. Reconnect through USB setup to read it again. ${updateWritten ? 'The signed application was verified and selected. Switch to normal speaker power so it can start and complete its health checks.' : 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.'} Device links open in a new tab. ${needsRecovery ? '' : 'Sign in as admin with the password you chose. '}${path === 'install' && !updateWritten ? 'Review your location, timezone and timetable, then select Finish setup.' : updateWritten ? 'Check the firmware version and update result on the device’s settings page. Startup success is confirmed there; an interrupted connection alone cannot establish success or rollback.' : 'Continue with your saved prayer settings.'} Athan playback waits until the device has synchronized its clock. A device address was not returned. Reconnect through USB setup to read it again. Keep Atom USB connected while the application is written and verified. You will switch to speaker power afterwards. Preparing signed update… Keep ${hardware === WAVESHARE ? 'Waveshare USB' : 'Atom USB'} connected while the application is written and verified. You will switch to speaker power afterwards. Preparing signed update…
${recoveryGuidance}${urls.length && !service.simulated ? '${escape(url)}Opens a local preview in a new tab
${recoveryGuidance}${urls.length && !service.simulated ? '${escape(url)}Opens a local preview in a new tab
You will need a computer with desktop Chrome or Edge, a USB-C data cable, and a 2.4 GHz WPA2/WPA3 personal Wi-Fi network. A charging-only cable will not work for USB setup. @@ -96,8 +100,8 @@ const { release } = await siteSelection(); > to enable the Athan. If the clock has not synchronized yet, Athan playback will wait.
Your speaker calculates prayer times and plays recordings stored on the device. The current - build needs internet access to set its clock after a restart, before it can play the Athan. - To open its settings, connect your phone or computer to the same home Wi-Fi. Keep the + Atom build needs internet access to set its clock after a restart, before it can play the + Athan. To open its settings, connect your phone or computer to the same home Wi-Fi. Keep the settings page on your trusted home network; do not make it accessible from the internet.
Additional hardware integrations require development and testing. The reusable core and ESPHome integration provide a starting point; broader support for optional peripherals is - still planned. The browser installer currently targets only our first reference build. + still planned. The browser installer offers qualified releases for supported boards.
For implementation details, see:
+ { + waveshareRelease + ? `OpenAthan ${waveshareRelease.tag} is available for this model.` + : 'Support is being prepared for v0.5.0 and becomes available after its qualified release is published.' + } + Use ESP32-S3-Touch-LCD-1.85C-BOX V2; V1 is not supported. +
++ Connect its rear USB-C port using a USB data cable. Existing OpenAthan reports its hardware + automatically. For a new installation, choose Waveshare Box V2 and confirm that existing + firmware and saved data will be erased. +
++ Save Wi-Fi and a device password, then open device settings to choose your location, prayer + settings and volume. The rear USB-C port also supplies normal power; RESET restarts the + device. Touch and microphones are not supported. +
++ The onboard RTC restores previously synchronized time across powered restarts. First use + needs internet time. Retention after unplugging, battery behavior and clock drift remain + unqualified. +
+@@ -17,9 +17,8 @@ const selection = await siteSelection();