mbusd: a Modbus TCP to Modbus RTU gateway for serial field buses
Open-source Modbus TCP to Modbus RTU (RS-232/485) gateway.
At a glance
- What is it?
- mbusd is a small C daemon that presents a network of Modbus RTU slaves as a single Modbus TCP server, so a TCP master can talk to RS-232 and RS-485 devices without a gateway box, with configurable serial timing, three ways of controlling RS-485 direction and retries for mismatched CRCs. It suits integrators who need to bridge a legacy serial bus to a TCP network on a Raspberry Pi or other small board, and the trade is that it deliberately covers only nine function codes and passes everything else through on a best-effort basis.
- Who is it for?
- Adopt mbusd when you have RS-232 or RS-485 Modbus devices that need to be reachable over Modbus TCP and you would rather run a daemon on your own hardware than buy a gateway.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 15 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One TCP endpoint in front of a whole RTU bus
mbusd is an open-source Modbus TCP to Modbus RTU, RS-232 and RS-485, gateway, and the README states its role precisely: it presents a network of RTU slaves as a single TCP slave, that is a TCP slave or server which acts as an RTU master to get data from Modbus RTU slave devices.
So the topology is a proxy, not a converter in the usual sense. A Modbus TCP master, a supervisory system or a SCADA client, connects to mbusd on TCP port 502 and addresses a register. mbusd takes that request, puts it on the serial line as an RTU master, waits for the slave to answer, and returns the answer over TCP. The RTU side of the world, which cannot have two masters on the same bus, is hidden behind one TCP endpoint that any number of masters can share.
That multi-master property is listed as a feature, and it is the feature that decides whether you need this at all. Multiple TCP masters can access slave devices in the RTU network using the same gateway, which is exactly the situation a serial bus cannot handle on its own.
The other features are about the serial side. The footprint is small, suitable to run on embedded devices and SBCs like Raspberry Pi, so the gateway can live next to the equipment rather than in a cabinet. The RTU mode is flexible, with speed, parity, stop bits and timeouts all configurable for the RTU network. Direction control for RS-485 transceivers supports both automatic and manual, using the RTS bit. And there is optional local echo handling for RS-485 adapters that receive their own transmissions.
The code is C, the licence is BSD-3-Clause, the last push was on 2026-10-08, and the releases are 0.5.3 from 2026-04-07, 0.5.2 from 2024-03-27 and 0.5.1 from 2022-08-18.
Nine function codes, and an honest note about the rest
The supported function codes are listed explicitly, and the list is short enough to be worth reading as a decision criterion rather than as an implementation detail.
The supported set is 01 read coil status, 02 read input status, 03 read holding registers, 04 read input registers, 05 force single coil, 06 preset single register, 07 read exception status, 15 force multiple coils and 16 preset multiple registers.
So the reads and writes that a supervisory system normally performs are all there. What is missing is everything else: the diagnostics, the file transfer codes, the vendor-specific extensions that some meters and inverters use, and the masking or read-multiple-register variants where a device offers them.
And the documentation is direct about that. Please note all other function codes, including vendor-specific extensions, are supported on a best-effort basis and most likely will fail.
That sentence matters more than it might look, because a Modbus TCP master is allowed to send anything and a gateway that silently drops the rest will look like a device that does not answer. The behaviour is at least predictable, and the function codes that are covered are the ones that make a gateway useful in the first place. If your master only polls registers and writes coils, the coverage is complete for your purposes. If it reads an energy meter's vendor extension block, you need to know before you deploy.
The repository layout also has a `tests/` directory and a `doc/` directory, and the top level entries include a `cmake/` directory for the build configuration, a `conf/` directory holding the example configuration file, a `systemd-units/` directory and a `CHANGELOG.md`.
The project also keeps a GitLab CI configuration alongside the GitHub Actions badge, so the tests run in both places.
Building with CMake, and the flags that matter
The installation is a standard CMake out-of-source build, and the commands are short enough to be worth keeping as a block.
$ git clone https://github.com/3cky/mbusd.git mbusd.git
$ cd mbusd.git
$ mkdir -p build && cd build
$ cmake -DCMAKE_INSTALL_PREFIX=/usr ..
$ make
$ sudo make installThe compile time options can be altered in many ways, for example by using ccmake, usually in the package cmake-curses-gui, or cmake-gui, usually in the package cmake-qt-gui, and the README says to use the following tools in the build directory.
The runtime flags are where the real configuration lives, and the usage block is long enough that the important ones deserve naming.
The serial side is `-p device` for the serial port device name, `-s speed` for the serial port speed, and `-m mode` for the serial port mode, like 8N1. There is also `-c cfgfile` to read the configuration from a file, with a complete list of available configuration options in an example configuration file in the conf directory.
The TCP side is `-A address` for the TCP server address to bind, default 0.0.0.0, and `-P port` for the TCP server port number, default 502. `-C maxconn` sets the maximum number of simultaneous TCP connections, default 32, and `-b` instructs mbusd to reply on a broadcast.
The timing options are the ones you will actually change on a real bus. `-R pause` is the pause between requests in milliseconds, default 100ms. `-W wait` is the response wait time in milliseconds, default 500ms. `-T timeout` is the connection timeout value in seconds, default 60, where 0 disables the timeout. And `-N retries` is the maximum number of request retries, default 3, where 0 disables retries. That last one connects directly to the retry feature, the ability to retry requests with mismatched response CRC, which matters because a noisy RS-485 line will occasionally produce a frame that fails its checksum.
Logging is `-L logfile`, where a dash means logging to STDOUT only and a relative path or bare filename is stored at /var/log, defaulting to /var/log/mbusd.log, and `-v level` sets the verbosity from 0 for errors only through 2 for informational messages, with a debug build going up to 9 where higher levels add information about additional internal events. `-d` instructs mbusd not to fork itself, which is the non-daemonize mode you want in a container or under a supervisor.
One operational note is easy to miss: running mbusd on the default Modbus TCP port, 502, requires root privileges.
RS-485 direction control, three ways and two polarities
The RS-485 half of the configuration is where a gateway like this usually goes wrong, so the three options deserve setting out.
The first is `-S`, which enables RS-485 support for the given serial port device, Linux only. The second is `-t`, which enables RTS RS-485 data direction control using RTS in active transmit mode, and `-r`, the same thing in active receive mode. The third is the sysfs route, where `-y sysfsfile` enables direction control by writing '1' to the sysfs file for transmitter enable and '0' to the file for transmitter disable, and `-Y sysfsfile` does the opposite, writing '0' for transmitter enable and '1' for transmitter disable.
Note the inversion between `-y` and `-Y`. Two flags for the same mechanism with opposite polarities is a usability trap, and it exists because the polarity depends on the transceiver. A USB RS-485 adapter that exposes a GPIO for transmitter enable expects one convention and one that expects the other will see the opposite. Getting it wrong means the transceiver never asserts transmit, and the symptom is that the gateway sees its own requests echoed back or sees nothing at all.
There is a related option for the echo problem. `-e` discards local echo of transmitted RTU requests, and the feature list describes optional local echo handling for RS-485 adapters that receive their own transmissions. On a half-duplex bus, an adapter that loops its own transmission back into the receive path produces a frame that looks like a slave response, and the gateway has to filter it out.
That filtering interacts with the retry logic. If the local echo were mistaken for a response, the CRC would not match, and the retry mechanism would then re-send the request, which would produce another echo. Discarding the echo first is what keeps the CRC retry from looping.
The upshot is that getting an RS-485 deployment right is a matter of three settings, the direction control method, the polarity, and whether to discard the local echo, and the documentation gives you all three but the correct combination depends on the adapter.
systemd units per serial port, and a container image
mbusd has systemd support, and the build system detects whether the system has systemd, after which sudo make install installs the [email protected] file on systems with systemd active. The template unit design is what makes multiple gateways practical.
Each instance is named after the serial port it serves. The service is started with:
# systemctl start mbusd@<serial port>.servicewhere the serial port is the escaped serial port device short name, like ttyUSB0 for a /dev/ttyUSB0 device or serial-rs485 for a /dev/serial/rs485 device name. An mbusd started by systemd reads its configuration from a file named /etc/mbusd/mbusd-<serial port>.conf, and this way it is possible to run multiple mbusd instances with different configurations.
So if you have two RS-485 buses with different baud rates and different slave address ranges, you get two units, two configuration files and two ports, and systemd keeps them separate. The status and monitoring commands follow the same pattern, systemctl status mbusd@<serial port>.service to see the status and journalctl -u mbusd@<serial port>.service -f -n 10 to monitor the service, and systemctl enable mbusd@<serial port>.service to start it on system boot.
The Docker route is a single privileged container. The command in the README is:
docker run -d --privileged \
--name=mbusd \
-p 502:502 \
-v /dev:/dev \
-v /path/to/mbusd.conf:/etc/mbusd.conf \
3cky/mbusd:latestThe --privileged flag and the /dev mount are both required in practice, because the daemon needs to open a serial device and the platform's cgroup rules for USB serial adapters are restrictive. The configuration file is mounted from the host.
The Dockerfile behind that image is a two stage Alpine build. The build stage installs alpine-sdk, cmake and linux-headers, copies the source, and runs the same cmake and make sequence as the manual build. The runtime stage copies only the /usr/bin/mbusd binary into a fresh Alpine image, with the entrypoint set to run it in the foreground with logging to stdout and the config file at /etc/mbusd.conf.
Editorial conclusion
Adopt mbusd when you have RS-232 or RS-485 Modbus devices that need to be reachable over Modbus TCP and you would rather run a daemon on your own hardware than buy a gateway. Do not adopt it if your master relies on function codes outside the nine listed, because everything else is passed through on a best-effort basis and will most likely fail, and check first that the serial port device and the RS-485 direction method match your adapter, since the daemon supports RTS control and sysfs control in opposite polarities and getting that wrong means the transceiver never transmits.
Frequently asked questions
What does mbusd do?
It is an open-source Modbus TCP to Modbus RTU gateway that presents a network of RTU slaves as a single TCP slave. It is a TCP server which acts as an RTU master to get data from Modbus RTU slave devices over RS-232 or RS-485.
Which Modbus function codes does mbusd support?
Nine: 01 read coil status, 02 read input status, 03 read holding registers, 04 read input registers, 05 force single coil, 06 preset single register, 07 read exception status, 15 force multiple coils and 16 preset multiple registers. All other function codes, including vendor-specific extensions, are supported on a best-effort basis and will most likely fail.
How do I build and install mbusd?
Clone the repository, create a build directory, run `cmake -DCMAKE_INSTALL_PREFIX=/usr ..`, then `make` and `sudo make install`. The build also supports ccmake and cmake-gui for changing the compile time options.
How do I control RS-485 direction in mbusd?
Three ways are supported: `-S` for Linux RS-485 support on the serial device, `-t` and `-r` for RTS based control in active transmit and active receive, and `-y` or `-Y` for sysfs control with opposite polarities, writing 1 or 0 to the file for transmitter enable. `-e` discards local echo of transmitted requests.
How do I run mbusd as a service?
The build installs a systemd template unit, [email protected], when systemd is detected. You start an instance with `systemctl start mbusd@<serial port>.service`, where the serial port is the escaped device short name such as ttyUSB0, and each instance reads its own config from /etc/mbusd/mbusd-<serial port>.conf, so several buses can run with different settings.
Is mbusd still being maintained?
The repository is not archived and the last push was on 2026-10-08. The releases are 0.5.3 from 2026-04-07, 0.5.2 from 2024-03-27 and 0.5.1 from 2022-08-18, and the code is BSD-3-Clause licensed with tests and CI in both GitHub Actions and GitLab CI.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/3cky-mbusd)Was this useful?