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.
Files changed (25) hide show
  1. {slmp_connect_python-1.0.1/slmp_connect_python.egg-info → slmp_connect_python-1.1.1}/PKG-INFO +7 -1
  2. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/README.md +9 -3
  3. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/pyproject.toml +1 -1
  4. slmp_connect_python-1.1.1/slmp/error_codes.py +45 -0
  5. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/utils.py +87 -60
  6. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1/slmp_connect_python.egg-info}/PKG-INFO +7 -1
  7. slmp_connect_python-1.0.1/slmp/error_codes.py +0 -2107
  8. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/LICENSE +0 -0
  9. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/MANIFEST.in +0 -0
  10. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/setup.cfg +0 -0
  11. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/__init__.py +0 -0
  12. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/_operations.py +0 -0
  13. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/async_client.py +0 -0
  14. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/cli.py +0 -0
  15. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/client.py +0 -0
  16. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/constants.py +0 -0
  17. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/core.py +0 -0
  18. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/device_ranges.py +0 -0
  19. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/errors.py +0 -0
  20. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp/py.typed +0 -0
  21. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp_connect_python.egg-info/SOURCES.txt +0 -0
  22. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp_connect_python.egg-info/dependency_links.txt +0 -0
  23. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp_connect_python.egg-info/entry_points.txt +0 -0
  24. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp_connect_python.egg-info/requires.txt +0 -0
  25. {slmp_connect_python-1.0.1 → slmp_connect_python-1.1.1}/slmp_connect_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slmp-connect-python
3
- Version: 1.0.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.0.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.upper()
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.upper()
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.upper()
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.upper()
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 or 0, bool(value))
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 or 0, bool(value))
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.strip().upper(), None
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
- return address.strip(), "U", None
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
- return SlmpAddress(text=text, base_device=text, dtype="U")
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
- if resolved_dtype not in {"BIT", "U", "S", "D", "L", "F"}:
541
- raise ValueError(f"Unsupported dtype {resolved_dtype!r}; expected BIT/U/S/D/L/F")
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}{suffix}",
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
- canonical_base = normalize_address(address.base_device, plc_profile=plc_profile)
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
- if address.explicit_dtype:
582
- if address.dtype not in {"BIT", "U", "S", "D", "L", "F"}:
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
- return str(parse_device(text, plc_profile=effective_address_profile))
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
- if ":" in text:
614
- return f"{canonical_base}:{dtype}"
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
- code = DEVICE_CODES.get(device.code)
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
- normalized = _normalize_dtype_for_device(device, dtype or "U")
636
- if not _address_has_explicit_dtype(address) and bit_index is None and device.code in _DEFAULT_DWORD_DEVICE_CODES:
637
- return "D"
638
- return normalized
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
- result[entry.address] = bool((word >> (entry.bit_index or 0)) & 1)
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
- result[entry.address] = bool((words[0] >> (entry.bit_index or 0)) & 1)
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 or "U")
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
- result[entry.address] = bool((word >> (entry.bit_index or 0)) & 1)
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
- result[entry.address] = bool((words[0] >> (entry.bit_index or 0)) & 1)
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 or "U")
1106
+ result[entry.address] = read_typed_sync(client, entry.device, entry.dtype)
1080
1107
 
1081
1108
  return result
1082
1109
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slmp-connect-python
3
- Version: 1.0.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>