Cellular (GSM)

Cellular (GSM)

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

API

API

Everything public, in the order you meet it.

#include <ModBeeGsm.h>

GsmDevice — the one object

Opening

bool begin(GsmTransport* transport);                  // host, or a test

template <typename StreamType>                        // Arduino
bool begin(StreamType* stream, uint32_t bitsPerSecond = 115200,
           size_t receiveCapacity = 256);

void end();                                           // orderly power-down

receiveCapacity is the size of your core's serial receive buffer, and it
matters — see 02. The default is the
pessimistic one. The Arduino form does not open the port: call
Serial1.begin() yourself first.

begin() returns false when the link will not open, or when the memory
setCapacity() asked for cannot be found; beginError() then says which.

end() sends the module its own power-down command and gives it, at most, the
module's documented settling time to answer before calling your powerOff()
hook. It is the one call that may spend a few seconds; see
06.

Configuration

bool apn(const char* name, const char* user = nullptr,
         const char* password = nullptr, GsmAuth auth = GsmAuth::Auto);
                                                      // APN up to 100 characters (3GPP),
                                                      // user and password up to 127; false,
                                                      // and nothing changed, when longer
void setCapacity(uint8_t sockets, size_t socketTxBytes, size_t socketRxBytes,
                 size_t commands);                    // before begin(); 0 = default
const char* beginError() const;                       // "out of memory for the sockets" /
                                                      // "... for the AT command queue"
void pin(const char* pin);                            // the SIM's PIN, if it has one
void powerControl(GsmPowerControl* hooks);            // optional; see 06
bool addDriver(GsmDriver* driver);                    // a module we do not ship

void radioPreference(GsmRadioPreference wanted);      // Automatic, GsmOnly, LteOnly,
                                                      // GsmAndLte, CatM, NbIot, CatMAndNbIot
void bands(const GsmBandList& catM, const GsmBandList& nbIot);
void operatorCode(const char* numericCode);           // MCC+MNC, e.g. "53001"; null = automatic
GsmResult clearForbiddenNetworks();

The APN, user and password are copied, not borrowed — a sketch may pass a
temporary, and the bearer is brought up again on every reconnect.

addDriver() returns false for a driver already registered, so calling it
twice costs nothing, and false when the registry is full (GSM_DRIVERS_MAX).
An added driver is asked after the two shipped ones; see
04.

radioPreference() matters on an LPWA module (LTE-M / NB-IoT): one
searching for a technology that is not on air never registers and never says
why. Left alone, the module keeps its own setting. bands() narrows the
search to the 3GPP band numbers a site actually uses; a GsmBandList is a
pointer to an array of band numbers and a count, and an empty list leaves the
module's configuration alone. Naming one or two bands turns a cold search of
minutes into seconds.

operatorCode() is not a default. Left null, the module searches — which is
what a controller on a pole needs, because pinning it to one operator means a
site where that operator is absent simply never connects, and nothing about the
failure says why. Name one to shorten a known cold start.

clearForbiddenNetworks() empties EF_FPLMN, the list of networks that
have refused this SIM. The list lives on the card and survives power
cycles, so a handful of rejected attempts can leave a module skipping the only
operator that would have worked, for good. The library clears it by itself when
registration will not happen; this is the manual lever. If naming an operator
works and automatic does not, this is why.

Driving

void poll();       // the only call that does work; never blocks
void start();
void stop();

State

bool isReady()        const;   // registered AND a bearer is up
bool isRegistered()   const;
bool hasBearer()      const;
GsmState state()      const;
const char* stateName() const;

const char* module()        const;   // the driver's family, once identified
const GsmIdentity& identity() const; // IMEI, ICCID, model, revision
const GsmSignal& signal()   const;
GsmRegistration registration() const;
GsmTechnology technology()  const;
const char* operatorName()  const;
const char* ipAddress()     const;

GsmResult lastError()       const;
const char* lastErrorText() const;   // the module's own words, never a number
const GsmModem::Counters& counters() const;
void refreshStatus();                // superseded; safe every scan

The same facts one value at a time, because a PLC function-block pin takes a
scalar and a wrapper is easiest to build from this surface:

const char* imei()     const;
const char* iccid()    const;
const char* imsi()     const;
const char* firmware() const;

int rssiDbm() const;                 // GSM_UNKNOWN_DBM if never read
int rsrpDbm() const;
int rsrqDb()  const;
GsmQuality signalQuality() const;

An unread reading returns GSM_UNKNOWN_DBM, not zero — zero dBm is a real
and excellent signal level, so reporting "nobody has asked" as zero is how an
alarm built on it never fires.

What the radio is actually set to

const GsmRadioConfig& radioConfig() const;
GsmDiagnostics& diagnostics();       // signal quality bands, radio read-back

radioConfig() reports what the module says, not what was asked for. Mode,
technology and bands are saved in the module's own non-volatile memory, so a
value left by a previous firmware or a diagnostic session outlives every power
cycle. Read once per bring-up, after the library has finished configuring it.

Network time

GsmResult refreshTime();             // queued, non-blocking
bool hasNetworkTime() const;
const GsmParse::GsmDateTime& networkTime() const;
gsm_time_t networkTimeAgeMillis() const;

The network sets the module's clock at registration, so this needs no NTP, no
socket and no bearer — it is the only time source available to a device that
is registered but has no data. Asked for, never polled.

What poll() costs

void   setMaxBytesPerPoll(size_t bytes);
size_t maxBytesPerPoll();
gsm_time_t serviceIntervalUs() const;
gsm_time_t serviceIntervalMs() const;

The library promises bounded work per poll(), not a wall-clock figure —
the same work is ~1150 µs on a 240 MHz ESP32-S3 and several times that on a
48 MHz part, so one number cannot be a contract across every target.

At most maxBytesPerPoll() bytes come off the link per call, at most one
command is submitted, and no loop is sized by anything the modem chose.
Measure it once on your board and budget for it; if that is still too much for
your scan cycle, turn the budget down and call poll() more often — same
throughput, smaller bites.

serviceIntervalUs() is how often the application actually calls poll(),
smoothed. Every timeout in the library is floored by it, so a caller
polling slower than a timeout does not manufacture timeouts out of its own
scheduling.

Sockets

GsmSocket* openTcp(const char* host, uint16_t port,
                   GsmSecurity security = GsmSecurity::Plain);
GsmSocket* openUdp(uint16_t localPort);
void close(GsmSocket* socket);

uint8_t socketsOpen()      const;
uint8_t socketsAvailable() const;

Returns nullptr when every slot is in use, or when the module cannot do what
was asked — refused at the moment you ask, not by a timeout later.

SMS

GsmResult sendSms(const char* number, const char* text,
                  GsmDoneFn done = nullptr, void* context = nullptr);
void onSms(GsmSmsHandler handler, void* context);

typedef void (*GsmSmsHandler)(void* context, const GsmSmsMessage& message);
typedef void (*GsmDoneFn)(void* context, GsmResult result);

Refused rather than truncated: text outside the default alphabet, or longer
than one message, returns InvalidArgument. SMS needs registration but no
data bearer; location needs neither.

A GsmSmsMessage carries sender, text, textLength, timestamp (with
hasTimestamp) and alphabet. A message that is one part of several says so
with isPart, reference, partIndex (from 1) and partCount; reassembling
the parts is left to the application.

The SMS layer is attached by the first call that uses it, so a program that
never sends or reads a message links none of the SMS code. Until then an
incoming message is left on the SIM, unread.

onSms() stays in force across module restarts. A module that reboots forgets
that it was asked to announce new messages, so after every restart the device
asks it again before sms().isReady() reports true; isReady() means
"configured in this life of the module", not "configured once".

Reading and deleting messages

GsmResult readSms(uint16_t index);
GsmResult deleteSms(uint16_t index);
uint32_t  smsReceived() const;
uint32_t  smsSent()     const;

TLS

GsmSocket* openTcp(const char* host, uint16_t port,
                   GsmSecurity security = GsmSecurity::Plain);

GsmSecurity::Plain is the default and the library does no crypto — a socket
is a byte pipe, which is what lets wolfSSL run above it. That is the
recommended arrangement, since modbee-mqtt already carries wolfSSL on both
host and Arduino.

GsmSecurity::Module uses the modem's own TLS engine, for boards where a
software engine plus a certificate chain will not fit. The two families
implement it completely differently and the application cannot tell:

SIM707x A76XX
Shape a flag on an ordinary socket a separate command family
Setup AT+CSSLCFG context, then AT+CASSLCFG=<cid>,"SSL",1 AT+CCHSET, AT+CCHSTART, AT+CCHSSLCFG
Open AT+CAOPEN, unchanged AT+CCHOPEN
Send / fetch / close AT+CASEND / CARECV / CACLOSE AT+CCHSEND / CCHRECV / CCHCLOSE
Sessions 13, the same as plain 2 (+CCHOPEN: (0,1)) against 10 plain
Send limit 1460, the same as plain 2048, larger than the 1500 plain allows

Asking for more TLS sessions than the module has is refused when you ask,
not by an open that fails twenty seconds later. Asking for module TLS on a
module that has none is refused the same way.

Location

Ask before you wait. hasGnssReceiver() reports what the module actually
answered, not what the family can do — GNSS is an order-code option on the
A7670 (-F has it, -L does not), so two modules with the same model string
differ. profile()->hasGnss means only "the driver knows these commands";
hasGnssReceiver() means "this one replied".

bool hasGnssReceiver() const;

GsmResult gnss(bool on);
GsmResult refreshFix();
bool hasFix() const;
const GsmFix& fix() const;
gsm_time_t fixAgeMillis() const;      // the number to build an alarm on

GsmFix carries latitude and longitude in signed decimal degrees,
altitudeMetres, speedKnots, courseDegrees, satellitesInView, hdop
and utc, each with a has... flag where the receiver may leave it out, and
a quality. A receiver that has lost the sky keeps reporting its last
position perfectly happily, which is why fixAgeMillis() is the number to
alarm on.

The escape hatch

GsmResult sendAt(const char* command, uint32_t timeoutMs,
                 GsmReplyFn onReply, GsmDoneFn onDone, void* context);

typedef void (*GsmReplyFn)(void* context, const char* line, size_t length);

onReply is handed each reply line as it arrives; onDone is called once,
with the conclusion.

There is always one more vendor command. Queued, timed and concluded like
anything else. A library that forced a fork to send one would be forked.

Nothing is a wall

GsmModem&    modem();
GsmSockets&  sockets();
GsmSms&      sms();
GsmGnss&     gnssLayer();
GsmAtEngine& engine();
GsmDriver*   driver()  const;
const GsmProfile* profile() const;    // what THIS module can do

GsmSocket

bool   begin();                       // starts the open and RETURNS; false if refused at once
void   end();                         // slot returns once the modem has answered
size_t readable();
size_t read(uint8_t* into, size_t length);
size_t write(const uint8_t* from, size_t length);
bool   isConnected() const;
bool   peerClosed()  const;

GsmSocketState state() const;         // Closed, Opening, Open, Closing, Failed
GsmResult lastError()  const;
size_t pendingOut()    const;

GsmClient / GsmUDP (Arduino)

Implement Arduino's own Client and UDP. Drop-in for WiFiClient,
WiFiUDP and their Ethernet equivalents.

There is no GsmServer. Arduino's Server is not implemented, and the
omission is deliberate rather than pending: an inbound connection needs a
fixed-IP APN, which ordinary IoT SIMs do not issue — they are typically handed
an address behind the operator's NAT, which nothing outside can reach. None of
the established modem libraries implements a server either, so there is
nothing proven to adapt, and a state machine written from the manuals alone
and never exercised is exactly the kind of code that fails in the field.

profile()->hasTcpServer reports whether the module could do it
(AT+SERVERSTART on A76XX, AT+CASERVER on SIM707x), and sendAt() will
send those if you have an APN that makes them useful.

GsmClient       net(gsm.sockets());       // plain TCP
GsmClientSecure tls(gsm.sockets());       // the modem's own TLS
GsmUDP          udp(gsm.sockets());

int connect(const char* host, uint16_t port);                       // 75 s deadline
int connect(const char* host, uint16_t port, int timeoutSeconds);
GsmResult  lastError() const;             // why the last connect or socket failed
GsmSocket* socket()    const;             // the socket underneath, or null

GsmClient::connect() blocks — bounded and documented — because Arduino's
interface demands a verdict. Everything else does not. See
05.


GsmResult

Four bytes. {status, space, code}.

GsmStatus status() const;
GsmErrorSpace space() const;
uint16_t code() const;

bool isOk()          const;
bool isConcluded()   const;   // Queued is NOT a conclusion
bool isRetryable()   const;
const char* describe() const;              // the library's own words
const char* describeStandardCode() const;  // +CME / +CMS only, or null

GsmStatus: Queued, Ok, NotInitialised, InvalidArgument, NotReady,
NoBearer, NoSocketAvailable, QueueFull, Superseded, Cancelled,
TooLarge, NotSupportedByModule, Error, ModuleError, Timeout,
SocketClosed, BearerLost, Unresponsive, PoweredOff,
RegistrationDenied.

GsmErrorSpace: None, Cme, Cms, SocketInfo, SocketErr,
ConnResult, Driver — see
03.

A vendor code is never rendered by the core — only the driver that owns that
table can say what it means, and guessing would be worse than saying nothing.


GsmProfile — what this module can actually do

Asked, never assumed.

const GsmProfile* p = gsm.profile();      // null until the module is identified
p->family;
p->maxSockets;  p->maxSendChunk;  p->maxRecvChunk;
p->maxTlsSessions;  p->maxTlsSendChunk;  p->tlsStyle;  p->sendAccounting;
p->hasUdp;  p->hasTcpServer;  p->hasGnss;  p->hasPsm;  p->hasEdrx;
p->hasMultiContext;
p->registrationTimeoutMs;  p->socketOpenTimeoutMs;  p->unreadCheckMs;
p->powerOnToUartMs;  p->powerKeyOnPulseMs;  p->powerKeyOffPulseMs;
p->hasRsrpConversion;  p->rsrpOffsetDbm;

Every field is commented in src/GsmProfile.h, with the manual section its
value comes from in each driver.


GsmPowerControl

virtual bool setPowerKey(bool asserted);     // preferred: move a line, return
virtual bool setSupply(bool on);
virtual bool setReset(bool asserted);

virtual bool powerOn();                      // fallback, for a board with no key
virtual bool powerOff();
virtual bool reset();

virtual GsmTristate isPoweredOn() const;     // Unknown, No, Yes

Seven hooks, all optional; each returns false for "this board cannot". The
library owns the order and the deadlines, including how long the power key is
held; your application owns the pins. See 06.


Lower layers

All public, all documented in their headers.

GsmModem the bring-up state machine and the recovery ladder; stateBudgetMs(state) is each state's deadline, moduleEpoch() moves on every time the module starts afresh
GsmAtEngine the queue; submit() a GsmCommand directly
GsmUrcRouter register a prefix and a handler
GsmAtParser the four-mode scanner; no I/O, no clock
GsmParse fields, numbers, addresses, timestamps — refuses, never guesses
GsmSmsPdu TS 23.040 / 23.038, pure
GsmTransport the byte channel to the modem
GsmDriver a dialect; see 04

Four of them are reachable straight off the device, and each returns the live
object
rather than a copy:

GsmNetwork&     network();        // registration, operator, technology, bands
const GsmApn&   bearer() const;   // the data context as configured
GsmClock&       clock();          // the module's clock; see "Network time" above
GsmDiagnostics& diagnostics();    // signal, quality thresholds, radio read-back

Capacity, set at run time

Taken once, in begin(), from setCapacity() — on an ESP32 in PSRAM first —
and never on the poll path. Too much for the board makes begin() fail with
beginError() saying which; it never carries on with less.

setCapacity() Default
sockets 2 (GSM_SOCKET_SLOTS) sockets open at once; the module's own limit still applies
socketTxBytes / socketRxBytes 1500 (GSM_SOCKET_TX_BUFFER / _RX_) per socket
commands 12 (GSM_QUEUE_CAPACITY) AT commands held at once

GsmUDP(sockets, packetBytes) takes its datagram size the same way (default
GSM_UDP_PACKET_MAX, 512); its begin() returns 0 when there is no room.

Build-time knobs

Every one has a default that works. None selects a modem — that is decided at
run time.

Default
GSM_AT_COMMAND_MAX 192 room for each queued command
GSM_AT_COMMAND_LONG_MAX 384 the one buffer for a longer command (the SIM707x bearer set-up with a long APN and credentials); a second long one waits its turn as QueueFull
GSM_APN_MAX / _USER_MAX / _PASSWORD_MAX 100 / 127 / 127 the APN, user name and password held
GSM_AT_LINE_MAX 512 longest reply held in full
GSM_POLL_BYTE_BUDGET 256 bytes drained per poll()
GSM_URC_HANDLERS_MAX 16
GSM_DRIVERS_MAX 6
GSM_UDP_PACKET_MAX 512 one datagram, unless the constructor says otherwise
Last updated 2 hours ago
Was this helpful?
Thanks!