test: add stress test to playground

This commit is contained in:
Christopher Willis-Ford 2022-11-07 15:52:08 -08:00
parent ae4eef4bb2
commit 10e4bf5943
2 changed files with 263 additions and 75 deletions

View file

@ -35,6 +35,18 @@
<button id="connectBLE">Connect to discovered peripheral</button>
<button id="getServicesBLE">Get services of connected peripheral</button>
</div>
<fieldset>
<legend>Stress Test</legend>
<button id="stressBLE">Go</button>
<input id="stressPingBLE" type="checkbox" title="Ping?" />
<label for="stressPingBLE">Ping?</label>
<input id="stressGetVersionBLE" type="checkbox" title="Get version?" />
<label for="stressGetVersionBLE">Get version?</label>
<input id="stressDiscoverBLE" type="checkbox" title="Discover?" />
<label for="stressDiscoverBLE">Discover?</label>
<input id="stressConnectBLE" type="checkbox" title="Connect?" />
<label for="stressConnectBLE">Connect?</label>
</fieldset>
<fieldset>
<legend>Optional Services</legend>
<textarea id="optionalServicesBLE" placeholder="Optional services, if any, separated by whitespace" style="width:20rem;height:3rem;"></textarea>
@ -66,6 +78,18 @@
<button id="discoverBT">Find BT devices</button>
<button id="closeBT">Goodbye</button>
</div>
<fieldset>
<legend>Stress Test</legend>
<button id="stressBT">Go</button>
<input id="stressPingBT" type="checkbox" title="Ping?" />
<label for="stressPingBT">Ping?</label>
<input id="stressGetVersionBT" type="checkbox" title="Get version?" />
<label for="stressGetVersionBT">Get version?</label>
<input id="stressDiscoverBT" type="checkbox" title="Discover?" />
<label for="stressDiscoverBT">Discover?</label>
<input id="stressConnectBT" type="checkbox" title="Connect?" />
<label for="stressConnectBT">Connect?</label>
</fieldset>
<div>
<input id="peripheralId" value="0016533d0504" type="text" placeholder="Peripheral id">
<button id="connectBT">Make friends</button>

View file

@ -6,9 +6,82 @@
// - ScratchLinkSafariSocket (if Safari extension is present)
/// <reference path="global.d.ts"/>
class LogDisplay {
/**
* @param {HTMLElement} logElement
*/
constructor (logElement, lineCount = 256) {
this._logElement = logElement;
this._lineCount = lineCount;
this._lines = [];
this._dirty = false;
this._follow = true;
}
/**
* @param {string} text
*/
addLine (text) {
this._lines.push(text);
if (!this._dirty) {
this._dirty = true;
requestAnimationFrame(() => {
this._trim();
this._logElement.textContent = this._lines.join('\n');
if (this._follow) {
this._logElement.scrollTop = this._logElement.scrollHeight;
}
this._dirty = false;
});
}
}
_trim () {
this._lines = this._lines.splice(-this._lineCount);
}
}
const log = document.getElementById('log');
const follow = document.getElementById('follow');
const logDisplay = new LogDisplay(log);
/**
* @param {string} text
*/
function addLine (text) {
logDisplay.addLine(text);
logDisplay._follow = follow.checked;
}
/**
* @param {object} o
*/
function stringify (o) {
if (o instanceof Event) {
if (o instanceof ErrorEvent) {
return `${o.constructor.name} {error: ${stringify(o.error)}`;
}
return `${o.constructor.name} {type: "${o.type}"}`;
}
return JSON.stringify(o);//, o && Object.getOwnPropertyNames(o));
}
class DidReceiveCallEvent extends Event {
constructor(method, params) {
super(method); // this is probably not a good idea in untrusted environments but it works for this playground
this.method = method;
this.params = params;
}
}
class ScratchLinkClient extends JSONRPC {
/**
* @param {string} type
*/
constructor(type) {
super();
this._scratchLinkPeripheralType = type;
this._events = new EventTarget();
const ScratchLinkSafariSocket = self.Scratch && self.Scratch.ScratchLinkSafariSocket;
const useSafariSocket = ScratchLinkSafariSocket && ScratchLinkSafariSocket.isSafariHelperCompatible();
addLine(`Using ${useSafariSocket ? 'Safari WebExtension' : 'WebSocket'}`);
@ -16,9 +89,6 @@ class ScratchLinkClient extends JSONRPC {
new ScratchLinkSafariSocket(type) :
new ScratchLinkWebSocket(type);
addLine(`Socket created for ${type}`);
this._socket.setOnOpen(() => {
addLine(`Socket opened for ${type}`);
});
this._socket.setOnClose(e => {
addLine(`Socket closed: ${stringify(e)}`);
});
@ -29,11 +99,23 @@ class ScratchLinkClient extends JSONRPC {
addLine(`Received message: ${stringify(message)}`);
this._handleMessage(message);
});
this._sendMessage = message => {
this._sendMessage = (/** @type {object} */ message) => {
addLine(`Sending message: ${stringify(message)}`);
return this._socket.sendMessage(message);
};
this._socket.open();
}
/**
* @returns {Promise<ScratchLinkClient>}
*/
open () {
return new Promise(resolve => {
this._socket.setOnOpen(() => {
addLine(`Socket opened for ${this._scratchLinkPeripheralType}`);
resolve(this);
});
this._socket.open();
});
}
}
@ -44,12 +126,20 @@ class ScratchBLE extends ScratchLinkClient {
this.discoveredPeripheralId = null;
}
/**
* @param {object} options
*/
requestDevice(options) {
return this.sendRemoteRequest('discover', options);
}
/**
* @param {any} method
* @param {{ [paramName: string]: any; }} params
*/
didReceiveCall(method, params) {
addLine(`Received call to method: ${method}`);
this._events.dispatchEvent(new DidReceiveCallEvent(method, params));
switch (method) {
case 'didDiscoverPeripheral':
addLine(`Peripheral discovered: ${stringify(params)}`);
@ -61,6 +151,10 @@ class ScratchBLE extends ScratchLinkClient {
}
}
/**
* @param {number | string} serviceId
* @param {number | string} characteristicId
*/
read(serviceId, characteristicId, optStartNotifications = false) {
const params = {
serviceId,
@ -72,6 +166,11 @@ class ScratchBLE extends ScratchLinkClient {
return this.sendRemoteRequest('read', params);
}
/**
* @param {number | string} serviceId
* @param {number | string} characteristicId
* @param {string} message
*/
write(serviceId, characteristicId, message, encoding = null, withResponse = null) {
const params = { serviceId, characteristicId, message };
if (encoding) {
@ -89,19 +188,34 @@ class ScratchBT extends ScratchLinkClient {
super('BT');
}
/**
* @param {object} options
*/
requestDevice(options) {
return this.sendRemoteRequest('discover', options);
}
/**
* @param {object} options
*/
connectDevice(options) {
return this.sendRemoteRequest('connect', options);
}
/**
* @param {object} options
*/
sendMessage(options) {
return this.sendRemoteRequest('send', options);
}
/**
* @param {string} method
* @param {{ [paramName: string]: any; }} params
*/
didReceiveCall(method, params) {
addLine(`Received call to method: ${method}`);
this._events.dispatchEvent(new DidReceiveCallEvent(method, params));
switch (method) {
case 'didDiscoverPeripheral':
addLine(`Peripheral discovered: ${stringify(params)}`);
@ -116,6 +230,10 @@ class ScratchBT extends ScratchLinkClient {
}
}
/**
* @param {string} buttonId
* @param {function} func
*/
function attachFunctionToButton(buttonId, func) {
const button = document.getElementById(buttonId);
button.onclick = () => {
@ -127,8 +245,11 @@ function attachFunctionToButton(buttonId, func) {
}
}
/**
* @param {ScratchLinkClient} session
*/
function getVersion(session) {
session.sendRemoteRequest('getVersion').then(
return session.sendRemoteRequest('getVersion').then(
x => {
addLine(`Version request resolved with: ${stringify(x)}`);
},
@ -138,23 +259,27 @@ function getVersion(session) {
);
}
/**
* @param {ScratchLinkClient} session
*/
function pingMe (session) {
return session.sendRemoteRequest('pingMe').then(
x => {
addLine(`Ping request resolved with: ${stringify(x)}`);
},
e => {
addLine(`Ping request rejected with: ${stringify(e)}`);
}
);
}
function initBLE() {
if (self.Scratch.BLE) {
self.Scratch.BLE._socket.close();
}
addLine('Connecting...');
self.Scratch.BLE = new ScratchBLE();
}
function pingBLE() {
Scratch.BLE.sendRemoteRequest('pingMe').then(
x => {
addLine(`Ping request resolved with: ${stringify(x)}`);
},
e => {
addLine(`Ping request rejected with: ${stringify(e)}`);
}
);
return self.Scratch.BLE.open();
}
const filterInputsBLE = [];
@ -257,7 +382,7 @@ function discoverBLE() {
if (filterInputs.exactNameInput.value) filter.name = filterInputs.exactNameInput.value;
if (filterInputs.namePrefixInput.value) filter.namePrefix = filterInputs.namePrefixInput.value;
if (filterInputs.servicesInput.value.trim()) filter.services = filterInputs.servicesInput.value.trim().split(/\s+/);
if (filter.services) filter.services = filter.services.map(s => parseInt(s));
if (filter.services) filter.services = filter.services.map((/** @type {string} */ s) => parseInt(s));
let hasManufacturerDataFilters = false;
const manufacturerDataFilters = {};
@ -267,8 +392,8 @@ function discoverBLE() {
const manufacturerDataFilter = {};
manufacturerDataFilters[id] = manufacturerDataFilter;
hasManufacturerDataFilters = true;
if (manufacturerDataFilterInputs.prefixInput.value) manufacturerDataFilter.dataPrefix = manufacturerDataFilterInputs.prefixInput.value.trim().split(/\s+/).map(p => parseInt(p));
if (manufacturerDataFilterInputs.maskInput.value) manufacturerDataFilter.mask = manufacturerDataFilterInputs.maskInput.value.trim().split(/\s+/).map(m => parseInt(m));
if (manufacturerDataFilterInputs.prefixInput.value) manufacturerDataFilter.dataPrefix = manufacturerDataFilterInputs.prefixInput.value.trim().split(/\s+/).map((/** @type {string} */ p) => parseInt(p));
if (manufacturerDataFilterInputs.maskInput.value) manufacturerDataFilter.mask = manufacturerDataFilterInputs.maskInput.value.trim().split(/\s+/).map((/** @type {string} */ m) => parseInt(m));
}
if (hasManufacturerDataFilters) {
filter.manufacturerData = manufacturerDataFilters;
@ -281,9 +406,11 @@ function discoverBLE() {
};
const optionalServicesBLE = document.getElementById('optionalServicesBLE');
if (optionalServicesBLE.value.trim()) deviceDetails.optionalServices = optionalServicesBLE.value.trim().split(/\s+/);
if (optionalServicesBLE.value.trim()) {
deviceDetails.optionalServices = optionalServicesBLE.value.trim().split(/\s+/);
}
Scratch.BLE.requestDevice(
return Scratch.BLE.requestDevice(
deviceDetails
).then(
x => {
@ -297,7 +424,7 @@ function discoverBLE() {
function connectBLE() {
// this should really be implicit in `requestDevice` but splitting it out helps with debugging
Scratch.BLE.sendRemoteRequest(
return Scratch.BLE.sendRemoteRequest(
'connect',
{ peripheralId: Scratch.BLE.discoveredPeripheralId }
).then(
@ -311,7 +438,7 @@ function connectBLE() {
}
function getServicesBLE() {
Scratch.BLE.sendRemoteRequest(
return Scratch.BLE.sendRemoteRequest(
'getServices'
).then(
x => {
@ -324,14 +451,14 @@ function getServicesBLE() {
}
function setServiceMicroBit() {
if (filtersBLE.length < 1) addFilterBLE();
if (filterInputsBLE.length < 1) addFilterBLE();
filterInputsBLE[0].namePrefixInput.value = null;
filterInputsBLE[0].exactNameInput.value = null;
filterInputsBLE[0].servicesInput.value = '0xf005';
}
function readMicroBit() {
Scratch.BLE.read(0xf005, '5261da01-fa7e-42ab-850b-7c80220097cc', true).then(
return Scratch.BLE.read(0xf005, '5261da01-fa7e-42ab-850b-7c80220097cc', true).then(
x => {
addLine(`read resolved to: ${stringify(x)}`);
},
@ -343,7 +470,7 @@ function readMicroBit() {
function writeMicroBit() {
const message = _encodeMessage('LINK');
Scratch.BLE.write(0xf005, '5261da02-fa7e-42ab-850b-7c80220097cc', message, 'base64').then(
return Scratch.BLE.write(0xf005, '5261da02-fa7e-42ab-850b-7c80220097cc', message, 'base64').then(
x => {
addLine(`write resolved to: ${stringify(x)}`);
},
@ -354,14 +481,14 @@ function writeMicroBit() {
}
function setServiceWeDo2() {
if (filtersBLE.length < 1) addFilterBLE();
if (filterInputsBLE.length < 1) addFilterBLE();
filterInputsBLE[0].namePrefixInput.value = null;
filterInputsBLE[0].exactNameInput.value = null;
filterInputsBLE[0].servicesInput.value = '00001523-1212-efde-1523-785feabcd123';
}
function setGDXFOR() {
if (filtersBLE.length < 1) addFilterBLE();
if (filterInputsBLE.length < 1) addFilterBLE();
const optionalServicesBLE = document.getElementById('optionalServicesBLE');
optionalServicesBLE.value = 'd91714ef-28b9-4f91-ba16-f0d9a604f112';
filterInputsBLE[0].namePrefixInput.value = 'GDX';
@ -371,6 +498,9 @@ function setGDXFOR() {
// micro:bit base64 encoding
// https://github.com/LLK/scratch-microbit-firmware/blob/master/protocol.md
/**
* @param {string} message
*/
function _encodeMessage(message) {
const output = new Uint8Array(message.length);
for (let i = 0; i < message.length; i++) {
@ -386,7 +516,7 @@ function _encodeMessage(message) {
attachFunctionToButton('initBLE', initBLE);
attachFunctionToButton('getVersionBLE', () => getVersion(self.Scratch.BLE));
attachFunctionToButton('pingBLE', pingBLE);
attachFunctionToButton('pingBLE', () => pingMe(self.Scratch.BLE));
attachFunctionToButton('discoverBLE', discoverBLE);
attachFunctionToButton('connectBLE', connectBLE);
attachFunctionToButton('getServicesBLE', getServicesBLE);
@ -409,10 +539,11 @@ function initBT() {
}
addLine('Connecting...');
self.Scratch.BT = new ScratchBT();
return self.Scratch.BT.open();
}
function discoverBT() {
Scratch.BT.requestDevice({
return Scratch.BT.requestDevice({
majorDeviceClass: 8,
minorDeviceClass: 1
}).then(
@ -426,7 +557,7 @@ function discoverBT() {
}
function connectBT() {
Scratch.BT.connectDevice({
return Scratch.BT.connectDevice({
peripheralId: document.getElementById('peripheralId').value,
pin: "1234"
}).then(
@ -439,8 +570,11 @@ function connectBT() {
);
}
/**
* @param {any} message
*/
function sendMessage(message) {
Scratch.BT.sendMessage({
return Scratch.BT.sendMessage({
message: document.getElementById('messageBody').value,
encoding: 'base64'
}).then(
@ -454,7 +588,7 @@ function sendMessage(message) {
}
function beep() {
Scratch.BT.sendMessage({
return Scratch.BT.sendMessage({
message: 'DwAAAIAAAJQBgQKC6AOC6AM=',
encoding: 'base64'
}).then(
@ -467,19 +601,6 @@ function beep() {
);
}
function stringify(o) {
if (o instanceof Event) {
if (o instanceof ErrorEvent) {
return `${o.constructor.name} {error: ${stringify(o.error)}`;
}
return `${o.constructor.name} {type: "${o.type}"}`;
}
return JSON.stringify(o);//, o && Object.getOwnPropertyNames(o));
}
const follow = document.getElementById('follow');
const log = document.getElementById('log');
const closeButton = document.getElementById('closeBT');
closeButton.onclick = () => {
self.Scratch.BT.dispose();
@ -492,37 +613,80 @@ attachFunctionToButton('connectBT', connectBT);
attachFunctionToButton('send', sendMessage);
attachFunctionToButton('beep', beep);
class LogDisplay {
constructor (logElement, lineCount = 256) {
this._logElement = logElement;
this._lineCount = lineCount;
this._lines = [];
this._dirty = false;
this._follow = true;
/**
* @param {string} type
*/
function makeStressTest(type) {
const goButton = document.getElementById(`stress${type}`);
if (!goButton) {
addLine(`Could not activate stress test button for ${type}`);
return;
}
goButton.onclick = () => runStressTest(type);
}
addLine (text) {
this._lines.push(text);
if (!this._dirty) {
this._dirty = true;
requestAnimationFrame(() => {
this._trim();
this._logElement.textContent = this._lines.join('\n');
if (this._follow) {
this._logElement.scrollTop = this._logElement.scrollHeight;
}
this._dirty = false;
});
makeStressTest('BLE');
makeStressTest('BT');
/**
* @param {number} seconds
* @param {any} [returnVal]
*/
function sleep(seconds, returnVal) {
return (new Promise(resolve => {
setTimeout(() => { resolve(returnVal); }, 1000*seconds);
}));
}
/**
* @param {string} type
*/
async function runStressTest(type) {
const doPingElement = document.getElementById(`stressPing${type}`);
const doGetVersionElement = document.getElementById(`stressGetVersion${type}`);
const doDiscoverElement = document.getElementById(`stressDiscover${type}`);
const doConnectElement = document.getElementById(`stressConnect${type}`);
const {init, connect, discover} = (() => {
switch (type) {
case 'BLE':
return {
init: initBLE,
discover: discoverBLE,
connect: connectBLE,
};
case 'BT':
return {
init: initBT,
discover: discoverBT,
connect: connectBT,
};
default:
const message = `Don't know how to run a stress test for ${type}`;
addLine(message);
throw new Error(message);
}
}
})();
_trim () {
this._lines = this._lines.splice(-this._lineCount);
while (true) {
const session = await init(); // also disconnects previous session
if (doPingElement.checked) await pingMe(session);
if (doGetVersionElement.checked) await getVersion(session);
if (doDiscoverElement.checked) {
const discoverPromise = discover();
const didDiscoverPromise = new Promise(resolve => {
/**
* @param {Event} e
*/
function listener(e) {
resolve(e.params);
session._events.removeEventListener('didDiscoverPeripheral', listener);
}
session._events.addEventListener('didDiscoverPeripheral', listener);
});
await discoverPromise;
await didDiscoverPromise;
}
if (doConnectElement.checked) await connect();
}
}
const logDisplay = new LogDisplay(log);
function addLine(text) {
logDisplay.addLine(text);
logDisplay._follow = follow.checked;
}