Skip to content

Commit e946f86

Browse files
author
bobfox
authored
Merge pull request #50 from sunspec/development
Development
2 parents ca6af43 + 5350e95 commit e946f86

File tree

10 files changed

+1407
-23
lines changed

10 files changed

+1407
-23
lines changed

scripts/__init__.py

Whitespace-only changes.

scripts/suns.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Copyright (c) 2021, SunSpec Alliance
5+
All Rights Reserved
6+
7+
"""
8+
9+
import sys
10+
import time
11+
import sunspec2.modbus.client as client
12+
import sunspec2.file.client as file_client
13+
from optparse import OptionParser
14+
15+
"""
16+
Original suns options:
17+
18+
-o: output mode for data (text, xml)
19+
-x: export model description (slang, xml)
20+
-t: transport type: tcp or rtu (default: tcp)
21+
-a: modbus slave address (default: 1)
22+
-i: ip address to use for modbus tcp (default: localhost)
23+
-P: port number for modbus tcp (default: 502)
24+
-p: serial port for modbus rtu (default: /dev/ttyUSB0)
25+
-R: parity for modbus rtu: None, E (default: None)
26+
-b: baud rate for modbus rtu (default: 9600)
27+
-T: timeout, in seconds (can be fractional, such as 1.5; default: 2.0)
28+
-r: number of retries attempted for each modbus read
29+
-m: specify model file
30+
-M: specify directory containing model files
31+
-s: run as a test server
32+
-I: logger id (for sunspec logger xml output)
33+
-N: logger id namespace (for sunspec logger xml output, defaults to 'mac')
34+
-l: limit number of registers requested in a single read (max is 125)
35+
-c: check models for internal consistency then exit
36+
-v: verbose level (up to -vvvv for most verbose)
37+
-V: print current release number and exit
38+
"""
39+
40+
if __name__ == "__main__":
41+
42+
usage = 'usage: %prog [options]'
43+
parser = OptionParser(usage=usage)
44+
parser.add_option('-t', metavar=' ',
45+
default='tcp',
46+
help='transport type: rtu, tcp, file [default: tcp]')
47+
parser.add_option('-a', metavar=' ', type='int',
48+
default=1,
49+
help='modbus slave address [default: 1]')
50+
parser.add_option('-i', metavar=' ',
51+
default='localhost',
52+
help='ip address to use for modbus tcp [default: localhost]')
53+
parser.add_option('-P', metavar=' ', type='int',
54+
default=502,
55+
help='port number for modbus tcp [default: 502]')
56+
parser.add_option('-p', metavar=' ',
57+
default='/dev/ttyUSB0',
58+
help='serial port for modbus rtu [default: /dev/ttyUSB0]')
59+
parser.add_option('-b', metavar=' ',
60+
default=9600,
61+
help='baud rate for modbus rtu [default: 9600]')
62+
parser.add_option('-R', metavar=' ',
63+
default=None,
64+
help='parity for modbus rtu: None, E [default: None]')
65+
parser.add_option('-T', metavar=' ', type='float',
66+
default=2.0,
67+
help='timeout, in seconds (can be fractional, such as 1.5) [default: 2.0]')
68+
parser.add_option('-m', metavar=' ',
69+
help='modbus map file')
70+
71+
options, args = parser.parse_args()
72+
73+
try:
74+
if options.t == 'tcp':
75+
sd = client.SunSpecModbusClientDeviceTCP(slave_id=options.a, ipaddr=options.i, ipport=options.P,
76+
timeout=options.T)
77+
elif options.t == 'rtu':
78+
sd = client.SunSpecModbusClientDeviceRTU(slave_id=options.a, name=options.p, baudrate=options.b,
79+
parity=options.R, timeout=options.T)
80+
elif options.t == 'file':
81+
sd = file_client.FileClientDevice(filename=options.m)
82+
else:
83+
print('Unknown -t option: %s' % (options.t))
84+
sys.exit(1)
85+
86+
except client.SunSpecModbusClientError as e:
87+
print('Error: %s' % e)
88+
sys.exit(1)
89+
except file_client.FileClientError as e:
90+
print('Error: %s' % e)
91+
sys.exit(1)
92+
93+
if sd is not None:
94+
print( '\nTimestamp: %s' % (time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())))
95+
96+
# read all models in the device
97+
sd.scan()
98+
99+
print(sd.get_text())

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99

1010
setup(
1111
name='pysunspec2',
12-
version='1.0.4',
12+
version='1.0.5',
1313
description='Python SunSpec Tools',
1414
author='SunSpec Alliance',
1515
author_email='support@sunspec.org',
1616
url='https://sunspec.org/',
1717
packages=['sunspec2', 'sunspec2.modbus', 'sunspec2.file', 'sunspec2.tests'],
1818
package_data={'sunspec2.tests': ['test_data/*'], 'sunspec2': ['models/json/*']},
19+
scripts=['scripts/suns.py'],
1920
python_requires='>=3.5',
2021
extras_require={
2122
'serial': ['pyserial'],

sunspec2/device.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
import sunspec2.mdef as mdef
66
import sunspec2.smdx as smdx
77
import sunspec2.mb as mb
8+
import time
89

910

1011
class ModelError(Exception):
1112
pass
1213

14+
1315
ACCESS_REGION_REGS = 123
1416

1517
this_dir, this_filename = os.path.split(__file__)
@@ -313,6 +315,34 @@ def is_impl(self):
313315
impl = self.info.is_impl(self.value)
314316
return impl
315317

318+
def get_text(self, index=None, parent_index=None):
319+
txt = ''
320+
name = self.pdef['name']
321+
val = self.value
322+
units = self.pdef.get('units')
323+
group_index = ''
324+
length = ''
325+
if index:
326+
if parent_index:
327+
group_index = f'{parent_index:02}'
328+
group_index += ':' + f'{index:02}'
329+
txt += '%6s' % (group_index + ':')
330+
else:
331+
group_index = f'{index:02}'
332+
txt += '%6s' % (group_index + ':')
333+
else:
334+
txt += ' ' * 6
335+
336+
txt += name
337+
length = 55 - len(name)
338+
txt += '%*s' % (length, str(val))
339+
if units:
340+
txt += ' ' + units + '\n'
341+
else:
342+
txt += '\n'
343+
344+
return txt
345+
316346

317347
class Group(object):
318348
def __init__(self, gdef=None, model=None, model_offset=0, group_len=0, data=None, data_offset=0, group_class=None,
@@ -616,6 +646,24 @@ def set_mb(self, data=None, computed=False, dirty=None):
616646
return None
617647
return int(offset/2)
618648

649+
def get_text(self, index=None, parent_index=None):
650+
txt = ''
651+
tmp_txt = ''
652+
for p in self.points:
653+
tmp_txt = self.points[p].get_text(index, parent_index)
654+
if tmp_txt:
655+
txt += tmp_txt
656+
for g in self.groups:
657+
if isinstance(self.groups[g], list):
658+
for i in range(len(self.groups[g])):
659+
if index:
660+
txt += self.groups[g][i].get_text(i+1, parent_index=index)
661+
else:
662+
txt += self.groups[g][i].get_text(i+1)
663+
else:
664+
txt += self.groups[g].get_text(index)
665+
return txt
666+
619667

620668
class Model(Group):
621669
def __init__(self, model_id=None, model_addr=0, model_len=0, model_def=None, data=None, group_class=Group):
@@ -689,6 +737,10 @@ def add_model(self, model):
689737

690738
model.device = self
691739

740+
def delete_models(self):
741+
self.models = {}
742+
self.model_list = []
743+
692744
def get_dict(self, computed=False):
693745
d = {'name': self.name, 'did': self.did, 'models': []}
694746
for m in self.model_list:
@@ -746,3 +798,13 @@ def _set_dict(self, data, computed=False, detail=False):
746798
model_len = 0
747799
model = Model(model_def=model_def, data=m, model_id=m['ID'], model_len=model_len)
748800
self.add_model(model=model)
801+
802+
def get_text(self):
803+
txt = 'Timestamp: %s\n' % (time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()))
804+
for m in self.model_list:
805+
if m.error_info:
806+
txt += '\nError: ' + m.error_info + '\n'
807+
continue
808+
txt += '\nModel: %s (%s)\n\n' % (m.model_def['group']['name'], m.model_id)
809+
txt += m.get_text()
810+
return txt

sunspec2/modbus/client.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ def __init__(self, gdef=None, model=None, model_offset=0, group_len=0, data=None
7070
data=data, data_offset=data_offset, group_class=group_class, point_class=point_class,
7171
index=index)
7272

73-
def read(self):
73+
def read(self, len=None):
74+
if len is None:
75+
len = self.len
7476
# check if currently connected
7577
connected = self.model.device.is_connected()
7678
if not connected:
@@ -82,7 +84,7 @@ def read(self):
8284
data += self.model.device.read(self.model.model_addr + self.offset + region[0], region[1])
8385
data = bytes(data)
8486
else:
85-
data = self.model.device.read(self.model.model_addr + self.offset, self.len)
87+
data = self.model.device.read(self.model.model_addr + self.offset, len)
8688
self.set_mb(data=data, dirty=False)
8789

8890
# disconnect if was not connected
@@ -171,6 +173,9 @@ def __init__(self, model_id=None, model_addr=0, model_len=0, model_def=None, dat
171173
def add_error(self, error_info):
172174
self.error_info = '%s%s\n' % (self.error_info, error_info)
173175

176+
def read(self, len=None):
177+
SunSpecModbusClientGroup.read(self, len=self.len + 2)
178+
174179

175180
class SunSpecModbusClientDevice(device.Device):
176181
def __init__(self, model_class=SunSpecModbusClientModel):
@@ -200,11 +205,13 @@ def read(self, addr, count):
200205
def write(self, addr, data):
201206
return
202207

203-
def scan(self, progress=None, delay=None, connect=True):
208+
def scan(self, progress=None, delay=None, connect=True, full_model_read=True):
204209
"""Scan all the models of the physical device and create the
205210
corresponding model objects within the device object based on the
206211
SunSpec model definitions.
207212
"""
213+
self.base_addr = None
214+
self.delete_models()
208215

209216
data = error = ''
210217
connected = False
@@ -259,7 +266,8 @@ def scan(self, progress=None, delay=None, connect=True):
259266
model_data = model_id_data + model_len_data
260267
model = self.model_class(model_id=model_id, model_addr=addr, model_len=model_len, data=model_data,
261268
mb_device=self)
262-
model.read()
269+
if full_model_read:
270+
model.read()
263271
model.mid = '%s_%s' % (self.did, mid)
264272
mid += 1
265273
self.add_model(model)
@@ -287,7 +295,8 @@ def scan(self, progress=None, delay=None, connect=True):
287295

288296
class SunSpecModbusClientDeviceTCP(SunSpecModbusClientDevice):
289297
def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx=None, trace_func=None,
290-
max_count=modbus_client.REQ_COUNT_MAX, test=False, model_class=SunSpecModbusClientModel):
298+
max_count=modbus_client.REQ_COUNT_MAX, max_write_count=modbus_client.REQ_WRITE_COUNT_MAX, test=False,
299+
model_class=SunSpecModbusClientModel):
291300
SunSpecModbusClientDevice.__init__(self, model_class=model_class)
292301
self.slave_id = slave_id
293302
self.ipaddr = ipaddr
@@ -297,10 +306,12 @@ def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx
297306
self.socket = None
298307
self.trace_func = trace_func
299308
self.max_count = max_count
309+
self.max_write_count = max_write_count
300310

301311
self.client = modbus_client.ModbusClientTCP(slave_id=slave_id, ipaddr=ipaddr, ipport=ipport, timeout=timeout,
302312
ctx=ctx, trace_func=trace_func,
303-
max_count=modbus_client.REQ_COUNT_MAX, test=test)
313+
max_count=modbus_client.REQ_COUNT_MAX,
314+
max_write_count=modbus_client.REQ_WRITE_COUNT_MAX, test=test)
304315
if self.client is None:
305316
raise SunSpecModbusClientError('No modbus tcp client set for device')
306317

@@ -353,7 +364,8 @@ class SunSpecModbusClientDeviceRTU(SunSpecModbusClientDevice):
353364
"""
354365

355366
def __init__(self, slave_id, name, baudrate=None, parity=None, timeout=None, ctx=None, trace_func=None,
356-
max_count=modbus_client.REQ_COUNT_MAX, model_class=SunSpecModbusClientModel):
367+
max_count=modbus_client.REQ_COUNT_MAX, max_write_count=modbus_client.REQ_WRITE_COUNT_MAX,
368+
model_class=SunSpecModbusClientModel):
357369
# test if this super class init is needed
358370
SunSpecModbusClientDevice.__init__(self, model_class=model_class)
359371
self.slave_id = slave_id
@@ -362,6 +374,7 @@ def __init__(self, slave_id, name, baudrate=None, parity=None, timeout=None, ctx
362374
self.ctx = ctx
363375
self.trace_func = trace_func
364376
self.max_count = max_count
377+
self.max_write_count = max_write_count
365378

366379
self.client = modbus_client.modbus_rtu_client(name, baudrate, parity)
367380
if self.client is None:
@@ -406,4 +419,5 @@ def write(self, addr, data):
406419
Byte string containing register contents.
407420
"""
408421

409-
return self.client.write(self.slave_id, addr, data, trace_func=self.trace_func, max_count=self.max_count)
422+
return self.client.write(self.slave_id, addr, data, trace_func=self.trace_func,
423+
max_write_count=self.max_write_count)

sunspec2/modbus/modbus.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
PARITY_EVEN = 'E'
2727

2828
REQ_COUNT_MAX = 125
29+
REQ_WRITE_COUNT_MAX = 123
2930

3031
FUNC_READ_HOLDING = 3
3132
FUNC_READ_INPUT = 4
@@ -405,7 +406,7 @@ def _write(self, slave_id, addr, data, trace_func=None):
405406
if resp_slave_id != slave_id or resp_func != func or resp_addr != addr or resp_count != count:
406407
raise ModbusClientError('Mobus response format error')
407408

408-
def write(self, slave_id, addr, data, trace_func=None, max_count=REQ_COUNT_MAX):
409+
def write(self, slave_id, addr, data, trace_func=None, max_write_count=REQ_WRITE_COUNT_MAX):
409410
"""
410411
Parameters:
411412
slave_id :
@@ -417,17 +418,17 @@ def write(self, slave_id, addr, data, trace_func=None, max_count=REQ_COUNT_MAX):
417418
trace_func :
418419
Trace function to use for detailed logging. No detailed logging
419420
is perform is a trace function is not supplied.
420-
max_count :
421-
Maximum register count for a single Modbus request.
421+
max_write_count :
422+
Maximum register count for a single Modbus write.
422423
"""
423424

424425
write_offset = 0
425426
count = len(data)/2
426427

427428
if self.serial is not None:
428429
while (count > 0):
429-
if count > max_count:
430-
write_count = max_count
430+
if count > max_write_count:
431+
write_count = max_write_count
431432
else:
432433
write_count = count
433434
start = int(write_offset * 2)
@@ -442,7 +443,7 @@ def write(self, slave_id, addr, data, trace_func=None, max_count=REQ_COUNT_MAX):
442443

443444
class ModbusClientTCP(object):
444445
def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx=None, trace_func=None,
445-
max_count=REQ_COUNT_MAX, test=False):
446+
max_count=REQ_COUNT_MAX, max_write_count=REQ_WRITE_COUNT_MAX, test=False):
446447
self.slave_id = slave_id
447448
self.ipaddr = ipaddr
448449
self.ipport = ipport
@@ -451,6 +452,7 @@ def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx
451452
self.socket = None
452453
self.trace_func = trace_func
453454
self.max_count = max_count
455+
self.max_write_count = max_write_count
454456

455457
if ipport is None:
456458
self.ipport = TCP_DEFAULT_PORT
@@ -672,8 +674,8 @@ def write(self, addr, data):
672674

673675
try:
674676
while (count > 0):
675-
if count > self.max_count:
676-
write_count = self.max_count
677+
if count > self.max_write_count:
678+
write_count = self.max_write_count
677679
else:
678680
write_count = count
679681
start = (write_offset * 2)

0 commit comments

Comments
 (0)