234 lines
5.7 KiB
JavaScript
234 lines
5.7 KiB
JavaScript
const AbstractBinding = require('@serialport/binding-abstract')
|
|
const debug = require('debug')('serialport/binding-mock')
|
|
const { wrapWithHiddenComName } = require('./legacy')
|
|
|
|
let ports = {}
|
|
let serialNumber = 0
|
|
|
|
function resolveNextTick(value) {
|
|
return new Promise(resolve => process.nextTick(() => resolve(value)))
|
|
}
|
|
|
|
const cancelError = message => {
|
|
const err = new Error(message)
|
|
err.canceled = true
|
|
return err
|
|
}
|
|
|
|
/**
|
|
* Mock bindings for pretend serialport access
|
|
*/
|
|
class MockBinding extends AbstractBinding {
|
|
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 = {}
|
|
serialNumber = 0
|
|
}
|
|
|
|
// Create a mock port
|
|
static createPort(path, opt) {
|
|
serialNumber++
|
|
opt = {
|
|
echo: false,
|
|
record: false,
|
|
readyData: Buffer.from('READY'),
|
|
manufacturer: 'The J5 Robotics Company',
|
|
vendorId: undefined,
|
|
productId: undefined,
|
|
...opt,
|
|
}
|
|
|
|
ports[path] = {
|
|
data: Buffer.alloc(0),
|
|
echo: opt.echo,
|
|
record: opt.record,
|
|
readyData: Buffer.from(opt.readyData),
|
|
info: {
|
|
path,
|
|
manufacturer: opt.manufacturer,
|
|
serialNumber,
|
|
pnpId: undefined,
|
|
locationId: undefined,
|
|
vendorId: opt.vendorId,
|
|
productId: opt.productId,
|
|
},
|
|
}
|
|
debug(serialNumber, 'created port', JSON.stringify({ path, opt }))
|
|
}
|
|
|
|
static async list() {
|
|
return wrapWithHiddenComName(Object.values(ports).map(port => port.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
|
|
}
|
|
}
|
|
|
|
async open(path, opt) {
|
|
debug(null, `opening path ${path}`)
|
|
const port = (this.port = ports[path])
|
|
await super.open(path, opt)
|
|
await resolveNextTick()
|
|
if (!port) {
|
|
throw new Error(`Port does not exist - please call MockBinding.createPort('${path}') first`)
|
|
}
|
|
this.serialNumber = port.info.serialNumber
|
|
|
|
if (port.openOpt && port.openOpt.lock) {
|
|
throw new Error('Port is locked cannot open')
|
|
}
|
|
|
|
if (this.isOpen) {
|
|
throw new Error('Open: binding is already open')
|
|
}
|
|
|
|
port.openOpt = { ...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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
async close() {
|
|
const port = this.port
|
|
debug(this.serialNumber, 'closing port')
|
|
if (!port) {
|
|
throw new Error('already closed')
|
|
}
|
|
|
|
await super.close()
|
|
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(cancelError('port is closed'))
|
|
}
|
|
}
|
|
|
|
async read(buffer, offset, length) {
|
|
debug(this.serialNumber, 'reading', length, 'bytes')
|
|
await super.read(buffer, offset, length)
|
|
await resolveNextTick()
|
|
if (!this.isOpen) {
|
|
throw cancelError('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 bytesRead = data.copy(buffer, offset)
|
|
this.port.data = this.port.data.slice(length)
|
|
debug(this.serialNumber, 'read', bytesRead, 'bytes')
|
|
return { bytesRead, buffer }
|
|
}
|
|
|
|
async 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(async () => {
|
|
await resolveNextTick()
|
|
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
|
|
}
|
|
|
|
async update(opt) {
|
|
await super.update(opt)
|
|
await resolveNextTick()
|
|
this.port.openOpt.baudRate = opt.baudRate
|
|
}
|
|
|
|
async set(opt) {
|
|
await super.set(opt)
|
|
await resolveNextTick()
|
|
}
|
|
|
|
async get() {
|
|
await super.get()
|
|
await resolveNextTick()
|
|
return {
|
|
cts: true,
|
|
dsr: false,
|
|
dcd: false,
|
|
}
|
|
}
|
|
|
|
async getBaudRate() {
|
|
await super.getBaudRate()
|
|
await resolveNextTick()
|
|
return {
|
|
baudRate: this.port.openOpt.baudRate,
|
|
}
|
|
}
|
|
|
|
async flush() {
|
|
await super.flush()
|
|
await resolveNextTick()
|
|
this.port.data = Buffer.alloc(0)
|
|
}
|
|
|
|
async drain() {
|
|
await super.drain()
|
|
await this.writeOperation
|
|
await resolveNextTick()
|
|
}
|
|
}
|
|
|
|
module.exports = MockBinding
|