The M-HAT: More than just a cellular modem for Linux devices
Ready to build your IoT product?
Create your Particle account and get access to:
- Discounted IoT devices
- Device management console
- Developer guides and resources
The Particle M-HAT
Designing a fleet of Linux-powered IoT devices means balancing connectivity, power efficiency, and functionality. For remote deployments where Wi-Fi isn’t available, an embedded Linux device will often be combined with an off-the-shelf cellular modem. However, in environments in which power is limited or intermittent and a device must rely on solar or battery power, the power requirements of this combination can become a challenge to optimize.
The Particle M-HAT and complementary B504 or B524 cellular modules provide an excellent alternative to this traditional architecture. This combination enables a low-bandwidth Cat 1 cellular uplink, and additionally unlocks a new hybrid computational approach leveraging the Linux host for processor-intensive tasks while the Particle module takes care of low-power monitoring. This architecture is ideal for use cases where continuous operation isn’t necessary, such as monitoring a vehicle that may be idle for long periods of time, collecting data from remote sensors, or building event-driven systems.
More than just a modem
There are several cellular modems available on the market that are compatible with Linux devices, often in USB or M.2 form factors. While easy to use, these products deliver only basic connectivity, leaving users to figure out how to manage communication, monitoring, and power efficiency on their own. Additionally with these devices it is likely required that Linux host to stay fully powered in order to respond to events or to communicate with the cloud.
The M-HAT is different; it can both deliver connectivity to a Linux host as well as acting as a coprocessor. A UART interface between the two acts both as an internet connection as well as a a point-to-point communication interface with the Particle module’s processor. The Particle module can operate independent of the Linux host — monitoring for events, reading data from sensors, or managing the state of the entire system — all while providing internet connectivity to the Linux host device, or doing so while the Linux host is asleep.
This enables the Particle module to be more than just a modem; instead it is a fully-capable coprocessor in addition to providing connectivity. Whether reducing power consumption in a solar-powered sensor deployment or enabling efficient event-driven processing in a vehicle, the M-HAT keeps integration simple while unlocking new capabilities for your IoT device.
Establishing a host-to-module network connection
In addition to providing a Tethering interface for host internet connectivity, Particle’s embedded operating system, Device OS, has APIs available for socket-level communication over a module’s network interface. This can be done using either TCP or UDP.
Often, these are used to establish a connection with devices on a local network, for example communicating with a PLC via Modbus TCP. In our case, we will be using them bound to the Tether interface, enabling them to function over the connection between the Linux host and Particle module.
The Tether interface runs over a UART connection between the Particle module and host. The PPP data link protocol is used to provide an IP layer over the UART. This architecture is shown below:
Using TCP
We’ll focus on using the module as the server in this case. If you wish the module to act as a client, the TCPClient API documentation contains examples of how to set that up.
First, set up the Tether interface and then instantiate a TCPServer on the newly-created Tether network interface:
#include "Particle.h"
#define SERVER_PORT 8091
TCPServer server = TCPServer(SERVER_PORT, Tether);
void setup() {
// waitUntil(Serial.isConnected); // for debugging
Cellular.on();
Cellular.connect();
// Enable host tethering for internet connectivity
Tether.bind(TetherSerialConfig().baudrate(460800).serial(Serial1));
Tether.on();
Tether.connect();
// Bring up TCP server
server.begin();
Log.info("TCP server up on port %u", SERVER_PORT);
}
Next, a function is needed to receive information from connected clients:
void process_tcp() {
TCPClient client = server.available(); // returns nullptr if no client
if (client) {
Log.info("Client with IP %s is connected via TCP!", client.remoteIP().toString().c_str());
while (client.available()) {
String data = client.readStringUntil('\n');
data.trim();
Log.info("Recevied: %s", data.c_str());
}
}
}
void loop() {
process_tcp();
}
Using UDP
Using UDP similar to the TCP example, but the instantiation and receive methods are different.
Getting the module IP address
These steps require that a Tethering connection has been set up and established between the module and the Linux host. Please refer to the Tethering documentation for more information.
Two methods exist to determine the Particle module’s IP address:
- Using the
localIP()
Device OS API:
Log.info("Local IP: %s", Tether.localIP().toString().c_str());
- Using ifconfig :
ppp0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST> mtu 1500 inet 10.223.223.2 netmask 255.255.255.255 destination 10.223.223.1 ppp txqueuelen 3 (Point-to-Point Protocol) RX packets 92307 bytes 60041213 (57.2 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 84737 bytes 28212162 (26.9 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
In this case, the Raspberry Pi has assumed the IP 10.223.223.2
and the Particle module (”destination”) is at 10.223.223.1
.
Note: the name of your network interface may differ depending on how you have set it up.
Talking over the interface
Now that we have our Particle module running a TCP server on port 8091 and we know the module’s IP address, a message can be sent from the command line and the module should echo back the message sent.
echo -n "Hello world!" | nc 10.223.223.1 8091
Full example
Building on the simple examples above, the instructions below will set up the Particle module to act as a TCP server and accept commands from the Linux host.
Command structure
Command Description | Command Syntax | Response Syntax |
---|---|---|
Cellular Connect (+ on, if off) | CCON | OK |
Cellular Disconnect | CDIS | OK |
Cellular Off | COFF | OK |
Get connection vitals | CSTA | STA,<ON OFF> ,<RDY NRDY> ,<RSRP> ,<RSRQ> |
Particle Publish | CPUB,"<TOPIC> ","<STR> " | PUB,OK: success PUB,FAIL: failed to publish PUB,NOT_CONNECTED: no connection to the Particle cloud PUB,INVALID_PUBLISH_FORMAT: incorrect command format |
Particle firmware
#include "Particle.h"
SerialLogHandler dbg(115200, LOG_LEVEL_ALL);
SYSTEM_MODE(SEMI_AUTOMATIC);
TCPServer server = TCPServer(8091, Tether);
void setup() {
// waitUntil(Serial.isConnected); // for debugging
Cellular.on();
Cellular.connect();
Tether.bind(TetherSerialConfig().baudrate(460800).serial(Serial1));
Tether.on();
Tether.connect();
// Particle.connect(); // Optional
// TCP server
server.begin();
Log.info("TCP server up on port 8091");
}
String process_command(const String &command) {
// Turn on cellular and initiate data connection
if (command.equalsIgnoreCase("CCON")) {
if (Cellular.isOff()) {
Cellular.on();
}
Cellular.connect();
return "OK\n";
}
// Disconnect, but leave cellular on
else if (command.equalsIgnoreCase("CDIS")) {
Cellular.disconnect();
return "OK\n";
}
// Disconnect and turn off cellular
else if (command.equalsIgnoreCase("COFF")) {
Cellular.disconnect();
Cellular.off();
return "OK\n";
}
// Get cellular connevtivity metrics
else if (command.equalsIgnoreCase("CSTA")) {
return String::format("STA,%s,%s,%0.0f,%0.0f\n",
Cellular.isOn() ? "ON" : "OFF",
Cellular.ready() ? "RDY" : "NRDY",
Cellular.getSignal().getStrength(),
Cellular.getSignal().getQuality()
);
}
// Particle publish (connect if necessary)
else if (command.startsWith("CPUB,")) {
// Extract topic and data from the command
int firstComma = command.indexOf(',');
int secondComma = command.indexOf(',', firstComma + 1);
if (firstComma > 0 && secondComma > firstComma) {
String topic = command.substring(firstComma + 1, secondComma);
String data = command.substring(secondComma + 1);
if (!Particle.connected()) {
Particle.connect();
}
waitFor(Particle.connected, 10000);
if (Particle.connected()) {
bool success = Particle.publish(topic, data);
Log.info("Publishing to topic \"%s\" with data \"%s\"", topic.c_str(), data.c_str());
return success ? "PUB,OK\n" : "PUB,FAIL\n";
}
return "PUB,NOT_CONNECTED\n";
} else {
return "ERR,INVALID_PUBLISH_FORMAT\n";
}
}
// Unknown command
else {
return "ERR,UNKNOWN_COMMAND\n";
}
}
void process_tcp() {
TCPClient client = server.available(); // returns nullptr if no client
if (client) {
Log.info("Client with IP %s is connected via TCP!", client.remoteIP().toString().c_str());
while (client.available()) {
String command = client.readStringUntil('\n');
command.trim();
String response = process_command(command);
if (response.length() > 0) {
client.write((uint8_t*)response.c_str(), response.length());
}
}
}
}
void loop() {
process_tcp();
// Status reporting of Tethering interface
static bool tether_ready = false;
if (!tether_ready) {
if (Tether.ready()) {
Log.info("Tethering connection established:");
Log.info("Local IP: %s", Tether.localIP().toString().c_str());
Log.info("Subnet mask: %s", Tether.subnetMask().toString().c_str());
Log.info("Gateway IP: %s", Tether.gatewayIP().toString().c_str());
tether_ready = true;
} else { // not ready
static system_tick_t last_log = 0;
if (last_log == 0 || millis() - last_log > 30000) {
last_log = millis();
Log.info("Tethering enabled, connection not established");
}
}
} else { // tether_ready == true
if (!Tether.ready()) {
tether_ready = false;
}
}
}
Linux host Python script
The following script provides a simple CLI-style interface for the commands implemented by the module:
#!/usr/bin/env python3 import sys import socket SERVER_IP = "10.223.223.1" SERVER_PORT = 8091 BUFFER_SIZE = 1024 def send_command(command): """Send a command to the server and return the response.""" try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect((SERVER_IP, SERVER_PORT)) sock.sendall(command.encode()) response = sock.recv(BUFFER_SIZE) res = response.decode().strip() # print(res) return res except socket.timeout: return "Error: No response from server." except Exception as e: return f"Error: {e}" def get_status(): """Fetch and display status in a human-readable format.""" response = send_command("CSTA") # Assuming the response format: STA,<ON|OFF>,<RDY|NRDY>,<RSRP>,<RSRQ> try: prefix, power, connection, rsrp, rsrq = response.split(',') if prefix != "STA": raise ValueError("Unexpected response format") # Convert metrics to human-readable output output = ( f"Power: {power}\n" f"Connection: {'READY' if connection == 'RDY' else 'NOT READY'}\n" f"Signal Strength (RSRP): {rsrp}%\n" f"Signal Quality (RSRQ): {rsrq}%" ) return output except Exception as e: return f"Error processing status response: {e}" def connect(): return send_command("CCON") def disconnect(): return send_command("CDIS") def turn_off(): return send_command("COFF") def publish(topic, data): """Example command to handle Particle Publish with custom arguments.""" command = f"CPUB,{topic},{data}" return send_command(command) def main(): if len(sys.argv) < 2: print("Usage: pmodem.py {status|connect|disconnect|off|publish} [args...]") sys.exit(1) command = sys.argv[1].lower() commands = { "status": get_status, "connect": connect, "disconnect": disconnect, "off": turn_off, "publish": lambda: publish(sys.argv[2], sys.argv[3]) if len(sys.argv) >= 4 else "Usage: pmodem.py publish <topic> <data>" } if command in commands: result = commands[command]() print(result) else: print("Unknown command. Available commands: status, connect, disconnect, off, publish") if __name__ == "__main__": main()
The simple “CLI” style interface in the example above can be extended to include task initiation, sensor readings, status reports, and more. By setting up both a tethered internet connection and a point-to-point connection between the Particle module and Linux host, the Particle module can act as a fully-independent coprocessor and handle any offloaded tasks from the host.
Summary
The M-HAT expands the capabilities of Linux systems by delivering connectivity and an efficient, low-power coprocessor through a single hardware interface. With minimal hardware complexity and easy integration, the M-HAT can simplify the design of Linux-powered IoT devices and enable new use cases for remote and energy-constrained environments.