Skip to content

Add SIM800L GSM/GPRS cellular driver#49

Merged
floitsch merged 3 commits intomainfrom
floitsch/sim800l
Mar 25, 2026
Merged

Add SIM800L GSM/GPRS cellular driver#49
floitsch merged 3 commits intomainfrom
floitsch/sim800l

Conversation

@floitsch
Copy link
Copy Markdown
Member

Add support for the SIMCom SIM800L 2G module, following the existing driver architecture (Quectel, Sequans, u-blox).

Key implementation details:

  • GSM/GPRS only: uses +CGREG instead of +CEREG for registration
  • SIMCom TCP/IP stack: AT+CSTT/CIICR/CIFSR for GPRS bearer, AT+CIPSTART/CIPSEND/CIPRXGET for sockets
  • Manual data receive mode (AT+CIPRXGET=1) for clean AT parsing
  • Async DNS via AT+CDNSGIP
  • Multi-connection mode (up to 6 simultaneous sockets)

Also extends the AT session to support prefixed terminations (e.g. "0, SEND OK") used by SIM800L in multi-connection mode.

Add support for the SIMCom SIM800L 2G module, following the existing
driver architecture (Quectel, Sequans, u-blox).

Key implementation details:
- GSM/GPRS only: uses +CGREG instead of +CEREG for registration
- SIMCom TCP/IP stack: AT+CSTT/CIICR/CIFSR for GPRS bearer,
  AT+CIPSTART/CIPSEND/CIPRXGET for sockets
- Manual data receive mode (AT+CIPRXGET=1) for clean AT parsing
- Async DNS via AT+CDNSGIP
- Multi-connection mode (up to 6 simultaneous sockets)

Also extends the AT session to support prefixed terminations
(e.g. "0, SEND OK") used by SIM800L in multi-connection mode.
Add comments referencing the SIM800 Series AT Command Manual V1.09
and Hardware Design V1.00 to all AT commands and hardware interactions
in the driver code, to help reviewers verify correctness.

main:
// Enable the board's power management IC.
power-control := gpio.Pin --output 23
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have this happen when something connects to the network.
That would require subclassing the existing class.
Investigate.

If you find a solution, don't forget to update the README.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the Olimex PoE Ethernet pattern, the power IC is now managed at the service provider level. Sim800lTCallService overrides open-network/close-network to enable/disable GPIO 23. Tested with two concurrent clients — power IC stays on while any client is connected, shuts off when the last disconnects.

done

The SIM800L operates at **3.7-4.2V** and can draw up to **2A** during
transmission bursts. Make sure your power supply can handle this.

- Do **not** power the module from the ESP32's 3.3V pin.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More importantly: don't power it with the 5V.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

- Do **not** power the module from the ESP32's 3.3V pin.
- A LiPo battery (3.7V, 1200mAh+) or a 2A-capable DC-DC converter is recommended.
- Some boards (like the LilyGO T-Call) include a power management IC.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add a note that the 3.3V can't go directly to the module for the RX.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

## Limitations

- 2G (GSM/GPRS) only — no LTE support.
- Maximum 6 simultaneous TCP/UDP connections.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does our driver handle things when there are more connections?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The driver throws ResourceExhaustedException when all 6 slots are in use (tested on hardware). Updated README.

done


- 2G (GSM/GPRS) only — no LTE support.
- Maximum 6 simultaneous TCP/UDP connections.
- Maximum ~1460 bytes per send operation.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question: what happens if we try to send something bigger? Do we chunk correctly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TCP try-write_ chunks automatically (if to - from > MAX-SIZE_: to = from + MAX-SIZE_). UDP throws if over MTU. Updated README.

done

connect_:
// On SIM800L, the connection is established after CIPSTART returns OK.
// The "<id>, CONNECT OK" notification is not a standard URC and is
// handled by the sleep in the constructor.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of this. Is there no way to wait for this? We can change the 'at.toit' if necessary.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed together with the comment above.

done

if to - from > MAX-SIZE_: to = from + MAX-SIZE_
data = data.byte-slice from to

e := catch --unwind=(: it is not UnavailableException):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to assign to a local if we don't do anything with it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

cellular_.sockets_.remove id

mtu -> int:
return 1500
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this related to the MAX-SIZE?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are related but distinct: MTU (1500) is the IP-level maximum, MAX-SIZE_ (1460) is the SIM800L per-AT-command data limit (≈ MTU minus TCP/IP headers). Added a clarifying comment.

done

"$address.port",
]
// Wait for CONNECT OK.
sleep --ms=3000
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for the tcp socket. Can we monitor the TX somehow?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed together with comment on simcom.toit:94.

done

else:
parts

// CCID response parser (ICCID is too large for int).
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean?
ints in Toit are 64 bit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded: ICCIDs are 19-20 digits. A 64-bit signed int holds up to ~9.2×10^18 (19 digits), so 20-digit ICCIDs overflow. The custom parser reads the value as a string to avoid inconsistent behavior from the default int-parsing fallback.

done

// Wait for CONNECT OK.
sleep --ms=3000
// Wait for the async "<n>, CONNECT OK" response.
cellular_.wait-for-urc_: state_.wait-for CONNECTED-STATE_
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we risk to miss the message because we do the socket-call first and then start waiting?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No risk of missing the message. The socket is registered in sockets_ before the connection is initiated (UDP: udp-open registers, then user calls connect; TCP: tcp-connect registers, then calls connect_). The on-unhandled-line callback sets CONNECTED-STATE_ as soon as CONNECT OK arrives. If it arrives before wait-for-urc_ starts, the state bit is already set and state_.wait-for returns immediately. done

cellular_.at_.do: | session/at.Session |
if not session.is-closed:
session.set "+CIPCLOSE" [id]
catch:
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this catch really necessary?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch around CIPCLOSE handles the case where the remote side already closed the connection (<n>, CLOSED notification was received). Without it, +CME ERROR: operation not allowed is thrown. We hit this in testing with concurrent clients. done

@floitsch
Copy link
Copy Markdown
Member Author

TBR.

@floitsch floitsch merged commit 0df706b into main Mar 25, 2026
2 checks passed
@floitsch floitsch deleted the floitsch/sim800l branch March 25, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant