j0h

ModbusLatencyLogger

j0h
Apr 6th, 2026
182
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.10 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. import socket
  3. import struct
  4. import time
  5. import csv
  6. from datetime import datetime
  7.  
  8. ip = "192.168.1.7"
  9. port = 502
  10.  
  11. unit_id = 1
  12. start_reg = 4607      # 304608
  13. count = 20            # number of registers to read
  14. interval_s = 0.001      # 0.1 = 100 ms between reads
  15. timeout_s = 2.0
  16. csv_file = "modbus_latency_log.csv"
  17.  
  18.  
  19. def build_modbus_read_request(transaction_id, unit_id, start_reg, count):
  20.     protocol_id = 0
  21.     length = 6
  22.     function_code = 4  # Read Input Registers
  23.     return struct.pack(
  24.         ">HHHBBHH",
  25.         transaction_id,
  26.         protocol_id,
  27.         length,
  28.         unit_id,
  29.         function_code,
  30.         start_reg,
  31.         count,
  32.     )
  33.  
  34.  
  35. def recv_exact(sock, n):
  36.     data = b""
  37.     while len(data) < n:
  38.         chunk = sock.recv(n - len(data))
  39.         if not chunk:
  40.             raise ConnectionError("Socket closed by peer")
  41.         data += chunk
  42.     return data
  43.  
  44.  
  45. def read_modbus_once(sock, transaction_id, unit_id, start_reg, count):
  46.     req = build_modbus_read_request(transaction_id, unit_id, start_reg, count)
  47.  
  48.     t0 = time.perf_counter()
  49.     sock.sendall(req)
  50.  
  51.     # Read MBAP header first
  52.     header = recv_exact(sock, 6)
  53.     rx_tid, rx_pid, rx_len = struct.unpack(">HHH", header)
  54.  
  55.     # Then read remaining bytes
  56.     body = recv_exact(sock, rx_len)
  57.     t1 = time.perf_counter()
  58.  
  59.     resp = header + body
  60.     latency_ms = (t1 - t0) * 1000.0
  61.  
  62.     unit = body[0]
  63.     func = body[1]
  64.  
  65.     status = "ok"
  66.     exception_code = ""
  67.     byte_count = ""
  68.     register_values = []
  69.  
  70.     if func == 0x84:
  71.         status = "modbus_exception"
  72.         exception_code = body[2]
  73.     elif func != 0x04:
  74.         status = f"unexpected_function_0x{func:02X}"
  75.     else:
  76.         byte_count = body[2]
  77.         data = body[3:3 + byte_count]
  78.         register_values = [
  79.             struct.unpack(">H", data[i:i+2])[0]
  80.             for i in range(0, len(data), 2)
  81.         ]
  82.  
  83.     return {
  84.         "latency_ms": latency_ms,
  85.         "status": status,
  86.         "exception_code": exception_code,
  87.         "transaction_id": rx_tid,
  88.         "unit_id": unit,
  89.         "function_code": func,
  90.         "byte_count": byte_count,
  91.         "registers": register_values,
  92.         #"raw_response_hex": resp.hex(),
  93.     }
  94.  
  95.  
  96. def main():
  97.     fieldnames = [
  98.         "timestamp_utc",
  99.         "elapsed_s",
  100.         "sample",
  101.         "latency_ms",
  102.         "status",
  103.         "exception_code",
  104.         "transaction_id",
  105.         "unit_id",
  106.         "function_code",
  107.         "byte_count",
  108.         #"raw_response_hex",
  109.     ]
  110.  
  111.     start_time = time.time()
  112.     sample = 0
  113.     transaction_id = 1
  114.  
  115.     print(f"Logging to {csv_file}")
  116.     print("Press Ctrl+C to stop.\n")
  117.  
  118.     with open(csv_file, "w", newline="") as f:
  119.         writer = csv.DictWriter(f, fieldnames=fieldnames)
  120.         writer.writeheader()
  121.  
  122.         with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  123.             s.settimeout(timeout_s)
  124.             s.connect((ip, port))
  125.  
  126.             while True:
  127.                 sample += 1
  128.                 now = datetime.utcnow().isoformat() + "Z"
  129.                 elapsed_s = time.time() - start_time
  130.  
  131.                 try:
  132.                     result = read_modbus_once(
  133.                         s, transaction_id, unit_id, start_reg, count
  134.                     )
  135.  
  136.                     row = {
  137.                         "timestamp_utc": now,
  138.                         "elapsed_s": f"{elapsed_s:.3f}",
  139.                         "sample": sample,
  140.                         "latency_ms": f"{result['latency_ms']:.3f}",
  141.                         "status": result["status"],
  142.                         "exception_code": result["exception_code"],
  143.                         "transaction_id": result["transaction_id"],
  144.                         "unit_id": result["unit_id"],
  145.                         "function_code": f"0x{result['function_code']:02X}",
  146.                         "byte_count": result["byte_count"],
  147.                         #"raw_response_hex": result["raw_response_hex"],
  148.                     }
  149.                     writer.writerow(row)
  150.                     f.flush()
  151.  
  152.                     print(
  153.                         f"[{sample:06d}] "
  154.                         f"{row['elapsed_s']} s  "
  155.                         f"{row['latency_ms']} ms  "
  156.                         f"{row['status']}"
  157.                     )
  158.  
  159.                 except socket.timeout:
  160.                     row = {
  161.                         "timestamp_utc": now,
  162.                         "elapsed_s": f"{elapsed_s:.3f}",
  163.                         "sample": sample,
  164.                         "latency_ms": "",
  165.                         "status": "timeout",
  166.                         "exception_code": "",
  167.                         "transaction_id": transaction_id,
  168.                         "unit_id": unit_id,
  169.                         "function_code": "0x04",
  170.                         "byte_count": "",
  171.                         #"raw_response_hex": "",
  172.                     }
  173.                     writer.writerow(row)
  174.                     f.flush()
  175.                     print(f"[{sample:06d}] {row['elapsed_s']} s  TIMEOUT")
  176.  
  177.                 except Exception as e:
  178.                     row = {
  179.                         "timestamp_utc": now,
  180.                         "elapsed_s": f"{elapsed_s:.3f}",
  181.                         "sample": sample,
  182.                         "latency_ms": "",
  183.                         "status": f"error: {e}",
  184.                         "exception_code": "",
  185.                         "transaction_id": transaction_id,
  186.                         "unit_id": unit_id,
  187.                         "function_code": "0x04",
  188.                         "byte_count": "",
  189.                         #"raw_response_hex": "",
  190.                     }
  191.                     writer.writerow(row)
  192.                     f.flush()
  193.                     print(f"[{sample:06d}] {row['elapsed_s']} s  ERROR: {e}")
  194.                     break
  195.  
  196.                 transaction_id = (transaction_id + 1) & 0xFFFF
  197.                 if transaction_id == 0:
  198.                     transaction_id = 1
  199.  
  200.                 time.sleep(interval_s)
  201.  
  202.  
  203. if __name__ == "__main__":
  204.     main()
  205.  
Advertisement
Add Comment
Please, Sign In to add comment