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.
Files changed (105) hide show
  1. {pymscada-0.2.6b2/src/pymscada.egg-info → pymscada-0.2.6b4}/PKG-INFO +1 -1
  2. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/pyproject.toml +1 -1
  3. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/bus_server.py +4 -2
  4. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_client.py +189 -21
  5. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_map.py +17 -2
  6. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/www_server.py +1 -0
  7. {pymscada-0.2.6b2 → pymscada-0.2.6b4/src/pymscada.egg-info}/PKG-INFO +1 -1
  8. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/LICENSE +0 -0
  9. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/MANIFEST.in +0 -0
  10. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/README.md +0 -0
  11. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/setup.cfg +0 -0
  12. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/__init__.py +0 -0
  13. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/__main__.py +0 -0
  14. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/alarms.py +0 -0
  15. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/bus_client.py +0 -0
  16. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/callout.py +0 -0
  17. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/checkout.py +0 -0
  18. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/config.py +0 -0
  19. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/console.py +0 -0
  20. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/README.md +0 -0
  21. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/__init__.py +0 -0
  22. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
  23. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/accuweather.yaml +0 -0
  24. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/alarms.yaml +0 -0
  25. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/bus.yaml +0 -0
  26. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/callout.yaml +0 -0
  27. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/files.yaml +0 -0
  28. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/history.yaml +0 -0
  29. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/logixclient.yaml +0 -0
  30. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/modbus_plc.py +0 -0
  31. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/modbusclient.yaml +0 -0
  32. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/modbusserver.yaml +0 -0
  33. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/openweather.yaml +0 -0
  34. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/opnotes.yaml +0 -0
  35. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/piapi.yaml +0 -0
  36. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/ping.yaml +0 -0
  37. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-alarms.service +0 -0
  38. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-bus.service +0 -0
  39. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-callout.service +0 -0
  40. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
  41. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-files.service +0 -0
  42. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-history.service +0 -0
  43. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
  44. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
  45. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
  46. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-openweather.service +0 -0
  47. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-piapi.service +0 -0
  48. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-ping.service +0 -0
  49. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-sms.service +0 -0
  50. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
  51. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-witsapi.service +0 -0
  52. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-opnotes.service +0 -0
  53. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
  54. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/sms.yaml +0 -0
  55. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/snmpclient.yaml +0 -0
  56. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/tags.yaml +0 -0
  57. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/witsapi.yaml +0 -0
  58. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/demo/wwwserver.yaml +0 -0
  59. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/files.py +0 -0
  60. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/history.py +0 -0
  61. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/__init__.py +0 -0
  62. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/accuweather.py +0 -0
  63. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/logix_client.py +0 -0
  64. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/logix_map.py +0 -0
  65. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_server.py +0 -0
  66. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/openweather.py +0 -0
  67. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/piapi.py +0 -0
  68. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/ping_client.py +0 -0
  69. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/ping_map.py +0 -0
  70. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/sms.py +0 -0
  71. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/snmp_client.py +0 -0
  72. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/snmp_map.py +0 -0
  73. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/witsapi.py +0 -0
  74. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/iodrivers/witsapi_POC.py +0 -0
  75. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/main.py +0 -0
  76. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/misc.py +0 -0
  77. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/module_config.py +0 -0
  78. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/opnotes.py +0 -0
  79. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/pdf/__init__.py +0 -0
  80. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
  81. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/pdf/one.pdf +0 -0
  82. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/pdf/two.pdf +0 -0
  83. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/periodic.py +0 -0
  84. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/protocol_constants.py +0 -0
  85. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/samplers.py +0 -0
  86. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/tag.py +0 -0
  87. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/tools/get_history.py +0 -0
  88. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/tools/snmp_client2.py +0 -0
  89. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada/tools/walk.py +0 -0
  90. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada.egg-info/SOURCES.txt +0 -0
  91. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada.egg-info/dependency_links.txt +0 -0
  92. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada.egg-info/entry_points.txt +0 -0
  93. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada.egg-info/requires.txt +0 -0
  94. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/src/pymscada.egg-info/top_level.txt +0 -0
  95. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_alarms.py +0 -0
  96. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_bus_server.py +0 -0
  97. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_callout.py +0 -0
  98. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_config.py +0 -0
  99. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_history.py +0 -0
  100. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_misc.py +0 -0
  101. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_opnotes.py +0 -0
  102. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_periodic.py +0 -0
  103. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_samplers.py +0 -0
  104. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_sms.py +0 -0
  105. {pymscada-0.2.6b2 → pymscada-0.2.6b4}/tests/test_tag.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymscada
3
- Version: 0.2.6b2
3
+ Version: 0.2.6b4
4
4
  Summary: Shared tag value SCADA with python backup and Angular UI
5
5
  Author-email: Jamie Walton <jamie@walton.net.nz>
6
6
  License: GPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pymscada"
3
- version = "0.2.6b2"
3
+ version = "0.2.6b4"
4
4
  description = "Shared tag value SCADA with python backup and Angular UI"
5
5
  authors = [
6
6
  {name = "Jamie Walton", email = "jamie@walton.net.nz"},
@@ -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.iodrivers.modbus_map import ModbusMaps
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
- mbap_tr, mbap_pr, mbap_len = unpack_from(">3H", self.buffer, start)
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: ModbusMaps):
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
- del self.sent[mbap_tr]
117
- elif pdu_fc == 16:
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.warn(f'start_connection {e}')
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.warn(f"no support for {file}")
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.warn(f"no support for {file}")
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, serve on ip:port for webclient.
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 = ModbusMaps(tags)
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 web server."""
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.split(':')
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymscada
3
- Version: 0.2.6b2
3
+ Version: 0.2.6b4
4
4
  Summary: Shared tag value SCADA with python backup and Angular UI
5
5
  Author-email: Jamie Walton <jamie@walton.net.nz>
6
6
  License: GPL-3.0-or-later
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes