diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..87b1322 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,36 @@ +name: Lint + +on: + push: + branches: [ "main" ] + paths: + - 'software/**' + pull_request: + branches: [ "main" ] + paths: + - 'software/**' + +jobs: + lint: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./software + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install ruff + + - name: Run Ruff (Linting) + run: ruff check . + diff --git a/software/examples/alohamini/record_bi.py b/software/examples/alohamini/record_bi.py index e897fce..c995968 100644 --- a/software/examples/alohamini/record_bi.py +++ b/software/examples/alohamini/record_bi.py @@ -28,13 +28,15 @@ def main(): parser.add_argument("--task_description", type=str, default="My task description4", help="Task description") parser.add_argument("--remote_ip", type=str, default="127.0.0.1", help="Robot host IP") parser.add_argument("--robot_id", type=str, default="lekiwi_host", help="Robot ID") + parser.add_argument("--left_arm_port", type=str, default="/dev/am_arm_leader_left", help="Left leader arm port") + parser.add_argument("--right_arm_port", type=str, default="/dev/am_arm_leader_right", help="Right leader arm port") args = parser.parse_args() # === Robot and teleop config === robot_config = LeKiwiClientConfig(remote_ip=args.remote_ip, id=args.robot_id) leader_arm_config = BiSO100LeaderConfig( - left_arm_port="/dev/am_arm_leader_left", - right_arm_port="/dev/am_arm_leader_right", + left_arm_port=args.left_arm_port, + right_arm_port=args.right_arm_port, id="so101_leader_bi3", ) keyboard_config = KeyboardTeleopConfig() diff --git a/software/examples/alohamini/teleoperate_bi.py b/software/examples/alohamini/teleoperate_bi.py index dda47f2..0f9c09a 100644 --- a/software/examples/alohamini/teleoperate_bi.py +++ b/software/examples/alohamini/teleoperate_bi.py @@ -14,6 +14,8 @@ parser.add_argument("--use_dummy", action="store_true", help="Do not connect robot, only print actions") parser.add_argument("--fps", type=int, default=30, help="Main loop frequency (frames per second)") parser.add_argument("--remote_ip", type=str, default="127.0.0.1", help="LeKiwi host IP address") +parser.add_argument("--left_arm_port", type=str, default="/dev/am_arm_leader_left", help="Left leader arm port") +parser.add_argument("--right_arm_port", type=str, default="/dev/am_arm_leader_right", help="Right leader arm port") args = parser.parse_args() @@ -27,8 +29,8 @@ # Create configs robot_config = LeKiwiClientConfig(remote_ip=args.remote_ip, id="my_alohamini") bi_cfg = BiSO100LeaderConfig( - left_arm_port="/dev/am_arm_leader_left", - right_arm_port="/dev/am_arm_leader_right", + left_arm_port=args.left_arm_port, + right_arm_port=args.right_arm_port, id="so101_leader_bi3", ) leader = BiSO100Leader(bi_cfg) diff --git a/software/examples/alohamini/teleoperate_bi_voice.py b/software/examples/alohamini/teleoperate_bi_voice.py index 64ba0f6..bdcee4b 100644 --- a/software/examples/alohamini/teleoperate_bi_voice.py +++ b/software/examples/alohamini/teleoperate_bi_voice.py @@ -18,6 +18,8 @@ parser.add_argument("--use_dummy", action="store_true", help="Do not connect robot, only print actions") parser.add_argument("--fps", type=int, default=30, help="Main loop frequency (frames per second)") parser.add_argument("--remote_ip", type=str, default="127.0.0.1", help="Alohamini host IP address") +parser.add_argument("--left_arm_port", type=str, default="/dev/am_arm_leader_left", help="Left leader arm port") +parser.add_argument("--right_arm_port", type=str, default="/dev/am_arm_leader_right", help="Right leader arm port") args = parser.parse_args() USE_DUMMY = args.use_dummy @@ -29,8 +31,8 @@ # Create configs robot_config = LeKiwiClientConfig(remote_ip=args.remote_ip, id="my_alohamini") bi_cfg = BiSO100LeaderConfig( - left_arm_port="/dev/am_arm_leader_left", - right_arm_port="/dev/am_arm_leader_right", + left_arm_port=args.left_arm_port, + right_arm_port=args.right_arm_port, id="so101_leader_bi3", ) diff --git a/software/examples/debug/motors.py b/software/examples/debug/motors.py index 6765b81..5c72f3a 100644 --- a/software/examples/debug/motors.py +++ b/software/examples/debug/motors.py @@ -11,7 +11,7 @@ OperatingMode, ) -DEFAULT_PORT = "/dev/ttyACM0" +DEFAULT_PORT = os.getenv("FEETECH_PORT", "/dev/ttyACM0") HALF_TURN_DEGREE = 180 GENERAL_ACTIONS = {"sleep", "print"} @@ -24,8 +24,8 @@ def probe_scan_ids(port: str) -> dict[int, str]: """ - 探针扫描 ID 范围,返回 {id: model_name}(只含在线电机)。 - 仅做 ping,不对电机写寄存器;断开时不关力矩。 + Probe ID range and return {id: model_name} (only online motors). + Only ping, do not write to registers; do not disable torque on disconnect. """ probe_bus = build_bus(port, {}) @@ -58,7 +58,7 @@ def probe_scan_ids(port: str) -> dict[int, str]: def build_motors_from_scan(port: str): """ - 基于 probe_scan_ids 结果,构建 motors 字典(名称统一为 motor_)。 + Build motors dict based on probe_scan_ids results (names unified as motor_). """ from lerobot.motors.motors_bus import Motor, MotorNormMode found = probe_scan_ids(port) @@ -125,13 +125,13 @@ def _motor_angle_from_position(position): def get_motors_states(port): """ - 动态表格显示电机状态;总是先扫描 [scan_start, scan_end],只显示在线电机。 - - 传了 motors 也不会直接用,而是按 ID 范围扫描后重建 motors(保证不读离线 ID) + Display motor states in a dynamic table; always scan [scan_start, scan_end] first, showing only online motors. + - Even if motors are passed, they are not used directly; motors are rebuilt after scanning the ID range (ensuring no offline IDs are read) """ import time, sys, shutil from lerobot.motors.motors_bus import Motor, MotorNormMode - # ---------- ANSI 工具 ---------- + # ---------- ANSI Utils ---------- CSI = "\x1b[" def _hide_cursor(): sys.stdout.write(f"{CSI}?25l"); sys.stdout.flush() def _show_cursor(): sys.stdout.write(f"{CSI}?25h"); sys.stdout.flush() @@ -152,7 +152,7 @@ def F(v, w, a=">"): f"{F(st.get('Temperature'),4)} | {F(st.get('Port','-'),16,'<')}") return row[:maxw] if len(row) > maxw else row - # ---------- 第一步:用探针扫描在线电机 ---------- + # ---------- Step 1: Scan for online motors using probe ---------- motors = build_motors_from_scan(port) if not motors: @@ -160,9 +160,9 @@ def F(v, w, a=">"): return - # ---------- 第二步:用“只包含在线电机”的字典进入动态显示 ---------- + # ---------- Step 2: Enter dynamic display using dict containing only online motors ---------- bus = build_bus(port, motors) - if not _connect_bus(bus): # 你的辅助函数里保持 handshake=False 更稳 + if not _connect_bus(bus): # Keep handshake=False in helper function for stability return try: @@ -213,7 +213,7 @@ def F(v, w, a=">"): maxw = _term_width() - sep = "-" * min(maxw, 140) # 由 120 放宽到 140 + sep = "-" * min(maxw, 140) # Widened from 120 to 140 header = (f"{'NAME':<15} | {'ID':>3} | {'POS':>6} | {'OFF':>6} | {'ANG':>6} | " f"{'LOAD':>6} | {'ACC':>6} | {'VOLT':>4} | {'CURR(MA)':>8} | {'TEMP':>4} | PORT") header = header[:maxw] if len(header) > maxw else header @@ -235,7 +235,7 @@ def F(v, w, a=">"): except KeyboardInterrupt: pass finally: - # 关键:不要在断开时去“对所有电机”关力矩,避免离线 ID 报错 + # Key: Do not disable torque for "all motors" on disconnect to avoid errors from offline IDs. try: bus.disconnect(disable_torque=False) finally: @@ -248,9 +248,9 @@ def configure_motor_id(port: str, current_id: int, new_id: int): current_id = int(current_id) new_id = int(new_id) if current_id == new_id: - raise SystemExit("current_id == new_id,没有必要改。") + raise SystemExit("current_id == new_id, no change needed.") - # 1) 先用“裸总线”去 ping 校验(不带 motors 映射,避免库做多余事) + # 1) Ping check using "bare bus" (without motors mapping to avoid extra library actions) probe_bus = FeetechMotorsBus(port=port, motors={}) try: probe_bus.connect(handshake=False) @@ -265,11 +265,11 @@ def configure_motor_id(port: str, current_id: int, new_id: int): pass if not ok_cur: - raise SystemExit(f"[ABORT] 未发现 ID={current_id} 在线,放弃改 ID。") + raise SystemExit(f"[ABORT] ID={current_id} not online, aborting ID change.") if ok_new: - raise SystemExit(f"[ABORT] 目标 ID={new_id} 已被占用,放弃改 ID。") + raise SystemExit(f"[ABORT] Target ID={new_id} is occupied, aborting ID change.") - # 2) 用“只包含 current_id 的单电机字典”建立总线(名字无所谓,只要 id 是 current_id) + # 2) Build bus with single-motor dict containing current_id (name doesn't matter, as long as id is current_id) tmp_name = f"motor_{current_id}" one_motor = { tmp_name: Motor(id=current_id, model=DEFAULT_FEETECH_MODEL, norm_mode=MotorNormMode.RANGE_0_100) @@ -280,7 +280,7 @@ def configure_motor_id(port: str, current_id: int, new_id: int): bus.connect(handshake=False) print(f"Connected on port {bus.port} (current_id={current_id})") - # 保险:解锁 & 力矩处理(按你电机需要,可调整/删掉) + # Safety: Unlock & Torque handling (adjust/remove as needed) try: bus.write("Lock", tmp_name, 0, normalize=False) except Exception: @@ -290,12 +290,12 @@ def configure_motor_id(port: str, current_id: int, new_id: int): except Exception: pass - # 3) 直接对“当前 id 的那颗”写寄存器:ID = new_id + # 3) Directly write register for "current id": ID = new_id bus.write("ID", tmp_name, new_id, normalize=False) time.sleep(0.2) - # 4) 改后校验:当前 id 应该失联,new_id 应该在线 - # 这里用一个“空 bus”去 ping,避免受 motors 映射影响 + # 4) Post-change verify: current id should be offline, new_id should be online + # Use an "empty bus" to ping here to avoid being affected by motors mapping probe_bus2 = FeetechMotorsBus(port=port, motors={}) try: probe_bus2.connect(handshake=False) @@ -309,7 +309,7 @@ def configure_motor_id(port: str, current_id: int, new_id: int): if ok_cur2 or not ok_new2: raise SystemExit( - f"[VERIFY FAIL] 改后验证失败:ID={current_id} 仍在线={ok_cur2}, ID={new_id} 在线={ok_new2}" + f"[VERIFY FAIL] Post-change verify failed: ID={current_id} still online={ok_cur2}, ID={new_id} online={ok_new2}" ) print(f"[OK] Changed ID {current_id} -> {new_id} (model={DEFAULT_FEETECH_MODEL})") @@ -386,16 +386,16 @@ def move_motor_to_position( position: int, ): """ - 直接用数值ID控制电机到指定 position(原始ticks)。 - - 不需要 motors{},不需要电机名。 - - 默认确保处于“位置模式”,必要时切换。 + Control motor directly by numeric ID to specified position (raw ticks). + - No motors{} or motor names needed. + - Default to ensuring "Position Mode", switching if necessary. """ tmp_name = f"motor_{int(motor_id)}" one_motor = { tmp_name: Motor(id=int(motor_id), model=DEFAULT_FEETECH_MODEL, norm_mode=MotorNormMode.RANGE_0_100) } - # 可选:简单范围夹取(12bit 0~4095;按你家实际寄存器范围调整) + # Optional: Simple range clamping (12bit 0~4095; adjust according to your register range) try: position = int(position) position = max(0, min(4095, position)) @@ -407,7 +407,7 @@ def move_motor_to_position( bus.connect(handshake=False) print(f"Connected on port {bus.port} (ID={motor_id})") - # 若不确定电机当前模式,保险起见切到“位置模式” + # Switch to "Position Mode" for safety if current mode is uncertain bus.disable_torque(tmp_name) bus.write("Operating_Mode", tmp_name, OperatingMode.POSITION.value, normalize=False) bus.enable_torque(tmp_name) diff --git a/software/src/lerobot/robots/alohamini/config_lekiwi.py b/software/src/lerobot/robots/alohamini/config_lekiwi.py index 5cff799..c72417f 100644 --- a/software/src/lerobot/robots/alohamini/config_lekiwi.py +++ b/software/src/lerobot/robots/alohamini/config_lekiwi.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from dataclasses import dataclass, field from lerobot.cameras.configs import CameraConfig, Cv2Rotation @@ -43,8 +44,8 @@ def lekiwi_cameras_config() -> dict[str, CameraConfig]: @RobotConfig.register_subclass("lekiwi") @dataclass class LeKiwiConfig(RobotConfig): - left_port: str = "/dev/am_arm_follower_left" # port to connect to the bus - right_port: str = "/dev/am_arm_follower_right" # port to connect to the bus + left_port: str = os.getenv("LEKIWI_LEFT_PORT", "/dev/am_arm_follower_left") # port to connect to the bus + right_port: str = os.getenv("LEKIWI_RIGHT_PORT", "/dev/am_arm_follower_right") # port to connect to the bus disable_torque_on_disconnect: bool = True # `max_relative_target` limits the magnitude of the relative positional target vector for safety purposes. diff --git a/software/src/lerobot/robots/alohamini/lekiwi.py b/software/src/lerobot/robots/alohamini/lekiwi.py index 701b6c3..6c5bc05 100644 --- a/software/src/lerobot/robots/alohamini/lekiwi.py +++ b/software/src/lerobot/robots/alohamini/lekiwi.py @@ -67,7 +67,6 @@ def __init__(self, config: LeKiwiConfig): "arm_left_shoulder_lift": Motor(2, "sts3215", norm_mode_body), "arm_left_elbow_flex": Motor(3, "sts3215", norm_mode_body), "arm_left_wrist_flex": Motor(4, "sts3215", norm_mode_body), - #"left_wrist_yaw": Motor(5, "sts3215", norm_mode_body), "arm_left_wrist_roll": Motor(5, "sts3215", norm_mode_body), "arm_left_gripper": Motor(6, "sts3215", MotorNormMode.RANGE_0_100), # base @@ -87,10 +86,8 @@ def __init__(self, config: LeKiwiConfig): "arm_right_shoulder_lift": Motor(2, "sts3215", norm_mode_body), "arm_right_elbow_flex": Motor(3, "sts3215", norm_mode_body), "arm_right_wrist_flex": Motor(4, "sts3215", norm_mode_body), - #"right_wrist_yaw": Motor(5, "sts3215", norm_mode_body), "arm_right_wrist_roll": Motor(5, "sts3215", norm_mode_body), "arm_right_gripper": Motor(6, "sts3215", MotorNormMode.RANGE_0_100), - #"lift_axis": Motor(12, "sts3215", MotorNormMode.DEGREES), }, calibration=self.calibration, ) @@ -98,13 +95,9 @@ def __init__(self, config: LeKiwiConfig): self.left_arm_motors = [m for m in self.left_bus.motors if m.startswith("arm_left_")] self.base_motors = [m for m in self.left_bus.motors if m.startswith("base_")] - #self.left_arm_motors = [m for m in self.left_bus.motors if m.startswith("right_arm_")] self.right_arm_motors = [m for m in (self.right_bus.motors if self.right_bus else []) if m.startswith("arm_right_")] - # self.arm_motors = [motor for motor in self.left_bus.motors if motor.startswith("arm")] - # self.base_motors = [motor for motor in self.left_bus.motors if motor.startswith("base")] - self.cameras = make_cameras_from_configs(config.cameras) @@ -136,8 +129,8 @@ def _state_ft(self) -> dict[str, type]: "x.vel", "y.vel", "theta.vel", - "lift_axis.height_mm", # ← 新增 - #"lift_axis.vel", # ← 新增(可选,做调试用) + "lift_axis.height_mm", # Added + #"lift_axis.vel", # Added (optional, for debugging) ), float, ) @@ -156,10 +149,6 @@ def observation_features(self) -> dict[str, type | tuple]: def action_features(self) -> dict[str, type]: return self._state_ft - # @property - # def is_connected(self) -> bool: - # return self.left_bus.is_connected and all(cam.is_connected for cam in self.cameras.values()) - @property def is_connected(self) -> bool: cams_ok = all(cam.is_connected for cam in self.cameras.values()) @@ -186,7 +175,7 @@ def connect(self, calibrate: bool = True) -> None: logger.info(f"{self} connected.") self.lift.home() - print("Lift axis homed to 0mm.") + logger.info("Lift axis homed to 0mm.") @@ -232,7 +221,7 @@ def calibrate(self) -> None: self.left_bus.write("Operating_Mode", name, OperatingMode.POSITION.value) input("Move LEFT arm to the middle of its range of motion, then press ENTER...") - left_homing = self.left_bus.set_half_turn_homings(self.left_arm_motors) # 仅左臂条目 + left_homing = self.left_bus.set_half_turn_homings(self.left_arm_motors) # Only left arm items for wheel in self.base_motors: left_homing[wheel] = 0 @@ -317,8 +306,6 @@ def configure(self): for name in self.base_motors: self.left_bus.write("Operating_Mode", name, OperatingMode.VELOCITY.value) - #self.left_bus.enable_torque() - self.right_bus.disable_torque() self.right_bus.configure_motors() for name in self.right_arm_motors: @@ -326,9 +313,7 @@ def configure(self): self.right_bus.write("P_Coefficient", name, 16) self.right_bus.write("I_Coefficient", name, 0) self.right_bus.write("D_Coefficient", name, 32) - #self.right_bus.enable_torque() - #self.lift.configure() @@ -485,9 +470,7 @@ def get_observation(self) -> dict[str, Any]: # Read actuators position for arm and vel for base start = time.perf_counter() - # arm_pos = self.left_bus.sync_read("Present_Position", self.arm_motors) - - #print(f"Left arm motors: {self.left_arm_motors}, Right arm motors: {self.right_arm_motors}") # debug + left_pos = self.left_bus.sync_read("Present_Position", self.left_arm_motors) # left_arm_* @@ -507,7 +490,6 @@ def get_observation(self) -> dict[str, Any]: obs_dict = {**left_arm_state, **right_arm_state,**base_vel} self.lift.contribute_observation(obs_dict) - #print(f"Observation dict so far: {obs_dict}") # debug dt_ms = (time.perf_counter() - start) * 1e3 logger.debug(f"{self} read state: {dt_ms:.1f}ms") @@ -553,12 +535,7 @@ def send_action(self, action: dict[str, Any]) -> dict[str, Any]: # Cap goal position when too far away from present position. # /!\ Slower fps expected due to reading from the follower. - # if self.config.max_relative_target is not None: - # present_pos = self.left_bus.sync_read("Present_Position", self.arm_motors) - # goal_present_pos = {key: (g_pos, present_pos[key]) for key, g_pos in arm_goal_pos.items()} - # arm_safe_goal_pos = ensure_safe_goal_position(goal_present_pos, self.config.max_relative_target) - # arm_goal_pos = arm_safe_goal_pos - + self.lift.apply_action(action) if left_pos and self.config.max_relative_target is not None: @@ -573,13 +550,6 @@ def send_action(self, action: dict[str, Any]) -> dict[str, Any]: # Send goal position to the actuators - # arm_goal_pos_raw = {k.replace(".pos", ""): v for k, v in arm_goal_pos.items()} - # self.left_bus.sync_write("Goal_Position", arm_goal_pos_raw) - # self.left_bus.sync_write("Goal_Velocity", base_wheel_goal_vel) - - # return {**arm_goal_pos, **base_goal_vel} - - #print(f"[{filename}:{lineno}]Sending left_pos:{left_pos}, right_pos:{right_pos}, base_wheel_goal_vel:{base_wheel_goal_vel}") # debug if left_pos: self.left_bus.sync_write("Goal_Position", {k.replace(".pos", ""): v for k, v in left_pos.items()}) @@ -597,7 +567,7 @@ def stop_base(self): def read_and_check_currents(self, limit_ma, print_currents): """Read left/right bus currents (mA), print them, and enforce overcurrent protection""" - scale = 6.5 # sts3215 电流单位转换系数 + scale = 6.5 # sts3215 current unit conversion coefficient left_curr_raw = {} left_curr_raw = self.left_bus.sync_read("Present_Current", list(self.left_bus.motors.keys())) right_curr_raw = {} @@ -606,15 +576,15 @@ def read_and_check_currents(self, limit_ma, print_currents): if print_currents: left_line = "{" + ",".join(str(int(v * scale)) for v in left_curr_raw.values()) + "}" - print(f"Left Bus currents: {left_line}") + logger.info(f"Left Bus currents: {left_line}") if right_curr_raw: right_line = "{" + ",".join(str(int(v * scale)) for v in right_curr_raw.values()) + "}" - print(f"Right Bus currents: {right_line}") + logger.info(f"Right Bus currents: {right_line}") for name, raw in {**left_curr_raw, **right_curr_raw}.items(): current_ma = float(raw) * scale if current_ma > limit_ma: - print(f"[Overcurrent] {name}: {current_ma:.1f} mA > {limit_ma:.1f} mA, disconnecting!") + logger.error(f"[Overcurrent] {name}: {current_ma:.1f} mA > {limit_ma:.1f} mA, disconnecting!") try: self.stop_base() except Exception: @@ -622,7 +592,7 @@ def read_and_check_currents(self, limit_ma, print_currents): try: self.disconnect() except Exception as e: - print(f"[Overcurrent] disconnect error: {e}") + logger.error(f"[Overcurrent] disconnect error: {e}") sys.exit(1) return {k: round(v * scale, 1) for k, v in {**left_curr_raw, **right_curr_raw}.items()}