Skip to content

Commit

Permalink
Add support for IPv6
Browse files Browse the repository at this point in the history
Use `urllib.parse.urlsplit` to parse hostname + port, defaulting to `localhost:502`.
Use `socket.getaddrinfo` to determine the suitable address family, an then
attempt to connect for every returned addrinfo.

This implementation is compatible with previous behavior for IPv4 hosts,
but it enables connecting to IPv6 hosts as well. Examples of device
arguments that were previously supported and are still supported:

- `localhost`
- `localhost:502`
- `example.com`
- `example:502`
- `192.0.2.1`
- `192.0.2.1:502`

Examples of device arguments that are newly supported:

- `"[2001:db8::1]"`
- `"[2001:db8::1]:502"`
- `"[fe80::1%eth0]"`
- `"[fe80::1%eth0]:502"`

In addition, IPv6-only hosts with hostnames that could only be resolved
via IPv6 are now also supported.
  • Loading branch information
op3 committed Jun 9, 2024
1 parent c889b6d commit 787b5ac
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 45 deletions.
77 changes: 39 additions & 38 deletions bin/modbus
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import argparse
import sys
import os
import logging
import urllib.parse

import colorama as clr

Expand All @@ -16,18 +17,17 @@ from modbus_cli.access import parse_accesses
class ColourHandler(logging.Handler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.formatter = logging.Formatter('%(style)s%(message)s'+clr.Style.RESET_ALL)
self.formatter = logging.Formatter("%(style)s%(message)s" + clr.Style.RESET_ALL)

def emit(self, record):

if record.levelname == "DEBUG":
record.style = clr.Style.DIM
elif record.levelname == "WARNING":
record.style = clr.Style.BRIGHT
elif record.levelname == "ERROR":
record.style = clr.Style.BRIGHT+clr.Fore.RED
record.style = clr.Style.BRIGHT + clr.Fore.RED
elif record.levelname == "CRITICAL":
record.style = clr.Style.BRIGHT+clr.Back.BLUE+clr.Fore.RED
record.style = clr.Style.BRIGHT + clr.Back.BLUE + clr.Fore.RED
else:
record.style = clr.Style.NORMAL

Expand All @@ -37,31 +37,24 @@ class ColourHandler(logging.Handler):


def connect_to_device(args):
if args.device[0] == '/':
if args.device[0] == "/":
modbus = ModbusRtu(
device=args.device,
baud=args.baud,
parity=args.parity,
stop_bits=args.stop_bits,
slave_id=args.slave_id,
timeout=args.timeout
)
device=args.device,
baud=args.baud,
parity=args.parity,
stop_bits=args.stop_bits,
slave_id=args.slave_id,
timeout=args.timeout,
)
else:
port = 502
parts = args.device.split(':')
if len(parts) == 2:
host, port = parts
elif len(parts) == 1:
host = parts[0]
else:
try:
result = urllib.parse.urlsplit("//" + args.device)
host = result.hostname or "localhost"
port = result.port or 502
except ValueError:
logging.error("Invalid device %r", args.device)
sys.exit(1)

if host == '':
host = 'localhost'

port = int(port)

modbus = ModbusTcp(host, port, args.slave_id)

modbus.connect()
Expand All @@ -71,23 +64,25 @@ def connect_to_device(args):

def main():
parser = argparse.ArgumentParser()
parser.add_argument('-r', '--registers', action='append', default=[])
parser.add_argument('-s', '--slave-id', type=int)
parser.add_argument('-b', '--baud', type=int, default=19200)
parser.add_argument('-p', '--stop-bits', type=int, default=1)
parser.add_argument('-P', '--parity', choices=['e', 'o', 'n'], default='n')
parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('-S', '--silent', action='store_true')
parser.add_argument('-t', '--timeout', type=float, default=5.0)
parser.add_argument('-B', '--byte-order', choices=['le', 'be', 'mixed'], default='be')
parser.add_argument('device')
parser.add_argument('access', nargs='+')
parser.add_argument("-r", "--registers", action="append", default=[])
parser.add_argument("-s", "--slave-id", type=int)
parser.add_argument("-b", "--baud", type=int, default=19200)
parser.add_argument("-p", "--stop-bits", type=int, default=1)
parser.add_argument("-P", "--parity", choices=["e", "o", "n"], default="n")
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("-S", "--silent", action="store_true")
parser.add_argument("-t", "--timeout", type=float, default=5.0)
parser.add_argument(
"-B", "--byte-order", choices=["le", "be", "mixed"], default="be"
)
parser.add_argument("device")
parser.add_argument("access", nargs="+")
args = parser.parse_args()

clr.init()

try:
mainLogger = logging.getLogger() # Main logger
mainLogger = logging.getLogger() # Main logger

if args.verbose:
mainLogger.setLevel(logging.DEBUG)
Expand All @@ -98,13 +93,19 @@ def main():
mainLogger.addHandler(ch)

definitions = Definitions(args.silent)
definitions.parse(args.registers + os.environ.get('MODBUS_DEFINITIONS', '').split(':'))
definitions.parse(
args.registers + os.environ.get("MODBUS_DEFINITIONS", "").split(":")
)

connect_to_device(args).perform_accesses(parse_accesses(args.access, definitions, args.byte_order, args.silent), definitions).close()
connect_to_device(args).perform_accesses(
parse_accesses(args.access, definitions, args.byte_order, args.silent),
definitions,
).close()

finally:
# restore stdout/stderr if colorama has modified them (mostly on windows)
# Leaving this out doesn't seem to hurt anything, but they say to call deinit, so we call deinit.
clr.deinit()


main()
27 changes: 20 additions & 7 deletions modbus_cli/modbus_tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,39 @@ def __init__(self, host, port, slave_id):
self.slave_id = slave_id

import umodbus.client.tcp as modbus

self.protocol = modbus

def connect(self):
import socket
self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.connection.settimeout(5)
self.connection.connect((self.host, self.port))

addr_info = socket.getaddrinfo(
self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM
)

for af, socktype, proto, _, sa in addr_info:
try:
self.connection = socket.socket(af, socktype, proto)
self.connection.connect(sa)
self.connection.settimeout(5)
return
except OSError:
if self.connection:
self.connection.close()
raise OSError(f"Could not connect to {self.host}")

def send(self, request):
self.connection.send(request)

def receive(self, request):
header = self.receive_n(6)
seq, _, count = struct.unpack('>3H', header)
sent_seq = struct.unpack('>H', request[:2])[0]
seq, _, count = struct.unpack(">3H", header)
sent_seq = struct.unpack(">H", request[:2])[0]
if seq != sent_seq:
logging.warn('Sequence mismatch: sent %s, received %s', sent_seq, seq)
logging.warn("Sequence mismatch: sent %s, received %s", sent_seq, seq)
response = header + self.receive_n(count)

logging.debug('← < %s > %s bytes', dump(response), len(response))
logging.debug("← < %s > %s bytes", dump(response), len(response))

return self.protocol.parse_response_adu(response, request)

Expand Down

0 comments on commit 787b5ac

Please sign in to comment.