August 10, 2025
Description
We used two main hardware components:
The ESP32 acts as a middleman between the legacy serial connection on the Mahr device and a computer via USB. It sends queries and parses incoming measurements, outputting them in a format suitable for logging or analysis.
Why did we build this? Because the original cable from the supplier costs €111 — mostly due to a built-in FTDI controller — and the official software only allows one data request per second, which is far too slow for practical use.
To solve this, we reverse-engineered the communication protocol between the MahrConnect software and the Mahr Extramess 2001. We discovered that the software only looks for a specific device name on the FTDI controller. With that insight, we used a generic UART device to snoop on the serial communication and determine the required signals and commands.
By analyzing the signal and documentation, we identified the following communication parameters:
Baud rate: 4800
Data bits: 7
Parity: Even
Stop bits: 2
The protocol accepts specific ASCII commands terminated with a carriage return (\r). Key commands include:
| Command | Function |
|---|---|
RES1\r | Set measurement range (preset 1) |
RES2\r | Set measurement range (preset 2) |
RES3\r | Set measurement range (preset 3) |
TOL?\r | Query tolerance settings |
SET?\r | Query current status |
?\r | Query current measurement value |
RST\r | Reset and deactivate ABS mode |
BAT?\r | Query battery status |
MAX\r | Show maximum measurement |
MIN\r | Show minimum measurement |
OFF\r | Power off the device |
ABS\r | Activate absolute measurement mode |
These commands allowed us to interact with the device in a fully controlled way.
We used an ESP32-C3 Super Mini — one of the smallest and most cost-effective microcontrollers — to act as a bridge between the legacy serial interface and a modern computer via USB.
The ESP32 was programmed to:
Send a measurement query ?\r via Serial1 every 5 milliseconds.
Listen for a response terminated by \r.
Parse and clean the result, extracting only the numeric measurement value (in mm).
Output the timestamp and value via USB serial in a CSV-friendly format.
The serial was configured with:
mySerial.begin(4800, SERIAL_7E2, 21, 20); // RX = GPIO21, TX = GPIO20
To monitor the serial data, we used CoolTerm, which lets us capture and save the output to a .txt file for later processing in Excel.
In practice, we achieved a sampling rate of approximately 14.5 Hz, a significant improvement over the original 1 Hz limit imposed by the official Mahr software.
While the code attempts to poll the device every 5 ms (equivalent to 200 Hz), the actual rate is limited by the serial protocol and device response time. At 4800 baud with a 7E2 configuration (7 data bits, even parity, 2 stop bits), each character transmission requires 11 bits, and the measurement response spans several bytes. Factoring in processing delays and timing overhead, the system achieves about 14–15 readings per second.
We also observed that polling faster than every 10 ms interferes with the device’s front panel buttons — likely due to resource contention inside the device firmware. For this reason, the polling interval was set to 5 ms in software.
Despite that, this setup provides a much denser data stream than the original software and is fully sufficient for most logging and analysis tasks.
The code on the ESP32C3:
// Use Serial1 for UART communication
// RES1, RES2, RES3 + \r --> Set measurement range
// TOL?\r --> Query tolerance
// SET?\r --> Query status
// ?\r --> Request measurement value
// RST\r --> Reset and disable ABS mode
// BAT?\r --> Query battery status
// MAX\r --> Display maximum value
// MIN\r --> Display minimum value
// OFF\r --> Power off
// ABS\r --> Switch to absolute mode
// Use CoolTerm to read the output from the serial interface
HardwareSerial mySerial(1);
bool dataRecived = false;
void setup() {
Serial.begin(115200);
mySerial.begin(4800, SERIAL_7E2, 21, 20); // UART setup RX/TX
Serial.println("ESP32 UART MahrConnect Extramess 2001");
}
void loop() {
// Send request
mySerial.write("?\r"); // or just "?" if carriage return (CR) is not required
// Wait for response (max. 500 ms)
String message = "";
float startTime = millis();
while (millis() - startTime < 300) {
// If data is available, read it until a carriage return ("\r") is received. This marks the end of the data set.
if (mySerial.available()) {
char c = mySerial.read();
message += c;
if(c == '\r') {
break;
}
}
}
// If the message contains data and is not an error, print it with the corresponding timestamp
if(message == "ERR0\r" && dataRecived) {
Serial.println(".");
}
if (message.length() > 0 && message != "ERR0\r") {
float currentTime = millis() / 1000.0;
String formatedTime = String(currentTime, 3);
formatedTime.replace(".", ",");
Serial.print(formatedTime);
// Separator for easier table saving
Serial.print(";");
// Remove all whitespace
message.trim();
// Get the index of "mm" in the string, so only the numeric data is extracted
// Since different resolutions result in different string lengths, using the index is more reliable
int index = message.indexOf("mm");
message.replace(".", ",");
Serial.println(message.substring(0, index));
dataRecived = true;
}
else {
dataRecived = false;
}
// The delay determines the sampling rate. At a baud rate of 4800, theoretically ~434 measurements
// with 11 bits per dataset are possible per second. However, below 10 ms the buttons on the device
// stop functioning, so we chose a sampling interval of 5 ms.
delay(5);
}License:
Creative Commons — Attribution
8