pymeu 0.0.1__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.
- pymeu-0.0.1/LICENSE +21 -0
- pymeu-0.0.1/PKG-INFO +48 -0
- pymeu-0.0.1/README.md +39 -0
- pymeu-0.0.1/pymeu/__init__.py +4 -0
- pymeu-0.0.1/pymeu/constants.py +32 -0
- pymeu-0.0.1/pymeu/main.py +294 -0
- pymeu-0.0.1/pymeu/messages.py +80 -0
- pymeu-0.0.1/pymeu/terminal.py +468 -0
- pymeu-0.0.1/pymeu/types.py +30 -0
- pymeu-0.0.1/pymeu.egg-info/PKG-INFO +48 -0
- pymeu-0.0.1/pymeu.egg-info/SOURCES.txt +14 -0
- pymeu-0.0.1/pymeu.egg-info/dependency_links.txt +1 -0
- pymeu-0.0.1/pymeu.egg-info/requires.txt +1 -0
- pymeu-0.0.1/pymeu.egg-info/top_level.txt +1 -0
- pymeu-0.0.1/pyproject.toml +11 -0
- pymeu-0.0.1/setup.cfg +4 -0
pymeu-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 aawilliams85
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pymeu-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pymeu
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python ME Utility
|
|
5
|
+
Author-email: aawilliams85 <adair.williams85@gmail.com>
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pycomm3
|
|
9
|
+
|
|
10
|
+
# pymeu
|
|
11
|
+
|
|
12
|
+
PyMEU (Python ME Utility) is a set of helper functions built around pycomm3 to interface with Rockwell Automation PanelView Plus 6/7 HMIs.<br>
|
|
13
|
+
|
|
14
|
+
The current release has only been tested on a very limited selection of hardware. It is alpha quality at best and could easily brick your device. Use at your own risk.<br>
|
|
15
|
+
|
|
16
|
+
## Example
|
|
17
|
+
|
|
18
|
+
Use the download function to transfer a *.MER file to the remote terminal:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from pymeu import MEUtility
|
|
22
|
+
meu = MEUtility('192.168.1.20')
|
|
23
|
+
meu.download('C:\\YourFolder\\YourProgram.mer')
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use the upload function to transfer a *.MER file from the remote terminal:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from pymeu import MEUtility
|
|
30
|
+
meu = MEUtility('192.168.1.20')
|
|
31
|
+
meu.upload('C:\\YourFolder\\YourProgram.mer')
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use the upload all function to transfer all *.MER files from the remote terminal:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from pymeu import MEUtility
|
|
38
|
+
meu = MEUtility('192.168.1.20')
|
|
39
|
+
meu.upload_all('C:\\YourFolder')
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use the reboot function to restart the remote terminal:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from pymeu import MEUtility
|
|
46
|
+
meu = MEUtility('192.168.1.20')
|
|
47
|
+
meu.reboot()
|
|
48
|
+
```
|
pymeu-0.0.1/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# pymeu
|
|
2
|
+
|
|
3
|
+
PyMEU (Python ME Utility) is a set of helper functions built around pycomm3 to interface with Rockwell Automation PanelView Plus 6/7 HMIs.<br>
|
|
4
|
+
|
|
5
|
+
The current release has only been tested on a very limited selection of hardware. It is alpha quality at best and could easily brick your device. Use at your own risk.<br>
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
Use the download function to transfer a *.MER file to the remote terminal:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from pymeu import MEUtility
|
|
13
|
+
meu = MEUtility('192.168.1.20')
|
|
14
|
+
meu.download('C:\\YourFolder\\YourProgram.mer')
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use the upload function to transfer a *.MER file from the remote terminal:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from pymeu import MEUtility
|
|
21
|
+
meu = MEUtility('192.168.1.20')
|
|
22
|
+
meu.upload('C:\\YourFolder\\YourProgram.mer')
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use the upload all function to transfer all *.MER files from the remote terminal:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from pymeu import MEUtility
|
|
29
|
+
meu = MEUtility('192.168.1.20')
|
|
30
|
+
meu.upload_all('C:\\YourFolder')
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use the reboot function to restart the remote terminal:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from pymeu import MEUtility
|
|
37
|
+
meu = MEUtility('192.168.1.20')
|
|
38
|
+
meu.reboot()
|
|
39
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
CHUNK_SIZE = 1984
|
|
2
|
+
|
|
3
|
+
HELPER_VERSIONS = {
|
|
4
|
+
'11.00.00'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
ME_VERSIONS = {
|
|
8
|
+
'11.00.25.230'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
PRODUCT_CODES = {
|
|
12
|
+
'102',
|
|
13
|
+
'51'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
PRODUCT_TYPES = {
|
|
17
|
+
'24'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
CREATE_DIR_SUCCESS = 183 # 'b\xb7'
|
|
21
|
+
|
|
22
|
+
GET_UNK1_VALUES = {
|
|
23
|
+
b'\x02\x00'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
GET_UNK2_VALUES = {
|
|
27
|
+
b'\x01\x60'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
GET_UNK3_VALUES = {
|
|
31
|
+
b'\x64\x00'
|
|
32
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pycomm3
|
|
3
|
+
|
|
4
|
+
from .types import *
|
|
5
|
+
from .terminal import *
|
|
6
|
+
|
|
7
|
+
class MEUtility(object):
|
|
8
|
+
def __init__(self, comms_path: str):
|
|
9
|
+
self.comms_path = comms_path
|
|
10
|
+
|
|
11
|
+
def __download_to_terminal(self, cip: pycomm3.CIPDriver, file: MEFile) -> bool:
|
|
12
|
+
# Create runtime folder
|
|
13
|
+
#
|
|
14
|
+
# TODO: Can we check if this already exists and skip?
|
|
15
|
+
if not(terminal_create_runtime_directory(cip, file)): raise Exception('Failed to create runtime path on terminal.')
|
|
16
|
+
|
|
17
|
+
# Get attributes
|
|
18
|
+
#
|
|
19
|
+
# Still no clue on what these are, or when/how they would change.
|
|
20
|
+
# If they aren't changed by creating paths, could be moved ahead
|
|
21
|
+
# to is_download_valid().
|
|
22
|
+
if not(terminal_is_get_unk_valid(cip)): raise Exception('Invalid response from an unknown attribute. Check packets.')
|
|
23
|
+
|
|
24
|
+
# Create a file exchange on the terminal
|
|
25
|
+
file_instance = terminal_create_file_exchange_for_download(cip, file)
|
|
26
|
+
self.device.log.append(f'Create file exchange {file_instance} for download.')
|
|
27
|
+
|
|
28
|
+
# Set attributes
|
|
29
|
+
#
|
|
30
|
+
# Still no clue what this is. Might be setting file up for write?
|
|
31
|
+
if not(terminal_is_set_unk_valid(cip)): raise Exception('Invalid response from an unknown attribute. Check packets.')
|
|
32
|
+
|
|
33
|
+
# Transfer *.MER chunk by chunk
|
|
34
|
+
terminal_file_download_mer(cip, file_instance, file)
|
|
35
|
+
|
|
36
|
+
# Mark file exchange as completed on the terminal
|
|
37
|
+
terminal_end_file_write(cip, file_instance)
|
|
38
|
+
self.device.log.append(f'Downloaded {file.path} to {file.name} using file exchange {file_instance}.')
|
|
39
|
+
|
|
40
|
+
# Delete file exchange on the terminal
|
|
41
|
+
terminal_delete_file_exchange(cip, file_instance)
|
|
42
|
+
self.device.log.append(f'Deleted file exchange {file_instance}.')
|
|
43
|
+
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
def __get_mer_list(self, cip: pycomm3.CIPDriver):
|
|
47
|
+
# Create *.MER list
|
|
48
|
+
terminal_create_mer_list(cip)
|
|
49
|
+
|
|
50
|
+
file_instance = terminal_create_file_exchange_for_mer_list(cip)
|
|
51
|
+
self.device.log.append(f'Create file exchange {file_instance} for upload.')
|
|
52
|
+
|
|
53
|
+
# Transfer *.MER list chunk by chunk
|
|
54
|
+
file_list = terminal_file_upload_mer_list(cip, file_instance)
|
|
55
|
+
self.device.log.append(f'Uploaded *.MER file list using file exchange {file_instance}.')
|
|
56
|
+
|
|
57
|
+
# Delete file exchange on the terminal
|
|
58
|
+
terminal_delete_file_exchange(cip, file_instance)
|
|
59
|
+
self.device.log.append(f'Deleted file exchange {file_instance}.')
|
|
60
|
+
|
|
61
|
+
return file_list
|
|
62
|
+
|
|
63
|
+
def __is_download_valid(self, cip: pycomm3.CIPDriver, file: MEFile) -> bool:
|
|
64
|
+
# Check that file is correct extension
|
|
65
|
+
if (file.get_ext() != '.mer'):
|
|
66
|
+
self.device.log.append(f'File {file.name} is not a *.mer file')
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Check that storage folder exists
|
|
70
|
+
resp_storage_exists = terminal_get_folder_exists(cip)
|
|
71
|
+
if not(resp_storage_exists):
|
|
72
|
+
self.device.log.append(f'Storage folder does not exist on terminal')
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
# Check free space
|
|
76
|
+
resp_free_space = terminal_get_free_space(cip)
|
|
77
|
+
if (resp_free_space > file.get_size()):
|
|
78
|
+
self.device.log.append(f'File {file.name} requires {file.get_size()} byes. Free space on terminal {resp_free_space} bytes.')
|
|
79
|
+
else:
|
|
80
|
+
self.device.log.append(f'File {file.name} requires {file.get_size()} bytes. Free space on terminal {resp_free_space} bytes is insufficient.')
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
# Check if file name already exists
|
|
84
|
+
resp_file_exists = terminal_get_file_exists(cip, file)
|
|
85
|
+
file.overwrite_required = False
|
|
86
|
+
if (resp_file_exists and file.overwrite_requested):
|
|
87
|
+
self.device.log.append(f'File {file.name} already exists on terminal, and overwrite was requested. Setting overwrite to required.')
|
|
88
|
+
file.overwrite_required = True
|
|
89
|
+
if (resp_file_exists and not(file.overwrite_requested)):
|
|
90
|
+
self.device.log.append(f'File {file.name} already exists on terminal, and overwrite was NOT requested. Use kwarg overwrite_requested=True to overwrite existing.')
|
|
91
|
+
return False
|
|
92
|
+
if not(resp_file_exists):
|
|
93
|
+
self.device.log.append(f'File {file.name} does not exist on terminal. Setting overwrite to not required.')
|
|
94
|
+
|
|
95
|
+
# Check space consumed by file if it exists
|
|
96
|
+
if resp_file_exists:
|
|
97
|
+
resp_file_size = terminal_get_file_size(cip, file)
|
|
98
|
+
self.device.log.append(f'File {file.name} on terminal is {resp_file_size} bytes.')
|
|
99
|
+
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
def __reboot(self, comms_path: str):
|
|
103
|
+
cip = pycomm3.CIPDriver(comms_path)
|
|
104
|
+
cip._cfg['socket_timeout'] = 0.25
|
|
105
|
+
cip.open()
|
|
106
|
+
terminal_reboot(cip)
|
|
107
|
+
cip.close()
|
|
108
|
+
|
|
109
|
+
def __get_terminal_info(self, cip: pycomm3.CIPDriver) -> MEDeviceInfo:
|
|
110
|
+
return MEDeviceInfo(self.comms_path,
|
|
111
|
+
terminal_get_helper_version(cip),
|
|
112
|
+
terminal_get_me_version(cip),
|
|
113
|
+
terminal_get_product_code(cip),
|
|
114
|
+
terminal_get_product_type(cip),
|
|
115
|
+
[])
|
|
116
|
+
|
|
117
|
+
def __is_terminal_valid(self, device: MEDeviceInfo) -> bool:
|
|
118
|
+
if device.helper_version not in HELPER_VERSIONS: return False
|
|
119
|
+
if device.me_version not in ME_VERSIONS: return False
|
|
120
|
+
if device.product_code not in PRODUCT_CODES: return False
|
|
121
|
+
if device.product_type not in PRODUCT_TYPES: return False
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def __upload_from_terminal(self, cip: pycomm3.CIPDriver, file: MEFile, rem_file: MEFile) -> bool:
|
|
125
|
+
# Verify file exists on terminal
|
|
126
|
+
if not(terminal_get_file_exists(cip, rem_file)): raise Exception(f'File {rem_file.name} does not exist on terminal.')
|
|
127
|
+
|
|
128
|
+
# Create file exchange
|
|
129
|
+
file_instance = terminal_create_file_exchange_for_upload(cip, rem_file)
|
|
130
|
+
self.device.log.append(f'Create file exchange {file_instance} for upload.')
|
|
131
|
+
|
|
132
|
+
# Transfer *.MER chunk by chunk
|
|
133
|
+
terminal_file_upload_mer(cip, file_instance, file)
|
|
134
|
+
self.device.log.append(f'Uploaded {rem_file.name} to {file.path} using file exchange {file_instance}.')
|
|
135
|
+
|
|
136
|
+
# Delete file exchange on the terminal
|
|
137
|
+
terminal_delete_file_exchange(cip, file_instance)
|
|
138
|
+
self.device.log.append(f'Deleted file exchange {file_instance}.')
|
|
139
|
+
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
def download(self, file_path: str, **kwargs) -> MEResponse:
|
|
143
|
+
#
|
|
144
|
+
# Used to download a *.MER file from this system to the remote Panelview terminal.
|
|
145
|
+
#
|
|
146
|
+
# File path: *.MER path on local system (ex: C:\MyFolder\MyHMI.MER)
|
|
147
|
+
#
|
|
148
|
+
# Optional Keyword Arguments:
|
|
149
|
+
# Delete Logs: Configures terminal to delete logs at startup
|
|
150
|
+
# Ignore Terminal Valid: Attempt to proceed with download, even if target
|
|
151
|
+
# product ID or version does not match tested whitelist.
|
|
152
|
+
# Proceed with caution...
|
|
153
|
+
# Overwrite: If file exists already on remote terminal, replace it.
|
|
154
|
+
# Replace Comms: Configures terminal to replace communications
|
|
155
|
+
# from *.MER at startup.
|
|
156
|
+
# Run At Startup: Configures terminal to run downloaded *.MER at startup.
|
|
157
|
+
# Note that this option must be selected to use Delete Logs or Replace Comms.
|
|
158
|
+
#
|
|
159
|
+
self.delete_logs = kwargs.get('delete_logs', False)
|
|
160
|
+
self.ignore_terminal_valid = kwargs.get('ignore_terminal_valid', False)
|
|
161
|
+
self.overwrite = kwargs.get('overwrite', False)
|
|
162
|
+
self.replace_comms = kwargs.get('replace_comms', False)
|
|
163
|
+
self.run_at_startup = kwargs.get('run_at_startup', True)
|
|
164
|
+
|
|
165
|
+
with pycomm3.CIPDriver(self.comms_path) as cip:
|
|
166
|
+
file = MEFile(os.path.basename(file_path), self.overwrite, False, file_path)
|
|
167
|
+
|
|
168
|
+
# Validate device at this communications path
|
|
169
|
+
# is a terminal of known version.
|
|
170
|
+
#
|
|
171
|
+
# TODO: Test on more hardware to expand validated list
|
|
172
|
+
self.device = self.__get_terminal_info(cip)
|
|
173
|
+
if not(self.__is_terminal_valid(self.device)):
|
|
174
|
+
if self.ignore_terminal_valid:
|
|
175
|
+
warn('Invalid device selected, but terminal validation is set to IGNORE.')
|
|
176
|
+
else:
|
|
177
|
+
raise Exception('Invalid device selected. Use kwarg ignore_terminal_valid=True to proceed at your own risk.')
|
|
178
|
+
|
|
179
|
+
# Validate that all starting conditions for downnload to terminal are as expected
|
|
180
|
+
if not(self.__is_download_valid(cip, file)): raise Exception('Download to terminal is invalid.')
|
|
181
|
+
|
|
182
|
+
# Perform *.MER download to terminal
|
|
183
|
+
if not(self.__download_to_terminal(cip, file)): raise Exception('Download to terminal failed.')
|
|
184
|
+
|
|
185
|
+
# Set *.MER to run at startup and then reboot
|
|
186
|
+
if self.run_at_startup:
|
|
187
|
+
terminal_set_startup_file(cip, file, self.replace_comms, self.delete_logs)
|
|
188
|
+
terminal_reboot(cip)
|
|
189
|
+
|
|
190
|
+
return MEResponse(self.device, 'Success')
|
|
191
|
+
|
|
192
|
+
def get_terminal_info(self) -> MEResponse:
|
|
193
|
+
#
|
|
194
|
+
# Used to print some info about the remote PanelView terminal.
|
|
195
|
+
#
|
|
196
|
+
with pycomm3.CIPDriver(self.comms_path) as cip:
|
|
197
|
+
self.device = self.__get_terminal_info(cip)
|
|
198
|
+
if (self.__is_terminal_valid(self.device)):
|
|
199
|
+
self.device.log.append(f'Terminal storage exists: {terminal_get_folder_exists(cip)}.')
|
|
200
|
+
self.device.log.append(f'Terminal has {terminal_get_free_space(cip)} free bytes')
|
|
201
|
+
self.device.log.append(f'Terminal has files: {self.__get_mer_list(cip)}')
|
|
202
|
+
|
|
203
|
+
return MEResponse(self.device, 'Success')
|
|
204
|
+
|
|
205
|
+
def reboot(self) -> MEResponse:
|
|
206
|
+
#
|
|
207
|
+
# Used to reboot the remote PanelView terminal.
|
|
208
|
+
#
|
|
209
|
+
with pycomm3.CIPDriver(self.comms_path) as cip:
|
|
210
|
+
self.device = self.__get_terminal_info(cip)
|
|
211
|
+
if (self.__is_terminal_valid(self.device)):
|
|
212
|
+
self.__reboot(self.comms_path)
|
|
213
|
+
|
|
214
|
+
return MEResponse(self.device, 'Success')
|
|
215
|
+
|
|
216
|
+
def upload(self, file_path: str, **kwargs) -> MEResponse:
|
|
217
|
+
#
|
|
218
|
+
# Used to upload a *.MER file from the remote PanelView terminal to this system.
|
|
219
|
+
#
|
|
220
|
+
# File Path: Path to upload to on the local system (ex: C:\MyFolder\MyHMI.MER)
|
|
221
|
+
#
|
|
222
|
+
# Optional Keyword Arguments:
|
|
223
|
+
# Overwrite: If the file already exists on the local system, replace it.
|
|
224
|
+
# Remote File Name: Use this to specify a different remote filename on the
|
|
225
|
+
# terminal than where the local file will end up.
|
|
226
|
+
#
|
|
227
|
+
file = MEFile(os.path.basename(file_path), False, False, file_path)
|
|
228
|
+
self.remote_file_name = kwargs.get('remote_file_name', file.name)
|
|
229
|
+
self.overwrite = kwargs.get('overwrite', False)
|
|
230
|
+
rem_file = MEFile(self.remote_file_name,False,False,file_path)
|
|
231
|
+
|
|
232
|
+
# Create upload folder if it doesn't exist yet
|
|
233
|
+
if not(os.path.exists(os.path.dirname(file.path))): os.makedirs(os.path.dirname(file.path))
|
|
234
|
+
|
|
235
|
+
# Check for existing *.MER
|
|
236
|
+
if not(self.overwrite) and (os.path.exists(file.path)): raise Exception(f'File {file.name} already exists. Use kwarg overwrite=True to overwrite existing local file from the remote terminal.')
|
|
237
|
+
|
|
238
|
+
with pycomm3.CIPDriver(self.comms_path) as cip:
|
|
239
|
+
# Validate device at this communications path
|
|
240
|
+
# is a terminal of known version.
|
|
241
|
+
#
|
|
242
|
+
# TODO: Test on more hardware to expand validated list
|
|
243
|
+
self.device = self.__get_terminal_info(cip)
|
|
244
|
+
if not(self.__is_terminal_valid(self.device)):
|
|
245
|
+
if self.ignore_terminal_valid:
|
|
246
|
+
warn('Invalid device selected, but terminal validation is set to IGNORE.')
|
|
247
|
+
else:
|
|
248
|
+
raise Exception('Invalid device selected. Use kwarg ignore_terminal_valid=True to proceed at your own risk.')
|
|
249
|
+
|
|
250
|
+
# Perform *.MER upload from terminal
|
|
251
|
+
if not(self.__upload_from_terminal(cip, file, rem_file)): raise Exception('Upload from terminal failed.')
|
|
252
|
+
|
|
253
|
+
return MEResponse(self.device, 'Success')
|
|
254
|
+
|
|
255
|
+
def upload_all(self, file_path: str, **kwargs):
|
|
256
|
+
#
|
|
257
|
+
# Used to upload all *.MER files from the remote PanelView terminal to this system.
|
|
258
|
+
#
|
|
259
|
+
# File Path: Path to upload to on the local system (ex: C:\MyFolder)
|
|
260
|
+
#
|
|
261
|
+
# Optional Keyword Arguments:
|
|
262
|
+
# Overwrite: If the file already exists on the local system, replace it.
|
|
263
|
+
#
|
|
264
|
+
self.overwrite = kwargs.get('overwrite', False)
|
|
265
|
+
|
|
266
|
+
# Create upload folder if it doesn't exist yet
|
|
267
|
+
if not(os.path.exists(file_path)): os.makedirs(os.path.dirname(file_path))
|
|
268
|
+
|
|
269
|
+
with pycomm3.CIPDriver(self.comms_path) as cip:
|
|
270
|
+
# Validate device at this communications path
|
|
271
|
+
# is a terminal of known version.
|
|
272
|
+
#
|
|
273
|
+
# TODO: Test on more hardware to expand validated list
|
|
274
|
+
self.device = self.__get_terminal_info(cip)
|
|
275
|
+
if not(self.__is_terminal_valid(self.device)):
|
|
276
|
+
if self.ignore_terminal_valid:
|
|
277
|
+
warn('Invalid device selected, but terminal validation is set to IGNORE.')
|
|
278
|
+
else:
|
|
279
|
+
raise Exception('Invalid device selected. Use kwarg ignore_terminal_valid=True to proceed at your own risk.')
|
|
280
|
+
|
|
281
|
+
mer_list = self.__get_mer_list(cip)
|
|
282
|
+
|
|
283
|
+
for mer in mer_list:
|
|
284
|
+
if len(mer) > 0:
|
|
285
|
+
mer_path = os.path.join(file_path, mer)
|
|
286
|
+
file = MEFile(os.path.basename(mer_path), self.overwrite, False, mer_path)
|
|
287
|
+
|
|
288
|
+
# Check for existing *.MER
|
|
289
|
+
if not(self.overwrite) and (os.path.exists(mer_path)): raise Exception(f'File {mer_path} already exists. Use kwarg overwrite=True to overwrite existing local file from the remote terminal.')
|
|
290
|
+
|
|
291
|
+
# Perform *.MER upload from terminal
|
|
292
|
+
if not(self.__upload_from_terminal(cip, file, file)): raise Exception('Upload from terminal failed.')
|
|
293
|
+
|
|
294
|
+
return MEResponse(self.device, 'Success')
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import pycomm3
|
|
2
|
+
|
|
3
|
+
def msg_create_file_exchange(cip: pycomm3.CIPDriver, data):
|
|
4
|
+
return cip.generic_message(
|
|
5
|
+
service=pycomm3.Services.create,
|
|
6
|
+
class_code=0x04ff,
|
|
7
|
+
instance=0x00,
|
|
8
|
+
request_data=data,
|
|
9
|
+
connected=False,
|
|
10
|
+
route_path=None
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def msg_delete_file_exchange(cip: pycomm3.CIPDriver, instance: int):
|
|
14
|
+
return cip.generic_message(
|
|
15
|
+
service=pycomm3.Services.delete,
|
|
16
|
+
class_code=0x04ff,
|
|
17
|
+
instance=instance,
|
|
18
|
+
connected=False,
|
|
19
|
+
route_path=None
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def msg_read_file_chunk(cip: pycomm3.CIPDriver, instance: int, data):
|
|
23
|
+
return cip.generic_message(
|
|
24
|
+
service=0x53,
|
|
25
|
+
class_code=0x04ff,
|
|
26
|
+
instance=instance,
|
|
27
|
+
request_data=data,
|
|
28
|
+
connected=False,
|
|
29
|
+
route_path=None
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def msg_read_registry(cip: pycomm3.CIPDriver, data):
|
|
33
|
+
return cip.generic_message(
|
|
34
|
+
service=0x51,
|
|
35
|
+
class_code=0x04fe,
|
|
36
|
+
instance=0x00,
|
|
37
|
+
request_data=data,
|
|
38
|
+
connected=False,
|
|
39
|
+
route_path=None
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def msg_run_function(cip: pycomm3.CIPDriver, data):
|
|
43
|
+
return cip.generic_message(
|
|
44
|
+
service=0x50,
|
|
45
|
+
class_code=0x04fd,
|
|
46
|
+
instance=0x00,
|
|
47
|
+
request_data=data,
|
|
48
|
+
connected=False,
|
|
49
|
+
route_path=None
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def msg_write_file_chunk(cip: pycomm3.CIPDriver, instance: int, data):
|
|
53
|
+
return cip.generic_message(
|
|
54
|
+
service=0x52,
|
|
55
|
+
class_code=0x04ff,
|
|
56
|
+
instance=instance,
|
|
57
|
+
request_data=data,
|
|
58
|
+
connected=False,
|
|
59
|
+
route_path=None
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def msg_get_attr_unk(cip: pycomm3.CIPDriver, data):
|
|
63
|
+
return cip.generic_message(
|
|
64
|
+
service=pycomm3.Services.get_attribute_single,
|
|
65
|
+
class_code=0x04ff,
|
|
66
|
+
instance=0x00,
|
|
67
|
+
request_data=data,
|
|
68
|
+
connected=False,
|
|
69
|
+
route_path=None
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def msg_set_attr_unk(cip: pycomm3.CIPDriver, data):
|
|
73
|
+
return cip.generic_message(
|
|
74
|
+
service=pycomm3.Services.set_attribute_single,
|
|
75
|
+
class_code=0x04ff,
|
|
76
|
+
instance=0x01,
|
|
77
|
+
request_data=data,
|
|
78
|
+
connected=False,
|
|
79
|
+
route_path=None
|
|
80
|
+
)
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import pycomm3
|
|
2
|
+
import struct
|
|
3
|
+
from warnings import warn
|
|
4
|
+
|
|
5
|
+
from .constants import *
|
|
6
|
+
from .messages import *
|
|
7
|
+
from .types import *
|
|
8
|
+
|
|
9
|
+
def terminal_create_directory(cip: pycomm3.CIPDriver, dir: str) -> bool:
|
|
10
|
+
req_args = ['\\Windows\\RemoteHelper.DLL', 'CreateRemDirectory', dir]
|
|
11
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
12
|
+
|
|
13
|
+
# Response format
|
|
14
|
+
#
|
|
15
|
+
# Byte 0 to 3 response code (183 = function ran, otherwise failed)
|
|
16
|
+
# Byte 4 null footer
|
|
17
|
+
resp = msg_run_function(cip, req_data)
|
|
18
|
+
if not resp: raise Exception('Failed to create directory on terminal.')
|
|
19
|
+
resp_code = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
20
|
+
if (resp_code != CREATE_DIR_SUCCESS):
|
|
21
|
+
raise Exception('Failed to create directory on terminal.')
|
|
22
|
+
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
def terminal_create_file_exchange_for_download(cip: pycomm3.CIPDriver, file: MEFile) -> int:
|
|
26
|
+
# Request format
|
|
27
|
+
#
|
|
28
|
+
# Byte 0 Transfer Type (always 1 for file download?)
|
|
29
|
+
# Byte 1 Overwrite (0 = New File, 1 = Overwrite Existing)
|
|
30
|
+
# Byte 2 to 3 Chunk size in bytes
|
|
31
|
+
# Byte 4 to 7 File size in bytes
|
|
32
|
+
# Byte 8 to N-1 File name
|
|
33
|
+
# Byte N null footer
|
|
34
|
+
req_header = struct.pack('<BBHI', 0x01, int(file.overwrite_required), CHUNK_SIZE, file.get_size())
|
|
35
|
+
req_args = [f'\\Application Data\\Rockwell Software\\RSViewME\\Runtime\\{file.name}']
|
|
36
|
+
req_data = req_header + b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
37
|
+
|
|
38
|
+
# Response format
|
|
39
|
+
#
|
|
40
|
+
# Byte 0 to 1 message instance (should match request, 0x00)
|
|
41
|
+
# Byte 2 to 3 unknown purpose
|
|
42
|
+
# Byte 4 to 5 file instance (use this instance for file transfer)
|
|
43
|
+
# Byte 6 to 7 chunk size in bytes
|
|
44
|
+
resp = msg_create_file_exchange(cip, req_data)
|
|
45
|
+
if not resp: raise Exception('Failed to create file exchange on terminal')
|
|
46
|
+
resp_msg_instance, resp_unk1, resp_file_instance, resp_chunk_size = struct.unpack('<HHHH', resp.value)
|
|
47
|
+
if (resp_msg_instance != 0): raise Exception(f'Response message instance {resp_msg_instance} is not zero. Most likely there was an incomplete transfer. Reboot terminal and try again.')
|
|
48
|
+
if (resp_unk1 != 0 ): raise Exception(f'Response unknown bytes {resp_unk1} are not zero. Examine packets.')
|
|
49
|
+
if (resp_file_instance != 1): warn(f'Response file instance {resp_file_instance} is not one. Examine packets.')
|
|
50
|
+
if (resp_chunk_size != CHUNK_SIZE): raise Exception(f'Response chunk size {resp_chunk_size} did not match request size {CHUNK_SIZE}')
|
|
51
|
+
return resp_file_instance
|
|
52
|
+
|
|
53
|
+
def terminal_create_file_exchange_for_mer_list(cip: pycomm3.CIPDriver) -> int:
|
|
54
|
+
# Request format
|
|
55
|
+
#
|
|
56
|
+
# Byte 0 Transfer Type (always 0 for file upload?)
|
|
57
|
+
# Byte 1 unknown purpose (used for overwrite in download... maybe no purpose here?)
|
|
58
|
+
# Byte 2 to 3 Chunk size in bytes
|
|
59
|
+
# Byte 4 to N-1 File name
|
|
60
|
+
# Byte N null footer
|
|
61
|
+
req_header = struct.pack('<BBH', 0x00, 0x00, CHUNK_SIZE)
|
|
62
|
+
req_args = ['\\Application Data\\Rockwell Software\\RSViewME\\Runtime\\Results.txt']
|
|
63
|
+
req_data = req_header + b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
64
|
+
|
|
65
|
+
# Response format
|
|
66
|
+
#
|
|
67
|
+
# Byte 0 to 1 message instance (should match request, 0x00)
|
|
68
|
+
# Byte 2 to 3 unknown purpose
|
|
69
|
+
# Byte 4 to 5 file instance (use this instance for file transfer, increases with each subsequent transfer until Delete is run)
|
|
70
|
+
# Byte 6 to 7 chunk size in bytes
|
|
71
|
+
# Byte 8 to 11 file size in bytes
|
|
72
|
+
resp = msg_create_file_exchange(cip, req_data)
|
|
73
|
+
if not resp: raise Exception('Failed to create file exchange on terminal')
|
|
74
|
+
resp_msg_instance, resp_unk1, resp_file_instance, resp_chunk_size, resp_file_size = struct.unpack('<HHHHI', resp.value)
|
|
75
|
+
if (resp_msg_instance != 0): raise Exception(f'Response message instance {resp_msg_instance} is not zero. Most likely there was an incomplete transfer. Reboot terminal and try again.')
|
|
76
|
+
if (resp_unk1 != 0 ): raise Exception(f'Response unknown bytes {resp_unk1} are not zero. Examine packets.')
|
|
77
|
+
if (resp_file_instance != 1): warn(f'Response file instance {resp_file_instance} is not one. Examine packets.')
|
|
78
|
+
if (resp_chunk_size != CHUNK_SIZE): raise Exception(f'Response chunk size {resp_chunk_size} did not match request size {CHUNK_SIZE}')
|
|
79
|
+
return resp_file_instance
|
|
80
|
+
|
|
81
|
+
def terminal_create_file_exchange_for_upload(cip: pycomm3.CIPDriver, file: MEFile) -> int:
|
|
82
|
+
# Request format
|
|
83
|
+
#
|
|
84
|
+
# Byte 0 Transfer Type (always 0 for file upload?)
|
|
85
|
+
# Byte 1 unknown purpose (used for overwrite in download... maybe no purpose here?)
|
|
86
|
+
# Byte 2 to 3 Chunk size in bytes
|
|
87
|
+
# Byte 4 to N-1 File name
|
|
88
|
+
# Byte N null footer
|
|
89
|
+
req_header = struct.pack('<BBH', 0x00, 0x00, CHUNK_SIZE)
|
|
90
|
+
req_args = [f'\\Application Data\\Rockwell Software\\RSViewME\\Runtime\\{file.name}']
|
|
91
|
+
req_data = req_header + b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
92
|
+
|
|
93
|
+
# Response format
|
|
94
|
+
#
|
|
95
|
+
# Byte 0 to 1 message instance (should match request, 0x00)
|
|
96
|
+
# Byte 2 to 3 unknown purpose
|
|
97
|
+
# Byte 4 to 5 file instance (use this instance for file transfer, increases with each subsequent transfer until Delete is run)
|
|
98
|
+
# Byte 6 to 7 chunk size in bytes
|
|
99
|
+
# Byte 8 to 11 file size in bytes
|
|
100
|
+
resp = msg_create_file_exchange(cip, req_data)
|
|
101
|
+
if not resp: raise Exception('Failed to create file exchange on terminal')
|
|
102
|
+
resp_msg_instance, resp_unk1, resp_file_instance, resp_chunk_size, resp_file_size = struct.unpack('<HHHHI', resp.value)
|
|
103
|
+
if (resp_msg_instance != 0): raise Exception(f'Response message instance {resp_msg_instance} is not zero. Most likely there was an incomplete transfer. Reboot terminal and try again.')
|
|
104
|
+
if (resp_unk1 != 0 ): raise Exception(f'Response unknown bytes {resp_unk1} are not zero. Examine packets.')
|
|
105
|
+
if (resp_file_instance != 1): warn(f'Response file instance {resp_file_instance} is not one. Examine packets.')
|
|
106
|
+
if (resp_chunk_size != CHUNK_SIZE): raise Exception(f'Response chunk size {resp_chunk_size} did not match request size {CHUNK_SIZE}')
|
|
107
|
+
return resp_file_instance
|
|
108
|
+
|
|
109
|
+
def terminal_create_mer_list(cip: pycomm3.CIPDriver):
|
|
110
|
+
req_args = ['\\Windows\\RemoteHelper.DLL','FileBrowse','\\Application Data\\Rockwell Software\\RSViewME\\Runtime\\*.mer::\\Application Data\\Rockwell Software\\RSViewME\\Runtime\\Results.txt']
|
|
111
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
112
|
+
return msg_run_function(cip, req_data)
|
|
113
|
+
|
|
114
|
+
def terminal_create_runtime_directory(cip: pycomm3.CIPDriver, file: MEFile) -> bool:
|
|
115
|
+
# Create paths
|
|
116
|
+
if not(terminal_create_directory(cip, '\\Application Data')): return False
|
|
117
|
+
if not(terminal_create_directory(cip, '\\Application Data\\Rockwell Software')): return False
|
|
118
|
+
if not(terminal_create_directory(cip, '\\Application Data\\Rockwell Software\\RSViewME')): return False
|
|
119
|
+
if not(terminal_create_directory(cip, '\\Application Data\\Rockwell Software\\RSViewME\\Runtime')): return False
|
|
120
|
+
if not(terminal_create_directory(cip, '\\Application Data\\Rockwell Software\\RSViewME\\Runtime\\')): return False
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
def terminal_delete_file_exchange(cip: pycomm3.CIPDriver, instance: int):
|
|
124
|
+
return msg_delete_file_exchange(cip, instance)
|
|
125
|
+
|
|
126
|
+
def terminal_delete_mer_list(cip: pycomm3.CIPDriver):
|
|
127
|
+
req_args = ['\\Windows\\RemoteHelper.DLL','DeleteRemFile','\\Application Data\\Rockwell Software\\RSViewME\\Runtime\\Results.txt']
|
|
128
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
129
|
+
return msg_run_function(cip, req_data)
|
|
130
|
+
|
|
131
|
+
def terminal_end_file_write(cip: pycomm3.CIPDriver, instance: int):
|
|
132
|
+
req_data = b'\x00\x00\x00\x00\x02\x00\xff\xff'
|
|
133
|
+
return msg_write_file_chunk(cip, instance, req_data)
|
|
134
|
+
|
|
135
|
+
def terminal_file_download_mer(cip: pycomm3.CIPDriver, instance: int, file: MEFile):
|
|
136
|
+
req_chunk_number = 1
|
|
137
|
+
with open(file.path, 'rb') as source_file:
|
|
138
|
+
while True:
|
|
139
|
+
# Request format
|
|
140
|
+
#
|
|
141
|
+
# Byte 0 to 3 chunk number
|
|
142
|
+
# Byte 4 to 5 chunk size in bytes
|
|
143
|
+
# Byte 6 to N chunk data
|
|
144
|
+
req_chunk = source_file.read(CHUNK_SIZE)
|
|
145
|
+
req_header = struct.pack('<IH', req_chunk_number, len(req_chunk))
|
|
146
|
+
req_next_chunk_number = req_chunk_number + 1
|
|
147
|
+
req_data = req_header + req_chunk
|
|
148
|
+
|
|
149
|
+
# End of file
|
|
150
|
+
if not req_chunk:
|
|
151
|
+
#print(f'File downloaded from: {file.path}')
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
# Response format
|
|
155
|
+
#
|
|
156
|
+
# Byte 0 to 3 unknown purpose
|
|
157
|
+
# Byte 4 to 7 req chunk number echo
|
|
158
|
+
# Byte 8 to 11 next chunk number
|
|
159
|
+
resp = msg_write_file_chunk(cip, instance, req_data)
|
|
160
|
+
if not resp: raise Exception(f'Failed to write chunk {req_chunk_number} to terminal.')
|
|
161
|
+
resp_unk1, resp_chunk_number, resp_next_chunk_number = struct.unpack('<III', resp.value)
|
|
162
|
+
if (resp_unk1 != 0 ): raise Exception(f'Response unknown bytes {resp_unk1} are not zero. Examine packets.')
|
|
163
|
+
if (resp_chunk_number != req_chunk_number ): raise Exception(f'Response chunk number {resp_chunk_number} did not match request chunk number {req_chunk_number}.')
|
|
164
|
+
if (resp_next_chunk_number != req_next_chunk_number): raise Exception(f'Response next chunk number {resp_next_chunk_number} did not match expected next chunk number {req_next_chunk_number}.')
|
|
165
|
+
|
|
166
|
+
# Continue to next chunk
|
|
167
|
+
req_chunk_number += 1
|
|
168
|
+
|
|
169
|
+
def terminal_file_upload_mer(cip: pycomm3.CIPDriver, instance: int, file: MEFile):
|
|
170
|
+
req_chunk_number = 1
|
|
171
|
+
with open(file.path, 'wb') as dest_file:
|
|
172
|
+
while True:
|
|
173
|
+
# Request format
|
|
174
|
+
#
|
|
175
|
+
# Byte 0 to 3 chunk number
|
|
176
|
+
req_data = struct.pack('<I', req_chunk_number)
|
|
177
|
+
|
|
178
|
+
# Response format
|
|
179
|
+
#
|
|
180
|
+
# Byte 0 to 3 unknown purpose
|
|
181
|
+
# Byte 4 to 7 req chunk number echo
|
|
182
|
+
# Byte 8 to 9 chunk size in bytes
|
|
183
|
+
# Byte 10 to N chunk data
|
|
184
|
+
resp = msg_read_file_chunk(cip, instance, req_data)
|
|
185
|
+
if not resp: raise Exception(f'Failed to read chunk {req_chunk_number} to terminal.')
|
|
186
|
+
resp_unk1 = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
187
|
+
resp_chunk_number = int.from_bytes(resp.value[4:8], byteorder='little', signed=False)
|
|
188
|
+
resp_chunk_size = int.from_bytes(resp.value[8:9], byteorder='little', signed=False)
|
|
189
|
+
resp_data = bytes(resp.value[10:])
|
|
190
|
+
|
|
191
|
+
if (resp_unk1 != 0 ): raise Exception(f'Response unknown bytes {resp_unk1} are not zero. Examine packets.')
|
|
192
|
+
if (resp_chunk_number != req_chunk_number) and (resp_chunk_number != 0): raise Exception(f'Response chunk number {resp_chunk_number} did not match request chunk number {req_chunk_number}.')
|
|
193
|
+
|
|
194
|
+
# End of file
|
|
195
|
+
if (resp_chunk_number == 0) and (resp_chunk_size == 2) and (resp_data == b'\xff\xff'):
|
|
196
|
+
#print(f'File uploaded to: {file.path}')
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
# Write to file
|
|
200
|
+
dest_file.write(resp_data)
|
|
201
|
+
|
|
202
|
+
# Continue to next chunk
|
|
203
|
+
req_chunk_number += 1
|
|
204
|
+
|
|
205
|
+
def terminal_file_upload_mer_list(cip: pycomm3.CIPDriver, instance: int):
|
|
206
|
+
req_chunk_number = 1
|
|
207
|
+
resp_binary = bytearray()
|
|
208
|
+
while True:
|
|
209
|
+
# Request format
|
|
210
|
+
#
|
|
211
|
+
# Byte 0 to 3 chunk number
|
|
212
|
+
req_data = struct.pack('<I', req_chunk_number)
|
|
213
|
+
|
|
214
|
+
# Response format
|
|
215
|
+
#
|
|
216
|
+
# Byte 0 to 3 unknown purpose
|
|
217
|
+
# Byte 4 to 7 req chunk number echo
|
|
218
|
+
# Byte 8 to 9 chunk size in bytes
|
|
219
|
+
# Byte 10 to N chunk data
|
|
220
|
+
resp = msg_read_file_chunk(cip, instance, req_data)
|
|
221
|
+
if not resp: raise Exception(f'Failed to read chunk {req_chunk_number} to terminal.')
|
|
222
|
+
resp_unk1 = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
223
|
+
resp_chunk_number = int.from_bytes(resp.value[4:8], byteorder='little', signed=False)
|
|
224
|
+
resp_chunk_size = int.from_bytes(resp.value[8:9], byteorder='little', signed=False)
|
|
225
|
+
resp_data = bytes(resp.value[10:])
|
|
226
|
+
|
|
227
|
+
if (resp_unk1 != 0 ): raise Exception(f'Response unknown bytes {resp_unk1} are not zero. Examine packets.')
|
|
228
|
+
if (resp_chunk_number != req_chunk_number) and (resp_chunk_number != 0): raise Exception(f'Response chunk number {resp_chunk_number} did not match request chunk number {req_chunk_number}.')
|
|
229
|
+
|
|
230
|
+
# End of file
|
|
231
|
+
if (resp_chunk_number == 0) and (resp_chunk_size == 2) and (resp_data == b'\xff\xff'):
|
|
232
|
+
#print('End of file')
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
# Write to mer list
|
|
236
|
+
resp_binary += resp_data
|
|
237
|
+
|
|
238
|
+
# Continue to next chunk
|
|
239
|
+
req_chunk_number += 1
|
|
240
|
+
|
|
241
|
+
# In the returned list, each file name character is separated
|
|
242
|
+
# by a null character.
|
|
243
|
+
#
|
|
244
|
+
# Each each is separate by a colon.
|
|
245
|
+
resp_str = "".join([chr(b) for b in resp_binary if b != 0])
|
|
246
|
+
resp_list = resp_str.split(':')
|
|
247
|
+
return resp_list
|
|
248
|
+
|
|
249
|
+
def terminal_get_file_exists(cip: pycomm3.CIPDriver, file: MEFile) -> bool:
|
|
250
|
+
req_args = ['\\Windows\\RemoteHelper.DLL', 'FileExists', f'\\Application Data\\Rockwell Software\\RSViewME\\Runtime\\{file.name}']
|
|
251
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
252
|
+
|
|
253
|
+
# Response format
|
|
254
|
+
#
|
|
255
|
+
# Byte 0 to 3 response code (0 = function ran, otherwise failed)
|
|
256
|
+
# Byte 4 file exists (1 = file exists)
|
|
257
|
+
# Byte 5 null footer
|
|
258
|
+
resp = msg_run_function(cip, req_data)
|
|
259
|
+
if not resp: raise Exception('Failed to get whether file exists.')
|
|
260
|
+
resp_code = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
261
|
+
if (resp_code != 0):
|
|
262
|
+
#This one can return false without warning since the outer function will set the overwrite bit accordingly.
|
|
263
|
+
#
|
|
264
|
+
#warn(f'Response code was not zero. Examine packets.')
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
resp_file_exists = bool(int(resp.value[4:].decode('utf-8').strip('\x00')))
|
|
268
|
+
return resp_file_exists
|
|
269
|
+
|
|
270
|
+
def terminal_get_file_size(cip: pycomm3.CIPDriver, file: MEFile) -> int:
|
|
271
|
+
req_args = ['\\Windows\\RemoteHelper.DLL', 'FileSize', f'\\Application Data\\Rockwell Software\\RSViewME\\Runtime\\{file.name}']
|
|
272
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
273
|
+
|
|
274
|
+
# Response format
|
|
275
|
+
#
|
|
276
|
+
# Byte 0 to 3 response code (0 = function ran, otherwise failed)
|
|
277
|
+
# Byte 4 to N-1 file size
|
|
278
|
+
# Byte N null footer
|
|
279
|
+
resp = msg_run_function(cip, req_data)
|
|
280
|
+
if not resp: raise Exception('Failed to get file size of {file.name} on terminal.')
|
|
281
|
+
resp_code = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
282
|
+
if (resp_code != 0):
|
|
283
|
+
raise Exception(f'Response code was not zero. Examine packets.')
|
|
284
|
+
|
|
285
|
+
resp_file_size = int(resp.value[4:].decode('utf-8').strip('\x00'))
|
|
286
|
+
return resp_file_size
|
|
287
|
+
|
|
288
|
+
def terminal_get_folder_exists(cip: pycomm3.CIPDriver) -> bool:
|
|
289
|
+
req_args = ['\\Windows\\RemoteHelper.DLL', 'StorageExists', '\\Application Data']
|
|
290
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
291
|
+
|
|
292
|
+
# Response format
|
|
293
|
+
#
|
|
294
|
+
# Byte 0 to 3 response code (0 = function ran, otherwise failed)
|
|
295
|
+
# Byte 4 storage exists (1 -> folder exists)
|
|
296
|
+
# Byte 5 null footer
|
|
297
|
+
resp = msg_run_function(cip, req_data)
|
|
298
|
+
if not resp: raise Exception('Failed to get whether storage exists.')
|
|
299
|
+
resp_code = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
300
|
+
if (resp_code != 0):
|
|
301
|
+
warn(f'Response code was not zero. Examine packets.')
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
resp_storage_exists = bool(int(resp.value[4:].decode('utf-8').strip('\x00')))
|
|
305
|
+
return resp_storage_exists
|
|
306
|
+
|
|
307
|
+
def terminal_get_free_space(cip: pycomm3.CIPDriver) -> int:
|
|
308
|
+
req_args = ['\\Windows\\RemoteHelper.DLL', 'FreeSpace', '\\Application Data\\Rockwell Software\\RSViewME\\Runtime\\']
|
|
309
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
310
|
+
|
|
311
|
+
# Response format
|
|
312
|
+
#
|
|
313
|
+
# Byte 0 to 3 response code (0 = function ran, otherwise failed)
|
|
314
|
+
# Byte 4 to N-1 free space
|
|
315
|
+
# Byte N null footer
|
|
316
|
+
resp = msg_run_function(cip, req_data)
|
|
317
|
+
if not resp: raise Exception('Failed to get terminal free space.')
|
|
318
|
+
resp_code = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
319
|
+
if (resp_code != 0):
|
|
320
|
+
raise Exception(f'Response code was not zero. Examine packets.')
|
|
321
|
+
|
|
322
|
+
resp_free_space = int(resp.value[4:].decode('utf-8').strip('\x00'))
|
|
323
|
+
return resp_free_space
|
|
324
|
+
|
|
325
|
+
def terminal_get_helper_version(cip: pycomm3.CIPDriver) -> str:
|
|
326
|
+
req_args = ['\\Windows\\RemoteHelper.DLL', 'GetVersion', '\\Windows\\RemoteHelper.DLL']
|
|
327
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
328
|
+
|
|
329
|
+
# Response format
|
|
330
|
+
#
|
|
331
|
+
# Byte 0 to 3 response code (0 = function ran, otherwise failed)
|
|
332
|
+
# Byte 4 to N-1 terminal version string
|
|
333
|
+
# Byte N null footer
|
|
334
|
+
resp = msg_run_function(cip, req_data)
|
|
335
|
+
if not resp: raise Exception('Failed to get terminal helper version.')
|
|
336
|
+
resp_code = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
337
|
+
if (resp_code != 0):
|
|
338
|
+
raise Exception(f'Response code was not zero. Examine packets.')
|
|
339
|
+
|
|
340
|
+
resp_helper_version = str(resp.value[4:].decode('utf-8').strip('\x00'))
|
|
341
|
+
return resp_helper_version
|
|
342
|
+
|
|
343
|
+
def terminal_get_me_version(cip: pycomm3.CIPDriver) -> str:
|
|
344
|
+
req_args = ['HKEY_LOCAL_MACHINE\\SOFTWARE\\Rockwell Software\\RSView Enterprise\\MEVersion']
|
|
345
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
346
|
+
|
|
347
|
+
# Response format
|
|
348
|
+
#
|
|
349
|
+
# Byte 0 to 3 response code (0 = function ran, otherwise failed)
|
|
350
|
+
# Byte 4 to 7 unknown purpose
|
|
351
|
+
# Byte 8 to N-1 ME version string
|
|
352
|
+
# Byte N null footer
|
|
353
|
+
resp = msg_read_registry(cip, req_data)
|
|
354
|
+
if not resp: raise Exception('Failed to get terminal ME version.')
|
|
355
|
+
resp_code = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
356
|
+
if (resp_code != 0):
|
|
357
|
+
raise Exception(f'Response code was not zero. Examine packets.')
|
|
358
|
+
|
|
359
|
+
resp_unk1 = int.from_bytes(resp.value[4:8], byteorder='little', signed=False)
|
|
360
|
+
resp_version = str(resp.value[8:].decode('utf-8').strip('\x00'))
|
|
361
|
+
return resp_version
|
|
362
|
+
|
|
363
|
+
def terminal_get_product_code(cip: pycomm3.CIPDriver) -> str:
|
|
364
|
+
req_args = ['HKEY_LOCAL_MACHINE\\SOFTWARE\\Rockwell Software\\RSLinxNG\\CIP Identity\\ProductCode']
|
|
365
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
366
|
+
|
|
367
|
+
# Response format
|
|
368
|
+
#
|
|
369
|
+
# Byte 0 to 3 response code (0 = function ran, otherwise failed)
|
|
370
|
+
# Byte 4 to 7 unknown purpose
|
|
371
|
+
# Byte 8 to N-1 product code string
|
|
372
|
+
# Byte N null footer
|
|
373
|
+
resp = msg_read_registry(cip, req_data)
|
|
374
|
+
if not resp: raise Exception('Failed to get terminal product code.')
|
|
375
|
+
resp_code = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
376
|
+
if (resp_code != 0):
|
|
377
|
+
raise Exception(f'Response code was not zero. Examine packets.')
|
|
378
|
+
|
|
379
|
+
resp_unk1 = int.from_bytes(resp.value[4:8], byteorder='little', signed=False)
|
|
380
|
+
resp_product_code = str(resp.value[8:].decode('utf-8').strip('\x00'))
|
|
381
|
+
return resp_product_code
|
|
382
|
+
|
|
383
|
+
def terminal_get_product_type(cip: pycomm3.CIPDriver) -> str:
|
|
384
|
+
req_args = ['HKEY_LOCAL_MACHINE\\SOFTWARE\\Rockwell Software\\RSLinxNG\\CIP Identity\\ProductType']
|
|
385
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
386
|
+
|
|
387
|
+
# Response format
|
|
388
|
+
#
|
|
389
|
+
# Byte 0 to 3 response code (0 = function ran, otherwise failed)
|
|
390
|
+
# Byte 4 to 7 unknown purpose
|
|
391
|
+
# Byte 8 to N-1 product type string
|
|
392
|
+
# Byte N null footer
|
|
393
|
+
resp = msg_read_registry(cip, req_data)
|
|
394
|
+
if not resp: raise Exception('Failed to get terminal product type.')
|
|
395
|
+
resp_code = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
396
|
+
if (resp_code != 0):
|
|
397
|
+
raise Exception(f'Response code was not zero. Examine packets.')
|
|
398
|
+
|
|
399
|
+
resp_unk1 = int.from_bytes(resp.value[4:8], byteorder='little', signed=False)
|
|
400
|
+
resp_product_type = str(resp.value[8:].decode('utf-8').strip('\x00'))
|
|
401
|
+
return resp_product_type
|
|
402
|
+
|
|
403
|
+
def terminal_is_get_unk_valid(cip: pycomm3.CIPDriver) -> bool:
|
|
404
|
+
# I don't know what any of these three attributes are for yet.
|
|
405
|
+
# It may be checking that the file exchange is available.
|
|
406
|
+
resp = msg_get_attr_unk(cip, b'\x30\x01')
|
|
407
|
+
if not resp: return False
|
|
408
|
+
if resp.value not in GET_UNK1_VALUES:
|
|
409
|
+
warn(f'Invalid UNK1 value. Examine packets.')
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
resp = msg_get_attr_unk(cip, b'\x30\x08')
|
|
413
|
+
if not resp: return False
|
|
414
|
+
if resp.value not in GET_UNK2_VALUES:
|
|
415
|
+
warn(f'Invalid UNK2 value. Examine packets.')
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
resp = msg_get_attr_unk(cip, b'\x30\x09')
|
|
419
|
+
if not resp: return False
|
|
420
|
+
if resp.value not in GET_UNK3_VALUES:
|
|
421
|
+
warn(f'Invalid UNK3 value. Examine packets.')
|
|
422
|
+
return False
|
|
423
|
+
|
|
424
|
+
return True
|
|
425
|
+
|
|
426
|
+
def terminal_is_set_unk_valid(cip: pycomm3.CIPDriver) -> bool:
|
|
427
|
+
# I don't know what setting this attribute does yet.
|
|
428
|
+
# It may be marking the file exchange as in use.
|
|
429
|
+
#
|
|
430
|
+
resp = msg_set_attr_unk(cip, b'\x30\x01\xff\xff')
|
|
431
|
+
if not resp: return False
|
|
432
|
+
|
|
433
|
+
return True
|
|
434
|
+
|
|
435
|
+
def terminal_reboot(cip: pycomm3.CIPDriver):
|
|
436
|
+
# For some reason this one has an extra trailing byte.
|
|
437
|
+
# Not sure if it has some other purpose yet
|
|
438
|
+
req_args = ['\\Windows\\RemoteHelper.DLL', 'BootTerminal']
|
|
439
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args) + b'\x00'
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
resp = msg_run_function(cip, req_data)
|
|
443
|
+
|
|
444
|
+
#Should never get here
|
|
445
|
+
raise Exception(resp)
|
|
446
|
+
except pycomm3.exceptions.CommError as e:
|
|
447
|
+
# Unlike most CIP messages, this one is always expected to
|
|
448
|
+
# create an exception. When it is received by the terminal,
|
|
449
|
+
# the device reboots and breaks the socket.
|
|
450
|
+
if (str(e) != 'failed to receive reply'): raise e
|
|
451
|
+
|
|
452
|
+
def terminal_set_startup_file(cip: pycomm3.CIPDriver, file: MEFile, replace_comms: bool, delete_logs: bool) -> bool:
|
|
453
|
+
req_args = ['\\Windows\\RemoteHelper.DLL', 'CreateRemMEStartupShortcut', f'\\Application Data:{file.name}: /r /delay']
|
|
454
|
+
if replace_comms: req_args = [req_args[1], req_args[2], req_args[3] + ' /o']
|
|
455
|
+
if delete_logs: req_args = [req_args[1], req_args[2], req_args[3] + ' /d']
|
|
456
|
+
req_data = b''.join(arg.encode() + b'\x00' for arg in req_args)
|
|
457
|
+
|
|
458
|
+
# Response format
|
|
459
|
+
#
|
|
460
|
+
# Byte 0 to 3 response code (0 = function ran, otherwise failed)
|
|
461
|
+
# Byte 4 null footer
|
|
462
|
+
resp = msg_run_function(cip, req_data)
|
|
463
|
+
if not resp: raise Exception('Failed to set terminal startup shortcut.')
|
|
464
|
+
resp_code = int.from_bytes(resp.value[:4], byteorder='little', signed=False)
|
|
465
|
+
if (resp_code != 0):
|
|
466
|
+
raise Exception(f'Response code was not zero. Examine packets.')
|
|
467
|
+
|
|
468
|
+
return True
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class MEFile:
|
|
6
|
+
name: str
|
|
7
|
+
overwrite_requested: bool
|
|
8
|
+
overwrite_required: bool
|
|
9
|
+
path: str
|
|
10
|
+
|
|
11
|
+
def get_ext(self) -> str:
|
|
12
|
+
filename, extension = os.path.splitext(self.path)
|
|
13
|
+
return extension.lower()
|
|
14
|
+
|
|
15
|
+
def get_size(self) -> int:
|
|
16
|
+
return os.path.getsize(self.path)
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class MEDeviceInfo:
|
|
20
|
+
comms_path: str
|
|
21
|
+
helper_version: str
|
|
22
|
+
me_version: str
|
|
23
|
+
product_code: str
|
|
24
|
+
product_type: str
|
|
25
|
+
log: list[str]
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class MEResponse(object):
|
|
29
|
+
device: MEDeviceInfo
|
|
30
|
+
status: str
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pymeu
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python ME Utility
|
|
5
|
+
Author-email: aawilliams85 <adair.williams85@gmail.com>
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pycomm3
|
|
9
|
+
|
|
10
|
+
# pymeu
|
|
11
|
+
|
|
12
|
+
PyMEU (Python ME Utility) is a set of helper functions built around pycomm3 to interface with Rockwell Automation PanelView Plus 6/7 HMIs.<br>
|
|
13
|
+
|
|
14
|
+
The current release has only been tested on a very limited selection of hardware. It is alpha quality at best and could easily brick your device. Use at your own risk.<br>
|
|
15
|
+
|
|
16
|
+
## Example
|
|
17
|
+
|
|
18
|
+
Use the download function to transfer a *.MER file to the remote terminal:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from pymeu import MEUtility
|
|
22
|
+
meu = MEUtility('192.168.1.20')
|
|
23
|
+
meu.download('C:\\YourFolder\\YourProgram.mer')
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use the upload function to transfer a *.MER file from the remote terminal:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from pymeu import MEUtility
|
|
30
|
+
meu = MEUtility('192.168.1.20')
|
|
31
|
+
meu.upload('C:\\YourFolder\\YourProgram.mer')
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use the upload all function to transfer all *.MER files from the remote terminal:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from pymeu import MEUtility
|
|
38
|
+
meu = MEUtility('192.168.1.20')
|
|
39
|
+
meu.upload_all('C:\\YourFolder')
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use the reboot function to restart the remote terminal:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from pymeu import MEUtility
|
|
46
|
+
meu = MEUtility('192.168.1.20')
|
|
47
|
+
meu.reboot()
|
|
48
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pymeu/__init__.py
|
|
5
|
+
pymeu/constants.py
|
|
6
|
+
pymeu/main.py
|
|
7
|
+
pymeu/messages.py
|
|
8
|
+
pymeu/terminal.py
|
|
9
|
+
pymeu/types.py
|
|
10
|
+
pymeu.egg-info/PKG-INFO
|
|
11
|
+
pymeu.egg-info/SOURCES.txt
|
|
12
|
+
pymeu.egg-info/dependency_links.txt
|
|
13
|
+
pymeu.egg-info/requires.txt
|
|
14
|
+
pymeu.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pycomm3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pymeu
|
pymeu-0.0.1/setup.cfg
ADDED