AT command server written in C++.
- Written in standard C++17
- Modern and self-explanatory APIs
- Robust and high efficiency I/O handling
- "Inhibit mode" to allow raw data transmission on same I/O medium
- The AT syntax parser can be used in standalone mode
- Rich built-in helper functions
The French word "chat" is "cat" in English.
- Prefer usability and speed over low RAM usage
- No strict syntax checking and man-made limitations
- Does not assume anything: Max flexibility
If you're using CPM
CPMAddPackage(
GITHUB_REPOSITORY SudoMaker/chAT
GIT_TAG <some tag, e.g. v1.0.0 or commit id>
GIT_SHALLOW ON
)
target_link_libraries(foo chAT)
#include <chAT.hpp>
using namespace SudoMaker;
chAT::Server at_srv;
>> AT+FOO\r\n
<< OK\r\n
>> AT+FOO?\r\n
<< +FOO: 123\r\n
<< OK\r\n
>> AT+FOO=456\r\n
<< OK\r\n
>> AT+FOO=?\r\n
<< +FOO: <number>
<< OK\r\n
<< +ALARM: "Your cat ate a rat"\r\n
Be aware that they can appear at anywhere. Like:
>> AT+FOO?\r\n
<< +ALARM: "Your cat scratched your curtain"\r\n
<< +FOO: 123\r\n
<< +ALARM: "Your cat destroyed your vase"\r\n
<< +ALARM: "Your cat opened your fridge"\r\n
<< OK\r\n
<< +ALARM: "Your cat ate your salmon"\r\n
at_srv.set_command_callback([](chAT::Server& srv, const std::string& command) {
auto &parser = srv.parser(); // type: chAT::ATParser&
auto cmd_mode = parser.cmd_mode; // type: chAT::CommandMode
auto &argv = parser.args; // type: std::vector<std::string>&
auto argc = argv.size(); // type: size_t
// Now assume user typed "AT+FOO=abc,123\r\n", then:
// 'command' contains "+FOO"
// 'cmd_mode' contains chAT::CommandMode::Write
// 'argv' contains ["abc", "123"] (yes it starts from 0)
// 'argc' contains 2
// ... do your stuff
if (success) {
return chAT::CommandStatus::OK; // User will see "OK\r\n"
}
if (cpu_is_on_fire) {
return chAT::CommandStatus::ERROR; // User will see "ERROR\r\n"
}
return chAT::CommandStatus::CUSTOM; // No output will be produced by default
// You can always add custom output via chAT::Server::write_*() functions.
});
These functions need to return the number of bytes actually read/written. If the operation can't be completed (i.e. not a single byte has been read/written), simply return -1. However the server will not read errno
. It up to the handler to handle unrecoverable errors.
at_srv.set_io_callback({
.callback_io_read = [](auto buf, auto len){
return read(STDIN_FILENO, buf, len);
},
.callback_io_write = [](auto buf, auto len){
return write(STDOUT_FILENO, buf, len);
},
});
If you're using non-blocking I/O (i.e. these functions return immediately), enable the nonblocking mode.
at_srv.set_nonblocking_mode(true);
These functions can be used anywhere. But they're not thread-safe (i.e. a mutex is needed if you call them from more than 1 thread).
Write data. It's simple enough.
The data is not copied and needs to be available before Server::run()
completed all write tasks.
Write C-style string. It's basically the same as write_data
, but it will comupte the string length if len
is not specified.
The data is not copied and needs to be available before Server::run()
completed all write tasks.
Write C++ std::string
. std::move
is used internally to save potential copies.
Write C++ std::vector<uint8_t>
. std::move
is used internally to save potential copies.
Write ERROR\r\n
.
Write error reason. std::move
is used internally to save potential copies.
Example: If str
is "putain", it will write +ERROR: putain\r\n
.
Write OK\r\n
.
Write response. std::move
is used internally to save potential copies.
Example: If the current command is +FOO
and str
is "bon", it will write +FOO: bon\r\n
.
Write the response prompt.
Example: If the current command is +FOO
, it will write +FOO:
.
Write +ERROR:
.
Write \r\n
.
You only need to care about the return value in nonblocking mode.
auto rc = at_srv.run(); // type: chAT::Server::RunStatus
using RunStatus = chAT::Server::RunStatus;
if (rc & RunStatus::WantRead) {
// Enable IN event
}
if (rc & RunStatus::WantWrite) {
// Enable OUT event
}
// Assume we need to read 128 bytes of raw data from the I/O medium.
// Since the server doesn't read one byte at a time (which is slow as hell),
// some data can be already buffered by the server, so we pull them out at first.
// Inhibit server read and pull out at most 128 bytes of data from server buffer
auto raw_data = at_srv.inhibit_read(128); // type: std::vector<uint8_t>
// Then, 'raw_data' contains 0~128 bytes of data,
// and the server won't initiate any more read operations.
// Now it's up to the user to read the remaining data from the operating system.
// After it's done, call
at_srv.continue_read();
// to continue normal operation.
// Note that the write operations are not affected.
// You can always utilize the server to arrange binary data writes.
Currently, it's hardcoded to 1024 bytes. It's 2 times of the maximum endpoint size of USB2.0.
The default limit is 16384 bytes. Use the following to change it:
at_srv.set_write_buffer_size_limit(1024);
When the buffer is full, the oldest data will be discarded.
If you attempt to put in a chunk of data larger than the buffer (e.g. write_vec8(large_vector)
), it will cause everything in the buffer to be discarded.
See the examples
directory.
parser.cpp
- Standalone AT command syntax parsingposix_stdio_blocking.cpp
- AT command server with blocking POSIX standard I/Oposix_stdio_nonblocking.cpp
- AT command server with nonblocking POSIX standard I/O
AGPLv3