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 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,4 @@
1
+ from .main import MEUtility
2
+
3
+ __version_info__ = (0, 0, 1)
4
+ __version__ = '.'.join(str(x) for x in __version_info__)
@@ -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
+ pycomm3
@@ -0,0 +1 @@
1
+ pymeu
@@ -0,0 +1,11 @@
1
+ [project]
2
+ name = "pymeu"
3
+ description = "Python ME Utility"
4
+ readme = "README.md"
5
+ version = "0.0.1"
6
+ authors = [
7
+ { name = "aawilliams85", email = "adair.williams85@gmail.com" }
8
+ ]
9
+ dependencies = [
10
+ "pycomm3"
11
+ ]
pymeu-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+