From 29c8378dca07b3731b150362a36f71a7e5c35337 Mon Sep 17 00:00:00 2001 From: Li Zhineng Date: Thu, 12 Jun 2025 22:19:44 +0800 Subject: main.mjs --- index.html | 176 +------------------------------------------------------------ main.mjs | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 175 deletions(-) create mode 100644 main.mjs diff --git a/index.html b/index.html index 1e7ffe6..e9b2647 100644 --- a/index.html +++ b/index.html @@ -1,6 +1,7 @@ Setup AIRMX device +

Setup your AIRMX device

@@ -9,180 +10,5 @@ Connect - diff --git a/main.mjs b/main.mjs new file mode 100644 index 0000000..5f962c4 --- /dev/null +++ b/main.mjs @@ -0,0 +1,173 @@ +const DEVICE_NAME = 'AIRMX Pro' +const MAIN_SERVICE_UUID = '22210000-554a-4546-5542-46534450464d' +const WRITE_CHAR_UUID = '22210001-554a-4546-5542-46534450464d' +const NOTIFY_CHAR_UUID = '22210002-554a-4546-5542-46534450464d' + +class Dispatcher { + #characteristic + #sequenceNumber = 1 + #chunkSize = 16 + + constructor(characteristic) { + this.#characteristic = characteristic + } + + 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) + } + } + + #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 + ]) + }) + } + + /** + * 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)) + } + + return packets + } + + #packetHeader(sequenceNumber, currentPacket, totalPacket, commandId) { + return new Uint8Array([ + sequenceNumber, + currentPacket << 4 | totalPacket, + 0x00, // Unencrypted flag + commandId + ]) + } +} + +class Command { + get commandId() { + throw new Error('The command ID does not exist.') + } + + get payload() { + throw new Error('The payload does not exist.') + } +} + +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 + ]) + } +} + +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 RequestIdentityCommand extends Command { + get commandId() { + return 0x16 + } + + get payload() { + return new Uint8Array([ + // + ]) + } +} + +async function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +async function connectToDevice() { + const device = await navigator.bluetooth.requestDevice({ + filters: [{ name: DEVICE_NAME }], + optionalServices: [MAIN_SERVICE_UUID] + }) + + const server = await device.gatt.connect() + const service = await server.getPrimaryService(MAIN_SERVICE_UUID) + + const writeChar = await service.getCharacteristic(WRITE_CHAR_UUID) + const notifyChar = await service.getCharacteristic(NOTIFY_CHAR_UUID) + + await notifyChar.startNotifications() + notifyChar.addEventListener('characteristicvaluechanged', handleDeviceResponse) + + const dispatcher = new Dispatcher(writeChar) + await dispatcher.dispatch(new HandshakeCommand()) + await dispatcher.dispatch(new ConfigureWifiCommand('', '')) + await dispatcher.dispatch(new RequestIdentityCommand()) +} + +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(' ')}`) +} + +const connect = document.getElementById('connect') +connect.addEventListener('click', connectToDevice) -- cgit v1.2.3