diff --git a/arm_panel.js b/arm_panel.js index a0fb624..d6e4991 100644 --- a/arm_panel.js +++ b/arm_panel.js @@ -125,7 +125,7 @@
RUCHY Z motions/
- +
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

+
+
+ + + + 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.
+
Sterowanie ramieniem
@@ -690,6 +699,7 @@

Co zbudowaliśmy

send({ type: 'set_mode', mode: 'manual' }); send({ type: 'keys', w: false, a: false, s: false, d: false }); if (armPanel) armPanel.stop(); + if (pickOn) fetch('/api/svc/pick/stop', { method: 'POST' }).catch(() => {}); addEvent(now(), 'STOP z panelu: jazda i ramię'); } $('stop').onclick = stopAll; @@ -758,7 +768,7 @@

Co zbudowaliśmy

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)