pytmrobot 0.1.0__py3-none-any.whl
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.
- pytmrobot/__init__.py +89 -0
- pytmrobot/_client.py +161 -0
- pytmrobot/_ft_base.py +115 -0
- pytmrobot/_protocol.py +171 -0
- pytmrobot/compliance.py +92 -0
- pytmrobot/decision.py +122 -0
- pytmrobot/exceptions.py +35 -0
- pytmrobot/force.py +151 -0
- pytmrobot/io.py +180 -0
- pytmrobot/modbus.py +274 -0
- pytmrobot/motion.py +297 -0
- pytmrobot/py.typed +0 -0
- pytmrobot/robot.py +287 -0
- pytmrobot/script_builder.py +790 -0
- pytmrobot/state.py +126 -0
- pytmrobot/tag_manager.py +40 -0
- pytmrobot/touchstop.py +119 -0
- pytmrobot/types.py +123 -0
- pytmrobot/vision.py +144 -0
- pytmrobot-0.1.0.dist-info/METADATA +229 -0
- pytmrobot-0.1.0.dist-info/RECORD +24 -0
- pytmrobot-0.1.0.dist-info/WHEEL +5 -0
- pytmrobot-0.1.0.dist-info/licenses/LICENSE +21 -0
- pytmrobot-0.1.0.dist-info/top_level.txt +1 -0
pytmrobot/__init__.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pytmrobot — Python TM Robot 控制函式庫
|
|
3
|
+
|
|
4
|
+
讓 Python 開發者無需了解 TMScript,直接以 Pythonic API 控制達明機器人。
|
|
5
|
+
|
|
6
|
+
Quick Start::
|
|
7
|
+
|
|
8
|
+
from pytmrobot import TMRobot, CartPoint
|
|
9
|
+
|
|
10
|
+
with TMRobot("192.168.1.101") as robot:
|
|
11
|
+
home = CartPoint(400, -100, 500, 180, 0, 90)
|
|
12
|
+
pick = CartPoint(400, -100, 280, 180, 0, 90)
|
|
13
|
+
|
|
14
|
+
robot.move.ptp(home, speed=30)
|
|
15
|
+
robot.move.ptp(pick, speed=10)
|
|
16
|
+
robot.io.set_do(2, True) # 夾爪閉合
|
|
17
|
+
robot.sleep(500)
|
|
18
|
+
robot.move.ptp(home, speed=30)
|
|
19
|
+
robot.io.set_do(2, False) # 夾爪張開
|
|
20
|
+
|
|
21
|
+
pos = robot.state.tcp_position
|
|
22
|
+
print(f"完成!位置: X={pos[0]:.2f}, Y={pos[1]:.2f}, Z={pos[2]:.2f}")
|
|
23
|
+
|
|
24
|
+
Public API
|
|
25
|
+
----------
|
|
26
|
+
主類別:
|
|
27
|
+
TMRobot 控制機器人的主要入口(支援 context manager)
|
|
28
|
+
|
|
29
|
+
點位型別:
|
|
30
|
+
CartPoint 笛卡爾座標目標點 (X, Y, Z, Rx, Ry, Rz)
|
|
31
|
+
JointPoint 關節角度目標點 (J1, J2, J3, J4, J5, J6)
|
|
32
|
+
|
|
33
|
+
列舉:
|
|
34
|
+
SpeedUnit 直線/圓弧速度單位 (PERCENT / ABS_SYNC / ABS_LOCK)
|
|
35
|
+
BlendUnit 軌跡混合單位 (PERCENT / RADIUS)
|
|
36
|
+
|
|
37
|
+
資料容器:
|
|
38
|
+
RobotState robot.state.snapshot() 的回傳型別
|
|
39
|
+
IOState robot.io.get_all_io() 的回傳型別
|
|
40
|
+
|
|
41
|
+
例外:
|
|
42
|
+
TMError 所有例外的基礎類別
|
|
43
|
+
TMConnectionError 連線失敗
|
|
44
|
+
TMScriptError 機器人回傳 ERROR
|
|
45
|
+
TMTimeoutError 等待逾時
|
|
46
|
+
TMValueError 參數超出範圍
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from .exceptions import (
|
|
50
|
+
TMConnectionError,
|
|
51
|
+
TMError,
|
|
52
|
+
TMScriptError,
|
|
53
|
+
TMTimeoutError,
|
|
54
|
+
TMValueError,
|
|
55
|
+
)
|
|
56
|
+
from .robot import TMRobot
|
|
57
|
+
from .types import (
|
|
58
|
+
BlendUnit,
|
|
59
|
+
CartPoint,
|
|
60
|
+
IOState,
|
|
61
|
+
JointPoint,
|
|
62
|
+
Pose6,
|
|
63
|
+
RobotState,
|
|
64
|
+
SpeedUnit,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
__version__ = "0.1.0"
|
|
68
|
+
__author__ = "TMDemo Contributors"
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
# 主類別
|
|
72
|
+
"TMRobot",
|
|
73
|
+
# 點位型別
|
|
74
|
+
"CartPoint",
|
|
75
|
+
"JointPoint",
|
|
76
|
+
"Pose6",
|
|
77
|
+
# 列舉
|
|
78
|
+
"SpeedUnit",
|
|
79
|
+
"BlendUnit",
|
|
80
|
+
# 資料容器
|
|
81
|
+
"RobotState",
|
|
82
|
+
"IOState",
|
|
83
|
+
# 例外
|
|
84
|
+
"TMError",
|
|
85
|
+
"TMConnectionError",
|
|
86
|
+
"TMScriptError",
|
|
87
|
+
"TMTimeoutError",
|
|
88
|
+
"TMValueError",
|
|
89
|
+
]
|
pytmrobot/_client.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pytmrobot._client — TMClientWrapper
|
|
3
|
+
|
|
4
|
+
薄包裝現有 TMClient,新增 send_and_recv_subcmd() 實現狀態讀取。
|
|
5
|
+
不修改 tm_listen_client.py 任何一行。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from ._protocol import TMClient, build_packet
|
|
13
|
+
from .exceptions import TMConnectionError, TMScriptError, TMTimeoutError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TMClientWrapper:
|
|
17
|
+
"""包裝 TMClient,對外提供更高階的收發介面。
|
|
18
|
+
|
|
19
|
+
主要新增 :meth:`send_and_recv_subcmd`,用於實作 ListenSend 狀態讀取。
|
|
20
|
+
其餘方法直接委派給底層 TMClient。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, ip: str, port: int = 5890, timeout: float = 60.0) -> None:
|
|
24
|
+
self._client = TMClient(ip=ip, port=port)
|
|
25
|
+
self._timeout = timeout
|
|
26
|
+
|
|
27
|
+
# ── 連線管理(委派) ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
def connect(self) -> None:
|
|
30
|
+
"""建立 TCP 連線並等待機器人進入 Listen Node。"""
|
|
31
|
+
try:
|
|
32
|
+
self._client.connect()
|
|
33
|
+
self._client.wait_for_listen_node()
|
|
34
|
+
except OSError as e:
|
|
35
|
+
raise TMConnectionError(str(e)) from e
|
|
36
|
+
|
|
37
|
+
def disconnect(self) -> None:
|
|
38
|
+
"""關閉 TCP 連線。"""
|
|
39
|
+
self._client.disconnect()
|
|
40
|
+
|
|
41
|
+
# ── 一般 script 發送(委派) ──────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
def send_script(self, script_id: str, lines: list[str]) -> bool:
|
|
44
|
+
"""發送 TMSCT,等待 OK/ERROR 回應(委派 TMClient.send_script)。
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True=OK, False=ERROR 或無回應
|
|
48
|
+
"""
|
|
49
|
+
return self._client.send_script(script_id, lines)
|
|
50
|
+
|
|
51
|
+
def send_script_checked(self, script_id: str, lines: list[str]) -> None:
|
|
52
|
+
"""發送 TMSCT,失敗時拋出 TMScriptError。"""
|
|
53
|
+
ok = self.send_script(script_id, lines)
|
|
54
|
+
if not ok:
|
|
55
|
+
raise TMScriptError(f"指令執行失敗:{lines}", script_id=script_id)
|
|
56
|
+
|
|
57
|
+
def wait_queue_tag(self, tag_id: int, poll_interval: float = 0.2) -> None:
|
|
58
|
+
"""等待 QueueTag 完成(委派 TMClient.wait_queue_tag)。"""
|
|
59
|
+
self._client.wait_queue_tag(tag_id, poll_interval)
|
|
60
|
+
|
|
61
|
+
def exit_listen(self, pass_exit: bool = True) -> None:
|
|
62
|
+
"""退出 Listen Node(委派 TMClient.exit_listen)。"""
|
|
63
|
+
self._client.exit_listen(pass_exit)
|
|
64
|
+
|
|
65
|
+
# ── 狀態讀取(ListenSend 往返) ───────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def send_and_recv_subcmd(
|
|
68
|
+
self,
|
|
69
|
+
script_id: str,
|
|
70
|
+
lines: list[str],
|
|
71
|
+
expected_subcmd: int = 90,
|
|
72
|
+
timeout: float = 5.0,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""發送含 ListenSend 的 script,等待 TMSCT OK 與 TMSTA SubCmd 回傳。
|
|
75
|
+
|
|
76
|
+
使用狀態機接收迴圈,不假設封包到達順序:
|
|
77
|
+
- TMSCT OK → ok_received = True
|
|
78
|
+
- TMSCT ERROR / CPERR → 拋出 TMScriptError
|
|
79
|
+
- TMSTA subcmd == expected_subcmd → data_received = csv_string
|
|
80
|
+
- TMSTA 其他 → 忽略(殘留 QueueTag 通知等)
|
|
81
|
+
- ok_received AND data_received → 回傳 csv_string
|
|
82
|
+
- 逾時 → 拋出 TMTimeoutError
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
script_id: TMSCT 的 Script ID
|
|
86
|
+
lines: TMScript 行列表(最後一行應含 ListenSend)
|
|
87
|
+
expected_subcmd: 期望的 TMSTA SubCmd(90-99)
|
|
88
|
+
timeout: 等待逾時秒數
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
TMSTA 回傳的資料字串(SubCmd 之後的部分)
|
|
92
|
+
"""
|
|
93
|
+
# 發送封包
|
|
94
|
+
pkt = build_packet("TMSCT", f"{script_id}," + "\r\n".join(lines))
|
|
95
|
+
self._client.send_raw(pkt)
|
|
96
|
+
|
|
97
|
+
ok_received = False
|
|
98
|
+
data_received: str | None = None
|
|
99
|
+
deadline = time.monotonic() + timeout
|
|
100
|
+
orig_timeout = self._client.sock.gettimeout() if self._client.sock else timeout
|
|
101
|
+
|
|
102
|
+
while time.monotonic() < deadline:
|
|
103
|
+
# 設短的 socket timeout 避免長時間阻塞
|
|
104
|
+
if self._client.sock:
|
|
105
|
+
self._client.sock.settimeout(min(0.5, deadline - time.monotonic()))
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
pkt_dict = self._client.recv_packet()
|
|
109
|
+
except TimeoutError:
|
|
110
|
+
continue
|
|
111
|
+
except Exception as e:
|
|
112
|
+
raise TMConnectionError(str(e)) from e
|
|
113
|
+
finally:
|
|
114
|
+
if self._client.sock:
|
|
115
|
+
try:
|
|
116
|
+
self._client.sock.settimeout(orig_timeout)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
if pkt_dict is None:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
hdr = pkt_dict["header"]
|
|
124
|
+
data = pkt_dict["data"]
|
|
125
|
+
|
|
126
|
+
if hdr == "TMSCT":
|
|
127
|
+
parts = data.split(",", 1)
|
|
128
|
+
status = parts[1] if len(parts) > 1 else ""
|
|
129
|
+
if status.startswith("OK"):
|
|
130
|
+
ok_received = True
|
|
131
|
+
else:
|
|
132
|
+
raise TMScriptError(status, script_id=script_id)
|
|
133
|
+
|
|
134
|
+
elif hdr == "TMSTA":
|
|
135
|
+
parts = data.split(",", 1)
|
|
136
|
+
subcmd_str = parts[0].strip()
|
|
137
|
+
try:
|
|
138
|
+
subcmd = int(subcmd_str)
|
|
139
|
+
except ValueError:
|
|
140
|
+
continue
|
|
141
|
+
if subcmd == expected_subcmd:
|
|
142
|
+
# data 欄位: "<subcmd>,<payload>"
|
|
143
|
+
data_received = parts[1] if len(parts) > 1 else ""
|
|
144
|
+
|
|
145
|
+
elif hdr == "CPERR":
|
|
146
|
+
raise TMScriptError(f"CPERR: {data}", script_id=script_id)
|
|
147
|
+
|
|
148
|
+
if ok_received and data_received is not None:
|
|
149
|
+
return data_received
|
|
150
|
+
|
|
151
|
+
raise TMTimeoutError(
|
|
152
|
+
f"等待 TMSTA SubCmd {expected_subcmd} 逾時({timeout}s),"
|
|
153
|
+
f"ok={ok_received}, data={data_received!r}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# ── 底層存取 ──────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def raw_client(self) -> TMClient:
|
|
160
|
+
"""取得底層 TMClient 實例(進階使用)。"""
|
|
161
|
+
return self._client
|
pytmrobot/_ft_base.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pytmrobot._ft_base — Compliance / TouchStop / Force 共用基底
|
|
3
|
+
|
|
4
|
+
三個控制器共享相同的「宣告 → 設定 → Start() → ListenSend」模式,
|
|
5
|
+
以及 Listen Node session 變數宣告追蹤邏輯,在此統一實作。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from .types import Pose6
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ._client import TMClientWrapper
|
|
16
|
+
from .tag_manager import TagManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _FTBase:
|
|
20
|
+
"""Compliance / TouchStop / Force 共用基底類別。
|
|
21
|
+
|
|
22
|
+
子類別需定義:
|
|
23
|
+
- ``_OBJ_NAME``: TMScript 物件變數名稱,如 ``"_ts"``
|
|
24
|
+
- ``_RESULT_VAR``: TMScript 整數結果變數名稱,如 ``"_ts_r"``
|
|
25
|
+
|
|
26
|
+
**Start() 傳回碼(int):**
|
|
27
|
+
|
|
28
|
+
+------+----------------------------------+
|
|
29
|
+
| 0 | Not Working |
|
|
30
|
+
| 1 | Working(Start() 仍在執行中) |
|
|
31
|
+
| 2 | Timeout |
|
|
32
|
+
| 3 | Distance reached(正常完成) |
|
|
33
|
+
| 4 | IO triggered |
|
|
34
|
+
| 5 | Resisted(偵測到外部阻力) |
|
|
35
|
+
| 6 | Error |
|
|
36
|
+
| 7 | Over Speed |
|
|
37
|
+
| 8 | Digital IO triggered |
|
|
38
|
+
| 9 | Analog IO triggered |
|
|
39
|
+
| 10 | Variable |
|
|
40
|
+
| 11 | Force is Satisfied |
|
|
41
|
+
| 12 | Allowable Position Tolerances |
|
|
42
|
+
| 13 | Motion Finish(軌跡結束正常完成) |
|
|
43
|
+
+------+----------------------------------+
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
_OBJ_NAME: str = ""
|
|
47
|
+
_RESULT_VAR: str = ""
|
|
48
|
+
|
|
49
|
+
def __init__(self, client: TMClientWrapper, tags: TagManager) -> None:
|
|
50
|
+
self._client = client
|
|
51
|
+
self._tags = tags
|
|
52
|
+
self._declared: dict[str, bool] = {}
|
|
53
|
+
|
|
54
|
+
def _reset_session(self) -> None:
|
|
55
|
+
"""重設所有 TMScript 變數宣告狀態(重新連線時呼叫)。"""
|
|
56
|
+
self._declared.clear()
|
|
57
|
+
|
|
58
|
+
# ── 內部工具 ──────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def _is_declared(self, var: str) -> bool:
|
|
61
|
+
return self._declared.get(var, False)
|
|
62
|
+
|
|
63
|
+
def _mark(self, var: str) -> None:
|
|
64
|
+
self._declared[var] = True
|
|
65
|
+
|
|
66
|
+
def _assign(self, tmtype: str, var: str, expr: str) -> str:
|
|
67
|
+
"""依宣告狀態回傳 'TYPE var = expr' 或 'var = expr'。"""
|
|
68
|
+
if not self._is_declared(var):
|
|
69
|
+
self._mark(var)
|
|
70
|
+
return f"{tmtype} {var} = {expr}"
|
|
71
|
+
return f"{var} = {expr}"
|
|
72
|
+
|
|
73
|
+
def _sid(self, suffix: str = "") -> str:
|
|
74
|
+
"""生成腳本 ID(使用物件名去掉底線為前綴)。"""
|
|
75
|
+
tag = self._tags.next()
|
|
76
|
+
prefix = self._OBJ_NAME.lstrip("_")
|
|
77
|
+
return f"{prefix}{suffix}{tag}"
|
|
78
|
+
|
|
79
|
+
# ── 核心執行 ──────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
def _run(
|
|
82
|
+
self,
|
|
83
|
+
setup_lines: list[str],
|
|
84
|
+
start_expr: str,
|
|
85
|
+
timeout_ms: int,
|
|
86
|
+
) -> int:
|
|
87
|
+
"""發送設定 + Start() 腳本,以 ListenSend SubCmd 90 接收結果碼。
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
setup_lines: 物件宣告/設定行(Reset, Frame, Single, Timeout…)
|
|
91
|
+
start_expr: Start() 呼叫字串,如 ``"_ts.Start()"``
|
|
92
|
+
timeout_ms: TMScript 逾時設定,用於計算 Python 端等待上限
|
|
93
|
+
"""
|
|
94
|
+
result_line = self._assign("int", self._RESULT_VAR, start_expr)
|
|
95
|
+
lines = setup_lines + [result_line,
|
|
96
|
+
f"ListenSend(90, GetString({self._RESULT_VAR}))"]
|
|
97
|
+
py_timeout = (timeout_ms / 1000.0 + 5.0) if timeout_ms > 0 else 3600.0
|
|
98
|
+
csv = self._client.send_and_recv_subcmd(self._sid(), lines, timeout=py_timeout)
|
|
99
|
+
return int(csv.strip())
|
|
100
|
+
|
|
101
|
+
def _stop(self) -> int:
|
|
102
|
+
"""發送 Stop() 腳本並以 ListenSend 接收結果碼。"""
|
|
103
|
+
result_line = self._assign("int", self._RESULT_VAR,
|
|
104
|
+
f"{self._OBJ_NAME}.Stop()")
|
|
105
|
+
lines = [result_line, f"ListenSend(90, GetString({self._RESULT_VAR}))"]
|
|
106
|
+
csv = self._client.send_and_recv_subcmd(self._sid("stop"), lines)
|
|
107
|
+
return int(csv.strip())
|
|
108
|
+
|
|
109
|
+
def _read_pos(self, method_call: str, var_name: str) -> Pose6:
|
|
110
|
+
"""發送 GetXxxPos(type) → ListenSend,解析 float[] 座標為 Pose6。"""
|
|
111
|
+
assign = self._assign("float[]", var_name, method_call)
|
|
112
|
+
lines = [assign, f"ListenSend(90, GetString({var_name}))"]
|
|
113
|
+
csv = self._client.send_and_recv_subcmd(self._sid("pos"), lines)
|
|
114
|
+
parts = [v.strip() for v in csv.strip().strip("{}").split(",") if v.strip()]
|
|
115
|
+
return tuple(float(p) for p in parts) # type: ignore[return-value]
|
pytmrobot/_protocol.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pytmrobot._protocol — TM Robot Listen Node 底層通訊協定。
|
|
3
|
+
|
|
4
|
+
包含封包編解碼與 TMClient 類別。
|
|
5
|
+
原始碼源自 tm_listen_client.py(同 repo),
|
|
6
|
+
抽出為獨立私有模組以支援 PyPI 打包安裝。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import socket
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
# ── 封包工具 ─────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def calc_checksum(raw: str) -> str:
|
|
19
|
+
"""計算封包 checksum:對 $ 與 * 之間的所有位元組做 XOR。
|
|
20
|
+
|
|
21
|
+
raw 為完整字串,含頭尾的 $ 與 *,但不含 \\r\\n。
|
|
22
|
+
例如 raw = "$TMSCT,5,10,OK,*"
|
|
23
|
+
|
|
24
|
+
使用 rindex 找最後一個 *(checksum 分隔符),
|
|
25
|
+
避免 TMScript 腳本內容含乘法符號 * 時計算出錯(CPERR 02)。
|
|
26
|
+
"""
|
|
27
|
+
inner = raw[1 : raw.rindex("*")]
|
|
28
|
+
cs = 0
|
|
29
|
+
for ch in inner.encode("utf-8"):
|
|
30
|
+
cs ^= ch
|
|
31
|
+
return format(cs, "02X")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_packet(header: str, data: str) -> bytes:
|
|
35
|
+
"""建立封包並計算 checksum。
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
header: ``'TMSCT'`` | ``'TMSTA'``
|
|
39
|
+
data: 資料欄位字串(不含 checksum)
|
|
40
|
+
"""
|
|
41
|
+
length = len(data.encode("utf-8"))
|
|
42
|
+
raw = f"${header},{length},{data},*"
|
|
43
|
+
cs = calc_checksum(raw)
|
|
44
|
+
packet = f"{raw}{cs}\r\n"
|
|
45
|
+
return packet.encode("utf-8")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_packet(line: str) -> dict | None:
|
|
49
|
+
"""解析一行機器人回覆,回傳 dict 或 None(格式錯誤時)。"""
|
|
50
|
+
line = line.strip()
|
|
51
|
+
if not line.startswith("$"):
|
|
52
|
+
return None
|
|
53
|
+
m = re.match(r"^\$([^,]+),(\d+),(.+),\*([0-9A-Fa-f]{2})$", line)
|
|
54
|
+
if not m:
|
|
55
|
+
return None
|
|
56
|
+
header, length, data, cs = m.groups()
|
|
57
|
+
return {"header": header, "length": int(length), "data": data, "cs": cs}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── TMClient ─────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
_DEFAULT_TIMEOUT = 60.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TMClient:
|
|
66
|
+
"""TM Robot Listen Node TCP 客戶端。
|
|
67
|
+
|
|
68
|
+
提供底層 send/recv 與常用操作(等待 Listen Node、送指令、等 QueueTag)。
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, ip: str, port: int = 5890, timeout: float = _DEFAULT_TIMEOUT):
|
|
72
|
+
self.ip = ip
|
|
73
|
+
self.port = port
|
|
74
|
+
self.sock: socket.socket | None = None
|
|
75
|
+
self._buf = ""
|
|
76
|
+
|
|
77
|
+
# ── 連線 / 斷線 ──────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
def connect(self) -> None:
|
|
80
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
81
|
+
self.sock.settimeout(_DEFAULT_TIMEOUT)
|
|
82
|
+
self.sock.connect((self.ip, self.port))
|
|
83
|
+
|
|
84
|
+
def disconnect(self) -> None:
|
|
85
|
+
if self.sock:
|
|
86
|
+
self.sock.close()
|
|
87
|
+
self.sock = None
|
|
88
|
+
|
|
89
|
+
# ── 底層收發 ─────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
def _recv_line(self) -> str:
|
|
92
|
+
"""從緩衝區讀取一行(以 \\r\\n 結尾)。"""
|
|
93
|
+
assert self.sock is not None
|
|
94
|
+
while "\r\n" not in self._buf:
|
|
95
|
+
chunk = self.sock.recv(4096).decode("utf-8", errors="replace")
|
|
96
|
+
if not chunk:
|
|
97
|
+
raise ConnectionError("連線中斷")
|
|
98
|
+
self._buf += chunk
|
|
99
|
+
line, self._buf = self._buf.split("\r\n", 1)
|
|
100
|
+
return line
|
|
101
|
+
|
|
102
|
+
def recv_packet(self) -> dict | None:
|
|
103
|
+
"""接收並解析一個封包。"""
|
|
104
|
+
line = self._recv_line()
|
|
105
|
+
return parse_packet(line)
|
|
106
|
+
|
|
107
|
+
def send_raw(self, packet: bytes) -> None:
|
|
108
|
+
assert self.sock is not None
|
|
109
|
+
self.sock.sendall(packet)
|
|
110
|
+
|
|
111
|
+
# ── 等待 Listen Node ─────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
def wait_for_listen_node(self) -> None:
|
|
114
|
+
"""確保機器人已進入 Listen Node。"""
|
|
115
|
+
if self.query_listen_mode():
|
|
116
|
+
return
|
|
117
|
+
while True:
|
|
118
|
+
pkt = self.recv_packet()
|
|
119
|
+
if pkt and pkt["header"] == "TMSCT":
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
def query_listen_mode(self) -> bool:
|
|
123
|
+
"""SubCmd 00:查詢是否已進入外部 Script 控制模式。"""
|
|
124
|
+
pkt = build_packet("TMSTA", "00")
|
|
125
|
+
self.send_raw(pkt)
|
|
126
|
+
resp = self.recv_packet()
|
|
127
|
+
if resp and resp["header"] == "TMSTA":
|
|
128
|
+
return resp["data"].split(",")[1].lower() == "true"
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# ── 發送 TMScript 指令 ────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
def send_script(self, script_id: str, script_lines: list[str]) -> bool:
|
|
134
|
+
"""發送一或多行 TMScript,等待 OK/ERROR 回應。"""
|
|
135
|
+
data = f"{script_id}," + "\r\n".join(script_lines)
|
|
136
|
+
pkt = build_packet("TMSCT", data)
|
|
137
|
+
self.send_raw(pkt)
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
resp = self.recv_packet()
|
|
141
|
+
if not resp:
|
|
142
|
+
return False
|
|
143
|
+
if resp["header"] == "TMSTA":
|
|
144
|
+
continue
|
|
145
|
+
if resp["header"] == "TMSCT":
|
|
146
|
+
parts = resp["data"].split(",", 1)
|
|
147
|
+
status = parts[1] if len(parts) > 1 else ""
|
|
148
|
+
return status.startswith("OK")
|
|
149
|
+
if resp["header"] == "CPERR":
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
# ── 等待 QueueTag ─────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
def wait_queue_tag(self, tag_id: int, poll_interval: float = 0.2) -> None:
|
|
155
|
+
"""等待機器人完成指定的 QueueTag(輪詢)。"""
|
|
156
|
+
while True:
|
|
157
|
+
pkt = build_packet("TMSTA", f"01,{tag_id}")
|
|
158
|
+
self.send_raw(pkt)
|
|
159
|
+
resp = self.recv_packet()
|
|
160
|
+
if resp and resp["header"] == "TMSTA":
|
|
161
|
+
parts = resp["data"].split(",")
|
|
162
|
+
if len(parts) >= 3 and parts[2].lower() == "true":
|
|
163
|
+
return
|
|
164
|
+
time.sleep(poll_interval)
|
|
165
|
+
|
|
166
|
+
# ── 退出 Listen Node ──────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
def exit_listen(self, pass_exit: bool = True) -> None:
|
|
169
|
+
"""發送 ScriptExit() 退出 Listen Node。"""
|
|
170
|
+
cmd = "ScriptExit(1)" if pass_exit else "ScriptExit(0)"
|
|
171
|
+
self.send_script("exit", [cmd])
|
pytmrobot/compliance.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pytmrobot.compliance — ComplianceController
|
|
3
|
+
|
|
4
|
+
透過 robot.compliance 存取。使用 TM Robot 的 Compliance 功能,
|
|
5
|
+
讓機器人以順從控制方式沿指定軸移動,遇到阻力時停止。
|
|
6
|
+
|
|
7
|
+
TMScript 對應:Chapter 20 Compliance
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from . import script_builder as sb
|
|
15
|
+
from ._ft_base import _FTBase
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ComplianceController(_FTBase):
|
|
22
|
+
"""Compliance 順從控制器。透過 ``robot.compliance`` 存取。
|
|
23
|
+
|
|
24
|
+
典型流程::
|
|
25
|
+
|
|
26
|
+
# 在 Z- 方向最多移動 30mm,目標力 3N,速度 5%
|
|
27
|
+
result = robot.compliance.run(
|
|
28
|
+
direction=32, # 32 = Z-
|
|
29
|
+
distance=30.0,
|
|
30
|
+
force=3.0,
|
|
31
|
+
speed=5,
|
|
32
|
+
)
|
|
33
|
+
if result == 5: # Resisted(偵測到外部阻力)
|
|
34
|
+
print("已接觸工件")
|
|
35
|
+
elif result == 3: # Distance reached
|
|
36
|
+
print("移動完成但未偵測到阻力")
|
|
37
|
+
|
|
38
|
+
Start() 結果碼(result 值)請參考 _FTBase docstring。
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_OBJ_NAME = "_cp"
|
|
42
|
+
_RESULT_VAR = "_cp_r"
|
|
43
|
+
|
|
44
|
+
def run(
|
|
45
|
+
self,
|
|
46
|
+
direction: int,
|
|
47
|
+
distance: float,
|
|
48
|
+
force: float,
|
|
49
|
+
speed: int,
|
|
50
|
+
*,
|
|
51
|
+
frame: int = 1,
|
|
52
|
+
timeout_ms: int = -1,
|
|
53
|
+
high_resistance: bool = False,
|
|
54
|
+
) -> int:
|
|
55
|
+
"""執行 Compliance 順從控制運動。
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
direction: 偵測軸方向代碼(TMScript ch20 Single() 參數,
|
|
59
|
+
如 1=X+, 2=X-, 4=Y+, 8=Y-, 16=Z+, 32=Z-)
|
|
60
|
+
distance: 最大順從移動距離(mm)
|
|
61
|
+
force: 目標/阻力閾值(N)
|
|
62
|
+
speed: 運動速度(%)
|
|
63
|
+
frame: 座標系(1=機器人基座, 2=工具, 3=使用者)
|
|
64
|
+
timeout_ms: 逾時毫秒(-1=不逾時)
|
|
65
|
+
high_resistance: True=啟用高阻力偵測模式
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Start() 結果碼(見 _FTBase docstring)
|
|
69
|
+
|
|
70
|
+
Examples::
|
|
71
|
+
|
|
72
|
+
result = robot.compliance.run(32, 30.0, 3.0, 5)
|
|
73
|
+
if result in (3, 5):
|
|
74
|
+
print("Compliance 完成")
|
|
75
|
+
"""
|
|
76
|
+
obj_declared = self._is_declared(self._OBJ_NAME)
|
|
77
|
+
setup = sb.build_compliance_setup(
|
|
78
|
+
direction, distance, force, speed,
|
|
79
|
+
frame=frame, timeout_ms=timeout_ms,
|
|
80
|
+
high_resistance=high_resistance,
|
|
81
|
+
obj_declared=obj_declared,
|
|
82
|
+
)
|
|
83
|
+
self._mark(self._OBJ_NAME)
|
|
84
|
+
return self._run(setup, f"{self._OBJ_NAME}.Start()", timeout_ms)
|
|
85
|
+
|
|
86
|
+
def stop(self) -> int:
|
|
87
|
+
"""中止 Compliance 運動。
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Stop() 結果碼
|
|
91
|
+
"""
|
|
92
|
+
return self._stop()
|