#!/usr/bin/env python3 """ mecharm_daemon.py Pi 側で常駐する MyCobot 制御デーモン。 TCP socket で Mac からの JSON コマンドを受け取り、永続化された MyCobot 経由で実機に転送する。 """ import json import socket import sys import time import signal import traceback from pymycobot.mycobot import MyCobot DAEMON_HOST = "0.0.0.0" DAEMON_PORT = 5555 SERIAL_PORT = "/dev/ttyAMA0" BAUDRATE = 1000000 GET_ANGLES_RETRIES = 12 GET_ANGLES_INTERVAL_SEC = 0.02 # 2026-11-05: 0.2 → 0.02 (動いている間は空で返ることが多く, 1 回の読み取りに最長 2.6 秒かかっていた) _mc = None _server_sock = None _running = True _fresh_mode_state = 0 def log(msg): print(f"[DAEMON {time.strftime('%H:%M:%S')}] {msg}", flush=True) def init_mycobot(): global _mc log(f"MyCobot 初期化中 ({SERIAL_PORT}, baud={BAUDRATE})...") _mc = MyCobot(SERIAL_PORT, BAUDRATE) log("✓ MyCobot 初期化完了") warmup_angles = read_angles_with_retry("startup_warmup", log_fail=False) log(f"startup_warmup angles={warmup_angles}") def read_angles_with_retry(label="get_angles", log_fail=True): """pymycobot の空配列対策つき角度取得。""" angles = None for attempt in range(GET_ANGLES_RETRIES): angles = _mc.get_angles() if isinstance(angles, (list, tuple)) and len(angles) == 6: if attempt > 0: log(f"{label}: get_angles success retry={attempt} angles={angles}") return angles time.sleep(GET_ANGLES_INTERVAL_SEC) if log_fail: log(f"{label}: get_angles empty/invalid after {GET_ANGLES_RETRIES} retries: {angles}") return None def read_angles_once_for_log(): """送ったあとの記録用に 1 回だけ読む (2026-11-05). 読めなくても待たない. 以前は読み直しつきで, 読めないと送る命令の返事が最長 2.6 秒遅れていた.""" try: a = _mc.get_angles() except Exception: return None return a if isinstance(a, (list, tuple)) and len(a) == 6 else None def handle_command(cmd_obj): """コマンドを処理してレスポンス dict を返す。""" global _fresh_mode_state cmd = cmd_obj.get("cmd") if cmd == "ping": return {"ok": True, "daemon": "mecharm_daemon", "fresh_mode": _fresh_mode_state} elif cmd == "send_angles": try: angles = cmd_obj["angles"] speed = int(cmd_obj.get("speed", 20)) wait = float(cmd_obj.get("wait", 0)) if len(angles) != 6: return {"ok": False, "error": f"angles must be length 6, got {len(angles)}"} log(f"send_angles 呼び出し前: angles={angles}, speed={speed}, wait={wait}") t0 = time.perf_counter() result = _mc.send_angles(angles, speed) elapsed_ms = (time.perf_counter() - t0) * 1000.0 log(f"send_angles 呼び出し後: result={result}, elapsed_ms={elapsed_ms:.1f}") if wait > 0: time.sleep(wait) current_angles = read_angles_once_for_log() log(f"send_angles 後の現在角度: {current_angles}") return {"ok": True, "result": result, "elapsed_ms": elapsed_ms} except Exception as e: log(f"send_angles 例外: {type(e).__name__}: {e}") log(traceback.format_exc().rstrip()) return {"ok": False, "error": f"{type(e).__name__}: {e}"} elif cmd == "send_angle": try: joint_id = int(cmd_obj["joint_id"]) angle = float(cmd_obj["angle"]) speed = int(cmd_obj.get("speed", 20)) wait = float(cmd_obj.get("wait", 0)) if not (1 <= joint_id <= 6): return {"ok": False, "error": f"joint_id must be 1..6, got {joint_id}"} log(f"send_angle 呼び出し前: joint_id={joint_id}, angle={angle}, speed={speed}, wait={wait}") t0 = time.perf_counter() result = _mc.send_angle(joint_id, angle, speed) elapsed_ms = (time.perf_counter() - t0) * 1000.0 log(f"send_angle 呼び出し後: result={result}, elapsed_ms={elapsed_ms:.1f}") if wait > 0: time.sleep(wait) current_angles = read_angles_once_for_log() log(f"send_angle 後の現在角度: {current_angles}") return {"ok": True, "result": result, "elapsed_ms": elapsed_ms} except Exception as e: log(f"send_angle 例外: {type(e).__name__}: {e}") log(traceback.format_exc().rstrip()) return {"ok": False, "error": f"{type(e).__name__}: {e}"} elif cmd == "get_angles": # DAY190: mc.get_angles() は起動直後や通信直後に [] / None を返すことがある。 a = read_angles_with_retry("get_angles") if a is None: return { "ok": False, "error": f"get_angles returned empty/invalid after {GET_ANGLES_RETRIES} retries", } return {"ok": True, "angles": a} elif cmd == "set_fresh_mode": value = int(cmd_obj["value"]) if value not in (0, 1): return {"ok": False, "error": f"value must be 0 or 1, got {value}"} try: _mc.set_fresh_mode(value) except AttributeError: return {"ok": False, "error": "set_fresh_mode is not available in this pymycobot environment"} _fresh_mode_state = value log(f"set_fresh_mode({value})") return {"ok": True, "fresh_mode": _fresh_mode_state} elif cmd == "set_gripper": state = int(cmd_obj["state"]) speed = int(cmd_obj.get("speed", 100)) wait = float(cmd_obj.get("wait", 0)) # DAY175 対策: 連続送信時の動作不具合回避のため事前 sleep time.sleep(2) result = _mc.set_gripper_state(state, speed) if wait > 0: time.sleep(wait) return {"ok": True, "result": result} # DAY261: 値ベース把持 cmd (純追記). # state ベースの set_gripper とは独立。閉じ切らない目標値で握って押し込み力を # 残す検証用。値域ガード必須 (value 0-100 / speed 1-100)。範囲外は # ハードに送らず error を返す。応答フォーマットは set_gripper と同形 {ok, result}。 elif cmd == "set_gripper_value": try: value = int(cmd_obj["value"]) speed = int(cmd_obj.get("speed", 100)) except (KeyError, TypeError, ValueError) as e: return {"ok": False, "error": f"bad args: {type(e).__name__}: {e}"} if not (0 <= value <= 100): return {"ok": False, "error": f"value must be 0..100, got {value}"} if not (1 <= speed <= 100): return {"ok": False, "error": f"speed must be 1..100, got {speed}"} try: result = _mc.set_gripper_value(value, speed) except AttributeError: return {"ok": False, "error": "set_gripper_value is not available in this pymycobot environment"} except Exception as e: return {"ok": False, "error": f"{type(e).__name__}: {e}"} log(f"set_gripper_value(value={value}, speed={speed}) result={result}") return {"ok": True, "result": result} # DAY259 グリッパー保持診断 (A-2)。読み取り専用の getter のみ追加。 # pymycobot の純 getter (シリアル read のみ・モーター駆動しない) を想定。 # 例外はクライアントへ転送するだけで、daemon は落とさない。 elif cmd == "get_gripper_value": try: val = _mc.get_gripper_value() return {"ok": True, "value": int(val) if val is not None else None} except Exception as e: return {"ok": False, "error": f"{type(e).__name__}: {e}"} elif cmd == "is_gripper_moving": try: flag = _mc.is_gripper_moving() return {"ok": True, "moving": bool(flag) if flag is not None else None} except Exception as e: return {"ok": False, "error": f"{type(e).__name__}: {e}"} # ---- LOG352 (2026-10-19): ストリーミング再生用の 2 コマンド (純追記) -------- # 既存 send_angles / get_angles は 1 文字も変えない (本線 run_pick_place.py が依存)。 # # 背景: 既存 send_angles は送信後に read_angles_with_retry() を呼ぶ。 # 動作中の _mc.get_angles() は空リストを返しやすく、12 回 x (読み ~0.11s + # sleep 0.2s) = 約 3.7 秒ブロックする。0.1 秒周期で送り続ける補間ストリーミング # 再生 (record_teach_demo.py) では、474 送信中 27 回がこの 3.7 秒を踏み、 # tick 周期が平均 0.46-0.55s まで伸びた (公称 0.1s / 実測 2026-10-19)。 # _mc.send_angles() 自体は平均 10.4ms なので、遅いのは読み直しだけ。 # Pi 直の waypoint_streaming_replay.py は送信後に角度を読まない。それと同じ # 送り方を daemon 経由でも使えるようにする。 elif cmd == "send_angles_fast": # 送信後に角度を読み直さない send_angles。 # 応答は _mc.send_angles() の実行時間だけ (実測 ~10ms)。 # 到達確認は呼び出し側が区間の終わりで既存 get_angles を使って行う。 try: angles = cmd_obj["angles"] speed = int(cmd_obj.get("speed", 20)) if not isinstance(angles, (list, tuple)) or len(angles) != 6: return {"ok": False, "error": f"angles must be length 6, got {len(angles) if isinstance(angles, (list, tuple)) else type(angles).__name__}"} t0 = time.perf_counter() result = _mc.send_angles(list(angles), speed) elapsed_ms = (time.perf_counter() - t0) * 1000.0 # 1 送信 1 行だけログに残す (今回の原因特定は daemon ログで出来た)。 log(f"send_angles_fast: angles={angles}, speed={speed}, " f"result={result}, elapsed_ms={elapsed_ms:.1f}") return {"ok": True, "result": result, "elapsed_ms": elapsed_ms} except Exception as e: log(f"send_angles_fast 例外: {type(e).__name__}: {e}") log(traceback.format_exc().rstrip()) return {"ok": False, "error": f"{type(e).__name__}: {e}"} elif cmd == "get_angles_once": # 1 回だけ読んで、読めなければ angles=None を返す (リトライしない)。 # 記録用の観測に使う。読めないことは異常ではないので ok=True で返す # (ok=False にすると呼び出し側の _daemon_request が例外にしてしまう)。 # 既存 get_angles (12 回リトライ) は到達確認用にそのまま残す。 try: a = _mc.get_angles() except Exception as e: return {"ok": True, "angles": None, "error": f"{type(e).__name__}: {e}"} if isinstance(a, (list, tuple)) and len(a) == 6: return {"ok": True, "angles": list(a)} return {"ok": True, "angles": None, "raw": repr(a)[:80]} # 2026-11-05: 動いている途中で止める (ゲームパッドの長押しを離したとき・L2+R2). # jog_probe (diag/jog_probe_20261105_*) で jog_stop と stop はどちらも効かない回があったので両方呼ぶ. elif cmd == "stop": res = {} for name in ("jog_stop", "stop"): try: res[name] = getattr(_mc, name)() except Exception as e: res[name] = f"{type(e).__name__}: {e}" log(f"stop: {res}") if all(isinstance(v, str) for v in res.values()): return {"ok": False, "error": f"stop failed: {res}"} return {"ok": True, "result": res} else: return {"ok": False, "error": f"unknown cmd: {cmd}"} def serve_client(client_sock, addr): log(f"接続: {addr}") buf = b"" try: while _running: data = client_sock.recv(4096) if not data: break buf += data while b"\n" in buf: line, buf = buf.split(b"\n", 1) line = line.strip() if not line: continue try: cmd_obj = json.loads(line.decode("utf-8")) log(f"cmd received: {cmd_obj.get('cmd')}") response = handle_command(cmd_obj) except Exception as e: response = {"ok": False, "error": f"{type(e).__name__}: {e}"} log(f"コマンド処理エラー: {e}") resp_line = (json.dumps(response) + "\n").encode("utf-8") client_sock.sendall(resp_line) except ConnectionResetError: log(f"接続リセット: {addr}") except Exception as e: log(f"クライアントエラー: {e}") finally: client_sock.close() log(f"切断: {addr}") def shutdown_handler(signum, frame): global _running log(f"シグナル受信 ({signum}), シャットダウン開始") _running = False if _mc is not None: try: _mc.set_fresh_mode(0) log("set_fresh_mode(0)") except Exception: pass if _server_sock: try: _server_sock.close() except Exception: pass sys.exit(0) def main(): global _server_sock signal.signal(signal.SIGTERM, shutdown_handler) signal.signal(signal.SIGINT, shutdown_handler) init_mycobot() _server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) _server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) _server_sock.bind((DAEMON_HOST, DAEMON_PORT)) _server_sock.listen(1) log(f"ポート {DAEMON_PORT} で待ち受け中...") try: while _running: try: client_sock, addr = _server_sock.accept() except OSError: break serve_client(client_sock, addr) finally: if _mc is not None: try: _mc.set_fresh_mode(0) log("set_fresh_mode(0)") except Exception: pass _server_sock.close() log("シャットダウン完了") if __name__ == "__main__": main()