slmp-connect-python 0.1.14__tar.gz → 0.1.15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {slmp_connect_python-0.1.14/slmp_connect_python.egg-info → slmp_connect_python-0.1.15}/PKG-INFO +8 -1
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/README.md +23 -16
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/pyproject.toml +1 -3
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp/cli.py +0 -772
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp/core.py +1 -1
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15/slmp_connect_python.egg-info}/PKG-INFO +8 -1
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp_connect_python.egg-info/entry_points.txt +0 -2
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/tests/test_slmp.py +13 -182
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/LICENSE +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/setup.cfg +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp/__init__.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp/async_client.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp/client.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp/constants.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp/device_ranges.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp/errors.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp/py.typed +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp/utils.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp_connect_python.egg-info/SOURCES.txt +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp_connect_python.egg-info/dependency_links.txt +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp_connect_python.egg-info/requires.txt +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp_connect_python.egg-info/top_level.txt +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/tests/test_async_client.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/tests/test_bugs_and_edges.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/tests/test_cpu_operation_state.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/tests/test_device_ranges.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/tests/test_device_vectors.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/tests/test_shared_spec.py +0 -0
- {slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/tests/test_utils.py +0 -0
{slmp_connect_python-0.1.14/slmp_connect_python.egg-info → slmp_connect_python-0.1.15}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slmp-connect-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.15
|
|
4
4
|
Summary: SLMP Connect Python: client library for Mitsubishi SLMP binary communication
|
|
5
5
|
Author: fa-yoshinobu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -43,6 +43,13 @@ Dynamic: license-file
|
|
|
43
43
|
|
|
44
44
|

|
|
45
45
|
|
|
46
|
+
[](https://github.com/fa-yoshinobu/plc-comm-slmp-python/actions/workflows/automated-release.yml)
|
|
47
|
+
[](https://github.com/fa-yoshinobu/plc-comm-slmp-python/actions/workflows/release.yml)
|
|
48
|
+
|
|
49
|
+
[](https://www.python.org/)
|
|
50
|
+
[](https://www.mkdocs.org/)
|
|
51
|
+
[](https://pages.github.com/)
|
|
52
|
+
|
|
46
53
|
High-level SLMP helpers for Mitsubishi PLC communication over Binary 3E and 4E frames.
|
|
47
54
|
|
|
48
55
|
This repository treats the high-level helper layer as the recommended user surface:
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|

|
|
11
11
|
|
|
12
|
+
[](https://github.com/fa-yoshinobu/plc-comm-slmp-python/actions/workflows/automated-release.yml)
|
|
13
|
+
[](https://github.com/fa-yoshinobu/plc-comm-slmp-python/actions/workflows/release.yml)
|
|
14
|
+
|
|
15
|
+
[](https://www.python.org/)
|
|
16
|
+
[](https://www.mkdocs.org/)
|
|
17
|
+
[](https://pages.github.com/)
|
|
18
|
+
|
|
12
19
|
High-level SLMP helpers for Mitsubishi PLC communication over Binary 3E and 4E frames.
|
|
13
20
|
|
|
14
21
|
This repository treats the high-level helper layer as the recommended user surface:
|
|
@@ -16,11 +23,11 @@ This repository treats the high-level helper layer as the recommended user surfa
|
|
|
16
23
|
- `SlmpConnectionOptions`
|
|
17
24
|
- `open_and_connect` / `open_and_connect_sync`
|
|
18
25
|
- `AsyncSlmpClient`
|
|
19
|
-
- `QueuedAsyncSlmpClient`
|
|
20
|
-
- `SlmpClient`
|
|
21
|
-
- `normalize_address`
|
|
22
|
-
- `parse_address` / `try_parse_address` / `format_address`
|
|
23
|
-
- `read_typed` / `write_typed`
|
|
26
|
+
- `QueuedAsyncSlmpClient`
|
|
27
|
+
- `SlmpClient`
|
|
28
|
+
- `normalize_address`
|
|
29
|
+
- `parse_address` / `try_parse_address` / `format_address`
|
|
30
|
+
- `read_typed` / `write_typed`
|
|
24
31
|
- `read_words_single_request` / `read_dwords_single_request`
|
|
25
32
|
- `read_words_chunked` / `read_dwords_chunked`
|
|
26
33
|
- `write_bit_in_word`
|
|
@@ -148,17 +155,17 @@ Maintainer-only notes and retained evidence live under `internal_docs/`.
|
|
|
148
155
|
|
|
149
156
|
### Address Normalization
|
|
150
157
|
|
|
151
|
-
```python
|
|
152
|
-
from slmp import format_address, normalize_address, parse_address
|
|
153
|
-
|
|
154
|
-
print(normalize_address("x20")) # X20
|
|
155
|
-
print(normalize_address("d200")) # D200
|
|
156
|
-
print(normalize_address("x100", plc_family="iq-f")) # X100
|
|
157
|
-
|
|
158
|
-
parsed = parse_address("d200:f")
|
|
159
|
-
print(parsed.base_device, parsed.dtype) # D200 F
|
|
160
|
-
print(format_address(parsed)) # D200:F
|
|
161
|
-
```
|
|
158
|
+
```python
|
|
159
|
+
from slmp import format_address, normalize_address, parse_address
|
|
160
|
+
|
|
161
|
+
print(normalize_address("x20")) # X20
|
|
162
|
+
print(normalize_address("d200")) # D200
|
|
163
|
+
print(normalize_address("x100", plc_family="iq-f")) # X100
|
|
164
|
+
|
|
165
|
+
parsed = parse_address("d200:f")
|
|
166
|
+
print(parsed.base_device, parsed.dtype) # D200 F
|
|
167
|
+
print(format_address(parsed)) # D200:F
|
|
168
|
+
```
|
|
162
169
|
|
|
163
170
|
### Single Typed Values
|
|
164
171
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slmp-connect-python"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.15"
|
|
8
8
|
description = "SLMP Connect Python: client library for Mitsubishi SLMP binary communication"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -46,8 +46,6 @@ dev = [
|
|
|
46
46
|
|
|
47
47
|
[project.scripts]
|
|
48
48
|
slmp-connection-check = "slmp.cli:connection_check_main"
|
|
49
|
-
slmp-compatibility-probe = "slmp.cli:compatibility_probe_main"
|
|
50
|
-
slmp-compatibility-matrix-render = "slmp.cli:compatibility_matrix_render_main"
|
|
51
49
|
slmp-device-range-probe = "slmp.cli:device_range_probe_main"
|
|
52
50
|
slmp-register-boundary-probe = "slmp.cli:register_boundary_probe_main"
|
|
53
51
|
slmp-other-station-check = "slmp.cli:other_station_check_main"
|
|
@@ -270,63 +270,6 @@ def _project_root() -> Path:
|
|
|
270
270
|
return Path(__file__).resolve().parents[1]
|
|
271
271
|
|
|
272
272
|
|
|
273
|
-
def _compatibility_default_output(*, plc_label: str, filename: str) -> str:
|
|
274
|
-
return _default_report_output(series="compatibility", model=plc_label, filename=filename)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def _compatibility_selected_specs(
|
|
278
|
-
*,
|
|
279
|
-
command_codes: Sequence[str] | None,
|
|
280
|
-
include_write_restore: bool,
|
|
281
|
-
include_remote_control: bool,
|
|
282
|
-
include_maintenance: bool,
|
|
283
|
-
) -> list[CompatibilityCommandSpec]:
|
|
284
|
-
enabled_risks = {COMPATIBILITY_SAFE_RISK_GROUP}
|
|
285
|
-
if include_write_restore:
|
|
286
|
-
enabled_risks.add(COMPATIBILITY_WRITE_RESTORE_RISK_GROUP)
|
|
287
|
-
if include_remote_control:
|
|
288
|
-
enabled_risks.add(COMPATIBILITY_REMOTE_RISK_GROUP)
|
|
289
|
-
if include_maintenance:
|
|
290
|
-
enabled_risks.add(COMPATIBILITY_MAINTENANCE_RISK_GROUP)
|
|
291
|
-
|
|
292
|
-
selected_codes = set(command_codes) if command_codes else None
|
|
293
|
-
specs: list[CompatibilityCommandSpec] = []
|
|
294
|
-
for spec in COMPATIBILITY_COMMAND_SPECS:
|
|
295
|
-
if spec.risk_group not in enabled_risks:
|
|
296
|
-
continue
|
|
297
|
-
if selected_codes is not None and spec.code not in selected_codes:
|
|
298
|
-
continue
|
|
299
|
-
specs.append(spec)
|
|
300
|
-
return specs
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def _compatibility_summarize_subresults(subresults: Sequence[tuple[str, str, str]]) -> str:
|
|
304
|
-
statuses = {status for _, status, _ in subresults}
|
|
305
|
-
if not statuses:
|
|
306
|
-
return "SKIP"
|
|
307
|
-
if statuses == {"SKIP"}:
|
|
308
|
-
return "SKIP"
|
|
309
|
-
if "NG" in statuses and "OK" in statuses:
|
|
310
|
-
return "PARTIAL"
|
|
311
|
-
if "NG" in statuses and "PARTIAL" in statuses:
|
|
312
|
-
return "PARTIAL"
|
|
313
|
-
if "PARTIAL" in statuses:
|
|
314
|
-
return "PARTIAL"
|
|
315
|
-
if "OK" in statuses:
|
|
316
|
-
return "OK"
|
|
317
|
-
return "NG"
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
def _compatibility_format_subresults(subresults: Sequence[tuple[str, str, str]]) -> str:
|
|
321
|
-
parts: list[str] = []
|
|
322
|
-
for name, status, detail in subresults:
|
|
323
|
-
if detail:
|
|
324
|
-
parts.append(f"{name}={status} ({detail})")
|
|
325
|
-
else:
|
|
326
|
-
parts.append(f"{name}={status}")
|
|
327
|
-
return "; ".join(parts)
|
|
328
|
-
|
|
329
|
-
|
|
330
273
|
def _compatibility_matrix_cell_status(results: Sequence[Mapping[str, object]]) -> str:
|
|
331
274
|
executed_statuses = [str(row.get("status", "")).upper() for row in results if str(row.get("status", ""))]
|
|
332
275
|
if not executed_statuses:
|
|
@@ -970,8 +913,6 @@ def _render_compatibility_matrix_markdown(
|
|
|
970
913
|
def _default_regression_help_scripts() -> tuple[str, ...]:
|
|
971
914
|
return (
|
|
972
915
|
"slmp_connection_check.py",
|
|
973
|
-
"slmp_compatibility_probe.py",
|
|
974
|
-
"slmp_compatibility_matrix_render.py",
|
|
975
916
|
"slmp_device_range_probe.py",
|
|
976
917
|
"slmp_register_boundary_probe.py",
|
|
977
918
|
"slmp_device_access_matrix_sync.py",
|
|
@@ -1815,467 +1756,6 @@ def _read_type_name_with_frame_and_series(
|
|
|
1815
1756
|
return client.read_type_name()
|
|
1816
1757
|
|
|
1817
1758
|
|
|
1818
|
-
def _compatibility_subprobe(
|
|
1819
|
-
name: str,
|
|
1820
|
-
func: Callable[[], str],
|
|
1821
|
-
) -> tuple[str, str, str]:
|
|
1822
|
-
try:
|
|
1823
|
-
return (name, "OK", func())
|
|
1824
|
-
except Exception as exc: # noqa: BLE001
|
|
1825
|
-
return (name, "NG", str(exc))
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
def _compatibility_request_subprobe(
|
|
1829
|
-
client: _StandardSlmpClient,
|
|
1830
|
-
*,
|
|
1831
|
-
name: str,
|
|
1832
|
-
command: int | Command,
|
|
1833
|
-
payload: bytes = b"",
|
|
1834
|
-
subcommand: int = 0x0000,
|
|
1835
|
-
) -> tuple[str, str, str]:
|
|
1836
|
-
try:
|
|
1837
|
-
resp = client.request(command, subcommand, payload, raise_on_error=False)
|
|
1838
|
-
except Exception as exc: # noqa: BLE001
|
|
1839
|
-
return (name, "NG", str(exc))
|
|
1840
|
-
status = "OK" if resp.end_code == 0 else "NG"
|
|
1841
|
-
return (name, status, f"end_code=0x{resp.end_code:04X}, data_len={len(resp.data)}")
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
def _compatibility_probe_direct_word_write_restore(
|
|
1845
|
-
client: _StandardSlmpClient,
|
|
1846
|
-
*,
|
|
1847
|
-
device: str,
|
|
1848
|
-
preferred_write_value: int,
|
|
1849
|
-
series: PLCSeries,
|
|
1850
|
-
) -> str:
|
|
1851
|
-
before = int(client.read_devices(device, 1, bit_unit=False, series=series)[0])
|
|
1852
|
-
write_value = _choose_probe_word_value(current=before, preferred=preferred_write_value)
|
|
1853
|
-
client.write_devices(device, [write_value], bit_unit=False, series=series)
|
|
1854
|
-
readback = int(client.read_devices(device, 1, bit_unit=False, series=series)[0])
|
|
1855
|
-
client.write_devices(device, [before], bit_unit=False, series=series)
|
|
1856
|
-
restored = int(client.read_devices(device, 1, bit_unit=False, series=series)[0])
|
|
1857
|
-
return (
|
|
1858
|
-
f"device={device}, before=0x{before:04X}, write=0x{write_value:04X}, "
|
|
1859
|
-
f"readback=0x{readback:04X}, restored=0x{restored:04X}"
|
|
1860
|
-
)
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
def _compatibility_probe_direct_bit_write_restore(
|
|
1864
|
-
client: _StandardSlmpClient,
|
|
1865
|
-
*,
|
|
1866
|
-
device: str,
|
|
1867
|
-
series: PLCSeries,
|
|
1868
|
-
) -> str:
|
|
1869
|
-
before = bool(client.read_devices(device, 1, bit_unit=True, series=series)[0])
|
|
1870
|
-
write_value = not before
|
|
1871
|
-
client.write_devices(device, [write_value], bit_unit=True, series=series)
|
|
1872
|
-
readback = bool(client.read_devices(device, 1, bit_unit=True, series=series)[0])
|
|
1873
|
-
client.write_devices(device, [before], bit_unit=True, series=series)
|
|
1874
|
-
restored = bool(client.read_devices(device, 1, bit_unit=True, series=series)[0])
|
|
1875
|
-
return f"device={device}, before={before}, write={write_value}, readback={readback}, restored={restored}"
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
def _compatibility_probe_random_word_write_restore(
|
|
1879
|
-
client: _StandardSlmpClient,
|
|
1880
|
-
*,
|
|
1881
|
-
devices: Sequence[str],
|
|
1882
|
-
preferred_write_value: int,
|
|
1883
|
-
series: PLCSeries,
|
|
1884
|
-
) -> str:
|
|
1885
|
-
before = client.read_random(word_devices=devices, series=series).word
|
|
1886
|
-
write_pairs: list[tuple[str, int]] = []
|
|
1887
|
-
for device in devices:
|
|
1888
|
-
current = int(before[str(device)])
|
|
1889
|
-
write_pairs.append((str(device), _choose_probe_word_value(current=current, preferred=preferred_write_value)))
|
|
1890
|
-
client.write_random_words(word_values=write_pairs, series=series)
|
|
1891
|
-
readback = client.read_random(word_devices=devices, series=series).word
|
|
1892
|
-
restore_pairs = [(str(device), int(before[str(device)])) for device in devices]
|
|
1893
|
-
client.write_random_words(word_values=restore_pairs, series=series)
|
|
1894
|
-
restored = client.read_random(word_devices=devices, series=series).word
|
|
1895
|
-
return (
|
|
1896
|
-
f"devices={list(devices)}, before={before}, write={dict(write_pairs)}, readback={readback}, restored={restored}"
|
|
1897
|
-
)
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
def _compatibility_probe_random_bit_write_restore(
|
|
1901
|
-
client: _StandardSlmpClient,
|
|
1902
|
-
*,
|
|
1903
|
-
devices: Sequence[str],
|
|
1904
|
-
series: PLCSeries,
|
|
1905
|
-
) -> str:
|
|
1906
|
-
before = {str(device): bool(client.read_devices(device, 1, bit_unit=True, series=series)[0]) for device in devices}
|
|
1907
|
-
write_pairs = [(device, not before[str(device)]) for device in devices]
|
|
1908
|
-
client.write_random_bits(bit_values=write_pairs, series=series)
|
|
1909
|
-
readback = {
|
|
1910
|
-
str(device): bool(client.read_devices(device, 1, bit_unit=True, series=series)[0]) for device in devices
|
|
1911
|
-
}
|
|
1912
|
-
restore_pairs = [(device, before[str(device)]) for device in devices]
|
|
1913
|
-
client.write_random_bits(bit_values=restore_pairs, series=series)
|
|
1914
|
-
restored = {
|
|
1915
|
-
str(device): bool(client.read_devices(device, 1, bit_unit=True, series=series)[0]) for device in devices
|
|
1916
|
-
}
|
|
1917
|
-
return (
|
|
1918
|
-
f"devices={list(devices)}, before={before}, write={dict(write_pairs)}, readback={readback}, restored={restored}"
|
|
1919
|
-
)
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
def _compatibility_probe_block_write_restore(
|
|
1923
|
-
client: _StandardSlmpClient,
|
|
1924
|
-
*,
|
|
1925
|
-
word_device: str | None,
|
|
1926
|
-
bit_device: str | None,
|
|
1927
|
-
preferred_word_value: int,
|
|
1928
|
-
preferred_bit_value: int,
|
|
1929
|
-
series: PLCSeries,
|
|
1930
|
-
) -> str:
|
|
1931
|
-
word_blocks = [(word_device, 1)] if word_device is not None else []
|
|
1932
|
-
bit_blocks = [(bit_device, 1)] if bit_device is not None else []
|
|
1933
|
-
before = client.read_block(word_blocks=word_blocks, bit_blocks=bit_blocks, series=series)
|
|
1934
|
-
|
|
1935
|
-
write_word_blocks: list[tuple[str, list[int]]] = []
|
|
1936
|
-
write_bit_blocks: list[tuple[str, list[int]]] = []
|
|
1937
|
-
if word_device is not None:
|
|
1938
|
-
current = int(before.word_blocks[0].values[0])
|
|
1939
|
-
write_word_blocks.append(
|
|
1940
|
-
(word_device, [_choose_probe_word_value(current=current, preferred=preferred_word_value)])
|
|
1941
|
-
)
|
|
1942
|
-
if bit_device is not None:
|
|
1943
|
-
current = int(before.bit_blocks[0].values[0])
|
|
1944
|
-
write_bit_blocks.append(
|
|
1945
|
-
(bit_device, [_choose_probe_word_value(current=current, preferred=preferred_bit_value)])
|
|
1946
|
-
)
|
|
1947
|
-
|
|
1948
|
-
client.write_block(
|
|
1949
|
-
word_blocks=write_word_blocks,
|
|
1950
|
-
bit_blocks=write_bit_blocks,
|
|
1951
|
-
series=series,
|
|
1952
|
-
split_mixed_blocks=False,
|
|
1953
|
-
retry_mixed_on_error=False,
|
|
1954
|
-
)
|
|
1955
|
-
readback = client.read_block(word_blocks=word_blocks, bit_blocks=bit_blocks, series=series)
|
|
1956
|
-
restore_word_blocks = [(row.device, row.values) for row in before.word_blocks]
|
|
1957
|
-
restore_bit_blocks = [(row.device, row.values) for row in before.bit_blocks]
|
|
1958
|
-
client.write_block(
|
|
1959
|
-
word_blocks=restore_word_blocks,
|
|
1960
|
-
bit_blocks=restore_bit_blocks,
|
|
1961
|
-
series=series,
|
|
1962
|
-
split_mixed_blocks=False,
|
|
1963
|
-
retry_mixed_on_error=False,
|
|
1964
|
-
)
|
|
1965
|
-
restored = client.read_block(word_blocks=word_blocks, bit_blocks=bit_blocks, series=series)
|
|
1966
|
-
return (
|
|
1967
|
-
f"word_device={word_device}, bit_device={bit_device}, "
|
|
1968
|
-
f"before_words={before.word_blocks}, before_bits={before.bit_blocks}, "
|
|
1969
|
-
f"write_words={write_word_blocks}, write_bits={write_bit_blocks}, "
|
|
1970
|
-
f"readback_words={readback.word_blocks}, readback_bits={readback.bit_blocks}, "
|
|
1971
|
-
f"restored_words={restored.word_blocks}, restored_bits={restored.bit_blocks}"
|
|
1972
|
-
)
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
def _compatibility_probe_memory_write_restore(
|
|
1976
|
-
client: _StandardSlmpClient,
|
|
1977
|
-
*,
|
|
1978
|
-
head_address: int,
|
|
1979
|
-
preferred_write_value: int,
|
|
1980
|
-
) -> str:
|
|
1981
|
-
before = int(client.memory_read_words(head_address, 1)[0])
|
|
1982
|
-
write_value = _choose_probe_word_value(current=before, preferred=preferred_write_value)
|
|
1983
|
-
client.memory_write_words(head_address, [write_value])
|
|
1984
|
-
readback = int(client.memory_read_words(head_address, 1)[0])
|
|
1985
|
-
client.memory_write_words(head_address, [before])
|
|
1986
|
-
restored = int(client.memory_read_words(head_address, 1)[0])
|
|
1987
|
-
return (
|
|
1988
|
-
f"head=0x{head_address:08X}, before=0x{before:04X}, write=0x{write_value:04X}, "
|
|
1989
|
-
f"readback=0x{readback:04X}, restored=0x{restored:04X}"
|
|
1990
|
-
)
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
def _compatibility_run_command(
|
|
1994
|
-
spec: CompatibilityCommandSpec,
|
|
1995
|
-
client: _StandardSlmpClient,
|
|
1996
|
-
*,
|
|
1997
|
-
series: PLCSeries,
|
|
1998
|
-
args: argparse.Namespace,
|
|
1999
|
-
) -> tuple[list[tuple[str, str, str]], dict[str, object]]:
|
|
2000
|
-
metadata: dict[str, object] = {}
|
|
2001
|
-
if spec.code == "0101":
|
|
2002
|
-
try:
|
|
2003
|
-
info = client.read_type_name()
|
|
2004
|
-
except Exception as exc: # noqa: BLE001
|
|
2005
|
-
return ([("type_name", "NG", str(exc))], metadata)
|
|
2006
|
-
model_label = _resolve_type_name_label(info)
|
|
2007
|
-
model_code = _format_model_code(info.model_code)
|
|
2008
|
-
inferred_series = _resolve_series_from_type_name(info)
|
|
2009
|
-
metadata = {
|
|
2010
|
-
"detected_model": model_label,
|
|
2011
|
-
"model_code": model_code,
|
|
2012
|
-
"detected_series": inferred_series.value if inferred_series is not None else None,
|
|
2013
|
-
}
|
|
2014
|
-
return ([("type_name", "OK", f"model={model_label}, model_code={model_code}")], metadata)
|
|
2015
|
-
if spec.code == "0401":
|
|
2016
|
-
return (
|
|
2017
|
-
[
|
|
2018
|
-
_compatibility_subprobe(
|
|
2019
|
-
"word_read",
|
|
2020
|
-
lambda: (
|
|
2021
|
-
f"device={args.word_device}, values={client.read_devices(args.word_device, 1, series=series)}"
|
|
2022
|
-
),
|
|
2023
|
-
),
|
|
2024
|
-
_compatibility_subprobe(
|
|
2025
|
-
"bit_read",
|
|
2026
|
-
lambda: (
|
|
2027
|
-
f"device={args.bit_device}, "
|
|
2028
|
-
f"values={client.read_devices(args.bit_device, 1, bit_unit=True, series=series)}"
|
|
2029
|
-
),
|
|
2030
|
-
),
|
|
2031
|
-
],
|
|
2032
|
-
metadata,
|
|
2033
|
-
)
|
|
2034
|
-
if spec.code == "1401":
|
|
2035
|
-
return (
|
|
2036
|
-
[
|
|
2037
|
-
_compatibility_subprobe(
|
|
2038
|
-
"word_write_restore",
|
|
2039
|
-
lambda: _compatibility_probe_direct_word_write_restore(
|
|
2040
|
-
client,
|
|
2041
|
-
device=args.word_device,
|
|
2042
|
-
preferred_write_value=args.word_write_value,
|
|
2043
|
-
series=series,
|
|
2044
|
-
),
|
|
2045
|
-
),
|
|
2046
|
-
_compatibility_subprobe(
|
|
2047
|
-
"bit_write_restore",
|
|
2048
|
-
lambda: _compatibility_probe_direct_bit_write_restore(
|
|
2049
|
-
client,
|
|
2050
|
-
device=args.bit_device,
|
|
2051
|
-
series=series,
|
|
2052
|
-
),
|
|
2053
|
-
),
|
|
2054
|
-
],
|
|
2055
|
-
metadata,
|
|
2056
|
-
)
|
|
2057
|
-
if spec.code == "0403":
|
|
2058
|
-
return (
|
|
2059
|
-
[
|
|
2060
|
-
_compatibility_subprobe(
|
|
2061
|
-
"random_read",
|
|
2062
|
-
lambda: (
|
|
2063
|
-
f"devices={args.random_read_word_device}, "
|
|
2064
|
-
f"values={client.read_random(word_devices=args.random_read_word_device, series=series).word}"
|
|
2065
|
-
),
|
|
2066
|
-
)
|
|
2067
|
-
],
|
|
2068
|
-
metadata,
|
|
2069
|
-
)
|
|
2070
|
-
if spec.code == "1402":
|
|
2071
|
-
return (
|
|
2072
|
-
[
|
|
2073
|
-
_compatibility_subprobe(
|
|
2074
|
-
"random_word_write_restore",
|
|
2075
|
-
lambda: _compatibility_probe_random_word_write_restore(
|
|
2076
|
-
client,
|
|
2077
|
-
devices=args.random_write_word_device,
|
|
2078
|
-
preferred_write_value=args.random_write_word_value,
|
|
2079
|
-
series=series,
|
|
2080
|
-
),
|
|
2081
|
-
),
|
|
2082
|
-
_compatibility_subprobe(
|
|
2083
|
-
"random_bit_write_restore",
|
|
2084
|
-
lambda: _compatibility_probe_random_bit_write_restore(
|
|
2085
|
-
client,
|
|
2086
|
-
devices=args.random_write_bit_device,
|
|
2087
|
-
series=series,
|
|
2088
|
-
),
|
|
2089
|
-
),
|
|
2090
|
-
],
|
|
2091
|
-
metadata,
|
|
2092
|
-
)
|
|
2093
|
-
if spec.code == "0801":
|
|
2094
|
-
|
|
2095
|
-
def _monitor_entry_detail() -> str:
|
|
2096
|
-
client.register_monitor_devices(word_devices=[args.monitor_word_device], series=series)
|
|
2097
|
-
return f"word_device={args.monitor_word_device}"
|
|
2098
|
-
|
|
2099
|
-
return (
|
|
2100
|
-
[
|
|
2101
|
-
_compatibility_subprobe(
|
|
2102
|
-
"monitor_entry",
|
|
2103
|
-
_monitor_entry_detail,
|
|
2104
|
-
)
|
|
2105
|
-
],
|
|
2106
|
-
metadata,
|
|
2107
|
-
)
|
|
2108
|
-
if spec.code == "0802":
|
|
2109
|
-
|
|
2110
|
-
def _monitor_execute_detail() -> str:
|
|
2111
|
-
client.register_monitor_devices(word_devices=[args.monitor_word_device], series=series)
|
|
2112
|
-
result = client.run_monitor_cycle(word_points=1, dword_points=0)
|
|
2113
|
-
return f"word_device={args.monitor_word_device}, values={result.word}"
|
|
2114
|
-
|
|
2115
|
-
return ([_compatibility_subprobe("monitor_execute", _monitor_execute_detail)], metadata)
|
|
2116
|
-
if spec.code == "0406":
|
|
2117
|
-
|
|
2118
|
-
def _block_read_mixed_detail() -> str:
|
|
2119
|
-
result = client.read_block(
|
|
2120
|
-
word_blocks=[(args.block_word_device, 1)],
|
|
2121
|
-
bit_blocks=[(args.block_bit_device, 1)],
|
|
2122
|
-
series=series,
|
|
2123
|
-
)
|
|
2124
|
-
return f"word_device={args.block_word_device}, bit_device={args.block_bit_device}, result={result}"
|
|
2125
|
-
|
|
2126
|
-
return (
|
|
2127
|
-
[
|
|
2128
|
-
_compatibility_subprobe(
|
|
2129
|
-
"block_read_word_only",
|
|
2130
|
-
lambda: (
|
|
2131
|
-
f"device={args.block_word_device}, "
|
|
2132
|
-
"result="
|
|
2133
|
-
f"{client.read_block(word_blocks=[(args.block_word_device, 1)], series=series).word_blocks}"
|
|
2134
|
-
),
|
|
2135
|
-
),
|
|
2136
|
-
_compatibility_subprobe(
|
|
2137
|
-
"block_read_bit_only",
|
|
2138
|
-
lambda: (
|
|
2139
|
-
f"device={args.block_bit_device}, "
|
|
2140
|
-
f"result={client.read_block(bit_blocks=[(args.block_bit_device, 1)], series=series).bit_blocks}"
|
|
2141
|
-
),
|
|
2142
|
-
),
|
|
2143
|
-
_compatibility_subprobe(
|
|
2144
|
-
"block_read_mixed",
|
|
2145
|
-
_block_read_mixed_detail,
|
|
2146
|
-
),
|
|
2147
|
-
],
|
|
2148
|
-
metadata,
|
|
2149
|
-
)
|
|
2150
|
-
if spec.code == "1406":
|
|
2151
|
-
return (
|
|
2152
|
-
[
|
|
2153
|
-
_compatibility_subprobe(
|
|
2154
|
-
"block_write_word_only",
|
|
2155
|
-
lambda: _compatibility_probe_block_write_restore(
|
|
2156
|
-
client,
|
|
2157
|
-
word_device=args.block_word_device,
|
|
2158
|
-
bit_device=None,
|
|
2159
|
-
preferred_word_value=args.block_word_write_value,
|
|
2160
|
-
preferred_bit_value=args.block_bit_write_value,
|
|
2161
|
-
series=series,
|
|
2162
|
-
),
|
|
2163
|
-
),
|
|
2164
|
-
_compatibility_subprobe(
|
|
2165
|
-
"block_write_bit_only",
|
|
2166
|
-
lambda: _compatibility_probe_block_write_restore(
|
|
2167
|
-
client,
|
|
2168
|
-
word_device=None,
|
|
2169
|
-
bit_device=args.block_bit_device,
|
|
2170
|
-
preferred_word_value=args.block_word_write_value,
|
|
2171
|
-
preferred_bit_value=args.block_bit_write_value,
|
|
2172
|
-
series=series,
|
|
2173
|
-
),
|
|
2174
|
-
),
|
|
2175
|
-
_compatibility_subprobe(
|
|
2176
|
-
"block_write_mixed",
|
|
2177
|
-
lambda: _compatibility_probe_block_write_restore(
|
|
2178
|
-
client,
|
|
2179
|
-
word_device=args.block_word_device,
|
|
2180
|
-
bit_device=args.block_bit_device,
|
|
2181
|
-
preferred_word_value=args.block_word_write_value,
|
|
2182
|
-
preferred_bit_value=args.block_bit_write_value,
|
|
2183
|
-
series=series,
|
|
2184
|
-
),
|
|
2185
|
-
),
|
|
2186
|
-
],
|
|
2187
|
-
metadata,
|
|
2188
|
-
)
|
|
2189
|
-
if spec.code == "1001":
|
|
2190
|
-
return (
|
|
2191
|
-
[
|
|
2192
|
-
_compatibility_request_subprobe(
|
|
2193
|
-
client,
|
|
2194
|
-
name="remote_run",
|
|
2195
|
-
command=Command.REMOTE_RUN,
|
|
2196
|
-
payload=b"\x01\x00\x00\x00",
|
|
2197
|
-
)
|
|
2198
|
-
],
|
|
2199
|
-
metadata,
|
|
2200
|
-
)
|
|
2201
|
-
if spec.code == "1002":
|
|
2202
|
-
return (
|
|
2203
|
-
[
|
|
2204
|
-
_compatibility_request_subprobe(
|
|
2205
|
-
client,
|
|
2206
|
-
name="remote_stop",
|
|
2207
|
-
command=Command.REMOTE_STOP,
|
|
2208
|
-
payload=b"\x01\x00",
|
|
2209
|
-
)
|
|
2210
|
-
],
|
|
2211
|
-
metadata,
|
|
2212
|
-
)
|
|
2213
|
-
if spec.code == "1005":
|
|
2214
|
-
return (
|
|
2215
|
-
[
|
|
2216
|
-
_compatibility_request_subprobe(
|
|
2217
|
-
client,
|
|
2218
|
-
name="remote_latch_clear",
|
|
2219
|
-
command=Command.REMOTE_LATCH_CLEAR,
|
|
2220
|
-
payload=b"\x01\x00",
|
|
2221
|
-
)
|
|
2222
|
-
],
|
|
2223
|
-
metadata,
|
|
2224
|
-
)
|
|
2225
|
-
if spec.code == "0619":
|
|
2226
|
-
return (
|
|
2227
|
-
[
|
|
2228
|
-
_compatibility_request_subprobe(
|
|
2229
|
-
client,
|
|
2230
|
-
name="self_test",
|
|
2231
|
-
command=Command.SELF_TEST,
|
|
2232
|
-
payload=b"\x04\x00\x11\x22\x33\x44",
|
|
2233
|
-
)
|
|
2234
|
-
],
|
|
2235
|
-
metadata,
|
|
2236
|
-
)
|
|
2237
|
-
if spec.code == "0613":
|
|
2238
|
-
return (
|
|
2239
|
-
[
|
|
2240
|
-
_compatibility_subprobe(
|
|
2241
|
-
"memory_read",
|
|
2242
|
-
lambda: (
|
|
2243
|
-
f"head=0x{args.memory_head:08X}, "
|
|
2244
|
-
f"values={client.memory_read_words(args.memory_head, args.memory_length)}"
|
|
2245
|
-
),
|
|
2246
|
-
)
|
|
2247
|
-
],
|
|
2248
|
-
metadata,
|
|
2249
|
-
)
|
|
2250
|
-
if spec.code == "1613":
|
|
2251
|
-
return (
|
|
2252
|
-
[
|
|
2253
|
-
_compatibility_subprobe(
|
|
2254
|
-
"memory_write_restore",
|
|
2255
|
-
lambda: _compatibility_probe_memory_write_restore(
|
|
2256
|
-
client,
|
|
2257
|
-
head_address=args.memory_head,
|
|
2258
|
-
preferred_write_value=args.memory_write_value,
|
|
2259
|
-
),
|
|
2260
|
-
)
|
|
2261
|
-
],
|
|
2262
|
-
metadata,
|
|
2263
|
-
)
|
|
2264
|
-
if spec.code == "1617":
|
|
2265
|
-
return (
|
|
2266
|
-
[
|
|
2267
|
-
_compatibility_request_subprobe(
|
|
2268
|
-
client,
|
|
2269
|
-
name="clear_error",
|
|
2270
|
-
command=Command.CLEAR_ERROR,
|
|
2271
|
-
payload=b"",
|
|
2272
|
-
)
|
|
2273
|
-
],
|
|
2274
|
-
metadata,
|
|
2275
|
-
)
|
|
2276
|
-
raise ValueError(f"unsupported compatibility command code: {spec.code}")
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
1759
|
def _probe_sm400_series(client: _StandardSlmpClient) -> tuple[PLCSeries, list[bool]]:
|
|
2280
1760
|
series, values = _probe_device_read_with_series(
|
|
2281
1761
|
client,
|
|
@@ -4984,258 +4464,6 @@ def pending_live_verification_main(argv: Sequence[str] | None = None) -> int:
|
|
|
4984
4464
|
return 0
|
|
4985
4465
|
|
|
4986
4466
|
|
|
4987
|
-
def compatibility_probe_main(argv: Sequence[str] | None = None) -> int:
|
|
4988
|
-
"""Probe SLMP compatibility across frame/profile combinations."""
|
|
4989
|
-
parser = argparse.ArgumentParser(description="Probe SLMP compatibility across frame/profile combinations")
|
|
4990
|
-
parser.add_argument("--host", required=True)
|
|
4991
|
-
parser.add_argument("--port", type=int, default=1025)
|
|
4992
|
-
parser.add_argument("--transport", choices=("tcp", "udp"), default="tcp")
|
|
4993
|
-
parser.add_argument("--timeout", type=float, default=3.0)
|
|
4994
|
-
parser.add_argument("--series", choices=("ql", "iqr"), default="ql")
|
|
4995
|
-
parser.add_argument("--frame-type", choices=("3e", "4e"), default="3e")
|
|
4996
|
-
parser.add_argument("--monitoring-timer", type=_int_auto, default=0x0010, help="e.g. 0x0010")
|
|
4997
|
-
parser.add_argument("--network", type=_int_auto, default=0x00)
|
|
4998
|
-
parser.add_argument("--station", type=_int_auto, default=0xFF)
|
|
4999
|
-
parser.add_argument("--module-io", type=_int_auto, default=0x03FF)
|
|
5000
|
-
parser.add_argument("--multidrop", type=_int_auto, default=0x00)
|
|
5001
|
-
parser.add_argument(
|
|
5002
|
-
"--target",
|
|
5003
|
-
help="optional SELF, SELF-CPU1..4, NWx-STy, or NAME,NETWORK,STATION,MODULE_IO,MULTIDROP",
|
|
5004
|
-
)
|
|
5005
|
-
parser.add_argument("--plc-label", help="stable label used for JSON/matrix output")
|
|
5006
|
-
parser.add_argument("--command", action="append", choices=COMPATIBILITY_COMMAND_CODES)
|
|
5007
|
-
parser.add_argument("--include-write-restore", action="store_true", help="include write/readback/restore probes")
|
|
5008
|
-
parser.add_argument("--include-remote-control", action="store_true", help="include remote RUN/STOP/Latch Clear")
|
|
5009
|
-
parser.add_argument("--include-maintenance", action="store_true", help="include Clear Error")
|
|
5010
|
-
parser.add_argument("--word-device", default="D130")
|
|
5011
|
-
parser.add_argument("--bit-device", default="M120")
|
|
5012
|
-
parser.add_argument("--random-read-word-device", action="append")
|
|
5013
|
-
parser.add_argument("--random-write-word-device", action="append")
|
|
5014
|
-
parser.add_argument("--random-write-bit-device", action="append")
|
|
5015
|
-
parser.add_argument("--monitor-word-device", default="D130")
|
|
5016
|
-
parser.add_argument("--block-word-device", default="D140")
|
|
5017
|
-
parser.add_argument("--block-bit-device", default="M140")
|
|
5018
|
-
parser.add_argument("--memory-head", type=_int_auto, default=0x0000)
|
|
5019
|
-
parser.add_argument("--memory-length", type=int, default=1)
|
|
5020
|
-
parser.add_argument("--word-write-value", type=_int_auto, default=0x0000)
|
|
5021
|
-
parser.add_argument("--random-write-word-value", type=_int_auto, default=0x0000)
|
|
5022
|
-
parser.add_argument("--block-word-write-value", type=_int_auto, default=0x0000)
|
|
5023
|
-
parser.add_argument("--block-bit-write-value", type=_int_auto, default=0x0000)
|
|
5024
|
-
parser.add_argument("--memory-write-value", type=_int_auto, default=0x0000)
|
|
5025
|
-
parser.add_argument("--output-markdown")
|
|
5026
|
-
parser.add_argument("--output-json")
|
|
5027
|
-
args = parser.parse_args(list(argv) if argv is not None else None)
|
|
5028
|
-
|
|
5029
|
-
selected_specs = _compatibility_selected_specs(
|
|
5030
|
-
command_codes=args.command,
|
|
5031
|
-
include_write_restore=args.include_write_restore,
|
|
5032
|
-
include_remote_control=args.include_remote_control,
|
|
5033
|
-
include_maintenance=args.include_maintenance,
|
|
5034
|
-
)
|
|
5035
|
-
if not selected_specs:
|
|
5036
|
-
parser.error("no commands selected; enable at least one probe group or command")
|
|
5037
|
-
if args.command:
|
|
5038
|
-
selected_codes = {spec.code for spec in selected_specs}
|
|
5039
|
-
missing = [code for code in args.command if code not in selected_codes]
|
|
5040
|
-
if missing:
|
|
5041
|
-
parser.error(
|
|
5042
|
-
"requested commands are not enabled by the selected risk flags: " + ", ".join(sorted(set(missing)))
|
|
5043
|
-
)
|
|
5044
|
-
|
|
5045
|
-
if args.target:
|
|
5046
|
-
try:
|
|
5047
|
-
target = _parse_named_target(args.target).target
|
|
5048
|
-
except ValueError as exc:
|
|
5049
|
-
parser.error(str(exc))
|
|
5050
|
-
else:
|
|
5051
|
-
target = SlmpTarget(
|
|
5052
|
-
network=args.network,
|
|
5053
|
-
station=args.station,
|
|
5054
|
-
module_io=args.module_io,
|
|
5055
|
-
multidrop=args.multidrop,
|
|
5056
|
-
)
|
|
5057
|
-
|
|
5058
|
-
args.random_read_word_device = args.random_read_word_device or [args.word_device, "D131"]
|
|
5059
|
-
args.random_write_word_device = args.random_write_word_device or ["D135"]
|
|
5060
|
-
args.random_write_bit_device = args.random_write_bit_device or ["M125"]
|
|
5061
|
-
plc_label = args.plc_label or f"{args.host}_n{target.network:02X}_s{target.station:02X}"
|
|
5062
|
-
markdown_output = args.output_markdown or _compatibility_default_output(
|
|
5063
|
-
plc_label=plc_label,
|
|
5064
|
-
filename="compatibility_probe_latest.md",
|
|
5065
|
-
)
|
|
5066
|
-
json_output = args.output_json or _compatibility_default_output(
|
|
5067
|
-
plc_label=plc_label,
|
|
5068
|
-
filename="compatibility_probe_latest.json",
|
|
5069
|
-
)
|
|
5070
|
-
|
|
5071
|
-
frame_candidates = [FrameType(args.frame_type)]
|
|
5072
|
-
series_candidates = [PLCSeries(args.series)]
|
|
5073
|
-
|
|
5074
|
-
rows: list[tuple[str, str, str]] = []
|
|
5075
|
-
result_rows: list[dict[str, object]] = []
|
|
5076
|
-
selected_command_codes = [spec.code for spec in selected_specs]
|
|
5077
|
-
risk_groups = sorted({spec.risk_group for spec in selected_specs})
|
|
5078
|
-
|
|
5079
|
-
print("=== Compatibility Probe ===")
|
|
5080
|
-
print(f"PLC Label={plc_label}")
|
|
5081
|
-
print(f"Host={args.host}, Port={args.port}, Transport={args.transport}")
|
|
5082
|
-
print(
|
|
5083
|
-
"Target="
|
|
5084
|
-
f"network=0x{target.network:02X}, station=0x{target.station:02X}, "
|
|
5085
|
-
f"module_io=0x{target.module_io:04X}, multidrop=0x{target.multidrop:02X}"
|
|
5086
|
-
)
|
|
5087
|
-
print(
|
|
5088
|
-
"Combinations="
|
|
5089
|
-
+ ",".join(f"{frame.value}/{series.value}" for frame in frame_candidates for series in series_candidates)
|
|
5090
|
-
)
|
|
5091
|
-
print(f"Commands={','.join(selected_command_codes)}")
|
|
5092
|
-
|
|
5093
|
-
for frame_type in frame_candidates:
|
|
5094
|
-
for series in series_candidates:
|
|
5095
|
-
combo_label = f"{frame_type.value}/{series.value}"
|
|
5096
|
-
command_rows: list[dict[str, object]] = []
|
|
5097
|
-
combo_result: dict[str, object] = {
|
|
5098
|
-
"frame_type": frame_type.value,
|
|
5099
|
-
"access_profile": series.value,
|
|
5100
|
-
"commands": command_rows,
|
|
5101
|
-
}
|
|
5102
|
-
try:
|
|
5103
|
-
with SlmpClient(
|
|
5104
|
-
args.host,
|
|
5105
|
-
port=args.port,
|
|
5106
|
-
transport=args.transport,
|
|
5107
|
-
timeout=args.timeout,
|
|
5108
|
-
plc_series=series,
|
|
5109
|
-
frame_type=frame_type,
|
|
5110
|
-
default_target=target,
|
|
5111
|
-
monitoring_timer=args.monitoring_timer,
|
|
5112
|
-
) as client:
|
|
5113
|
-
for spec in selected_specs:
|
|
5114
|
-
subresults, metadata = _compatibility_run_command(spec, client, series=series, args=args)
|
|
5115
|
-
status = _compatibility_summarize_subresults(subresults)
|
|
5116
|
-
detail = _compatibility_format_subresults(subresults)
|
|
5117
|
-
item = f"{combo_label} {spec.code} {spec.name}"
|
|
5118
|
-
rows.append((item, status, detail))
|
|
5119
|
-
print(f"[{status}] {item}: {detail}")
|
|
5120
|
-
command_row: dict[str, object] = {
|
|
5121
|
-
"code": spec.code,
|
|
5122
|
-
"category": spec.category,
|
|
5123
|
-
"name": spec.name,
|
|
5124
|
-
"risk_group": spec.risk_group,
|
|
5125
|
-
"status": status,
|
|
5126
|
-
"detail": detail,
|
|
5127
|
-
"subresults": [
|
|
5128
|
-
{"name": name, "status": sub_status, "detail": sub_detail}
|
|
5129
|
-
for name, sub_status, sub_detail in subresults
|
|
5130
|
-
],
|
|
5131
|
-
}
|
|
5132
|
-
command_rows.append(command_row)
|
|
5133
|
-
for key, value in metadata.items():
|
|
5134
|
-
if value is not None and key not in combo_result:
|
|
5135
|
-
combo_result[key] = value
|
|
5136
|
-
except Exception as exc: # noqa: BLE001
|
|
5137
|
-
detail = str(exc)
|
|
5138
|
-
print(f"[NG] {combo_label} connection: {detail}")
|
|
5139
|
-
combo_result["connection_error"] = detail
|
|
5140
|
-
for spec in selected_specs:
|
|
5141
|
-
item = f"{combo_label} {spec.code} {spec.name}"
|
|
5142
|
-
rows.append((item, "NG", detail))
|
|
5143
|
-
command_rows.append(
|
|
5144
|
-
{
|
|
5145
|
-
"code": spec.code,
|
|
5146
|
-
"category": spec.category,
|
|
5147
|
-
"name": spec.name,
|
|
5148
|
-
"risk_group": spec.risk_group,
|
|
5149
|
-
"status": "NG",
|
|
5150
|
-
"detail": detail,
|
|
5151
|
-
"subresults": [{"name": "connection", "status": "NG", "detail": detail}],
|
|
5152
|
-
}
|
|
5153
|
-
)
|
|
5154
|
-
result_rows.append(combo_result)
|
|
5155
|
-
|
|
5156
|
-
json_payload: dict[str, object] = {
|
|
5157
|
-
"schema_version": 1,
|
|
5158
|
-
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
5159
|
-
"plc_label": plc_label,
|
|
5160
|
-
"host": args.host,
|
|
5161
|
-
"port": args.port,
|
|
5162
|
-
"transport": args.transport,
|
|
5163
|
-
"requested_series": args.series,
|
|
5164
|
-
"requested_frame_type": args.frame_type,
|
|
5165
|
-
"risk_groups": risk_groups,
|
|
5166
|
-
"selected_commands": selected_command_codes,
|
|
5167
|
-
"target": {
|
|
5168
|
-
"network": target.network,
|
|
5169
|
-
"station": target.station,
|
|
5170
|
-
"module_io": int(target.module_io),
|
|
5171
|
-
"multidrop": target.multidrop,
|
|
5172
|
-
},
|
|
5173
|
-
"results": result_rows,
|
|
5174
|
-
}
|
|
5175
|
-
_write_json_report(json_output, json_payload)
|
|
5176
|
-
|
|
5177
|
-
summary = Counter(status for _, status, _ in rows)
|
|
5178
|
-
_write_markdown_report(
|
|
5179
|
-
markdown_output,
|
|
5180
|
-
title="# Compatibility Probe Report",
|
|
5181
|
-
header_lines=[
|
|
5182
|
-
f"- Date: {json_payload['generated_at']}",
|
|
5183
|
-
f"- PLC Label: {plc_label}",
|
|
5184
|
-
f"- Host: {args.host}",
|
|
5185
|
-
f"- Port: {args.port}",
|
|
5186
|
-
f"- Transport: {args.transport}",
|
|
5187
|
-
f"- Requested series: {args.series}",
|
|
5188
|
-
f"- Requested frame: {args.frame_type}",
|
|
5189
|
-
(
|
|
5190
|
-
f"- Target: network=0x{target.network:02X}, station=0x{target.station:02X}, "
|
|
5191
|
-
f"module_io=0x{int(target.module_io):04X}, multidrop=0x{target.multidrop:02X}"
|
|
5192
|
-
),
|
|
5193
|
-
f"- Risk groups: {', '.join(risk_groups)}",
|
|
5194
|
-
f"- Commands: {', '.join(selected_command_codes)}",
|
|
5195
|
-
f"- Summary: OK={summary['OK']}, PARTIAL={summary['PARTIAL']}, NG={summary['NG']}, SKIP={summary['SKIP']}",
|
|
5196
|
-
f"- JSON: {json_output}",
|
|
5197
|
-
],
|
|
5198
|
-
rows=rows,
|
|
5199
|
-
)
|
|
5200
|
-
print(f"[DONE] markdown={markdown_output}")
|
|
5201
|
-
print(f"[DONE] json={json_output}")
|
|
5202
|
-
return 0
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
def compatibility_matrix_render_main(argv: Sequence[str] | None = None) -> int:
|
|
5206
|
-
"""Render PLC_COMPATIBILITY.md from compatibility probe JSON files."""
|
|
5207
|
-
parser = argparse.ArgumentParser(description="Render PLC_COMPATIBILITY.md from compatibility probe JSON files")
|
|
5208
|
-
parser.add_argument("--input", action="append", required=True, help="compatibility_probe_latest.json; repeatable")
|
|
5209
|
-
parser.add_argument("--output", default="internal_docs/validation/reports/PLC_COMPATIBILITY.md")
|
|
5210
|
-
parser.add_argument(
|
|
5211
|
-
"--policy-output",
|
|
5212
|
-
default=_compatibility_default_policy_output(),
|
|
5213
|
-
help="structured policy JSON generated from the same probe inputs",
|
|
5214
|
-
)
|
|
5215
|
-
parser.add_argument(
|
|
5216
|
-
"--omit-pending-columns",
|
|
5217
|
-
action="store_true",
|
|
5218
|
-
help="hide command columns that are PENDING for every PLC row",
|
|
5219
|
-
)
|
|
5220
|
-
args = parser.parse_args(list(argv) if argv is not None else None)
|
|
5221
|
-
|
|
5222
|
-
source_paths = [Path(path) for path in args.input]
|
|
5223
|
-
loaded: list[Mapping[str, object]] = []
|
|
5224
|
-
for path in source_paths:
|
|
5225
|
-
loaded.append(json.loads(path.read_text(encoding="utf-8")))
|
|
5226
|
-
|
|
5227
|
-
content = _render_compatibility_matrix_markdown(
|
|
5228
|
-
loaded,
|
|
5229
|
-
source_paths=source_paths,
|
|
5230
|
-
omit_pending_columns=args.omit_pending_columns,
|
|
5231
|
-
)
|
|
5232
|
-
_write_text_report(args.output, content)
|
|
5233
|
-
_write_json_report(args.policy_output, _build_compatibility_policy(loaded))
|
|
5234
|
-
print(f"[DONE] output={args.output}")
|
|
5235
|
-
print(f"[DONE] policy={args.policy_output}")
|
|
5236
|
-
return 0
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
4467
|
def device_access_matrix_sync_main(argv: Sequence[str] | None = None) -> int:
|
|
5240
4468
|
"""Render device_access_matrix.md from device_access_matrix.csv."""
|
|
5241
4469
|
parser = argparse.ArgumentParser(description="Render device_access_matrix.md from device_access_matrix.csv")
|
|
@@ -1271,7 +1271,7 @@ _RANDOM_DWORD_ONLY_DIRECT_CODES = frozenset({"LCN", "LZ"})
|
|
|
1271
1271
|
_G_HG_CODES = frozenset({"G", "HG"})
|
|
1272
1272
|
_TEMPORARILY_UNSUPPORTED_TYPED_CODES = frozenset({"G", "HG"})
|
|
1273
1273
|
_BOUNDARY_START_ACCEPTANCE_CODES = frozenset({"R", "ZR"})
|
|
1274
|
-
_MIXED_BLOCK_RETRY_END_CODES = frozenset({0xC056,
|
|
1274
|
+
_MIXED_BLOCK_RETRY_END_CODES = frozenset({0xC056, 0xC061})
|
|
1275
1275
|
|
|
1276
1276
|
|
|
1277
1277
|
def _encode_label_name(label: str) -> bytes:
|
{slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15/slmp_connect_python.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slmp-connect-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.15
|
|
4
4
|
Summary: SLMP Connect Python: client library for Mitsubishi SLMP binary communication
|
|
5
5
|
Author: fa-yoshinobu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -43,6 +43,13 @@ Dynamic: license-file
|
|
|
43
43
|
|
|
44
44
|

|
|
45
45
|
|
|
46
|
+
[](https://github.com/fa-yoshinobu/plc-comm-slmp-python/actions/workflows/automated-release.yml)
|
|
47
|
+
[](https://github.com/fa-yoshinobu/plc-comm-slmp-python/actions/workflows/release.yml)
|
|
48
|
+
|
|
49
|
+
[](https://www.python.org/)
|
|
50
|
+
[](https://www.mkdocs.org/)
|
|
51
|
+
[](https://pages.github.com/)
|
|
52
|
+
|
|
46
53
|
High-level SLMP helpers for Mitsubishi PLC communication over Binary 3E and 4E frames.
|
|
47
54
|
|
|
48
55
|
This repository treats the high-level helper layer as the recommended user surface:
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
[console_scripts]
|
|
2
|
-
slmp-compatibility-matrix-render = slmp.cli:compatibility_matrix_render_main
|
|
3
|
-
slmp-compatibility-probe = slmp.cli:compatibility_probe_main
|
|
4
2
|
slmp-connection-check = slmp.cli:connection_check_main
|
|
5
3
|
slmp-device-access-matrix-sync = slmp.cli:device_access_matrix_sync_main
|
|
6
4
|
slmp-device-range-probe = slmp.cli:device_range_probe_main
|
|
@@ -1221,169 +1221,6 @@ class TestCli(unittest.TestCase):
|
|
|
1221
1221
|
self.assertEqual(PolicyOtherStationClient.type_name_calls, [("4e", "ql")])
|
|
1222
1222
|
self.assertIn("Resolved frame: 4e", report)
|
|
1223
1223
|
|
|
1224
|
-
def test_compatibility_probe_main_writes_json_and_markdown(self) -> None:
|
|
1225
|
-
"""Test test_compatibility_probe_main_writes_json_and_markdown."""
|
|
1226
|
-
|
|
1227
|
-
class CompatibilityProbeClient(SlmpClient):
|
|
1228
|
-
def __init__(
|
|
1229
|
-
self,
|
|
1230
|
-
host: str,
|
|
1231
|
-
port: int = 5000,
|
|
1232
|
-
*,
|
|
1233
|
-
transport: str = "tcp",
|
|
1234
|
-
timeout: float = 3.0,
|
|
1235
|
-
plc_series: PLCSeries | str = PLCSeries.QL,
|
|
1236
|
-
frame_type: cli.FrameType | str = cli.FrameType.FRAME_4E,
|
|
1237
|
-
default_target: SlmpTarget | None = None,
|
|
1238
|
-
monitoring_timer: int = 0x0010,
|
|
1239
|
-
raise_on_error: bool = True,
|
|
1240
|
-
trace_hook=None,
|
|
1241
|
-
) -> None:
|
|
1242
|
-
super().__init__(
|
|
1243
|
-
host,
|
|
1244
|
-
port,
|
|
1245
|
-
transport=transport,
|
|
1246
|
-
timeout=timeout,
|
|
1247
|
-
plc_series=plc_series,
|
|
1248
|
-
frame_type=frame_type,
|
|
1249
|
-
default_target=default_target,
|
|
1250
|
-
monitoring_timer=monitoring_timer,
|
|
1251
|
-
raise_on_error=raise_on_error,
|
|
1252
|
-
trace_hook=trace_hook,
|
|
1253
|
-
)
|
|
1254
|
-
|
|
1255
|
-
def connect(self) -> None:
|
|
1256
|
-
return None
|
|
1257
|
-
|
|
1258
|
-
def close(self) -> None:
|
|
1259
|
-
return None
|
|
1260
|
-
|
|
1261
|
-
def read_type_name(self) -> TypeNameInfo:
|
|
1262
|
-
return TypeNameInfo(raw=b"\x00" * 18, model="Q26UDEHCPU", model_code=0x026C)
|
|
1263
|
-
|
|
1264
|
-
def read_devices(self, device, points, *, bit_unit=False, series=None): # type: ignore[override]
|
|
1265
|
-
if bit_unit:
|
|
1266
|
-
return [True]
|
|
1267
|
-
return [0]
|
|
1268
|
-
|
|
1269
|
-
def read_block(self, *, word_blocks=(), bit_blocks=(), series=None, split_mixed_blocks=False): # type: ignore[override]
|
|
1270
|
-
return BlockReadResult(word_blocks=[], bit_blocks=[])
|
|
1271
|
-
|
|
1272
|
-
with TemporaryDirectory() as tmp:
|
|
1273
|
-
output_md = Path(tmp) / "compatibility_probe_latest.md"
|
|
1274
|
-
output_json = Path(tmp) / "compatibility_probe_latest.json"
|
|
1275
|
-
with patch.object(cli, "SlmpClient", CompatibilityProbeClient):
|
|
1276
|
-
rc = cli.compatibility_probe_main(
|
|
1277
|
-
[
|
|
1278
|
-
"--host",
|
|
1279
|
-
"192.168.250.100",
|
|
1280
|
-
"--plc-label",
|
|
1281
|
-
"Q26UDEHCPU_BuiltIn",
|
|
1282
|
-
"--series",
|
|
1283
|
-
"ql",
|
|
1284
|
-
"--frame-type",
|
|
1285
|
-
"3e",
|
|
1286
|
-
"--command",
|
|
1287
|
-
"0101",
|
|
1288
|
-
"--command",
|
|
1289
|
-
"0401",
|
|
1290
|
-
"--command",
|
|
1291
|
-
"0406",
|
|
1292
|
-
"--output-markdown",
|
|
1293
|
-
str(output_md),
|
|
1294
|
-
"--output-json",
|
|
1295
|
-
str(output_json),
|
|
1296
|
-
]
|
|
1297
|
-
)
|
|
1298
|
-
report = output_md.read_text(encoding="utf-8")
|
|
1299
|
-
payload = json.loads(output_json.read_text(encoding="utf-8"))
|
|
1300
|
-
|
|
1301
|
-
self.assertEqual(rc, 0)
|
|
1302
|
-
self.assertEqual(payload["plc_label"], "Q26UDEHCPU_BuiltIn")
|
|
1303
|
-
self.assertEqual(payload["selected_commands"], ["0101", "0401", "0406"])
|
|
1304
|
-
self.assertEqual(payload["results"][0]["frame_type"], "3e")
|
|
1305
|
-
self.assertEqual(payload["results"][0]["access_profile"], "ql")
|
|
1306
|
-
self.assertEqual(payload["results"][0]["detected_model"], "Q26UDHCPU, Q26UDEHCPU")
|
|
1307
|
-
self.assertIn("3e/ql 0101 Read Type Name", report)
|
|
1308
|
-
self.assertIn("3e/ql 0406 Block Read", report)
|
|
1309
|
-
|
|
1310
|
-
def test_compatibility_probe_main_rejects_auto_series_and_frame(self) -> None:
|
|
1311
|
-
"""Test test_compatibility_probe_main_rejects_auto_series_and_frame."""
|
|
1312
|
-
|
|
1313
|
-
with self.assertRaises(SystemExit) as cm:
|
|
1314
|
-
cli.compatibility_probe_main(
|
|
1315
|
-
[
|
|
1316
|
-
"--host",
|
|
1317
|
-
"192.168.250.100",
|
|
1318
|
-
"--series",
|
|
1319
|
-
"auto",
|
|
1320
|
-
"--frame-type",
|
|
1321
|
-
"auto",
|
|
1322
|
-
]
|
|
1323
|
-
)
|
|
1324
|
-
|
|
1325
|
-
self.assertEqual(cm.exception.code, 2)
|
|
1326
|
-
|
|
1327
|
-
def test_compatibility_matrix_render_main_renders_output(self) -> None:
|
|
1328
|
-
"""Test test_compatibility_matrix_render_main_renders_output."""
|
|
1329
|
-
with TemporaryDirectory() as tmp:
|
|
1330
|
-
input_a = Path(tmp) / "probe_a.json"
|
|
1331
|
-
input_b = Path(tmp) / "probe_b.json"
|
|
1332
|
-
output = Path(tmp) / "PLC_COMPATIBILITY.md"
|
|
1333
|
-
policy_output = Path(tmp) / "compatibility_policy.json"
|
|
1334
|
-
input_a.write_text(
|
|
1335
|
-
json.dumps(
|
|
1336
|
-
{
|
|
1337
|
-
"plc_label": "PLC-A",
|
|
1338
|
-
"results": [
|
|
1339
|
-
{
|
|
1340
|
-
"frame_type": "3e",
|
|
1341
|
-
"access_profile": "ql",
|
|
1342
|
-
"commands": [{"code": "0101", "status": "OK"}],
|
|
1343
|
-
}
|
|
1344
|
-
],
|
|
1345
|
-
}
|
|
1346
|
-
),
|
|
1347
|
-
encoding="utf-8",
|
|
1348
|
-
)
|
|
1349
|
-
input_b.write_text(
|
|
1350
|
-
json.dumps(
|
|
1351
|
-
{
|
|
1352
|
-
"plc_label": "PLC-B",
|
|
1353
|
-
"results": [
|
|
1354
|
-
{
|
|
1355
|
-
"frame_type": "4e",
|
|
1356
|
-
"access_profile": "iqr",
|
|
1357
|
-
"commands": [{"code": "0101", "status": "NG"}],
|
|
1358
|
-
}
|
|
1359
|
-
],
|
|
1360
|
-
}
|
|
1361
|
-
),
|
|
1362
|
-
encoding="utf-8",
|
|
1363
|
-
)
|
|
1364
|
-
rc = cli.compatibility_matrix_render_main(
|
|
1365
|
-
[
|
|
1366
|
-
"--input",
|
|
1367
|
-
str(input_a),
|
|
1368
|
-
"--input",
|
|
1369
|
-
str(input_b),
|
|
1370
|
-
"--omit-pending-columns",
|
|
1371
|
-
"--policy-output",
|
|
1372
|
-
str(policy_output),
|
|
1373
|
-
"--output",
|
|
1374
|
-
str(output),
|
|
1375
|
-
]
|
|
1376
|
-
)
|
|
1377
|
-
content = output.read_text(encoding="utf-8")
|
|
1378
|
-
policy = json.loads(policy_output.read_text(encoding="utf-8"))
|
|
1379
|
-
|
|
1380
|
-
self.assertEqual(rc, 0)
|
|
1381
|
-
self.assertIn("| PLC-A | unknown_target | YES |", content)
|
|
1382
|
-
self.assertIn("| PLC-B | unknown_target | NO |", content)
|
|
1383
|
-
self.assertNotIn("**1401**", content)
|
|
1384
|
-
self.assertIn("global", policy)
|
|
1385
|
-
self.assertIn("preferred_profiles", policy["global"])
|
|
1386
|
-
|
|
1387
1224
|
def test_manual_label_verification_main_requires_label_args(self) -> None:
|
|
1388
1225
|
"""Test test_manual_label_verification_main_requires_label_args."""
|
|
1389
1226
|
with self.assertRaises(SystemExit) as ctx:
|
|
@@ -2288,25 +2125,19 @@ class TestDeviceApi(unittest.TestCase):
|
|
|
2288
2125
|
self.assertEqual(len(client.requests), 1)
|
|
2289
2126
|
self.assertEqual(client.requests[0][0], Command.DEVICE_WRITE_BLOCK)
|
|
2290
2127
|
|
|
2291
|
-
def
|
|
2292
|
-
"""Test
|
|
2293
|
-
client = FakeClient()
|
|
2294
|
-
client.response_queue = [(0xC05B, b"")
|
|
2295
|
-
with
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
self.assertEqual(len(client.requests),
|
|
2304
|
-
self.assertEqual([request[0] for request in client.requests], [Command.DEVICE_WRITE_BLOCK] * 3)
|
|
2305
|
-
self.assertEqual([request[2][:2] for request in client.requests], [b"\x01\x01", b"\x01\x00", b"\x00\x01"])
|
|
2306
|
-
self.assertTrue(all(request[3]["raise_on_error"] is False for request in client.requests))
|
|
2307
|
-
self.assertTrue(
|
|
2308
|
-
any(isinstance(item.message, SlmpPracticalPathWarning) and "0xC05B" in str(item.message) for item in caught)
|
|
2309
|
-
)
|
|
2128
|
+
def test_write_block_retry_mixed_on_c05b_does_not_split(self) -> None:
|
|
2129
|
+
"""Test test_write_block_retry_mixed_on_c05b_does_not_split."""
|
|
2130
|
+
client = FakeClient()
|
|
2131
|
+
client.response_queue = [(0xC05B, b"")]
|
|
2132
|
+
with self.assertRaises(SlmpError) as ctx:
|
|
2133
|
+
client.write_block(
|
|
2134
|
+
word_blocks=[("D100", [0x1111])],
|
|
2135
|
+
bit_blocks=[("M200", [0x0001])],
|
|
2136
|
+
series=PLCSeries.IQR,
|
|
2137
|
+
retry_mixed_on_error=True,
|
|
2138
|
+
)
|
|
2139
|
+
self.assertEqual(ctx.exception.end_code, 0xC05B)
|
|
2140
|
+
self.assertEqual(len(client.requests), 1)
|
|
2310
2141
|
|
|
2311
2142
|
def test_write_block_retry_mixed_on_c056_splits_after_failed_combined_request(self) -> None:
|
|
2312
2143
|
"""Test test_write_block_retry_mixed_on_c056_splits_after_failed_combined_request."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp_connect_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp_connect_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{slmp_connect_python-0.1.14 → slmp_connect_python-0.1.15}/slmp_connect_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|