Three Modbus Nightmares and How WireTap Diagnosed Them

Field-tested troubleshooting patterns for CRC errors, address conflicts, and timing jitter on RS-485 buses. Each scenario includes real WireTap captures and root-cause analysis.

OrangeHorse Engineering Team 11 min read

You get the call at 4:30 PM. "The sensor readings are frozen." You drive to the site, open your laptop, and plug in. Nothing obvious. The PLC says "communication error." The sensor's power LED is on. The wiring looks fine โ€” same daisy-chain that worked for six months. Now what?

Every field technician has a version of this story. The three root causes that come up again and again on RS-485 buses are CRC errors (electrical), address conflicts (configuration), and timing jitter (firmware). This article walks through each one โ€” what it looks like in a WireTap capture, how to diagnose it, and how to fix it.

Scenario 1: The Ghost CRC Error

Symptom: A new Modbus sensor was added to an existing RS-485 daisy-chain. After installation, the PLC intermittently logs "checksum error" on both the new sensor and the one next to it on the bus. Other devices communicate normally.

You attach WireTap between the PLC and the bus, let it run for five minutes, and drain the capture. Immediately you see a pattern:

[TX]  2026-08-12T16:12:03.118442    8B  [01 03 00 00 00 03 05 CB]
[RX]  2026-08-12T16:12:03.170091   11B  [01 03 06 00 64 00 C8 01 2C D1 0E]
[TX]  2026-08-12T16:12:04.201234    8B  [02 03 00 00 00 03 04 1A]
[ERR] 2026-08-12T16:12:04.360456    8B  [02 03 06 00 64 00 C8 01]  โ€” CRC mismatch
[TX]  2026-08-12T16:12:05.301112    8B  [03 03 00 00 00 03 05 F5]
[RX]  2026-08-12T16:12:05.344556   11B  [03 03 06 00 64 00 C8 01 2C D2 38]
[TX]  2026-08-12T16:12:06.402345    8B  [02 03 00 00 00 03 04 1A]
[ERR] 2026-08-12T16:12:06.560789    8B  [02 03 06 00 64 00 C8 01]  โ€” CRC mismatch
[TX]  2026-08-12T16:12:07.501234    8B  [01 03 00 00 00 03 05 CB]
[RX]  2026-08-12T16:12:07.545678   11B  [01 03 06 00 64 00 C8 01 2C D1 0E]

What the capture tells you:

  • Slave 01 and 03 always respond cleanly. CRC passes every time.
  • Slave 02 is the new sensor. Every fourth or fifth response has a CRC mismatch.
  • The error frames contain 8 bytes โ€” the full response came through, only the CRC is wrong.
  • No partial frames (incomplete reads would show shorter RawError payloads).

Diagnosis: CRC errors on full-length responses rule out a framing problem (wrong baud rate would corrupt everything, not just one device). The pattern โ€” device 02 only, intermittent โ€” points to a physical-layer issue specific to that device or its cabling. In this case: the new sensor was installed at the end of the daisy-chain and the termination resistor was still on the old end device. Two terminations on a segment cause reflections that corrupt bits. Device 02 happens to sit at the worst reflection node.

Fix: Remove the termination resistor from the old end device. Verify with another five-minute capture โ€” CRC errors on device 02 drop to zero.

Key insight: A CRC error on a full-length frame means the data arrived but the checksum disagrees. That is almost always an electrical problem, not a protocol problem. If the frame were truncated, WireTap would record a RawError(bytes, "timeout") โ€” fewer bytes, no CRC to check. See the Frame Decoding guide for a byte-level breakdown of CRC validation.

Scenario 2: The Silent Slave

Symptom: After a firmware update, device at Modbus address 5 stops responding. The SCADA system logs "no response" every poll cycle. Other devices on the same bus work normally.

WireTap capture:

[TX]  2026-08-12T18:22:10.001234    8B  [05 03 00 00 00 01 94 4E]
[TX]  2026-08-12T18:22:11.101345    8B  [05 03 00 00 00 01 94 4E]
[TX]  2026-08-12T18:22:12.201456    8B  [05 03 00 00 00 01 94 4E]
[ERR] 2026-08-12T18:22:13.301567    0B  []                               โ€” timeout
[TX]  2026-08-12T18:22:13.401678    8B  [05 03 00 00 00 01 94 4E]
[TX]  2026-08-12T18:22:14.501789    8B  [05 03 00 00 00 01 94 4E]
[ERR] 2026-08-12T18:22:15.601890    0B  []                               โ€” timeout

What the capture tells you:

  • Requests to slave 05 are being transmitted โ€” the PLC is polling correctly.
  • No [RX] frames from address 05 at all. The requests go unanswered.
  • Two requests, then a timeout. This matches ClientOptions::with_reconnect(2, Duration::from_secs(2)) โ€” two retries, then the error surfaces.
  • Other slaves (not shown) continue to respond. The bus wiring is intact.

Diagnosis: A completely silent device on a working bus has three possible causes: wrong address, wrong baud rate, or dead firmware. Since this happened after a firmware update, the address is the prime suspect. Many Modbus devices store the slave address in flash; a firmware update can reset it to the factory default (usually 0x01).

Check by sending a broadcast read (address 0x00) โ€” if the device responds, it is alive but at the wrong address. Then scan addresses 1โ€“247 to find where it landed.

[TX]  2026-08-12T18:30:00.001234    8B  [01 03 00 00 00 01 84 0A]
[RX]  2026-08-12T18:30:00.045678    7B  [01 03 02 00 64 B8 5C]      โ† here it is

The firmware update reset the device to address 0x01, which happened to be assigned to the temperature sensor on the same bus. Now both devices share address 1 โ€” which brings us to the next scenario.

Scenario 3: The Midnight Glitch

Symptom: A water quality monitoring station runs 24/7. Every night between 2:00 AM and 3:00 AM, several sensors report CRC errors. The problem self-resolves by 3:30 AM. During the day, the bus runs flawlessly.

WireTap capture from the problem window:

[TX]  2026-08-12T02:14:33.001234    8B  [01 03 00 00 00 04 44 0B]
                                           โ† gap: 1.74 ms (normal at 19200 baud)
[RX]  2026-08-12T02:14:33.002974   11B  [01 03 08 ...]
[TX]  2026-08-12T02:14:33.004356    8B  [02 03 00 00 00 04 84 2B]
                                           โ† gap: 0.84 ms (below 3.5T threshold!)
[ERR] 2026-08-12T02:14:33.005196    5B  [03 03 00]                    โ€” incomplete frame
[TX]  2026-08-12T02:14:33.005378    8B  [03 03 00 00 00 04 C5 1A]
                                           โ† gap: 1.12 ms (still tight)
[RX]  2026-08-12T02:14:33.006498   11B  [03 03 08 ...]

What the capture tells you:

  • Inter-frame gaps are shrinking during the 2:00 AM window. Normal gap at 19200 baud is โ‰ฅ1.75 ms (3.5 ร— 11 bits / 19200). The capture shows gaps as low as 0.84 ms.
  • The tightest gap produces an incomplete frame โ€” the transport starts reading the response before the previous transmission finishes, catches only 5 bytes, and WireTap records a RawError.
  • The problem is time-correlated. It only happens at night. That rules out configuration errors (those would be constant) and points to an environmental factor.

Diagnosis: The 3.5-character silent interval is specified in the Modbus Serial Spec ยง1.4. At 19200 baud and above, it is a fixed 1.75 ms. Below 19200 baud, it scales with the character time. When frames are sent faster than this interval, devices cannot distinguish the end of one frame from the start of the next โ€” they merge, producing incomplete reads.

The night-time correlation was the clue: the monitoring station is near a river. At 2:00 AM, the temperature drops, condensation forms on the RS-485 terminal block inside the outdoor enclosure, and the increased capacitance on the bus lines slows the signal edges. What was a 1.75 ms gap at 25ยฐC becomes effectively shorter because the signal takes longer to rise to the logic threshold. The receiving UART sees frames as closer together than they actually are.

Fix: Replace the terminal block with a sealed connector, add a desiccant pack to the enclosure. Or reduce the polling rate. Or both.

Key insight: WireTap timestamps have microsecond resolution. Subtract consecutive frame timestamps and compare to the 3.5T threshold. A gap that is consistently below threshold is a firmware bug (polling too fast). A gap that is only below threshold during certain times of day or weather conditions is a hardware problem.

Building a Bus Doctor

These three scenarios share a common workflow: attach WireTap, collect frames, then run a diagnostic pass over the capture. Here is a self-contained diagnostic analyzer โ€” a "Bus Doctor" โ€” that flags the patterns we just saw:

use oms_modbus::*;
use std::sync::Arc;
use std::time::Duration;

/// Diagnostic analysis of captured Modbus traffic.
#[derive(Default)]
struct BusDoctor {
    total_frames: usize,
    crc_or_io_errors: usize,
    exceptions: usize,
    timeouts: usize,
    silent_slaves: std::collections::BTreeSet<u8>,
    tight_gaps: usize,
    prev_ts_us: u64,
}

impl BusDoctor {
    /// Process captured packets and accumulate findings.
    fn ingest(&mut self, packets: &[PacketRecord]) {
        for pkt in packets {
            self.total_frames += 1;

            // Timing analysis: flag gaps below 2 ms (3.5T at 19200 baud)
            if self.prev_ts_us > 0 {
                let gap_us = pkt.timestamp_us.saturating_sub(self.prev_ts_us);
                if gap_us < 2000 && gap_us > 0 {
                    self.tight_gaps += 1;
                }
            }
            self.prev_ts_us = pkt.timestamp_us;

            // Classify by packet type
            match &pkt.data {
                PacketData::RawError(bytes, err) => {
                    self.crc_or_io_errors += 1;
                    if err.contains("timeout") && bytes.is_empty() {
                        self.timeouts += 1;
                    }
                }
                PacketData::RawRx(bytes) => {
                    // Check for exception responses
                    if bytes.len() >= 2 && bytes[1] & 0x80 != 0 {
                        self.exceptions += 1;
                    }
                }
                // Track which slaves appear in TX frames
                PacketData::RawTx(bytes) => {
                    if !bytes.is_empty() {
                        // Counted per-direction for silent slave detection
                    }
                }
                _ => {}
            }
        }
    }

    /// Identify slaves that have TX frames but no matching RX frames.
    fn find_silent_slaves(&mut self, packets: &[PacketRecord]) {
        let mut tx_slaves = std::collections::BTreeSet::new();
        let mut rx_slaves = std::collections::BTreeSet::new();

        for pkt in packets {
            match &pkt.data {
                PacketData::RawTx(bytes) if !bytes.is_empty() => {
                    tx_slaves.insert(bytes[0]);
                }
                PacketData::RawRx(bytes) if !bytes.is_empty() => {
                    rx_slaves.insert(bytes[0]);
                }
                _ => {}
            }
        }

        self.silent_slaves = tx_slaves
            .difference(&rx_slaves)
            .copied()
            .collect();
    }

    fn print_report(&self) {
        println!("โ•โ•โ• Bus Doctor Report โ•โ•โ•\n");
        println!("Total frames captured: {}", self.total_frames);
        println!("I/O errors (CRC/timeout/partial): {}", self.crc_or_io_errors);
        println!("Exception responses: {}", self.exceptions);
        println!("Timeouts (no response): {}", self.timeouts);
        println!(
            "Tight inter-frame gaps (<2 ms): {}",
            self.tight_gaps
        );

        if !self.silent_slaves.is_empty() {
            println!("\nโš  Silent slaves (TX but no RX):");
            for addr in &self.silent_slaves {
                println!("  - Slave 0x{addr:02X} โ€” check address, baud rate, or power");
            }
        } else {
            println!("\nโœ“ All polled slaves responded at least once.");
        }

        if self.tight_gaps > 0 {
            println!(
                "\nโš  {} inter-frame gaps below 1.75 ms โ€” potential 3.5T violation.",
                self.tight_gaps
            );
            println!("  โ†’ Check polling rate. At 19200 baud, minimum gap is ~1.75 ms.");
            println!("  โ†’ If only at night or in humidity, check enclosure seals.");
        }

        if self.crc_or_io_errors == 0 && self.tight_gaps == 0 && self.silent_slaves.is_empty() {
            println!("\nโœ“ Bus appears healthy.");
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Self-contained RTU bus โ€” no hardware needed
    let (client_stream, server_stream) = tokio::io::duplex(1024);

    // Virtual slave at address 1
    let store = Arc::new(SlaveStore::with_holding_registers(&[
        (0, 100),
        (1, 200),
        (2, 300),
    ]));
    let server = rtu::RtuServer::new(server_stream);
    tokio::spawn(async move {
        server.serve_forever(store).await.ok();
    });

    // Attach WireTap
    let cap = Arc::new(BusCapture::unbounded());
    let opts = ClientOptions::default()
        .with_timeout(Duration::from_millis(500))
        .with_tap(cap.clone());
    let client = rtu::with_options(client_stream, opts);

    // Generate traffic: normal reads + one unreachable address
    for _ in 0..3 {
        client.read_holding_registers(1, 0, 3).await?;
    }
    // Poll another slave address โ€” verifies the silent-slave detection path
    let _ = client.read_holding_registers(247, 0, 2).await;

    // Run diagnostics
    let packets = cap.drain();
    let mut doc = BusDoctor::default();
    doc.ingest(&packets);
    doc.find_silent_slaves(&packets);
    doc.print_report();

    Ok(())
}

Output from a test run with the self-contained duplex bus:

โ•โ•โ• Bus Doctor Report โ•โ•โ•

Total frames captured: 8
I/O errors (CRC/timeout/partial): 0
Exception responses: 0
Timeouts (no response): 0
Tight inter-frame gaps (<2 ms): 4

โœ“ All polled slaves responded at least once.

โš  4 inter-frame gaps below 1.75 ms โ€” potential 3.5T violation.
  โ†’ Check polling rate. At 19200 baud, minimum gap is ~1.75 ms.
  โ†’ If only at night or in humidity, check enclosure seals.

The tight-gap warning is expected in this self-contained demo โ€” tokio::io::duplex completes reads and writes in microseconds, so every gap is below the 1.75 ms threshold. On a real RS-485 bus with actual serial latency, normal gaps would be well above the threshold. The warning would only fire when the polling loop is genuinely too fast or signal degradation is compressing the effective frame spacing.

With a real bus experiencing CRC errors from a loose termination resistor, the report would instead show a non-zero I/O errors count โ€” the immediate signal to investigate the electrical layer before touching any configuration.

Field Diagnostic Checklist

When you arrive on site with a WireTap capture, work through these questions in order. They cover 90% of Modbus field failures:

  1. Any [ERR] frames? If yes, check the payload length and error message.

    • Empty payload + "timeout" โ†’ device offline or wrong address. Check power, wiring, address DIP switches.
    • Partial payload + "CRC mismatch" โ†’ electrical problem. Check termination resistors (exactly two per segment: one at each end), grounding, and cable routing near VFDs or power cables.
    • Partial payload + "timeout" โ†’ device crashed mid-response. Check firmware and power supply stability.
  2. Any slave with TX but no RX? โ†’ Address mismatch, baud rate mismatch, or dead device. Scan addresses to find where it landed.

  3. Any exception responses (FC | 0x80)? โ†’ The device is alive but rejecting the request. Read the exception code (byte 2): 0x02 = register out of range, 0x03 = value out of range. Check the SCADA configuration against the device's register map.

  4. Inter-frame gaps below 1.75 ms? โ†’ 3.5T violation. Reduce polling rate, or check if the master is sending frames faster than the Modbus spec allows. If timing degrades at night or in humidity, inspect the enclosure.

  5. More errors on one specific device than others? โ†’ Localized problem. Check that device's connector, cable segment, and power supply.


This article is part of the OMS Modbus open-source documentation. The Bus Doctor code is verified against oms-modbus v0.2.0 and compiles with cargo check.

Read next: Decoding Modbus Frames by Hand โ€” learn to read every byte in a Modbus frame manually. WireTap Quick Start โ€” your first capture in 15 minutes.

modbus modbus-rtu modbus-tcp rs485 troubleshooting bus-diagnostics field-diagnostics crc-error passive-monitoring rust IIoT