yrcom 0.2.0__tar.gz → 0.3.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 (27) hide show
  1. {yrcom-0.2.0/src/yrcom.egg-info → yrcom-0.3.1}/PKG-INFO +11 -2
  2. {yrcom-0.2.0 → yrcom-0.3.1}/README.md +5 -1
  3. {yrcom-0.2.0 → yrcom-0.3.1}/pyproject.toml +2 -1
  4. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom/__init__.py +9 -1
  5. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom/hses_client.py +21 -1
  6. yrcom-0.3.1/src/yrcom/hses_server_codec.py +93 -0
  7. {yrcom-0.2.0 → yrcom-0.3.1/src/yrcom.egg-info}/PKG-INFO +11 -2
  8. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom.egg-info/SOURCES.txt +2 -0
  9. yrcom-0.3.1/src/yrcom.egg-info/requires.txt +9 -0
  10. {yrcom-0.2.0 → yrcom-0.3.1}/tests/test_hses_client.py +24 -0
  11. yrcom-0.3.1/tests/test_hses_server_codec.py +74 -0
  12. yrcom-0.2.0/src/yrcom.egg-info/requires.txt +0 -3
  13. {yrcom-0.2.0 → yrcom-0.3.1}/LICENSE +0 -0
  14. {yrcom-0.2.0 → yrcom-0.3.1}/setup.cfg +0 -0
  15. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom/hses_errors.py +0 -0
  16. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom/hses_file.py +0 -0
  17. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom/hses_sim.py +0 -0
  18. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom/robot_connection.py +0 -0
  19. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom/yaskawa_client.py +0 -0
  20. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom.egg-info/dependency_links.txt +0 -0
  21. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom.egg-info/entry_points.txt +0 -0
  22. {yrcom-0.2.0 → yrcom-0.3.1}/src/yrcom.egg-info/top_level.txt +0 -0
  23. {yrcom-0.2.0 → yrcom-0.3.1}/tests/test_hses_file.py +0 -0
  24. {yrcom-0.2.0 → yrcom-0.3.1}/tests/test_hses_sim.py +0 -0
  25. {yrcom-0.2.0 → yrcom-0.3.1}/tests/test_robot_connection.py +0 -0
  26. {yrcom-0.2.0 → yrcom-0.3.1}/tests/test_yaskawa_client.py +0 -0
  27. {yrcom-0.2.0 → yrcom-0.3.1}/tests/test_yaskawa_commands.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yrcom
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Yaskawa robot communication — Ethernet Server, HSES and HSES File Control (pure stdlib)
5
5
  Author: cmosbk
6
6
  License: MIT
@@ -12,12 +12,21 @@ Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: pytest>=8; extra == "dev"
15
+ Provides-Extra: docs
16
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
17
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
18
+ Requires-Dist: mkdocstrings[python]>=0.26; extra == "docs"
19
+ Requires-Dist: black>=24; extra == "docs"
15
20
  Dynamic: license-file
16
21
 
17
22
  # yrcom
18
23
 
19
24
  [![CI](https://github.com/CMOSBK/yrcom/actions/workflows/ci.yml/badge.svg)](https://github.com/CMOSBK/yrcom/actions/workflows/ci.yml)
20
25
 
26
+ 📖 **Documentation: <https://cmosbk.github.io/yrcom/>** — getting started, the
27
+ transport guide and the full API reference (every command, generated from the
28
+ source).
29
+
21
30
  Yaskawa robot communication as a standalone Python library — a single,
22
31
  hardware-verified communication core meant to be shared across projects
23
32
  instead of being duplicated.
@@ -109,4 +118,4 @@ and a small HSES capture fixture (`tests/fixtures/`).
109
118
 
110
119
  ## License
111
120
 
112
- MIT — see [LICENSE](LICENSE).
121
+ MIT — see [LICENSE](https://github.com/CMOSBK/yrcom/blob/main/LICENSE).
@@ -2,6 +2,10 @@
2
2
 
3
3
  [![CI](https://github.com/CMOSBK/yrcom/actions/workflows/ci.yml/badge.svg)](https://github.com/CMOSBK/yrcom/actions/workflows/ci.yml)
4
4
 
5
+ 📖 **Documentation: <https://cmosbk.github.io/yrcom/>** — getting started, the
6
+ transport guide and the full API reference (every command, generated from the
7
+ source).
8
+
5
9
  Yaskawa robot communication as a standalone Python library — a single,
6
10
  hardware-verified communication core meant to be shared across projects
7
11
  instead of being duplicated.
@@ -93,4 +97,4 @@ and a small HSES capture fixture (`tests/fixtures/`).
93
97
 
94
98
  ## License
95
99
 
96
- MIT — see [LICENSE](LICENSE).
100
+ MIT — see [LICENSE](https://github.com/CMOSBK/yrcom/blob/main/LICENSE).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "yrcom"
7
- version = "0.2.0"
7
+ version = "0.3.1"
8
8
  description = "Yaskawa robot communication — Ethernet Server, HSES and HSES File Control (pure stdlib)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -15,6 +15,7 @@ dependencies = []
15
15
 
16
16
  [project.optional-dependencies]
17
17
  dev = ["pytest>=8"]
18
+ docs = ["mkdocs>=1.6", "mkdocs-material>=9.5", "mkdocstrings[python]>=0.26", "black>=24"]
18
19
 
19
20
  [project.scripts]
20
21
  yrcom-sim = "yrcom.hses_sim:_main"
@@ -32,8 +32,15 @@ from .robot_connection import (
32
32
  TCP_PORT,
33
33
  )
34
34
  from .hses_sim import HsesReplayServer
35
+ from .hses_server_codec import (
36
+ ServerCommand,
37
+ MalformedRequest,
38
+ decode_request,
39
+ encode_response,
40
+ encode_error,
41
+ )
35
42
 
36
- __version__ = "0.2.0"
43
+ __version__ = "0.3.1"
37
44
 
38
45
  __all__ = [
39
46
  "YaskawaClient", "VarType", "EthernetServerClient",
@@ -45,4 +52,5 @@ __all__ = [
45
52
  "RobotConnection", "PersistentTcpClient", "HsesWriteBlockedError",
46
53
  "TRANSPORT_HSES", "TRANSPORT_ETHERNET", "HSES_PORT", "TCP_PORT",
47
54
  "HsesReplayServer",
55
+ "ServerCommand", "MalformedRequest", "decode_request", "encode_response", "encode_error",
48
56
  ]
@@ -581,6 +581,8 @@ class HsesClient(_HsesSocket):
581
581
  position_group: int = 0,
582
582
  b_vars: "list[dict]" = [],
583
583
  i_vars: "list[dict]" = [],
584
+ d_vars: "list[dict]" = [],
585
+ r_vars: "list[dict]" = [],
584
586
  io_signals: "list[dict]" = [],
585
587
  read_cio: bool = False,
586
588
  ) -> dict:
@@ -590,7 +592,11 @@ class HsesClient(_HsesSocket):
590
592
  alarm_slots: number of alarm history slots (0=skip, max 4)
591
593
  include_job: read executing job info
592
594
  position_group: 0=skip, 1-8=pulse, 101-108=cartesian
593
- b_vars/i_vars: list of {"label": str, "no": int}
595
+ b_vars/i_vars/d_vars/r_vars:
596
+ list of {"label": str, "no": int}. Each becomes one
597
+ request inside the burst, so its value shares the
598
+ instant of the rest of the row rather than arriving a
599
+ round trip later.
594
600
  io_signals: list of {"label": str, "logical": int}
595
601
 
596
602
  Returns flat dict of all decoded fields.
@@ -622,6 +628,14 @@ class HsesClient(_HsesSocket):
622
628
  reqs.append((_CMD_I_VAR, v["no"], 1, _SVC_GET_SINGLE, b""))
623
629
  tags.append(f"i_var:{v['label']}")
624
630
 
631
+ for v in d_vars:
632
+ reqs.append((_CMD_D_VAR, v["no"], 1, _SVC_GET_SINGLE, b""))
633
+ tags.append(f"d_var:{v['label']}")
634
+
635
+ for v in r_vars:
636
+ reqs.append((_CMD_R_VAR, v["no"], 1, _SVC_GET_SINGLE, b""))
637
+ tags.append(f"r_var:{v['label']}")
638
+
625
639
  # CIO counter: low byte at logical 72000 (inst 7200), high byte at 72010 (inst 7201)
626
640
  if read_cio:
627
641
  reqs.append((_CMD_IO_PLURAL, 7200, 0, _SVC_READ_ALL, struct.pack("<I", 2)))
@@ -666,6 +680,12 @@ class HsesClient(_HsesSocket):
666
680
  elif tag.startswith("i_var:"):
667
681
  if resp.status == 0 and len(resp.data) >= 2:
668
682
  out[f"var_{tag[6:]}"] = struct.unpack_from("<h", resp.data, 0)[0]
683
+ elif tag.startswith("d_var:"):
684
+ if resp.status == 0 and len(resp.data) >= 4:
685
+ out[f"var_{tag[6:]}"] = struct.unpack_from("<i", resp.data, 0)[0]
686
+ elif tag.startswith("r_var:"):
687
+ if resp.status == 0 and len(resp.data) >= 4:
688
+ out[f"var_{tag[6:]}"] = struct.unpack_from("<f", resp.data, 0)[0]
669
689
  elif tag == "cio":
670
690
  if resp.status == 0 and len(resp.data) >= 6:
671
691
  # response: 4-byte count + 2 data bytes
@@ -0,0 +1,93 @@
1
+ """
2
+ Server-side HSES codec — the mirror image of hses_client.py's request
3
+ builder/response parser. Decodes incoming request bytes into a
4
+ ServerCommand; encodes results back into wire bytes.
5
+
6
+ Wire header is the 32-byte YERC header, identical in both directions:
7
+ 0:4 magic "YERC"
8
+ 4:6 header size (u16, always 32)
9
+ 6:8 data size (u16)
10
+ 8 Reserve1 (0x03 fixed)
11
+ 9 processing division (1 = robot control)
12
+ 10 ACK (0 = request, 1 = response)
13
+ 11 request id (echoed back)
14
+ 12:16 block number
15
+ 16:24 Reserve2 ("99999999" fixed)
16
+ 24:26 command (u16) — in a response this is the ECHOED command
17
+ 26:28 request: instance (u16) | response: status (byte) + pad
18
+ 28 request: attribute (byte) | response: added-status size (byte)
19
+ 29 request: service (byte) | response: pad
20
+ 30:32 request: padding | response: added-status word (u16)
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import struct
25
+ from dataclasses import dataclass
26
+
27
+ _MAGIC = b"YERC"
28
+ _HDR_SIZE = 0x0020
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class ServerCommand:
33
+ cmd: int
34
+ instance: int
35
+ attribute: int
36
+ service: int
37
+ req_id: int
38
+ payload: bytes
39
+
40
+
41
+ class MalformedRequest(Exception):
42
+ """Raw bytes are too short, or the header magic/size doesn't match."""
43
+
44
+
45
+ def decode_request(raw: bytes) -> ServerCommand:
46
+ if len(raw) < _HDR_SIZE:
47
+ raise MalformedRequest(f"request too short: {len(raw)} B")
48
+ if raw[0:4] != _MAGIC:
49
+ raise MalformedRequest(f"bad magic: {raw[0:4]!r}")
50
+ data_size = struct.unpack_from("<H", raw, 6)[0]
51
+ req_id = raw[11]
52
+ cmd = struct.unpack_from("<H", raw, 24)[0]
53
+ instance = struct.unpack_from("<H", raw, 26)[0]
54
+ attribute = raw[28]
55
+ service = raw[29]
56
+ payload = raw[_HDR_SIZE:_HDR_SIZE + data_size]
57
+ return ServerCommand(cmd=cmd, instance=instance, attribute=attribute,
58
+ service=service, req_id=req_id, payload=payload)
59
+
60
+
61
+ def _response_header(cmd: int, req_id: int, data_size: int) -> bytearray:
62
+ hdr = bytearray(_HDR_SIZE)
63
+ hdr[0:4] = _MAGIC
64
+ struct.pack_into("<H", hdr, 4, _HDR_SIZE)
65
+ struct.pack_into("<H", hdr, 6, data_size)
66
+ hdr[8] = 0x03
67
+ hdr[9] = 1 # processing division: robot control
68
+ hdr[10] = 0x01 # ACK = response
69
+ hdr[11] = req_id
70
+ struct.pack_into("<I", hdr, 12, 0)
71
+ hdr[16:24] = b"99999999"
72
+ struct.pack_into("<H", hdr, 24, cmd)
73
+ return hdr
74
+
75
+
76
+ def encode_response(cmd: int, req_id: int, data: bytes = b"") -> bytes:
77
+ """Success response (status 0x00, no added-status)."""
78
+ hdr = _response_header(cmd, req_id, len(data))
79
+ hdr[26] = 0x00 # status
80
+ hdr[27] = 0
81
+ hdr[28] = 0 # added-status size
82
+ struct.pack_into("<H", hdr, 30, 0)
83
+ return bytes(hdr) + data
84
+
85
+
86
+ def encode_error(cmd: int, req_id: int, status: int, added: int = 0) -> bytes:
87
+ """Error response — empty data, given status/added-status."""
88
+ hdr = _response_header(cmd, req_id, 0)
89
+ hdr[26] = status
90
+ hdr[27] = 0
91
+ hdr[28] = 1 if added else 0
92
+ struct.pack_into("<H", hdr, 30, added)
93
+ return bytes(hdr)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yrcom
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Yaskawa robot communication — Ethernet Server, HSES and HSES File Control (pure stdlib)
5
5
  Author: cmosbk
6
6
  License: MIT
@@ -12,12 +12,21 @@ Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: pytest>=8; extra == "dev"
15
+ Provides-Extra: docs
16
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
17
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
18
+ Requires-Dist: mkdocstrings[python]>=0.26; extra == "docs"
19
+ Requires-Dist: black>=24; extra == "docs"
15
20
  Dynamic: license-file
16
21
 
17
22
  # yrcom
18
23
 
19
24
  [![CI](https://github.com/CMOSBK/yrcom/actions/workflows/ci.yml/badge.svg)](https://github.com/CMOSBK/yrcom/actions/workflows/ci.yml)
20
25
 
26
+ 📖 **Documentation: <https://cmosbk.github.io/yrcom/>** — getting started, the
27
+ transport guide and the full API reference (every command, generated from the
28
+ source).
29
+
21
30
  Yaskawa robot communication as a standalone Python library — a single,
22
31
  hardware-verified communication core meant to be shared across projects
23
32
  instead of being duplicated.
@@ -109,4 +118,4 @@ and a small HSES capture fixture (`tests/fixtures/`).
109
118
 
110
119
  ## License
111
120
 
112
- MIT — see [LICENSE](LICENSE).
121
+ MIT — see [LICENSE](https://github.com/CMOSBK/yrcom/blob/main/LICENSE).
@@ -5,6 +5,7 @@ src/yrcom/__init__.py
5
5
  src/yrcom/hses_client.py
6
6
  src/yrcom/hses_errors.py
7
7
  src/yrcom/hses_file.py
8
+ src/yrcom/hses_server_codec.py
8
9
  src/yrcom/hses_sim.py
9
10
  src/yrcom/robot_connection.py
10
11
  src/yrcom/yaskawa_client.py
@@ -16,6 +17,7 @@ src/yrcom.egg-info/requires.txt
16
17
  src/yrcom.egg-info/top_level.txt
17
18
  tests/test_hses_client.py
18
19
  tests/test_hses_file.py
20
+ tests/test_hses_server_codec.py
19
21
  tests/test_hses_sim.py
20
22
  tests/test_robot_connection.py
21
23
  tests/test_yaskawa_client.py
@@ -0,0 +1,9 @@
1
+
2
+ [dev]
3
+ pytest>=8
4
+
5
+ [docs]
6
+ mkdocs>=1.6
7
+ mkdocs-material>=9.5
8
+ mkdocstrings[python]>=0.26
9
+ black>=24
@@ -624,6 +624,30 @@ def test_poll_all_status_and_job():
624
624
  assert out["job_name"] == "MASTER"
625
625
 
626
626
 
627
+ def test_poll_all_reads_every_variable_kind_in_one_burst():
628
+ """D and R used to be reachable only as standalone reads, which put their
629
+ value a round trip away from the rest of the row."""
630
+ status_data = struct.pack("<II", 0x40, 0x40)
631
+ c, _ = _client([
632
+ _reply(hc._CMD_STATUS, status_data, req_id=1),
633
+ _reply(hc._CMD_B_VAR, struct.pack("<B", 7), req_id=2),
634
+ _reply(hc._CMD_I_VAR, struct.pack("<h", -3), req_id=3),
635
+ _reply(hc._CMD_D_VAR, struct.pack("<i", -70000), req_id=4),
636
+ _reply(hc._CMD_R_VAR, struct.pack("<f", 1.5), req_id=5),
637
+ ])
638
+ out = c.poll_all(
639
+ include_job=False,
640
+ b_vars=[{"label": "b", "no": 1}],
641
+ i_vars=[{"label": "i", "no": 2}],
642
+ d_vars=[{"label": "d", "no": 3}],
643
+ r_vars=[{"label": "r", "no": 4}],
644
+ )
645
+ assert out["var_b"] == 7
646
+ assert out["var_i"] == -3
647
+ assert out["var_d"] == -70000 # signed 32-bit, beyond an I variable
648
+ assert out["var_r"] == pytest.approx(1.5)
649
+
650
+
627
651
  def test_poll_all_raises_on_status_error():
628
652
  c, _ = _client([_reply(hc._CMD_STATUS, b"", req_id=1, status=0x08)])
629
653
  with pytest.raises(HsesError):
@@ -0,0 +1,74 @@
1
+ """Pure protocol-logic tests — no sockets, no MotoSim. Builds real request
2
+ bytes via HsesClient._build (the verified client-side packer) and checks the
3
+ new server-side decoder reads them back correctly."""
4
+ from __future__ import annotations
5
+
6
+ import pytest
7
+
8
+ from yrcom.hses_client import HsesClient
9
+ from yrcom.hses_server_codec import MalformedRequest, ServerCommand, decode_request
10
+
11
+
12
+ def test_decode_matches_a_real_status_request():
13
+ raw = HsesClient._build(cmd=0x72, instance=1, attribute=0, service=0x01, req_id=5)
14
+ sc = decode_request(raw)
15
+ assert sc == ServerCommand(cmd=0x72, instance=1, attribute=0, service=0x01,
16
+ req_id=5, payload=b"")
17
+
18
+
19
+ def test_decode_carries_payload():
20
+ raw = HsesClient._build(cmd=0x7A, instance=3, attribute=1, service=0x10,
21
+ req_id=200, payload=b"\x2a\x00\x00\x00")
22
+ sc = decode_request(raw)
23
+ assert sc.cmd == 0x7A
24
+ assert sc.instance == 3
25
+ assert sc.payload == b"\x2a\x00\x00\x00"
26
+
27
+
28
+ def test_decode_rejects_short_input():
29
+ with pytest.raises(MalformedRequest):
30
+ decode_request(b"too short")
31
+
32
+
33
+ def test_decode_rejects_bad_magic():
34
+ raw = bytearray(HsesClient._build(cmd=0x72, instance=1, attribute=0,
35
+ service=0x01, req_id=1))
36
+ raw[0:4] = b"XXXX"
37
+ with pytest.raises(MalformedRequest):
38
+ decode_request(bytes(raw))
39
+
40
+
41
+ def test_encode_response_round_trips_through_real_client_parser():
42
+ # HsesClient._parse_resp is the real, hardware-verified response parser —
43
+ # proving our encoder's bytes satisfy it is the strongest test we have
44
+ # without a socket.
45
+ from yrcom.hses_client import HsesClient
46
+ from yrcom.hses_server_codec import encode_response
47
+
48
+ raw = encode_response(cmd=0x72, req_id=9, data=b"\x01\x00\x00\x00\x00\x00\x00\x00")
49
+ resp = HsesClient._parse_resp(raw)
50
+ assert resp.cmd == 0x72
51
+ assert resp.status == 0x00
52
+ assert resp.added == 0
53
+ assert resp.data == b"\x01\x00\x00\x00\x00\x00\x00\x00"
54
+
55
+
56
+ def test_encode_error_round_trips_and_raises_hses_error():
57
+ from yrcom.hses_client import HsesClient, HsesError
58
+ from yrcom.hses_server_codec import encode_error
59
+
60
+ raw = encode_error(cmd=0x75, req_id=1, status=0x1F, added=0xB005)
61
+ resp = HsesClient._parse_resp(raw)
62
+ assert resp.status == 0x1F
63
+ assert resp.added == 0xB005
64
+ assert resp.data == b""
65
+
66
+
67
+ def test_encode_error_without_added_status():
68
+ from yrcom.hses_client import HsesClient
69
+ from yrcom.hses_server_codec import encode_error
70
+
71
+ raw = encode_error(cmd=0x00, req_id=1, status=0x08)
72
+ resp = HsesClient._parse_resp(raw)
73
+ assert resp.status == 0x08
74
+ assert resp.added == 0
@@ -1,3 +0,0 @@
1
-
2
- [dev]
3
- pytest>=8
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes