UNPKG

pigpio-client

Version:

A nodejs client for pigpio socket interface.

1,171 lines (1,065 loc) 40.6 kB
/* Construct a pigpio client object that connects with a remote pigpio server (pigpiod) and allows manipulation of its gpio pins. */ const assert = require('assert') const EventEmitter = require('events') class MyEmitter extends EventEmitter {} const util = require('util') const SIF = require('./SIF.js') const API = SIF.APInames const ERR = SIF.PigpioErrors // These commands are currently supported by pigpio-client: const { BR1, BR2, TICK, HWVER, PIGPV, PUD, MODES, MODEG, READ, WRITE, PWM, WVCLR, WVCRE, WVBSY, WVAG, WVCHA, NOIB, NB, NP, NC, SLRO, SLR, SLRC, SLRI, WVTXM, WVTAT, WVHLT, WVDEL, WVAS, HP, HC, GDC, PFS, FG, SERVO, GPW, TRIG, I2CO, I2CC, I2CRD, I2CWD, BSCX, EVM } = SIF.Commands // These command types can not fail, ie, always return p3/res as positive integer const canNeverFailCmdSet = new Set([HWVER, PIGPV, BR1, BR2, TICK]) // These command types have extended command arguments const extReqCmdSet = SIF.extReqCmdSet // These command types have extended response arguments const extResCmdSet = SIF.extResCmdSet /* pigpio constants */ const {PUD_OFF, PUD_DOWN, PUD_UP, PI_WAVE_MODE_ONE_SHOT, PI_WAVE_MODE_REPEAT, PI_WAVE_MODE_ONE_SHOT_SYNC, PI_WAVE_MODE_REPEAT_SYNC} = SIF.Constants var info = { host: 'localhost', port: 8888, pipelining: false, commandSocket: undefined, // connection status undefined until 1st connect notificationSocket: undefined, // connection status undefined until 1st connect pigpioVersion: '', hwVersion: '', hardware_type: 2, // 26 pin plus 8 pin connectors (ie rpi model B) userGpioMask: 0xfbc6cf9c, timeout: 0, // Default is back compatible with v1.0.3. Change to 5 in next ver. version: '1.5.1', } var log = function(...args) { if (/pigpio/i.test(process.env.DEBUG) || process.env.DEBUG === '*') { console.log('pigpio-client ', ...args) } } /*****************************************************************************/ exports.pigpio = function (pi) { var requestQueue = [] var callbackQueue = [] const net = require('net') // update info if (typeof pi === 'undefined') pi = {} info.host = pi.host || info.host info.port = pi.port || info.port info.pipelining = pi.pipelining || info.pipelining info.timeout = (pi.hasOwnProperty('timeout'))? pi.timeout : info.timeout // constructor object inherits from EventEmitter var that = new MyEmitter() // can't use prototypal inheritance // Command socket var commandSocket = initSocket('commandSocket') function initSocket (name) { let socket = new net.Socket() socket.name = name socket.on('connect', connectHandler(socket)) socket.reconnectHandler = returnErrorHandler(socket) socket.disconnectHandler = disconnector(socket) socket.closeHandler = returnCloseHandler(socket) socket.addListener('error', socket.reconnectHandler) socket.addListener('close', socket.closeHandler) return socket } connect(commandSocket) function startRetryTimer(sock) { if (info.timeout) { sock.retryTimer = setTimeout( () => { if (sock.reconnectTimer) { clearTimeout(sock.reconnectTimer) sock.reconnectTimer = null } log(`${sock.name} retry timeout`) // hack: we don't want two error events if (sock.name === 'commandSocket') that.emit('error', new MyError({ api: 'connect', message: 'Could not connect, retry timeout expired.' })) }, info.timeout * 60 * 1000) sock.retryTimer.unref() } } function connect(sock) { startRetryTimer(sock) sock.connect(info.port, info.host) } function stopRetryTimer(sock) { if (sock.retryTimer) { clearTimeout(sock.retryTimer) sock.retryTimer = null } } function connectHandler(sock) { var handler = function() { stopRetryTimer(sock) if (typeof info[sock.name] === 'undefined') { log(`${sock.name} connected`) } else log(`${sock.name} reconnected`) // run the unique portion of connect handlers if (sock.name === 'commandSocket') { commandSocketConnectHandler( () => { info[sock.name] = true // indicates socket is connected if (info.notificationSocket) { log('pigpio-client ready') that.emit('connected', info) } }) } if (sock.name === 'notificationSocket') { notificationSocketConnectHandler( () => { info[sock.name] = true // indicates socket is connected if (info.commandSocket) { that.emit('connected', info) log('pigpio-client ready') } }) } } return handler } function commandSocketConnectHandler(done) { // (re)initialize stuff requestQueue = [] // flush callbackQueue = [] // flush // get pigpio version info then signal 'connected' request(PIGPV, 0, 0, 0, (err, res) => { info.pigpioVersion = res request(HWVER, 0, 0, 0, (err, version) => { info.hwVersion = version if ((version >= 2) && (version <= 3)) { info.hardware_type = 1 info.userGpioMask = 0x3e6cf93 } if ((version > 4) && (version < 15)) { info.hardware_type = 2 info.userGpioMask = 0xfbc6cf9c // default } if (version > 15) { info.hardware_type = 3 info.userGpioMask = 0xffffffc } done() }) }) } function disconnector(sock) { var handler = function(reason) { sock.destroy() if (sock.reconnectTimer) { clearTimeout(sock.reconnectTimer) sock.reconnectTimer = null } if (sock.retryTimer) { clearTimeout(sock.retryTimer) sock.retryTimer = null } if (sock.name === 'notificationSocket') { sock.setTimeout(0) } log(`${sock.name} destroyed due to ${reason}`) info[sock.name] = false // mark socket disconnected // after all sockets are destroyed, alert application if ( (!info.commandSocket && !info.notificationSocket) ) { that.emit('disconnected', reason) log('sent disconnect event to application') } } return handler } commandSocket.on('end', function () { log('pigpio command socket end received') }) function returnErrorHandler(sock) { var handler = function (e) { log(`${sock.name} error code: ${e.code}, message: ${e.message}`) if (sock.pending /*&& sock.connecting)*/) { if (sock.retryTimer) { sock.reconnectTimer = setTimeout( () => { sock.connect(info.port, info.host) }, 5000) sock.reconnectTimer.unref() log(`retry connection on ${sock.name} in 5 sec ...`) // For each error code, inform user of retry timeout activity. if ( !sock.retryEcode && sock.name === 'commandSocket') { sock.retryEcode = e.code console.log(`${e.code}, retrying ${info.host}:${info.port} ...`) } } // Inform user/app that connection could not be established. else if (sock.name === 'commandSocket') { console.log(`Unable to connect to pigpiod. No retry timeout option ` + 'was specified. Verify that daemon is running from ' + `${info.host}:${info.port}.` ) that.emit('error', new MyError(e.message)) } } else if ( !sock.pending) { return sock.disconnectHandler(`${sock.name}: ${e.code}, ${e.message}`) } else { // On any other socket condition, throw that.emit('error', new MyError('Unhandled socket error, '+e.message)) } } return handler } function returnCloseHandler(sock) { var handler = function (had_error) { if (had_error) { // Error handler has already called disconnectHandler as needed. log(`${sock.name} closed on error`) } // If closed without error, must call disconnectHandler from here. else { log(`${sock.name} closed`) if (info[sock.name]) sock.disconnectHandler('closed unexpectedly') } } return handler } var resBuf = Buffer.allocUnsafe(0) // see responseHandler() commandSocket.on('data', commandSocketDataHandler) function commandSocketDataHandler (chunk) { resBuf = Buffer.concat([resBuf, chunk]) if (resBuf.length >= 16) responseHandler() } function responseHandler () { /* Extract response parameter (along with extended params) from response buffer (resBuf), return response as array argument to queued callback function in 'callbackQueue.' p3 contains either error code (if negative) OR response OR length of extended parameters. Decoding cmd tells us if p3 is extended type of command. Partial response is saved to be used in subsequent 'data' callbacks. If response buffer contains more than a single response, the remainder will either be saved or called recursively. */ const resArrBuf = new Uint8Array(resBuf).buffer // creates an Array Buffer copy const cmd = new Uint32Array(resArrBuf, 0, 1) // view of first 4 32bit params var extLen // length of extended response var res = [] var err = null if (canNeverFailCmdSet.has(cmd[0])) { // case p3 is uint32, always 16 length var p3 = new Uint32Array(resArrBuf, 12, 1) extLen = 0 // res[0] = p3[0]; } else { var p3 = new Int32Array(resArrBuf, 12, 1) if (p3[0] > 0) { // is this extended response? if (extResCmdSet.has(cmd[0])) { extLen = p3[0] // p3 is length of extension // is response buffer incomplete? if (resArrBuf.byteLength < (extLen + 16)) { return } // wait for more data else { let uint8Arr = new Uint8Array(resArrBuf, 16, extLen) for (let i = 0; i < extLen; i++) { res[i] = uint8Arr[i] } } } else { // res[0] = p3[0]; // p3 is normal response param extLen = 0 } } else { // p3 is less than (error) or equal (normal) to zero extLen = 0 if (p3[0] < 0) { err = p3[0] // param[3] contains error code (negative) } } } // prepare the error object -> FIXME, create an error subclass? let error = null if (err) { error = new MyError({ name: "pigpioError", code: ERR[err].code, message: ERR[err].message, api: API[cmd[0]] }) //error.code = ERR[err].code //error.message = `${ERR[err].message}, api: ${API[cmd[0]]}` } if (process.env.PIGPIO) { let b = resBuf.slice(0, 16).toJSON().data console.log('response= ', ...b) if (extLen > 0) { let bx = resBuf.slice(16).toJSON().data console.log('extended params= ', ...bx) } } resBuf = resBuf.slice(extLen + 16) // leave remainder for later processing // process the response callback var callback = callbackQueue.shift() // FIXME: test for queue underflow if (typeof callback === 'function') callback(error, p3[0], ...res) else { if (error) { that.emit('error', error) } } // does response buffer contain another response (potentially)? if (resBuf.length >= 16) responseHandler() // recurse // check requestQueue for more requests to send if (requestQueue.length > 0 && (info.pipelining || callbackQueue.length === 0)) { var req = requestQueue.shift() commandSocket.write(req.buffer) callbackQueue.push(req.callback) if (process.env.PIGPIO) { let b = req.buffer.slice(0, 16).toJSON().data// w/o ext params! console.log('deferred request= ', ...b) if (req.buffer.length > 16) { let bx = req.buffer.slice(16).toJSON().data // w/ext console.log('extended params= ', ...bx) } } } } // responseHandler // helper functions var request = (cmd, p1, p2, p3, cb, extArrBuf) => { var bufSize = 16 var buf = Buffer.from(Uint32Array.from([cmd, p1, p2, p3]).buffer) // basic if (extReqCmdSet.has(cmd)) { // following is not true for waveAddSerial! // assert.equal(extArrBuf.byteLength, p3, "incorrect p3 or array length"); bufSize = 16 + extArrBuf.byteLength let extBuf = Buffer.from(extArrBuf) // extension buf = Buffer.concat([buf, extBuf]) } var promise; if (typeof cb !== 'function') { promise = new Promise((resolve, reject) => { cb = (error, ...args) => { if (error) { reject(error) } else { resolve(args.length > 1 ? args : args[0]); } } }) } // Queue request if request queue is not empty OR callback queue is not empty and pipelining disabled if (requestQueue.length > 0 || (callbackQueue.length > 0 && !info.pipelining)) { requestQueue.push({buffer: buf, callback: cb}) } else { commandSocket.write(buf) callbackQueue.push(cb) if (process.env.PIGPIO) { let b = buf.slice(0, 16).toJSON().data // exclude extended params! console.log('request= ', ...b) if (bufSize > 16) { let bx = buf.slice(16).toJSON().data // extended params console.log('extended params= ', ...bx) } } } return promise } // request() // Notifications socket = ToDo: check for notification errors response (res[3]) var handle var chunklet = Buffer.allocUnsafe(0) // notify chunk fragments var oldLevels var notificationSocket = initSocket('notificationSocket') connect(notificationSocket) function notificationSocketConnectHandler(done) { // connect handler here let noib = Buffer.from(new Uint32Array([NOIB, 0, 0, 0]).buffer) notificationSocket.write(noib, () => { // listener once to get handle from NOIB request notificationSocket.once('data', (resBuf) => { let cmd = resBuf.readUIntLE(0, 4) let p1 = resBuf.readUIntLE(4, 4) let p2 = resBuf.readUIntLE(8, 4) let res = resBuf.readIntLE(12, 4) if (cmd!==NOIB || p1!==0 || p2!==0) { res = 255 // set to invalid handle value that.emit('error', new MyError({ message: 'Unexpected response to NOIB command on notification socket', api: 'construct pigpio' })) } else if (res < 0) { that.emit('error', new MyError({ name: "pigpioError", code: ERR[res].code, message: ERR[res].message, api: API[cmd] })) } handle= res log('opened notification socket with handle= ' + handle) // Enable BSC peripheral event monitoring (pigpio built-in event) let arrayBuffer = new Uint8Array(4) let bscEventBits = new Uint32Array(arrayBuffer, 0, 1) bscEventBits[0] = 0x80000000 that.request(EVM, handle, bscEventBits[0], 0, (err) => { if (err) { log('bsc event monitoring FAILED: ' + err) } else log('bsc event monitoring active') }) // Pigpio keep-alive: Wait options.timeout minutes before disconnecting. notificationSocket.setTimeout(info.timeout * 60 * 1000, () => { log('Pigpio keep-alive packet not received before timeout expired') // generate an (custom) error exception on the socket(s) notificationSocket.disconnectHandler('pigpio keep-alive timeout') commandSocket.disconnectHandler('pigpio keep-alive timeout') }) // listener that monitors all gpio bits notificationSocket.on('data', notificationSocketDataHandler) done() // connect handler completed callback }) }) } function notificationSocketDataHandler (chunk) { if (process.env.PIGPIO) { console.log(`notification received: chunk size = ${chunk.length}`) } var buf = Buffer.concat([chunklet, chunk]) let remainder = buf.length % 12 chunklet = buf.slice(buf.length-remainder) // skip if buf is a fragment if (buf.length / 12 > 0) { // process notifications, issue callbacks to registerd notifier if bits have changed for (let i = 0; i < buf.length - remainder; i += 12) { let seqno = buf.readUInt16LE(i + 0), flags = buf.readUInt16LE(i + 2), tick = buf.readUInt32LE(i + 4), levels = buf.readUInt32LE(i + 8) if (flags & 0x80 && (flags & 0x1F) === 31) { that.emit('EVENT_BSC') } let changes = oldLevels ^ levels oldLevels = levels for (let nob of notifiers.keys()) { if (nob.bits & changes) { nob.func(levels, tick) } } } } } notificationSocket.on('end', function () { log('pigpio notification socket end received') }) /** * Public Methods ***/ that.request = request that.connect = function() { if (commandSocket.destroy) { log('Remove all listeners and destroy command socket.') commandSocket.removeAllListeners() commandSocket.destroy() } commandSocket = initSocket('commandSocket') commandSocket.addListener('data', commandSocketDataHandler) connect(commandSocket) if (notificationSocket.destroy) { log('Remove all listeners and destroy notification socket.') notificationSocket.removeAllListeners() notificationSocket.destroy() } notificationSocket = initSocket('notificationSocket') notificationSocket.addListener('data', notificationSocketDataHandler) connect(notificationSocket) } // Notifications // Must **always** use 'request()' to configure/control pigpio. Ie, don't to this: // commandSocket.write(...); // will screw up request callbackQueue!!! const MAX_NOTIFICATIONS = 32 var nID = 0 var notifiers = new Set() var monitorBits = 0 that.startNotifications = function (bits, cb) { if (notifiers.size === MAX_NOTIFICATIONS) { let error = new MyError('Notification limit reached, cannot add this notifier') error.code = 'PI_CLIENT_NOTIFICATION_LIMIT' that.emit('error', error) return null } // Registers callbacks for this gpio var nob = { id: nID++, func: cb, bits: +bits } notifiers.add(nob) // If not currently monitoring, update the current levels (oldLevels) if (monitorBits === 0) { request(BR1, 0, 0, 0, (err, levels) => { if (err) { //that.emit('error', new Error('pigpio: ', ERR[err].message)) let error = new MyError('internal pigpio error: ' +ERR[err].message) error.code = ERR[err].code that.emit('error', error) } oldLevels = levels }) } // Update monitor with the new bits to monitor monitorBits |= bits // start monitoring new bits request(NB, handle, monitorBits, 0) // return the callback 'id' return nob.id } that.pauseNotifications = function (cb) { // Caution: This will pause **all** notifications! return request(NP, handle, 0, 0, cb) } that.stopNotifications = function (id, cb) { // Clear monitored bits and unregister callback var result for (let nob of notifiers.keys()) { if (nob.id === id) { monitorBits &= ~nob.bits // clear gpio bit in monitorBits // Stop the notifications on pigpio hardware result = request(NB, handle, monitorBits, 0, (err, res) => { // last callback with null arguments nob.func(null, null) notifiers.delete(nob) cb(err, res) }) } } return result } that.closeNotifications = function (cb) { // Caution: This will close **all** notifications! return request(NC, handle, 0, 0, cb) } var isUserGpio = function (gpio) { return !!(((1 << gpio) & info.userGpioMask)) } that.getInfo = function () { return (info) } that.getHandle = function () { return handle } that.getCurrentTick = function (cb) { return that.request(TICK, 0, 0, 0, cb) } that.readBank1 = function (cb) { return that.request(BR1, 0, 0, 0, cb) } that.hwClock = function (gpio, freq, cb) { return that.request(HC, gpio, freq, 0, cb) } that.destroy = function () { // Should only be called if an error occurs on socket commandSocket.destroy() notificationSocket.destroy() } that.end = function (cb) { commandSocket.end() // calls disconnectHandler, destroys connection. notificationSocket.end() // calls disconnectHandler, destroys connection. that.once('disconnected', () => { if (typeof cb === 'function') cb() }) } that.i2cOpen = function (bus, device, callback) { let flags = new Uint8Array(4); // inits to zero return request(I2CO, bus, device, 4, callback, flags) } that.i2cClose = function (handle, callback) { return request(I2CC, handle, 0, 0, callback) } that.i2cReadDevice = function (handle, count, callback) { return request(I2CRD, handle, count, 0, callback) } that.i2cWriteDevice = function (handle, data, callback) { let buffer = Buffer.from(data); return request(I2CWD, handle, 0, data.length, callback, buffer) } that.bscI2C = function (address, data, callback) { let control if (address > 0 && address < 128) { control = (address<<16)|0x305 } else { control = 0 } let buffer if (data && typeof data !== 'function') { buffer = Buffer.from(data) return request(BSCX, control, 0, data.length, callback, buffer) } if (typeof data === 'undefined') { buffer = Buffer.from([]); return request(BSCX, control, 0, 0, callback, buffer) } throw new MyError({api: 'bscI2C', message: 'Bad argument'}) } /* ___________________________________________________________________________ */ that.gpio = function (gpio) { var _gpio = function (gpio) { assert(typeof gpio === 'number' && isUserGpio(gpio), "Argument 'gpio' is not a user GPIO.") var modeSet = function (gpio, mode, callback) { assert(typeof mode === 'string', "Argument 'mode' must be string.") let m = /^outp?u?t?/.test(mode) ? 1 : /^inp?u?t?/.test(mode) ? 0 : undefined assert(m !== undefined, "Argument 'mode' is not a valid string.") return request(MODES, gpio, m, 0, callback) } var pullUpDown = function (gpio, pud, callback) { assert(typeof pud === 'number', "Argument 'pud' is not a number.") // Rely on pigpio library to range check pud argument. return request(PUD, gpio, pud, 0, callback) } // basic methods this.modeSet = function (...args) { modeSet(gpio, ...args) } this.pullUpDown = function (...args) { pullUpDown(gpio, ...args) } this.write = function (level, callback) { assert(typeof level === 'number' && (level === 0 || level === 1), "Argument 'level' must be numeric 0 or 1") //if ((+level >= 0) && (+level <= 1)) { return request(WRITE, gpio, +level, 0, callback) //} else throw new MyError('gpio.write level argument must be numeric 0 or 1') } this.read = function (callback) { return request(READ, gpio, 0, 0, callback) } this.modeGet = function (callback) { return request(MODEG, gpio, 0, 0, callback) } this.trigger = function (len, level, callback) { const buf = Buffer.allocUnsafe(4); buf.writeUInt32LE(level); return request(TRIG, gpio, len, 4, callback, buf); } // PWM this.analogWrite = function (dutyCycle, cb) { return request(PWM, gpio, dutyCycle, 0, cb) } // Notification methods var notifierID = null this.notify = function (callback) { // only allow one notifier per gpio object if (notifierID !== null) { that.emit('error', new MyError('Notifier already registered for this gpio.')) return } let gpioBitValue = 1 << gpio notifierID = that.startNotifications(gpioBitValue, (levels, tick) => { // When notifications are ended, last callback has null arguments if (levels === null) { return callback(null, null) } let level = (gpioBitValue & levels) >> gpio callback(level, tick) }) } this.endNotify = function (cb) { if (notifierID !== null) { that.stopNotifications(notifierID, (err, res) => { notifierID = null if (cb && typeof cb === 'function') cb(err, res) else if (err) that.emit('error', new MyError(err)) }) } } // glitch this.glitchSet = function (steady, callback) { assert(typeof steady === 'number' && steady >= 0 && steady <= 300000, "Argument 'steady' must be a numeric bewtween 0 or 300000") return request(FG, gpio, steady, 0, callback) } // Waveform generation methods this.waveClear = function (callback) { return request(WVCLR, 0, 0, 0, callback) } this.waveCreate = function (callback) { return request(WVCRE, 0, 0, 0, callback) } this.waveBusy = function (callback) { return request(WVBSY, 0, 0, 0, callback) } this.waveNotBusy = function (time, cb) { let timer, callback if (typeof time !== 'number') { timer = 25 callback = time } else { timer = time callback = cb } var promise = new Promise(function(resolve) { let originalCallback = callback callback = function() { if (typeof originalCallback === 'function') { originalCallback() } resolve() } }) var waitWaveBusy = (done) => { setTimeout(() => { request(WVBSY, 0, 0, 0, (err, busy) => { if (!busy) done() else waitWaveBusy(done) }) }, timer) } waitWaveBusy(callback) return promise } this.waveAddPulse = function (tripletArr, callback) { // test triplets is an array of arrays tripletArr.forEach(function (triplet) { assert.equal((Object.prototype.toString.apply(triplet)), '[object Array]', 'tripletArr not an array') assert.equal(triplet.length, 3, 'triplet array length is not 3') }) // use Typed Arrays var arrBuf = new ArrayBuffer(tripletArr.length * 3 * 4) // items are 3 x 32-bit values var uint32Triplet = new Uint32Array(arrBuf, 0, tripletArr.length * 3) // 32-bit view of buffer let i = 0 tripletArr.forEach(function (triplet) { uint32Triplet[i + 0] = triplet[0] << gpio // 'set' gpio (bit value) uint32Triplet[i + 1] = triplet[1] << gpio // 'clear' gpio (bit value) uint32Triplet[i + 2] = triplet[2] i = i + 3 }) // ship it return request(WVAG, 0, 0, arrBuf.byteLength, callback, arrBuf) } this.waveChainTx = function (paramArray, callback) { // Todo: assert paramArray elements are single property objects var chain = [] paramArray.forEach((param) => { let temp if (param.hasOwnProperty('loop')) { temp = chain.concat(255, 0) } else if (param.hasOwnProperty('repeat')) { if (param.repeat === true) { temp = chain.concat(255, 3) } else { assert.equal(param.repeat <= 0xffff, true, 'param must be <= 65535') temp = chain.concat(255, 1, param.repeat & 0xff, param.repeat >> 8) } } else if (param.hasOwnProperty('delay')) { assert.equal(param.delay <= 0xffff, true, 'param must be <= 65535') temp = chain.concat(255, 2, param.delay & 0xff, param.delay >> 8) } else if (param.hasOwnProperty('waves')) { param.waves.forEach((wid) => { assert.equal(wid <= 250, true, 'wid must be <= 250') }) temp = chain.concat(param.waves) } chain = temp temp = [] }) var arrBuf = new ArrayBuffer(chain.length) var buffer = new Uint8Array(arrBuf) for (let i = 0; i < chain.length; i++) buffer[i] = chain[i] return request(WVCHA, 0, 0, arrBuf.byteLength, callback, arrBuf) } this.waveTxStop = function (cb) { return request(WVHLT, 0, 0, 0, cb) } this.waveSendSync = function (wid, cb) { return request(WVTXM, wid, PI_WAVE_MODE_ONE_SHOT_SYNC, 0, cb) } this.waveSendOnce = function (wid, cb) { return request(WVTXM, wid, PI_WAVE_MODE_ONE_SHOT, 0, cb) } this.waveTxAt = function (cb) { return request(WVTAT, 0, 0, 0, cb) } this.waveDelete = function (wid, cb) { return request(WVDEL, wid, 0, 0, cb) } // Pulse Width Modulation this.setPWMdutyCycle = function (dutyCycle, cb) { // alias of analogWrite return request(PWM, gpio, dutyCycle, 0, cb) } this.setPWMfrequency = function (freq, cb) { return request(PFS, gpio, freq, 0, cb) } this.getPWMdutyCycle = function (cb) { return request(GDC, gpio, 0, 0, cb) } this.hardwarePWM = function (frequency, dutyCycle, callback) { var arrBuf = new ArrayBuffer(4) var dcBuf = new Uint32Array(arrBuf, 0, 1) dcBuf[0] = dutyCycle return request(HP, gpio, frequency, 4, callback, arrBuf) } // Servo pulse width this.setServoPulsewidth = function (pulseWidth, cb) { return request(SERVO, gpio, pulseWidth, 0, cb) } this.getServoPulsewidth = function (cb) { return request(GPW, gpio, 0, 0, cb) } // Bit-Bang Serial IO this.serialReadOpen = function (baudRate, dataBits, callback) { var arrBuf = new ArrayBuffer(4) var dataBitsBuf = new Uint32Array(arrBuf, 0, 1) dataBitsBuf[0] = dataBits return request(SLRO, gpio, baudRate, 4, callback, arrBuf) } this.serialRead = function (count, callback) { return request(SLR, gpio, count, 0, callback) } this.serialReadClose = function (callback) { return request(SLRC, gpio, 0, 0, callback) } this.serialReadInvert = function (mode, callback) { var flag if (mode === 'invert') flag = 1 if (mode === 'normal') flag = 0 assert(typeof flag !== 'undefined', "Argument 'mode' is invalid.") return request(SLRI, gpio, flag, 0, callback) } this.waveAddSerial = function (baud, bits, delay, data, callback) { let dataBuf = Buffer.from(data) let paramBuf = Buffer.from(Uint32Array.from([bits, 2, delay]).buffer) let buf = Buffer.concat([paramBuf, dataBuf]) // request take array buffer (this conversion from ZachB on SO) // let arrBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); return request(WVAS, gpio, baud, buf.length, callback, buf) } }// var gpio _gpio.prototype = that // inheritance return new _gpio(gpio) }// that.gpio constructor /* * Serial Port Constructor * * Return a serialport object using specified pins. Frame format is 1-32 databits, * no parity and 1 stop bit. Baud rates from 50-250000 are allowed. * Usage: Application must poll for read data to prevent data loss. Read method * uses callback. (Desire to make this readable.read() like) * Todo: - make rts/cts, dsr/dtr more general purpose. * - implement duplex stream api */ that.serialport = function (rx, tx, dtr) { var _serialport = function (rx, tx, dtr) { if (dtr) assert(isUserGpio(rx) && isUserGpio(tx) && isUserGpio(dtr), "Arguments 'rx', 'tx', and 'dtr' must be valid user GPIO") else assert(isUserGpio(rx) && isUserGpio(tx), "Arguments 'rx' and 'tx' must be valid user GPIO") var baud, bits, isOpen=false, txBusy=false, maxChars, buffer='' var _rx, _tx, _dtr _rx = new that.gpio(rx) if (tx === rx) { // loopback mode _tx = _rx } else _tx = new that.gpio(tx) if (dtr) _dtr = (dtr === tx) ? _tx : new that.gpio(dtr) _rx.modeSet('input') // need a pullup? _tx.modeSet('output') _tx.write(1) if (dtr) { _dtr.modeSet('output') _dtr.write(1) } this.open = function (baudrate, databits, cb) { if (cb) assert(typeof cb === 'function', "argument 'cb' must be a function") baud = baudrate || 9600 assert(typeof baud === 'number', "argument 'baud' must be a number") assert(!isNaN(baud) && baud > 49 && baud < 250001, "argument 'baud' must be a positive number between 50 and 250000") bits = databits || 8 assert(typeof bits === 'number', "argument 'dataBits' must be a number") assert(!isNaN(bits) && bits > 0 && bits < 33, "argument 'dataBits' must be a positive number between 1 and 32") // initialize rx _rx.serialReadOpen(baud, bits, (err) => { if (err && err.code === 'PI_GPIO_IN_USE') { log("PI_GPIO_IN_USE, try close then re-open") _rx.serialReadClose((err) => { if (err) { log("something is wrong on retry open serial port") isOpen = false if (cb) cb(createSPError(err), false) else that.emit(createSPError(err)) } else _rx.serialReadOpen(baud, bits, (err) => { log("retrying open, abort on error") if (err) throw(createSPError(err)) log("retry success") isOpen = true if (dtr && dtr !== tx) { // pulse dtr pin to reset Arduino _dtr.write(0, () => { setTimeout(() => { _dtr.write(1) }, 10) }) } if (cb) cb(null, true) }) }) } else if (err) { // unexpected error on open isOpen = false if (cb) cb(createSPError(err), false) else that.emit(createSPError(err)) } else { // normal success isOpen = true if (dtr && dtr !== tx) { // pulse dtr pin to reset Arduino _dtr.write(0, () => { setTimeout(() => { _dtr.write(1) }, 10) }) } if (cb) cb(null, true) } }) // initialize tx _tx.waveClear((err) => { if (err) throw(createSPError(err)) that.request(35, 2, 0, 0, (err, maxPulses) => { maxChars = maxPulses / (bits + 2) log('maxChars = ', maxChars) }) }) } /* * Read from serialport. Arguments: * * size A number representing the number of bytes to read. Size is optional. * If not specified, all the data in the buffer is returned (<=8192). * * cb On success, invoked as cb(null, data) where 'data' is a string. * On failure, invoked as cb(err) where 'err' is a PigpioError * object. */ this.read = function (size, cb) { let count, callb if (typeof size === 'function') { callb = size count = 8192 } else { callb = cb count = size || 8192 // must read at least a byte at a time } if (isOpen) { _rx.serialRead(count, (err, len, ...bytes) => { if (err) { callb(createSPError(err)) } else if (len === 0) { callb(null, null) } else { let buf = Buffer.from(bytes) callb(null, ""+buf) // coerce to string } }) } else callb(null) } this.write = function (data) { /* Saves data, coerced to utf8 string, to a buffer then sends chunks of * of size 'maxChars' to waveAddSerial(). Returns the size (>=0) of buffer. * If the serial port is not open, returns -1. * Pigpio errors will be thrown to limit possible data corruption. */ if (isOpen === false) return -1 buffer += data // fast concatenation with coercion to string type if (txBusy) return buffer.length let chunk = buffer.slice(0, maxChars) // computed in serialport.open() buffer = buffer.slice(chunk.length) txBusy = true if (chunk) send(chunk) return buffer.length function send(data) { log('serialport sending data ', data.length) _tx.waveAddSerial(baud, bits, 0, data, (err) => { if (err) throw(createSPError(err)) _tx.waveCreate( (err, wid)=> { if (err) throw(createSPError(err)) _tx.waveSendOnce(wid, (err) => { if (err) throw(createSPError(err)) setTimeout(() => { _tx.waveNotBusy(1, () => { _tx.waveDelete(wid, (err) => { if (err) throw(createSPError(err)) if (buffer) { chunk = buffer.slice(0, maxChars) buffer = buffer.slice(chunk.length) send(chunk) } else txBusy = false }) }) }, Math.ceil((data.length+1) * 10 * 1000 / baud)) }) }) }) } } this.close = function (callback) { if (isOpen) { isOpen = false _rx.serialReadClose((err) => { if(err && err.code === 'PI_NOT_SERIAL_GPIO') log('Serial read is already closed: '+err.message) else if (err) if (callback && typeof callback === 'function') callback(createSPError(err)) else that.emit(createSPError(err)) }) } if (typeof callback === 'function') callback(null, 0) } this.end = function (callback) { if (callback) assert(typeof callback === 'function', "Argument 'cb' must be a function") this.close((err) => { if (err) if (callback) callback(createSPError(err)) else that.emit(createSPError(err)) _tx.modeSet('in', (err) => { if (err) if (callback) callback(createSPError(err)) else that.emit(createSPError(err)) if (dtr) _dtr.modeSet('input', (err) => { if (err) if (callback) callback(createSPError(err)) else that.emit(createSPError(err)) // success, finally! if (callback) callback() }) else if (callback) callback() }) }) } }// _serialport() function createSPError(err) { return new MyError( { name: 'pigpioClientError', api: 'serialport', code: (typeof err === 'string')? 'PI_CLIENT' : err.code, message: (typeof err === 'string')? err : err.message }) } _serialport.prototype = that return new _serialport(rx, tx, dtr) }// pigpio serialport constructor return that }// pigpio constructor function MyError(settings, context) { settings = settings || {} if (typeof settings === 'string') settings = {message: settings} this.name = settings.name || "pigpioClientError" this.code = settings.code || "PI_CLIENT" this.message = settings.message || "An error occurred" this.api = settings.api || "" Error.captureStackTrace(this, context || MyError) } util.inherits(MyError, Error)