VoCore2 Ultimate as SMS gateway

This article describes a setup of OpenWRT on the VoCore2 Ultimate to send SMS via a HTTP API using smstools and a GSM modem (a Huawei E3531, in this case). This method uses docker to build a custom OpenWRT image, which can be then be installed via the firmware update mechanism in the preinstalled OpenWRT on the ROM of the VoCore2.

Project origin

As my personal servers at home and in the datacenter grow and grow, I have long since started to do monitoring of these VMs, physical servers and services and of course soon got the need to not just alarm me via email, but also via some out of band mechanism, in case the networking is unavailable (be that due to a configuration issue, power loss to the router, mail server being down, etc.). SMS is a good choice for this, as I can access it on even a basic cell phone and sending them from the datacenter or home works even when networking is unavailable, and it doesn’t require to put another wire in, like with a phone line (which are by now obsolete and been replaced with VoIP and therefore internet connections, anyway).

This monitoring was originally done via Nagios in a VM and it connected to the smstools running on the KVM hypervisor using a limited SSH account, sending the SMS over one of those cheap USB sticks with a GSM/UMTS modem. In 2016 I replaced this with a Raspberry Pi 2 B running Zabbix for a number of reasons™ (the pi was looking for something to do and I wanted to play with Zabbix). A few years later, the monitoring itself moved to containers running on another device and the Pi only ran smstools and an apache webserver with mod_cgi to expose a python script that wrote the messages into file in the outgoing queue folder, where the smstools daemon would pick it up and send it.

That Raspberry Pi, even though it didn’t really have any high CPU load started to crash about every 2 to 3 weeks and only after I did connect it’s syslog to a central syslog instance did I get those last kernel messages about it detecting undervoltage conditions did I learn that I had to replace the power supply. A few months later it started to have another condition, during summer, where it occasionally overheated. Again, I only could deduce this via the central syslog instance, as in those crashing conditions it often isn’t able to write the logs to disk/SD card before it fails. But the UDP packages still got out and helped me figure out what caused the crashes.

When the system thats supposed to alert you in emergencies fails and you then don’t get notified about it and only find out later, when something more obvious fails and you wonder why you didn’t get notfied, it kind of defeats the point of having that system. At that point I had the VoCore2 with no more purpose then being impressed at having such a small and compact Linux system. So the plan was hatched to migrate from the flaky Pi to the VoCore before the next summer would cause another series of instabilities.


  • VoCore2 Ultimate, which is the version that includes the dock with the 100 Mb/s ethernet port, USB-A port (for the GSM modem) and the micro-USB port for powering it. This should also work with a bare VoCore2 and WiFi connectivity, if you are willing to solder on your own USB-A port and some kind of power connector.
  • Either an Ubuntu 18.04 system or Docker (or podman or similar), to compile your custom OpenWRT image on.
  • A network to run the VoCore2 in and internet connectivity on the build system to download the sources, install the packages, etc.
  • A USB stick with GSM/3G/UMTS modem and Linux support – it is a bit tricky to figure this out, best is to research some cheap options that are currently available to you and then google around to find out if people had success using that model and/or check if there is support in the USB-modeswitch utility for it.
  • A SIM-card in a format supported by your modem stick including a paid plan that includes sending SMS. Many people seem to use a really cheap prepaid card for this, but if you want to avoid the hassle of charging the card every once in a while, you may find a low cost plan that suits your need. I got the cheapest plan from one of my local providers that happens to include an unlimited amount of SMS.


Getting the modem recognized by the Kernel

My initial attempt at this, last year, got as far as setting up the OpenWRT’s networking, mostly via the luci web interface. I had installed a zabbix agent and smstools3 packages, but then discovered that the usb-serial module wasn’t compiled in or included as loadable module. Normally you can just install missing kernel modules from the package sources, but the VoCore2 runs with a custom kernel, so I couldn’t load the modules packaged by the OpenWRT in it, even if they are for the same version of the kernel.

In a nutshell, I needed to re-build the VoCore2 OpenWRT image, but with usb-serial and usb-serial-wwan modules included. While I was at it, I also included all the configuration files and scripts that I wanted to be baked in and also the zabbix agent and smstools packages. The latest guide on the build process can be found in the OpenWRT docs.

The process to install all dependencies, download the source code and put all the modifications in place (into a folder called „files“) can be seen in the following Dockerfile:

FROM ubuntu:18.04

RUN apt-get update && \
    apt-get install -y \
        build-essential gawk git libncurses5-dev libncursesw5-dev libssl-dev ncurses-term python python-dev unzip wget zlib1g-dev \
    && \
    rm -rf /var/lib/apt/lists/*
RUN adduser --disabled-password --gecos "VoCore2 Builder" --uid 1000 --home /home/build --ingroup users build

USER build:users
WORKDIR /home/build

RUN git clone -b v18.06.5 --depth 1 https://github.com/openwrt/openwrt.git

WORKDIR /home/build/openwrt

RUN ./scripts/feeds update -a && \
    ./scripts/feeds install -a && \
    git clone --depth 1 https://github.com/vonger/vocore2.git package/vocore2

RUN patch -p1 -i package/vocore2/mt7628/openwrt/000-*.patch && \
    patch -p1 -i package/vocore2/mt7628/openwrt/luci/*.patch && \
    mkdir package/network/utils/iwinfo/patches && \
    cp package/vocore2/mt7628/openwrt/080-*.patch package/network/utils/iwinfo/patches && \
    cp package/vocore2/config-4.14 target/linux/ramips/mt76x8/

COPY --chown=build:users .config /home/build/openwrt/.config
COPY --chown=build:users files /home/build/openwrt/files

RUN make -j1 V=s download
RUN make -j4 V=s

Since this uses the Ubuntu 18.04 image, you can replicate this on an ubuntu install without docker. For reference, the full build context and Dockerfile can be found here: vocore2-smstools.zip

To create a custom .config file, skip the last make and instead run make menuconfig in your build environment, choosing the options that you need. Finally you run the crosscompile using make (single CPU core) or make -j<number of CPU cores>.

Note that the VoCore2 only has a 10 MiB ROM. The result in my case was about 4.5 MiB, but don’t go overboard adding tons of packages.

The image can be uploaded to OpenWRT and installed, either via the luci web interface or ssh and command line. After a reboot, modprobe/lsmod should confirm that the module is now available and can be loaded.

Getting the modem into the right mode

After you plug in you modem, you will usually find that dmesg tells you about some storage device or CD drive being detected, instead. This is a common mechanism in these devices to provide drivers on the stick to Windows or MacOS – the driver would, upon successfull installation, switch the USB stick into modem mode. On Linux this isn’t needed and just a hassle and we have to use another piece of software that issues the neccesary mode switch to the device to get it’s modem functionality.

The full details on setting up the usbmode switch utility is described over in the USB modeswitching guide of the OpenWRT project. While I already knew what code to send to my stick from the Raspberry Pi setup, I had to translate this into the JSON format used by the OpenWRTs tool. I baked this json configuration into the image as well.

If you did it correctly, dmesg should notify you about a newly detected USBtty or ttyACM device shortly after you plug in the USB stick.

Introducing the modem to smstools

Again, this was a configuration that I could just replicate from the former setup on the Pi. It really is as simple as pointing the smsd at the right device file and providing the PIN code to unlock the SIM card (no, 1234 is not my PIN code). It may be necessary to add the phone number of the SMS service center of your provider, but this usually gets configured via a configuration SMS sent by the provider to your modem upon unlocking the SIM card for the first time and gets persisted on the SIM card. Further details (including what kernel modules get used) can be found in a guide at the Onno Center Wiki on smstools on OpenWRT.

Exposing smstools‘ outgoing queue to the web

On the Raspberry Pi the apache web server (and therefore the CGI python script it launched) and the smsd service ran as different users and I had set up sudo rules to allow the www-data user to run a particular shell script as the smstools user, which created and moved the SMS file into the outgoing folder to be picked up by smsd for processing. Also the smstools user had to be in the dialout group to be allowed to access the modem.

On OpenWRT, at least by default, both the webserver (uHTTPd) and smsd run as the root user. Hence I could consider to solve this all in one script. But python isn’t installed by default and wouldn’t fit into the 10 MiB ROM of the VoCore2. The luci webinterface is provided using lua as a CGI script, so I did rewrite my python and shell scripts into a single lua script:


print ("Content-type: text/plain; charset=utf-8\n")

local info = os.getenv("QUERY_STRING")
local params = {}

for name, value in string.gmatch(info .. '&', '(.-)%=(.-)%&') do
    value = string.gsub(value, '+', ' ')
    value = string.gsub(value, '%%(%x%x)', function(dpc)
        return string.char(tonumber(dpc, 16))
    end )
    params[name] = value

if params["phone"] ~= "" and params["phone"] ~= nil and params["message"] ~= "" and params["message"] then
    tmp = os.tmpname ()
    file = io.open (tmp, "a")
    io.output (file)
    io.write ("To: " .. params["phone"] .. "\n\n" .. params["message"])
    io.close (file)
    os.rename (tmp, "/var/spool/sms/outgoing/" .. tmp:match("^.+/(.+)$"))
    print ("done")
    print "The GET parameters \"message\" and/or \"phone\" are missing."

It accepts two GET parameters for message and recipient phone number, writes the file into the format accepted by smsd and moves it into outgoing queue (this avoids that smsd picks it up before the file is fully written). The argument parsing from QUERY_STRING is a bit weak, but it can handle URL-encoded non-ASCII characters, if they are UTF-8 encoded. One can send SMS via the gateway like this:

curl -G -s "http://<hostname or IP of the VoCore2>/cgi-bin/sms" --data-urlencode "phone=<phone number of recipient>" --data-urlencode "message=<message including spaces or UTF-8 characters>"

On another system using just busybox‘ wget implementation, URL encoding has to be done manually or externally:

wget -q -O - "http://<hostname or IP of the VoCore2>/cgi-bin/sms?phone=<phone number of recipient>&message=<message%20with%20encoded%20spaces>"


I am very pleased with the result. The white case was part of the kickstarter tier that I had backed and it looks really nice and compact with the (accidentally) matching white USB stick – the stick is larger then the computer. 🙂

The lua script is very short and so far OpenWRT is running very stable on the new device – over a month of uptime and 60 SMS sent, so far. Let’s see how it deals with the hot summer months.

Edited 2021-06-22: Extended the lua script with spaces as plus sign encoding support, still runs great.

Discussion Area - Leave a Comment