Please note that this is still a beta version
Promise based AT(tention) command handler for serial ports (typically for use with external modem components and the like).
This module is ment to serve only as a basis for your specific device implementations - it is rather device agnostic, so to speak.
For a sample (beta) implementation of a real device see telit-modem
Features:
- Send simple commands and receive boolean success/failure responses
- Catch complex responses and preprocess
- Set up notifications / event handlers for unsolicited messages
- Command queue
This module uses the npm https://www.npmjs.com/package/serialport for serial communication.
- Complete documentation..
- Add more serialport configuration options (modem options are passthrough anyway, so just note that in the documentation...)
- Add tests
- Generic refactoring
- Rethink timeout principle - is it ok like this or should it be remodelled? (timeout not absolute to actual command start but relative to last incoming data) (-> process.nextTick ??)
In case something doesn't work as expected, please first look here.
- After an inbuffer handling change (auto-discard of CR/NL prefixes) reading a specific number of bytes might have an unexpected behaviour. Well, changing this ment a simplification of usage but also a change in semantics as incoming data is being interpreted.
- Usage
- Classes
- Modem
- Modem(options)
- getConfig()
- setConfig(options)
- open(path)
- isOpen()
- pause()
- close(callback)
- closeGracefully(callback)
- on(event, callback)
- isProcessingCommands()
- startProcessing()
- stopProcessing(abortCurrent, callback)
- getPendingCommands()
- clearPendingCommands()
- getCurrentCommand()
- abortCurrentCommand()
- run(command, expected, options)
- addCommand(command, expected, options)
- read(n)
- write(buf)
- getInBuffer()
- clearInBuffer()
- getNotifications()
- clearNotifications()
- addNotification(name, regex, handler)
- removeNotification(name)
- Command
- Notification
- Modem
- Events
var ATCommander = require('at-commander');
var Command = ATCommander.Command;
// all options are optional, these are the default options
var opts = {
// the following options define the options used by serialport
parser: serialport.parsers.raw,
baudRate: 115200,
dataBits: 8,
stopBits: 1,
// command termination string (is added to every normal string type command)
EOL: "\r\n",
// this regex is used by default to detect one-line responses
lineRegex: /^\r\n(.+)\r\n/,
// (default) command timeout
timeout: 500
};
var modem = new ATCommander.Modem(opts);
var port = 'COM4'; // on Windows
var port = '/tty/serial/by-id/blabalbla'; // linux based machines
modem.open(port).catch((err) => {
console.log("Failed to open serial", err);
}).then(function(){
// check if a response is coming
// NOTE: run(command) bypasses the command queue and is executed immediatly (unless another command is being executed already)
modem.run('AT').then((success) => {
modem.startProcessing();
});
// fill up command queue
// queue is only processed it modem.startProcessing() is called.
modem.addCommand('AT+CMG=1');
// identical to previous command
modem.addCommand('AT+CMG=1', undefined);
// with expected result 'OK' and command specific timeout
modem.addCommand('AT+FOOO', 'OK', {
timeout: 10000
}).then(function(){
// command got expected response
}).catch(function(command){
// some error occurred
});
// consider the next incoming 6 bytes as the wanted response
modem.addCommand('AT+FOOO', 6).then(function(buffer){
// buffer contains the next 6 incoming bytes (please note, that beginning CR + NL characters are trimmed automatically, thus (at the moment) if you expect to be reading only these characters your logic will fail)
}).catch(function(command){
// most likely to fail only if there is a timeout
});
modem.addCommand('AT+CREG=?', /\+CREG=(.*),(.*)/).then((matches) => {
// matches contains the response's string matches according to the given regex
});
modem.addCommand('AT+FOOO', function(buffer){
// complex response detectors are passed the updated response buffer contents whenever there is new data arriving
var str = buffer.toString();
if (str.matches(/^OK/r/n/){
return 4; // return the byte count the response (these many bytes will be consumed from the buffer)
}
return 0; // return 0 if expected response not received yet
}).then((buffer) => {
// complex response detectors receive the whole (consumed) buffer as argument
});
// add a notification
modem.addNotification('myEventName', /^+CMI=(.*),(.*)/, function(buffer, matches) {
modem.addCommand("AT+CMR="+matches[1], parseInt(matches[2])).then((buf) => {
// buf contains my wanted result
});
});
modem.addNotification('shutdown', /SHUTDOWN/, function(){
modem.close();
});
});
The Modem
methods run
, addCommand
return a promise that will be resolved/rejected with variable parameters that depend on the (Command)[#command] options.
The following setup illustrates the differences
var CommandStates = require('at-commander').CommandStates;
// please note, it is also possible to call modem.run directly with the arguments as passed to the constructor of command
// modem.run thus is just a nice wrapper
var myCommand = new ATCommander.Command(cmd, expected);
modem.run(myCommand).then(function(result){
if (typeof expected === 'undefined' || typeof expected === 'string'){
// result is a boolean denoting wether the one-line response matched the expected value
// in case expected was undefined, the default response (OK) is assumed
// NOTE this will have to be refactored to make it configurable on the fly
}
if (typeof expected === 'number'){
// result will be of type Buffer container the number of bytes as denoted by expected
}
if (expected instanceof RegExp){
// result will be the return value of inBufferString.match(expected)
}
if (typeof expected === 'function'){
// result will be the relevant inBuffer part that was detected using expected
}
}).catch(function(command){
// in case of an error, the given object is an instance of Command
// command is the same object as myCommand
// furthermore several fields will be set:
switch (command.state){
case CommandStates.Init:
//this state should never occur in an error case
break;
case CommandStates.Rejected:
// this state only occurs when passing a command using .run() (or write(), read())
// and denotes the situation where the modem is already processing a command
// (this is because .run() bypasses the command queue)
break;
case CommandStates.Running:
// this state should never occur in an error/catch case
// it denotes that the command is being processed by the modem
break;
case CommandStates.Finished:
// this state should never occur in an error/catch case
// it denotes that the command terminated as configured
// command.result.buf -> read buffer that satisfied the expected result requirements
break;
case CommandStates.Failed:
// this state occurs if the commands result processor function returns an undefined value
// by default this will also be the case if the expected result is a string type and the read in line
// did not match (thus causing a rejection)
// note that if you provide result processor functions yourself, you might want to be aware of this (or
// make use of it)
// command.result.buf -> read line that did not match
break;
case CommandStates.Timeout:
// this state denotes that there was no reply from the attached serial device in the given time constraint
// also the contents of the inBuffer will be passed to the command (and consumed from the inBuffer)
// command.result.buf -> will be a Buffer object
break;
case CommandStates.Aborted:
// this state denotes that the command was user aborted
break;
}
});
See setConfig(options).
Returns config..
options (optional)
parser
: See https://www.npmjs.com/package/serialport#serialport-path-options-opencallback (Note: likely you will never want to change this!)baudRate
: See https://www.npmjs.com/package/serialport#serialport-path-options-opencallbackdataBits
: See https://www.npmjs.com/package/serialport#serialport-path-options-opencallbackstopBits
: See https://www.npmjs.com/package/serialport#serialport-path-options-opencallbackEOL
: (default:"\r\n"
) Command termination string (is added to every normal string type command)lineRegex
: (default"^(.+)\r\n"
) This RegExp is used to detect one-line responses and notifications.timeout
: (default:500
) default command timeout in millisec as well as the unsolicited notification timeoutdefaultExpectdResult
: (default:"OK"
) Expected result if none given (see run(), addCommand)
path
Denotes path to serial port (on linux typically something like /tty/tty.serialXYZ
, on windows COM4
)
Returns a promise.
Facade for https://www.npmjs.com/package/serialport#isopen
Facade for https://www.npmjs.com/package/serialport#pause
Forces serial shutdown. Facade for https://www.npmjs.com/package/serialport#close-callback
If tries to finish any pending commands before shutting down serial.
Please refer to Events
If set to true, command queue will be automatically processed.
Start automatic processing of command queue.
Stop automatic processing of command queue.
boolean abortCurrent (optional)
function callback (optional)
Callback to run once abortion completes.
Returns array of pending (Commands)[#command]
Cleats pending commands list.
Returns false if no command is pending at the moment, (Command)[#command] otherwise.
If and only if no other command is currently being processed, runs the given command
string|buffer|Command command (required)
If it is a (Command)[#command], any other parameters are ignored, otherwise the string|buffer is used as command to write to the serial.
string|number|regex|function expected (optional, default: OK
)
object options (optional)
timeout
: command timeout in msec (if not defined, default of modem is used, see setConfig())resultProcessor
: result preprocessor, it's result will be considered the processed and final result as passed to promise
Returns a promise.
Adds the given command to the pending commands list.
The calling semantics are identical to run(command, expected, callback, processor)
Returns a promise.
Shortcut helper to run
a command that just reads n bytes.
NOTE: after some refactoring initial CR|NL are automatically discarded and will thus never be read. This will likely have to change..
number n (required)
Number of bytes to read.
Returns a promise.
Shortcut helper to run
a command that just writes buffer
to serial and does not wait for a response.
Buffer buffer (required)
Buffer to write to serial.
Returns a promise.
Get contents of serial in buffer.
Clear contents of serial in buffer.
Get array of registered notifications.
Clear deregister all notifications.
Register a new notification.
string|Notification notification (required)
In case a Notification is passed the remaining parameters are ignored. Otherwise a string to uniquely identify the notification is expected. Will overwrite any previsouly notifications with the same value.
RegExp regex (optional)
Matching expression that will be looked out for in the buffer to detect any unsolicited incoming data.
function handler(Buffer buffer, Array matches) (optional)
Notification handler that will be called once regex
matches incoming data. Will be passed the whole matches buffer and corresponding matches as arguments.
Unregister notification with given name.
var Command = require('at-commander').Command;
var myCommand = new Command(command, expected, options);
modem.run(myCommand); // or
modem.addCommand(myCommand);
The constructor semantics are very much identical to the options of run(command, expected, options) which serves as shortcut.
var Notification = require('at-commander').Notification;
var myNotification = new Notification(name, regex, handler);
modem.addNotification(myNotification);
Please note that addNotification(notification, regex, handler) is the friendly shortcut.
Event handlers can be set using Modem.on(eventName, callback)
Please see https://www.npmjs.com/package/serialport#onopen-callback
https://www.npmjs.com/package/serialport#onclose-callback
Please see https://www.npmjs.com/package/serialport#ondata-callback
Please see https://www.npmjs.com/package/serialport#ondisconnect-callback
Please see https://www.npmjs.com/package/serialport#onerror-callback
Will be called if any registered notification matches incoming data. WARNING: currently disabled, will have to be refactored
The command event is triggered if a command successfully completes.
function callback(Command command, result)
The type/contents of result
is according to the command operations (also see section Promise based commands).
The most interesting thing about this callback is that it contains the used Command
object which in particular also has the following interesting properties:
command.result.buf -> complete accepted response of type Buffer
command.result.matches -> if and only if an expected response using a matching mechanism is used: the resulting matches
command.result.processed -> if and only if a (default or custom) processor function is passed to the command (will be the same as result)
The discarding event is triggered if the inBuffer discards data due to a timeout.
function callback(Buffer buffer)