mirror of
https://github.com/odriverobotics/ODrive.git
synced 2026-08-20 22:14:34 +08:00
725 lines
26 KiB
JavaScript
725 lines
26 KiB
JavaScript
|
|
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);
|
|
}
|
|
}
|
|
}
|