Skip to content

thegridelectric/modbus-experiments

Repository files navigation

Modbus Experiments

Modbus is a de facto standard serial request/response protocol used in industrial control since the 1980s. This document investigates the use of Modbus over half duplex RS485. Half duplex 485 uses 3 wires (A+, A- and Common), but acts like only a single wire, since A+ and A- are a differential pair and Common is ground. This means there are no control signals and only one device may drive signals on the bus at a time. As consequences, only one client may be present on the bus, only the client may initiate conversation, servers must only respond to messages with their device ID and the client must wait for a response from each request.

Serial Modbus ("Modbus RTU") has an extension to IP networks via Modbus TCP. This essentially wraps the serial protocol into messages exchanged over a TCP stream. This allows an entire Modbus serial bus to be available at an IP address via a Modbus gateway, and conveniently allows Modbus protocol to be sniffed by, for example, Wireshark.

This document summarizes various Modbus experiments, in particular:

  • Setting up a Modbus mini lab
  • A Modbus sniffing kit
  • Connecting Modbus devices to automatable hubs such as Hubitat

Shopping lists

Bus Hardware

These are useful for setting up a Modbus bus:

Experiments described here used both MacOS and Raspberry Pi clients, both connected over serial and IP. Both serial and IP connectivity might be used in different contexts. Connectivity equipment includes:

  • Waveshare ModbusTCP gateway: purchase, web page and manual.
  • Waveshare USB to RS485 dongle: purchase and manual. Two might be useful to both speak on and sniff a serial-only bus.
  • USB-C/USB-A cable for connecting Macintosh to USB dongle.
  • USB-A extension cable for connecting Pi to USB dongle when a different dongle, such as the [Zooz Z-Wave dongle] blocks access.

Sniffing hardware and software

In a diagnostic situation either IP or serial sniffing might be required. For IP sniffing an ethernet hub that supports Port Mirroring is necessary if the sniffing is not done from the client machine.

For serial sniffing, display of raw hex bytes is easy via pyserial, but useful parsing of messages is non-trivial since there is not obvious packet framing - a parser must check find a frame boundary by parsing each byte as the potential start of a request or a response. Instead, I recommend existing Modbus parsing software. I like IONinja, but it is not free after a 14 day eval. There are other tools available. I found Serial Port Monitor unsatisfactory. The SerialTool free version might be worth a try.

In addition to the client connectivity listed above,

Test devices

It is useful to have cheap and simple Modbus devices to experiment with. Experients described here were carried out with. I did experiments with:

Instructions

A simple read

Start with a really simple script in which the use of pymodbus to interact with a device is very directly visible. In this case, we will read the temperature from the Taidecent thermometer.

  1. Wire the Taidecent thermometer to power and to the RS485 data bus. The Taidecent thermomenter does not use the RS485 Common wire.

  2. Wire one USB serial dongle to the RS485 data bus. The dongle gets power from the USB connection.

  3. Connect the USB serial dongle to the USB-C/USB-A cable, and connect the USB-C/USB-A cable to your macintosh.

  4. Clone this repo, Create virtual environment, activate it, and install the prequisites.

    git clone https://github.com/thegridelectric/modbus-experiments.git
    cd modbus-experiments
    poetry install
    poetry shell
    pre-commit install 

    This will install pymodbus, pyserial, a simple script, a cli for more involved experiments (mbe) and development tools. To just run the simple script all you would need to do is:

    git clone https://github.com/thegridelectric/modbus-experiments.git
    cd modbus-experiments
    python -m venv .venv
    source .venv/bin/activate
    pip install pyserial pymodbus rich
  5. Find your serial port:

    ls /dev | grep tty.usbserial

    You should see an entry such as:

    tty.usbserial-B001K2B8
    

    in which case your serial port will be:

    /dev/tty.usbserial-B001K2B8
    
  6. Modify mbe/script.py to contain your actual serial port.

  7. Run mbe/script.py:

    python mbe/script.py

    You should see output such as:

    Read device 1, memory address 0x00  (Temperature)  Raw:2757  27.57°C  81.63°F
    

    This code boils down to:

    import time
    from pymodbus.client import ModbusSerialClient
    client = ModbusSerialClient("/dev/tty.usbserial-B001K2B8", baudrate=9600)
    client.connect()
    time.sleep(.2)
    resp = client.read_holding_registers(address=0, slave=1, count=1)
    print(resp.registers[0])

    If you have errors, the most likely sources are:

    • Serial port name is wrong.
    • Device id ('slave') is not configured to 1 on the Taidecent (it should be by default).
    • Faulty wiring or bus power.

    If that does not resolve questions, see next section.

Simple sniffing

Even if you don't have errors it is instructive to watch the raw bytes on the bus. You can do that with a second USB serial dongle:

  1. Wire the second USB serial dongle to the RS485 data bus.

  2. Connect the second USB serial dongle to the other USB-A connector on the USB-C/USB-A cable.

  3. Find the name of the second serial port:

    ls /dev | grep tty.usbserial

    You should see something like:

    tty.usbserial-B001K2B8
    tty.usbserial-B001K6G3
    

    Note which is the old one and which is the new one.

  4. Open a new terminal (correct this command for the actual second port):

    cd modbus-experiments
    poetry shell
    mbe sniff --port /dev/tty.usbserial-B001K6G3

    You should see something like:

    Sniffing on: RS485<id=0x100ca3400, open=True>(port='/dev/tty.usbserial-B001K6G3', 
    baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=False, 
    rtscts=False, dsrdtr=False)
    

    If you do not, check your config with:

    mbe config

    and also consider error sources listed above.

  5. Now in the first terminal, run the simple script again:

    python mbe/script.py

    In the first terminal you should see something like:

    Read device 1, memory address 0x00  (Temperature)  Raw:2758  27.58°C  81.64°F
    

    and in the second terminal you should see something like:

    01030000  0001840A  0103020A  C63EB6
    

    What does this mean? Since we saw the temperature printed, we know there was a request and a response, but because there is no framing we don't know where the request ends and the response begins. We could parse it from the spec but it's much faster to use a parser. A quick compromise is to make a guess and then use this awesome online Modbus parser from Rapid SCADA. If we paste all those bytes into the parser we get an error. But without carefully parsing the request and response we can note that non-exotic requests starts with one byte of device id and one byte of function code and the response mirrors that. Both have a two byte CRC at the end which is likely to look like "some random hex number". With that in mind we can glance at the bytes and guess that:

    request:  01030000  0001840A  
    response: 0103020A  C63EB6
    

    We can now paste each of those into the online Modbus parser and get a sensible explanation of data exchange. This is too cumbersome for interesting conversations. For those we need a real parser like Wireshark or IONinja. Wireshark is free, so we will start with that. To watch Modbus with Wireshark, we need to set up the Waveshare ModbusTCP gateway.

The Waveshare ModbusTCP gateway allows us to communicate to a Modbus serial bus over TCP. To use the Waveshare gateway we need choose static IP or DHCP or DHCP with a network name reservation. I will describe the latter since it is practical and robust. As Rod McBain notes, the Waveshare gateway needs to be configured with Waveshare's vircom windows program. In order to use Wireshark to sniff communication to the gateway, the machine running Wireshark must see the traffic going to the gateway. Here we assume the only traffic going to the gateway is coming from the machine running wireshark, so we do not need a special network switch or to configure port mirroring.

  1. Attach the gateway to RS485 data, to power, and to Ethernet.
  2. Download and install Vircom on a Windows machine.
  3. Start Vircom.
  4. Press "Device"
  5. The factory setting gateway will be at 192.168.1.254. Press "Auto Search" if the gateway isn't present.
  6. Double click on the row for the gateway.
  7. Note the "Dev Name" field. This is the gateway's network name. Confirm that with:
    # replace "DEV_NAME_SHOWN" with value of Dev Name field.
    ping DEV_NAME_SHOWN.local
  8. Log into your home router. In the section that configures the LAN, add an address reservation for that network name. Save your changes.
  9. Make these changes in vircom:
    • Change "IP Mode" to DHCP.
    • Set the baud rate to 9600.
    • Set the "Transfer Protocol" to "Modbus_TCP Protocol"
    • Press "Modify Setting"
  10. Wait a moment. If the IP address does not change to the value of your reservation, stop stop all Vircom windows, cycle power on the gateway and restart Vircom. Vircom should now show the expected IP address. You cross check by pinging the network name and by logging into your home router and looking at devices attached to the LAN.
  11. Once the gateway has the desired IP, stop all Vircom windows. You can now put away your Windows machine.
  12. Test that Modbus is working via the gateway by modifying mbe/script.py so that serial = False and host has the correct IP address.
  13. Run
    python mbe/script.py
    You should see the temperature printed as before. If you have errors, check that the IP address is correct. Another likey source of errors is the gateway wiring. You can get also information about your gateway connection by setting serial = True and noting what behavior changes. If that does not resolve questions, see next section.

Wireshark sniffing

Even if you don't have errors it is instructive to watch communication using Wireshark.

Note that Wireshark uses two kinds of filters, with two different filter languages:

  • Capture filters control which packets are actually captured. You select capture filters when you start capturing. They can reduce the size of captured data.
  • Display filters control which packets are displayed during capture. They can be changed during capture.

If you don't expect IP connection issues, a capture filter will reduce clutter. On the other hand, if you are having trouble connecting, it might be better to use no capture filter and use display filters to try to better understand what is happening. A comparison of simple filters:

capture: host 192.168.1.210
display: ip.addr==192.168.1.210

Setup and usage:

  1. Install Wireshark.
  2. Start Wireshark.
  3. Choose which network interface to capture on (e.g. "Wi-Fi en0").
  4. If you think your script execution found the right IP address, use a capture filter with the IP address of the gateway such as
    host 192.168.1.210
    
    If you are having IP connectivity issues, use no capture filter.
  5. Start the capture.
  6. In a terminal, run the script:
    python mbe/script.py
    You should see a bunch of rows in Wireshark containint at least the entire TCP connection used by the script. If you can successfully reach the gateway you should see two rows with Protocol of Modbus/TCP. If you select the "Query" row, you should see a break down of the protocol stack used by that packet in the lower left hand corner. You would be able to click on "Modbus" in that breakdown and see a breakdown of the fields that will be put on the serial bus. Note there is no CRC; that is computed before putting the data on the bus; to see that you need to sniff on the actual serial port.

If you are having connectivity issues you might want to start with no capture filter and instead use display filters to remove unrelated packets from view, at least until you've concluded that you and the gateway agree about its IP address. You can also filter by MAC address since you should be able to figure out the MAC address of the gateway. You might want to try pinging the device while the capture is running.

Configuring multiple devices

If multiple servers are present on a Modbus bus, they must have different device IDs. Generally devices ship with a the same default ID (1), so you have to change their device IDs. How to change their device ID must be documented by the device. It is typically done by writing a register. We will use the mbe cli to change device address, which just calls write_register() on the pymodbus client.

Check out the mbe cli:

mbe --help
mbe config --help
mbe tai --help
mbe rly --help

Check out the current config:

mbe config

Config can also be changed by editing config.json file or with the config command. For example, this command sets the IP address of the gateway to 192.168.1.210 and specifies ModbusTCP, not serial, should be used.

mbe config --host 192.168.1.210 --no-serial

Now we change the device IDs of the Taidecent and Waveshare Relays so they can co-exist on the bus:

  1. Using mbe config or the config.json file, verify serial or TCP mode is specified and host IP and serial port are set up as expected.
  2. Change the device ID of the Taidecent:
    mbe tai set-device-id --from-id 1 --to-id 2
  3. Wire the Waveshare relay board to to power and to the RS485 data bus.
  4. Configure Waveshare relay device ID to default value:
    mbe config --waveshare-relay-device-id 1
  5. Verify you hear at least one click when you run:
    mbe rly set-all 0
    mbe rly read
    mbe rly set-all 1
    mbe rly read
  6. Change the device ID of the relay board:
    mbe rly set-device-id --from-id 1 --to-id 3
  7. Verify you can interact with both devices at their new addresses:
    mbe run

IONinja sniffing

There are time when it is preferable to sniff the RS495 bus directly, for example because the client does not or cannot communicate ModbusTCP. A trivial serial program such as mbe sniff can do this, but it does not provide useful parsing. IONinja is program that parses Modbus serial communication and provides a nice visual display similar to Wireshark. IONinja is not free but it has a free eval and a reasonable subscription price (note there is also one time $35 cost for the serial plugin when upgrading the eval).

To set up IONinja:

  1. Download and install IONinja.
  2. Create a free account on IONinja.
  3. Sign up for the eval.
  4. When you start IONinja, choose "Sign in online".
  5. You might need to follow instructions to enter the key for the eval.

Sniffing:

  1. Start IONinja
  2. File / New Session / Serial
  3. File / Layer Pipeline / Add / Modbus Analyzer
  4. Choose a serial port
  5. Set:
    • Baud rate: 9600
    • DTR: off
    • Protocol: ModbusRTU
    • Streams: Half duplex RX
  6. Session / Open (you must do this each time)

To generate experimental traffic:

  1. Verify mbe is configured to communicat using a different mechanism:
    mbe config
    # Check that "mode" and "serial" or "tcp" are as you desire.
  2. If necessary configure mbe to communicate on a different serial port or on a ModbusTCP gateway. For example:
    # To enable serial on port /dev/tty.usbserial-B001K2B8:
    mbe config --serial --port /dev/tty.usbserial-B001K2B8
    # Or, to enable tcp on host 192.168.1.210:
    mbe config --no-serial --host 192.168.1.210
  3. Generate a simple request / response with:
    mbe tai read 

You should be able to click on the '+' icon in each row to get a detailed parsing of that packet.

Port mirroring

Wireshark, but it can only show packets that your computer sees. If you trying to watch TCP communication between a ModbusTCP client and ModbusTCP gateway, and the client is an actual other device, not software running on your computer, there is a good chance that the LAN hardware the client, the gateway and your computer will not present packets between the client and the gateway to your computer. This is primarily for efficiency reasons, but also for security.

This problem is solvable using a network switch that supports Port Mirroring, assuming you can reconnect the Ethernet cable of at least one monitored device to the new swich, and connect the new switch to the existing LAN. We verified this set up with a GS108ev3 from NetGear.

To set up this sniffing Ethernet link

  1. Connect the GS108ev3 to your LAN.
  2. Enable port mirroring on the switch per the GS108ev3 manual. Basically:
    • Determine the IP address of the switch by logging into your router or using the NetGear discovery tool
    • Log onto the switch using a browser.
    • Change the switch password.
    • Follow instruction in the manual to enable port mirroring to one of the ports of the switch (I chose port 8). For efficiency and security only one port receives the packets mirrored from other ports.
  3. Connect either or both of the client and the gateway to the GS1018ev3.
  4. Connect your sniffing computer to the port on the swich you configured to receive mirrored packets.

About

Notes on testing and sniffing Modbus

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages