This article shows how to use the Arduino Mega 2560 development board and a SIM7600G modem in a cellular IoT node capable of collecting network data, GPS, and SIM status, packing it into JSON, which will later be sent to any back-end.
Why a Mega 2560 + SIM7600G?
- Abundant SRAM (8 kB) — it generates JSON, holds buffers, and still has room left for application logic; an UNO would be at the limit.
- Dedicated UART (Serial1) — the Mega offers extra serial ports, isolating modem traffic from the USB console.
- SIM7600G — covers 2G/3G/4G and GNSS on a single chip, simplifying the hardware.
The pinout used for this project is explained in another post.
Project Structure
arduino-mobile-sensor/
├─ src/ // main sketch
├─ include/ // custom headers
├─ lib/ // internal libraries
└─ .pio/ // PlatformIO cache
Source code and project files: https://gitlab.com/mfs.eng.br/hardware-quirks/arduino-sim7600-telemetry
Anatomy of the C++ Code
Minimal Includes
#include <Arduino.h> // AVR HAL
#include <avr/pgmspace.h> // PROGMEM
#include <ArduinoJson.h> // The only external lib
Constant Definitions
constexpr uint32_t USB_BAUD = 115200;
constexpr uint32_t MODEM_BAUD = 115200;
constexpr uint16_t TIMEOUT_MS = 1500;
#define OUTPUTDEBUG 0
/* ------------ reception buffer ----------------------------- */
static char respBuf[160];
static size_t respLen = 0;
constexprguarantees compile-time evaluation;#defineflags enable logs at no cost when off;- global buffers (
char respBuf[160]) live in BSS, avoiding heap usage.
AT Strings in Flash
/* ------------ commands in flash ------------------------------ */
const char CMD_CGSN[] PROGMEM = "AT+CGSN";
const char CMD_CIMI[] PROGMEM = "AT+CIMI";
const char CMD_CGMM[] PROGMEM = "AT+CGMM";
const char CMD_CSPN[] PROGMEM = "AT+CSPN?";
const char CMD_CSQ[] PROGMEM = "AT+CSQ";
const char CMD_CREG[] PROGMEM = "AT+CREG?";
const char CMD_COPS[] PROGMEM = "AT+COPS?";
const char CMD_CGPADDR[] PROGMEM = "AT+CGPADDR";
const char CMD_CGDCONT[] PROGMEM = "AT+CGDCONT?";
const char AT_GPSINFO[] PROGMEM = "AT+CGPSINFO";
PROGMEM saves SRAM; every command would otherwise live in Flash anyway.
Flash-print Compatibility
#ifndef FPSTR
#define FPSTR(p) (reinterpret_cast<const __FlashStringHelper *>(p))
#endif
This allows Serial.println() to print strings stored in Flash.
The AT Command Read/Write Machine (sendAT)
Sending
Serial1.println(cmd); // already includes \r\n
Block Collection
// Main loop: stays active while elapsed time is less than 'tout'
while (millis() - t0 < tout)
{
// Read loop: runs while there are bytes in the UART *and*
// there is still space in the buffer (leaves 1 byte free for '\0')
while (Serial1.available() && respLen < sizeof(respBuf) - 1)
// Reads one byte from Serial1 and stores it at the current buffer
// position, then increments 'respLen' to point to the next free slot
respBuf[respLen++] = Serial1.read();
// Places a null terminator at the current position, turning respBuf
// into a valid C string (required for <string.h> functions)
respBuf[respLen] = '\0';
// Checks whether the response already contains a "\nOK" or "\nERROR"
// — patterns that signal the end of an AT response.
// If found, exits the outer loop immediately (timeout no longer needed)
if (strstr(respBuf, "\nOK") || strstr(respBuf, "\nERROR"))
break;
}
The singleLineAT Function
/* ---------- returns the first "useful" line -------------------- */
bool singleLineAT(const __FlashStringHelper *cmd, char *dst, size_t dstSz)
{
if (!sendAT(cmd)) return false;
char *p = respBuf;
while (*p) {
/* skip CR/LF */
while (*p == '\r' || *p == '\n') ++p;
if (!*p) break;
/* find end of line */
char *q = p;
while (*q && *q != '\r' && *q != '\n') ++q;
/* copy line to tmp */
char line[128];
size_t len = min((size_t)(q - p), sizeof line - 1);
memcpy(line, p, len); line[len] = '\0';
/* trim start */
char *s = line;
while (*s == ' ') ++s;
/* discard echo, OK, ERROR */
if (!strncmp(s, "AT", 2) || !strcmp(s, "OK") || !strncmp(s, "ERROR", 5)) {
p = q;
continue;
}
/* found payload */
strncpy(dst, s, dstSz - 1);
dst[dstSz - 1] = '\0';
return true;
}
return false;
}
The singleLineAT() routine encapsulates all the work of firing an AT command and returning exactly the first useful line of the response, already cleaned of echoes, line breaks, extra spaces, and control tokens.
- Firing and success check:
if (!sendAT(cmd)) return false;reusessendAT()to send the command and fillrespBuf. If anERRORor timeout occurs, the function returnsfalseright away. - The
piterator walks the raw buffer:char *p = respBuf; while (*p) { ... }scans the buffer until it finds a useful line. - Skipping line breaks:
while (*p == '\r' || *p == '\n') ++p;ensurespalways points to the first real character of the current line. - Finding the end of the current line:
char *q = p; while (*q && *q != '\r' && *q != '\n') ++q;defines the line’s[p, q)range. - Copying to a temporary buffer using
memcpy()and limiting the size to avoid overflow. - Copying the payload to the destination with
strncpy(dst, s, dstSz - 1); dst[dstSz - 1] = '\0';. - If no useful line appears, the loop ends naturally and the function returns
false.
Embedded Best Practices — Fixed Buffers Instead of String
| Benefit | Field Consequence |
|---|---|
| Zero heap fragmentation | Uptime without unexpected resets |
| Deterministic execution time | No random realloc pauses |
Conversion and Cleaning Functions
| Function | Purpose |
|---|---|
ddmmToDecimal() |
GPS: converts NMEA ddmm.mmm coordinates → decimal degrees. |
csqToDbm() |
Converts 0-31/99 → dBm (-113 to -51). |
cregText() |
Code 0-5 → text “registered (roaming)”. |
techText() |
0/1/2/7/8 → 2G/3G/4G/CDMA. |
csvField() |
Extracts field n from "CMD: a,b,c". |
stripQuotes() |
Removes quotes and spaces at the ends. |
All of these functions take pointers or primitive values for memory optimization.
Roadmap
Setup
- Opens USB and the modem UART.
- Enables automatic GPS (
AT+CGPSAUTO=1). - Sets numeric operator display (
AT+COPS=3,2).
Loop
- If
"INFO"arrives over USB, firessendInfoJSON()and prints the JSON. - Otherwise, works as a transparent PC ↔ modem serial bridge.
The sendInfoJSON() function runs the AT command sequence, converts metrics, and builds the final JSON.
Generated Fields and Value for Telecom
| Field | Practical Use |
|---|---|
imei |
Asset inventory, device (modem) id. |
imsi |
Asset inventory, simcard id. |
spn |
Carrier’s commercial name. |
csq / rssi |
Antenna diagnostics and coverage quality. |
reg / ran |
Registration state and technology (2G/3G/4G). |
ip / apn |
Data context validation (PDP/Bearer). |
location |
Georeferencing. |
The full JSON fits in ~300 bytes — ideal for transport over MQTT.
Tricks and Tips
- Reuse
respBuf— the same buffer serves both end-of-command detection and response analysis, avoiding duplicated traffic. - Universal
singleLineAT()— cleans echo and noise from any command that returns a single line. - Strings in Flash — frees 300+ bytes of SRAM, which is critical on AVR.
Suggested Next Steps
- Publish via MQTT/TLS.
- Manage power — turn off GPS (
AT+CGPS=0) outside reading windows. - Implement FOTA — remote firmware updates straight to the module.
Conclusion
This is the first step: understanding the “AT” dialogue and turning it into business data. In the next article, we’ll see how to transmit this telemetry securely and at scale to the cloud.