import {SerialPortFlushMode, SerialPortRemote, SerialReceiveError, SerialPortReceiver, SerialSendError} from '/gen/services/device/public/mojom/serial.mojom.m.js';
import {SerialService, SerialServiceReceiver} from '/gen/third_party/blink/public/mojom/serial/serial.mojom.m.js';
// Implementation of an UnderlyingSource to create a ReadableStream from a Mojo
// data pipe consumer handle.
class DataPipeSource {
constructor(consumer) {
this.consumer_ = consumer;
}
async pull(controller) {
let chunk = new ArrayBuffer(64);
let {result, numBytes} = this.consumer_.readData(chunk);
if (result == Mojo.RESULT_OK) {
controller.enqueue(new Uint8Array(chunk, 0, numBytes));
return;
} else if (result == Mojo.RESULT_FAILED_PRECONDITION) {
controller.close();
return;
} else if (result == Mojo.RESULT_SHOULD_WAIT) {
await this.readable();
return this.pull(controller);
}
}
cancel() {
if (this.watcher_)
this.watcher_.cancel();
this.consumer_.close();
}
readable() {
return new Promise((resolve) => {
this.watcher_ =
this.consumer_.watch({ readable: true, peerClosed: true }, () => {
this.watcher_.cancel();
this.watcher_ = undefined;
resolve();
});
});
}
}
// Implementation of an UnderlyingSink to create a WritableStream from a Mojo
// data pipe producer handle.
class DataPipeSink {
constructor(producer) {
this._producer = producer;
}
async write(chunk, controller) {
while (true) {
let {result, numBytes} = this._producer.writeData(chunk);
if (result == Mojo.RESULT_OK) {
if (numBytes == chunk.byteLength) {
return;
}
chunk = chunk.slice(numBytes);
} else if (result == Mojo.RESULT_FAILED_PRECONDITION) {
throw new DOMException('The pipe is closed.', 'InvalidStateError');
} else if (result == Mojo.RESULT_SHOULD_WAIT) {
await this.writable();
}
}
}
close() {
assert_equals(undefined, this._watcher);
this._producer.close();
}
abort(reason) {
if (this._watcher)
this._watcher.cancel();
this._producer.close();
}
writable() {
return new Promise((resolve) => {
this._watcher =
this._producer.watch({ writable: true, peerClosed: true }, () => {
this._watcher.cancel();
this._watcher = undefined;
resolve();
});
});
}
}
// Implementation of device.mojom.SerialPort.
class FakeSerialPort {
constructor() {
this.inputSignals_ = {
dataCarrierDetect: false,
clearToSend: false,
ringIndicator: false,
dataSetReady: false
};
this.inputSignalFailure_ = false;
this.outputSignals_ = {
dataTerminalReady: false,
requestToSend: false,
break: false
};
this.outputSignalFailure_ = false;
}
open(options, client) {
if (this.receiver_ !== undefined) {
// Port already open.
return null;
}
let port = new SerialPortRemote();
this.receiver_ = new SerialPortReceiver(this);
this.receiver_.$.bindHandle(port.$.bindNewPipeAndPassReceiver().handle);
this.options_ = options;
this.client_ = client;
// OS typically sets DTR on open.
this.outputSignals_.dataTerminalReady = true;
return port;
}
write(data) {
return this.writer_.write(data);
}
read() {
return this.reader_.read();
}
// Reads from the port until at least |targetLength| is read or the stream is
// closed. The data is returned as a combined Uint8Array.
readWithLength(targetLength) {
return readWithLength(this.reader_, targetLength);
}
simulateReadError(error) {
this.writer_.close();
this.writer_.releaseLock();
this.writer_ = undefined;
this.writable_ = undefined;
this.client_.onReadError(error);
}
simulateParityError() {
this.simulateReadError(SerialReceiveError.PARITY_ERROR);
}
simulateDisconnectOnRead() {
this.simulateReadError(SerialReceiveError.DISCONNECTED);
}
simulateWriteError(error) {
this.reader_.cancel();
this.reader_ = undefined;
this.readable_ = undefined;
this.client_.onSendError(error);
}
simulateSystemErrorOnWrite() {
this.simulateWriteError(SerialSendError.SYSTEM_ERROR);
}
simulateDisconnectOnWrite() {
this.simulateWriteError(SerialSendError.DISCONNECTED);
}
simulateInputSignals(signals) {
this.inputSignals_ = signals;
}
simulateInputSignalFailure(fail) {
this.inputSignalFailure_ = fail;
}
get outputSignals() {
return this.outputSignals_;
}
simulateOutputSignalFailure(fail) {
this.outputSignalFailure_ = fail;
}
writable() {
if (this.writable_)
return Promise.resolve();
if (!this.writablePromise_) {
this.writablePromise_ = new Promise((resolve) => {
this.writableResolver_ = resolve;
});
}
return this.writablePromise_;
}
readable() {
if (this.readable_)
return Promise.resolve();
if (!this.readablePromise_) {
this.readablePromise_ = new Promise((resolve) => {
this.readableResolver_ = resolve;
});
}
return this.readablePromise_;
}
async startWriting(in_stream) {
this.readable_ = new ReadableStream(new DataPipeSource(in_stream));
this.reader_ = this.readable_.getReader();
if (this.readableResolver_) {
this.readableResolver_();
this.readableResolver_ = undefined;
this.readablePromise_ = undefined;
}
}
async startReading(out_stream) {
this.writable_ = new WritableStream(new DataPipeSink(out_stream));
this.writer_ = this.writable_.getWriter();
if (this.writableResolver_) {
this.writableResolver_();
this.writableResolver_ = undefined;
this.writablePromise_ = undefined;
}
}
async flush(mode) {
switch (mode) {
case SerialPortFlushMode.kReceive:
this.writer_.abort();
this.writer_.releaseLock();
this.writer_ = undefined;
this.writable_ = undefined;
break;
case SerialPortFlushMode.kTransmit:
if (this.reader_) {
this.reader_.cancel();
this.reader_ = undefined;
}
this.readable_ = undefined;
break;
}
}
async drain() {
await this.reader_.closed;
}
async getControlSignals() {
if (this.inputSignalFailure_) {
return {signals: null};
}
const signals = {
dcd: this.inputSignals_.dataCarrierDetect,
cts: this.inputSignals_.clearToSend,
ri: this.inputSignals_.ringIndicator,
dsr: this.inputSignals_.dataSetReady
};
return {signals};
}
async setControlSignals(signals) {
if (this.outputSignalFailure_) {
return {success: false};
}
if (signals.hasDtr) {
this.outputSignals_.dataTerminalReady = signals.dtr;
}
if (signals.hasRts) {
this.outputSignals_.requestToSend = signals.rts;
}
if (signals.hasBrk) {
this.outputSignals_.break = signals.brk;
}
return { success: true };
}
async configurePort(options) {
this.options_ = options;
return { success: true };
}
async getPortInfo() {
return {
bitrate: this.options_.bitrate,
dataBits: this.options_.datBits,
parityBit: this.options_.parityBit,
stopBits: this.options_.stopBits,
ctsFlowControl:
this.options_.hasCtsFlowControl && this.options_.ctsFlowControl,
};
}
async close() {
// OS typically clears DTR on close.
this.outputSignals_.dataTerminalReady = false;
if (this.writer_) {
this.writer_.close();
this.writer_.releaseLock();
this.writer_ = undefined;
}
this.writable_ = undefined;
// Close the receiver asynchronously so the reply to this message can be
// sent first.
const receiver = this.receiver_;
this.receiver_ = undefined;
setTimeout(() => {
receiver.$.close();
}, 0);
return {};
}
}
// Implementation of blink.mojom.SerialService.
class FakeSerialService {
constructor() {
this.interceptor_ =
new MojoInterfaceInterceptor(SerialService.$interfaceName);
this.interceptor_.oninterfacerequest = e => this.bind(e.handle);
this.receiver_ = new SerialServiceReceiver(this);
this.clients_ = [];
this.nextToken_ = 0;
this.reset();
}
start() {
this.interceptor_.start();
}
stop() {
this.interceptor_.stop();
}
reset() {
this.ports_ = new Map();
this.selectedPort_ = null;
}
addPort(info) {
let portInfo = {};
if (info?.usbVendorId !== undefined) {
portInfo.hasUsbVendorId = true;
portInfo.usbVendorId = info.usbVendorId;
}
if (info?.usbProductId !== undefined) {
portInfo.hasUsbProductId = true;
portInfo.usbProductId = info.usbProductId;
}
portInfo.connected = true;
if (info?.connected !== undefined) {
portInfo.connected = info.connected;
}
let token = ++this.nextToken_;
portInfo.token = {high: 0n, low: BigInt(token)};
let record = {
portInfo: portInfo,
fakePort: new FakeSerialPort(),
};
this.ports_.set(token, record);
if (portInfo.connected) {
for (let client of this.clients_) {
client.onPortConnectedStateChanged(portInfo);
}
}
return token;
}
removePort(token) {
let record = this.ports_.get(token);
if (record === undefined) {
return;
}
this.ports_.delete(token);
record.portInfo.connected = false;
for (let client of this.clients_) {
client.onPortConnectedStateChanged(record.portInfo);
}
}
setPortConnectedState(token, connected) {
let record = this.ports_.get(token);
if (record === undefined) {
return;
}
let was_connected = record.portInfo.connected;
if (was_connected === connected) {
return;
}
record.portInfo.connected = connected;
for (let client of this.clients_) {
client.onPortConnectedStateChanged(record.portInfo);
}
}
setSelectedPort(token) {
this.selectedPort_ = this.ports_.get(token);
}
getFakePort(token) {
let record = this.ports_.get(token);
if (record === undefined)
return undefined;
return record.fakePort;
}
bind(handle) {
this.receiver_.$.bindHandle(handle);
}
async setClient(client_remote) {
this.clients_.push(client_remote);
}
async getPorts() {
return {
ports: Array.from(this.ports_, ([token, record]) => record.portInfo)
};
}
async requestPort(filters) {
if (this.selectedPort_)
return { port: this.selectedPort_.portInfo };
else
return { port: null };
}
async openPort(token, options, client) {
let record = this.ports_.get(Number(token.low));
if (record !== undefined) {
return {port: record.fakePort.open(options, client)};
} else {
return {port: null};
}
}
async forgetPort(token) {
let record = this.ports_.get(Number(token.low));
if (record === undefined) {
return {success: false};
}
this.ports_.delete(Number(token.low));
if (record.fakePort.receiver_) {
record.fakePort.receiver_.$.close();
record.fakePort.receiver_ = undefined;
}
return {success: true};
}
}
export const fakeSerialService = new FakeSerialService();