Hey
sorry for not answering earlier. don't have much time at the moment
Attached (too big to fit on screen) is a something I had been working on in the past
Indeed Your explanation reminded me that I had somewhere an async Stream communication class (templated) that I had developed for handling multiple Serial communications on a MEGA (send a command, expect an answer ending with an arbitrary key phrase - ie an endMarker that is a cString, not just one char).
It's probably not super clean (was not meant to be published and I needed something quick) but should work with anything that inherits from the Stream class as long as you can concurrently read and write from that stream and others (if you have multiple). I used it with a MEGA and Hardware Serial. SoftwareSerial would be an issue as you can't listen from many Streams at the same time.
This is how it works:
You instantiate the StreamQueue class through a template by indicating how long the queue needs to be (max numbers of pending commands) and what's the max size for the answer. Then you provide the Stream you want to be attached to.
For example
StreamQueue<3, 20> serialQueue(Serial); // on Serial port, 3 commands max in queue, response buffer has 20 bytes max (+ a trailing NULL).
StreamQueue<10, 60> serial1Queue(Serial1); // on Serial1, 10 commands max, response buffer has 60 bytes max (+ a trailing NULL)
serialQueue
could be used for end user terminal menu GUI for example whilst serial1Queue
could be used for AT commands with a SIMxxx modem
a Command is a structure:
struct t_AsyncCommand
{
t_commandID _commandID; // your unique ID for this command, used in callBack
const char* _command; // the text of the command. needs to be persistant text or NULL if just waiting for an answer
const char* _endMarker; // the text of the end Marker. needs to be persistant text or NULL if just sending a command not waiting any anwser
uint32_t _maxWait; // the max delay you are willing to wait before you timeOut on this command
t_callback _callback; // a pointer to the function you want to execute after the command is complete (either got the end marker or timed out or was just sent if no answer was needed)
};
For example for a ESP-01 initialization at 9600 bauds you could do something like (typed here so for illustration not sure it's a fully correct sequence)
t_AsyncCommand espCommands[] = { // {commandID, command, endMarker, maxWait, callBack}
{101, "AT", "OK", 1000, okCallback1}, // AT requires an OK answer,
{102, "AT+RESTORE", NULL, 5000, NULL}, // reset to factory default
{103, "AT", "OK", 1000, okCallback2}, // AT requires an OK answer
{104, "AT+UART_DEF=9600,8,1,0,0", "OK", 5000, swicthBaudsCallBack},
};
const uint8_t nbCommands = sizeof(espCommands) / sizeof(espCommands[0]);
each command has
- an ID (101, 102, 103, ...) you arbitrarily select (passed in the callback)
- the text of the command you want to send
- the answer you want to wait for
- how long you are willing to wait before timeout
- the callback function you want to call upon completion
you can make your magic happen in the callbacks through a state machine. it's up to you there.
The callback can be NULL if you don't have anything to do (for example when I sent the restore)
but ideally you would want to trap timeout at least.
To add a command to the queue there is a registerAsyncCommand()
method, for example if you want to load the 4 commands we defined above, you would do:
for (uint8_t i = 0; i < nbCommands; i++)
serial1Queue.registerAsyncCommand(&(espCommands[i]));
Then in the loop you need to ping the StreamQueue to check for update (It would not make much sense to have that in a timer ISR as most Streams depends on interruptions being available to receive data).
void loop()
{
serialQueue.updateQueue(); // ping each of your StreamQueues
serial1Queue.updateQueue(); // ping each of your StreamQueues
}
there is one extra feature, when you doStreamQueue<10, 100> serialQueue(Serial);
you are using the default end of line validation so every command will be sent with a trailing CR+LF and every end Marker will be expected to have also CR+LF. (most common way with AT commands and UI stuff)
The constructor for the StreamQueue class actually takes a second parameter if you want something differentStreamQueue(Stream& commChannel, uint8_t tail = asyncCRLF_TAIL)
[b]tail[/b]
comes from an enumenum : uint8_t {asyncNO_TAIL = 0b00, asyncLF_TAIL = 0b01, asyncCR_TAIL = 0b10, asyncCRLF_TAIL = 0b11};
where you have the choice of NO_TAIL
(then you have to provide your own exact characters in your commands and markers), only LF
, only CR
or both CR+LF
(which is the default).
The Stream is under the responsibility of the caller, so your main code has to ensure it exists before attaching it to a StreamQueue and of course open it at the right speed before asking the commands to be sent.
Between two consecutive commands there is a bit of code that will try to empty the Stream input (whatever is there) so that we try as much as we can to start a command with a fresh empty incoming buffer. This is not perfect as you can't really predict when things arrive in an async protocol but proved to be good enough for my use (in case of timeouts or buffer overflows)
You need to install the StreamQueue.h
(which embeds the full code...) next to your Sketch and start your .ino with#include "StreamQueue.h"
This is not great but if I remember well I faced some challenges with the IDE trying to second guess what needed to happen at compile time when using templates in a .h and .cpp and I was ending up with cryptic double definitions of functions.
I can't say it was heavily debugged nor memory optimized, if you or anyone in the forum want to have a look and explore - I'm happy to provide comments on the ideas behind the code.
it probably deserve a full rewrite though
the example attached runs a simple quizz test on Serial, open the console at 115200 bauds with CR+LF as validation line. you should see the commands being sent in sequence and you can type your answer in the Serial monitor. The call back function verifies if your input corresponds to the right answer of the quizz.
after you completed the quizz, the callback reload the queue with one new question to see if you want to play again. that will call a different callback, if you answer 'Y' it will reload the (same) set of commands to run the quizz again.
that should help demonstrate how things work. You can let the answer timeout or answer something wrong as well as this is handled in the callback.
AsyncCommandQuizz.zip (8.08 KB)