From 2e412a9a6ab27622b56ed693cc10aa41802b728d Mon Sep 17 00:00:00 2001 From: MengFengWu Date: Sun, 22 Mar 2026 18:36:44 +0800 Subject: [PATCH 1/3] add seek --- .gitignore | 2 +- command-center/src/api/__init__.py | 11 +++++ command-center/src/handlers/control.py | 58 ++++++++++++++++++++++++ command-center/src/lps_ctrl/bt_sender.py | 13 +++++- command-center/src/screens/control.py | 3 ++ 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 5daa12c3..ac2188dd 100644 --- a/.gitignore +++ b/.gitignore @@ -159,7 +159,7 @@ target/ utils/createUsers.js # Local lighttable data -lighttable +lighttable* # large music files *mp3 diff --git a/command-center/src/api/__init__.py b/command-center/src/api/__init__.py index 56ef1e40..f2de7812 100644 --- a/command-center/src/api/__init__.py +++ b/command-center/src/api/__init__.py @@ -63,6 +63,17 @@ def download(self, dancers): ) frame_result_value = requests.post(f"{HTTPS_URL}/frameDat", json=payload) + if ( + control_result_value.status_code != 200 + or frame_result_value.status_code != 200 + ): + self.app_ref.control_table.update_cell( + DANCER_LIST[dancer_id][0], + "Response", + f"Failed to save file locally. Status code: {control_result_value.status_code}, {frame_result_value.status_code}.", + ) + continue + files_to_save = [ (f"control_{dancer_id-1}.dat", control_result_value.content), (f"frame_{dancer_id-1}.dat", frame_result_value.content), diff --git a/command-center/src/handlers/control.py b/command-center/src/handlers/control.py index d4b4591d..57be4a0c 100644 --- a/command-center/src/handlers/control.py +++ b/command-center/src/handlers/control.py @@ -11,12 +11,37 @@ START_MUSIC_EVENT = pygame.USEREVENT + 1 music_timer = None play_cmd = None +time_stamp = None def play_music(): pygame.mixer.music.play() +def revive(screen_ref, sender, target_ids): + try: + response = sender.send_burst( + cmd_input="PLAY", + delay_sec=1.0, + prep_led_sec=0.0, + target_ids=target_ids, + data=[0, 0, 0], + # retries=3, + ) + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) + # screen_ref.notify("Seek") + except: + screen_ref.notify("Seek failed", severity="error") + + def control_handler( id: str, selected_dancers: list[str], screen_ref: ControlScreenType, sender ): # TODO @@ -24,7 +49,9 @@ def control_handler( screen_vars: ControlScreenParamsType = screen_ref.local_vars global music_timer global play_cmd + global time_stamp if id == "control-play": + time_stamp = time.time_ns() // 1000000 + screen_vars.delay * 1000 music_timer = threading.Timer(screen_vars.delay, play_music) music_timer.start() response = sender.send_burst( @@ -68,6 +95,7 @@ def control_handler( severity="error", ) elif id == "control-stop": + time_stamp = None try: if music_timer: music_timer.cancel() @@ -379,5 +407,35 @@ def control_handler( } ) screen_ref.notify("Forced Restart") + elif id == "control-seek": + now = time.time_ns() // 1000000 + 7000 + if time_stamp is None or now < time_stamp: + screen_ref.notify(f"Invalid timestamp", severity="error") + try: + screen_ref.notify(f"Seek will play at {(now-time_stamp)/1000} s") + reviver = threading.Timer( + 6, + revive, + args=[ + screen_ref, + sender, + [int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], + ], + ) + reviver.start() + response = sender.send_burst( + cmd_input="SEEK", + delay_sec=0.0, + prep_led_sec=0.0, + target_time_sec=(now - time_stamp) / 1000, + target_ids=[ + int(dancer.split("_")[0]) + 1 for dancer in selected_dancers + ], + data=[255, 255, 255], + # retries=3, + ) + except: + screen_ref.notify(f"Seek failed", severity="error") + else: screen_ref.notify(f"Unknown button {id}") diff --git a/command-center/src/lps_ctrl/bt_sender.py b/command-center/src/lps_ctrl/bt_sender.py index 21cb9279..3f8ab711 100644 --- a/command-center/src/lps_ctrl/bt_sender.py +++ b/command-center/src/lps_ctrl/bt_sender.py @@ -22,6 +22,7 @@ class ESP32BTSender: "CHECK": 0x07, "UPLOAD": 0x08, "RESET": 0x09, + "SEEK": 0x0A, } CMD_MAP_INV = { 0x01: "PLAY", @@ -33,6 +34,7 @@ class ESP32BTSender: 0x07: "CHECK", 0x08: "UPLOAD", 0x09: "RESET", + 0x0A: "SEEK", } # Maps internal state integers to readable strings for reporting STATE_MAP = {0: "UNLOADED", 1: "READY", 2: "PLAYING", 3: "PAUSE", 4: "TEST"} @@ -155,7 +157,13 @@ def _parse_found_line(self, line): self.screen_ref.notify(f"Parse error: {e}", severity="error") def send_burst( - self, cmd_input, delay_sec, prep_led_sec=0.0, target_ids=None, data=None + self, + cmd_input, + delay_sec, + prep_led_sec=0.0, + target_time_sec=0.0, + target_ids=None, + data=None, ): """Sends a scheduled broadcast command to the ESP32 Sender.""" self._drain_serial() @@ -176,6 +184,7 @@ def send_burst( ) delay_ms = int(delay_sec * 1000) prep_led_ms = int(prep_led_sec * 1000) + target_time_ms = int(target_time_sec * 1000) target_mask = 0 if not target_ids: @@ -193,7 +202,7 @@ def send_burst( if self.cmd_list[i] < t_start_pc and i != self.idx: self.cmd_list[i] = target_time cmd_int = i * 16 + cmd_int - packet = f"{cmd_int},{delay_ms},{prep_led_ms},{target_mask:x},{data[0]},{data[1]},{data[2]}\n" + packet = f"{cmd_int},{delay_ms},{prep_led_ms},{target_mask:x},{data[0]},{data[1]},{data[2]},{target_time_ms}\n" add_cmd_fail = 0 self.idx = i break diff --git a/command-center/src/screens/control.py b/command-center/src/screens/control.py index c6c564ff..bdf6e441 100644 --- a/command-center/src/screens/control.py +++ b/command-center/src/screens/control.py @@ -71,6 +71,7 @@ def compose(self) -> ComposeResult: yield Button("Sync", id="control-sync") yield Button("Download", id="control-upload") yield Button("Upload", id="control-load") + yield Button("Seek", id="control-seek", classes="danger-buttons") with Horizontal(): yield Button("R", id="control-r") yield Button("G", id="control-g") @@ -266,6 +267,8 @@ def update_connection_status(self) -> None: # TODO: Test this self.app.dancer_status[name].response = "" for item in connection_result["payload"]["found_devices"]: + if target_id == 0: + continue target_id = item["target_id"] cmd_id = item["cmd_id"] cmd_type = item["cmd_type"] From 66adb3e0565fd41a9398d72cc5f10aafabdfe853 Mon Sep 17 00:00:00 2001 From: MengFengWu Date: Mon, 23 Mar 2026 01:00:50 +0800 Subject: [PATCH 2/3] modify seek logic --- command-center/src/config.py | 2 +- command-center/src/handlers/control.py | 8 +++--- command-center/src/lps_ctrl/tcp_server.py | 15 ++++++++++++ command-center/src/screens/control.py | 30 +++++++++++++++++++++-- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/command-center/src/config.py b/command-center/src/config.py index 4675bd59..3cf6e043 100644 --- a/command-center/src/config.py +++ b/command-center/src/config.py @@ -1,6 +1,6 @@ BT_SENDER_PORT = "/dev/tty.usbserial-0001" -MUSIC_FILE_PATH = "../files/music/0317.wav" +MUSIC_FILE_PATH = "../files/music/0322.wav" DANCER_LIST = [ ("all", "none"), diff --git a/command-center/src/handlers/control.py b/command-center/src/handlers/control.py index 57be4a0c..57314e30 100644 --- a/command-center/src/handlers/control.py +++ b/command-center/src/handlers/control.py @@ -408,13 +408,13 @@ def control_handler( ) screen_ref.notify("Forced Restart") elif id == "control-seek": - now = time.time_ns() // 1000000 + 7000 + now = time.time_ns() // 1000000 + 4000 if time_stamp is None or now < time_stamp: screen_ref.notify(f"Invalid timestamp", severity="error") try: screen_ref.notify(f"Seek will play at {(now-time_stamp)/1000} s") reviver = threading.Timer( - 6, + 3, revive, args=[ screen_ref, @@ -425,13 +425,13 @@ def control_handler( reviver.start() response = sender.send_burst( cmd_input="SEEK", - delay_sec=0.0, + delay_sec=1.0, prep_led_sec=0.0, target_time_sec=(now - time_stamp) / 1000, target_ids=[ int(dancer.split("_")[0]) + 1 for dancer in selected_dancers ], - data=[255, 255, 255], + data=[0, 0, 0], # retries=3, ) except: diff --git a/command-center/src/lps_ctrl/tcp_server.py b/command-center/src/lps_ctrl/tcp_server.py index bb6e00ab..97a8094d 100644 --- a/command-center/src/lps_ctrl/tcp_server.py +++ b/command-center/src/lps_ctrl/tcp_server.py @@ -42,6 +42,21 @@ def _update_response(self, id, text): try: self.dancer_status[DANCER_LIST[id][0]].interface = "wifi" self.dancer_status[DANCER_LIST[id][0]].response = text + # self.app_ref.table.update_cell( + # DANCER_LIST[id][0], + # "Name", + # f"[#00ff00]{DANCER_LIST[id][0]}[/]", + # ) + # self.app_ref.table.update_cell( + # DANCER_LIST[id][0], + # "wifi", + # text, + # ) + # self.app_ref.table.update_cell( + # DANCER_LIST[id][0], + # "Response", + # text, + # ) self.act_fcn() except: self.screen_ref.notify( diff --git a/command-center/src/screens/control.py b/command-center/src/screens/control.py index bdf6e441..1874cff7 100644 --- a/command-center/src/screens/control.py +++ b/command-center/src/screens/control.py @@ -114,7 +114,7 @@ async def init_server(self): uploadServer = Esp32TcpServer( screen_ref=self.screen, dancer_status=self.app.dancer_status, - act_fcn=self.update_connection_status, + act_fcn=self.update_connection_status_wifi, control_paths_list=[ "../lighttable/control_" + str(i) + ".dat" for i in range(0, 27) @@ -267,7 +267,7 @@ def update_connection_status(self) -> None: # TODO: Test this self.app.dancer_status[name].response = "" for item in connection_result["payload"]["found_devices"]: - if target_id == 0: + if item["target_id"] == 0 or item["target_id"] > 27: continue target_id = item["target_id"] cmd_id = item["cmd_id"] @@ -306,6 +306,32 @@ def update_connection_status(self) -> None: # TODO: Test this self.table.refresh_column(3) self.notify("Updated connection status") + def update_connection_status_wifi(self) -> None: # TODO: Test this + new_dancer_status: DancerStatus = self.app.dancer_status + for name, dancer in new_dancer_status.items(): + try: + self.table.update_cell( + name, + "Name", + f"[#00ff00]{name}[/]" + if dancer.ethernet_info.connected or dancer.wifi_info.connected + else f"[#ff0000]{name}[/]", + ) + self.table.update_cell( + name, + "Interface", + dancer.interface + if dancer.ethernet_info.connected or dancer.wifi_info.connected + else "none", + ) + self.table.update_cell(name, "Response", dancer.response) + except: + continue + self.table.refresh_column(0) + self.table.refresh_column(2) + self.table.refresh_column(3) + self.notify("Updated connection status") + def on_data_table_cell_selected(self, event: DataTable.CellSelected): row_key = event.cell_key[0].value column_key = event.cell_key[1].value From 4b0af0b0f9f02c6678c7f83ecf6aa9df8c0aad0b Mon Sep 17 00:00:00 2001 From: MengFengWu Date: Mon, 23 Mar 2026 16:20:05 +0800 Subject: [PATCH 3/3] auto pilot now do sync in routine --- command-center/src/handlers/control.py | 443 ++++++++++++++--------- command-center/src/lps_ctrl/bt_sender.py | 10 +- command-center/src/screens/control.py | 51 ++- 3 files changed, 319 insertions(+), 185 deletions(-) diff --git a/command-center/src/handlers/control.py b/command-center/src/handlers/control.py index 57314e30..365b362e 100644 --- a/command-center/src/handlers/control.py +++ b/command-center/src/handlers/control.py @@ -10,7 +10,7 @@ START_MUSIC_EVENT = pygame.USEREVENT + 1 music_timer = None -play_cmd = None +play_cmd = [None for i in range(27)] time_stamp = None @@ -18,7 +18,7 @@ def play_music(): pygame.mixer.music.play() -def revive(screen_ref, sender, target_ids): +def revive(screen_ref, sender, target_ids, auto): try: response = sender.send_burst( cmd_input="PLAY", @@ -26,24 +26,32 @@ def revive(screen_ref, sender, target_ids): prep_led_sec=0.0, target_ids=target_ids, data=[0, 0, 0], + report=not auto # retries=3, ) - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) - # screen_ref.notify("Seek") + for id in target_ids: + play_cmd[id - 1] = int(response["payload"]["command_id"]) + if auto == False: + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) + # screen_ref.notify("Seek") except: screen_ref.notify("Seek failed", severity="error") def control_handler( - id: str, selected_dancers: list[str], screen_ref: ControlScreenType, sender + id: str, + selected_dancers: list[str], + screen_ref: ControlScreenType, + sender, + auto: bool = False, ): # TODO # sender = ESP32BTSender(port="dev/tty3") screen_vars: ControlScreenParamsType = screen_ref.local_vars @@ -60,21 +68,24 @@ def control_handler( prep_led_sec=10.0, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[0, 0, 0], + report=not auto # retries=3, ) - play_cmd = int(response["payload"]["command_id"]) - screen_ref.notify( - f"Play: delay={screen_vars.delay} / start time={screen_vars.start_time}" - ) - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: + for dancer in selected_dancers: + play_cmd[int(dancer.split("_")[0])] = int(response["payload"]["command_id"]) + if auto == False: screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", + f"Play: delay={screen_vars.delay} / start time={screen_vars.start_time}" ) + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-pause": response = sender.send_burst( cmd_input="PAUSE", @@ -82,57 +93,101 @@ def control_handler( prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[0, 0, 0], + report=not auto # retries=3, ) screen_ref.notify("Pause") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-stop": + # screen_ref.notify(str(play_cmd)) time_stamp = None try: if music_timer: music_timer.cancel() music_timer = None pygame.mixer.music.stop() - if play_cmd is not None: - sender.send_burst( + myCIDdict = {} + keys = [] + for dancer in selected_dancers: + if play_cmd[int(dancer.split("_")[0])] is not None: + if myCIDdict.get(play_cmd[int(dancer.split("_")[0])]) is None: + keys.append(play_cmd[int(dancer.split("_")[0])]) + myCIDdict[play_cmd[int(dancer.split("_")[0])]] = [ + int(dancer.split("_")[0]) + 1 + ] + else: + myCIDdict[play_cmd[int(dancer.split("_")[0])]].append( + int(dancer.split("_")[0]) + 1 + ) + + for key in keys: + response = sender.send_burst( cmd_input="CANCEL", - delay_sec=1, + delay_sec=2, prep_led_sec=1, - target_ids=[ - int(dancer.split("_")[0]) + 1 for dancer in selected_dancers - ], - data=[play_cmd, 0, 0], + target_ids=myCIDdict[key], + data=[key, 0, 0], + report=not auto # retries=3, ) - play_cmd = None + if response["statusCode"] == 0: + for id in myCIDdict[key]: + play_cmd[id - 1] = None + if auto == False: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + if auto == False: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) + # if play_cmd is not None: + # sender.send_burst( + # cmd_input="CANCEL", + # delay_sec=1, + # prep_led_sec=1, + # target_ids=[ + # int(dancer.split("_")[0]) + 1 for dancer in selected_dancers + # ], + # data=[play_cmd, 0, 0], + # report=not auto + # # retries=3, + # ) + # play_cmd = None except: - screen_ref.notify("Nothing to stop!") + if auto == False: + screen_ref.notify("Nothing to stop!") response = sender.send_burst( cmd_input="STOP", delay_sec=1, prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[0, 0, 0], + report=not auto # retries=3, ) screen_ref.notify("Stop") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-refresh": response = sender.send_burst( cmd_input="RESET", @@ -140,11 +195,14 @@ def control_handler( prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[0, 0, 0], + report=not auto # retries=3, ) - screen_ref.notify("Reset") + if auto == False: + screen_ref.notify("Reset") elif id == "control-sync": - screen_ref.notify("Sync") + if auto == False: + screen_ref.notify("Sync") elif id == "control-upload": # api.send( # { @@ -155,7 +213,8 @@ def control_handler( # } # ) api.download([int(dancer.split("_")[0]) + 1 for dancer in selected_dancers]) - screen_ref.notify("Download") + if auto == False: + screen_ref.notify("Download") elif id == "control-load": response = sender.send_burst( cmd_input="UPLOAD", @@ -163,174 +222,192 @@ def control_handler( prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[0, 0, 0], + report=not auto # retries=3, ) - screen_ref.notify("Upload") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + screen_ref.notify("Upload") + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-r": response = sender.send_burst( cmd_input="TEST", - delay_sec=1, + delay_sec=2, prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[255, 0, 0], + report=not auto # retries=3, ) - screen_ref.notify("Red") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + screen_ref.notify("Red") + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-g": response = sender.send_burst( cmd_input="TEST", - delay_sec=1, + delay_sec=2, prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[0, 255, 0], + report=not auto # retries=3, ) - screen_ref.notify("Green") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + screen_ref.notify("Green") + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-b": response = sender.send_burst( cmd_input="TEST", - delay_sec=1, + delay_sec=2, prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[0, 0, 255], + report=not auto # retries=3, ) - screen_ref.notify("Blue") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + screen_ref.notify("Blue") + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-rg": response = sender.send_burst( cmd_input="TEST", - delay_sec=1, + delay_sec=2, prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[255, 255, 0], + report=not auto # retries=3, ) - screen_ref.notify("Yellow/RG") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + screen_ref.notify("Yellow/RG") + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-gb": response = sender.send_burst( cmd_input="TEST", - delay_sec=1, + delay_sec=2, prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[0, 255, 255], + report=not auto # retries=3, ) - screen_ref.notify("Cyan/GB") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + screen_ref.notify("Cyan/GB") + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-rb": response = sender.send_burst( cmd_input="TEST", - delay_sec=1, + delay_sec=2, prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[255, 0, 255], + report=not auto # retries=3, ) - screen_ref.notify("Magenta/RB") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + screen_ref.notify("Magenta/RB") + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-d": response = sender.send_burst( cmd_input="TEST", - delay_sec=1, + delay_sec=2, prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[0, 0, 0], + report=not auto # retries=3, ) - screen_ref.notify("Rainbow") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + screen_ref.notify("Rainbow") + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-w": response = sender.send_burst( cmd_input="TEST", - delay_sec=1, + delay_sec=2, prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[255, 255, 255], + report=not auto # retries=3, ) - screen_ref.notify("White") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + screen_ref.notify("White") + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-send-color": response = sender.send_burst( cmd_input="TEST", - delay_sec=1, + delay_sec=2, prep_led_sec=1, target_ids=[int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], data=[ @@ -338,18 +415,20 @@ def control_handler( int(screen_vars.color_code[3:5], 16), int(screen_vars.color_code[5:7], 16), ], + report=not auto # retries=3, ) - screen_ref.notify(f"Color: {screen_vars.color_code}") - if response["statusCode"] == 0: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}" - ) - else: - screen_ref.notify( - f"BTSender Response: {str(response['payload']['message'])}", - severity="error", - ) + if auto == False: + screen_ref.notify(f"Color: {screen_vars.color_code}") + if response["statusCode"] == 0: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}" + ) + else: + screen_ref.notify( + f"BTSender Response: {str(response['payload']['message'])}", + severity="error", + ) elif id == "control-send-command": api.send( { @@ -360,9 +439,11 @@ def control_handler( }, } ) - screen_ref.notify(f"Command: {screen_vars.command} (abandoned)") + if auto == False: + screen_ref.notify(f"Command: {screen_vars.command} (abandoned)") elif id == "control-connect-serial": - screen_ref.notify("Connect") + if auto == False: + screen_ref.notify("Connect") elif id == "control-load-music": MUSIC_FILE_PATH = screen_vars.music try: @@ -373,9 +454,11 @@ def control_handler( pygame.mixer.init(frequency=frequency, size=-16, channels=2) # print(pygame.mixer.get_init()) pygame.mixer.music.load(MUSIC_FILE_PATH) - screen_ref.notify("Music loaded") + if auto == False: + screen_ref.notify("Music loaded") except: - screen_ref.notify("Failed to load the music!", severity="error") + if auto == False: + screen_ref.notify("Failed to load the music!", severity="error") elif id == "control-danger-close-gpio": api.send( { @@ -385,7 +468,8 @@ def control_handler( }, } ) - screen_ref.notify("Close GPIO") + if auto == False: + screen_ref.notify("Close GPIO") elif id == "control-danger-reboot": api.send( { @@ -395,7 +479,8 @@ def control_handler( }, } ) - screen_ref.notify("Reboot") + if auto == False: + screen_ref.notify("Reboot") elif id == "control-danger-forced-restart": api.send( { @@ -406,11 +491,14 @@ def control_handler( }, } ) - screen_ref.notify("Forced Restart") + if auto == False: + screen_ref.notify("Forced Restart") elif id == "control-seek": now = time.time_ns() // 1000000 + 4000 if time_stamp is None or now < time_stamp: - screen_ref.notify(f"Invalid timestamp", severity="error") + if auto == False: + screen_ref.notify(f"Invalid timestamp", severity="error") + return try: screen_ref.notify(f"Seek will play at {(now-time_stamp)/1000} s") reviver = threading.Timer( @@ -420,22 +508,27 @@ def control_handler( screen_ref, sender, [int(dancer.split("_")[0]) + 1 for dancer in selected_dancers], + auto, ], ) reviver.start() response = sender.send_burst( cmd_input="SEEK", - delay_sec=1.0, + delay_sec=2.0, prep_led_sec=0.0, target_time_sec=(now - time_stamp) / 1000, target_ids=[ int(dancer.split("_")[0]) + 1 for dancer in selected_dancers ], data=[0, 0, 0], + report=not auto # retries=3, ) except: - screen_ref.notify(f"Seek failed", severity="error") - + if auto == False: + screen_ref.notify(f"Seek failed", severity="error") + elif id == "control-auto-pilot": + pass else: - screen_ref.notify(f"Unknown button {id}") + if auto == False: + screen_ref.notify(f"Unknown button {id}") diff --git a/command-center/src/lps_ctrl/bt_sender.py b/command-center/src/lps_ctrl/bt_sender.py index 3f8ab711..1fc7d345 100644 --- a/command-center/src/lps_ctrl/bt_sender.py +++ b/command-center/src/lps_ctrl/bt_sender.py @@ -164,6 +164,7 @@ def send_burst( target_time_sec=0.0, target_ids=None, data=None, + report=True, ): """Sends a scheduled broadcast command to the ESP32 Sender.""" self._drain_serial() @@ -213,7 +214,8 @@ def send_burst( ) # logger.info(f"Sending: {packet.strip()}") - self.screen_ref.notify(f"Sending: {packet.strip()}") + if report == True: + self.screen_ref.notify(f"Sending: {packet.strip()}") self.ser.write(packet.encode("utf-8")) success, msg = self._read_until_ack_or_timeout( @@ -223,12 +225,14 @@ def send_burst( status = 0 if success else -1 return self._format_response(status, cmd_input, target_ids, self.idx, msg) - def trigger_check(self, target_ids=[]): + def trigger_check(self, target_ids=[], report=True): """Sends a CHECK command to trigger receivers to broadcast their status.""" if not self.ser or not self.ser.is_open: return self._format_response(-1, "CHECK", target_ids, -1, "Port not open") - resp = self.send_burst(cmd_input="CHECK", delay_sec=1.0, target_ids=target_ids) + resp = self.send_burst( + cmd_input="CHECK", delay_sec=1.0, target_ids=target_ids, report=report + ) if resp["statusCode"] != 0: return resp diff --git a/command-center/src/screens/control.py b/command-center/src/screens/control.py index 1874cff7..722af5df 100644 --- a/command-center/src/screens/control.py +++ b/command-center/src/screens/control.py @@ -44,7 +44,7 @@ class ControlScreen(Screen): countdown: reactive[int] = reactive(0) timer: Timer | None = None sender: ESP32BTSender | None = None - uploadServer: Esp32TcpServer | None = None + auto_running: bool = False def compose(self) -> ComposeResult: with Vertical(): @@ -71,6 +71,7 @@ def compose(self) -> ComposeResult: yield Button("Sync", id="control-sync") yield Button("Download", id="control-upload") yield Button("Upload", id="control-load") + yield Button("Auto pilot", id="control-auto-pilot") yield Button("Seek", id="control-seek", classes="danger-buttons") with Horizontal(): yield Button("R", id="control-r") @@ -111,7 +112,7 @@ def compose(self) -> ComposeResult: yield Footer() async def init_server(self): - uploadServer = Esp32TcpServer( + upload_server = Esp32TcpServer( screen_ref=self.screen, dancer_status=self.app.dancer_status, act_fcn=self.update_connection_status_wifi, @@ -127,7 +128,7 @@ async def init_server(self): ], port=3333, ) - await uploadServer.start() + await upload_server.start() def start_server_thread(self): loop = asyncio.new_event_loop() @@ -169,6 +170,14 @@ def on_button_pressed(self, event: Button.Pressed) -> None: ) and self.timer: self.timer.stop() self.countdown = 0 + elif event.button.id == "control-auto-pilot": + if self.auto_running == True: + self.notify("Killing auto pilot thread...") + self.auto_running = False + else: + self.auto_running = True + auto_thread = threading.Thread(target=self.auto_pilot, daemon=True) + auto_thread.start() def on_input_changed(self, event: Input.Changed) -> None: value = event.input.value @@ -228,13 +237,13 @@ def init_dancer_table(self) -> None: ) self.refresh() - def update_connection_status(self) -> None: # TODO: Test this + def update_connection_status(self, auto=False) -> None: # TODO: Test this if not self.dancer_table_initialized and self.app.dancer_status: self.init_dancer_table() self.dancer_table_initialized = True return try: - self.sender.trigger_check([]) + self.sender.trigger_check([], report=not auto) time.sleep(2) # Wait for ESP32 to scan connection_result = self.sender.get_latest_report() # self.notify(str(connection_result["payload"])) @@ -258,7 +267,8 @@ def update_connection_status(self) -> None: # TODO: Test this # } # } except: - self.notify("Can't get connection report", severity="error") + if auto == False: + self.notify("Can't get connection report", severity="error") return for name, dancer in self.app.dancer_status.items(): @@ -274,6 +284,9 @@ def update_connection_status(self) -> None: # TODO: Test this cmd_type = item["cmd_type"] target_delay = item["target_delay"] state = item["state"] + if state == "READY" and self.auto_running == True: + self.notify("Unexpected dancer reset occured!", severity="error") + self.auto_running = False # timestamp = item["timestamp"] self.app.dancer_status[DANCER_LIST[target_id][0]].interface = "BLE" self.app.dancer_status[DANCER_LIST[target_id][0]].wifi_info.connected = True @@ -304,7 +317,8 @@ def update_connection_status(self) -> None: # TODO: Test this self.table.refresh_column(0) self.table.refresh_column(2) self.table.refresh_column(3) - self.notify("Updated connection status") + if auto == False: + self.notify("Updated connection status") def update_connection_status_wifi(self) -> None: # TODO: Test this new_dancer_status: DancerStatus = self.app.dancer_status @@ -392,3 +406,26 @@ def timer_decrement(self) -> None: else: if self.timer: self.timer.stop() + + def auto_pilot(self) -> None: + self.notify(f"Auto pilot thread started") + try: + while self.auto_running == True: + self.update_connection_status(auto=True) + time.sleep(2) + # control_handler( + # "control-seek", + # [ + # dancer.name + # for dancer in self.app.dancer_status.values() + # ], + # self.screen, + # self.sender, + # False + # ) + # time.sleep(5) + self.auto_running == False + self.notify(f"Auto pilot thread terminated successfully") + except Exception as e: + self.auto_running == False + self.notify(f"Auto pilot thread terminated unexpectedly: {e}")