tryb bez HOME: odtwarzaj tylko ruchy nagrane pod aktualny montaz (grasp_mid/home/drop_box koncza w HOME i uderza w kamere)
+
tryb bez HOME: odtwarzaj tylko ruchy nagrane pod aktualny montaz (sloik = wrzut trzymanej szyszki do sloika, konczy w HOME)
diff --git a/docs/LOG.md b/docs/LOG.md
index 0db2c7d..b7fca0d 100644
--- a/docs/LOG.md
+++ b/docs/LOG.md
@@ -1008,3 +1008,18 @@ bez rsync `deploy/push_to_pi.sh` idzie przez scp i nadpisuje na Pi `motions/drop
**Nastepny krok:** zdecydowac, ktora kalibracja/jazda zostaje (raczej zygzak po mapie z frane/mapa-d435); Xiao
wpiac z powrotem (teraz w jego USB jest leader), test na robocie z wylacznikiem.
**Sprzet:** nie
+
+## 2026-09-27 - pawel120 (Claude) - panel: zbieranie ACT + sloik
+**Zrobione:** Branch `pawel/panel-sloik`: przycisk "Zbierz szyszke" w `tools/robot_panel.py` (zadanie `act_pick.py`,
+wagi 007000; panel zatrzymuje ramie i kamere na czas chwytu i sam je wznawia, odmawia gdy chodza poza panelem; STOP i
+"Przerwij" zabijaja grupe procesow, wiec tez `lerobot-rollout`). Nowy ruch `motions/sloik.json` ze sciezki nagranej reka
+(`tools/live_grasp/data/jar_cycle.json`, JAR_UP/JAR_BACK z `pick.py`); `act_pick.py` wrzuca nim zamiast placeholdera
+`drop_box`. `grasp_mid` i `drop_box` z `"panel": false` - schowane w panelu ramienia. Testy: 308 zielonych, UI w `--demo`.
+Wczesniej w sesji: panel na Pi po kablu 192.168.137.5 (Pi zrestartowalo sie ok. 16:03), prog HSV na zewnatrz wpisany
+tylko na Pi (lo [104,40,40] hi [130,125,200], kopia starego configu `.bak-20260927-1620-hala`). Zygzak nie ruszyl:
+phyphox na iPhonie nie odpowiadal (port 80 zamkniety, ping OK).
+**Nie dziala / otwarte:** przycisk na robocie niesprawdzony; wagi 007000 na Pi niekompletne (2 MB, zawieszony `tar`
+od 16:22); ACT nie sprawdza chwytu przed `sloik`; prog HSV nie w repo. Na laptopie `stash@{0}` (docs + kopie modeli,
+identyczne z origin/master) i `.git/index.lock` po przerwanym `git pull` (lock usuniety).
+**Nastepny krok:** review + merge, push na Pi, wagi 007000, test z wylacznikiem: najpierw `sloik` z panelu ramienia.
+**Sprzet:** dotkniety (Pi: start panelu, restart kamery, prog HSV w configu; ramie i baza nie ruszane przez Claude)
diff --git a/docs/STATUS.md b/docs/STATUS.md
index 35a4d64..83190b4 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -3,7 +3,7 @@
Jeden ekran. Aktualizuje go KAZDY PR (checkbox w szablonie PR). Historia jest w `docs/LOG.md`,
zadania i przypisania na tablicy Projects (link nizej). Czego nie ma tutaj albo w issue, nie istnieje.
-**Stan na:** 2026-09-27 15:00 (ACT: dataset 50 epizodow leaderem, trening na RTX 3070, checkpointy na Pi i masterze; mapa ogrodu D435 + zygzak po mapie - `docs/MAPA.md`)
+**Stan na:** 2026-09-27 17:00 (panel: przycisk "Zbierz szyszke" = ACT + sloik, PR pawel/panel-sloik; ACT: dataset 50 epizodow leaderem, trening na RTX 3070, checkpointy na Pi i masterze; mapa ogrodu D435 + zygzak po mapie - `docs/MAPA.md`)
**Robot (kto ma sprzet, do kiedy):** frane (sesja trwa)
**Tablica zadan:** TODO wkleic link do GitHub Projects (zaklada pawel120, patrz docs/CONTRIBUTING.md)
@@ -51,6 +51,17 @@ zadania i przypisania na tablicy Projects (link nizej). Czego nie ma tutaj albo
## Nie dziala / nie sprawdzone
+- Przycisk "Zbierz szyszke" w panelu `tools/robot_panel.py` (branch pawel/panel-sloik): zadanie `act_pick.py` (wagi
+ `~/models/act_so101_grasp2/007000`, `--policy`/`ROBOT_ACT_POLICY`), na czas chwytu panel zatrzymuje ramie i kamere,
+ po koncu wznawia; STOP (spacja) i "Przerwij" zabijaja cala grupe procesow (tez `lerobot-rollout`). Wrzut: nowy
+ `motions/sloik.json` ze sciezki nagranej reka (`tools/live_grasp/data/jar_cycle.json`, sloik przy podstawie ok. -22 st).
+ Testy zielone, UI sprawdzone w `--demo`; NA ROBOCIE NIE SPRAWDZONE. Wagi 007000 na Pi byly niekompletne (2 MB z 207 MB,
+ zawieszony `tar`). ACT nie sprawdza, czy chwycil: `sloik` jedzie takze z pustym chwytakiem.
+ `grasp_mid` i `drop_box` maja `"panel": false` (schowane z przyciskow). Pulapka z LOG: `push_to_pi.sh` nadpisze na Pi
+ `motions/drop_box.json`, jesli tam lezy wersja nagrana na sprzecie.
+- Prog HSV na Pi (tylko na Pi, 2026-09-27 16:20, kopia starego: `pinecone_config.json.bak-20260927-1620-hala`):
+ lo [104,40,40] hi [130,125,200] - na zewnatrz, balans bieli zamrozony na 4600 K daje niebieski obraz, szyszki H 111-116.
+ 3/3 szyszki, 0 falszywych na trawie i chwytaku (2 klatki). Nie w repo: push z mastera go nadpisze.
- Jog XYZ w panelu ramienia (`pinecone_bot/kinematics.py`, sekcja JOG XYZ): testy + atrapa, NIE sprawdzony na ramieniu. Najpierw ZERO URDF (ramie prosto poziomo do przodu), potem sprawdzic, czy GORA jedzie w gore (inaczej `arm.urdf_sign`).
- 2026-09-26: ROBOT WJECHAL W RAMIE I JE USZKODZIL (panel jazdy po hotspocie z duzym opoznieniem). Stan ramienia do oceny, serwa nie zasilac przed ogledzinami. Pi przestal odpowiadac (ping 100% strat).
- `tools/drive_calib.py` (branch pawel/drive-calib, draft PR): kalibracja jazdy bez miarki - droga z glebi RealSense
@@ -142,8 +153,8 @@ Dwa tory rownolegle. Tor mapa + zygzak (frane/mapa-d435, `docs/MAPA.md`):
3. Poza ramienia "szukaj" (kamera 38 st w dol) i szyszki w zygzaku (detektor + podjazd z `brain.py`); merge frane/mapa-d435.
Tor ACT / ramie (master):
-A. (ACT) wagi `pretrained_model` na Pi, `python tools/act_pick.py --policy --skip-drop`
- z wylacznikiem w rece, potem bez `--skip-drop`.
+A. (ACT) review + merge pawel/panel-sloik, `deploy/push_to_pi.sh`, wagi 007000 na Pi w calosci (ubic wiszacy `tar`),
+ przycisk "Zbierz szyszke" z wylacznikiem w rece; najpierw sam `sloik` z panelu ramienia z szyszka w chwytaku.
0. (sesja przy Pi, rownolegle z ACT) `docs/ARM_FRAMES.md`: `tools/frame_check.py` z wylacznikiem, potem
`tools/hand_eye_calib.py collect/solve` z markerem ArUco -> `camera_on_arm.json` do repo.
0. Chwytanie samym ramieniem: czyste uczenie (1 szyszka naraz, jeden styl chwytu, 12 pozycji), `fit.py`, test `pick.py`
diff --git a/motions/drop_box.json b/motions/drop_box.json
index 2793347..3e979e2 100644
--- a/motions/drop_box.json
+++ b/motions/drop_box.json
@@ -1,5 +1,6 @@
{
"name": "drop_box",
+ "panel": false,
"note": "PLACEHOLDER, nie nagrane na sprzecie. Zalozenie: pojemnik po lewej stronie ramienia (shoulder_pan +90 od HOME_POSE, zakres pan wg DANCE_RANGE to ok. +-109). Po zamontowaniu pojemnika nagrac: tools/record_waypoints.py --name drop_box. Start z HOME_POSE z zacisnietym chwytakiem (po grasp_*), koniec w HOME_POSE.",
"waypoints": [
{
diff --git a/motions/grasp_mid.json b/motions/grasp_mid.json
index d8fc5b0..9b9c4d3 100644
--- a/motions/grasp_mid.json
+++ b/motions/grasp_mid.json
@@ -1,5 +1,6 @@
{
"name": "grasp_mid",
+ "panel": false,
"note": "Kluczowe klatki z legacy/arm_recordings/demo2_fixed.csv (reczne demo 2026-09-25, kalibracja po legacy/arm_recordings/fix_shoulder_offset.py). Chwyt 17 cm przed kamera przy pan -13.6 (szyszka ok. 6 cm w lewo od osi). Od zacisku gripper=0: pusty chwytak czyta ~2 (< empty_gripper_below 6). Nie sprawdzone jeszcze w pinecone_bot na sprzecie (interpolacja miedzy punktami zamiast gestej trajektorii).",
"waypoints": [
{
diff --git a/motions/sloik.json b/motions/sloik.json
new file mode 100644
index 0000000..e657757
--- /dev/null
+++ b/motions/sloik.json
@@ -0,0 +1,510 @@
+{
+ "name": "sloik",
+ "note": "Wrzut do sloika po chwycie ACT. Do otwarcia nad sloikiem: nagranie z robota 2026-09-27 12:40 (~/hackaton/motions/drop_box_oneway.json na Pi, 'po chwycie ACT -> sloik', przyciete od t8.40), chwytak w czasie przenoszenia = nagrany - 4 (docisk jak w tools/live_grasp/pick.py). Powrot: JAR_BACK z tools/live_grasp/pick.py (sloik w tym samym miejscu, podstawa ok. -20 st), koniec w HOME z motions/home.json.",
+ "waypoints": [
+ {
+ "label": "nagranie t8.40",
+ "pose": {
+ "shoulder_pan": -2.11,
+ "shoulder_lift": 28.62,
+ "elbow_flex": 44.48,
+ "wrist_flex": 0.57,
+ "wrist_roll": 88.53,
+ "gripper": 8.76
+ },
+ "seconds": 1.5
+ },
+ {
+ "label": "nagranie t8.70",
+ "pose": {
+ "shoulder_pan": -1.93,
+ "shoulder_lift": 28.18,
+ "elbow_flex": 44.48,
+ "wrist_flex": 0.57,
+ "wrist_roll": 88.53,
+ "gripper": 8.69
+ },
+ "seconds": 0.29999516199995924
+ },
+ {
+ "label": "nagranie t9.00",
+ "pose": {
+ "shoulder_pan": -1.93,
+ "shoulder_lift": 27.82,
+ "elbow_flex": 43.87,
+ "wrist_flex": 0.48,
+ "wrist_roll": 88.44,
+ "gripper": 8.69
+ },
+ "seconds": 0.299994765999827
+ },
+ {
+ "label": "nagranie t9.30",
+ "pose": {
+ "shoulder_pan": -1.93,
+ "shoulder_lift": 21.85,
+ "elbow_flex": 28.84,
+ "wrist_flex": 0.4,
+ "wrist_roll": 88.88,
+ "gripper": 8.82
+ },
+ "seconds": 0.29999913300002845
+ },
+ {
+ "label": "nagranie t9.60",
+ "pose": {
+ "shoulder_pan": -1.93,
+ "shoulder_lift": 10.42,
+ "elbow_flex": 2.11,
+ "wrist_flex": 0.31,
+ "wrist_roll": 88.7,
+ "gripper": 8.82
+ },
+ "seconds": 0.29999580000003334
+ },
+ {
+ "label": "nagranie t9.90",
+ "pose": {
+ "shoulder_pan": -1.93,
+ "shoulder_lift": 4.26,
+ "elbow_flex": -28.13,
+ "wrist_flex": 0.31,
+ "wrist_roll": 88.62,
+ "gripper": 8.82
+ },
+ "seconds": 0.29998959299996386
+ },
+ {
+ "label": "nagranie t10.20",
+ "pose": {
+ "shoulder_pan": -1.93,
+ "shoulder_lift": 2.59,
+ "elbow_flex": -58.9,
+ "wrist_flex": 0.31,
+ "wrist_roll": 88.53,
+ "gripper": 8.82
+ },
+ "seconds": 0.29999193800017565
+ },
+ {
+ "label": "nagranie t10.50",
+ "pose": {
+ "shoulder_pan": -2.02,
+ "shoulder_lift": 2.59,
+ "elbow_flex": -92.66,
+ "wrist_flex": 0.4,
+ "wrist_roll": 88.53,
+ "gripper": 8.82
+ },
+ "seconds": 0.29999484499990103
+ },
+ {
+ "label": "nagranie t10.80",
+ "pose": {
+ "shoulder_pan": -2.55,
+ "shoulder_lift": -15.52,
+ "elbow_flex": -97.85,
+ "wrist_flex": 0.48,
+ "wrist_roll": 88.97,
+ "gripper": 8.82
+ },
+ "seconds": 0.2999945310000385
+ },
+ {
+ "label": "nagranie t11.10",
+ "pose": {
+ "shoulder_pan": -2.73,
+ "shoulder_lift": -23.43,
+ "elbow_flex": -97.85,
+ "wrist_flex": 0.4,
+ "wrist_roll": 88.97,
+ "gripper": 8.82
+ },
+ "seconds": 0.2999953519999963
+ },
+ {
+ "label": "nagranie t11.40",
+ "pose": {
+ "shoulder_pan": -2.81,
+ "shoulder_lift": -26.24,
+ "elbow_flex": -98.02,
+ "wrist_flex": -7.08,
+ "wrist_roll": 89.05,
+ "gripper": 8.82
+ },
+ "seconds": 0.29999653699997
+ },
+ {
+ "label": "nagranie t11.70",
+ "pose": {
+ "shoulder_pan": -8.44,
+ "shoulder_lift": -27.47,
+ "elbow_flex": -97.85,
+ "wrist_flex": -34.77,
+ "wrist_roll": 88.62,
+ "gripper": 8.89
+ },
+ "seconds": 0.29999836999991203
+ },
+ {
+ "label": "nagranie t12.00",
+ "pose": {
+ "shoulder_pan": -15.38,
+ "shoulder_lift": -36.0,
+ "elbow_flex": -97.85,
+ "wrist_flex": -66.59,
+ "wrist_roll": 88.44,
+ "gripper": 8.89
+ },
+ "seconds": 0.29999667800007046
+ },
+ {
+ "label": "nagranie t12.30",
+ "pose": {
+ "shoulder_pan": -16.44,
+ "shoulder_lift": -49.8,
+ "elbow_flex": -97.93,
+ "wrist_flex": -81.54,
+ "wrist_roll": 88.53,
+ "gripper": 8.89
+ },
+ "seconds": 0.29999887700000727
+ },
+ {
+ "label": "nagranie t12.60",
+ "pose": {
+ "shoulder_pan": -20.84,
+ "shoulder_lift": -57.54,
+ "elbow_flex": -97.85,
+ "wrist_flex": -83.21,
+ "wrist_roll": 88.53,
+ "gripper": 8.89
+ },
+ "seconds": 0.2999846749999051
+ },
+ {
+ "label": "nagranie t12.90",
+ "pose": {
+ "shoulder_pan": -20.48,
+ "shoulder_lift": -57.63,
+ "elbow_flex": -97.85,
+ "wrist_flex": -86.46,
+ "wrist_roll": 88.79,
+ "gripper": 8.89
+ },
+ "seconds": 0.2999952080001549
+ },
+ {
+ "label": "nagranie t13.20",
+ "pose": {
+ "shoulder_pan": -20.48,
+ "shoulder_lift": -57.63,
+ "elbow_flex": -97.85,
+ "wrist_flex": -86.55,
+ "wrist_roll": 88.97,
+ "gripper": 9.28
+ },
+ "seconds": 0.2999939579999591
+ },
+ {
+ "label": "nagranie t13.50",
+ "pose": {
+ "shoulder_pan": -20.31,
+ "shoulder_lift": -57.63,
+ "elbow_flex": -97.76,
+ "wrist_flex": -86.29,
+ "wrist_roll": 89.32,
+ "gripper": 9.54
+ },
+ "seconds": 0.2999949770000967
+ },
+ {
+ "label": "nagranie t13.80",
+ "pose": {
+ "shoulder_pan": -20.31,
+ "shoulder_lift": -57.45,
+ "elbow_flex": -97.67,
+ "wrist_flex": -86.11,
+ "wrist_roll": 89.41,
+ "gripper": 33.69
+ },
+ "seconds": 0.29999349799982156
+ },
+ {
+ "label": "nagranie t14.10",
+ "pose": {
+ "shoulder_pan": -20.31,
+ "shoulder_lift": -57.71,
+ "elbow_flex": -97.76,
+ "wrist_flex": -86.37,
+ "wrist_roll": 89.14,
+ "gripper": 86.2
+ },
+ "seconds": 0.2999957600000016
+ },
+ {
+ "label": "nagranie t14.40",
+ "pose": {
+ "shoulder_pan": -20.22,
+ "shoulder_lift": -57.71,
+ "elbow_flex": -97.76,
+ "wrist_flex": -86.37,
+ "wrist_roll": 88.62,
+ "gripper": 97.34
+ },
+ "seconds": 0.2999932059999537
+ },
+ {
+ "label": "nagranie t14.70",
+ "pose": {
+ "shoulder_pan": -20.22,
+ "shoulder_lift": -66.77,
+ "elbow_flex": -97.76,
+ "wrist_flex": -86.02,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.3000027420000606
+ },
+ {
+ "label": "nagranie t15.00",
+ "pose": {
+ "shoulder_pan": -20.22,
+ "shoulder_lift": -69.76,
+ "elbow_flex": -97.67,
+ "wrist_flex": -86.29,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.2999954040001285
+ },
+ {
+ "label": "nagranie t15.30",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.2999874889999319
+ },
+ {
+ "label": "nagranie t15.60",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.29999371200005953
+ },
+ {
+ "label": "nagranie t15.90",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.29999652199990123
+ },
+ {
+ "label": "nagranie t16.20",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.29999417900012304
+ },
+ {
+ "label": "nagranie t16.50",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.29999625400000696
+ },
+ {
+ "label": "nagranie t16.80",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.2999931059998744
+ },
+ {
+ "label": "nagranie t17.10",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.2999967580001339
+ },
+ {
+ "label": "nagranie t17.40",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.2999949259999539
+ },
+ {
+ "label": "nagranie t17.70",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.2999949069999275
+ },
+ {
+ "label": "nagranie t18.00",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.29999724999993305
+ },
+ {
+ "label": "nagranie t18.30",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.29999320800015994
+ },
+ {
+ "label": "nagranie t18.60",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.30000103999987004
+ },
+ {
+ "label": "nagranie t18.90",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.29998948200000086
+ },
+ {
+ "label": "nagranie t19.00",
+ "pose": {
+ "shoulder_pan": -20.04,
+ "shoulder_lift": -73.19,
+ "elbow_flex": -97.67,
+ "wrist_flex": -85.85,
+ "wrist_roll": 88.62,
+ "gripper": 97.28
+ },
+ "seconds": 0.10000714400007382
+ },
+ {
+ "label": "odjazd od sloika",
+ "pose": {
+ "shoulder_pan": -21.8,
+ "shoulder_lift": -59.7,
+ "elbow_flex": -76.0,
+ "wrist_flex": -80.3,
+ "wrist_roll": 88.62,
+ "gripper": 60.0
+ },
+ "seconds": 1.5
+ },
+ {
+ "label": "powrot 1",
+ "pose": {
+ "shoulder_pan": -21.9,
+ "shoulder_lift": -45.1,
+ "elbow_flex": -33.3,
+ "wrist_flex": -80.3,
+ "wrist_roll": 88.62,
+ "gripper": 60.0
+ },
+ "seconds": 1.5
+ },
+ {
+ "label": "powrot 2",
+ "pose": {
+ "shoulder_pan": -21.8,
+ "shoulder_lift": -29.8,
+ "elbow_flex": -3.1,
+ "wrist_flex": -80.3,
+ "wrist_roll": 88.62,
+ "gripper": 60.0
+ },
+ "seconds": 1.5
+ },
+ {
+ "label": "powrot 3",
+ "pose": {
+ "shoulder_pan": -13.2,
+ "shoulder_lift": 60.1,
+ "elbow_flex": 17.8,
+ "wrist_flex": -80.2,
+ "wrist_roll": 88.62,
+ "gripper": 60.0
+ },
+ "seconds": 3.0
+ },
+ {
+ "label": "home",
+ "pose": {
+ "shoulder_pan": -5.45,
+ "shoulder_lift": 88.92,
+ "elbow_flex": 7.56,
+ "wrist_flex": -87.87,
+ "wrist_roll": 88.88,
+ "gripper": 41.06
+ },
+ "seconds": 2.5
+ }
+ ]
+}
diff --git a/pinecone_bot/arm_panel.py b/pinecone_bot/arm_panel.py
index fcdf1dc..84f17d7 100644
--- a/pinecone_bot/arm_panel.py
+++ b/pinecone_bot/arm_panel.py
@@ -37,8 +37,8 @@
bez HOME przy starcie, "home" odrzucane, jog i chwytak od razu, liczone od
odczytanej pozycji. Ruchy z motions/ sa dozwolone (nagrywa sie je z panelu pod
biezacy montaz), ale przy pustym chwycie ramie NIE wraca do HOME (home_on_empty=False).
-UWAGA: stare ruchy (grasp_mid, home, drop_box) koncza w HOME_POSE - w tym trybie
-odtwarzaj tylko ruchy nagrane pod aktualny montaz.
+Stare ruchy (grasp_mid pod poprzedni HOME, drop_box = placeholder) maja w pliku "panel": false:
+panel ich nie pokazuje i nie odtwarza (panel_motions). Wrzut do sloika: motions/sloik.json.
Jog liczy cel od OSTATNIEJ WYSLANEJ komendy (setpoint), a nie od odczytu -
dzieki temu komenda nie robi sync_read. HOME i ruchy z motions/ odtwarza
@@ -47,6 +47,7 @@
from __future__ import annotations
import collections
+import json
import logging
import os
import re
@@ -152,6 +153,24 @@ def list_motions(motions_dir: str) -> list:
return sorted(f[:-5] for f in os.listdir(path) if f.endswith(".json"))
+def panel_motions(motions_dir: str) -> list:
+ """Ruchy do przyciskow panelu: bez tych z "panel": false (stare, pod poprzedni montaz albo placeholder).
+
+ Kod robota (brain.py, arm_play.py) dalej je widzi przez load_motion; panel ich nie pokazuje i nie odtwarza.
+ """
+ path = resolve_motions_dir(motions_dir)
+ names = []
+ for name in list_motions(motions_dir):
+ try:
+ with open(os.path.join(path, name + ".json"), encoding="utf-8") as fh:
+ hidden = json.load(fh).get("panel", True) is False
+ except (OSError, ValueError, AttributeError):
+ hidden = False # zly plik zglosi sie przy odtwarzaniu, tak jak dotad
+ if not hidden:
+ names.append(name)
+ return names
+
+
# ---------------------------------------------------------------------------
# ramie z zapamietana ostatnia komenda
# ---------------------------------------------------------------------------
@@ -282,7 +301,7 @@ def submit(self, data: dict) -> tuple:
data = {"cmd": "jog", "joint": joint, "step": step}
if cmd == "motion":
name = data.get("name")
- if name not in list_motions(self.cfg.arm.motions_dir):
+ if name not in panel_motions(self.cfg.arm.motions_dir):
return False, f"brak ruchu '{name}' w motions/"
data = {"cmd": "motion", "name": name}
if self.manual_only and cmd == "home":
@@ -432,7 +451,7 @@ def snapshot(self) -> dict:
"error": self.last_error,
"result": self.last_result,
"read_age_s": None if self.last_read_t is None else round(self._clock() - self.last_read_t, 1),
- "motions": list_motions(self.cfg.arm.motions_dir),
+ "motions": panel_motions(self.cfg.arm.motions_dir),
"joints": JOINT_NAMES,
"steps": list(STEP_CHOICES),
"xyz_steps": list(XYZ_STEPS_MM) if self.kin is not None else [],
diff --git a/pinecone_bot/supervisor.py b/pinecone_bot/supervisor.py
index ae38105..c2c6e77 100644
--- a/pinecone_bot/supervisor.py
+++ b/pinecone_bot/supervisor.py
@@ -38,6 +38,8 @@ class ServiceSpec:
env: dict = field(default_factory=dict)
link: int | None = None # port strony uslugi do otwarcia w nowej karcie
note: str = ""
+ oneshot: bool = False # zadanie, ktore samo sie konczy (kod 0 = sukces, nie awaria); bez autostartu
+ takes: tuple = () # uslugi zatrzymywane na czas zadania i wznawiane po nim (port ramienia, kamera)
def port_open(port: int | None, host: str = "127.0.0.1", timeout: float = 0.3) -> bool:
@@ -115,6 +117,10 @@ def start(self) -> tuple[bool, str]:
kwargs = {}
if os.name == "nt":
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
+ elif self.spec.oneshot:
+ # zadanie odpala dzieci (act_pick -> lerobot-rollout, arm_play): STOP musi trafic w cala grupe,
+ # inaczej rollout jedzie ramieniem dalej po zabiciu rodzica
+ kwargs["start_new_session"] = True
try:
self.proc = subprocess.Popen(
self.spec.argv, cwd=self.cwd, env=env, stdin=subprocess.DEVNULL,
@@ -142,6 +148,8 @@ def _pump(self, proc: subprocess.Popen) -> None:
self._append(f"=== koniec, kod {code}")
if self._stopping:
self.events.add("stop", f"{self.spec.title}: zatrzymany")
+ elif self.spec.oneshot and code == 0:
+ self.events.add("stop", f"{self.spec.title}: zakonczone")
else:
self.events.add("error", f"{self.spec.title}: proces sie zakonczyl (kod {code}) - zajrzyj w log")
@@ -150,21 +158,47 @@ def stop(self, timeout: float = 4.0) -> tuple[bool, str]:
if proc is None or proc.poll() is not None:
return False, f"{self.spec.title}: nie chodzi z panelu"
self._stopping = True
+ group = self.spec.oneshot and os.name != "nt"
try:
if os.name == "nt":
proc.send_signal(signal.CTRL_BREAK_EVENT)
+ elif group:
+ os.killpg(proc.pid, signal.SIGINT)
else:
proc.send_signal(signal.SIGINT)
proc.wait(timeout=timeout)
except (subprocess.TimeoutExpired, OSError, ValueError):
- proc.terminate()
+ self._kill(proc, signal.SIGTERM if group else None)
try:
proc.wait(timeout=2.0)
except subprocess.TimeoutExpired:
- proc.kill()
+ self._kill(proc, signal.SIGKILL if group else None, hard=True)
proc.wait(timeout=2.0)
+ if group:
+ # dzieci moga jeszcze sprzatac po SIGINT (lerobot disconnect); potem SIGKILL dla maruderow
+ deadline = time.monotonic() + timeout
+ try:
+ while time.monotonic() < deadline:
+ os.killpg(proc.pid, 0) # grupa = pid rodzica; OSError = nikt juz nie zyje
+ time.sleep(0.1)
+ os.killpg(proc.pid, signal.SIGKILL)
+ except OSError:
+ pass
return True, f"{self.spec.title}: zatrzymany"
+ @staticmethod
+ def _kill(proc: subprocess.Popen, group_sig=None, hard: bool = False) -> None:
+ if group_sig is not None:
+ try:
+ os.killpg(proc.pid, group_sig)
+ return
+ except OSError:
+ pass
+ if hard:
+ proc.kill()
+ else:
+ proc.terminate()
+
def restart(self) -> tuple[bool, str]:
if self.running():
self.stop()
diff --git a/robot_panel.html b/robot_panel.html
index 2ac1426..f9d503c 100644
--- a/robot_panel.html
+++ b/robot_panel.html
@@ -361,6 +361,15 @@
Szyszki w kadrze ostatnia minuta
Ramię SO-101
+
+
+
+
+ Podgląd chwytu
+ model ACT chwyta, potem ruch „sloik” wrzuca do słoika
+
+
Na czas chwytu panel wyłącza sterowanie ramieniem i kamerę, po końcu włącza je sam. Wyłącznik w ręce, szyszka przed chwytakiem, słoik na swoim miejscu.
if (hubOk === false) addEvent(now(), 'Panel na Pi: połączenie wróciło');
hubOk = true;
for (const e of s.events) { addEvent(e.t, e.text, e.kind); lastEv = Math.max(lastEv, e.id); }
- if (!SHOW) { renderSvc(s.services); renderHealth(s.health); }
+ if (!SHOW) { renderSvc(s.services); renderHealth(s.health); renderPick(s.services); }
} catch (e) {
setChip('chip-hub', 'bad', 'panel na Pi nie odpowiada');
if (hubOk) addEvent(now(), 'Panel na Pi: brak połączenia', 'error');
@@ -767,6 +777,34 @@
Co zbudowaliśmy
}
setInterval(pollHub, 1500); pollHub();
+ // ---- zbieranie: chwyt ACT + sloik (zadanie "pick" w tools/robot_panel.py) ----
+ let pickOn = false;
+ function renderPick(list) {
+ const p = list.find((s) => s.name === 'pick');
+ $('pick').style.display = p ? '' : 'none';
+ if (!p) return;
+ pickOn = p.state === 'running' || p.state === 'starting';
+ $('pick-go').disabled = pickOn;
+ $('pick-stop').disabled = !pickOn;
+ $('pick-cam').style.display = pickOn && p.link ? '' : 'none';
+ if (p.link) $('pick-cam').href = `http://${HOST}:${p.link}/`;
+ $('pick-state').textContent = pickOn ? `zbieranie trwa ${fmtUp(p.uptime_s) || '< 1 min'} - STOP albo Przerwij zatrzymuje ramię`
+ : p.state === 'crashed' ? `ostatnie zbieranie przerwane (kod ${p.exit_code}) - log: Usługi, Zbieranie ACT`
+ : 'model ACT chwyta, potem ruch „sloik” wrzuca do słoika';
+ }
+ async function pickAct(act) {
+ try {
+ const r = await fetch(`/api/svc/pick/${act}`, { method: 'POST' });
+ const j = await r.json();
+ addEvent(now(), j.msg, j.ok ? undefined : 'error');
+ } catch (err) { addEvent(now(), 'Panel na Pi nie odpowiada', 'error'); }
+ pollHub();
+ }
+ $('pick-go').onclick = () => {
+ if (confirm('Zbierz szyszkę: model ACT rusza ramieniem, potem wrzut do słoika. Wyłącznik w ręce?')) pickAct('start');
+ };
+ $('pick-stop').onclick = () => pickAct('stop');
+
// ---- logi ------------------------------------------------------------
const logEl = $('log');
async function refreshLog() {
diff --git a/tests/test_act_pick.py b/tests/test_act_pick.py
index 73b5fe8..930ec16 100644
--- a/tests/test_act_pick.py
+++ b/tests/test_act_pick.py
@@ -37,7 +37,7 @@ def test_with_preview_keeps_lerobot_args_and_can_be_disabled():
def test_drop_goes_home_after():
cmd = drop_cmd(port="/dev/x")
- assert cmd[1:] == ["tools/arm_play.py", "--motion", "drop_box", "--port", "/dev/x", "--home-after"]
+ assert cmd[1:] == ["tools/arm_play.py", "--motion", "sloik", "--port", "/dev/x", "--home-after"]
def test_main_runs_stages_in_order_and_repeats():
diff --git a/tests/test_arm_panel.py b/tests/test_arm_panel.py
index 8a9cdb1..4c86480 100644
--- a/tests/test_arm_panel.py
+++ b/tests/test_arm_panel.py
@@ -24,6 +24,7 @@
jog_target,
limits_from_calibration,
list_motions,
+ panel_motions,
)
from pinecone_bot.config import Config # noqa: E402
@@ -211,12 +212,12 @@ def test_open_and_close_gripper():
def test_motion_list_and_replay():
names = list_motions(MOTIONS_DIR)
- assert {"home", "grasp_mid", "drop_box"} <= set(names)
+ assert {"home", "grasp_mid", "drop_box", "sloik"} <= set(names)
panel, arm, _ = make_panel()
- assert panel.submit({"cmd": "motion", "name": "drop_box"})[0]
+ assert panel.submit({"cmd": "motion", "name": "sloik"})[0]
panel.process_one()
assert panel.last_error is None, panel.last_error
- assert "drop_box" in panel.last_result
+ assert "sloik" in panel.last_result
assert not panel.submit({"cmd": "motion", "name": "../pinecone_config"})[0]
@@ -247,7 +248,7 @@ def press_stop():
def test_stop_interrupts_waypoint_replay():
panel, arm, clock = make_panel()
- panel.submit({"cmd": "motion", "name": "grasp_mid"})
+ panel.submit({"cmd": "motion", "name": "sloik"})
ticks = {"n": 0}
def press_stop():
@@ -341,7 +342,7 @@ def test_manual_only_never_homes_and_jogs_from_read():
ok, msg = panel.submit({"cmd": "home"})
assert not ok and "no-home" in msg
# ruchy z motions/ dozwolone (nagrywane z panelu pod biezacy montaz), ale bez powrotu do HOME
- assert panel.submit({"cmd": "motion", "name": "grasp_mid"})[0]
+ assert panel.submit({"cmd": "motion", "name": "sloik"})[0]
panel.stop()
assert panel.submit({"cmd": "jog", "joint": "shoulder_lift", "step": -5})[0]
panel.process_one()
@@ -387,9 +388,17 @@ def test_jog_blocked_when_joint_outside_calibration_range():
assert all(set(a) == {"elbow_flex.pos"} for a in arm.actions)
-def test_manual_only_motion_empty_gripper_does_not_go_home():
+def test_manual_only_motion_empty_gripper_does_not_go_home(tmp_path):
+ # ruch z chwytem (check_gripper): grasp_mid jest w panelu schowany ("panel": false), wiec kopia bez flagi
+ import json
+
+ for name in ("grasp_mid", "home"):
+ with open(os.path.join(MOTIONS_DIR, name + ".json"), encoding="utf-8") as fh:
+ data = json.load(fh)
+ data.pop("panel", None)
+ (tmp_path / (name + ".json")).write_text(json.dumps(data))
cfg = Config()
- cfg.arm.motions_dir = MOTIONS_DIR
+ cfg.arm.motions_dir = str(tmp_path)
clock = FakeClock()
arm = FakeSO101() # serwa sledza komendy idealnie: po zacisku odczyt 0 < 6 = pusty chwytak
limits = limits_from_calibration(FAKE_CALIBRATION, FAKE_NORM_MODES)
@@ -509,3 +518,35 @@ def test_default_speed_is_halved():
assert all(DEFAULT_MAX_STEP[j] == 1.0 for j in JOINT_NAMES if j != "gripper")
assert DEFAULT_MAX_STEP["gripper"] == 2.0
+
+
+def test_panel_hides_outdated_motions():
+ # grasp_mid (poprzedni HOME) i drop_box (placeholder) maja "panel": false: kod robota je widzi, panel nie
+ shown = panel_motions(MOTIONS_DIR)
+ assert "sloik" in shown and "home" in shown
+ assert "drop_box" not in shown and "grasp_mid" not in shown
+ panel, _, _ = make_panel()
+ assert not panel.submit({"cmd": "motion", "name": "drop_box"})[0]
+ assert panel.snapshot()["motions"] == shown
+
+
+def test_panel_motions_tolerates_broken_file(tmp_path):
+ (tmp_path / "ok.json").write_text('{"waypoints": []}')
+ (tmp_path / "zly.json").write_text("{nie json")
+ (tmp_path / "stary.json").write_text('{"panel": false, "waypoints": []}')
+ assert panel_motions(str(tmp_path)) == ["ok", "zly"]
+
+
+def test_sloik_motion_stays_in_base_range_and_ends_home():
+ from pinecone_bot.arm import load_motion
+
+ m = load_motion(MOTIONS_DIR, "sloik")
+ # podstawa ma w EEPROM zakres tylko +-23 st (docs/LIVE_GRASP.md)
+ assert all(abs(wp.pose["shoulder_pan"]) <= 23.0 for wp in m.waypoints)
+ last = m.waypoints[-1].pose
+ assert all(last[j] == pytest.approx(HOME_POSE[j]) for j in JOINT_NAMES if j != "gripper")
+ assert any(wp.pose["gripper"] >= 90 for wp in m.waypoints), "brak otwarcia chwytaka nad sloikiem"
+ opening = next(i for i, wp in enumerate(m.waypoints) if wp.pose["gripper"] > 20)
+ assert opening > 0 and all(wp.pose["gripper"] < 10 for wp in m.waypoints[:opening]) # szyszka trzymana do sloika
+ over_jar = m.waypoints[opening].pose
+ assert over_jar["shoulder_pan"] < -15 # sloik przy podstawie ok. -20 st, jak przy nagraniu
diff --git a/tests/test_robot_panel.py b/tests/test_robot_panel.py
index 715afe7..a3fe320 100644
--- a/tests/test_robot_panel.py
+++ b/tests/test_robot_panel.py
@@ -12,6 +12,8 @@
import zipfile
from http.server import ThreadingHTTPServer
+import pytest
+
from pinecone_bot.supervisor import (
EventLog,
Service,
@@ -194,3 +196,108 @@ def post(path, origin=None):
httpd.shutdown()
if s.running():
s.stop()
+
+
+# --- zadanie "pick": chwyt ACT + sloik, zabiera ramie i kamere --------------
+
+QUICK = "import time\nprint('zadanie', flush=True)\ntime.sleep(0.3)\n"
+
+
+def named(tmp_path, name, argv, events, port=None, probe=None, **spec_kw):
+ spec = ServiceSpec(name, name.title(), argv, port=port, **spec_kw)
+ kwargs = {"probe": probe} if probe else {}
+ return Service(spec, str(tmp_path), str(tmp_path / "logs"), events, **kwargs)
+
+
+def test_specs_pick_job():
+ import robot_panel
+
+ demo = {s.name: s for s in robot_panel.build_specs(demo=True, python="py", policy="/m/act")}
+ pick = demo["pick"]
+ assert pick.oneshot and pick.takes == ("arm", "vision")
+ assert pick.argv[:5] == ["py", "-u", "tools/act_pick.py", "--policy", "/m/act"]
+ assert "--dry-run" in pick.argv
+ real = {s.name: s for s in robot_panel.build_specs(demo=False, python="py")}
+ assert "--dry-run" not in real["pick"].argv
+ assert real["pick"].argv[4].endswith("act_so101_grasp2/007000/pretrained_model")
+ assert not any(s.oneshot for n, s in real.items() if n != "pick")
+
+
+def test_job_stops_taken_services_and_restores_them(tmp_path):
+ import robot_panel
+
+ ev = EventLog()
+ arm = named(tmp_path, "arm", [sys.executable, "-u", "-c", CHILD], ev)
+ vision = named(tmp_path, "vision", [sys.executable, "-u", "-c", CHILD], ev)
+ job = named(tmp_path, "pick", [sys.executable, "-u", "-c", QUICK], ev, oneshot=True, takes=("arm", "vision"))
+ hub = robot_panel.Hub([arm, vision, job], ev, repo=REPO_ROOT)
+ try:
+ assert arm.start()[0] and vision.start()[0]
+ code, body = hub.action("pick", "start")
+ assert code == 200 and body["ok"]
+ assert not arm.running() and not vision.running()
+ assert wait_for(lambda: not job.running())
+ assert wait_for(lambda: arm.running() and vision.running())
+ assert any("zakonczone" in e["text"] for e in ev.since(0))
+ assert job.status()["state"] == "stopped"
+ finally:
+ for s in (arm, vision, job):
+ if s.running():
+ s.stop()
+
+
+def test_job_refuses_when_taken_service_runs_outside_panel(tmp_path):
+ import robot_panel
+
+ ev = EventLog()
+ arm = named(tmp_path, "arm", [sys.executable, "-c", "pass"], ev, port=8010, probe=lambda port: True)
+ job = named(tmp_path, "pick", [sys.executable, "-u", "-c", QUICK], ev, oneshot=True, takes=("arm",))
+ hub = robot_panel.Hub([arm, job], ev, repo=REPO_ROOT)
+ code, body = hub.action("pick", "start")
+ assert code == 409 and "poza panelem" in body["msg"]
+ assert not job.running()
+
+
+def test_stop_all_during_job_does_not_restore(tmp_path):
+ import robot_panel
+
+ ev = EventLog()
+ arm = named(tmp_path, "arm", [sys.executable, "-u", "-c", CHILD], ev)
+ job = named(tmp_path, "pick", [sys.executable, "-u", "-c", CHILD], ev, oneshot=True, takes=("arm",))
+ hub = robot_panel.Hub([arm, job], ev, repo=REPO_ROOT)
+ try:
+ assert arm.start()[0]
+ assert hub.action("pick", "start")[1]["ok"]
+ hub.stop_all()
+ assert not job.running()
+ time.sleep(0.5)
+ assert not arm.running()
+ finally:
+ for s in (arm, job):
+ if s.running():
+ s.stop()
+
+
+@pytest.mark.skipif(os.name == "nt", reason="grupy procesow tylko na Linuksie (Pi)")
+def test_oneshot_stop_kills_grandchildren(tmp_path):
+ # act_pick uruchamia lerobot-rollout jako dziecko: STOP zadania musi zabic tez wnuka
+ pidfile = tmp_path / "wnuk.pid"
+ parent = ("import subprocess, sys, time\n"
+ f"p = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(60)'])\n"
+ f"open({str(pidfile)!r}, 'w').write(str(p.pid))\n"
+ "print('start', flush=True)\n"
+ "p.wait()\n")
+ job = named(tmp_path, "pick", [sys.executable, "-u", "-c", parent], EventLog(), oneshot=True)
+ assert job.start()[0]
+ assert wait_for(lambda: pidfile.exists() and pidfile.read_text())
+ grandchild = int(pidfile.read_text())
+ job.stop(timeout=2.0)
+
+ def gone():
+ try:
+ os.kill(grandchild, 0)
+ except OSError:
+ return True
+ return False
+
+ assert wait_for(gone)
diff --git a/tools/act_pick.py b/tools/act_pick.py
index 5bf330d..e5c2be6 100644
--- a/tools/act_pick.py
+++ b/tools/act_pick.py
@@ -3,8 +3,9 @@
Etap 1 (uczony): `lerobot-rollout` z wytrenowanym ACT przez --grasp-s sekund. ACT byl uczony
tylko na chwycie: z HOME do szyszki, zamkniecie chwytaka, lekkie uniesienie
(dataset local/so101_grasp, docs/POLICIES_LEROBOT.md).
-Etap 2 (deterministyczny): `tools/arm_play.py --motion drop_box --home-after` - nad sloik,
-otwarcie, powrot tym samym torem, HOME. Sloik stoi na robocie, wiec tego nie trzeba uczyc.
+Etap 2 (deterministyczny): `tools/arm_play.py --motion sloik --home-after` - nad sloik,
+otwarcie, powrot, HOME (sciezka nagrana reka, motions/sloik.json). Sloik stoi na robocie, wiec tego
+nie trzeba uczyc. Z panelu: przycisk "Zbierz szyszke" w tools/robot_panel.py.
Uzycie (na Pi, z katalogu repo; nic innego nie moze trzymac portu ramienia):
python tools/act_pick.py --policy ~/models/act_grasp/pretrained_model --dry-run # tylko komendy
@@ -69,7 +70,7 @@ def with_preview(cmd: list, preview_port: int, python: str = ".venv/bin/python")
return [python, "tools/cam_preview.py", f"--preview-port={preview_port}", entry] + cmd[1:]
-def drop_cmd(port: str = "/dev/robot-arm", motion: str = "drop_box", python: str = ".venv/bin/python") -> list:
+def drop_cmd(port: str = "/dev/robot-arm", motion: str = "sloik", python: str = ".venv/bin/python") -> list:
"""Komenda etapu 2: nagrany ruch do sloika i HOME na koniec."""
return [python, "tools/arm_play.py", "--motion", motion, "--port", port, "--home-after"]
@@ -91,7 +92,7 @@ def main(argv=None, run=subprocess.run) -> int:
parser.add_argument("--grasp-s", type=float, default=15.0, help="czas etapu 1 [s] (jak episode_time_s)")
parser.add_argument("--port", default=os.environ.get("ROBOT_ARM_PORT", "/dev/robot-arm"))
parser.add_argument("--fps", type=int, default=30)
- parser.add_argument("--motion", default="drop_box", help="ruch etapu 2 (motions/.json)")
+ parser.add_argument("--motion", default="sloik", help="ruch etapu 2 (motions/.json)")
parser.add_argument("--repeat", type=int, default=1)
parser.add_argument("--skip-drop", action="store_true", help="tylko etap 1")
parser.add_argument("--preview-port", type=int, default=8081,
diff --git a/tools/robot_panel.py b/tools/robot_panel.py
index eb1f9d6..b5b9236 100644
--- a/tools/robot_panel.py
+++ b/tools/robot_panel.py
@@ -14,6 +14,9 @@
jazda web_control.py :8000 strona, :8765 WebSocket (port Xiao z ROBOT_DRIVE_PORT)
ramie tools/arm_web.py :8010 (domyslnie --no-home: kamera siedzi na ramieniu)
kamera tools/vision_web.py :8020 (RealSense; kamere moze trzymac tylko jeden proces)
+ zbieranie tools/act_pick.py zadanie: chwyt polityka ACT + wrzut do sloika (motions/sloik.json).
+ Na czas zadania panel zatrzymuje ramie i kamere (port ramienia i RealSense na wylacznosc),
+ po koncu wznawia je sam. Podglad chwytu: :8081. Wagi: --policy albo ROBOT_ACT_POLICY.
Usluga odpalona recznie w SSH jest widoczna jako "poza panelem" i panel z niej korzysta.
Logi: logs/.log, paczka ZIP pod /logs.zip (razem z pinecone_log.csv).
"""
@@ -50,10 +53,11 @@
"/arm_panel.js": ("arm_panel.js", "text/javascript; charset=utf-8"),
}
EXTRA_LOGS = [os.path.join(REPO_ROOT, "pinecone_log.csv")]
+DEFAULT_POLICY = "~/models/act_so101_grasp2/007000/pretrained_model"
def build_specs(demo: bool = False, arm_home: bool = False, vision_source: str | None = None,
- python: str = sys.executable) -> list[ServiceSpec]:
+ python: str = sys.executable, policy: str | None = None) -> list[ServiceSpec]:
linux = sys.platform.startswith("linux")
drive_env = {}
if "ROBOT_DRIVE_PORT" not in os.environ and linux:
@@ -77,6 +81,14 @@ def build_specs(demo: bool = False, arm_home: bool = False, vision_source: str |
ServiceSpec("vision", "Kamera", vision_argv, port=8020, link=8020,
note=f"detekcja HSV, zrodlo: {source or 'RealSense'}"),
]
+ policy = os.path.expanduser(policy or os.environ.get("ROBOT_ACT_POLICY") or DEFAULT_POLICY)
+ pick_argv = [python, "-u", "tools/act_pick.py", "--policy", policy]
+ if demo:
+ pick_argv.append("--dry-run")
+ specs.append(ServiceSpec("pick", "Zbieranie ACT", pick_argv, link=8081, oneshot=True,
+ takes=("arm", "vision"),
+ note="chwyt ACT + sloik; zatrzymuje ramie i kamere, po koncu wznawia"
+ + (" (demo: tylko komendy)" if demo else "")))
if os.path.exists(os.path.join(REPO_ROOT, "tools", "estop_server.py")):
specs.append(ServiceSpec("estop", "E-stop", [python, "-u", "tools/estop_server.py"], port=8001,
link=8001, note="STOP z telefonu"))
@@ -97,6 +109,8 @@ def __init__(self, services: list[Service], events: EventLog, repo: str = REPO_R
self.repo = repo
self.started = time.time()
self._facts: dict | None = None
+ self._lock = threading.Lock()
+ self._restore: dict[str, list[str]] = {} # zadanie -> uslugi do wznowienia po nim
def facts(self) -> dict:
if self._facts is None:
@@ -119,11 +133,57 @@ def action(self, name: str, act: str) -> tuple[int, dict]:
svc = self.services.get(name)
if svc is None or act not in ("start", "stop", "restart"):
return 404, {"ok": False, "msg": "nie ma takiej uslugi albo akcji"}
- ok, msg = getattr(svc, act)()
+ if svc.spec.takes and act in ("start", "restart"):
+ ok, msg = self.start_job(svc)
+ else:
+ ok, msg = getattr(svc, act)()
return (200 if ok else 409), {"ok": ok, "msg": msg}
+ def start_job(self, job: Service) -> tuple[bool, str]:
+ """Zadanie z takes: zatrzymaj zabierane uslugi, uruchom zadanie, po jego koncu wznow je."""
+ with self._lock:
+ if job.running():
+ return False, f"{job.spec.title}: juz chodzi"
+ taken = [self.services[n] for n in job.spec.takes if n in self.services]
+ outside = [s.spec.title for s in taken if s.status()["state"] == "external"]
+ if outside:
+ return False, (f"{job.spec.title}: {', '.join(outside)} chodzi poza panelem (okno SSH?) - "
+ "zamknij tam, bo zadanie potrzebuje portu ramienia i kamery")
+ stopped = []
+ for s in taken:
+ if s.running():
+ s.stop()
+ stopped.append(s.spec.name)
+ ok, msg = job.start()
+ if not ok:
+ for name in stopped:
+ self.services[name].start()
+ return False, msg
+ self._restore[job.spec.name] = stopped
+ proc = job.proc
+ if stopped:
+ self.events.add("stop", f"{job.spec.title}: zatrzymane na czas zadania: "
+ + ", ".join(self.services[n].spec.title for n in stopped))
+ threading.Thread(target=self._after_job, args=(job, proc), daemon=True,
+ name=f"job-{job.spec.name}").start()
+ return True, msg
+
+ def _after_job(self, job: Service, proc) -> None:
+ proc.wait()
+ with self._lock:
+ names = self._restore.pop(job.spec.name, [])
+ for name in names:
+ svc = self.services[name]
+ for _ in range(30): # port chwile zwalnia sie po wyjsciu procesu
+ if not svc._probe(svc.spec.port):
+ break
+ time.sleep(0.1)
+ svc.start()
+
def stop_all(self) -> list[str]:
msgs = []
+ with self._lock:
+ self._restore.clear() # STOP wszystkiego: po zadaniu niczego nie wznawiamy
for s in self.services.values():
if s.running():
msgs.append(s.stop()[1])
@@ -214,16 +274,20 @@ def main() -> int:
parser.add_argument("--demo", action="store_true", help="bez sprzetu: ramie --fake, kamera --source sim")
parser.add_argument("--arm-home", action="store_true", help="ramie z HOME przy starcie (domyslnie --no-home)")
parser.add_argument("--vision-source", default=None, help="zrodlo kamery dla tools/vision_web.py --source")
+ parser.add_argument("--policy", default=None,
+ help=f"wagi ACT dla przycisku Zbierz (domyslnie ROBOT_ACT_POLICY albo {DEFAULT_POLICY})")
args = parser.parse_args()
events = EventLog()
services = [Service(spec, REPO_ROOT, LOG_DIR, events)
- for spec in build_specs(args.demo, args.arm_home, args.vision_source)]
+ for spec in build_specs(args.demo, args.arm_home, args.vision_source, policy=args.policy)]
hub = Hub(services, events)
threading.Thread(target=hub.facts, daemon=True).start() # git i liczenie linii w tle
events.add("start", "panel wystartowal" + (" (demo)" if args.demo else ""))
if args.autostart:
for s in services:
+ if s.spec.oneshot:
+ continue # zadania (zbieranie) tylko z przycisku
ok, msg = s.start()
print(msg, flush=True)