Easily and asynchronously interact with a serial device requiring call-and-response style commands.

Arduino Managed Serial Device

Note This library was formerly less-descriptively named "Arduino Async Duplex"

This library allows you to asynchronously interact with any device having a call-and-response style serial command interface.

If you've ever used one of the many serial-controlled devices that exist, you're familiar with the frustration that is waiting for a response from a long-running command. Between sending your command and receiving a response (or worse -- that command timing out), your program is halted, and your microcontroller is wasiting valuable cycles. This library aims to fix that problem by allowing you to queue commands that will be asynchronously sent to your device without blocking your microcontroller loop.

Requirements

Examples

The following examples are based upon interactions with a SIM7000 LTE modem.

Simple

Sending a command is as easy as queueing it:

#include <ManagedSerialDevice.h>
#include <Regexp.h>

ManagedSerialDevice handler = ManagedSerialDevice();

void setup() {
    handler.begin(&Serial1);

    handler.execute("AT");
}

void loop() {
    handler.loop();
}

But that isn't much more useful than just writing to the stream directly; for more useful applications, keep reading.

Independent

When executing multiple independent commands, you can follow the below pattern:

#include <ManagedSerialDevice.h>
#include <Regexp.h>

ManagedSerialDevice handler = ManagedSerialDevice();

void setup() {
    handler.begin(&Serial);

    handler.execute(
        "AT+CCLK?",
        "+CCLK:.*\n",
    );
    handler.execute(
        "AT+CIPSTATUS",
        "STATE:.*\n"
    );
}

void loop() {
    handler.loop();
}

This pattern will work great for independent commands like the above, but for a few reasons this isn't the recommended pattern to follow for sequential steps that are dependent upon one another:

  1. If one of these commands' expectations are not met (i.e. AT+CIPSTART in the examples below returns ERROR instead of OK), the subsequent commands will still be executed.
  2. No guarantee is made that these will be executed sequentially. More commands could be queued and inserted between the above commands if another function queues a high-priority (ManagedSerialDevice::Timing::NEXT) command.
  3. There are a limited number of independent queue slots (by default: 5, but this value can be adjusted by changing COMMAND_QUEUE_SIZE).

Sequential (Nested Callbacks)

This is a simplified overview of connecting to a TCP server using a SIM7000 LTE modem.

  1. Send AT+CIPSTART...; wait for OK followed by the line ending.
  2. Send AT+CIPSEND...; wait for a > to be printed.
  3. Send the data you want to send followed by CTRL+Z (\x1a).

The below commands will be executed sequentially and should any command's expectations not be met, subsequent commands will not be executed.

#include <ManagedSerialDevice.h>
#include <Regexp.h>

ManagedSerialDevice handler = ManagedSerialDevice();

void setup() {
    handler.begin(&Serial);

    handler.execute(
        "AT+CIPSTART=\"TCP\",\"mywebsite.com\",\"80\"", // Command
        "OK\r\n",  // Expectation regex
        [&handler](MatchState ms) -> void {
            Serial.println("Connected");

            handler.execute(
                "AT+CIPSEND",
                ">",
                NEXT,
                [&handler](MatchState ms) -> void {
                    handler.execute(
                        "abc\r\n\x1a"
                        "SEND OK\r\n"
                        NEXT,
                    );
                }
            )
        }
    );
}


void loop() {
    handler.loop();
}

Sequential (Chained)

Most of the time, you probably just want to run a few commands in sequence, and the callback structure above may become tedious. For sequential commands that should be chained together (like above: aborting subsequent steps should any command's expectations not be met), there is a simpler way of handling these:

#include <ManagedSerialDevice.h>
#include <Regexp.h>

ManagedSerialDevice handler = ManagedSerialDevice();

void setup() {
    handler.begin(&Serial);

    ManagedSerialDevice::Command commands[] = {
        ManagedSerialDevice::Command(
            "AT+CIPSTART=\"TCP\",\"mywebsite.com\",\"80\"", // Command
            "OK\r\n",  // Expectation regex
            [](MatchState ms){
                Serial.println("Connected");
            }
        ),
        ManagedSerialDevice::Command(
            "AT+CIPSEND",
            ">",
        ),
        ManagedSerialDevice::Command(
            "abc\r\n\x1a"
            "SEND OK\r\n"
        )
    }
    handler.executeChain(commands, 3);
}


void loop() {
    handler.loop();
}

The above is identical in function to the "Nested Callbacks" example earlier, but using this pattern allows you define callbacks that are automatically prepended to any callback that might have originally been defined for every member of the chain:

#include <ManagedSerialDevice.h>
#include <Regexp.h>

ManagedSerialDevice handler = ManagedSerialDevice();

void setup() {
    handler.begin(&Serial);

    ManagedSerialDevice::Command commands[] = {
        ManagedSerialDevice::Command(
            "AT+CIPSTART=\"TCP\",\"mywebsite.com\",\"80\"", // Command
            "OK\r\n",  // Expectation regex
            [](MatchState ms){
                Serial.println("Connected");
            }
        ),
        ManagedSerialDevice::Command(
            "AT+CIPSEND",
            ">",
        ),
        ManagedSerialDevice::Command(
            "abc\r\n\x1a"
            "SEND OK\r\n"
        )
    }
    handler.executeChain(
        commands,
        3,
        [](MatchState ms) { // Common success function
            // This will cause a message to be printed
            // after the completion of every command in the chain
            Serial1.println("Success!");
        },
        [](Command*) { // Common failure function
            // This will cause a message to be printed
            // after any member of the chain fails; this
            // might be a good place to put retry logic!
            Serial1.println("Failed!");
        }
    );
}


void loop() {
    handler.loop();
}

Capture Groups

The below example will execute the relevant commands and, when the response is received, set local variables using captured data.

#include <ManagedSerialDevice.h>
#include <Regexp.h>

ManagedSerialDevice handler = ManagedSerialDevice();

void setup() {
    handler.begin(&Serial);

    // Get the current timestamp
    time_t currentTime;
    handler.execute(
        "AT+CCLK?",
        "+CCLK: \"([%d]+)/([%d]+)/([%d]+),([%d]+):([%d]+):([%d]+)([\\+\\-])([%d]+)\"",
        [&currentTime](MatchState ms) {
            char year_str[3];
            char month_str[3];
            char day_str[3];
            char hour_str[3];
            char minute_str[3];
            char second_str[3];
            char zone_dir_str[2];
            char zone_str[3];

            ms.GetCapture(year_str, 0);
            ms.GetCapture(month_str, 1);
            ms.GetCapture(day_str, 2);
            ms.GetCapture(hour_str, 3);
            ms.GetCapture(minute_str, 4);
            ms.GetCapture(second_str, 5);
            ms.GetCapture(zone_dir_str, 6);
            ms.GetCapture(zone_str, 7);

            tmElements_t timeEts;
            timeEts.Hour = atoi(hour_str);
            timeEts.Minute = atoi(minute_str);
            timeEts.Second = atoi(second_str);
            timeEts.Day = atoi(day_str);
            timeEts.Month = atoi(month_str);
            timeEts.Year = (2000 + atoi(year_str)) - 1970;

            currentTime = makeTime(timeEts);
        }
    );

    char connectionStatus[10];
    handler.execute(
        "AT+CIPSTATUS",
        "STATE: (.*)\n"
        [&connectionStatus](MatchState ms) {
            ms.GetCapture(connectionStatus, 0);
        }
    );
}

void loop() {
    handler.loop();
}

Failure Handling

You can pass a second function parameter to be executed should the request timeout. If you would like to retry the command (and subsequent commands chained with it), you are able to do so.

#include <ManagedSerialDevice.h>
#include <Regexp.h>

ManagedSerialDevice handler = ManagedSerialDevice();

void setup() {
    handler.begin(&Serial);
    handler.execute(
        "AT+CIPSTART=\"TCP\",\"mywebsite.com\",\"80\"", // Command
        "OK\r\n",  // Expectation regex
        [](MatchState ms) -> void {
            Serial.println("Connected");
        },
        [&handler](ManagedSerialDevice::Command* cmd) -> void {  // Run this function on failure
            Serial.println("Connection failed; retrying");

            // Retry this immediately
            handler.execute(cmd, ManagedSerialDevice::Timing::NEXT);
        }
    );
}

void loop() {
    handler.loop();
}

If you only want to print to the console that an error occurred, you can use the ManagedSerialDevice::printFailure helper:

#include <ManagedSerialDevice.h>
#include <Regexp.h>

ManagedSerialDevice handler = ManagedSerialDevice();

void setup() {
    handler.begin(&Serial);
    handler.execute(
        "AT+CIPSTART=\"TCP\",\"mywebsite.com\",\"80\"", // Command
        "OK\r\n",  // Expectation regex
        [](MatchState ms) -> void {
            Serial.println("Connected");
        },
        ManagedSerialDevice::printFailure(&Serial1), // Will print "Command 'AT+CIPSTART...' failed."
    );
}

void loop() {
    handler.loop();
}

Timeouts

By default, commands time out after 2.5s (see COMMAND_TIMEOUT); sometimes you may need to run a command that needs extra time to complete:

#include <ManagedSerialDevice.h>
#include <Regexp.h>

ManagedSerialDevice handler = ManagedSerialDevice();

void setup() {
    handler.begin(&Serial);
    handler.execute(
        "AT+CIPSTART=\"TCP\",\"mywebsite.com\",\"80\"", // Command
        "OK\r\n",  // Expectation regex
        NULL,
        NULL,
        10000  // Extended Timeout
    );
}

void loop() {
    handler.loop();
}

Delaying

Occasionally, especially when chaining commands, you may need to ensure that a subsequent command isn't executed immediately; in these cases, you are able to set a delay:

#include <ManagedSerialDevice.h>
#include <Regexp.h>

ManagedSerialDevice handler = ManagedSerialDevice();

void setup() {
    handler.begin(&Serial);

    ManagedSerialDevice::Command commands[] = {
        ManagedSerialDevice::Command(
            "AT+CIPSTART=\"TCP\",\"mywebsite.com\",\"80\"", // Command
            "OK\r\n",  // Expectation regex
            [](MatchState ms){
                Serial.println("Connected");
            }
        ),
        ManagedSerialDevice::Command(
            "AT+CIPSEND",
            ">",
            NULL,
            NULL,
            COMMAND_TIMEOUT,
            1000  // Wait for 1s before running this command 
        ),
        ManagedSerialDevice::Command(
            "abc\r\n\x1a"
            "SEND OK\r\n"
        )
    }
    handler.executeChain(commands, 3);
}


void loop() {
    handler.loop();
}

Note from the author

I'm not a particularly great C++ programmer, and all of the projects I work on using this language are ones I work on for fun in my free time. It's very likely that you, dear reader, will have a better understanding of either C++ or programming for microcontrollers in general and will find ways of improving this that I either wouldn't have the skills to pull off on my own, or wouldn't even have the awareness of to know how much better things could be. If any of those situations occur, please either reach out on freenode -- I'm coddingtonbear there, too, search the issues list on Github or create a new issue if one doesn't exist, or, better yet, post a pull request making things better. Cheers!

Owner
Adam Coddington
Not affiliated with non-:rainbow_flag: types of bears.
Adam Coddington
Similar Resources

A simple two-axis gimbal built using two servo motors, an mpu6050 gyro and accelerometer sensor, and an Arduino (Uno)

A simple two-axis gimbal built using two servo motors, an mpu6050 gyro and accelerometer sensor, and an Arduino (Uno)

Makeshift Gimbal Project A simple two-axis gimbal built using two servo motors, an mpu6050 gyro and accelerometer sensor, and an Arduino (Uno). A shor

Jun 17, 2022

Arduino library for interfacing with any GPS, GLONASS, Galileo or GNSS module and interpreting its NMEA messages.

Arduino library for interfacing with any GPS, GLONASS, Galileo or GNSS module and interpreting its NMEA messages.

107-Arduino-NMEA-Parser Arduino library for interfacing with any GPS, GLONASS, Galileo or GNSS module and interpreting its NMEA messages. This library

Jan 1, 2023

Arduino, esp32 and esp8266 library for ABB (ex PowerOne) Aurora Inverter, implement a full methods to retrieve data from the Inverter via RS-485

Arduino, esp32 and esp8266 library for ABB (ex PowerOne) Aurora Inverter, implement a full methods to retrieve data from the Inverter via RS-485

ABB Aurora protocol You can refer the complete documentation on my site ABB Aurora PV inverter library for Arduino, esp8266 and esp32 I create this li

Nov 22, 2022

CAN Driver for Teensy 3.1 / 3.2, 3.5 and 3.6

CAN Library for Teensy 3.1 / 3.2, 3.5, 3.6 Compatibility with the ACANxxxx libraries This library is fully compatible with the MCP2515 CAN Controller

Dec 9, 2022

Arduino library to access Adafruit IO from WiFi, cellular, and ethernet modules.

Arduino library to access Adafruit IO from WiFi, cellular, and ethernet modules.

Adafruit IO Arduino Library This library provides a simple device independent interface for interacting with Adafruit IO using Arduino. It allows you

Dec 23, 2022

Arduino library for SPI and I2C access to the PN532 RFID/Near Field Communication chip

Adafruit-PN532 This is a library for the Adafruit PN532 NFC/RFID breakout boards This library works with the Adafruit NFC breakout https://www.adafrui

Dec 23, 2022

Arduino library for sending email and SMS from nothing but the ESP8266!

Arduino library for sending email and SMS from nothing but the ESP8266!

Did you know your ESP8266 could send Email and SMS without any special hardware or paid services like Twilio? With AlertMe, your ESP8266 project can:

Feb 24, 2022

IOTBOT, which is designed as an Internet-oriented robotic coding training kit and powered by the ESP32 processor

IOTBOT, which is designed as an Internet-oriented robotic coding training kit and powered by the ESP32 processor

IOTBOT-Firmware! Test Series IOTBOT, which is designed as an Internet-oriented robotic coding training kit and powered by the ESP32 processor, knows n

Dec 29, 2021

An implementation of a ANT driver for Arduino, Mbed and ESP-IDF

ant-arduino Arduino library for communicating with ANT radios, with support for nRF51 devices. This library Includes support for the majority of packe

Dec 4, 2022
Comments
  • Error compiling for Arduino Mega (even with installed requirements)

    Error compiling for Arduino Mega (even with installed requirements)

    Hi, first of all thank you for the library.

    I'm trying to make it work with my Arduino Mega so I can manage properly the AT comm with my SIM7000, but Arduino IDE throws an error when compiling because of the std::functional.

    I have installed StandardCplusplus and Regexp libraries.

    Thanks!

  • Specify library dependencies in library.properties

    Specify library dependencies in library.properties

    Specifying the library dependencies in the depends field of library.properties causes the Arduino Library Manager (Arduino IDE 1.8.10 and newer) to offer to install any missing dependencies during installation of this library.

    arduino-cli lib install will automatically install the dependencies (arduino-cli 0.7.0 and newer).

    Reference: https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5:-Library-specification#libraryproperties-file-format

  • millis() timer overflow after 49 days of runtime

    millis() timer overflow after 49 days of runtime

    These kinds of comparisons don't allow for overflow:

    commandQueue[position].delay = _delay + millis(); ... if(!processing && queueLength > 0 && commandQueue[0].delay <= millis()) {

    timeout = millis() + commandQueue[0].timeout; ... if(_timeout && (millis() > started + _timeout)) {

    Admittedly this is a problem that will only occur every 49 days, but still...

    See this for examples of best practices: Worried about millis() timer overflow?

    Basically, you should do this instead (add a new "start" property): command.start = millis(); command.delay = _delay;

    And then this should be the check: if (millis() - command.start >= command.delay) {

  • Statically allocate callback functions

    Statically allocate callback functions

    Currently this library relies on use of std::functional which itself creates function objects on the heap. This isn't ideal for use on constrained memory devices due to fragmentation issues, but is extremely handy for defining callback functions. My C++ skills aren't strong enough at the moment to handle this, but it's apparently possible for functions to be statically allocated to avoid this problem.

Asynchronously interact with your SIM7000 LTE modem

Arduino-Async-Modem If you've ever used one of the many modem-handling libraries that exist, you're familiar with the frustration that is waiting for

Sep 10, 2022
A Wiring/Arduino library to tokenize and parse commands received over a phisical/software serial port or buffer.

A Wiring/Arduino library to tokenize and parse commands received over a phisical/software serial port or buffer.

Jun 9, 2022
An easy to use C++ Library to interact with the Algorand Blockchain.
An easy to use C++ Library to interact with the Algorand Blockchain.

Algoduino An easy to use Library to interact with the Algorand Blockchain. Lead Maintainer: Ted Nivan Documentation You can find installation instruct

Nov 25, 2022
A library to control esp-8266 from Arduino by AT commands easier.
A library to control esp-8266 from Arduino by AT commands easier.

ArduinoESPAT A library to control esp-8266 from Arduino by AT commands easier. Wiring Diagram Arduino Uno ESPr 5V Vin GND GND D2 TX D3 RX Usage Defini

Dec 18, 2022
An Arduino library with additions to vanilla Serial.print(). Chainable methods and verbosity levels. Suitable for debug messages.

advancedSerial This library provides some additions to vanilla Serial.print(): 1. Chainable print() and println() methods: // you can chain print() a

Dec 13, 2022
Minimal bit-bang send serial 38400/115200 baud for 1MHz or 115200/230400 baud for 8 or 16MHz ATtiny clock
Minimal bit-bang send serial 38400/115200 baud for 1MHz or 115200/230400 baud for 8 or 16MHz ATtiny clock

Attiny Serial Out Available as Arduino library "ATtinySerialOut" Version 2.0.1 - work in progress Minimal bit-bang send serial 115200 baud for 1/8/16

Jan 4, 2023
Software emulated serial using hardware timers for improved compatibility
Software emulated serial using hardware timers for improved compatibility

AltSoftSerial Library Improved software emulated serial, using hardware timers for precise signal timing and availability of CPU time for other librar

Dec 12, 2022
Arduino client for the Serial To TCP Bridge Protocol PC side service

Arduino Serial to TCP Bridge Client Arduino client for the Serial To TCP Bridge Protocol gateway service. Open a TCP connection to a server from the A

Apr 12, 2022
This class provides functionality useful for debugging sketches via printf-style statements.

Arduino_DebugUtils This class provides functionality useful for debugging sketches via printf-style statements. How-To-Use Basic Arduino_DebugUtils ha

Dec 13, 2022
ESP32-S2 and CC1101S 433Mhz usb stick to record and send car gates/garages data keys and open stuff
ESP32-S2 and CC1101S 433Mhz usb stick to record and send car gates/garages data keys and open stuff

HackZeGarage ESP32-S2 and CC1101S 433Mhz usb stick to record and send car gates/garages data keys and open stuff **HackZeGarage @sulfuroid / Dr CADIC

Mar 16, 2022