Skip to content

Commit d0fb6e0

Browse files
author
Tom Softreck
committed
update
1 parent 605cde4 commit d0fb6e0

5 files changed

Lines changed: 717 additions & 10 deletions

File tree

examples/multi_device_scanner.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Multi-Device Modbus RTU Scanner
4+
5+
This script scans for multiple Modbus RTU devices connected to a serial port
6+
via RS485. It tests different unit IDs and function codes to identify all
7+
connected devices.
8+
"""
9+
10+
import sys
11+
import logging
12+
import argparse
13+
from typing import List, Dict, Any, Tuple
14+
15+
# Configure logging
16+
logging.basicConfig(
17+
level=logging.INFO,
18+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
19+
)
20+
logger = logging.getLogger("ModbusScanner")
21+
22+
# Import the ModbusRTU client
23+
import os
24+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
25+
from modapi.api.rtu import ModbusRTU
26+
27+
28+
def scan_device(client: ModbusRTU, unit_id: int) -> Dict[str, Any]:
29+
"""
30+
Scan a specific unit ID for supported function codes and register ranges
31+
32+
Args:
33+
client: ModbusRTU client instance
34+
unit_id: Unit ID to scan
35+
36+
Returns:
37+
Dict with device information and supported functions
38+
"""
39+
device_info = {
40+
"unit_id": unit_id,
41+
"connected": False,
42+
"supported_functions": [],
43+
"coils": None,
44+
"discrete_inputs": None,
45+
"holding_registers": None,
46+
"input_registers": None,
47+
}
48+
49+
# Test connection by reading first coil
50+
logger.info(f"Testing connection to unit ID {unit_id}...")
51+
52+
# Test read_coils (function code 0x01)
53+
try:
54+
result = client.read_coils(unit_id=unit_id, address=0, count=1)
55+
if result is not None:
56+
device_info["connected"] = True
57+
device_info["supported_functions"].append("read_coils")
58+
device_info["coils"] = result
59+
logger.info(f"Unit {unit_id}: read_coils supported, result: {result}")
60+
except Exception as e:
61+
logger.debug(f"Unit {unit_id}: read_coils failed: {e}")
62+
63+
# Test read_discrete_inputs (function code 0x02)
64+
try:
65+
result = client.read_discrete_inputs(unit_id=unit_id, address=0, count=1)
66+
if result is not None:
67+
device_info["connected"] = True
68+
device_info["supported_functions"].append("read_discrete_inputs")
69+
device_info["discrete_inputs"] = result
70+
logger.info(f"Unit {unit_id}: read_discrete_inputs supported, result: {result}")
71+
except Exception as e:
72+
logger.debug(f"Unit {unit_id}: read_discrete_inputs failed: {e}")
73+
74+
# Test read_holding_registers (function code 0x03)
75+
try:
76+
result = client.read_holding_registers(unit_id=unit_id, address=0, count=1)
77+
if result is not None:
78+
device_info["connected"] = True
79+
device_info["supported_functions"].append("read_holding_registers")
80+
device_info["holding_registers"] = result
81+
logger.info(f"Unit {unit_id}: read_holding_registers supported, result: {result}")
82+
except Exception as e:
83+
logger.debug(f"Unit {unit_id}: read_holding_registers failed: {e}")
84+
85+
# Test read_input_registers (function code 0x04)
86+
try:
87+
result = client.read_input_registers(unit_id=unit_id, address=0, count=1)
88+
if result is not None:
89+
device_info["connected"] = True
90+
device_info["supported_functions"].append("read_input_registers")
91+
device_info["input_registers"] = result
92+
logger.info(f"Unit {unit_id}: read_input_registers supported, result: {result}")
93+
except Exception as e:
94+
logger.debug(f"Unit {unit_id}: read_input_registers failed: {e}")
95+
96+
return device_info
97+
98+
99+
def scan_unit_id_range(client: ModbusRTU, start_id: int = 1, end_id: int = 10) -> List[Dict[str, Any]]:
100+
"""
101+
Scan a range of unit IDs for Modbus devices
102+
103+
Args:
104+
client: ModbusRTU client instance
105+
start_id: Starting unit ID
106+
end_id: Ending unit ID
107+
108+
Returns:
109+
List of dictionaries with device information
110+
"""
111+
devices = []
112+
113+
for unit_id in range(start_id, end_id + 1):
114+
logger.info(f"Scanning unit ID {unit_id}...")
115+
device_info = scan_device(client, unit_id)
116+
117+
if device_info["connected"]:
118+
devices.append(device_info)
119+
logger.info(f"Found device at unit ID {unit_id}")
120+
logger.info(f"Supported functions: {device_info['supported_functions']}")
121+
else:
122+
logger.info(f"No device found at unit ID {unit_id}")
123+
124+
return devices
125+
126+
127+
def main():
128+
"""Main function to run the scanner"""
129+
parser = argparse.ArgumentParser(description='Modbus RTU Multi-Device Scanner')
130+
parser.add_argument('--port', type=str, default='/dev/ttyACM0',
131+
help='Serial port to use (default: /dev/ttyACM0)')
132+
parser.add_argument('--baudrate', type=int, default=9600,
133+
help='Baudrate to use (default: 9600)')
134+
parser.add_argument('--timeout', type=float, default=1.0,
135+
help='Communication timeout in seconds (default: 1.0)')
136+
parser.add_argument('--start-id', type=int, default=1,
137+
help='Starting unit ID to scan (default: 1)')
138+
parser.add_argument('--end-id', type=int, default=10,
139+
help='Ending unit ID to scan (default: 10)')
140+
parser.add_argument('--debug', action='store_true',
141+
help='Enable debug logging')
142+
143+
args = parser.parse_args()
144+
145+
if args.debug:
146+
logger.setLevel(logging.DEBUG)
147+
148+
logger.info(f"Starting Modbus RTU scanner on {args.port} at {args.baudrate} baud")
149+
logger.info(f"Scanning unit IDs from {args.start_id} to {args.end_id}")
150+
151+
# Create and connect ModbusRTU client
152+
client = ModbusRTU(
153+
port=args.port,
154+
baudrate=args.baudrate,
155+
timeout=args.timeout
156+
)
157+
158+
if not client.connect():
159+
logger.error(f"Failed to connect to {args.port}")
160+
return 1
161+
162+
try:
163+
# Scan for devices
164+
devices = scan_unit_id_range(client, args.start_id, args.end_id)
165+
166+
# Print results
167+
logger.info("\n--- SCAN RESULTS ---")
168+
if devices:
169+
logger.info(f"Found {len(devices)} Modbus RTU device(s):")
170+
for device in devices:
171+
logger.info(f"Unit ID: {device['unit_id']}")
172+
logger.info(f" Supported functions: {device['supported_functions']}")
173+
if 'read_coils' in device['supported_functions']:
174+
logger.info(f" Coils: {device['coils']}")
175+
if 'read_discrete_inputs' in device['supported_functions']:
176+
logger.info(f" Discrete inputs: {device['discrete_inputs']}")
177+
if 'read_holding_registers' in device['supported_functions']:
178+
logger.info(f" Holding registers: {device['holding_registers']}")
179+
if 'read_input_registers' in device['supported_functions']:
180+
logger.info(f" Input registers: {device['input_registers']}")
181+
else:
182+
logger.info("No Modbus RTU devices found.")
183+
184+
finally:
185+
# Disconnect client
186+
client.disconnect()
187+
188+
return 0
189+
190+
191+
if __name__ == "__main__":
192+
sys.exit(main())

examples/quick_device_scan.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Quick Modbus Device Scanner
4+
Scans for Modbus RTU devices on common serial ports
5+
"""
6+
7+
import os
8+
import sys
9+
import logging
10+
from typing import List, Dict, Tuple
11+
12+
# Add parent directory to path for imports
13+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
14+
15+
from modapi.api.rtu import ModbusRTU, find_serial_ports
16+
17+
# Configure logging
18+
logging.basicConfig(
19+
level=logging.INFO,
20+
format='%(asctime)s - %(levelname)s - %(message)s'
21+
)
22+
logger = logging.getLogger("modbus_scanner")
23+
24+
def scan_ports() -> List[str]:
25+
"""Find all available serial ports"""
26+
ports = find_serial_ports()
27+
print(f"Found {len(ports)} serial ports: {', '.join(ports)}")
28+
return ports
29+
30+
def scan_device(port: str, baudrate: int = 9600) -> List[Tuple[int, str]]:
31+
"""
32+
Scan a single port for Modbus devices
33+
34+
Args:
35+
port: Serial port to scan
36+
baudrate: Baudrate to use
37+
38+
Returns:
39+
List of (unit_id, type) tuples for detected devices
40+
"""
41+
devices = []
42+
print(f"Scanning {port} at {baudrate} baud...")
43+
44+
try:
45+
client = ModbusRTU(port=port, baudrate=baudrate, timeout=0.3)
46+
if not client.connect():
47+
print(f"Could not connect to {port}")
48+
return []
49+
50+
# Scan unit IDs 1-10
51+
for unit_id in range(1, 11):
52+
print(f" Testing Unit ID: {unit_id}...", end="", flush=True)
53+
54+
# Try reading coils first (most common)
55+
try:
56+
result = client.read_coils(unit_id=unit_id, address=0, count=1)
57+
if result is not None:
58+
print(f" FOUND! (coils)")
59+
devices.append((unit_id, "coils"))
60+
continue
61+
except Exception:
62+
pass
63+
64+
# Try reading holding registers
65+
try:
66+
result = client.read_holding_registers(unit_id=unit_id, address=0, count=1)
67+
if result is not None:
68+
print(f" FOUND! (registers)")
69+
devices.append((unit_id, "registers"))
70+
continue
71+
except Exception:
72+
pass
73+
74+
print(" not found")
75+
76+
client.close()
77+
78+
except Exception as e:
79+
print(f"Error scanning {port}: {e}")
80+
81+
return devices
82+
83+
def main():
84+
"""Main function"""
85+
print("=== MODBUS DEVICE SCANNER ===")
86+
87+
# Find all available ports
88+
ports = scan_ports()
89+
90+
if not ports:
91+
print("No serial ports found!")
92+
return
93+
94+
# Scan each port
95+
results = {}
96+
for port in ports:
97+
devices = scan_device(port)
98+
if devices:
99+
results[port] = devices
100+
101+
# Print summary
102+
print("\n=== SCAN RESULTS ===")
103+
if not results:
104+
print("No Modbus devices found on any port.")
105+
else:
106+
for port, devices in results.items():
107+
print(f"\nPort: {port}")
108+
print("-" * 30)
109+
for unit_id, device_type in devices:
110+
print(f" Unit ID: {unit_id}, Type: {device_type}")
111+
112+
if __name__ == "__main__":
113+
main()

0 commit comments

Comments
 (0)