slmp-connect-python 1.0.1__tar.gz → 1.1.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.
- {slmp_connect_python-1.0.1/slmp_connect_python.egg-info → slmp_connect_python-1.1.1}/PKG-INFO +7 -1
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/README.md +9 -3
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/pyproject.toml +1 -1
- slmp_connect_python-1.1.1/slmp/error_codes.py +45 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/utils.py +87 -60
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1/slmp_connect_python.egg-info}/PKG-INFO +7 -1
- slmp_connect_python-1.0.1/slmp/error_codes.py +0 -2107
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/LICENSE +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/MANIFEST.in +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/setup.cfg +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/__init__.py +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/_operations.py +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/async_client.py +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/cli.py +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/client.py +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/constants.py +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/core.py +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/device_ranges.py +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/errors.py +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/py.typed +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp_connect_python.egg-info/SOURCES.txt +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp_connect_python.egg-info/dependency_links.txt +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp_connect_python.egg-info/entry_points.txt +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp_connect_python.egg-info/requires.txt +0 -0
- {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp_connect_python.egg-info/top_level.txt +0 -0
{slmp_connect_python-1.0.1/slmp_connect_python.egg-info → slmp_connect_python-1.1.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slmp-connect-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: SLMP Connect Python: client library for MELSEC SLMP binary communication
|
|
5
5
|
Author: fa-yoshinobu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -93,3 +93,9 @@ See that page for verified PLC models, transports, dates, limitations, and retai
|
|
|
93
93
|
| License | [MIT](LICENSE) |
|
|
94
94
|
| Registry | [PyPI](https://pypi.org/project/slmp-connect-python/) |
|
|
95
95
|
| Package | `slmp-connect-python` |
|
|
96
|
+
|
|
97
|
+
## Commercial support
|
|
98
|
+
|
|
99
|
+
If you plan to embed this library in a paid or commercial product, please consider a separate support agreement or supporting the project as a sponsor.
|
|
100
|
+
|
|
101
|
+
Contact: <https://fa-labo.com/contact.html>
|
|
@@ -41,9 +41,9 @@ asyncio.run(main())
|
|
|
41
41
|
| Page | Use it for |
|
|
42
42
|
| --- | --- |
|
|
43
43
|
| [Full documentation site](https://fa-yoshinobu.github.io/plc-comm-docs-site/) | Unified docs for all PLC communication libraries. |
|
|
44
|
-
| [Getting started](docsrc/user/GETTING_STARTED.md) | Install the package, connect to your PLC, and run your first SLMP read/write. |
|
|
45
|
-
| [Usage guide](docsrc/user/USAGE_GUIDE.md) | Use the high-level API and common SLMP workflows. |
|
|
46
|
-
| [Supported registers](docsrc/user/SUPPORTED_REGISTERS.md) | Check supported device families, address syntax, and numbering rules. |
|
|
44
|
+
| [Getting started](docsrc/user/GETTING_STARTED.md) | Install the package, connect to your PLC, and run your first SLMP read/write. |
|
|
45
|
+
| [Usage guide](docsrc/user/USAGE_GUIDE.md) | Use the high-level API and common SLMP workflows. |
|
|
46
|
+
| [Supported registers](docsrc/user/SUPPORTED_REGISTERS.md) | Check supported device families, address syntax, and numbering rules. |
|
|
47
47
|
| [PLC profiles](docsrc/user/PROFILES.md) | Choose the canonical MELSEC profile and frame behavior. |
|
|
48
48
|
| [Examples](samples/README.md) | Run maintained Python samples. |
|
|
49
49
|
|
|
@@ -59,3 +59,9 @@ See that page for verified PLC models, transports, dates, limitations, and retai
|
|
|
59
59
|
| License | [MIT](LICENSE) |
|
|
60
60
|
| Registry | [PyPI](https://pypi.org/project/slmp-connect-python/) |
|
|
61
61
|
| Package | `slmp-connect-python` |
|
|
62
|
+
|
|
63
|
+
## Commercial support
|
|
64
|
+
|
|
65
|
+
If you plan to embed this library in a paid or commercial product, please consider a separate support agreement or supporting the project as a sponsor.
|
|
66
|
+
|
|
67
|
+
Contact: <https://fa-labo.com/contact.html>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slmp-connect-python"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.1.1"
|
|
8
8
|
description = "SLMP Connect Python: client library for MELSEC SLMP binary communication"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""SLMP end-code keys and categories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
SlmpEndCodeLanguage = Literal["en", "ja"]
|
|
8
|
+
|
|
9
|
+
_REMOTE_PASSWORD_END_CODES = {
|
|
10
|
+
0xC200,
|
|
11
|
+
0xC201,
|
|
12
|
+
0xC202,
|
|
13
|
+
0xC203,
|
|
14
|
+
0xC204,
|
|
15
|
+
0xC205,
|
|
16
|
+
0xC810,
|
|
17
|
+
0xC811,
|
|
18
|
+
0xC812,
|
|
19
|
+
0xC813,
|
|
20
|
+
0xC814,
|
|
21
|
+
0xC815,
|
|
22
|
+
0xC816,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_end_code_name(end_code: int) -> str:
|
|
27
|
+
"""Return the stable code-derived key for an SLMP end code."""
|
|
28
|
+
return f"slmp_end_code_{int(end_code) & 0xFFFF:04x}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_end_code_message(end_code: int, language: SlmpEndCodeLanguage = "en") -> str | None:
|
|
32
|
+
"""Return a user-facing message for an SLMP end code.
|
|
33
|
+
|
|
34
|
+
Localized message text is not embedded in this package. Resolve
|
|
35
|
+
get_end_code_name(end_code) in an application-owned catalog when text is
|
|
36
|
+
required.
|
|
37
|
+
"""
|
|
38
|
+
_ = end_code
|
|
39
|
+
_ = language
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_remote_password_end_code(end_code: int) -> bool:
|
|
44
|
+
"""Return True if the SLMP end code is related to remote password protection."""
|
|
45
|
+
return (int(end_code) & 0xFFFF) in _REMOTE_PASSWORD_END_CODES
|
|
@@ -29,7 +29,6 @@ if TYPE_CHECKING:
|
|
|
29
29
|
_WORD_DTYPES = frozenset({"U", "S"})
|
|
30
30
|
_DWORD_DTYPES = frozenset({"D", "L", "F"})
|
|
31
31
|
_UNBATCHED_DEVICE_CODES = frozenset({"G", "HG"})
|
|
32
|
-
_DEFAULT_DWORD_DEVICE_CODES = frozenset({"LTN", "LSTN", "LCN", "LZ"})
|
|
33
32
|
_RANDOM_DWORD_SCALAR_DEVICE_CODES = frozenset({"LCN", "LZ"})
|
|
34
33
|
_LONG_COUNTER_STATE_DEVICE_CODES = frozenset({"LCS", "LCC"})
|
|
35
34
|
_LONG_TIMER_READ_FAMILIES: dict[str, tuple[str, str]] = {
|
|
@@ -196,7 +195,7 @@ async def read_typed(
|
|
|
196
195
|
``bool`` for ``BIT``, otherwise ``int`` or ``float``.
|
|
197
196
|
"""
|
|
198
197
|
ref = _parse_device_for_client(client, device)
|
|
199
|
-
key = dtype
|
|
198
|
+
key = _require_dtype(dtype)
|
|
200
199
|
long_read = _get_long_timer_read(ref)
|
|
201
200
|
if long_read is not None:
|
|
202
201
|
_validate_long_timer_entry(str(ref), ref, key)
|
|
@@ -235,7 +234,7 @@ async def write_typed(
|
|
|
235
234
|
value: Application value to encode and write.
|
|
236
235
|
"""
|
|
237
236
|
ref = _parse_device_for_client(client, device)
|
|
238
|
-
key = dtype
|
|
237
|
+
key = _require_dtype(dtype)
|
|
239
238
|
long_read = _get_long_timer_read(ref)
|
|
240
239
|
if long_read is not None:
|
|
241
240
|
_validate_long_timer_entry(str(ref), ref, key)
|
|
@@ -268,7 +267,7 @@ def read_typed_sync(
|
|
|
268
267
|
) -> int | float | bool:
|
|
269
268
|
"""Synchronously read one logical value as a Python scalar."""
|
|
270
269
|
ref = _parse_device_for_client(client, device)
|
|
271
|
-
key = dtype
|
|
270
|
+
key = _require_dtype(dtype)
|
|
272
271
|
long_read = _get_long_timer_read(ref)
|
|
273
272
|
if long_read is not None:
|
|
274
273
|
_validate_long_timer_entry(str(ref), ref, key)
|
|
@@ -300,7 +299,7 @@ def write_typed_sync(
|
|
|
300
299
|
) -> None:
|
|
301
300
|
"""Synchronously write one logical value using the requested type."""
|
|
302
301
|
ref = _parse_device_for_client(client, device)
|
|
303
|
-
key = dtype
|
|
302
|
+
key = _require_dtype(dtype)
|
|
304
303
|
long_read = _get_long_timer_read(ref)
|
|
305
304
|
if long_read is not None:
|
|
306
305
|
_validate_long_timer_entry(str(ref), ref, key)
|
|
@@ -444,10 +443,11 @@ async def write_named(
|
|
|
444
443
|
base, dtype, bit_idx = _parse_address(address)
|
|
445
444
|
if dtype == "BIT_IN_WORD":
|
|
446
445
|
_validate_bit_in_word_target(address, _parse_device_for_address_profile(base, address_profile))
|
|
447
|
-
await write_bit_in_word(client, base, bit_idx
|
|
446
|
+
await write_bit_in_word(client, base, _require_bit_in_word_index(address, bit_idx), bool(value))
|
|
448
447
|
else:
|
|
449
448
|
device = _parse_device_for_address_profile(base, address_profile)
|
|
450
449
|
resolved_dtype = _resolve_dtype_for_address(address, device, dtype, bit_idx)
|
|
450
|
+
_validate_device_dtype(address, device, resolved_dtype)
|
|
451
451
|
_validate_long_timer_entry(address, device, resolved_dtype)
|
|
452
452
|
await write_typed(client, base, resolved_dtype, value)
|
|
453
453
|
|
|
@@ -462,10 +462,11 @@ def write_named_sync(
|
|
|
462
462
|
base, dtype, bit_idx = _parse_address(address)
|
|
463
463
|
if dtype == "BIT_IN_WORD":
|
|
464
464
|
_validate_bit_in_word_target(address, _parse_device_for_address_profile(base, address_profile))
|
|
465
|
-
write_bit_in_word_sync(client, base, bit_idx
|
|
465
|
+
write_bit_in_word_sync(client, base, _require_bit_in_word_index(address, bit_idx), bool(value))
|
|
466
466
|
else:
|
|
467
467
|
device = _parse_device_for_address_profile(base, address_profile)
|
|
468
468
|
resolved_dtype = _resolve_dtype_for_address(address, device, dtype, bit_idx)
|
|
469
|
+
_validate_device_dtype(address, device, resolved_dtype)
|
|
469
470
|
_validate_long_timer_entry(address, device, resolved_dtype)
|
|
470
471
|
write_typed_sync(client, base, resolved_dtype, value)
|
|
471
472
|
|
|
@@ -483,14 +484,33 @@ def _parse_address(address: str) -> tuple[str, str, int | None]:
|
|
|
483
484
|
address = address.strip()
|
|
484
485
|
if ":" in address:
|
|
485
486
|
base, dtype = address.split(":", 1)
|
|
486
|
-
return base.strip(), dtype
|
|
487
|
+
return base.strip(), _require_dtype(dtype), None
|
|
487
488
|
if "." in address:
|
|
488
489
|
base, bit_str = address.split(".", 1)
|
|
489
490
|
bit_text = bit_str.strip()
|
|
490
491
|
if len(bit_text) == 1 and bit_text.upper() in "0123456789ABCDEF":
|
|
491
492
|
return base.strip(), "BIT_IN_WORD", int(bit_text, 16)
|
|
492
493
|
raise ValueError(f"Invalid bit-in-word index {bit_str!r}; use one hex digit 0-F or ':' for dtype.")
|
|
493
|
-
|
|
494
|
+
raise ValueError(f"Address {address!r} requires an explicit dtype such as ':U', ':D', or ':BIT'.")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _require_dtype(dtype: str) -> str:
|
|
498
|
+
key = str(dtype).strip().upper()
|
|
499
|
+
if not key:
|
|
500
|
+
raise ValueError("dtype is required; specify BIT/U/S/D/L/F explicitly.")
|
|
501
|
+
if key == "BIT_IN_WORD":
|
|
502
|
+
raise ValueError("BIT_IN_WORD requires '.bit' notation such as 'D50.A'.")
|
|
503
|
+
if key not in {"BIT", "U", "S", "D", "L", "F"}:
|
|
504
|
+
raise ValueError(f"Unsupported dtype {key!r}; expected BIT/U/S/D/L/F")
|
|
505
|
+
return key
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _require_bit_in_word_index(address: str, bit_index: int | None) -> int:
|
|
509
|
+
if bit_index is None:
|
|
510
|
+
raise ValueError(f"bit-in-word address requires explicit bit index 0-F: {address!r}")
|
|
511
|
+
if not 0 <= bit_index <= 15:
|
|
512
|
+
raise ValueError(f"bit-in-word index must be 0-F: {address!r}")
|
|
513
|
+
return bit_index
|
|
494
514
|
|
|
495
515
|
|
|
496
516
|
def _effective_address_profile(
|
|
@@ -510,13 +530,13 @@ def parse_address(
|
|
|
510
530
|
) -> SlmpAddress:
|
|
511
531
|
"""Parse public SLMP helper-layer address notation.
|
|
512
532
|
|
|
513
|
-
Supported forms match :func:`read_named`: ``"D100"``, ``"D200:F"``,
|
|
514
|
-
``"D50.A"``, and direct bit devices such as ``"M100"``.
|
|
533
|
+
Supported forms match :func:`read_named`: ``"D100:U"``, ``"D200:F"``,
|
|
534
|
+
``"D50.A"``, and direct bit devices such as ``"M100:BIT"``.
|
|
515
535
|
"""
|
|
516
536
|
|
|
517
537
|
if not isinstance(address, str):
|
|
518
538
|
text = str(address)
|
|
519
|
-
|
|
539
|
+
raise ValueError(f"Address {text!r} requires an explicit dtype; pass a string such as '{text}:U'.")
|
|
520
540
|
|
|
521
541
|
effective_address_profile = _effective_address_profile(plc_profile=plc_profile)
|
|
522
542
|
raw_text = address.strip()
|
|
@@ -537,12 +557,10 @@ def parse_address(
|
|
|
537
557
|
)
|
|
538
558
|
|
|
539
559
|
resolved_dtype = _resolve_dtype_for_address(raw_text, device, dtype, bit_index)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
explicit_dtype = ":" in raw_text
|
|
543
|
-
suffix = f":{resolved_dtype}" if explicit_dtype else ""
|
|
560
|
+
_validate_device_dtype(raw_text, device, resolved_dtype)
|
|
561
|
+
explicit_dtype = True
|
|
544
562
|
return SlmpAddress(
|
|
545
|
-
text=f"{canonical_base}{
|
|
563
|
+
text=f"{canonical_base}:{resolved_dtype}",
|
|
546
564
|
base_device=canonical_base,
|
|
547
565
|
dtype=resolved_dtype,
|
|
548
566
|
bit_index=None,
|
|
@@ -573,16 +591,14 @@ def format_address(
|
|
|
573
591
|
if not isinstance(address, SlmpAddress):
|
|
574
592
|
return parse_address(address, plc_profile=plc_profile).text
|
|
575
593
|
|
|
576
|
-
|
|
594
|
+
effective_address_profile = _effective_address_profile(plc_profile=plc_profile)
|
|
595
|
+
canonical_base = str(parse_device(address.base_device, plc_profile=effective_address_profile))
|
|
577
596
|
if address.dtype == "BIT_IN_WORD":
|
|
578
597
|
if address.bit_index is None or not 0 <= address.bit_index <= 15:
|
|
579
598
|
raise ValueError("bit-in-word address requires bit_index 0-F")
|
|
580
599
|
return f"{canonical_base}.{address.bit_index:X}"
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
raise ValueError(f"Unsupported dtype {address.dtype!r}; expected BIT/U/S/D/L/F")
|
|
584
|
-
return f"{canonical_base}:{address.dtype}"
|
|
585
|
-
return canonical_base
|
|
600
|
+
dtype = _require_dtype(address.dtype)
|
|
601
|
+
return f"{canonical_base}:{dtype}"
|
|
586
602
|
|
|
587
603
|
|
|
588
604
|
def normalize_address(
|
|
@@ -604,15 +620,15 @@ def normalize_address(
|
|
|
604
620
|
|
|
605
621
|
text = address.strip()
|
|
606
622
|
if ":" not in text and "." not in text:
|
|
607
|
-
|
|
623
|
+
raise ValueError(f"Address {text!r} requires an explicit dtype such as ':U', ':D', or ':BIT'.")
|
|
608
624
|
|
|
609
625
|
base, dtype, bit_index = _parse_address(text)
|
|
610
626
|
canonical_base = str(parse_device(base, plc_profile=effective_address_profile))
|
|
611
627
|
if bit_index is not None:
|
|
612
628
|
return f"{canonical_base}.{bit_index:X}"
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return canonical_base
|
|
629
|
+
device = parse_device(base, plc_profile=effective_address_profile)
|
|
630
|
+
_validate_device_dtype(text, device, dtype)
|
|
631
|
+
return f"{canonical_base}:{dtype}"
|
|
616
632
|
|
|
617
633
|
|
|
618
634
|
def _is_batchable_word_device(device: DeviceRef) -> bool:
|
|
@@ -620,22 +636,28 @@ def _is_batchable_word_device(device: DeviceRef) -> bool:
|
|
|
620
636
|
return code is not None and code.unit == DeviceUnit.WORD and device.code not in _UNBATCHED_DEVICE_CODES
|
|
621
637
|
|
|
622
638
|
|
|
623
|
-
def _address_has_explicit_dtype(address: str) -> bool:
|
|
624
|
-
return ":" in address
|
|
625
|
-
|
|
626
|
-
|
|
627
639
|
def _normalize_dtype_for_device(device: DeviceRef, dtype: str) -> str:
|
|
628
|
-
|
|
629
|
-
if code is not None and code.unit == DeviceUnit.BIT and dtype == "U":
|
|
630
|
-
return "BIT"
|
|
631
|
-
return dtype
|
|
640
|
+
return _require_dtype(dtype)
|
|
632
641
|
|
|
633
642
|
|
|
634
643
|
def _resolve_dtype_for_address(address: str, device: DeviceRef, dtype: str, bit_index: int | None) -> str:
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
644
|
+
if bit_index is not None:
|
|
645
|
+
return "BIT_IN_WORD"
|
|
646
|
+
return _normalize_dtype_for_device(device, dtype)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _validate_device_dtype(address: str, device: DeviceRef, dtype: str) -> None:
|
|
650
|
+
if dtype == "BIT_IN_WORD":
|
|
651
|
+
return
|
|
652
|
+
code = DEVICE_CODES.get(device.code)
|
|
653
|
+
is_bit_device = code is not None and code.unit == DeviceUnit.BIT
|
|
654
|
+
if is_bit_device and dtype != "BIT":
|
|
655
|
+
raise ValueError(f"Address '{address}' is a bit device and requires ':BIT'.")
|
|
656
|
+
if not is_bit_device and dtype == "BIT":
|
|
657
|
+
raise ValueError(
|
|
658
|
+
f"Address '{address}' uses ':BIT', which is only valid for bit devices. "
|
|
659
|
+
"Use '.bit' notation for a bit inside a word device."
|
|
660
|
+
)
|
|
639
661
|
|
|
640
662
|
|
|
641
663
|
def _get_long_timer_read(device: DeviceRef) -> tuple[str, str] | None:
|
|
@@ -649,14 +671,10 @@ def _validate_long_timer_entry(address: str, device: DeviceRef, dtype: str) -> N
|
|
|
649
671
|
_, role = long_read
|
|
650
672
|
if role == "current":
|
|
651
673
|
if dtype not in {"D", "L"}:
|
|
652
|
-
raise ValueError(
|
|
653
|
-
f"Address '{address}' uses a 32-bit long current value. Use the plain form or ':D' / ':L'."
|
|
654
|
-
)
|
|
674
|
+
raise ValueError(f"Address '{address}' uses a 32-bit long current value. Specify ':D' or ':L'.")
|
|
655
675
|
return
|
|
656
676
|
if dtype != "BIT":
|
|
657
|
-
raise ValueError(
|
|
658
|
-
f"Address '{address}' is a long timer state device. Use the plain device form without a dtype override."
|
|
659
|
-
)
|
|
677
|
+
raise ValueError(f"Address '{address}' is a long timer state device. Specify ':BIT'.")
|
|
660
678
|
|
|
661
679
|
|
|
662
680
|
async def _write_long_family_value(
|
|
@@ -792,11 +810,23 @@ def _compile_read_plan(
|
|
|
792
810
|
for address in addresses:
|
|
793
811
|
base, dtype, bit_index = _parse_address(address)
|
|
794
812
|
device = _parse_device_for_address_profile(base, address_profile)
|
|
795
|
-
dtype = _resolve_dtype_for_address(address, device, dtype, bit_index)
|
|
796
|
-
_validate_long_timer_entry(address, device, dtype)
|
|
797
813
|
batch_kind: str | None = None
|
|
798
814
|
long_timer_read = _get_long_timer_read(device)
|
|
799
815
|
|
|
816
|
+
if dtype == "BIT_IN_WORD":
|
|
817
|
+
dtype = _resolve_dtype_for_address(address, device, dtype, bit_index)
|
|
818
|
+
bit_index = _require_bit_in_word_index(address, bit_index)
|
|
819
|
+
_validate_bit_in_word_target(address, device)
|
|
820
|
+
if _is_batchable_word_device(device):
|
|
821
|
+
batch_kind = "WORD"
|
|
822
|
+
if device not in seen_words:
|
|
823
|
+
word_devices.append(device)
|
|
824
|
+
seen_words.add(device)
|
|
825
|
+
else:
|
|
826
|
+
dtype = _resolve_dtype_for_address(address, device, dtype, bit_index)
|
|
827
|
+
_validate_device_dtype(address, device, dtype)
|
|
828
|
+
_validate_long_timer_entry(address, device, dtype)
|
|
829
|
+
|
|
800
830
|
if long_timer_read is not None and not (device.code == "LCN" and long_timer_read[1] == "current"):
|
|
801
831
|
batch_kind = "LONG_TIMER"
|
|
802
832
|
elif long_timer_read is not None:
|
|
@@ -804,13 +834,6 @@ def _compile_read_plan(
|
|
|
804
834
|
if device not in seen_dwords:
|
|
805
835
|
dword_devices.append(device)
|
|
806
836
|
seen_dwords.add(device)
|
|
807
|
-
elif dtype == "BIT_IN_WORD":
|
|
808
|
-
_validate_bit_in_word_target(address, device)
|
|
809
|
-
if _is_batchable_word_device(device):
|
|
810
|
-
batch_kind = "WORD"
|
|
811
|
-
if device not in seen_words:
|
|
812
|
-
word_devices.append(device)
|
|
813
|
-
seen_words.add(device)
|
|
814
837
|
elif dtype in _WORD_DTYPES:
|
|
815
838
|
if _is_batchable_word_device(device):
|
|
816
839
|
batch_kind = "WORD"
|
|
@@ -1019,7 +1042,8 @@ async def _read_named_with_plan(
|
|
|
1019
1042
|
if entry.batch_kind == "WORD":
|
|
1020
1043
|
word = word_values[str(entry.device)]
|
|
1021
1044
|
if entry.dtype == "BIT_IN_WORD":
|
|
1022
|
-
|
|
1045
|
+
bit_index = _require_bit_in_word_index(entry.address, entry.bit_index)
|
|
1046
|
+
result[entry.address] = bool((word >> bit_index) & 1)
|
|
1023
1047
|
else:
|
|
1024
1048
|
result[entry.address] = _decode_word_value(word, entry.dtype)
|
|
1025
1049
|
continue
|
|
@@ -1028,9 +1052,10 @@ async def _read_named_with_plan(
|
|
|
1028
1052
|
continue
|
|
1029
1053
|
if entry.dtype == "BIT_IN_WORD":
|
|
1030
1054
|
words = await client.read_devices(entry.device, 1, bit_unit=False)
|
|
1031
|
-
|
|
1055
|
+
bit_index = _require_bit_in_word_index(entry.address, entry.bit_index)
|
|
1056
|
+
result[entry.address] = bool((words[0] >> bit_index) & 1)
|
|
1032
1057
|
else:
|
|
1033
|
-
result[entry.address] = await read_typed(client, entry.device, entry.dtype
|
|
1058
|
+
result[entry.address] = await read_typed(client, entry.device, entry.dtype)
|
|
1034
1059
|
|
|
1035
1060
|
return result
|
|
1036
1061
|
|
|
@@ -1065,7 +1090,8 @@ def _read_named_with_plan_sync(
|
|
|
1065
1090
|
if entry.batch_kind == "WORD":
|
|
1066
1091
|
word = word_values[str(entry.device)]
|
|
1067
1092
|
if entry.dtype == "BIT_IN_WORD":
|
|
1068
|
-
|
|
1093
|
+
bit_index = _require_bit_in_word_index(entry.address, entry.bit_index)
|
|
1094
|
+
result[entry.address] = bool((word >> bit_index) & 1)
|
|
1069
1095
|
else:
|
|
1070
1096
|
result[entry.address] = _decode_word_value(word, entry.dtype)
|
|
1071
1097
|
continue
|
|
@@ -1074,9 +1100,10 @@ def _read_named_with_plan_sync(
|
|
|
1074
1100
|
continue
|
|
1075
1101
|
if entry.dtype == "BIT_IN_WORD":
|
|
1076
1102
|
words = client.read_devices(entry.device, 1, bit_unit=False)
|
|
1077
|
-
|
|
1103
|
+
bit_index = _require_bit_in_word_index(entry.address, entry.bit_index)
|
|
1104
|
+
result[entry.address] = bool((words[0] >> bit_index) & 1)
|
|
1078
1105
|
else:
|
|
1079
|
-
result[entry.address] = read_typed_sync(client, entry.device, entry.dtype
|
|
1106
|
+
result[entry.address] = read_typed_sync(client, entry.device, entry.dtype)
|
|
1080
1107
|
|
|
1081
1108
|
return result
|
|
1082
1109
|
|
{slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1/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: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: SLMP Connect Python: client library for MELSEC SLMP binary communication
|
|
5
5
|
Author: fa-yoshinobu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -93,3 +93,9 @@ See that page for verified PLC models, transports, dates, limitations, and retai
|
|
|
93
93
|
| License | [MIT](LICENSE) |
|
|
94
94
|
| Registry | [PyPI](https://pypi.org/project/slmp-connect-python/) |
|
|
95
95
|
| Package | `slmp-connect-python` |
|
|
96
|
+
|
|
97
|
+
## Commercial support
|
|
98
|
+
|
|
99
|
+
If you plan to embed this library in a paid or commercial product, please consider a separate support agreement or supporting the project as a sponsor.
|
|
100
|
+
|
|
101
|
+
Contact: <https://fa-labo.com/contact.html>
|