From b8b44d62fdfd6a690885bdd6ad7b76a97764b2ff Mon Sep 17 00:00:00 2001 From: Li Zhineng Date: Fri, 11 Jul 2025 18:09:10 +0800 Subject: wip --- main.mjs | 460 +++++++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 283 insertions(+), 177 deletions(-) (limited to 'main.mjs') diff --git a/main.mjs b/main.mjs index 31d4056..e19400e 100644 --- a/main.mjs +++ b/main.mjs @@ -1,229 +1,335 @@ class Device { - #deviceName - #primaryServiceUuid - #writeCharacteristicUuid - #notifyCharacteristicUuid - - constructor(deviceName, primaryServiceUuid, writeCharacteristicUuid, notifyCharacteristicUuid) { - this.#deviceName = deviceName - this.#primaryServiceUuid = primaryServiceUuid - this.#writeCharacteristicUuid = writeCharacteristicUuid - this.#notifyCharacteristicUuid = notifyCharacteristicUuid - } - - static airmxPro() { - return new Device( - 'AIRMX Pro', - '22210000-554a-4546-5542-46534450464d', - '22210001-554a-4546-5542-46534450464d', - '22210002-554a-4546-5542-46534450464d' - ) - } + #deviceName + #primaryServiceUuid + #writeCharacteristicUuid + #notifyCharacteristicUuid + + constructor(deviceName, primaryServiceUuid, writeCharacteristicUuid, notifyCharacteristicUuid) { + this.#deviceName = deviceName + this.#primaryServiceUuid = primaryServiceUuid + this.#writeCharacteristicUuid = writeCharacteristicUuid + this.#notifyCharacteristicUuid = notifyCharacteristicUuid + } + + static airmxPro() { + return new Device( + 'AIRMX Pro', + '22210000-554a-4546-5542-46534450464d', + '22210001-554a-4546-5542-46534450464d', + '22210002-554a-4546-5542-46534450464d' + ) + } + + get name() { + return this.#deviceName + } + + get primaryServiceUuid() { + return this.#primaryServiceUuid + } + + get writeCharacteristicUuid() { + return this.#writeCharacteristicUuid + } + + get notifyCharacteristicUuid() { + return this.#notifyCharacteristicUuid + } +} - get name() { - return this.#deviceName - } +class Dispatcher { + #characteristic + #sequenceNumber = 1 + #chunkSize = 16 - get primaryServiceUuid() { - return this.#primaryServiceUuid - } + constructor(characteristic) { + this.#characteristic = characteristic + } - get writeCharacteristicUuid() { - return this.#writeCharacteristicUuid - } + async dispatch(command) { + const data = this.#chunk(command.payload, command) - get notifyCharacteristicUuid() { - return this.#notifyCharacteristicUuid + for (let chunk of data) { + console.log(`Sending chunk: ${Array.from(chunk).map(b => b.toString(16).padStart(2, '0')).join(' ')}`) + await this.#characteristic.writeValueWithResponse(chunk) + await delay(500) } -} + } -class Dispatcher { - #characteristic - #sequenceNumber = 1 - #chunkSize = 16 + #chunk(data, command) { + const packets = this.#chunked(data, this.#chunkSize) + const total = packets.length - constructor(characteristic) { - this.#characteristic = characteristic + if (total === 0) { + return [ + this.#packetHeader( + this.#sequenceNumber++, 1, 1, // 1 of 1 packet + command.commandId + ) + ] } - async dispatch(command) { - const data = this.#chunk(command.payload, command) - - for (let chunk of data) { - console.log(`Sending chunk: ${Array.from(chunk).map(b => b.toString(16).padStart(2, '0')).join(' ')}`) - await this.#characteristic.writeValueWithResponse(chunk) - await delay(500) - } + return packets.map((chunk, index) => { + return new Uint8Array([ + ...this.#packetHeader( + this.#sequenceNumber++, index + 1, total, + command.commandId + ), + ...chunk + ]) + }) + } + + /** + * Splits an array into chunks of `size`. + * + * @param {Uint8Array} data - The array of 8-bit unsigned integers. + * @param {number} size - The size of each chunk. + */ + #chunked(data, size) { + const packets = [] + + for (let i = 0; i < data.length; i += size) { + packets.push(data.slice(i, i + size)) } - #chunk(data, command) { - const packets = this.#chunked(data, this.#chunkSize) - const total = packets.length - - if (total === 0) { - return [ - this.#packetHeader( - this.#sequenceNumber++, 1, 1, // 1 of 1 packet - command.commandId - ) - ] - } - - return packets.map((chunk, index) => { - return new Uint8Array([ - ...this.#packetHeader( - this.#sequenceNumber++, index + 1, total, - command.commandId - ), - ...chunk - ]) - }) - } + return packets + } + + #packetHeader(sequenceNumber, currentPacket, totalPacket, commandId) { + return new Uint8Array([ + sequenceNumber, + currentPacket << 4 | totalPacket, + 0x00, // Unencrypted flag + commandId + ]) + } +} - /** - * Splits an array into chunks of `size`. - * - * @param {Uint8Array} data - The array of 8-bit unsigned integers. - * @param {number} size - The size of each chunk. - */ - #chunked(data, size) { - const packets = [] +class Command { + get commandId() { + throw new Error('The command ID does not exist.') + } - for (let i = 0; i < data.length; i += size) { - packets.push(data.slice(i, i + size)) - } + get payload() { + throw new Error('The payload does not exist.') + } +} - return packets - } +class HandshakeCommand extends Command { + get commandId() { + return 0x0b + } + + get payload() { + return new Uint8Array([ + 0x08, // The storage size of the token + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Token: 0 + 0x05, // The length of the version + 0x31, 0x2e, 0x30, 0x2e, 0x30 // Version: 1.0.0 + ]) + } +} - #packetHeader(sequenceNumber, currentPacket, totalPacket, commandId) { - return new Uint8Array([ - sequenceNumber, - currentPacket << 4 | totalPacket, - 0x00, // Unencrypted flag - commandId - ]) - } +class ConfigureWifiCommand extends Command { + #ssid + #password + + constructor(ssid, password) { + super() + this.#ssid = ssid + this.#password = password + } + + get commandId() { + return 0x15 + } + + get payload() { + const encoder = new TextEncoder() + const ssid = encoder.encode(this.#ssid) + const password = encoder.encode(this.#password) + + return new Uint8Array([ + ssid.length, ...ssid, + password.length, ...password + ]) + } } -class Command { - get commandId() { - throw new Error('The command ID does not exist.') - } +class RequestIdentityCommand extends Command { + get commandId() { + return 0x16 + } + + get payload() { + return new Uint8Array([ + // + ]) + } +} - get payload() { - throw new Error('The payload does not exist.') - } +async function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) } -class HandshakeCommand extends Command { - get commandId() { - return 0x0b - } +async function connect() { + const ssid = document.getElementById('ssid') + const password = document.getElementById('password') - get payload() { - return new Uint8Array([ - 0x08, // The storage size of the token - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Token: 0 - 0x05, // The length of the version - 0x31, 0x2e, 0x30, 0x2e, 0x30 // Version: 1.0.0 - ]) - } + if (ssid.value === '' || password.value === '') { + return + } + + const device = Device.airmxPro() + const wifiCredentials = { ssid: ssid.value, password: password.value } + await connectToDevice(device, wifiCredentials) } -class ConfigureWifiCommand extends Command { - #ssid - #password +async function connectToDevice(device, wifiCredentials) { + const bluetoothDevice = await navigator.bluetooth.requestDevice({ + filters: [{ name: device.name }], + optionalServices: [device.primaryServiceUuid] + }) - constructor(ssid, password) { - super() - this.#ssid = ssid - this.#password = password - } + const server = await bluetoothDevice.gatt.connect() + const service = await server.getPrimaryService(device.primaryServiceUuid) - get commandId() { - return 0x15 - } + const writeCharacteristic = await service.getCharacteristic(device.writeCharacteristicUuid) + const notifyCharacteristic = await service.getCharacteristic(device.notifyCharacteristicUuid) - get payload() { - const encoder = new TextEncoder() - const ssid = encoder.encode(this.#ssid) - const password = encoder.encode(this.#password) + await notifyCharacteristic.startNotifications() + notifyCharacteristic.addEventListener('characteristicvaluechanged', handleDeviceResponse) - return new Uint8Array([ - ssid.length, ...ssid, - password.length, ...password - ]) - } + const dispatcher = new Dispatcher(writeCharacteristic) + await dispatcher.dispatch(new HandshakeCommand()) + await dispatcher.dispatch(new ConfigureWifiCommand(wifiCredentials.ssid, wifiCredentials.password)) + await dispatcher.dispatch(new RequestIdentityCommand()) } -class RequestIdentityCommand extends Command { - get commandId() { - return 0x16 - } +function handleDeviceResponse(event) { + const value = event.target.value + const receivedBytes = [] + for (let i = 0; i < value.byteLength; i++) { + receivedBytes.push(value.getUint8(i).toString(16).padStart(2, '0')) + } + console.log(`Received data from device: ${receivedBytes.join(' ')}`) +} - get payload() { - return new Uint8Array([ - // - ]) +class Form { + #activeClassName = 'form--active' + + constructor(id) { + this.form = document.getElementById(id) + if (this.form === null) { + throw new Error(`Form with id ${id} does not exist.`) } -} + } -async function delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)) + display() { + this.form.classList.add(this.#activeClassName) + } + + hide() { + this.form.classList.remove(this.#activeClassName) + } } -async function connect() { - const ssid = document.getElementById('ssid') - const password = document.getElementById('password') +class ProgressibleForm extends Form { + #nextForm = null - if (ssid.value === '' || password.value === '') { - return + nextTo(form) { + this.#nextForm = form + return this + } + + nextForm() { + if (this.#nextForm === null) { + return } - const device = Device.airmxPro() - const wifiCredentials = { ssid: ssid.value, password: password.value } - await connectToDevice(device, wifiCredentials) + this.hide() + this.#nextForm.display() + } } -async function connectToDevice(device, wifiCredentials) { - const bluetoothDevice = await navigator.bluetooth.requestDevice({ - filters: [{ name: device.name }], - optionalServices: [device.primaryServiceUuid] - }) +class WelcomeForm extends ProgressibleForm { + constructor(id) { + super(id) + this.form.addEventListener('submit', this.handleSubmit.bind(this)) + } + + handleSubmit(event) { + event.preventDefault() + this.nextForm() + } +} - const server = await bluetoothDevice.gatt.connect() - const service = await server.getPrimaryService(device.primaryServiceUuid) +class WifiCredentialsForm extends ProgressibleForm { + constructor(id) { + super(id) + this.form.addEventListener('submit', this.handleSubmit.bind(this)) + } - const writeCharacteristic = await service.getCharacteristic(device.writeCharacteristicUuid) - const notifyCharacteristic = await service.getCharacteristic(device.notifyCharacteristicUuid) + handleSubmit(event) { + event.preventDefault() + this.nextForm() + } +} - await notifyCharacteristic.startNotifications() - notifyCharacteristic.addEventListener('characteristicvaluechanged', handleDeviceResponse) +class PairingActivationForm extends ProgressibleForm { + constructor(id) { + super(id) + this.form.addEventListener('submit', this.handleSubmit.bind(this)) + } - const dispatcher = new Dispatcher(writeCharacteristic) - await dispatcher.dispatch(new HandshakeCommand()) - await dispatcher.dispatch(new ConfigureWifiCommand(wifiCredentials.ssid, wifiCredentials.password)) - await dispatcher.dispatch(new RequestIdentityCommand()) + handleSubmit(event) { + event.preventDefault() + this.nextForm() + } } -function handleDeviceResponse(event) { - const value = event.target.value - const receivedBytes = [] - for (let i = 0; i < value.byteLength; i++) { - receivedBytes.push(value.getUint8(i).toString(16).padStart(2, '0')) - } - console.log(`Received data from device: ${receivedBytes.join(' ')}`) +class CommunicationForm extends Form { + #successForm = null + #failureForm = null + + succeedTo(form) { + this.#successForm = form + return this + } + + failTo(form) { + this.#failureForm = form + return this + } } -function supportBluetoothApi() { +class Application { + static supportBluetoothApi() { return 'bluetooth' in navigator -} + } -if (! supportBluetoothApi()) { - const unsupportedMessage = document.getElementById('unsupported-message') - unsupportedMessage.style.display = 'block' + static run() { + if (! this.supportBluetoothApi()) { + const unsupportedForm = new Form('form-unsupported') + unsupportedForm.display() + return + } - const main = document.querySelector('main') - main.style.display = 'none' + const successForm = new Form('form-result-success') + const failureForm = new Form('form-result-failure') + const communicationForm = new CommunicationForm('form-communication') + .succeedTo(successForm) + .failTo(failureForm) + const pairingActivationForm = new PairingActivationForm('form-pairing-activation') + .nextTo(communicationForm) + const wifiCredentialsForm = new WifiCredentialsForm('form-wifi-credentials') + .nextTo(pairingActivationForm) + const welcomeForm = new WelcomeForm('form-welcome') + .nextTo(wifiCredentialsForm) + + welcomeForm.display() + } } + +Application.run() -- cgit v1.2.3