const debug = require('debug')('serialport:bindings:mock');
const Buffer = require('safe-buffer').Buffer;
const BaseBinding = require('./base');
let ports = {};
let serialNumber = 0;
function resolveNextTick(value) {
return new Promise(resolve => process.nextTick(() => resolve(value)));
}
/**
* Mock bindings for pretend serialport access
*/
class MockBinding extends BaseBinding {
constructor(opt) {
super(opt);
this.pendingRead = null; // thunk for a promise or null
this.isOpen = false;
this.port = null;
this.lastWrite = null;
this.recording = Buffer.alloc(0);
this.writeOperation = null; // in flight promise or null
}
// Reset mocks
static reset() {
ports = {};
}
// Create a mock port
static createPort(path, opt) {
serialNumber++;
opt = Object.assign({
echo: false,
record: false,
readyData: Buffer.from('READY')
}, opt);
ports[path] = {
data: Buffer.alloc(0),
echo: opt.echo,
record: opt.record,
readyData: Buffer.from(opt.readyData),
info: {
comName: path,
manufacturer: 'The J5 Robotics Company',
serialNumber,
pnpId: undefined,
locationId: undefined,
vendorId: undefined,
productId: undefined
}
};
debug(serialNumber, 'created port', JSON.stringify({ path, opt }));
}
static list() {
const info = Object.keys(ports).map((path) => {
return ports[path].info;
});
return Promise.resolve(info);
}
// Emit data on a mock port
emitData(data) {
if (!this.isOpen) {
throw new Error('Port must be open to pretend to receive data');
}
if (!Buffer.isBuffer(data)) {
data = Buffer.from(data);
}
debug(this.serialNumber, 'emitting data - pending read:', Boolean(this.pendingRead));
this.port.data = Buffer.concat([this.port.data, data]);
if (this.pendingRead) {
process.nextTick(this.pendingRead);
this.pendingRead = null;
}
}
open(path, opt) {
debug(null, `opening path ${path}`);
const port = this.port = ports[path];
return super.open(path, opt)
.then(resolveNextTick)
.then(() => {
if (!port) {
return Promise.reject(new Error(`Port does not exist - please call MockBinding.createPort('${path}') first`));
}
this.serialNumber = port.info.serialNumber;
if (port.openOpt && port.openOpt.lock) {
return Promise.reject(new Error('Port is locked cannot open'));
}
if (this.isOpen) {
return Promise.reject(new Error('Open: binding is already open'));
}
port.openOpt = Object.assign({}, opt);
this.isOpen = true;
debug(this.serialNumber, 'port is open');
if (port.echo) {
process.nextTick(() => {
if (this.isOpen) {
debug(this.serialNumber, 'emitting ready data');
this.emitData(port.readyData);
}
});
}
});
}
close() {
const port = this.port;
debug(this.serialNumber, 'closing port');
if (!port) {
return Promise.reject(new Error('already closed'));
}
return super.close()
.then(() => {
delete port.openOpt;
// reset data on close
port.data = Buffer.alloc(0);
debug(this.serialNumber, 'port is closed');
delete this.port;
delete this.serialNumber;
this.isOpen = false;
if (this.pendingRead) {
this.pendingRead(new Error('port is closed'));
}
});
}
read(buffer, offset, length) {
debug(this.serialNumber, 'reading', length, 'bytes');
return super.read(buffer, offset, length)
.then(resolveNextTick)
.then(() => {
if (!this.isOpen) {
throw new Error('Read canceled');
}
if (this.port.data.length <= 0) {
return new Promise((resolve, reject) => {
this.pendingRead = (err) => {
if (err) { return reject(err) }
this.read(buffer, offset, length).then(resolve, reject);
};
});
}
const data = this.port.data.slice(0, length);
const readLength = data.copy(buffer, offset);
this.port.data = this.port.data.slice(length);
debug(this.serialNumber, 'read', readLength, 'bytes');
return readLength;
});
}
write(buffer) {
debug(this.serialNumber, 'writing');
if (this.writeOperation) {
throw new Error('Overlapping writes are not supported and should be queued by the serialport object');
}
this.writeOperation = super.write(buffer)
.then(resolveNextTick)
.then(() => {
if (!this.isOpen) {
throw new Error('Write canceled');
}
const data = this.lastWrite = Buffer.from(buffer); // copy
if (this.port.record) {
this.recording = Buffer.concat([this.recording, data]);
}
if (this.port.echo) {
process.nextTick(() => {
if (this.isOpen) { this.emitData(data) }
});
}
this.writeOperation = null;
debug(this.serialNumber, 'writing finished');
});
return this.writeOperation;
}
update(opt) {
return super.update(opt)
.then(resolveNextTick)
.then(() => {
this.port.openOpt.baudRate = opt.baudRate;
});
}
set(opt) {
return super.set(opt)
.then(resolveNextTick);
}
get() {
return super.get()
.then(resolveNextTick)
.then(() => {
return {
cts: true,
dsr: false,
dcd: false
};
});
}
getBaudRate() {
return super.getBaudRate()
.then(resolveNextTick)
.then(() => {
return {
baudRate: this.port.openOpt.baudRate
};
});
}
flush() {
return super.flush()
.then(resolveNextTick)
.then(() => {
this.port.data = Buffer.alloc(0);
});
}
drain() {
return super.drain()
.then(() => this.writeOperation)
.then(() => resolveNextTick());
}
}
module.exports = MockBinding;