..

Sending SMS with USB Modem

Imagine a Linux computer installed in a bunker and controlling a doomsday machine. This control computer is fully authonomous and has no internet connection. The only way it (sometimes) communicate with the outside world is by GSM modem. If controlling computer discovers that the machine mailfunctions or needs a service, it sends a SMS to a service engineer. The engineer then comes and fix the machine. The bunker was sealed 20 years ago and was long forgotten … until recently a team of archeologists discovered it.

That’s how I know about it, because shortly after I was commissioned to update the Linux distribution on the control computer. You see, doomsday machine or not, according to the insurance policy no past-end-of-life OS is allowed in any bunker πŸ˜„


Well, maybe the story I just told you is slightly exaggerated πŸ˜…, but the overall sense is right.

On top of the new OS we also got to update the modem itself, because the world does not stay still and hardware becomes obsolete as much as software. Of course with both OS and modem update we need to be sure that everythign works as before.

The good news is that telecom standards live long and sending SMS through AT commands works in 2025 exactly the same as it worked in 2005 … or does it?

Traditional Way

The idea is simple. There are so-called AT commands, a plain-text telnet-style protocol, and it’s possible to send SMS using a sequence of AT commands. Literally all modems from the 80-s to the present day understand AT commands, and our only problem should be to find a way to send them to the modem and get the response back.

My modem is Teltonika TRM240 with a badass-looking ruggedized case and external antenna, but it connects to PC (and gets power) through a regular USB and in many other aspects similar to a consumer-grade USB dongles.

modem

So, how do we send SMS? Teltonika wiki conveniently has a page with a step-by-step manual https://wiki.teltonika-networks.com/view/SMS_sending_command Let’s take a look.


When you connect the modem through the USB, the OS detects it and creates several /dev/ttyUSBx devices to interact with the modem.

A device enumerated in the USB bus is able to expose in the system multiple ports, so there is no limitation on the number of control ports or data ports that can be found. USB modems with multiple control ports are very common, e.g. allowing different operations to be run in parallel in the different ports

β€” ModemManager docs

After connecting the modem it’s a good idea to check that the modem shows up in the lsusb:

Bus 003 Device 010: ID 2c7c:0121 Quectel Wireless Solutions Co., Ltd. EC21 LTE modem

power consumption

β€” Sometimes USB port can’t give modem enough power, so having a USB hub with external power is a good idea

Quectel EC21 is the name of the chip that my modem is based on:

modem PCB

It’s useful to know because you can refer to the chip manufacturer documentation about the supported AT commands.

As I said, the modem is represented by several /dev/ttyUSBx devices, in my case by four different devices:

$ sudo dmesg | grep tty
[  620.750378] usb 3-3: GSM modem (1-port) converter now attached to ttyUSB0
[  620.750539] usb 3-3: GSM modem (1-port) converter now attached to ttyUSB1
[  620.750623] usb 3-3: GSM modem (1-port) converter now attached to ttyUSB2
[  620.750679] usb 3-3: GSM modem (1-port) converter now attached to ttyUSB3

Now we need to find out (by trial and error, I guess, the manufacturer wiki is vague about it) which tty device is responsible for AT commands. In our case it’s /dev/ttyUSB2.

Then the manual suggests connecting to this port using either screen or minicom, e.g.

sudo minicom -D /dev/ttyUSB2 -b 115200

and getting an interactive shell with the modem:

AT commands

If you are curious, minicom is a reimplementation of some ancient utility from the times when connecting to the terminal was done through physical RS232. That said, neither screen or minicom are necessary, we can do everything just using printf and cat (or by read and write to the file descriptor if you work from some other language) 🀷

printf and cat

Result 1:

result 1

What you’ve just read is the “classical” way to send SMS through a modem. It’s described in wikis, blog posts, and that’s what ChatGPT will suggest you if you ask.

It works, but when I started to experiment with it more, I discovered that it works glitchy.

Sometimes modem refused to switch to the echo mode (ATE1). Sometimes echo worked, but some characters disappeared. Sometimes AT commands hiccup-ed or stopped working altogether.

My first thought was that I messed up with terminal device settings (which is the pretty arcane topic), but then I quickly realized that the reason is more simple: something was accessing the same tty device in parallel with me:

$ sudo fuser /dev/ttyUSB2
/dev/ttyUSB2:         2041

and this “something” is ModemManager.

$ ps aux | grep 2041
root        2041  0.0  0.0 392540 14824 ?        Ssl  13:40   0:01 /usr/sbin/ModemManager

ModemManager

ModemManager is a system daemon which controls WWAN (2G/3G/4G/5G) devices and connections.

Alongside NetworkManager, ModemManager is the default mobile broadband management system in most standard GNU/Linux distributions (Debian, Fedora, Ubuntu, Arch Linux…), and is also available in custom systems built with e.g. buildroot, yocto/openembedded or ptxdist.

ModemManager may also be used in routers running openwrt (integrated with netifd as a new protocol handler), or to manage both voice and data in mobile phones running postmarketos (PINE64 PinePhone, BQ Aquaris X5, OnePlus 6…).

β€” https://modemmanager.org/

It’s not a surprise that ModemManager … well, manages /dev/ttyUSBx devices for USB modems. Honestly, I’m not even sure why it needs the access to the AT commands because studying debug logs shows that pretty much evertything is done using QMI. But apparently it polls some data through AT commands and direct reading/writing /dev/ttyUSBx clash with ModemManager and break things.

There are several workarounds that let us send SMS with AT commands the “traditional way”. I will list them all, but if you are interested in how to do things the proper way (or, better to say ModemManager way), you can jump straight to the Sending SMS with ModemManager.

Workaround A (hostile): Disabling ModemManager

… by masking ModemManager.service

The dumbest and most straightforward way is to disable and mask ModemManager.service. In my case it was actually an option, because as I said control computer has only one modem and we use it in a primitive way, so ModemManager is unnecessary.

Of course, disabling a system component that was put there by your distribution maintainers doesn’t feel good. Also, what if your use case goes beyond sending and receiving SMS? I’m not sure that I would like to figure out how to manually set up IP connectivity with my modem without the help of ModemManager.

… by preventing ModemManager from managing our modem with udev rules

It’s a little less brutal option than disabling the whole ModemManaging.service and in some cases can be a legit solution, for example when ModemManager mistakes some other device with a USB modem. People who use something like RS232 development boards are often stuck in this problem, so there’s even an official (tm) solution from the ModemManager author himself.

The downsides are mostly the same as in the previous option. Plus, I’d like to minimize the amount of tweaks I apply to the OS.

… by inhibiting the modem

Inhibiting the modem means taking it out of ModemManager temporarily. I’m not sure what is the intended use of this feature, but it’s quite simple (-m 0 is the index of the modem):

$ sudo mmcli -m 0 --inhibit
successfully inhibited device with uid '/sys/devices/pci0000:00/0000:00:14.0/usb3/3-2/3-2.3'
type Ctrl+C to abort this program and remove the inhibition

This will temporarily take the modem out from ModemManager and we will be able to peacefully chat with it using /dev/ttyUSBx.

For some time I was seriously considering this option because on one hand it doesn’t need any OS-level tweaks and on the other lets me reuse the legacy SMS sending script with minimum modifications.

What made me discard this option is the fact that after uninhibiting, the ID of the modem changes (increments). In principle, it’s not a big deal, but I felt uneasy about it. I’d like to run the control computer uninterruptible for as long as possible, but I’m not sure if this incremental ID consumes any system resource.

Another downside is that inhibiting requires admin rights (access to the tty device itself can be achieved by joining the dialout group).

Workaround B (compromise): Sending Raw AT Commands through ModemManager

Turns out that it’s possible to send raw AT commands through ModemManager using mmcli. This way we can simply replace printf "<at-command>" > /dev/ttyUSB2 from the example above with the mmcli -m 0 --command "<at-command>".

The downside is that this option works only when ModemManager works in the debug mode, i.e. we would need to create systemd unit file override and add there ExecStart=/usr/sbin/ModemManager --debug. As I said earlier, I’d like to avoid tweaking OS as much as possible, but in principle, it can be an option. Also, it’s not a surprise, but this flag makes ModemManager shit tonnes of debug info into the systemd journal, so you might need to consciously consider journal storage and rotation policies if you end up using this in the production.

Sending SMS with ModemManager

Probably you already suspected that there should be an option to deal with the modem (including sending SMS and much more!) through the ModemManager itself. And you are right.

The official CLI to interact with the ModemManager is mmcil. I need to send SMS from a bash script, so it’s good for me, but if you need to communicate with ModemManager from some other language (e.g. Python or C++) and you hate shelling out, there’s a D-Bus interface (it’s how mmcli talks to ModemManager.servcie) and some language wrappers.

But let’s focus on the mmcli.

Sending SMS is a simple and straightforward process. First, we need to find out the ID of our modem:

$ mmcli --list-modems
/org/freedesktop/ModemManager1/Modem/0 [QUALCOMM INCORPORATED] QUECTEL Mobile Broadband Module

Btw, don’t rely on the fact that if you have only one modem, the ID is always 0.

By default mmcli outputs information in the human-readable form that is not very convenient for parsing, but there’s a -J option that makes mmcli output data in JSON:

$ mmcli --list-modems -J
{"modem-list":["/org/freedesktop/ModemManager1/Modem/0"]}

For the sake of readability I’ll keep using human-readable form, but keep in mind that in the scripts I always use -J.

After we figured out the ID of the modem we can query the information about it:

$ mmcli -m 0
  ----------------------------------
  General  |                   path: /org/freedesktop/ModemManager1/Modem/0
           |              device id: cafebabecafebabecafebabecafebabecafebabe
  ----------------------------------
  Hardware |           manufacturer: QUALCOMM INCORPORATED
           |                  model: QUECTEL Mobile Broadband Module
           |      firmware revision: EC21ECGAR06A10M1G
           |         carrier config: default
           |           h/w revision: 10000
           |              supported: gsm-umts, lte
           |                current: gsm-umts, lte
           |           equipment id: 123456789101112
  ----------------------------------
  System   |                 device: /sys/devices/pci0000:00/0000:00:14.0/usb3/3-2/3-2.3
           |                physdev: /sys/devices/pci0000:00/0000:00:14.0/usb3/3-2/3-2.3
           |                drivers: option, qmi_wwan
           |                 plugin: quectel
           |           primary port: cdc-wdm0
           |                  ports: cdc-wdm0 (qmi), ttyUSB0 (ignored), ttyUSB1 (gps),
           |                         ttyUSB2 (at), ttyUSB3 (at), wwan0 (net)
  ----------------------------------
  Status   |                   lock: sim-pin2
           |         unlock retries: sim-pin (3), sim-puk (10), sim-pin2 (3), sim-puk2 (10)
           |                  state: registered
           |            power state: on
           |            access tech: lte
           |         signal quality: 100% (recent)
  ----------------------------------
  Modes    |              supported: allowed: 2g; preferred: none
           |                         allowed: 3g; preferred: none
           |                         allowed: 4g; preferred: none
           |                         allowed: 2g, 3g; preferred: 3g
           |                         allowed: 2g, 3g; preferred: 2g
           |                         allowed: 2g, 4g; preferred: 4g
           |                         allowed: 2g, 4g; preferred: 2g
           |                         allowed: 3g, 4g; preferred: 4g
           |                         allowed: 3g, 4g; preferred: 3g
           |                         allowed: 2g, 3g, 4g; preferred: 4g
           |                         allowed: 2g, 3g, 4g; preferred: 3g
           |                         allowed: 2g, 3g, 4g; preferred: 2g
           |                current: allowed: 2g, 3g, 4g; preferred: 4g
  ----------------------------------
  Bands    |              supported: egsm, dcs, utran-1, utran-8, eutran-1, eutran-3, eutran-7,
           |                         eutran-8, eutran-20, eutran-28
           |                current: egsm, dcs, utran-1, utran-8, eutran-1, eutran-3, eutran-7,
           |                         eutran-8, eutran-20, eutran-28
  ----------------------------------
  IP       |              supported: ipv4, ipv6, ipv4v6
  ----------------------------------
  3GPP     |                   imei: 123456789101112
           |          enabled locks: fixed-dialing
           |            operator id: 24412
           |          operator name: FI DNA
           |           registration: home
           |   packet service state: attached
  ----------------------------------
  3GPP EPS |   ue mode of operation: csps-2
           |    initial bearer path: /org/freedesktop/ModemManager1/Bearer/1
           | initial bearer ip type: ipv4
  ----------------------------------
  SIM      |       primary sim path: /org/freedesktop/ModemManager1/SIM/1

There’s a lot of information as you see, including signal strength, SIM lock status and operator name.

It’s quite interesting that in this mode I don’t need PIN to send SMS, despite the lock status being “fixed dialing”. Anyways, if you need PIN to unlock the SIM, it can be done by 2:

mmcli -m 0 --pin <PIN>

Sending SMS is a two steps process. First, we need to create SMS:

$ mmcli -m 0 --messaging-create-sms="text='Hellow from mmcli',number='+358123123123'"
Messaging | created sms: /org/freedesktop/ModemManager1/SMS/8

And then send it:

$ mmcli -s 8 --send
successfully sent the SMS

Result:

result 2

Conclusion

In the end, I’d like to note it’s not all rosy with using ModemManager. The ModemManager API does not cover 100% of what you can do through raw AT commands (although, the opposite is also true and not everything that is possible through ModemManager is possible through AT commands).

Even for such a simple task as sending an SMS, ModemManager is not always a 100% replacement. For example, sending SMS through AT commands does not put them into storage area before sending. May be storing the SMS before sending is a better way, but on the other hand you need to care about deleting sended SMS afterwards and this complicates the logic. But the most limiting is that mmcli doesn’t allow creating SMS in PDU mode, the --messaging-create-sms accepts only the text. So, if you want (or have to) work in the PDU mode, you might need to stick to one of the workarounds I’ve described before. But for the simple use cases MM is just perfect.


  1. Getting rid of > in the last example is left as an excersise for the reader πŸ˜„. ↩︎

  2. In case of AT commands it would be AT+CPIN↩︎