pymscada 0.0.13__tar.gz → 0.0.15__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.

Potentially problematic release.


This version of pymscada might be problematic. Click here for more details.

Files changed (75) hide show
  1. {pymscada-0.0.13 → pymscada-0.0.15}/PKG-INFO +27 -7
  2. {pymscada-0.0.13 → pymscada-0.0.15}/README.md +25 -6
  3. {pymscada-0.0.13 → pymscada-0.0.15}/pyproject.toml +2 -1
  4. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/logixclient.yaml +21 -16
  5. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/pymscada-io-logixclient.service +2 -2
  6. pymscada-0.0.15/src/pymscada/demo/pymscada-io-snmpclient.service +15 -0
  7. pymscada-0.0.15/src/pymscada/demo/snmpclient.yaml +73 -0
  8. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/tags.yaml +48 -0
  9. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/iodrivers/logix_map.py +61 -46
  10. pymscada-0.0.15/src/pymscada/iodrivers/snmp_client.py +75 -0
  11. pymscada-0.0.15/src/pymscada/iodrivers/snmp_client2.py +53 -0
  12. pymscada-0.0.15/src/pymscada/iodrivers/snmp_map.py +71 -0
  13. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/main.py +5 -0
  14. pymscada-0.0.15/src/pymscada/tools/walk.py +55 -0
  15. {pymscada-0.0.13 → pymscada-0.0.15}/LICENSE +0 -0
  16. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/__init__.py +0 -0
  17. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/__main__.py +0 -0
  18. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/bus_client.py +0 -0
  19. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/bus_server.py +0 -0
  20. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/checkout.py +0 -0
  21. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/config.py +0 -0
  22. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/console.py +0 -0
  23. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/README.md +0 -0
  24. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/__init__.py +0 -0
  25. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/bus.yaml +0 -0
  26. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/files.yaml +0 -0
  27. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/history.yaml +0 -0
  28. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/modbus_plc.py +0 -0
  29. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/modbusclient.yaml +0 -0
  30. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/modbusserver.yaml +0 -0
  31. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/pymscada-bus.service +0 -0
  32. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
  33. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/pymscada-files.service +0 -0
  34. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/pymscada-history.service +0 -0
  35. /pymscada-0.0.13/src/pymscada/demo/pymscada-modbusclient.service → /pymscada-0.0.15/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
  36. /pymscada-0.0.13/src/pymscada/demo/pymscada-modbusserver.service → /pymscada-0.0.15/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
  37. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
  38. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/simulate.yaml +0 -0
  39. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/demo/wwwserver.yaml +0 -0
  40. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/files.py +0 -0
  41. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/history.py +0 -0
  42. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/iodrivers/__init__.py +0 -0
  43. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/iodrivers/logix_client.py +0 -0
  44. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/iodrivers/modbus_client.py +0 -0
  45. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/iodrivers/modbus_map.py +0 -0
  46. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/iodrivers/modbus_server.py +0 -0
  47. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/misc.py +0 -0
  48. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/pdf/__init__.py +0 -0
  49. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/pdf/one.pdf +0 -0
  50. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/pdf/two.pdf +0 -0
  51. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/periodic.py +0 -0
  52. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/protocol_constants.py +0 -0
  53. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/samplers.py +0 -0
  54. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/simulate.py +0 -0
  55. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/tag.py +0 -0
  56. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/validate.py +0 -0
  57. {pymscada-0.0.13 → pymscada-0.0.15}/src/pymscada/www_server.py +0 -0
  58. {pymscada-0.0.13 → pymscada-0.0.15}/tests/__init__.py +0 -0
  59. {pymscada-0.0.13 → pymscada-0.0.15}/tests/bus_echo.py +0 -0
  60. {pymscada-0.0.13 → pymscada-0.0.15}/tests/iodrivers/test_logix.py +0 -0
  61. {pymscada-0.0.13 → pymscada-0.0.15}/tests/iodrivers/test_modbus.py +0 -0
  62. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_assets/busserver.yaml +0 -0
  63. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_0.dat +0 -0
  64. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_10_2.dat +0 -0
  65. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_15.dat +0 -0
  66. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_26.dat +0 -0
  67. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_50.dat +0 -0
  68. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_bus_server.py +0 -0
  69. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_config.py +0 -0
  70. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_misc.py +0 -0
  71. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_periodic.py +0 -0
  72. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_samplers.py +0 -0
  73. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_tag.py +0 -0
  74. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_tag_history.py +0 -0
  75. {pymscada-0.0.13 → pymscada-0.0.15}/tests/test_validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymscada
3
- Version: 0.0.13
3
+ Version: 0.0.15
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
@@ -16,6 +16,7 @@ Requires-Dist: aiohttp>=3.8.5
16
16
  Requires-Dist: pymscada-html<=0.1.0
17
17
  Requires-Dist: cerberus>=1.3.5
18
18
  Requires-Dist: pycomm3>=1.2.14
19
+ Requires-Dist: pysnmplib>=5.0.24
19
20
  Description-Content-Type: text/markdown
20
21
 
21
22
  # pymscada
@@ -25,20 +26,39 @@ Description-Content-Type: text/markdown
25
26
 
26
27
  ## Python Mobile SCADA
27
28
 
29
+ This is an open source Python SCADA package intended for use over your
30
+ mobile phone. It has been developed to connect custom python scripts to
31
+ Modicon and Rockwell PLCs, and present a web based user interface, with
32
+ minimal coding. ```pymscada``` does expect you to be able to use python.
33
+
34
+ ```pymscada``` shares values through a ```Tag``` for all data. Tag values
35
+ are updated by exception through a message bus. Changes are in any
36
+ direction with simple loops stopped with a simplistic _don't send updates
37
+ back to where they came from_ check. It's too simple to stop you defeating
38
+ it (but there are easier ways to break the code).
39
+
40
+ There are primary owner Tag's. A _request to set_ command is passed to the
41
+ author of the Tag, where it is updated. This allows database information
42
+ to pass through the Tag values as well. Tag values are typically float or
43
+ int however they may also be megabyte sized binary or dictionary values.
44
+
45
+ Example ```systemd``` service files, pymscada module config files and
46
+ custom script examples are included. The example scripts include polling
47
+ weather from tommorrow.io and a python script based Modbus TCP PLC.
48
+
49
+ ## Introduction
50
+
28
51
  This is a small SCADA package that will run on Linux (preferably) or
29
52
  Windows. The server runs as several modules on the host, sharing
30
53
  information through a message bus. A __subset__ of modules is:
31
54
 
32
55
  - Bus server - shares tag values with by exception updates
33
56
  - Modbus client - reads and writes to a PLC using Modbus/TCP
57
+ - Logix client - uses ```pycomm3``` to read / write to Rockwell PLCs
58
+ - SNMP client - polls SNMP OID values
34
59
  - History - saves data changes, serves history to web pages
35
60
  - Web server - serves web pages which connect with a web socket
36
- - Web pages - an Angular single page web application
37
-
38
- Web pages are responsive and defined procedurally from the
39
- ```wwwserver.yaml``` config file.
40
-
41
- Trends use [uPlot](https://github.com/leeoniya/uPlot).
61
+ - Web pages - procedurally generated setpoint, indication and trends
42
62
 
43
63
  ## Objectives
44
64
 
@@ -5,20 +5,39 @@
5
5
 
6
6
  ## Python Mobile SCADA
7
7
 
8
+ This is an open source Python SCADA package intended for use over your
9
+ mobile phone. It has been developed to connect custom python scripts to
10
+ Modicon and Rockwell PLCs, and present a web based user interface, with
11
+ minimal coding. ```pymscada``` does expect you to be able to use python.
12
+
13
+ ```pymscada``` shares values through a ```Tag``` for all data. Tag values
14
+ are updated by exception through a message bus. Changes are in any
15
+ direction with simple loops stopped with a simplistic _don't send updates
16
+ back to where they came from_ check. It's too simple to stop you defeating
17
+ it (but there are easier ways to break the code).
18
+
19
+ There are primary owner Tag's. A _request to set_ command is passed to the
20
+ author of the Tag, where it is updated. This allows database information
21
+ to pass through the Tag values as well. Tag values are typically float or
22
+ int however they may also be megabyte sized binary or dictionary values.
23
+
24
+ Example ```systemd``` service files, pymscada module config files and
25
+ custom script examples are included. The example scripts include polling
26
+ weather from tommorrow.io and a python script based Modbus TCP PLC.
27
+
28
+ ## Introduction
29
+
8
30
  This is a small SCADA package that will run on Linux (preferably) or
9
31
  Windows. The server runs as several modules on the host, sharing
10
32
  information through a message bus. A __subset__ of modules is:
11
33
 
12
34
  - Bus server - shares tag values with by exception updates
13
35
  - Modbus client - reads and writes to a PLC using Modbus/TCP
36
+ - Logix client - uses ```pycomm3``` to read / write to Rockwell PLCs
37
+ - SNMP client - polls SNMP OID values
14
38
  - History - saves data changes, serves history to web pages
15
39
  - Web server - serves web pages which connect with a web socket
16
- - Web pages - an Angular single page web application
17
-
18
- Web pages are responsive and defined procedurally from the
19
- ```wwwserver.yaml``` config file.
20
-
21
- Trends use [uPlot](https://github.com/leeoniya/uPlot).
40
+ - Web pages - procedurally generated setpoint, indication and trends
22
41
 
23
42
  ## Objectives
24
43
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pymscada"
3
- version = "0.0.13"
3
+ version = "0.0.15"
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" },
@@ -11,6 +11,7 @@ dependencies = [
11
11
  "pymscada-html<=0.1.0",
12
12
  "cerberus>=1.3.5",
13
13
  "pycomm3>=1.2.14",
14
+ "pysnmplib>=5.0.24",
14
15
  ]
15
16
  requires-python = ">=3.9"
16
17
  readme = "README.md"
@@ -9,37 +9,42 @@ rtus:
9
9
  - {addr: Iout, type: 'DINT[]', start: 0, end: 99}
10
10
  - {addr: OutVar, type: REAL}
11
11
  writeok:
12
- - {addr: Fin, start: 0, end: 99}
13
- - {addr: Iin, start: 0, end: 99}
12
+ - {addr: Fin, type: 'REAL[]', start: 0, end: 99}
13
+ - {addr: Iin, type: 'REAL[]', start: 0, end: 99}
14
14
  - {addr: InVar, type: REAL}
15
15
  tags:
16
+ Ani_Fin_20:
17
+ type: float32
18
+ read: 'Ani:Fout[20]'
19
+ write: 'Ani:Fin[20]'
16
20
  Ani_Fout_20:
17
21
  type: float32
18
22
  addr: 'Ani:Fout[20]'
23
+ Ani_Iin_20:
24
+ type: int32
25
+ read: 'Ani:Iout[20]'
26
+ write: 'Ani:Iin[20]'
19
27
  Ani_Iout_20:
20
28
  type: int32
21
29
  addr: 'Ani:Iout[20]'
30
+ Ani_Iin_21_0:
31
+ type: bool
32
+ read: 'Ani:Iout[21].0'
33
+ write: 'Ani:Iin[21].0'
22
34
  Ani_Iout_21_0:
23
35
  type: bool
24
36
  addr: 'Ani:Iout[21].0'
37
+ Ani_Iin_21_1:
38
+ type: bool
39
+ read: 'Ani:Iout[21].1'
40
+ write: 'Ani:Iin[21].1'
25
41
  Ani_Iout_21_1:
26
42
  type: bool
27
43
  addr: 'Ani:Iout[21].1'
28
- Ani_Fin_20:
29
- type: float32
30
- addr: 'Ani:Fin[20]'
31
- Ani_Iin_20:
32
- type: int32
33
- addr: 'Ani:Iin[20]'
34
44
  InVar:
35
45
  type: float32
36
- addr: Ani:InVar
46
+ read: Ani:OutVar
47
+ write: Ani:InVar
37
48
  OutVar:
38
49
  type: float32
39
- addr: Ani:OutVar
40
- Ani_Iin_21_0:
41
- type: bool
42
- addr: 'Ani:Iin[21].0'
43
- Ani_Iin_21_1:
44
- type: bool
45
- addr: 'Ani:Iin[21].1'
50
+ addr: Ani:OutVar
@@ -4,8 +4,8 @@
4
4
  After=pymscada-bus.service
5
5
 
6
6
  [Service]
7
- WorkingDirectory=/home/mscada
8
- ExecStart=/usr/bin/python -m pymscada_pycomm3
7
+ WorkingDirectory=__DIR__
8
+ ExecStart=__PYMSCADA__ logixclient --config __DIR__/config/logixclient.yaml
9
9
  Restart=always
10
10
  RestartSec=5
11
11
  User=mscada
@@ -0,0 +1,15 @@
1
+ [Unit]
2
+ Description=pymscada - SNMP client
3
+ BindsTo=pymscada-bus.service
4
+ After=pymscada-bus.service
5
+
6
+ [Service]
7
+ WorkingDirectory=__DIR__
8
+ ExecStart=__PYMSCADA__ snmpclient --config __DIR__/config/snmpclient.yaml
9
+ Restart=always
10
+ RestartSec=5
11
+ User=mscada
12
+ Group=mscada
13
+
14
+ [Install]
15
+ WantedBy=multi-user.target
@@ -0,0 +1,73 @@
1
+ bus_ip: 127.0.0.1
2
+ bus_port: 1324
3
+ rtus:
4
+ - name: router
5
+ ip: 172.26.3.254
6
+ community: public
7
+ rate: 10
8
+ read:
9
+ - '1.3.6.1.2.1.31.1.1.1.6.1'
10
+ - '1.3.6.1.2.1.31.1.1.1.6.2'
11
+ - '1.3.6.1.2.1.31.1.1.1.6.3'
12
+ - '1.3.6.1.2.1.31.1.1.1.6.4'
13
+ - '1.3.6.1.2.1.31.1.1.1.6.5'
14
+ - '1.3.6.1.2.1.31.1.1.1.6.6'
15
+ - '1.3.6.1.2.1.31.1.1.1.6.7'
16
+ - '1.3.6.1.2.1.31.1.1.1.6.8'
17
+ - '1.3.6.1.2.1.31.1.1.1.10.1'
18
+ - '1.3.6.1.2.1.31.1.1.1.10.2'
19
+ - '1.3.6.1.2.1.31.1.1.1.10.3'
20
+ - '1.3.6.1.2.1.31.1.1.1.10.4'
21
+ - '1.3.6.1.2.1.31.1.1.1.10.5'
22
+ - '1.3.6.1.2.1.31.1.1.1.10.6'
23
+ - '1.3.6.1.2.1.31.1.1.1.10.7'
24
+ - '1.3.6.1.2.1.31.1.1.1.10.8'
25
+ tags:
26
+ Router_eth1_bytes_in:
27
+ type: int_roc
28
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.6.1'
29
+ Router_eth1_bytes_out:
30
+ type: int_roc
31
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.10.1'
32
+ Router_eth2_bytes_in:
33
+ type: int_roc
34
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.6.2'
35
+ Router_eth2_bytes_out:
36
+ type: int_roc
37
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.10.2'
38
+ Router_eth3_bytes_in:
39
+ type: int_roc
40
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.6.3'
41
+ Router_eth3_bytes_out:
42
+ type: int_roc
43
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.10.3'
44
+ Router_eth4_bytes_in:
45
+ type: int_roc
46
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.6.4'
47
+ Router_eth4_bytes_out:
48
+ type: int_roc
49
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.10.4'
50
+ Router_eth5_bytes_in:
51
+ type: int_roc
52
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.6.5'
53
+ Router_eth5_bytes_out:
54
+ type: int_roc
55
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.10.5'
56
+ Router_eth6_bytes_in:
57
+ type: int_roc
58
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.6.6'
59
+ Router_eth6_bytes_out:
60
+ type: int_roc
61
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.10.6'
62
+ Router_eth7_bytes_in:
63
+ type: int_roc
64
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.6.7'
65
+ Router_eth7_bytes_out:
66
+ type: int_roc
67
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.10.7'
68
+ Router_eth8_bytes_in:
69
+ type: int_roc
70
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.6.8'
71
+ Router_eth8_bytes_out:
72
+ type: int_roc
73
+ addr: 'router:1.3.6.1.2.1.31.1.1.1.10.8'
@@ -165,4 +165,52 @@ sim_DateTimeSet:
165
165
  type: int
166
166
  sim_DateTimeVal:
167
167
  desc: Simulation tag
168
+ type: int
169
+ Router_eth1_bytes_in:
170
+ desc: eth1 bytes in
171
+ type: int
172
+ Router_eth1_bytes_out:
173
+ desc: eth1 bytes out
174
+ type: int
175
+ Router_eth2_bytes_in:
176
+ desc: eth2 bytes in
177
+ type: int
178
+ Router_eth2_bytes_out:
179
+ desc: eth2 bytes out
180
+ type: int
181
+ Router_eth3_bytes_in:
182
+ desc: eth3 bytes in
183
+ type: int
184
+ Router_eth3_bytes_out:
185
+ desc: eth3 bytes out
186
+ type: int
187
+ Router_eth4_bytes_in:
188
+ desc: eth4 bytes in
189
+ type: int
190
+ Router_eth4_bytes_out:
191
+ desc: eth4 bytes out
192
+ type: int
193
+ Router_eth5_bytes_in:
194
+ desc: eth5 bytes in
195
+ type: int
196
+ Router_eth5_bytes_out:
197
+ desc: eth5 bytes out
198
+ type: int
199
+ Router_eth6_bytes_in:
200
+ desc: eth6 bytes in
201
+ type: int
202
+ Router_eth6_bytes_out:
203
+ desc: eth6 bytes out
204
+ type: int
205
+ Router_eth7_bytes_in:
206
+ desc: eth7 bytes in
207
+ type: int
208
+ Router_eth7_bytes_out:
209
+ desc: eth7 bytes out
210
+ type: int
211
+ Router_eth8_bytes_in:
212
+ desc: eth8 bytes in
213
+ type: int
214
+ Router_eth8_bytes_out:
215
+ desc: eth8 bytes out
168
216
  type: int
@@ -12,42 +12,51 @@ DTYPES = {
12
12
  }
13
13
 
14
14
 
15
+ def tag_split(plc_tag: str):
16
+ separator = plc_tag.find(':')
17
+ arr_start_loc = plc_tag.find('[')
18
+ arr_end_loc = plc_tag.find(']')
19
+ bit_loc = plc_tag.find('.')
20
+ plc = plc_tag[:separator]
21
+ if arr_start_loc == -1 and bit_loc == -1:
22
+ var = plc_tag[separator + 1:]
23
+ elm = None
24
+ bit = None
25
+ elif arr_start_loc == -1:
26
+ var = plc_tag[separator + 1:bit_loc]
27
+ elm = None
28
+ bit = int(plc_tag[bit_loc + 1:])
29
+ elif bit_loc == -1:
30
+ var = plc_tag[separator + 1:arr_start_loc]
31
+ elm = int(plc_tag[arr_start_loc + 1:arr_end_loc])
32
+ bit = None
33
+ else:
34
+ var = plc_tag[separator + 1:arr_start_loc]
35
+ elm = int(plc_tag[arr_start_loc + 1:arr_end_loc])
36
+ bit = int(plc_tag[bit_loc + 1:])
37
+ return plc, var, elm, bit
38
+
39
+
15
40
  class LogixMap:
16
41
  """Do value updates for each tag."""
17
42
 
18
- def __init__(self, tagname: str, src_type: str, plc_tag: str):
43
+ def __init__(self, tagname: str, src_type: str, read_tag: str,
44
+ write_tag: str):
19
45
  """Initialise modbus map and Tag."""
20
46
  dtype, dmin, dmax = DTYPES[src_type][0:3]
21
47
  self.tag = Tag(tagname, dtype)
22
48
  self.map_bus = id(self)
23
- plc_loc = plc_tag.find(':')
24
- arr_start_loc = plc_tag.find('[')
25
- arr_end_loc = plc_tag.find(']')
26
- bit_loc = plc_tag.find('.')
27
- self.plc = plc_tag[:plc_loc]
28
- if arr_start_loc == -1 and bit_loc == -1:
29
- self.var = plc_tag[plc_loc + 1:]
30
- self.elm = None
31
- self.bit = None
32
- elif arr_start_loc == -1:
33
- self.var = plc_tag[plc_loc + 1:bit_loc]
34
- self.elm = None
35
- self.bit = int(plc_tag[bit_loc + 1:])
36
- elif bit_loc == -1:
37
- self.var = plc_tag[plc_loc + 1:arr_start_loc]
38
- self.elm = int(plc_tag[arr_start_loc + 1:arr_end_loc])
39
- self.bit = None
40
- else:
41
- self.var = plc_tag[plc_loc + 1:arr_start_loc]
42
- self.elm = int(plc_tag[arr_start_loc + 1:arr_end_loc])
43
- self.bit = int(plc_tag[bit_loc + 1:])
44
- self.plc_tag = plc_tag
49
+ self.read_plc, self.read_var, self.read_elm, self.read_bit = \
50
+ tag_split(read_tag)
51
+ self.plc_read_tag = read_tag
52
+ self.write_plc, self.write_var, self.write_elm, self.write_bit = \
53
+ tag_split(write_tag)
54
+ self.plc_write_tag = write_tag
45
55
  self.callback = None
46
56
  if dmin is not None:
47
57
  self.tag.value_min = dmin
48
58
  if dmax is not None:
49
59
  self.tag.value_max = dmax
50
- self.write_cb = None # used?
51
60
 
52
61
  def set_callback(self, callback):
53
62
  """Add tag callback interface."""
@@ -56,8 +65,8 @@ class LogixMap:
56
65
 
57
66
  def set_tag_value(self, value, time_us):
58
67
  """Pass update from IO driver to tag value."""
59
- if self.bit is not None:
60
- if value & 1 << self.bit:
68
+ if self.read_bit is not None:
69
+ if value & 1 << self.read_bit:
61
70
  value = 1
62
71
  else:
63
72
  value = 0
@@ -66,18 +75,18 @@ class LogixMap:
66
75
 
67
76
  def tag_value_changed(self, tag: Tag):
68
77
  """Pass update from tag value to IO driver."""
69
- if self.elm is None and self.bit is None:
70
- addr = self.var
71
- elif self.elm is None:
72
- addr = f'{self.var}.{self.bit}'
73
- elif self.bit is None:
74
- addr = f'{self.var}[{self.elm}]'
78
+ if self.write_elm is None and self.write_bit is None:
79
+ addr = self.write_var
80
+ elif self.write_elm is None:
81
+ addr = f'{self.write_var}.{self.write_bit}'
82
+ elif self.write_bit is None:
83
+ addr = f'{self.write_var}[{self.write_elm}]'
75
84
  else:
76
- addr = f'{self.var}[{self.elm}].{self.bit}'
85
+ addr = f'{self.write_var}[{self.write_elm}].{self.write_bit}'
77
86
  self.callback(addr, tag.value)
78
87
 
79
88
 
80
- class LogixMaps():
89
+ class LogixMaps:
81
90
  """Link tags with protocol connector."""
82
91
 
83
92
  def __init__(self, tags: dict):
@@ -85,16 +94,21 @@ class LogixMaps():
85
94
  # use the tagname to access the map.
86
95
  self.tag_map: dict[str, LogixMap] = {}
87
96
  # use the plc_name then variable name to access a list of maps.
88
- self.var_map: dict[str, dict[str, list[LogixMap]]] = {}
97
+ self.read_var_map: dict[str, dict[str, list[LogixMap]]] = {}
89
98
  for tagname, v in tags.items():
90
- addr = v['addr']
91
- map = LogixMap(tagname, v['type'], addr)
92
- if map.plc not in self.var_map:
93
- self.var_map[map.plc] = {}
94
- if map.var not in self.var_map[map.plc]:
99
+ if 'addr' in v:
100
+ read_addr = v['addr']
101
+ write_addr = v['addr']
102
+ else:
103
+ read_addr = v['read']
104
+ write_addr = v['write']
105
+ map = LogixMap(tagname, v['type'], read_addr, write_addr)
106
+ if map.read_plc not in self.read_var_map:
107
+ self.read_var_map[map.read_plc] = {}
108
+ if map.read_var not in self.read_var_map[map.read_plc]:
95
109
  # make a list so multiple bits can map to a word
96
- self.var_map[map.plc][map.var] = []
97
- self.var_map[map.plc][map.var].append(map)
110
+ self.read_var_map[map.read_plc][map.read_var] = []
111
+ self.read_var_map[map.read_plc][map.read_var].append(map)
98
112
  self.tag_map[map.tag.name] = map
99
113
 
100
114
  def add_write_callback(self, plcname, writeok, callback):
@@ -110,7 +124,8 @@ class LogixMaps():
110
124
  # where the mapped tag uses a valid address, add callback to
111
125
  # the connection writer
112
126
  for map in self.tag_map.values():
113
- if map.plc == plcname and (map.var, map.elm) in write_set:
127
+ if map.write_plc == plcname and \
128
+ (map.write_var, map.write_elm) in write_set:
114
129
  map.set_callback(callback)
115
130
 
116
131
  def polled_data(self, plcname, polls):
@@ -121,12 +136,12 @@ class LogixMaps():
121
136
  logging.error(poll.error)
122
137
  arr_start_loc = poll.tag.find('[')
123
138
  if arr_start_loc == -1:
124
- for map in self.var_map[plcname][poll.tag]:
139
+ for map in self.read_var_map[plcname][poll.tag]:
125
140
  map.set_tag_value(poll.value, time_us)
126
141
  else:
127
142
  var = poll.tag[:arr_start_loc]
128
143
  elm = int(poll.tag[arr_start_loc + 1: -1])
129
- for map in self.var_map[plcname][var]:
130
- elm_offset = map.elm - elm
144
+ for map in self.read_var_map[plcname][var]:
145
+ elm_offset = map.read_elm - elm
131
146
  if elm_offset > 0 and elm_offset < len(poll.value):
132
147
  map.set_tag_value(poll.value[elm_offset], time_us)
@@ -0,0 +1,75 @@
1
+ import logging
2
+ import pysnmp.hlapi.asyncio as snmp
3
+ from pymscada.bus_client import BusClient
4
+ from pymscada.periodic import Periodic
5
+ from pymscada.iodrivers.snmp_map import SnmpMaps
6
+
7
+
8
+ class SnmpClientConnector:
9
+ """Poll snmp devices, write and traps are not implemented."""
10
+
11
+ def __init__(self, name: str, ip:str, rate: float, read: list,
12
+ community: str, mapping: SnmpMaps):
13
+ """Set up polling client."""
14
+ self.snmp_name = name
15
+ self.ip = ip
16
+ self.community = community
17
+ self.read_oids = [snmp.ObjectType(snmp.ObjectIdentity(x))
18
+ for x in read]
19
+ self.mapping = mapping
20
+ self.periodic = Periodic(self.poll, rate)
21
+ self.snmp_engine = snmp.SnmpEngine()
22
+
23
+ async def poll(self):
24
+ """Poll data."""
25
+ r = await snmp.getCmd(
26
+ self.snmp_engine,
27
+ snmp.CommunityData(self.community),
28
+ snmp.UdpTransportTarget((self.ip, 161)),
29
+ snmp.ContextData(),
30
+ *self.read_oids
31
+ )
32
+ errorIndication, errorStatus, errorIndex, varBinds = r
33
+ if errorIndication:
34
+ logging.error(errorIndication)
35
+ elif errorStatus:
36
+ logging.error('%s at %s' % (
37
+ errorStatus.prettyPrint(),
38
+ errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
39
+ else:
40
+ self.mapping.polled_data(self.snmp_name, varBinds)
41
+
42
+ async def start(self):
43
+ """Start polling."""
44
+ await self.periodic.start()
45
+
46
+
47
+ class SnmpClient:
48
+
49
+ def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
50
+ rtus: dict = {}, tags: dict = {}) -> None:
51
+ """
52
+ Connect to bus on bus_ip:bus_port, connect to snmp devices.
53
+
54
+ Event loop must be running.
55
+ """
56
+ self.busclient = None
57
+ if bus_ip is not None:
58
+ self.busclient = BusClient(bus_ip, bus_port)
59
+ self.mapping = SnmpMaps(tags)
60
+ self.connections: list[SnmpClientConnector] = []
61
+ for rtu in rtus:
62
+ connection = SnmpClientConnector(**rtu, mapping=self.mapping)
63
+ self.connections.append(connection)
64
+
65
+ async def _poll(self):
66
+ """For testing."""
67
+ for connection in self.connections:
68
+ await connection.poll()
69
+
70
+ async def start(self):
71
+ """Start bus connection and PLC polling."""
72
+ if self.busclient is not None:
73
+ await self.busclient.start()
74
+ for connection in self.connections:
75
+ await connection.start()
@@ -0,0 +1,53 @@
1
+ import asyncio
2
+ from pysnmp.hlapi.asyncio import *
3
+
4
+
5
+ async def run():
6
+ base = [
7
+ '1.3.6.1.2.1.2.2.1.2.', # name
8
+ # '.1.3.6.1.2.1.2.2.1.4.', # mtu
9
+ # '.1.3.6.1.2.1.2.2.1.6.', # mac address
10
+ # '.1.3.6.1.2.1.2.2.1.7.', # admin status
11
+ # '.1.3.6.1.2.1.2.2.1.8.', # oper status
12
+ '1.3.6.1.2.1.31.1.1.1.6.', # bytes in
13
+ # '.1.3.6.1.2.1.31.1.1.1.7.', # packets in
14
+ # '.1.3.6.1.2.1.2.2.1.13.', # discards in
15
+ # '.1.3.6.1.2.1.2.2.1.14.', # errors in
16
+ '1.3.6.1.2.1.31.1.1.1.10.', # bytes out
17
+ # '.1.3.6.1.2.1.31.1.1.1.11.', # packets out
18
+ # '.1.3.6.1.2.1.2.2.1.19.', # discards out
19
+ # '.1.3.6.1.2.1.2.2.1.20.', # errors out
20
+ ]
21
+ oids = []
22
+ for i in range(1, 9):
23
+ for b in base:
24
+ oids.append(ObjectType(ObjectIdentity(f'{b}{i}')))
25
+ ip_address = '172.26.3.254'
26
+ community = 'public'
27
+
28
+ snmp_engine = SnmpEngine()
29
+
30
+ r = await getCmd(
31
+ snmp_engine,
32
+ CommunityData(community),
33
+ UdpTransportTarget((ip_address, 161)),
34
+ ContextData(),
35
+ *oids
36
+ )
37
+ errorIndication, errorStatus, errorIndex, varBinds = r
38
+ if errorIndication:
39
+ print(errorIndication)
40
+ elif errorStatus:
41
+ print('%s at %s' % (
42
+ errorStatus.prettyPrint(),
43
+ errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
44
+ else:
45
+ for varBind in varBinds:
46
+ oid, value = varBind
47
+ print(str(oid), type(oid), str(value), type(value))
48
+ # snmp_engine.transportDispatcher.closeDispatcher()
49
+
50
+
51
+ if __name__ == '__main__':
52
+ """Starts with creating an event loop."""
53
+ asyncio.run(run())
@@ -0,0 +1,71 @@
1
+ """Map between snmp MIB and Tag."""
2
+ import logging
3
+ from time import time
4
+ from pymscada.tag import Tag
5
+
6
+
7
+ # data types for MIBs
8
+ DTYPES = {
9
+ 'int_roc': [int]
10
+ }
11
+
12
+
13
+ class SnmpMap:
14
+ """Do value updates for each tag."""
15
+
16
+ def __init__(self, tagname: str, src_type: str, plc_tag: str):
17
+ """initialise MIB map and Tag."""
18
+ dtype = DTYPES[src_type][0]
19
+ self.last_value = None
20
+ self.tag = Tag(tagname, dtype)
21
+ self.map_bus = id(self)
22
+ separator = plc_tag.find(':')
23
+ self.plc = plc_tag[:separator]
24
+ self.var = plc_tag[separator + 1:]
25
+
26
+ def set_tag_value(self, value, time_us):
27
+ """Pass update from IO driver to tag value."""
28
+ vtype = type(value).__name__
29
+ if vtype == 'Counter64':
30
+ v = int(value)
31
+ if self.last_value is None:
32
+ self.last_value = v
33
+ return
34
+ d = v - self.last_value
35
+ self.last_value = v
36
+ if d < 0:
37
+ d += 2**64
38
+ if self.tag.value != d:
39
+ self.tag.value = d, time_us, self.map_bus
40
+ else:
41
+ logging.warning(
42
+ f'SnmpMap: {self.tag.name} {vtype} not implemented')
43
+
44
+
45
+ class SnmpMaps:
46
+ """Link tags with protocol connector."""
47
+
48
+ def __init__(self, tags: dict):
49
+ """Collect maps based on a tag dictionary."""
50
+ # use the tagname to access the map.
51
+ self.tag_map: dict[str, SnmpMap] = {}
52
+ # use the plc_name then variable name to access a list of maps.
53
+ self.var_map: dict[str, dict[str, list[SnmpMap]]] = {}
54
+ for tagname, v in tags.items():
55
+ addr = v['addr']
56
+ map = SnmpMap(tagname, v['type'], addr)
57
+ if map.plc not in self.var_map:
58
+ self.var_map[map.plc] = {}
59
+ if map.var not in self.var_map[map.plc]:
60
+ # make a list so multiple bits can map to a word
61
+ self.var_map[map.plc][map.var] = []
62
+ self.var_map[map.plc][map.var].append(map)
63
+ self.tag_map[map.tag.name] = map
64
+
65
+ def polled_data(self, plcname, polls):
66
+ """Pass updates read from the PLC to the tags."""
67
+ time_us = int(time() * 1e6)
68
+ for poll in polls:
69
+ oid, value = poll
70
+ for map in self.var_map[plcname][str(oid)]:
71
+ map.set_tag_value(value, time_us)
@@ -11,6 +11,7 @@ from pymscada.history import History
11
11
  from pymscada.iodrivers.logix_client import LogixClient
12
12
  from pymscada.iodrivers.modbus_client import ModbusClient
13
13
  from pymscada.iodrivers.modbus_server import ModbusServer
14
+ from pymscada.iodrivers.snmp_client import SnmpClient
14
15
  from pymscada.simulate import Simulate
15
16
  from pymscada.www_server import WwwServer
16
17
  from pymscada.validate import validate
@@ -26,6 +27,7 @@ def args():
26
27
  commands = ['bus', 'console', 'wwwserver', 'history', 'files',
27
28
  'logixclient',
28
29
  'modbusserver', 'modbusclient',
30
+ 'snmpclient',
29
31
  'simulate', 'checkout',
30
32
  'validate']
31
33
  parser.add_argument('module', type=str, choices=commands, metavar='action',
@@ -77,6 +79,9 @@ async def run():
77
79
  elif options.module == 'modbusserver':
78
80
  config = Config(options.config)
79
81
  module = ModbusServer(**config)
82
+ elif options.module == 'snmpclient':
83
+ config = Config(options.config)
84
+ module = SnmpClient(**config)
80
85
  elif options.module == 'simulate':
81
86
  config = Config(options.config)
82
87
  tag_info = dict(Config(options.tags))
@@ -0,0 +1,55 @@
1
+ """
2
+ Walk whole MIB
3
+ ++++++++++++++
4
+
5
+ Send a series of SNMP GETNEXT requests using the following options:
6
+
7
+ * with SNMPv3, user 'usr-md5-none', MD5 authentication, no privacy
8
+ * over IPv4/UDP
9
+ * to an Agent at demo.snmplabs.com:161
10
+ * for all OIDs in IF-MIB
11
+
12
+ Functionally similar to:
13
+
14
+ | $ snmpwalk -v3 -lauthPriv -u usr-md5-none -A authkey1 -X privkey1 demo.snmplabs.com IF-MIB::
15
+
16
+ """#
17
+ from pysnmp.hlapi import *
18
+
19
+ mibs = {
20
+ 'MIKROTIK-MIB': '1.3.6.1.4.1.14988',
21
+ 'MIB-2': '1.3.6.1.2.1',
22
+ 'HOST-RESOURCES-MIB': '1.3.6.1.2.1.25',
23
+ 'IF-MIB': '1.3.6.1.2.1.2',
24
+ 'IP-MIB': '1.3.6.1.2.1.4',
25
+ 'IP-FORWARD-MIB': '1.3.6.1.2.1.4.21',
26
+ 'IPV6-MIB': '1.3.6.1.2.1.55',
27
+ 'BRIDGE-MIB': '1.3.6.1.2.1.17',
28
+ 'DHCP-SERVER-MIB': '1.3.6.1.4.1.14988.1.1.8',
29
+ 'CISCO-AAA-SESSION-MIB': '1.3.6.1.4.1.9.9.39',
30
+ 'ENTITY-MIB': '1.3.6.1.2.1.47',
31
+ 'UPS-MIB': '1.3.6.1.2.1.33',
32
+ 'SQUID-MIB': '1.3.6.1.4.1.3495',
33
+ }
34
+
35
+ iterator = nextCmd(
36
+ SnmpEngine(),
37
+ UsmUserData('public'),
38
+ UdpTransportTarget(('172.26.3.254', 161)),
39
+ ContextData(),
40
+ # ObjectType(ObjectIdentity('1.3.6.1.2.1.1.1.0')),
41
+ # ObjectType(ObjectIdentity('1.3.6.1.2.1.1.6.0'))
42
+ ObjectType(ObjectIdentity(mibs['MIB-2']))
43
+ )
44
+
45
+ for errorIndication, errorStatus, errorIndex, varBinds in iterator:
46
+ if errorIndication:
47
+ print(errorIndication)
48
+ break
49
+ elif errorStatus:
50
+ print('%s at %s' % (errorStatus.prettyPrint(),
51
+ errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
52
+ break
53
+ else:
54
+ for varBind in varBinds:
55
+ print(' = '.join([x.prettyPrint() for x in varBind]))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes