Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions src/transports/bluetooth.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,49 @@ export class WebBluetooth extends Transport {
await this._dropStream()
}

async writeBytes(data) {
//await this.tx.writeValueWithoutResponse(data)
await this.tx.writeValue(data)
await sleep(1)
/*
* Web Bluetooth allows one GATT operation at a time on a link: a second
* writeValue() while one is in flight fails with "GATT operation already in
* progress", and the bytes are lost. Typing at the REPL calls write() for every
* key, so writes arrive while the last one is still on the air. They wait in
* a queue instead, and whatever is waiting when the link frees up goes out
* together, up to one characteristic's worth.
*/
writeBytes(data) {
return new Promise((resolve, reject) => {
this._txQueue = this._txQueue || []
this._txQueue.push({ data, resolve, reject })
if (!this._txBusy) { this._drainTx() }
})
}

async _drainTx() {
this._txBusy = true
try {
while (this._txQueue.length) {
const limit = Math.max(1, this.writeChunk)
const batch = []
let size = 0
while (this._txQueue.length &&
(batch.length === 0 || size + this._txQueue[0].data.byteLength <= limit)) {
const item = this._txQueue.shift()
batch.push(item)
size += item.data.byteLength
}
const out = new Uint8Array(size)
let at = 0
for (const item of batch) { out.set(item.data, at); at += item.data.byteLength }
try {
//await this.tx.writeValueWithoutResponse(out)
await this.tx.writeValue(out)
await sleep(1)
for (const item of batch) { item.resolve() }
} catch (err) {
for (const item of batch) { item.reject(err) }
}
}
} finally {
this._txBusy = false
}
}
}
47 changes: 47 additions & 0 deletions test/suites/ble_write.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* The Bluetooth transport must never start a GATT write while another is still
* in flight, however fast the REPL is typed into.
*/
import { assert } from 'chai'

Object.defineProperty(globalThis.navigator, 'bluetooth', { value: {}, configurable: true })
const { WebBluetooth } = await import('../../src/transports/bluetooth.js')

function makeTransport(chunk) {
const t = new WebBluetooth()
t.writeChunk = chunk
const sent = []
let inFlight = false
t.tx = {
async writeValue(v) {
if (inFlight) { throw new DOMException('GATT operation already in progress.', 'NetworkError') }
inFlight = true
await new Promise(r => setTimeout(r, 5))
sent.push(Array.from(v))
inFlight = false
},
}
return { t, sent }
}

describe('Bluetooth transport writes', function () {
it('keeps every keystroke typed back to back', async function () {
const { t, sent } = makeTransport(20)
const errors = []
t.handleWriteError = (e) => { errors.push(e.message); return true }
const text = 'print("hello world")\r'
await Promise.all([...text].map(c => t.write(c)))
assert.deepEqual(errors, [])
assert.equal(String.fromCharCode(...sent.flat()), text)
})

it('batches waiting keystrokes without exceeding the characteristic size', async function () {
const { t, sent } = makeTransport(8)
t.handleWriteError = () => true
const text = 'abcdefghijklmnopqrstuvwxyz'
await Promise.all([...text].map(c => t.write(c)))
assert.equal(String.fromCharCode(...sent.flat()), text)
assert.isTrue(sent.every(s => s.length <= 8))
assert.isBelow(sent.length, text.length)
})
})
Loading