#!/usr/bin/env python
"""
K.NAKADA, kengo.nakada@gmail.com
"""
from collections import deque
from dataclasses import dataclass
import socket
import struct
import threading
import time
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
[docs]
@dataclass
class UrError:
_ROBOT_MODE_NAMES = {
-1: "NO_CONTROLLER",
0: "DISCONNECTED",
1: "CONFIRM_SAFETY",
2: "BOOTING",
3: "POWER_OFF",
4: "POWER_ON",
5: "IDLE",
6: "BACKDRIVE",
7: "RUNNING",
8: "UPDATING_FIRMWARE",
}
_ROBOT_MODE_DESCRIPTIONS = {
-1: "RTDE からコントローラ状態を取得できない。ネットワーク、RTDE サービス、または接続タイミングを確認する",
0: "RTDE から見てロボットコントローラが切断状態。IP 到達性と UR 側サービス状態を確認する",
1: "TP 側で安全確認待ち。外部制御前に TP 上の安全確認操作が必要な可能性がある",
2: "ロボット制御系が起動中。POWER_OFF/POWER_ON/IDLE/RUNNING への状態遷移を待つ",
3: "コントローラと TP は起動しているが、ロボット駆動系が Power Off。TP/コントローラ電源 OFF ではない。Dashboard powerOn() の対象",
4: "ロボット駆動系は Power On。ブレーキ解除が未完了なら Dashboard brakeRelease() の対象",
5: "アイドル状態。ロボット駆動系は起動済み。RTDE control script を開始できる候補状態",
6: "Backdrive 状態。手動誘導状態のため通常の外部移動指令は不可",
7: "実行中。UR プログラムまたは RTDE control script が動作中",
8: "ファームウェア更新中。外部制御不可",
}
_SAFETY_MODE_NAMES = {
1: "SAFETY_MODE_NORMAL",
2: "SAFETY_MODE_REDUCED",
3: "SAFETY_MODE_PROTECTIVE_STOP",
4: "SAFETY_MODE_RECOVERY",
5: "SAFETY_MODE_SAFEGUARD_STOP",
6: "SAFETY_MODE_SYSTEM_EMERGENCY_STOP",
7: "SAFETY_MODE_ROBOT_EMERGENCY_STOP",
8: "SAFETY_MODE_VIOLATION",
9: "SAFETY_MODE_FAULT",
10: "SAFETY_MODE_VALIDATE_JOINT_ID",
11: "SAFETY_MODE_UNDEFINED_SAFETY_MODE",
}
_SAFETY_MODE_DESCRIPTIONS = {
1: "通常安全状態。安全停止・Safety Fault は検出されていない",
2: "Reduced mode。安全設定により速度・力などが制限されている",
3: "Protective Stop。原因確認後、closeSafetyPopup() と unlockProtectiveStop() の対象",
4: "Recovery Mode。安全復帰用の低速/手動復帰状態",
5: "Safeguard Stop。安全扉、ライトカーテン、外部安全入力などが停止要求中",
6: "System Emergency Stop。システム側非常停止が有効",
7: "Robot Emergency Stop。ロボット側非常停止が有効",
8: "Safety Violation。安全制限違反。原因除去後 restartSafety() の対象",
9: "Safety Fault。安全系異常。原因除去後 restartSafety()、続いて powerOn()/brakeRelease() の対象",
10: "Validate Joint ID。関節 ID 確認状態。通常の外部制御状態ではない",
11: "未定義の安全モード。状態値の解釈不可",
}
_SAFETY_STATUS_BIT_NAMES = {
0: "normal_mode",
1: "reduced_mode",
2: "protective_stopped",
3: "recovery_mode",
4: "safeguard_stopped",
5: "system_emergency_stopped",
6: "robot_emergency_stopped",
7: "emergency_stopped",
8: "violation",
9: "fault",
10: "stopped_due_to_safety",
}
_SAFETY_STATUS_BIT_DESCRIPTIONS = {
"normal_mode": "通常安全状態",
"reduced_mode": "縮退安全モード",
"protective_stopped": "Protective Stop 発生中",
"recovery_mode": "Recovery Mode 発生中",
"safeguard_stopped": "Safeguard Stop 発生中",
"system_emergency_stopped": "システム側非常停止発生中",
"robot_emergency_stopped": "ロボット側非常停止発生中",
"emergency_stopped": "非常停止発生中",
"violation": "Safety Violation 発生中",
"fault": "Safety Fault 発生中",
"stopped_due_to_safety": "安全機能により停止中",
}
ROBOT_STATUS_BIT_DESCRIPTIONS = {
"power_on": "ロボット駆動系(サーボ電源)が ON。TP/コントローラ電源の意味ではない",
"program_running": "UR プログラムまたは RTDE 制御スクリプトが実行中",
"teach_button_pressed": "TP の teach button が押下中",
"power_button_pressed": "TP/操作画面上の power button が押下中",
}
_SAFETY_ERROR_PRIORITIES = (
"system_emergency_stopped",
"robot_emergency_stopped",
"emergency_stopped",
"fault",
"violation",
"protective_stopped",
"safeguard_stopped",
"recovery_mode",
"stopped_due_to_safety",
)
_RUNTIME_EXCEPTION_DESCRIPTIONS = {
"inv_kin_convergence": {
"error_type": "TARGET_UNREACHABLE",
"description_ja": (
"ロボットは指定された目標姿勢に到達できません。"
),
"description_en": (
"The robot cannot reach the specified target pose."
),
},
}
[docs]
@classmethod
def decode_runtime_exception(
cls,
raw_message: str,
) -> Dict[str, Any]:
"""Primary Interface の runtime exception を解釈する。"""
raw_text = str(raw_message or "").strip()
parts = raw_text.split(":")
error_id = ""
routine = ""
if parts:
error_id = parts[0].strip()
if len(parts) >= 2:
routine = parts[1].strip()
definition = cls._RUNTIME_EXCEPTION_DESCRIPTIONS.get(
error_id
)
if definition is None:
return {
"raw_message": raw_text,
"error_id": error_id or None,
"routine": routine or None,
"error_type": "UNKNOWN_RUNTIME_EXCEPTION",
"description_ja": (
"URコントローラで未登録の実行時エラーが"
"発生しました。"
),
"description_en": (
"An unregistered runtime exception occurred "
"in the UR controller."
),
"is_known": False,
}
decoded = dict(definition)
decoded["raw_message"] = raw_text
decoded["error_id"] = error_id
decoded["routine"] = routine or None
decoded["is_known"] = True
return decoded
[docs]
@classmethod
def decode_safety_state(
cls,
state: Dict[str, Any],
) -> Optional[Dict[str, Any]]:
"""RTDE safety state を具体的なエラー分類へ変換する。"""
flags = state.get("safety_status_flags") or {}
definitions = (
(
"system_emergency_stopped",
"SYSTEM_EMERGENCY_STOP",
"システム非常停止が作動しています。",
"The system emergency stop is active.",
),
(
"robot_emergency_stopped",
"ROBOT_EMERGENCY_STOP",
"ロボット非常停止が作動しています。",
"The robot emergency stop is active.",
),
(
"emergency_stopped",
"EMERGENCY_STOP",
"非常停止が作動しています。",
"An emergency stop is active.",
),
(
"fault",
"SAFETY_FAULT",
"安全システムで異常が発生しています。",
"A safety-system fault is active.",
),
(
"violation",
"SAFETY_VIOLATION",
"安全制限違反が発生しています。",
"A safety-limit violation is active.",
),
(
"protective_stopped",
"PROTECTIVE_STOP",
(
"ロボットが保護停止しました。障害物との接触、"
"過大な外力、または動作監視条件の超過を確認してください。"
),
(
"The robot entered a protective stop. Check for contact "
"with an obstacle, excessive external force, or a motion-"
"monitoring limit violation."
),
),
(
"safeguard_stopped",
"SAFEGUARD_STOP",
"セーフガード停止入力が作動しています。",
"A safeguard stop input is active.",
),
(
"recovery_mode",
"RECOVERY_MODE",
"ロボットは安全回復モードです。",
"The robot is in safety recovery mode.",
),
(
"stopped_due_to_safety",
"SAFETY_STOP",
"安全機能によりロボットが停止しました。",
"The robot was stopped by a safety function.",
),
)
for flag_name, error_type, description_ja, description_en in definitions:
if flags.get(flag_name, False):
return {
"error_type": error_type,
"safety_flag": flag_name,
"description_ja": description_ja,
"description_en": description_en,
}
return None
[docs]
class UrPrimaryErrorReceiver:
"""UR Primary Interface からロボットメッセージを受信する。"""
MESSAGE_TYPE_ROBOT_MESSAGE = 20
ROBOT_MESSAGE_TYPE_TEXT = 0
ROBOT_MESSAGE_TYPE_ERROR_CODE = 6
ROBOT_MESSAGE_TYPE_RUNTIME_EXCEPTION = 10
def __init__(
self,
robot_ip: str,
logger: Any,
port: int = 30001,
) -> None:
self._robot_ip = robot_ip
self._port = int(port)
self._logger = logger
self._socket = None
self._thread = None
self._stop_event = threading.Event()
self._messages = deque(maxlen=256)
self._lock = threading.Lock()
self._sequence = 0
[docs]
def connect(self) -> bool:
if self.is_connected():
return True
try:
sock = socket.create_connection(
(self._robot_ip, self._port),
timeout=2.0,
)
sock.settimeout(0.5)
self._socket = sock
self._stop_event.clear()
self._thread = threading.Thread(
target=self._receive_loop,
name="UrPrimaryErrorReceiver",
daemon=True,
)
self._thread.start()
return True
except Exception as exc:
self._logger.error(
"Primary Interface connection failed / "
"Primary Interface 接続失敗: %s",
exc,
)
self._socket = None
return False
[docs]
def disconnect(self) -> bool:
self._stop_event.set()
sock = self._socket
self._socket = None
if sock is not None:
try:
sock.shutdown(socket.SHUT_RDWR)
except Exception:
pass
try:
sock.close()
except Exception:
pass
thread = self._thread
self._thread = None
if thread is not None:
thread.join(timeout=1.0)
return True
[docs]
def is_connected(self) -> bool:
return self._socket is not None
[docs]
def mark(self) -> int:
with self._lock:
return self._sequence
[docs]
def get_messages_after(
self,
sequence: int,
timeout: float = 0.5,
) -> List[Dict[str, Any]]:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
with self._lock:
result = [
dict(message)
for message in self._messages
if message["sequence"] > sequence
]
if result:
return result
time.sleep(0.01)
with self._lock:
return [
dict(message)
for message in self._messages
if message["sequence"] > sequence
]
[docs]
def _receive_exact(self, size: int) -> bytes:
data = bytearray()
while len(data) < size:
sock = self._socket
if sock is None:
raise ConnectionError(
"Primary Interface socket is not connected"
)
chunk = sock.recv(size - len(data))
if not chunk:
raise ConnectionError(
"Primary Interface connection was closed"
)
data.extend(chunk)
return bytes(data)
[docs]
def _receive_loop(self) -> None:
while not self._stop_event.is_set():
try:
header = self._receive_exact(4)
message_size = struct.unpack(">I", header)[0]
if message_size < 5:
raise ValueError(
f"Invalid Primary message size: {message_size}"
)
body = self._receive_exact(message_size - 4)
self._parse_message(header + body)
except socket.timeout:
continue
except Exception as exc:
if not self._stop_event.is_set():
self._logger.warning(
"Primary Interface receive stopped / "
"Primary Interface 受信停止: %s",
exc,
)
break
[docs]
def _parse_message(self, packet: bytes) -> None:
if len(packet) < 5:
return
message_type = packet[4]
if message_type != self.MESSAGE_TYPE_ROBOT_MESSAGE:
return
if len(packet) < 15:
return
timestamp, source, robot_message_type = struct.unpack(
">QbB",
packet[5:15],
)
message = None
if robot_message_type == self.ROBOT_MESSAGE_TYPE_RUNTIME_EXCEPTION:
if len(packet) < 23:
return
script_line, script_column = struct.unpack(
">II",
packet[15:23],
)
text = packet[23:].decode(
"utf-8",
errors="replace",
).rstrip("\x00")
message = {
"layer": "UR Primary Interface",
"message_type": "RUNTIME_EXCEPTION",
"message_type_id": robot_message_type,
"timestamp": timestamp,
"source": source,
"script_line": script_line,
"script_column": script_column,
"message": text,
}
elif robot_message_type == self.ROBOT_MESSAGE_TYPE_ERROR_CODE:
if len(packet) < 25:
return
message_code, message_argument = struct.unpack(
">II",
packet[15:23],
)
report_level = packet[23]
data_type = packet[24]
data = packet[25:].decode(
"utf-8",
errors="replace",
).rstrip("\x00")
message = {
"layer": "UR Primary Interface",
"message_type": "ERROR_CODE",
"message_type_id": robot_message_type,
"timestamp": timestamp,
"source": source,
"message_code": message_code,
"message_argument": message_argument,
"report_level": report_level,
"data_type": data_type,
"message": data,
}
elif robot_message_type == self.ROBOT_MESSAGE_TYPE_TEXT:
text = packet[15:].decode(
"utf-8",
errors="replace",
).rstrip("\x00")
message = {
"layer": "UR Primary Interface",
"message_type": "TEXT",
"message_type_id": robot_message_type,
"timestamp": timestamp,
"source": source,
"message": text,
}
if message is None:
return
with self._lock:
self._sequence += 1
message["sequence"] = self._sequence
self._messages.append(message)