*
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# fibre-js
|
||||
|
||||
This directory provides JavaScript bindings for [Fibre](https://github.com/samuelsadok/fibre). Its home is located [here](https://github.com/samuelsadok/fibre/tree/master/js). There's also a standalone repository for this directory [here](https://github.com/samuelsadok/fibre-js).
|
||||
|
||||
## Current Status
|
||||
|
||||
So far fibre-js was only tested to run in Chrome but it should work in any browser that [supports WebAssembly](https://caniuse.com/wasm).
|
||||
|
||||
fibre-js currently supports the following transport providers:
|
||||
|
||||
- WebUSB ([Chrome and Edge only](https://caniuse.com/webusb))
|
||||
|
||||
## How to use
|
||||
|
||||
So you want your website or Electron App to connect to some resource(s) using Fibre. Let's start with a simple HTML scaffolding.
|
||||
|
||||
```HTML
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>fibre-js Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="statusText">not connected</h1>
|
||||
<button id="connectBtn" disabled>Connect...</button>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Now, right before the closing `</body>`, insert a script tag to load and open fibre-js:
|
||||
|
||||
```HTML
|
||||
<script type="module">
|
||||
import { fibreOpen } from './fibre.js';
|
||||
fibreOpen().then((libfibre) => {
|
||||
// fibre-js is ready to use!
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
This will load the files `fibre.js`, `libfibre-wasm.js` and `libfibre-wasm.wasm` from the same directory the HTML file comes from so make sure those files exist.
|
||||
|
||||
Now that fibre-js is ready to use we can start discovering remote objects. For this we need to specify a filter string which tells Fibre where to look for objects. In this example we want to talk to a USB device that runs Fibre (more specfically an [ODrive](https://odriverobotics.com/)). The parameters we use here are something you can find out from reading the docs of your USB device or inspecting the device by using `lsusb` on a Linux or macOS computer.
|
||||
|
||||
```JS
|
||||
const filter = 'usb:idVendor=0x1209,idProduct=0x0D32,bInterfaceClass=0,bInterfaceSubClass=1,bInterfaceProtocol=0';
|
||||
const onFoundObject = async (obj) => {
|
||||
console.log("found an object!", obj);
|
||||
// TODO: do something with obj
|
||||
}
|
||||
libfibre.startDiscovery(filter, onFoundObject);
|
||||
```
|
||||
|
||||
Now that we have the object we can start calling functions on it. Let's assume our object has the property `vbus_voltage`. In the `onFoundObject` handle, add:
|
||||
|
||||
```JS
|
||||
while (true) {
|
||||
document.getElementById("statusText").innerHTML = "vbus_voltage: " + (await obj.vbus_voltage.read());
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
```
|
||||
|
||||
This will read the `vbus_voltage` property every 100ms and display the value on the web page.
|
||||
|
||||
Optionally you could add a callback to the `obj._onLost` promise in order to gracefully stop polling when the device disconnects.
|
||||
|
||||
Since we're using WebUSB in this example, there's one more thing left to do: The user needs to authorize the website (only once) to access the USB device. This must happen on a user gesture, which is why earlier we put a Connect-button into our HTML.
|
||||
|
||||
So directly after the call to `libfibre.startDiscovery()`, add:
|
||||
|
||||
```JS
|
||||
document.getElementById("connectBtn").onclick = libfibre.usbDiscoverer.showDialog;
|
||||
document.getElementById("connectBtn").removeAttribute('disabled');
|
||||
```
|
||||
|
||||
That's it! You can now open the HTML page in the browser and should see something like this (using Chrome 86 on Linux):
|
||||
|
||||

|
||||
|
||||
Note how the user doesn't need to manually reselect the device after the website is reloaded. The website is now already authorized to access the device so Fibre immediately connects to it. The same is true when the device is unpluggend and replugged to the computer.
|
||||
|
||||
You can find the complete example code [here](example.html). A more elaborate example which can connect to multiple objects can be found here [here](multidevice_example.html).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>fibre-js Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="statusText">not connected</h1>
|
||||
<button id="connectBtn" disabled>Connect...</button>
|
||||
<script type="module">
|
||||
import { fibreOpen } from './fibre.js';
|
||||
fibreOpen().then((libfibre) => {
|
||||
const filter = 'usb:idVendor=0x1209,idProduct=0x0D32,bInterfaceClass=0,bInterfaceSubClass=1,bInterfaceProtocol=0';
|
||||
const onFoundObject = async (obj) => {
|
||||
console.log("found an object!", obj);
|
||||
while (true) {
|
||||
document.getElementById("statusText").innerHTML = "vbus_voltage: " + (await obj.vbus_voltage.read());
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
libfibre.startDiscovery(filter, onFoundObject);
|
||||
|
||||
document.getElementById("connectBtn").onclick = libfibre.usbDiscoverer.showDialog;
|
||||
document.getElementById("connectBtn").removeAttribute('disabled');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,724 @@
|
||||
|
||||
import wasm from './libfibre-wasm.js';
|
||||
|
||||
function assert(expression) {
|
||||
if (!expression) {
|
||||
throw Error("assert failed");
|
||||
}
|
||||
}
|
||||
|
||||
const FIBRE_STATUS_OK = 0;
|
||||
const FIBRE_STATUS_CANCELLED = 1;
|
||||
const FIBRE_STATUS_CLOSED = 2;
|
||||
const FIBRE_STATUS_INVALID_ARGUMENT = 3;
|
||||
const FIBRE_STATUS_INTERNAL_ERROR = 4;
|
||||
|
||||
const ptrSize = 4;
|
||||
|
||||
function _getError(code) {
|
||||
if (code == FIBRE_CANCELLED) {
|
||||
return Error("libfibre: Operation Cancelled");
|
||||
} else if (code == FIBRE_STATUS_CLOSED) {
|
||||
return Error("libfibre: Closed");
|
||||
} else if (code == FIBRE_STATUS_INVALID_ARGUMENT) {
|
||||
return Error("libfibre: Invalid Argument");
|
||||
} else if (code == FIBRE_STATUS_INTERNAL_ERROR) {
|
||||
return Error("libfibre: Internal Error");
|
||||
} else {
|
||||
return Error("libfibre: Unknown Error " + code);
|
||||
}
|
||||
}
|
||||
|
||||
class BasicCodec {
|
||||
constructor(typeName, byteLength, littleEndian) {
|
||||
this.getSize = () => byteLength;
|
||||
this.serialize = (libfibre, val) => {
|
||||
var myBuf = new ArrayBuffer(byteLength);
|
||||
new DataView(myBuf)['set' + typeName](0, val, littleEndian)
|
||||
return Array.from(new Uint8Array(myBuf));
|
||||
}
|
||||
this.deserialize = (libfibre, buf) => {
|
||||
var myBuf = new ArrayBuffer(buf.length);
|
||||
new Uint8Array(myBuf).set(buf);
|
||||
return new DataView(myBuf)['get' + typeName](0, littleEndian);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const codecs = {
|
||||
'int8': new BasicCodec('Int8', 1, true),
|
||||
'uint8': new BasicCodec('Uint8', 1, true),
|
||||
'int16': new BasicCodec('Int16', 2, true),
|
||||
'uint16': new BasicCodec('Uint16', 2, true),
|
||||
'int32': new BasicCodec('Int32', 4, true),
|
||||
'uint32': new BasicCodec('Uint32', 4, true),
|
||||
'int64': new BasicCodec('BigInt64', 8, true),
|
||||
'uint64': new BasicCodec('BigUint64', 8, true),
|
||||
'float': new BasicCodec('Float32', 4, true),
|
||||
};
|
||||
|
||||
class EofError extends Error {
|
||||
constructor(msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
class TxStream {
|
||||
constructor(libfibre, handle) {
|
||||
this._libfibre = libfibre;
|
||||
this._handle = handle;
|
||||
this.isClosed = false;
|
||||
this._libfibre._txStreamMap[this._handle] = this;
|
||||
}
|
||||
|
||||
write(data) {
|
||||
assert(this._handle);
|
||||
return new Promise((resolve, reject) => {
|
||||
let byteLength = Array.isArray(data) ? data.length : data.byteLength;
|
||||
if (!Array.isArray(data)) {
|
||||
data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
||||
}
|
||||
this._buf = [this._libfibre.wasm.malloc(byteLength), byteLength];
|
||||
this._libfibre.wasm.Module.HEAPU8.set(data, this._buf[0]);
|
||||
this._writeResolve = resolve;
|
||||
this._writeReject = reject;
|
||||
this._libfibre.wasm.Module._libfibre_start_tx(this._handle, this._buf[0], this._buf[1], this._libfibre._onTxCompleted, this._handle);
|
||||
});
|
||||
}
|
||||
|
||||
async writeAll(data) {
|
||||
while (true) {
|
||||
const nWritten = await this.write(data);
|
||||
data = data.slice(nWritten);
|
||||
if (data.length == 0) {
|
||||
break;
|
||||
}
|
||||
if (this.isClosed) {
|
||||
throw EofError(`the TX stream was closed but there are still ${data.length} bytes left to send`);
|
||||
}
|
||||
assert(nWritten > 0); // ensure progress
|
||||
}
|
||||
}
|
||||
|
||||
close(status) {
|
||||
assert(this._handle);
|
||||
this._libfibre.wasm.Module._libfibre_close_tx(this._handle, status);
|
||||
delete this._handle;
|
||||
}
|
||||
|
||||
_complete(status, txEnd) {
|
||||
const len = txEnd - this._buf[0];
|
||||
assert(len <= this._buf[1]);
|
||||
this._libfibre.wasm.free(this._buf[0]);
|
||||
delete this._buf;
|
||||
const resolve = this._writeResolve;
|
||||
const reject = this._writeReject;
|
||||
delete this._writeResolve;
|
||||
delete this._writeReject;
|
||||
|
||||
this.isClosed = this.isClosed || (status == FIBRE_STATUS_CLOSED);
|
||||
|
||||
if (status == FIBRE_STATUS_OK || status == FIBRE_STATUS_CLOSED) {
|
||||
resolve(len);
|
||||
} else {
|
||||
reject(_getError(status));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RxStream {
|
||||
constructor(libfibre, handle) {
|
||||
this._libfibre = libfibre;
|
||||
this._handle = handle;
|
||||
this.isClosed = false;
|
||||
this._libfibre._rxStreamMap[this._handle] = this;
|
||||
}
|
||||
|
||||
read(len) {
|
||||
assert(this._handle);
|
||||
return new Promise((resolve, reject) => {
|
||||
this._buf = [this._libfibre.wasm.malloc(len), len];
|
||||
this._readResolve = resolve;
|
||||
this._readReject = reject;
|
||||
this._libfibre.wasm.Module._libfibre_start_rx(this._handle, this._buf[0], this._buf[1], this._libfibre._onRxCompleted, this._handle);
|
||||
});
|
||||
}
|
||||
|
||||
async readAll(len) {
|
||||
let data = [];
|
||||
while (true) {
|
||||
let chunk = await this.read(len - data.length);
|
||||
data.push(...chunk);
|
||||
if (data.length >= len) {
|
||||
break;
|
||||
}
|
||||
if (this.isClosed) {
|
||||
throw EofError(`the RX stream was closed but there are still ${len - data.length} bytes left to receive`);
|
||||
}
|
||||
assert(chunk.length > 0); // ensure progress
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
close(status) {
|
||||
assert(this._handle);
|
||||
this._libfibre.wasm.Module._libfibre_close_rx(this._handle, status);
|
||||
delete this._handle;
|
||||
}
|
||||
|
||||
_complete(status, rxEnd) {
|
||||
const len = rxEnd - this._buf[0];
|
||||
assert(len <= this._buf[1]);
|
||||
const result = new Uint8Array(this._libfibre.wasm.Module.HEAPU8.buffer, this._buf[0], len).slice(0);
|
||||
this._libfibre.wasm.free(this._buf[0]);
|
||||
delete this._buf;
|
||||
const resolve = this._readResolve;
|
||||
const reject = this._readReject;
|
||||
delete this._readResolve;
|
||||
delete this._readReject;
|
||||
|
||||
this.isClosed = this.isClosed || (status == FIBRE_STATUS_CLOSED);
|
||||
|
||||
if (status == FIBRE_STATUS_OK || status == FIBRE_STATUS_CLOSED) {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(_getError(status));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteInterface {
|
||||
constructor(libfibre, intfName) {
|
||||
this._libfibre = libfibre;
|
||||
this.intfName = intfName;
|
||||
this._onLost = new Promise((resolve) => this._onLostResolve = resolve);
|
||||
}
|
||||
}
|
||||
|
||||
const _staleObject = {};
|
||||
|
||||
class RemoteAttribute {
|
||||
constructor(libfibre, handle, subintf, subintfName) {
|
||||
this._libfibre = libfibre;
|
||||
this._handle = handle;
|
||||
this._subintf = subintf;
|
||||
this._subintfName = subintfName;
|
||||
}
|
||||
|
||||
get(obj) {
|
||||
let objHandlePtr = this._libfibre.wasm.malloc(ptrSize);
|
||||
try {
|
||||
this._libfibre.wasm.Module._libfibre_get_attribute(obj._handle, this._handle, objHandlePtr);
|
||||
return this._libfibre._objMap[this._libfibre._derefIntPtr(objHandlePtr)];
|
||||
} finally {
|
||||
this._libfibre.wasm.free(objHandlePtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteFunction extends Function {
|
||||
constructor(libfibre, handle, inputArgs, outputArgs) {
|
||||
let closure = function(...args) {
|
||||
return closure._call(this, ...args);
|
||||
}
|
||||
closure._libfibre = libfibre;
|
||||
closure._handle = handle;
|
||||
closure._inputArgs = inputArgs;
|
||||
closure._outputArgs = outputArgs;
|
||||
// Return closure instead of the original "this" object that was
|
||||
// associated with the constructor call.
|
||||
return Object.setPrototypeOf(closure, new.target.prototype);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Starts a function call on this function.
|
||||
* @return {TxStream, RxStream, Promise} A TX stream that can be used to
|
||||
* send data on this function call, an RX stream that can be used to receive
|
||||
* data on this function call and a promise that will be completed once the
|
||||
* function call completes.
|
||||
*/
|
||||
startCall(obj) {
|
||||
let _callHandle, _txStreamHandle, _rxStreamHandle;
|
||||
let completionPromise = new Promise((resolve, reject) => {
|
||||
let completer = (status) => {
|
||||
if (status == FIBRE_STATUS_OK) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(_getError(status));
|
||||
}
|
||||
};
|
||||
[_callHandle, _txStreamHandle, _rxStreamHandle] = this._libfibre._withOutputArg(
|
||||
(_callHandlePtr, _txStreamHandlePtr, _rxStreamHandlePtr) =>
|
||||
this._libfibre.wasm.Module._libfibre_start_call(obj._handle, this._handle,
|
||||
_callHandlePtr,
|
||||
_txStreamHandlePtr, _rxStreamHandlePtr, this._libfibre._onCallCompleted,
|
||||
this._libfibre._allocRef(completer)));
|
||||
});
|
||||
return [
|
||||
new TxStream(this._libfibre, _txStreamHandle),
|
||||
new RxStream(this._libfibre, _rxStreamHandle),
|
||||
completionPromise];
|
||||
}
|
||||
|
||||
async _call(obj, ...args) {
|
||||
assert(args.length == this._inputArgs.length);
|
||||
|
||||
let txBuf = [];
|
||||
for (let argSpec of this._inputArgs) {
|
||||
txBuf.push(...argSpec.codec.serialize());
|
||||
}
|
||||
|
||||
const rxLen = this._outputArgs.reduce((a, b) => a + b.codec.getSize(), 0);
|
||||
assert(Number.isInteger(rxLen));
|
||||
|
||||
const [txStream, rxStream, completionPromise] = this.startCall(obj);
|
||||
|
||||
// RX must be started before TX
|
||||
const rxPromise = rxStream.readAll(rxLen);
|
||||
await txStream.writeAll(txBuf);
|
||||
let rxBuf = await rxPromise;
|
||||
await completionPromise;
|
||||
|
||||
let outputs = [];
|
||||
for (let argSpec of this._outputArgs) {
|
||||
let argLength = argSpec.codec.getSize()
|
||||
outputs.push(argSpec.codec.deserialize(self._libfibre, rxBuf.slice(0, argLength)))
|
||||
rxBuf = rxBuf.slice(argLength);
|
||||
}
|
||||
|
||||
if (outputs.length == 1) {
|
||||
return outputs[0];
|
||||
} else if (outputs.length > 1) {
|
||||
return outputs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LibFibre {
|
||||
constructor(wasm) {
|
||||
this.wasm = wasm;
|
||||
|
||||
// The following functions are JavaScript callbacks that get invoked from C
|
||||
// code (WebAssembly). We convert each function to a pointer which can then
|
||||
// be passed to C code.
|
||||
// Function signature codes are documented here:
|
||||
// https://github.com/aheejin/emscripten/blob/master/site/source/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.rst#calling-javascript-functions-as-function-pointers-from-c
|
||||
|
||||
this._onPost = wasm.addFunction((cb, cbCtx) => {
|
||||
// TODO
|
||||
console.log("event loop post");
|
||||
return new Promise((resolve, reject) => {
|
||||
cb(cbCtx);
|
||||
resolve();
|
||||
});
|
||||
}, 'iii');
|
||||
|
||||
this._onCallLater = wasm.addFunction((delay, cb, cbCtx) => {
|
||||
console.log("event loop call later");
|
||||
return setTimeout(() => cb(cbCtx), delay * 1000);
|
||||
}, 'ifii')
|
||||
|
||||
this._onCancelTimer = wasm.addFunction((timer) => {
|
||||
return cancelTimeout(timer);
|
||||
}, 'ii')
|
||||
|
||||
this._onConstructObject = wasm.addFunction((ctx, obj, intf, intfName, intfNameLength) => {
|
||||
let jsIntf = this._loadJsIntf(intf, this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, intfName, intfNameLength));
|
||||
this._objMap[obj] = new jsIntf(this, obj);
|
||||
}, 'viiiii')
|
||||
|
||||
this._onDestroyObject = wasm.addFunction((ctx, obj) => {
|
||||
let jsObj = this._objMap[obj];
|
||||
delete this._objMap[obj];
|
||||
delete jsObj._handle;
|
||||
Object.setPrototypeOf(jsObj, _staleObject);
|
||||
console.log("destroyed remote object");
|
||||
jsObj._oldHandle = obj;
|
||||
jsObj._onLostResolve();
|
||||
}, 'vii')
|
||||
|
||||
this._onStartDiscovery = wasm.addFunction((ctx, discCtx, specs, specsLength) => {
|
||||
console.log("start discovery");
|
||||
let discoverer = this._deref(ctx);
|
||||
discoverer.startChannelDiscovery(
|
||||
this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, specs, specsLength),
|
||||
discCtx);
|
||||
}, 'viiii')
|
||||
|
||||
this._onStopDiscovery = wasm.addFunction((ctx, discCtx) => {
|
||||
console.log("stop discovery");
|
||||
this._freeRef(ctx);
|
||||
}, 'viiii')
|
||||
|
||||
this._onFoundObject = wasm.addFunction((ctx, obj) => {
|
||||
let onFoundObject = this._deref(ctx);
|
||||
onFoundObject(this._objMap[obj]);
|
||||
}, 'vii')
|
||||
|
||||
this._onAttributeAdded = wasm.addFunction((ctx, attr, name, nameNength, subintf, subintfName, subintfNameLength) => {
|
||||
let jsIntf = this._intfMap[ctx];
|
||||
let jsAttr = new RemoteAttribute(this, attr, subintf, this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, subintfName, subintfNameLength));
|
||||
Object.defineProperty(jsIntf.prototype, this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, name, nameNength), {
|
||||
get: function () { return jsAttr.get(this); }
|
||||
});
|
||||
}, 'viiiiiii')
|
||||
|
||||
this._onAttributeRemoved = wasm.addFunction((ctx, attr) => {
|
||||
console.log("attribute removed"); // TODO
|
||||
}, 'vii')
|
||||
|
||||
this._onFunctionAdded = wasm.addFunction((ctx, func, name, nameLength, inputNames, inputCodecs, outputNames, outputCodecs) => {
|
||||
const jsIntf = this._intfMap[ctx];
|
||||
const jsInputParams = [...this._decodeArgList(inputNames, inputCodecs)];
|
||||
const jsOutputParams = [...this._decodeArgList(outputNames, outputCodecs)];
|
||||
let jsFunc = new RemoteFunction(this, func, jsInputParams, jsOutputParams);
|
||||
jsIntf.prototype[this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, name, nameLength)] = jsFunc;
|
||||
}, 'viiiiiiii')
|
||||
|
||||
this._onFunctionRemoved = wasm.addFunction((ctx, func) => {
|
||||
console.log("function removed"); // TODO
|
||||
}, 'vii')
|
||||
|
||||
this._onCallCompleted = wasm.addFunction((ctx, status) => {
|
||||
let completer = this._freeRef(ctx);
|
||||
completer(status);
|
||||
}, 'vii');
|
||||
|
||||
this._onTxCompleted = wasm.addFunction((ctx, txStream, status, txEnd) => {
|
||||
this._txStreamMap[txStream]._complete(status, txEnd);
|
||||
}, 'viiii')
|
||||
|
||||
this._onRxCompleted = wasm.addFunction((ctx, rxStream, status, rxEnd) => {
|
||||
this._rxStreamMap[rxStream]._complete(status, rxEnd);
|
||||
}, 'viiii')
|
||||
|
||||
const ptr = this.wasm.Module._libfibre_get_version();
|
||||
const version_array = (new Uint16Array(wasm.Module.HEAPU8.buffer, ptr, 3))
|
||||
this.version = {
|
||||
major: version_array[0],
|
||||
minor: version_array[1],
|
||||
patch: version_array[2]
|
||||
};
|
||||
|
||||
this._intfMap = {};
|
||||
this._refMap = {};
|
||||
this._objMap = {};
|
||||
this._txStreamMap = {};
|
||||
this._rxStreamMap = {};
|
||||
|
||||
this._handle = this.wasm.Module._libfibre_open(
|
||||
this._onPost,
|
||||
0,
|
||||
0,
|
||||
this._onCallLater,
|
||||
this._onCancelTimer,
|
||||
this._onConstructObject,
|
||||
this._onDestroyObject
|
||||
);
|
||||
|
||||
this.usbDiscoverer = new WebUsbDiscoverer(this);
|
||||
|
||||
let buf = [this.wasm.malloc(4), 4];
|
||||
try {
|
||||
let len = this.wasm.stringToUTF8Array("usb", this.wasm.Module.HEAPU8, buf[0], buf[1]);
|
||||
this.wasm.Module._libfibre_register_discoverer(this._handle, buf[0], len,
|
||||
this._onStartDiscovery, this._onStopDiscovery, this._allocRef(this.usbDiscoverer));
|
||||
} finally {
|
||||
this.wasm.free(buf[0]);
|
||||
}
|
||||
}
|
||||
|
||||
addChannels(mtu, channelDiscoveryHandle) {
|
||||
console.log("add channel with mtu " + mtu);
|
||||
const [txChannelId, rxChannelId] = this._withOutputArg((txChannelIdPtr, rxChannelIdPtr) =>
|
||||
this.wasm.Module._libfibre_add_channels(this._handle, channelDiscoveryHandle, txChannelIdPtr, rxChannelIdPtr, mtu)
|
||||
);
|
||||
return [
|
||||
new RxStream(this, txChannelId),
|
||||
new TxStream(this, rxChannelId)
|
||||
];
|
||||
}
|
||||
|
||||
startDiscovery(filter, onFoundObject) {
|
||||
let buf = [this.wasm.malloc(filter.length + 1), filter.length + 1];
|
||||
try {
|
||||
let len = this.wasm.stringToUTF8Array(filter, this.wasm.Module.HEAPU8, buf[0], buf[1]);
|
||||
this._withOutputArg((discoveryHandlePtr) =>
|
||||
this.wasm.Module._libfibre_start_discovery(this._handle, buf[0], len,
|
||||
discoveryHandlePtr,
|
||||
this._onFoundObject, this._onStopped, this._allocRef(onFoundObject)));
|
||||
} finally {
|
||||
this.wasm.free(buf[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Allocates a unique integer key for the specified JavaScript object.
|
||||
*
|
||||
* The unique key can later be used to look up the JavaScript object using
|
||||
* _freeRef().
|
||||
*/
|
||||
_allocRef(obj) {
|
||||
let id = 0;
|
||||
while (++id in this._refMap) {}
|
||||
console.log("allocating id " + id, obj);
|
||||
this._refMap[id] = obj;
|
||||
return id;
|
||||
}
|
||||
|
||||
_deref(id) {
|
||||
return this._refMap[id];
|
||||
}
|
||||
|
||||
_derefIntPtr(ptr) {
|
||||
return new Uint32Array(this.wasm.Module.HEAPU8.buffer, ptr, 1)[0]
|
||||
}
|
||||
|
||||
* _getStringList(ptr) {
|
||||
for (let i = 0; this._derefIntPtr(ptr + i * ptrSize); ++i) {
|
||||
yield this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, this._derefIntPtr(ptr + i * ptrSize));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @bried Deallocates a unique integer key that was previously obtained with
|
||||
* _allocRef.
|
||||
*
|
||||
* @return The JavaScript object that was passed to the corresponding
|
||||
* _allocRef call.
|
||||
*/
|
||||
_freeRef(id) {
|
||||
let obj = this._refMap[id];
|
||||
delete this._refMap[id];
|
||||
console.log("freeing id " + id, obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
_withOutputArg(func) {
|
||||
let ptrs = [];
|
||||
try {
|
||||
for (let i = 0; i < func.length; ++i) {
|
||||
ptrs.push(this.wasm.malloc(ptrSize));
|
||||
}
|
||||
func(...ptrs);
|
||||
return ptrs.map((ptr) => this._derefIntPtr(ptr));
|
||||
} finally {
|
||||
for (let ptr of ptrs) {
|
||||
this.wasm.free(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
* _decodeArgList(argNames, codecNames) {
|
||||
argNames = [...this._getStringList(argNames)];
|
||||
codecNames = [...this._getStringList(codecNames)];
|
||||
assert(argNames.length == codecNames.length);
|
||||
for (let i in argNames) {
|
||||
yield {
|
||||
name: argNames[i],
|
||||
codecName: codecNames[i],
|
||||
codec: codecs[codecNames[i]]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
_loadJsIntf(intfHandle, intfName) {
|
||||
if (intfHandle in this._intfMap) {
|
||||
return this._intfMap[intfHandle];
|
||||
} else {
|
||||
class RemoteObject extends RemoteInterface {
|
||||
constructor(libfibre, handle) {
|
||||
super(libfibre, intfName);
|
||||
this._handle = handle;
|
||||
}
|
||||
};
|
||||
let jsIntf = RemoteObject;
|
||||
this._intfMap[intfHandle] = jsIntf;
|
||||
this.wasm.Module._libfibre_subscribe_to_interface(intfHandle,
|
||||
this._onAttributeAdded,
|
||||
this._onAttributeRemoved,
|
||||
this._onFunctionAdded,
|
||||
this._onFunctionRemoved,
|
||||
intfHandle);
|
||||
return jsIntf;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function fibreOpen() {
|
||||
return new Promise(async (resolve) => {
|
||||
let Module = {
|
||||
preRun: [function() {Module.ENV.FIBRE_LOG = "4"}], // enable logging
|
||||
instantiateWasm: async (info, receiveInstance) => {
|
||||
const isWebpack = typeof __webpack_require__ === 'function';
|
||||
const wasmPath = isWebpack ? (await import("!!file-loader!./libfibre-wasm.wasm")).default
|
||||
: "./libfibre-wasm.wasm";
|
||||
const response = fetch(wasmPath, { credentials: 'same-origin' });
|
||||
const result = await WebAssembly.instantiateStreaming(response, info);
|
||||
receiveInstance(result['instance']);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
Module = await wasm(Module);
|
||||
await Module.ready;
|
||||
wasm.addFunction = Module.addFunction;
|
||||
wasm.stringToUTF8Array = Module.stringToUTF8Array;
|
||||
wasm.UTF8ArrayToString = Module.UTF8ArrayToString;
|
||||
wasm.malloc = Module._malloc;
|
||||
wasm.free = Module._free;
|
||||
wasm.Module = Module;
|
||||
|
||||
resolve(new LibFibre(wasm));
|
||||
});
|
||||
}
|
||||
|
||||
class WebUsbDiscoverer {
|
||||
constructor(libfibre) {
|
||||
this._libfibre = libfibre;
|
||||
this._discoveryProcesses = [];
|
||||
|
||||
if (navigator.usb.onconnect != null) {
|
||||
console.warn("There was already a subscriber for usb.onconnect.");
|
||||
}
|
||||
navigator.usb.onconnect = (event) => {
|
||||
for (let discovery of this._discoveryProcesses) {
|
||||
this._consider(event.device, discovery.filter, discovery.handle);
|
||||
}
|
||||
}
|
||||
navigator.usb.ondisconnect = () => console.log("disconnect");
|
||||
|
||||
this._filterKeys = {
|
||||
'idVendor': 'vendorId',
|
||||
'idProduct': 'productId',
|
||||
'bInterfaceClass': 'classCode',
|
||||
'bInterfaceSubClass': 'subclassCode',
|
||||
'bInterfaceProtocol': 'protocolCode',
|
||||
};
|
||||
|
||||
this.showDialog = async () => {
|
||||
let filters = this._discoveryProcesses.map((d) => d.filter);
|
||||
let dev = await navigator.usb.requestDevice({filters: filters});
|
||||
for (let discovery of this._discoveryProcesses) {
|
||||
this._consider(dev, discovery.filter, discovery.handle);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async startChannelDiscovery(specs, discoveryHandle) {
|
||||
let filter = {}
|
||||
for (let item of specs.split(',')) {
|
||||
filter[this._filterKeys[item.split('=')[0]]] = item.split('=')[1]
|
||||
}
|
||||
|
||||
this._discoveryProcesses.push({
|
||||
filter: filter,
|
||||
handle: discoveryHandle
|
||||
});
|
||||
|
||||
for (let dev of await navigator.usb.getDevices({filters: [filter]})) {
|
||||
this._consider(dev, filter, discoveryHandle);
|
||||
}
|
||||
}
|
||||
|
||||
async _consider(device, filter, channelDiscoveryHandle) {
|
||||
if ((filter.vendorId != undefined) && (filter.vendorId != device.vendorId)) {
|
||||
return;
|
||||
}
|
||||
if ((filter.productId != undefined) && (filter.productId != device.productId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let config of device.configurations) {
|
||||
if (device.configuration !== null && device.configuration.configurationValue != config.configurationValue) {
|
||||
continue; // A configuration was already set and it's different from this one
|
||||
}
|
||||
|
||||
for (let intf of config.interfaces) {
|
||||
for (let alternate of intf.alternates) {
|
||||
const mismatch = ((filter.classCode != undefined) && (filter.classCode != alternate.interfaceClass))
|
||||
|| ((filter.subclassCode != undefined) && (filter.subclassCode != alternate.interfaceSubclass))
|
||||
|| ((filter.protocolCode != undefined) && (filter.protocolCode != alternate.interfaceProtocol));
|
||||
if (mismatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await device.open();
|
||||
await device.selectConfiguration(config.configurationValue);
|
||||
await device.claimInterface(intf.interfaceNumber);
|
||||
|
||||
let epOut = null, epIn = null;
|
||||
for (let ep of alternate.endpoints) {
|
||||
if (ep.type == "bulk" && ep.direction == "in")
|
||||
epIn = ep;
|
||||
else if (ep.type == "bulk" && ep.direction == "out")
|
||||
epOut = ep;
|
||||
}
|
||||
|
||||
device.knownInEndpoints = device.knownInEndpoints || [];
|
||||
device.knownOutEndpoints = device.knownOutEndpoints || [];
|
||||
console.log(device.knownOutEndpoints);
|
||||
if (device.knownInEndpoints.indexOf(epIn.endpointNumber) >= 0) {
|
||||
continue;
|
||||
}
|
||||
if (device.knownOutEndpoints.indexOf(epOut.endpointNumber) >= 0) {
|
||||
continue;
|
||||
}
|
||||
device.knownInEndpoints.push(epIn.endpointNumber);
|
||||
device.knownOutEndpoints.push(epOut.endpointNumber);
|
||||
|
||||
let mtu = Math.min(epIn.packetSize, epOut.packetSize);
|
||||
const [txChannel, rxChannel] = this._libfibre.addChannels(mtu, channelDiscoveryHandle);
|
||||
this._connectBulkInEp(device, epIn, rxChannel);
|
||||
this._connectBulkOutEp(device, epOut, txChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _connectBulkOutEp(dev, ep, stream) {
|
||||
while (true) {
|
||||
//console.log("waiting for data from libfibre...");
|
||||
const data = await stream.read(ep.packetSize);
|
||||
//console.log("forwarding " + data.length + " bytes to USB...");
|
||||
//console.log(data);
|
||||
let result;
|
||||
try {
|
||||
result = await dev.transferOut(ep.endpointNumber, data);
|
||||
} catch (e) { // TODO: propagate actual transfer errors
|
||||
result = {status: "stall"};
|
||||
}
|
||||
assert(result.bytesWritten == data.length);
|
||||
console.log(dev.opened);
|
||||
if (!dev.opened || result.status == "stall") {
|
||||
stream.close(FIBRE_STATUS_CLOSED);
|
||||
break;
|
||||
}
|
||||
assert(result.status == "ok")
|
||||
};
|
||||
}
|
||||
|
||||
async _connectBulkInEp(dev, ep, stream) {
|
||||
while (true) {
|
||||
//console.log("waiting for up to " + epIn.packetSize + " bytes from USB...");
|
||||
let result;
|
||||
try {
|
||||
result = await dev.transferIn(ep.endpointNumber, ep.packetSize);
|
||||
} catch (e) { // TODO: propagate actual transfer errors
|
||||
result = {status: "stall"};
|
||||
}
|
||||
//console.log("got for data from USB...");
|
||||
console.log(dev.opened);
|
||||
console.log(dev);
|
||||
if (!dev.opened || result.status == "stall") {
|
||||
stream.close(FIBRE_STATUS_CLOSED);
|
||||
break;
|
||||
}
|
||||
assert(result.status == "ok");
|
||||
//console.log("forwarding " + result.data.byteLength + " bytes to libfibre...");
|
||||
const len = await stream.write(result.data);
|
||||
assert(len == result.data.byteLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>WebAssembly Test</title>
|
||||
<style>
|
||||
div {
|
||||
font-size : 30px; text-align : left; color:darkblue;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="textcontent">loading...</div>
|
||||
<script src="fibre.js"></script>
|
||||
<script async>
|
||||
const filter = 'usb:idVendor=0x1209,idProduct=0x0D32,bInterfaceClass=0,bInterfaceSubClass=1,bInterfaceProtocol=0';
|
||||
let showStatus = (status) => document.getElementById("textcontent").innerHTML = status;
|
||||
var connectedObjects = [];
|
||||
|
||||
displayLoop = async () => {
|
||||
while (connectedObjects.length) {
|
||||
console.log(connectedObjects.length);
|
||||
let results = await Promise.all(connectedObjects.map(async (dev) => await dev.vbus_voltage.read()));
|
||||
showStatus('connected to ' + results.length + ' devices' + results.map((x) => "<br>vbus_voltage: " + x + "V").join());
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
showStatus("no devices connected");
|
||||
}
|
||||
|
||||
let onFoundObject = async (obj) => {
|
||||
connectedObjects.push(obj);
|
||||
obj._onLost.then(() => connectedObjects.splice(connectedObjects.indexOf(obj), 1));
|
||||
if (connectedObjects.length == 1) {
|
||||
displayLoop();
|
||||
}
|
||||
}
|
||||
|
||||
fibreOpen().then((libfibre) => {
|
||||
libfibre.startDiscovery(filter, onFoundObject);
|
||||
document.getElementById("connectBtn").onclick = libfibre.usbDiscoverer.showDialog;
|
||||
document.getElementById("connectBtn").removeAttribute('disabled');
|
||||
showStatus("loaded WASM libfibre version " + libfibre.version.major + "." + libfibre.version.minor + "." + libfibre.version.patch);
|
||||
});
|
||||
</script>
|
||||
<button id="connectBtn" disabled>Connect</button>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "fibre-js",
|
||||
"version": "0.1.0",
|
||||
"eslintConfig": {
|
||||
"rules": {
|
||||
"no-empty": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-undef": "off",
|
||||
"no-constant-condition": "off",
|
||||
"no-extra-semi": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"no-redeclare": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-global-assign": "off",
|
||||
"no-useless-escape": "off",
|
||||
"no-useless-catch": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user