Arduino Mega 2560 + SIM7600G: Cellular IoT Node with JSON Telemetry

by Müller | Nov 15, 2025 | Hardware & IoT | 0 comments

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;
  • constexpr guarantees compile-time evaluation;
  • #define flags 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; reuses sendAT() to send the command and fill respBuf. If an ERROR or timeout occurs, the function returns false right away.
  • The p iterator 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; ensures p always 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, fires sendInfoJSON() 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.

Table of Contents