pymscada 0.2.6b2__tar.gz → 0.2.6b4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pymscada-0.2.6b2/src/pymscada.egg-info → pymscada-0.2.6b4}/PKG-INFO +1 -1
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/pyproject.toml +1 -1
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/bus_server.py +4 -2
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_client.py +189 -21
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_map.py +17 -2
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/www_server.py +1 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4/src/pymscada.egg-info}/PKG-INFO +1 -1
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/LICENSE +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/MANIFEST.in +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/README.md +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/setup.cfg +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/__init__.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/__main__.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/alarms.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/bus_client.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/callout.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/checkout.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/config.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/console.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/README.md +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/__init__.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/accuweather.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/alarms.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/bus.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/callout.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/files.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/history.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/logixclient.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/modbus_plc.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/modbusclient.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/modbusserver.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/openweather.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/opnotes.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/piapi.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/ping.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-alarms.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-bus.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-callout.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-files.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-history.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-openweather.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-piapi.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-ping.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-sms.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-witsapi.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-opnotes.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/sms.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/snmpclient.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/tags.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/witsapi.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/wwwserver.yaml +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/files.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/history.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/__init__.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/accuweather.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/logix_client.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/logix_map.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_server.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/openweather.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/piapi.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/ping_client.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/ping_map.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/sms.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/snmp_client.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/snmp_map.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/witsapi.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/witsapi_POC.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/main.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/misc.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/module_config.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/opnotes.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/pdf/__init__.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/pdf/one.pdf +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/pdf/two.pdf +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/periodic.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/protocol_constants.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/samplers.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/tag.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/tools/get_history.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/tools/snmp_client2.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/tools/walk.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada.egg-info/SOURCES.txt +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada.egg-info/dependency_links.txt +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada.egg-info/entry_points.txt +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada.egg-info/requires.txt +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada.egg-info/top_level.txt +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_alarms.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_bus_server.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_callout.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_config.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_history.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_misc.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_opnotes.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_periodic.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_samplers.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_sms.py +0 -0
- {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_tag.py +0 -0
|
@@ -111,7 +111,8 @@ class BusConnection():
|
|
|
111
111
|
head = await self.reader.readexactly(14)
|
|
112
112
|
_, cmd, tag_id, size, time_us = unpack('!BBHHQ', head)
|
|
113
113
|
except (ConnectionResetError, asyncio.IncompleteReadError,
|
|
114
|
-
asyncio.CancelledError):
|
|
114
|
+
asyncio.CancelledError) as e:
|
|
115
|
+
logging.warning(f'{self.addr} read error: {e}')
|
|
115
116
|
break
|
|
116
117
|
# if the command packet indicates data, get that too
|
|
117
118
|
if size == 0:
|
|
@@ -121,7 +122,8 @@ class BusConnection():
|
|
|
121
122
|
payload = await self.reader.readexactly(size)
|
|
122
123
|
data = unpack(f'!{size}s', payload)[0]
|
|
123
124
|
except (ConnectionResetError, asyncio.IncompleteReadError,
|
|
124
|
-
asyncio.CancelledError):
|
|
125
|
+
asyncio.CancelledError) as e:
|
|
126
|
+
logging.warning(f'{self.addr} read payload error: {e}')
|
|
125
127
|
break
|
|
126
128
|
# if MAX_LEN then a continuation packet is required
|
|
127
129
|
if size == pc.MAX_LEN:
|
|
@@ -2,18 +2,186 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import logging
|
|
4
4
|
from struct import pack, unpack_from
|
|
5
|
+
from time import time
|
|
5
6
|
from pymscada.bus_client import BusClient
|
|
6
|
-
from pymscada.
|
|
7
|
+
from pymscada.tag import Tag
|
|
7
8
|
from pymscada.periodic import Periodic
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
# Modbus
|
|
12
|
+
#
|
|
13
|
+
# Transaction ID 2 bytes incrementing
|
|
14
|
+
# Protocol 2 bytes always 0
|
|
15
|
+
# Length 2 bytes number of following bytes
|
|
16
|
+
# Unit address 1 byte PLC address
|
|
17
|
+
# Message N bytes max size, 253 bytes
|
|
18
|
+
# max overall 260 bytes
|
|
19
|
+
# Function code 1 byte 3 Read registers
|
|
20
|
+
# First address 2 bytes 0 == 40001
|
|
21
|
+
# Register count 2 bytes 125 is the largest
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# data types for PLCs
|
|
25
|
+
DTYPES = {
|
|
26
|
+
'int16': [int, -32768, 32767, 1],
|
|
27
|
+
'int32': [int, -2147483648, 2147483647, 2],
|
|
28
|
+
'int64': [int, -2**63, 2**63 - 1, 4],
|
|
29
|
+
'uint16': [int, 0, 65535, 1],
|
|
30
|
+
'uint32': [int, 0, 4294967295, 2],
|
|
31
|
+
'uint64': [int, 0, 2**64 - 1, 4],
|
|
32
|
+
'float32': [float, None, None, 2],
|
|
33
|
+
'float64': [float, None, None, 4],
|
|
34
|
+
'bool': [int, 0, 1, 1]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def tag_split(modbus_tag: str):
|
|
39
|
+
"""Split the address into rtu, variable, element and bit."""
|
|
40
|
+
name, unit, file, word = modbus_tag.split(':')
|
|
41
|
+
bit_loc = word.find('.')
|
|
42
|
+
if bit_loc == -1:
|
|
43
|
+
bit = None
|
|
44
|
+
word = word
|
|
45
|
+
else:
|
|
46
|
+
bit = word[bit_loc + 1:]
|
|
47
|
+
word = word[:bit_loc]
|
|
48
|
+
return name, unit, file, word, bit
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ModbusClientMap:
|
|
52
|
+
"""Map the data table to a Tag."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, tagname: str, src_type: str, addr: str, data: dict,
|
|
55
|
+
value_chg: dict):
|
|
56
|
+
"""Initialise modbus map and Tag."""
|
|
57
|
+
name, unit, file, word, bit = tag_split(addr)
|
|
58
|
+
self.data_file = f'{name}:{unit}:{file}'
|
|
59
|
+
self.data = data[self.data_file]
|
|
60
|
+
self.value_chg = value_chg[self.data_file]
|
|
61
|
+
self.src_type = src_type
|
|
62
|
+
self.bit = bit
|
|
63
|
+
dtype, dmin, dmax = DTYPES[src_type][0:3]
|
|
64
|
+
self.tag = Tag(tagname, dtype)
|
|
65
|
+
self.map_bus = id(self)
|
|
66
|
+
self.tag.add_callback(self.tag_value_ext, self.map_bus)
|
|
67
|
+
if dmin is not None:
|
|
68
|
+
self.tag.value_min = dmin
|
|
69
|
+
if dmax is not None:
|
|
70
|
+
self.tag.value_max = dmax
|
|
71
|
+
self.byte = (int(word) - 1) * 2
|
|
72
|
+
|
|
73
|
+
def update_tag(self, time_us):
|
|
74
|
+
"""Unpack from modbus registers to tag value if different."""
|
|
75
|
+
if self.bit is not None:
|
|
76
|
+
word_value = unpack_from('>H', self.data, self.byte)[0]
|
|
77
|
+
bit_num = int(self.bit)
|
|
78
|
+
value = (word_value >> bit_num) & 1
|
|
79
|
+
elif self.src_type == 'int16':
|
|
80
|
+
value = unpack_from('>h', self.data, self.byte)[0]
|
|
81
|
+
elif self.src_type == 'uint16':
|
|
82
|
+
value = unpack_from('>H', self.data, self.byte)[0]
|
|
83
|
+
elif self.src_type == 'int32':
|
|
84
|
+
value = unpack_from('>i', self.data, self.byte)[0]
|
|
85
|
+
elif self.src_type == 'uint32':
|
|
86
|
+
value = unpack_from('>I', self.data, self.byte)[0]
|
|
87
|
+
elif self.src_type == 'int64':
|
|
88
|
+
value = unpack_from('>q', self.data, self.byte)[0]
|
|
89
|
+
elif self.src_type == 'uint64':
|
|
90
|
+
value = unpack_from('>Q', self.data, self.byte)[0]
|
|
91
|
+
elif self.src_type == 'float32':
|
|
92
|
+
value = unpack_from('>f', self.data, self.byte)[0]
|
|
93
|
+
elif self.src_type == 'float64':
|
|
94
|
+
value = unpack_from('>d', self.data, self.byte)[0]
|
|
95
|
+
else:
|
|
96
|
+
return
|
|
97
|
+
if value != self.tag.value:
|
|
98
|
+
logging.info(f'updating {self.tag.name} from {self.tag.value}'
|
|
99
|
+
f' to {value}')
|
|
100
|
+
self.tag.value = value, time_us, self.map_bus
|
|
101
|
+
|
|
102
|
+
def tag_value_ext(self, tag: Tag):
|
|
103
|
+
"""Call external tag value update to write remote table."""
|
|
104
|
+
logging.info(f'tag_value_changed {tag.name} {tag.value}')
|
|
105
|
+
if self.src_type == 'int16':
|
|
106
|
+
self.value_chg(self.data_file, self.byte, pack('>h', tag.value))
|
|
107
|
+
elif self.src_type == 'uint16':
|
|
108
|
+
self.value_chg(self.data_file, self.byte, pack('>H', tag.value))
|
|
109
|
+
elif self.src_type == 'int32':
|
|
110
|
+
self.value_chg(self.data_file, self.byte, pack('>i', tag.value))
|
|
111
|
+
elif self.src_type == 'uint32':
|
|
112
|
+
self.value_chg(self.data_file, self.byte, pack('>I', tag.value))
|
|
113
|
+
elif self.src_type == 'int64':
|
|
114
|
+
self.value_chg(self.data_file, self.byte, pack('>q', tag.value))
|
|
115
|
+
elif self.src_type == 'uint64':
|
|
116
|
+
self.value_chg(self.data_file, self.byte, pack('>Q', tag.value))
|
|
117
|
+
elif self.src_type == 'float32':
|
|
118
|
+
self.value_chg(self.data_file, self.byte, pack('>f', tag.value))
|
|
119
|
+
elif self.src_type == 'float64':
|
|
120
|
+
self.value_chg(self.data_file, self.byte, pack('>d', tag.value))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ModbusClientMaps():
|
|
124
|
+
"""Shared modbus mapping."""
|
|
125
|
+
|
|
126
|
+
def __init__(self, tags):
|
|
127
|
+
"""Singular please."""
|
|
128
|
+
self.tags = tags
|
|
129
|
+
self.data = {}
|
|
130
|
+
self.value_chg = {}
|
|
131
|
+
self.maps = {}
|
|
132
|
+
|
|
133
|
+
def add_data_table(self, tables, value_chg):
|
|
134
|
+
"""Add a bytes data table."""
|
|
135
|
+
for table in tables:
|
|
136
|
+
self.data[table] = bytearray(2 * (tables[table] - 1))
|
|
137
|
+
self.value_chg[table] = value_chg
|
|
138
|
+
|
|
139
|
+
def make_map(self):
|
|
140
|
+
"""Make the maps."""
|
|
141
|
+
for tagname, v in self.tags.items():
|
|
142
|
+
dtype = v['type']
|
|
143
|
+
try:
|
|
144
|
+
addr = v['read']
|
|
145
|
+
except KeyError:
|
|
146
|
+
addr = v['addr']
|
|
147
|
+
map = ModbusClientMap(tagname, dtype, addr, self.data, self.value_chg)
|
|
148
|
+
size = DTYPES[dtype][3]
|
|
149
|
+
name, unit, file, word, _bit = tag_split(addr)
|
|
150
|
+
for i in range(0, size):
|
|
151
|
+
word_addr = f'{name}:{unit}:{file}:{int(word) + i}'
|
|
152
|
+
if word_addr not in self.maps:
|
|
153
|
+
self.maps[word_addr] = []
|
|
154
|
+
self.maps[word_addr].append(map)
|
|
155
|
+
|
|
156
|
+
def set_data(self, name: str, unit: int, file: str, pdu_start: int,
|
|
157
|
+
pdu_count: int, data: bytearray):
|
|
158
|
+
"""Set data, start and end in byte count."""
|
|
159
|
+
time_us = int(time() * 1e6)
|
|
160
|
+
start = pdu_start * 2
|
|
161
|
+
end = start + pdu_count * 2
|
|
162
|
+
data_file = f'{name}:{unit}:{file}'
|
|
163
|
+
self.data[data_file][start:end] = data
|
|
164
|
+
maps: set[ModbusClientMap] = set()
|
|
165
|
+
for word_count in range(1, pdu_count + 1):
|
|
166
|
+
word = word_count + pdu_start
|
|
167
|
+
word_addr = f'{name}:{unit}:{file}:{word}'
|
|
168
|
+
try:
|
|
169
|
+
word_maps = self.maps[word_addr]
|
|
170
|
+
maps.update(word_maps)
|
|
171
|
+
except KeyError:
|
|
172
|
+
pass
|
|
173
|
+
logging.debug(f'set_data {name} {unit} {file} {start} {end}')
|
|
174
|
+
for map in maps:
|
|
175
|
+
map.update_tag(time_us)
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
|
|
10
179
|
class ModbusClientProtocol(asyncio.Protocol):
|
|
11
180
|
"""Modbus TCP and UDP client."""
|
|
12
181
|
|
|
13
182
|
def __init__(self, process):
|
|
14
183
|
"""Modbus client protocol."""
|
|
15
184
|
self.process = process
|
|
16
|
-
self._mbap_tr = 0 # start at 0
|
|
17
185
|
self.buffer = b""
|
|
18
186
|
self.peername = None
|
|
19
187
|
self.sockname = None
|
|
@@ -45,11 +213,12 @@ class ModbusClientProtocol(asyncio.Protocol):
|
|
|
45
213
|
def unpack_mb(self):
|
|
46
214
|
"""Return complete modbus packets and trim the buffer."""
|
|
47
215
|
start = 0
|
|
216
|
+
end = 0
|
|
48
217
|
while True:
|
|
49
218
|
buf_len = len(self.buffer)
|
|
50
219
|
if buf_len < 6 + start: # enough to unpack length
|
|
51
220
|
break
|
|
52
|
-
|
|
221
|
+
_mbap_tr, _mbap_pr, mbap_len = unpack_from(">3H", self.buffer, start)
|
|
53
222
|
if buf_len < start + 6 + mbap_len: # there is a complete message
|
|
54
223
|
break
|
|
55
224
|
end = start + 6 + mbap_len
|
|
@@ -75,7 +244,7 @@ class ModbusClientConnector:
|
|
|
75
244
|
"""Poll Modbus device, write on change in write range."""
|
|
76
245
|
|
|
77
246
|
def __init__(self, name: str, ip: str, port: int, rate: int, tcp_udp: str,
|
|
78
|
-
poll: list, mapping:
|
|
247
|
+
poll: list, mapping: ModbusClientMaps):
|
|
79
248
|
"""
|
|
80
249
|
Set up polling client.
|
|
81
250
|
|
|
@@ -88,7 +257,6 @@ class ModbusClientConnector:
|
|
|
88
257
|
self.transport = None
|
|
89
258
|
self.protocol = None
|
|
90
259
|
self.read = poll
|
|
91
|
-
self.writeok = None
|
|
92
260
|
self.periodic = Periodic(self.poll, rate)
|
|
93
261
|
self.mapping = mapping
|
|
94
262
|
self.sent = {}
|
|
@@ -113,8 +281,11 @@ class ModbusClientConnector:
|
|
|
113
281
|
data = msg[9:]
|
|
114
282
|
self.mapping.set_data(name=self.name, data=data,
|
|
115
283
|
**self.sent[mbap_tr])
|
|
116
|
-
|
|
117
|
-
|
|
284
|
+
try:
|
|
285
|
+
del self.sent[mbap_tr]
|
|
286
|
+
except KeyError:
|
|
287
|
+
logging.warning(f"mbap_tr {mbap_tr} not found in sent")
|
|
288
|
+
elif pdu_fc == 16: # provision for future
|
|
118
289
|
pdu_start, pdu_count = unpack_from(">2H", msg, 8)
|
|
119
290
|
pass
|
|
120
291
|
elif pdu_fc > 128:
|
|
@@ -142,7 +313,7 @@ class ModbusClientConnector:
|
|
|
142
313
|
lambda: ModbusClientProtocol(self.process),
|
|
143
314
|
self.ip, self.port)
|
|
144
315
|
except Exception as e:
|
|
145
|
-
logging.
|
|
316
|
+
logging.warning(f'start_connection {e}')
|
|
146
317
|
|
|
147
318
|
def mbap_tr(self):
|
|
148
319
|
"""Global transaction number provider."""
|
|
@@ -164,15 +335,15 @@ class ModbusClientConnector:
|
|
|
164
335
|
pdu = pack(">B2H", pdu_fc, pdu_start, pdu_count)
|
|
165
336
|
pdu_len = 5
|
|
166
337
|
else:
|
|
167
|
-
logging.
|
|
338
|
+
logging.warning(f"no support for {file}")
|
|
168
339
|
return
|
|
169
340
|
mbap_len = pdu_len + 1
|
|
170
341
|
mbap = pack(">3H1B", mbap_tr, mbap_pr, mbap_len, mbap_unit)
|
|
171
342
|
msg = mbap + pdu
|
|
172
343
|
if self.tcp_udp == "udp":
|
|
173
|
-
self.transport.sendto(msg)
|
|
344
|
+
self.transport.sendto(msg) # type: ignore
|
|
174
345
|
else:
|
|
175
|
-
self.transport.write(msg)
|
|
346
|
+
self.transport.write(msg) # type: ignore
|
|
176
347
|
self.sent[mbap_tr] = {"unit": mbap_unit, "file": file,
|
|
177
348
|
"pdu_start": pdu_start, "pdu_count": pdu_count}
|
|
178
349
|
|
|
@@ -192,17 +363,17 @@ class ModbusClientConnector:
|
|
|
192
363
|
# logging.info(f"{pdu_fc} {start} {count} "
|
|
193
364
|
# f"{count * 2} {data} {pdu.hex()}")
|
|
194
365
|
else:
|
|
195
|
-
logging.
|
|
366
|
+
logging.warning(f"no support for {file}")
|
|
196
367
|
return
|
|
197
368
|
mbap_len = pdu_len + 1
|
|
198
369
|
mbap = pack(">3H1B", mbap_tr, mbap_pr, mbap_len, mbap_unit)
|
|
199
370
|
msg = mbap + pdu
|
|
200
371
|
if self.tcp_udp == "udp":
|
|
201
372
|
logging.info(f"UDP write {mbap_unit} {file} {start} {end}")
|
|
202
|
-
self.transport.sendto(msg)
|
|
373
|
+
self.transport.sendto(msg) # type: ignore
|
|
203
374
|
else:
|
|
204
375
|
logging.info(f"TCP write {mbap_unit} {file} {start} {end}")
|
|
205
|
-
self.transport.write(msg)
|
|
376
|
+
self.transport.write(msg) # type: ignore
|
|
206
377
|
|
|
207
378
|
def write_tag_update(self, addr: str, byte: int, data: bytes):
|
|
208
379
|
"""Write out any tag updates."""
|
|
@@ -239,19 +410,16 @@ class ModbusClient:
|
|
|
239
410
|
def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
|
|
240
411
|
rtus: dict = {}, tags: dict = {}) -> None:
|
|
241
412
|
"""
|
|
242
|
-
Connect to bus on bus_ip:bus_port
|
|
243
|
-
|
|
244
|
-
Serves the webclient files at /, as a relative path. The webclient uses
|
|
245
|
-
a websocket connection to request and set tag values and subscribe to
|
|
246
|
-
changes.
|
|
413
|
+
Connect to bus on bus_ip:bus_port.
|
|
247
414
|
|
|
415
|
+
Makes connections to Modbus PLCs to read and write data.
|
|
248
416
|
Event loop must be running.
|
|
249
417
|
"""
|
|
250
418
|
self.busclient = None
|
|
251
419
|
if bus_ip is not None:
|
|
252
420
|
self.busclient = BusClient(bus_ip, bus_port,
|
|
253
421
|
module='Modbus Client')
|
|
254
|
-
self.mapping =
|
|
422
|
+
self.mapping = ModbusClientMaps(tags)
|
|
255
423
|
self.connections: list[ModbusClientConnector] = []
|
|
256
424
|
for rtu in rtus:
|
|
257
425
|
connection = ModbusClientConnector(**rtu, mapping=self.mapping)
|
|
@@ -259,7 +427,7 @@ class ModbusClient:
|
|
|
259
427
|
self.mapping.make_map()
|
|
260
428
|
|
|
261
429
|
async def start(self):
|
|
262
|
-
"""Provide a
|
|
430
|
+
"""Provide a modbus client."""
|
|
263
431
|
if self.busclient is not None:
|
|
264
432
|
await self.busclient.start()
|
|
265
433
|
for connection in self.connections:
|
|
@@ -27,21 +27,36 @@ DTYPES = {
|
|
|
27
27
|
'uint32': [int, 0, 4294967295, 2],
|
|
28
28
|
'uint64': [int, 0, 2**64 - 1, 4],
|
|
29
29
|
'float32': [float, None, None, 2],
|
|
30
|
-
'float64': [float, None, None, 4]
|
|
30
|
+
'float64': [float, None, None, 4],
|
|
31
|
+
'bool': [int, 0, 1, 1]
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def tag_split(modbus_tag: str):
|
|
36
|
+
"""Split the address into rtu, variable, element and bit."""
|
|
37
|
+
name, unit, file, word = modbus_tag.split(':')
|
|
38
|
+
bit_loc = word.find('.')
|
|
39
|
+
if bit_loc == -1:
|
|
40
|
+
bit = None
|
|
41
|
+
word = word
|
|
42
|
+
else:
|
|
43
|
+
bit = word[bit_loc + 1:]
|
|
44
|
+
word = word[:bit_loc]
|
|
45
|
+
return name, unit, file, word, bit
|
|
46
|
+
|
|
47
|
+
|
|
34
48
|
class ModbusMap:
|
|
35
49
|
"""Map the data table to a Tag."""
|
|
36
50
|
|
|
37
51
|
def __init__(self, tagname: str, src_type: str, addr: str, data: dict,
|
|
38
52
|
value_chg: dict):
|
|
39
53
|
"""Initialise modbus map and Tag."""
|
|
40
|
-
name, unit, file, word = addr
|
|
54
|
+
name, unit, file, word, bit = tag_split(addr)
|
|
41
55
|
self.data_file = f'{name}:{unit}:{file}'
|
|
42
56
|
self.data = data[self.data_file]
|
|
43
57
|
self.value_chg = value_chg[self.data_file]
|
|
44
58
|
self.src_type = src_type
|
|
59
|
+
self.bit = bit
|
|
45
60
|
dtype, dmin, dmax = DTYPES[src_type][0:3]
|
|
46
61
|
self.tag = Tag(tagname, dtype)
|
|
47
62
|
self.map_bus = id(self)
|
|
@@ -132,6 +132,7 @@ class WSHandler():
|
|
|
132
132
|
elif tag.type is bytes:
|
|
133
133
|
rta_id = unpack_from('>H', tag.value)[0]
|
|
134
134
|
if rta_id in [0, self.rta_id]:
|
|
135
|
+
logging.info(f'{self.rta_id}: {tag.name} bytes matches id')
|
|
135
136
|
self.queue.put_nowait((True, pack(
|
|
136
137
|
f'!HHQ{len(tag.value)}s', # Network big-endian
|
|
137
138
|
tag.id, # Uint16
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/__pycache__/__init__.cpython-311.pyc
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|