mirror of
https://github.com/huggingface/lerobot.git
synced 2026-06-02 20:01:25 +00:00
* Fix imports * Add feetech write tests * Nit * Add autoclosing fixture * Assert ping stub called * Add CalibrationMode * Add Motor in dxl robots * Simplify split_int_bytes * Rename read/write -> sync_read/write, refactor, add write * Rename tests * Refactor dxl tests by functionality * Add dxl write test * Refactor _is_comm_success * Refactor feetech tests by functionality * Add feetech write test * Simplify _is_comm_success & _is_error * Move mock_serial patch to dedicated file * Remove test skips & fix docstrings * Nit * Add dxl operating modes * Add is_connected in robots and teleops * Update Koch * Add feetech operating modes * Caps dxl OperatingMode * Update ensure_safe_goal_position * Update so100 * Privatize methods & renames * Fix dict * Add _configure_motors & move ping methods * Return models (str) with pings * Implement feetech broadcast ping * Add raw_values option * Rename idx -> id_ * Improve errors * Fix feetech ping tests * Ensure motors exist at connection time * Update tests * Add test_motors_bus * Move DriveMode & TorqueMode * Update Koch imports * Update so100 imports * Fix visualize_motors_bus * Fix imports * Add calibration * Rename idx -> id_ * Rename idx -> id_ * (WIP) _async_read * Add new calibration method for robot refactor (#896) Co-authored-by: Simon Alibert <simon.alibert@huggingface.co> * Remove deprecated scripts * Rename CalibrationMode -> MotorNormMode * Fix calibration functions * Remove todo * Add scan_port utility * Add calibration utilities * Move encoding functions to encoding_utils * Add test_encoding_utils * Rename test * Add more calibration utilities * Format baudrate tables * Implement SO-100 leader calibration * Implement SO-100 follower calibration * Implement Koch calibration * Add test_scan_port (TODO) * Fix calibration * Hack feetech firmware bug * Update tests * Update Koch & SO-100 * Improve format * Rename SO-100 classes * Rename Koch classes * Add calibration tests * Remove old calibration tests * Revert feetech hack and monkeypatch instead * Simplify motors mocks * Add is_calibrated test * Update viperx & widowx * Rename viperx & widowx * Remove old calibration * feat(teleop): thread-safe keyboard teleop implementation (#869) Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com> * Add support for feetech scs series + various fixes * Update dynamixel with motors bus & tables changes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * (WIP) Add Hope Jr * Rename arm -> hand * (WIP) Add homonculus arm & glove * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add Feetech protocol version * Implement read * Use constants from sdks * (nit) move write * Fix broadcast ping type hint * Add protocol 1 broadcast ping * Refactor & add _serialize_data * Add feetech sm8512bl * Make feetech broadcast ping faster in protocol 1 * Cleanup * Add support for feetech protocol 1 to _split_into_byte_chunks * Fix unormalize * Remove test_motors_bus fixtures * Add more segmented tests (base motor bus & feetech), add feetech protocol 1 support * Add more segmented tests (dynamixel) * Refactor tests * Add handshake, fix feetech _read_firmware_version * Fix tests * Motors config & disconnect fixes * Add torque_disabled context * Update branch & fix pre-commit errors * Fix hand & glove readings * Update feetech tables * Move read/write_calibration implementations * Add setup_motor * Fix calibration msg display * Fix setup_motor & add it to robots * Fix _find_single_motor * Remove deprecated configure_motor * Remove deprecated dynamixel_calibration * Remove names * Remove deprecated import * refactor/lekiwi robot (#863) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Simon Alibert <simon.alibert@huggingface.co> Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com> * fix(teleoperators): use property is_connected (#1075) * Remove deprecated manipulator * Update robot features & naming * Update teleop features & naming * Add make_teleoperator_from_config * Rename find_port * Fix config parsing * Remove app script * Add setup_motors * Add teleoperate * Add record * Add replay * Fix test_datasets * Add mock robot & teleop * Add new test_control_robot * Add test_record_and_resume * Remove deprecated scripts & tests * Add calibrate * Add docstrings * Fix tests (no-extras install) * Add SO101 * Remove pynput from optional deps * Rename example 7 * Remove unecessary id * Add MotorsBus docstrings * Rename arm -> bus * Remove Moss arm * Fix setup_motors & calibrate configs * Fix test_calibrate * Add copyrights * Update hand & arm * Update homonculus hand & arm * Fix dxl _find_single_motor * Update glove * Add setup_motors for lekiwi * Fix glove calibration * Complete docstring * Add check for same min and max during calibration * Move MockMotorsBus * Add so100_follower tests * (WIP) add calibration gui * Fix test * Add setup_motors * Update calibration gui * Remove old .cache folder * Replace deprecated abc.abstractproperty * Fix feetech protocol 1 configure * Cleanup gui & add copyrights * Anatomically precise joint names * (WIP) Add glove to hand joints translation * Move make_robot_config * Add drive_mode & norm_mode in glove calibration * Fix joints translation * Fix normalization drive_mode * nit * Fix glove to hand conversion * Adapt feetech calibration * Remove pygame prompt * Implement arm calibration (hacks) * Better MotorsBus error messages * Update feetech read_calibration * Fix feetech test_is_calibrated * Cleanup glove * (WIP) Update arm * Add changes from #1117 * refactor(cameras): cameras implementations + tests improvements (#1108) Co-authored-by: Simon Alibert <simon.alibert@huggingface.co> Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * Fix arm joints order * Add timeout/event logic * Fix arm & glove * Fix predict_action from record * fix(cameras): update docstring + handle sn when starts with 0 + update timeouts to more reasonable value (#1154) * fix(scripts): parser instead of draccus in record + add __get_path_fields__() to RecordConfig (#1155) * Left/Right sides + other fixes * Arm fixes and add config * More hacks * Add control scripts * Fix merge errors * push changes to calibration, teleop and docs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Move readme to docs * update readme Signed-off-by: Martino Russi <77496684+nepyope@users.noreply.github.com> * Add files via upload Signed-off-by: Martino Russi <77496684+nepyope@users.noreply.github.com> * Update image sources * Symlink doc * Compress image * Move image * Update docs link * fix docs * simplify teleop scripts * fix variable names * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address code review * add EMA to glove * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * integrate teleoperation for hand * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update docs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * import hopejr/homunculus in teleoperate * update docs for teleoperate, record, replay, train and inference * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore(hopejr): address comments * chore(hopejr): address coments 2 * chore(docs): update teleoperation instructions for the hand/glove * fix(hopejr): calibration int + update docs --------- Signed-off-by: Martino Russi <77496684+nepyope@users.noreply.github.com> Signed-off-by: Simon Alibert <75076266+aliberts@users.noreply.github.com> Co-authored-by: Pepijn <138571049+pkooij@users.noreply.github.com> Co-authored-by: Steven Palma <imstevenpmwork@ieee.org> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: nepyope <nopyeps@gmail.com> Co-authored-by: Martino Russi <77496684+nepyope@users.noreply.github.com> Co-authored-by: Steven Palma <steven.palma@huggingface.co>
402 lines
14 KiB
Python
402 lines
14 KiB
Python
# Copyright 2025 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import math
|
|
import os
|
|
from dataclasses import dataclass
|
|
|
|
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
|
|
|
|
from lerobot.motors import MotorCalibration, MotorsBus
|
|
|
|
BAR_LEN, BAR_THICKNESS = 450, 8
|
|
HANDLE_R = 10
|
|
BRACKET_W, BRACKET_H = 6, 14
|
|
TRI_W, TRI_H = 12, 14
|
|
|
|
BTN_W, BTN_H = 60, 22
|
|
SAVE_W, SAVE_H = 80, 28
|
|
LOAD_W = 80
|
|
DD_W, DD_H = 160, 28
|
|
|
|
TOP_GAP = 50
|
|
PADDING_Y, TOP_OFFSET = 70, 60
|
|
FONT_SIZE, FPS = 20, 60
|
|
|
|
BG_COLOR = (30, 30, 30)
|
|
BAR_RED, BAR_GREEN = (200, 60, 60), (60, 200, 60)
|
|
HANDLE_COLOR, TEXT_COLOR = (240, 240, 240), (250, 250, 250)
|
|
TICK_COLOR = (250, 220, 40)
|
|
BTN_COLOR, BTN_COLOR_HL = (80, 80, 80), (110, 110, 110)
|
|
DD_COLOR, DD_COLOR_HL = (70, 70, 70), (100, 100, 100)
|
|
|
|
|
|
def dist(a, b):
|
|
return math.hypot(a[0] - b[0], a[1] - b[1])
|
|
|
|
|
|
@dataclass
|
|
class RangeValues:
|
|
min_v: int
|
|
pos_v: int
|
|
max_v: int
|
|
|
|
|
|
class RangeSlider:
|
|
"""One motor = one slider row"""
|
|
|
|
def __init__(self, motor, idx, res, calibration, present, label_pad, base_y):
|
|
import pygame
|
|
|
|
self.motor = motor
|
|
self.res = res
|
|
self.x0 = 40 + label_pad
|
|
self.x1 = self.x0 + BAR_LEN
|
|
self.y = base_y + idx * PADDING_Y
|
|
|
|
self.min_v = calibration.range_min
|
|
self.max_v = calibration.range_max
|
|
self.pos_v = max(self.min_v, min(present, self.max_v))
|
|
|
|
self.min_x = self._pos_from_val(self.min_v)
|
|
self.max_x = self._pos_from_val(self.max_v)
|
|
self.pos_x = self._pos_from_val(self.pos_v)
|
|
|
|
self.min_btn = pygame.Rect(self.x0 - BTN_W - 6, self.y - BTN_H // 2, BTN_W, BTN_H)
|
|
self.max_btn = pygame.Rect(self.x1 + 6, self.y - BTN_H // 2, BTN_W, BTN_H)
|
|
|
|
self.drag_min = self.drag_max = self.drag_pos = False
|
|
self.tick_val = present
|
|
self.font = pygame.font.Font(None, FONT_SIZE)
|
|
|
|
def _val_from_pos(self, x):
|
|
return round((x - self.x0) / BAR_LEN * self.res)
|
|
|
|
def _pos_from_val(self, v):
|
|
return self.x0 + (v / self.res) * BAR_LEN
|
|
|
|
def set_tick(self, v):
|
|
self.tick_val = max(0, min(v, self.res))
|
|
|
|
def _triangle_hit(self, pos):
|
|
import pygame
|
|
|
|
tri_top = self.y - BAR_THICKNESS // 2 - 2
|
|
return pygame.Rect(self.pos_x - TRI_W // 2, tri_top - TRI_H, TRI_W, TRI_H).collidepoint(pos)
|
|
|
|
def handle_event(self, e):
|
|
import pygame
|
|
|
|
if e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
|
|
if self.min_btn.collidepoint(e.pos):
|
|
self.min_x, self.min_v = self.pos_x, self.pos_v
|
|
return
|
|
if self.max_btn.collidepoint(e.pos):
|
|
self.max_x, self.max_v = self.pos_x, self.pos_v
|
|
return
|
|
if dist(e.pos, (self.min_x, self.y)) <= HANDLE_R:
|
|
self.drag_min = True
|
|
elif dist(e.pos, (self.max_x, self.y)) <= HANDLE_R:
|
|
self.drag_max = True
|
|
elif self._triangle_hit(e.pos):
|
|
self.drag_pos = True
|
|
|
|
elif e.type == pygame.MOUSEBUTTONUP and e.button == 1:
|
|
self.drag_min = self.drag_max = self.drag_pos = False
|
|
|
|
elif e.type == pygame.MOUSEMOTION:
|
|
x = e.pos[0]
|
|
if self.drag_min:
|
|
self.min_x = max(self.x0, min(x, self.pos_x))
|
|
elif self.drag_max:
|
|
self.max_x = min(self.x1, max(x, self.pos_x))
|
|
elif self.drag_pos:
|
|
self.pos_x = max(self.min_x, min(x, self.max_x))
|
|
|
|
self.min_v = self._val_from_pos(self.min_x)
|
|
self.max_v = self._val_from_pos(self.max_x)
|
|
self.pos_v = self._val_from_pos(self.pos_x)
|
|
|
|
def _draw_button(self, surf, rect, text):
|
|
import pygame
|
|
|
|
clr = BTN_COLOR_HL if rect.collidepoint(pygame.mouse.get_pos()) else BTN_COLOR
|
|
pygame.draw.rect(surf, clr, rect, border_radius=4)
|
|
t = self.font.render(text, True, TEXT_COLOR)
|
|
surf.blit(t, (rect.centerx - t.get_width() // 2, rect.centery - t.get_height() // 2))
|
|
|
|
def draw(self, surf):
|
|
import pygame
|
|
|
|
# motor name above set-min button (right-aligned)
|
|
name_surf = self.font.render(self.motor, True, TEXT_COLOR)
|
|
surf.blit(
|
|
name_surf,
|
|
(self.min_btn.right - name_surf.get_width(), self.min_btn.y - name_surf.get_height() - 4),
|
|
)
|
|
|
|
# bar + active section
|
|
pygame.draw.rect(surf, BAR_RED, (self.x0, self.y - BAR_THICKNESS // 2, BAR_LEN, BAR_THICKNESS))
|
|
pygame.draw.rect(
|
|
surf, BAR_GREEN, (self.min_x, self.y - BAR_THICKNESS // 2, self.max_x - self.min_x, BAR_THICKNESS)
|
|
)
|
|
|
|
# tick
|
|
tick_x = self._pos_from_val(self.tick_val)
|
|
pygame.draw.line(
|
|
surf,
|
|
TICK_COLOR,
|
|
(tick_x, self.y - BAR_THICKNESS // 2 - 4),
|
|
(tick_x, self.y + BAR_THICKNESS // 2 + 4),
|
|
2,
|
|
)
|
|
|
|
# brackets
|
|
for x, sign in ((self.min_x, +1), (self.max_x, -1)):
|
|
pygame.draw.line(
|
|
surf, HANDLE_COLOR, (x, self.y - BRACKET_H // 2), (x, self.y + BRACKET_H // 2), 2
|
|
)
|
|
pygame.draw.line(
|
|
surf,
|
|
HANDLE_COLOR,
|
|
(x, self.y - BRACKET_H // 2),
|
|
(x + sign * BRACKET_W, self.y - BRACKET_H // 2),
|
|
2,
|
|
)
|
|
pygame.draw.line(
|
|
surf,
|
|
HANDLE_COLOR,
|
|
(x, self.y + BRACKET_H // 2),
|
|
(x + sign * BRACKET_W, self.y + BRACKET_H // 2),
|
|
2,
|
|
)
|
|
|
|
# triangle ▼
|
|
tri_top = self.y - BAR_THICKNESS // 2 - 2
|
|
pygame.draw.polygon(
|
|
surf,
|
|
HANDLE_COLOR,
|
|
[
|
|
(self.pos_x, tri_top),
|
|
(self.pos_x - TRI_W // 2, tri_top - TRI_H),
|
|
(self.pos_x + TRI_W // 2, tri_top - TRI_H),
|
|
],
|
|
)
|
|
|
|
# numeric labels
|
|
fh = self.font.get_height()
|
|
pos_y = tri_top - TRI_H - 4 - fh
|
|
txts = [
|
|
(self.min_v, self.min_x, self.y - BRACKET_H // 2 - 4 - fh),
|
|
(self.max_v, self.max_x, self.y - BRACKET_H // 2 - 4 - fh),
|
|
(self.pos_v, self.pos_x, pos_y),
|
|
]
|
|
for v, x, y in txts:
|
|
s = self.font.render(str(v), True, TEXT_COLOR)
|
|
surf.blit(s, (x - s.get_width() // 2, y))
|
|
|
|
# buttons
|
|
self._draw_button(surf, self.min_btn, "set min")
|
|
self._draw_button(surf, self.max_btn, "set max")
|
|
|
|
# external
|
|
def values(self) -> RangeValues:
|
|
return RangeValues(self.min_v, self.pos_v, self.max_v)
|
|
|
|
|
|
class RangeFinderGUI:
|
|
def __init__(self, bus: MotorsBus, groups: dict[str, list[str]] | None = None):
|
|
import pygame
|
|
|
|
self.bus = bus
|
|
self.groups = groups if groups is not None else {"all": list(bus.motors)}
|
|
self.group_names = list(groups)
|
|
self.current_group = self.group_names[0]
|
|
|
|
if not bus.is_connected:
|
|
bus.connect()
|
|
|
|
self.calibration = bus.read_calibration()
|
|
self.res_table = bus.model_resolution_table
|
|
self.present_cache = {
|
|
m: bus.read("Present_Position", m, normalize=False) for motors in groups.values() for m in motors
|
|
}
|
|
|
|
pygame.init()
|
|
self.font = pygame.font.Font(None, FONT_SIZE)
|
|
|
|
label_pad = max(self.font.size(m)[0] for ms in groups.values() for m in ms)
|
|
self.label_pad = label_pad
|
|
width = 40 + label_pad + BAR_LEN + 6 + BTN_W + 10 + SAVE_W + 10
|
|
self.controls_bottom = 10 + SAVE_H
|
|
self.base_y = self.controls_bottom + TOP_GAP
|
|
height = self.base_y + PADDING_Y * len(groups[self.current_group]) + 40
|
|
|
|
self.screen = pygame.display.set_mode((width, height))
|
|
pygame.display.set_caption("Motors range finder")
|
|
|
|
# ui rects
|
|
self.save_btn = pygame.Rect(width - SAVE_W - 10, 10, SAVE_W, SAVE_H)
|
|
self.load_btn = pygame.Rect(self.save_btn.left - LOAD_W - 10, 10, LOAD_W, SAVE_H)
|
|
self.dd_btn = pygame.Rect(width // 2 - DD_W // 2, 10, DD_W, DD_H)
|
|
self.dd_open = False # dropdown expanded?
|
|
|
|
self.clock = pygame.time.Clock()
|
|
self._build_sliders()
|
|
self._adjust_height()
|
|
|
|
def _adjust_height(self):
|
|
import pygame
|
|
|
|
motors = self.groups[self.current_group]
|
|
new_h = self.base_y + PADDING_Y * len(motors) + 40
|
|
if new_h != self.screen.get_height():
|
|
w = self.screen.get_width()
|
|
self.screen = pygame.display.set_mode((w, new_h))
|
|
|
|
def _build_sliders(self):
|
|
self.sliders: list[RangeSlider] = []
|
|
motors = self.groups[self.current_group]
|
|
for i, m in enumerate(motors):
|
|
self.sliders.append(
|
|
RangeSlider(
|
|
motor=m,
|
|
idx=i,
|
|
res=self.res_table[self.bus.motors[m].model] - 1,
|
|
calibration=self.calibration[m],
|
|
present=self.present_cache[m],
|
|
label_pad=self.label_pad,
|
|
base_y=self.base_y,
|
|
)
|
|
)
|
|
|
|
def _draw_dropdown(self):
|
|
import pygame
|
|
|
|
# collapsed box
|
|
hover = self.dd_btn.collidepoint(pygame.mouse.get_pos())
|
|
pygame.draw.rect(self.screen, DD_COLOR_HL if hover else DD_COLOR, self.dd_btn, border_radius=6)
|
|
|
|
txt = self.font.render(self.current_group, True, TEXT_COLOR)
|
|
self.screen.blit(
|
|
txt, (self.dd_btn.centerx - txt.get_width() // 2, self.dd_btn.centery - txt.get_height() // 2)
|
|
)
|
|
|
|
tri_w, tri_h = 12, 6
|
|
cx = self.dd_btn.right - 14
|
|
cy = self.dd_btn.centery + 1
|
|
pygame.draw.polygon(
|
|
self.screen,
|
|
TEXT_COLOR,
|
|
[(cx - tri_w // 2, cy - tri_h // 2), (cx + tri_w // 2, cy - tri_h // 2), (cx, cy + tri_h // 2)],
|
|
)
|
|
|
|
if not self.dd_open:
|
|
return
|
|
|
|
# expanded list
|
|
for i, name in enumerate(self.group_names):
|
|
item_rect = pygame.Rect(self.dd_btn.left, self.dd_btn.bottom + i * DD_H, DD_W, DD_H)
|
|
clr = DD_COLOR_HL if item_rect.collidepoint(pygame.mouse.get_pos()) else DD_COLOR
|
|
pygame.draw.rect(self.screen, clr, item_rect)
|
|
t = self.font.render(name, True, TEXT_COLOR)
|
|
self.screen.blit(
|
|
t, (item_rect.centerx - t.get_width() // 2, item_rect.centery - t.get_height() // 2)
|
|
)
|
|
|
|
def _handle_dropdown_event(self, e):
|
|
import pygame
|
|
|
|
if e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
|
|
if self.dd_btn.collidepoint(e.pos):
|
|
self.dd_open = not self.dd_open
|
|
return True
|
|
if self.dd_open:
|
|
for i, name in enumerate(self.group_names):
|
|
item_rect = pygame.Rect(self.dd_btn.left, self.dd_btn.bottom + i * DD_H, DD_W, DD_H)
|
|
if item_rect.collidepoint(e.pos):
|
|
if name != self.current_group:
|
|
self.current_group = name
|
|
self._build_sliders()
|
|
self._adjust_height()
|
|
self.dd_open = False
|
|
return True
|
|
self.dd_open = False
|
|
return False
|
|
|
|
def _save_current(self):
|
|
for s in self.sliders:
|
|
self.calibration[s.motor].range_min = s.min_v
|
|
self.calibration[s.motor].range_max = s.max_v
|
|
|
|
with self.bus.torque_disabled():
|
|
self.bus.write_calibration(self.calibration)
|
|
|
|
def _load_current(self):
|
|
self.calibration = self.bus.read_calibration()
|
|
for s in self.sliders:
|
|
s.min_v = self.calibration[s.motor].range_min
|
|
s.max_v = self.calibration[s.motor].range_max
|
|
s.min_x = s._pos_from_val(s.min_v)
|
|
s.max_x = s._pos_from_val(s.max_v)
|
|
|
|
def run(self) -> dict[str, MotorCalibration]:
|
|
import pygame
|
|
|
|
while True:
|
|
for e in pygame.event.get():
|
|
if e.type == pygame.QUIT:
|
|
pygame.quit()
|
|
return self.calibration
|
|
|
|
if self._handle_dropdown_event(e):
|
|
continue
|
|
|
|
if e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
|
|
if self.save_btn.collidepoint(e.pos):
|
|
self._save_current()
|
|
elif self.load_btn.collidepoint(e.pos):
|
|
self._load_current()
|
|
|
|
for s in self.sliders:
|
|
s.handle_event(e)
|
|
|
|
# live goal write while dragging
|
|
for s in self.sliders:
|
|
if s.drag_pos:
|
|
self.bus.write("Goal_Position", s.motor, s.pos_v, normalize=False)
|
|
|
|
# tick update
|
|
for s in self.sliders:
|
|
pos = self.bus.read("Present_Position", s.motor, normalize=False)
|
|
s.set_tick(pos)
|
|
self.present_cache[s.motor] = pos
|
|
|
|
# ─ drawing
|
|
self.screen.fill(BG_COLOR)
|
|
for s in self.sliders:
|
|
s.draw(self.screen)
|
|
|
|
self._draw_dropdown()
|
|
|
|
# load / save buttons
|
|
for rect, text in ((self.load_btn, "LOAD"), (self.save_btn, "SAVE")):
|
|
clr = BTN_COLOR_HL if rect.collidepoint(pygame.mouse.get_pos()) else BTN_COLOR
|
|
pygame.draw.rect(self.screen, clr, rect, border_radius=6)
|
|
t = self.font.render(text, True, TEXT_COLOR)
|
|
self.screen.blit(t, (rect.centerx - t.get_width() // 2, rect.centery - t.get_height() // 2))
|
|
|
|
pygame.display.flip()
|
|
self.clock.tick(FPS)
|