summaryrefslogtreecommitdiffhomepage
path: root/main.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'main.mjs')
-rw-r--r--main.mjs460
1 files changed, 283 insertions, 177 deletions
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()