From d4c22da4cbf2b63a30dbe835adbfc3a9fa8d0f29 Mon Sep 17 00:00:00 2001 From: Teresa Pelinski Date: Wed, 29 Nov 2023 21:18:12 +0000 Subject: [PATCH 1/7] updated todo --- readme.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/readme.md b/readme.md index d44fd59..ac2896a 100644 --- a/readme.md +++ b/readme.md @@ -198,12 +198,10 @@ pipenv run python -m build --sdist # builds the .tar.gz file ## To do and known issues -- [ ] **To do:** Upload to pyPI (so that the package can be installed using `pip`) -- [ ] **To do:** Upload built package to `releases` (so that the package can be installed using `pip install pybela-.tar.gz`) - [ ] **Issue:** Monitor and streamer can't be used simultaneously –  This is due to both monitor and streamer both using the same websocket connection and message format. This could be fixed by having a different message format for the monitor and the streamer (e.g., adding a header to the message) - [ ] **Issue:** The plotting routine does not work when variables are updated at different rates. - [ ] **Issue**: The plotting routine does not work for the monitor (it only works for the streamer) -- [ ] **Code refactor:** There's two routines for generating filenames (for Streamer and for Logger). This should be unified. +- [ ] **Code refactor:** There are two routines for generating filenames (for Streamer and for Logger). This should be unified. - [ ] **Possible feature:** Flexible backend buffer size for streaming: if the assign rate of variables is too slow, the buffers might not be filled and hence not sent (since the data flushed is not collected in the frontend), and there will be long delays between the variable assign and the data being sent to the frontend. - [ ] **Issue:** Flushed buffers are not collected after `stop_streaming` in the frontend. - [ ] **Bug:** `OSError: [Errno 12] Cannot allocate memory` From 81e76e4f9eae7324df0b221aa76b1e09875d9dc8 Mon Sep 17 00:00:00 2001 From: Teresa Pelinski Date: Tue, 5 Mar 2024 15:59:58 +0000 Subject: [PATCH 2/7] added -L to populate symbolic links on rsync --- test/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/readme.md b/test/readme.md index 43aa8a2..253e2b5 100644 --- a/test/readme.md +++ b/test/readme.md @@ -7,7 +7,7 @@ The watcher code is already included in `bela-test`. You can update your Bela AP To run the tests, copy the `bela-test` code into your Bela, add the `Watcher`` library compile and run it: ```bash -rsync -rv test/bela-test root@bela.local:Bela/projects/ +rsync -rvL test/bela-test root@bela.local:Bela/projects/ ssh root@bela.local "make -C Bela stop Bela PROJECT=bela-test run" ``` From 12ea711decb801ae43c72469bca9a9cfbd1bcc38 Mon Sep 17 00:00:00 2001 From: Teresa Pelinski Date: Tue, 5 Mar 2024 16:18:46 +0000 Subject: [PATCH 3/7] updated watcher --- watcher | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher b/watcher index 903573a..60a09e0 160000 --- a/watcher +++ b/watcher @@ -1 +1 @@ -Subproject commit 903573a59fe4e6a13cefe4f8fb8277215932b27e +Subproject commit 60a09e0939edf1172d500df0c7c1111bfd80bedc From e9e8feb128a32ba856644463ceec61f77ad581cd Mon Sep 17 00:00:00 2001 From: Teresa Pelinski Date: Wed, 6 Mar 2024 13:09:16 +0000 Subject: [PATCH 4/7] controller prototype --- pybela/Controller.py | 46 +++++++++++++++++++++++ pybela/Watcher.py | 7 ++-- pybela/__init__.py | 3 +- test/test.py | 87 +++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 pybela/Controller.py diff --git a/pybela/Controller.py b/pybela/Controller.py new file mode 100644 index 0000000..35ea78e --- /dev/null +++ b/pybela/Controller.py @@ -0,0 +1,46 @@ +from .Watcher import Watcher +from .utils import print_info, print_error + + +class Controller(Watcher): + def __init__(self, ip="192.168.7.2", port=5555, data_add="gui_data", control_add="gui_control"): + """Controller class + + Args: + ip (str, optional): Remote address IP. If using internet over USB, the IP won't work, pass "bela.local". Defaults to "192.168.7.2". + port (int, optional): Remote address port. Defaults to 5555. + data_add (str, optional): Data endpoint. Defaults to "gui_data". + control_add (str, optional): Control endpoint. Defaults to "gui_control". + """ + super(Controller, self).__init__(ip, port, data_add, control_add) + + self._mode = "CONTROL" + + def start_controlling(self, variables=[]): + """Starts the controller""" + self.send_ctrl_msg( + {"watcher": [{"cmd": "control", "watchers": variables}]}) + print_info( + f"Started controlling variables {variables}... Run stop_controlling() to stop controlling the variable values.") + # TODO wait until list returns controlled otherwise throw error + + def stop_controlling(self, variables=[]): + """Stops the controller""" + self.send_ctrl_msg( + {"watcher": [{"cmd": "uncontrol", "watchers": variables}]}) + + # TODO wait until list returns not controlled otherwise throw error + + print_info(f"Stopped controlling variables {variables}.") + pass + + def send_ctrl_value(self, variables=[], values=[]): + """Sends the control values""" + assert len(variables) == len( + values), "The number of variables and values should be the same." + + # TODO check value types + + self.send_ctrl_msg( + {"watcher": [{"cmd": "set", "watchers": variables, "values": values}]}) + pass diff --git a/pybela/Watcher.py b/pybela/Watcher.py index dd423a2..f31d39d 100644 --- a/pybela/Watcher.py +++ b/pybela/Watcher.py @@ -48,7 +48,8 @@ def __init__(self, ip="192.168.7.2", port=5555, data_add="gui_data", control_add _pybela_ws_register = {"WATCH": {}, "STREAM": {}, "LOG": {}, - "MONITOR": {}} + "MONITOR": {}, + "CONTROL": {}} self._pybela_ws_register = _pybela_ws_register @@ -300,7 +301,7 @@ def _parse_binary_data(self, binary_data, timestamp_mode, _type): ref_timestamp, * \ data = struct.unpack( - 'Q' + f"{_type}"*int((len(binary_data) - struct.calcsize('Q'))/struct.calcsize(_type)), binary_data) + 'Q' + f"{_type}"*int((len(binary_data) - struct.calcsize('Q'))/struct.calcsize(_type)), binary_data) parsed_buffer = { "ref_timestamp": ref_timestamp, "data": data} @@ -342,7 +343,7 @@ def _filtered_watcher_vars(self, watchers, filter_func): return [{ "name": var["name"], "type": var["type"], - "timestamp_mode":"sparse" if var["timestampMode"] == 1 else "dense" if var["timestampMode"] == 0 else None, + "timestamp_mode": "sparse" if var["timestampMode"] == 1 else "dense" if var["timestampMode"] == 0 else None, # "log_filename": var["logFileName"], # this is updated every time log is called so better not to store it "data_length": self.get_data_length(var["type"], "sparse" if var["timestampMode"] == 1 else "dense" if var["timestampMode"] == 0 else None,), "monitor": var["monitor"] diff --git a/pybela/__init__.py b/pybela/__init__.py index 2fe1709..2239653 100644 --- a/pybela/__init__.py +++ b/pybela/__init__.py @@ -2,5 +2,6 @@ from .Streamer import Streamer from .Logger import Logger from .Monitor import Monitor +from .Controller import Controller -__all__ = ['Watcher', 'Streamer', 'Logger', 'Monitor'] +__all__ = ['Watcher', 'Streamer', 'Logger', 'Monitor', 'Controller'] diff --git a/test/test.py b/test/test.py index 5dcb54d..620c371 100644 --- a/test/test.py +++ b/test/test.py @@ -2,7 +2,7 @@ import asyncio import os import numpy as np -from pybela import Watcher, Streamer, Logger, Monitor +from pybela import Watcher, Streamer, Logger, Monitor, Controller # all tests should be run with Bela connected and the bela-test project (in test/bela-test) running on the board @@ -353,6 +353,79 @@ async def async_test_save_monitor(): asyncio.run(async_test_save_monitor()) +class test_Controller(unittest.TestCase): + def setUp(self): + self.controlled_vars = ["myvar", "myvar2", "myvar3", "myvar4"] + + self.controller = Controller() + self.controller.connect() + self.controller._printall_responses = True + + self.monitor = Monitor() + self.period = 1000 + self.monitor.connect() + + def tearDown(self): + self.controller.__del__() + + def get_controlled_status(self): + return [i['controlled'] for i in self.controller.list()['watchers']] + + def test_start_stop_controlling(self): + + async def async_test_start_stop_controlling(): + + self.controller.start_controlling(variables=self.controlled_vars) + + await asyncio.sleep(2) # TODO remove once start_controlling synchronously waits + + self.assertEqual(self.get_controlled_status(), [ + True]*len(self.controlled_vars), "The controlled status of the variables should be True after start_controlling") + + self.controller.stop_controlling(variables=self.controlled_vars) + await asyncio.sleep(5) # TODO remove once stop_controlling synchronously waits + + self.assertEqual(self.get_controlled_status(), [ + False]*len(self.controlled_vars), "The controlled status of the variables should be False after stop_controlling") + + asyncio.run(async_test_start_stop_controlling()) + + def test_send_ctrl_value(self): + async def async_test_send_ctrl_value(): + # TODO add streamer to check values are being sent + self.controller.start_controlling(variables=self.controlled_vars) + + set_values = [4]*len(self.controlled_vars) + + self.controller.send_ctrl_value( + variables=self.controlled_vars, values=set_values) + await asyncio.sleep(3) + n_values = 25 + monitored_buffer = self.monitor.monitor_n_values( + variables=self.controlled_vars, + periods=[self.period]*len(self.controlled_vars), n_values=n_values) + + list_values = {i['name']: i['value'] + for i in self.controller.list()['watchers']} + + monitored_values = { + var: monitored_buffer[var]["values"] for var in self.controlled_vars} + + print(list_values) + print(monitored_values) + + for idx, var in enumerate(list_values): + self.assertTrue( + list_values[var] == set_values[idx], "The controlled value should be 4") + # self.assertTrue( + # all(monitored_values[var] == set_values[idx]), "The monitored values should be 4") + + self.controller.stop_controlling(variables=self.controlled_vars) + self.assertEqual(self.get_controlled_status(), [ + False]*len(self.controlled_vars), "The controlled status of the variables should be False after stop_controlling") + asyncio.run(async_test_send_ctrl_value()) + + def remove_file(file_path): if os.path.exists(file_path): os.remove(file_path) @@ -370,25 +443,29 @@ def remove_file(file_path): if 1: suite = unittest.TestSuite() - if 1: + if 0: suite.addTest(test_Watcher('test_list')) suite.addTest(test_Watcher('test_start_stop')) - if 1: + if 0: suite.addTest(test_Streamer('test_stream_n_values')) suite.addTest(test_Streamer('test_start_stop_streaming')) suite.addTest(test_Streamer('test_scheduling_streaming')) - if 1: + if 0: suite.addTest(test_Logger('test_logged_files_with_transfer')) suite.addTest(test_Logger('test_logged_files_wo_transfer')) suite.addTest(test_Logger('test_scheduling_logging')) - if 1: + if 0: suite.addTest(test_Monitor('test_peek')) suite.addTest(test_Monitor('test_period_monitor')) suite.addTest(test_Monitor('test_monitor_n_values')) suite.addTest(test_Monitor('test_save_monitor')) + if 1: + suite.addTest(test_Controller('test_start_stop_controlling')) + # suite.addTest(test_Controller('test_send_ctrl_value')) + runner = unittest.TextTestRunner(verbosity=2) runner.run(suite) From 0a7713fcc53581fe3074f02985100bfc09b76a2b Mon Sep 17 00:00:00 2001 From: Teresa Pelinski Date: Thu, 7 Mar 2024 15:31:21 +0000 Subject: [PATCH 5/7] finished control class and tests --- pybela/Controller.py | 93 +++++++++++++++++++++++++++++++++++++++----- pybela/Monitor.py | 6 +-- pybela/Watcher.py | 19 +++++---- test/test.py | 60 +++++++++------------------- 4 files changed, 116 insertions(+), 62 deletions(-) diff --git a/pybela/Controller.py b/pybela/Controller.py index 35ea78e..f0fe8ff 100644 --- a/pybela/Controller.py +++ b/pybela/Controller.py @@ -1,10 +1,12 @@ +import asyncio from .Watcher import Watcher -from .utils import print_info, print_error +from .utils import print_info, print_warning class Controller(Watcher): def __init__(self, ip="192.168.7.2", port=5555, data_add="gui_data", control_add="gui_control"): """Controller class + Note: All values set with the controller class will be only visible through the "get_controlled_value" function, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected. Args: ip (str, optional): Remote address IP. If using internet over USB, the IP won't work, pass "bela.local". Defaults to "192.168.7.2". @@ -17,30 +19,101 @@ def __init__(self, ip="192.168.7.2", port=5555, data_add="gui_data", control_add self._mode = "CONTROL" def start_controlling(self, variables=[]): - """Starts the controller""" + """Starts controlling given variables. This function will block until all requested variables are set to 'controlled' in the list. + Note: All values set with the controller class will be only visible through the "get_controlled_value" function, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected. + + Args: + variables (list, optional): List of variables to control. If no variables are specified, stream all watcher variables (default). + """ + + variables = self._var_arg_checker(variables) + self.send_ctrl_msg( {"watcher": [{"cmd": "control", "watchers": variables}]}) + + async def async_wait_for_control_mode_to_be_set(variables=variables): + # wait for variables to be set as 'controlled' in list + _controlled_status = self.get_controlled_status( + variables) # avoid multiple calls to list + while not all([_controlled_status[var] for var in variables]): + await asyncio.sleep(0.5) + + asyncio.run(async_wait_for_control_mode_to_be_set(variables=variables)) + print_info( f"Started controlling variables {variables}... Run stop_controlling() to stop controlling the variable values.") - # TODO wait until list returns controlled otherwise throw error def stop_controlling(self, variables=[]): - """Stops the controller""" + """Stops controlling given variables. This function will block until all requested variables are set to 'uncontrolled' in the list. + Note: All values set with the controller class will be only visible through the "get_controlled_value" function, or the "value" field in the list() function. + + Args: + variables (list, optional): List of variables to control. If no variables are specified, stream all watcher variables (default). + """ + + variables = self._var_arg_checker(variables) + self.send_ctrl_msg( {"watcher": [{"cmd": "uncontrol", "watchers": variables}]}) - # TODO wait until list returns not controlled otherwise throw error + async def async_wait_for_control_mode_to_be_set(variables=variables): + # wait for variables to be set as 'uncontrolled' in list + _controlled_status = self.get_controlled_status( + variables) # avoid multiple calls to list + while all([_controlled_status[var] for var in variables]): + await asyncio.sleep(0.5) + + asyncio.run(async_wait_for_control_mode_to_be_set(variables=variables)) print_info(f"Stopped controlling variables {variables}.") - pass - def send_ctrl_value(self, variables=[], values=[]): - """Sends the control values""" + def send_ctrl_value(self, variables, values): + """Send a value to the given variables. + Note: All values set with this function will be only visible through the "get_controlled_value" function, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected. + + Args: + variables (list, required): List of variables to control. + values (list, required): Values to be set for each variable. + """ + + assert isinstance(values, list) and len( + values) > 0, "At least one value per variable should be provided." + + variables = self._var_arg_checker(variables) + assert len(variables) == len( values), "The number of variables and values should be the same." - # TODO check value types + for var in variables: + _type = self.get_prop_of_var(var, "type") + + value = values[variables.index(var)] + + if value % 1 is not 0 and _type in ["i", "j"]: + print_warning( + f"Value {value} is not an integer, but the variable {var} is of type {_type}. Only the integer part will be sent.") self.send_ctrl_msg( {"watcher": [{"cmd": "set", "watchers": variables, "values": values}]}) - pass + + def get_controlled_status(self, variables): + """Gets the controlled status (controlled or uncontrolled) of the variables + + Args: + variables (list of str): List of variables to check + + Returns: + list of str: List of controlled status of the variables + """ + return {var['name']: var['controlled'] for var in self.list()['watchers'] if var['name'] in variables} + + def get_controlled_value(self, variables): + """ Gets the controlled value of the variables + + Args: + variables (list of str): List of variables to get the controlled value + + Returns: + list of numbers: List of controlled values of the variables + """ + return {var['name']: var['value'] for var in self.list()['watchers'] if var['name'] in variables} diff --git a/pybela/Monitor.py b/pybela/Monitor.py index 552ac16..26bdad8 100644 --- a/pybela/Monitor.py +++ b/pybela/Monitor.py @@ -18,9 +18,9 @@ def __init__(self, ip="192.168.7.2", port=5555, data_add="gui_data", control_add self._mode = "MONITOR" def connect(self): - super().connect() - # longer queue for monitor since each buffer has only one value - self.streaming_buffers_queue_length = 2000 + if (super().connect()): + # longer queue for monitor since each buffer has only one value + self.streaming_buffers_queue_length = 2000 @property def values(self): diff --git a/pybela/Watcher.py b/pybela/Watcher.py index f31d39d..4228b90 100644 --- a/pybela/Watcher.py +++ b/pybela/Watcher.py @@ -107,12 +107,19 @@ def connect(self): async def _async_connect(): try: - # Close any open ctrl websocket for the same mode (STREAM, LOG, MONITOR, WATCH) + # Close any open ctrl websocket open for the same mode (STREAM, LOG, MONITOR, WATCH) if self._pybela_ws_register[self._mode].get(self.ws_ctrl_add) is not None and self._pybela_ws_register[self._mode][self.ws_ctrl_add].open: print_warning( f"pybela doesn't support more than one active connection at a time for a given mode. Closing previous connection for {self._mode} at {self.ws_ctrl_add}.") await self._pybela_ws_register[self._mode][self.ws_ctrl_add].close() + # Control and monitor can't be used at the same time + if (self._mode == "MONITOR" and self._pybela_ws_register["CONTROL"].get(self.ws_ctrl_add) is not None and self._pybela_ws_register["CONTROL"][self.ws_ctrl_add].open) or (self._mode == "CONTROL" and self._pybela_ws_register["MONITOR"].get(self.ws_ctrl_add) is not None and self._pybela_ws_register["MONITOR"][self.ws_ctrl_add].open): + print_warning( + f"pybela doesn't support running control and monitor modes at the same time. You are currently running {'CONTROL' if self._mode=='MONITOR' else 'MONITOR'} at {self.ws_ctrl_add}. You can close it running controller.disconnect()") + print_error("Connection failed") + return 0 + # Connect to the control websocket self.ws_ctrl = await websockets.connect(self.ws_ctrl_add) self._pybela_ws_register[self._mode][self.ws_ctrl_add] = self.ws_ctrl @@ -124,12 +131,6 @@ async def _async_connect(): # Send connection reply to establish the connection self.send_ctrl_msg({"event": "connection-reply"}) - # Close any open data websocket for the same mode (STREAM, LOG, MONITOR, WATCH) - if self._pybela_ws_register[self._mode].get(self.ws_data_add) is not None and self._pybela_ws_register[self._mode][self.ws_data_add].open: - print_warning( - f"pybela doesn't support more than one active connection at a time for a given mode. Closing previous connection for {self._mode} at {self.data_add}.") - await self._pybela_ws_register[self._mode][self.ws_data_add].close() - # Connect to the data websocket self.ws_data = await websockets.connect(self.ws_data_add) @@ -143,8 +144,10 @@ async def _async_connect(): self._watcher_vars = self._filtered_watcher_vars(self._list["watchers"], lambda var: True) print_ok("Connection successful") + return 1 else: print_error("Connection failed") + return 0 except Exception as e: raise ConnectionError(f"Connection failed: {str(e)}.") @@ -358,7 +361,7 @@ def _var_arg_checker(self, variables): """ if len(variables) == 0: - # if no variables are specified, stream all watcher variables (default) + # if no variables are specified, return all watcher variables (default) return [var["name"] for var in self.watcher_vars] variables = variables if isinstance(variables, list) else [ diff --git a/test/test.py b/test/test.py index 620c371..1435cca 100644 --- a/test/test.py +++ b/test/test.py @@ -359,34 +359,23 @@ def setUp(self): self.controller = Controller() self.controller.connect() - self.controller._printall_responses = True - - self.monitor = Monitor() - self.period = 1000 - self.monitor.connect() def tearDown(self): self.controller.__del__() - def get_controlled_status(self): - return [i['controlled'] for i in self.controller.list()['watchers']] - def test_start_stop_controlling(self): async def async_test_start_stop_controlling(): self.controller.start_controlling(variables=self.controlled_vars) - await asyncio.sleep(2) # TODO remove once start_controlling synchronously waits - - self.assertEqual(self.get_controlled_status(), [ - True]*len(self.controlled_vars), "The controlled status of the variables should be True after start_controlling") + self.assertEqual(self.controller.get_controlled_status(variables=self.controlled_vars), { + var: True for var in self.controlled_vars}, "The controlled status of the variables should be True after start_controlling") self.controller.stop_controlling(variables=self.controlled_vars) - await asyncio.sleep(5) # TODO remove once stop_controlling synchronously waits - self.assertEqual(self.get_controlled_status(), [ - False]*len(self.controlled_vars), "The controlled status of the variables should be False after stop_controlling") + self.assertEqual(self.controller.get_controlled_status(variables=self.controlled_vars), { + var: False for var in self.controlled_vars}, "The controlled status of the variables should be False after stop_controlling") asyncio.run(async_test_start_stop_controlling()) @@ -395,34 +384,23 @@ async def async_test_send_ctrl_value(): # TODO add streamer to check values are being sent self.controller.start_controlling(variables=self.controlled_vars) - set_values = [4]*len(self.controlled_vars) + set_value = 4.6 self.controller.send_ctrl_value( - variables=self.controlled_vars, values=set_values) - await asyncio.sleep(3) - n_values = 25 - monitored_buffer = self.monitor.monitor_n_values( - variables=self.controlled_vars, - periods=[self.period]*len(self.controlled_vars), n_values=n_values) - - list_values = {i['name']: i['value'] - for i in self.controller.list()['watchers']} + variables=self.controlled_vars, values=[set_value]*len(self.controlled_vars)) + await asyncio.sleep(0.1) # wait for the values to be set - monitored_values = { - var: monitored_buffer[var]["values"] for var in self.controlled_vars} + _controlled_values = self.controller.get_controlled_value( + variables=self.controlled_vars) # avoid multiple calls to list - print(list_values) - print(monitored_values) + integer_types = ["i", "j"] + expected_values = [int(set_value) if self.controller.get_prop_of_var( + var, "type") in integer_types else set_value for var in self.controlled_vars] - for idx, var in enumerate(list_values): + for idx, var in enumerate(self.controlled_vars): self.assertTrue( - list_values[var] == set_values[idx], "The controlled value should be 4") - # self.assertTrue( - # all(monitored_values[var] == set_values[idx]), "The monitored values should be 4") + _controlled_values[var] == expected_values[idx], "The controlled value should be 4") - self.controller.stop_controlling(variables=self.controlled_vars) - self.assertEqual(self.get_controlled_status(), [ - False]*len(self.controlled_vars), "The controlled status of the variables should be False after stop_controlling") asyncio.run(async_test_send_ctrl_value()) @@ -443,21 +421,21 @@ def remove_file(file_path): if 1: suite = unittest.TestSuite() - if 0: + if 1: suite.addTest(test_Watcher('test_list')) suite.addTest(test_Watcher('test_start_stop')) - if 0: + if 1: suite.addTest(test_Streamer('test_stream_n_values')) suite.addTest(test_Streamer('test_start_stop_streaming')) suite.addTest(test_Streamer('test_scheduling_streaming')) - if 0: + if 1: suite.addTest(test_Logger('test_logged_files_with_transfer')) suite.addTest(test_Logger('test_logged_files_wo_transfer')) suite.addTest(test_Logger('test_scheduling_logging')) - if 0: + if 1: suite.addTest(test_Monitor('test_peek')) suite.addTest(test_Monitor('test_period_monitor')) suite.addTest(test_Monitor('test_monitor_n_values')) @@ -465,7 +443,7 @@ def remove_file(file_path): if 1: suite.addTest(test_Controller('test_start_stop_controlling')) - # suite.addTest(test_Controller('test_send_ctrl_value')) + suite.addTest(test_Controller('test_send_ctrl_value')) runner = unittest.TextTestRunner(verbosity=2) runner.run(suite) From 435b4511583131fae3003d68047b39449cadddb8 Mon Sep 17 00:00:00 2001 From: Teresa Pelinski Date: Fri, 8 Mar 2024 12:33:22 +0000 Subject: [PATCH 6/7] added Controller tutorial, minor method renaming, updated readme --- pybela/Controller.py | 24 ++-- readme.md | 16 ++- test/test.py | 12 +- tutorials/notebooks/5_Controller.ipynb | 175 +++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 22 deletions(-) create mode 100644 tutorials/notebooks/5_Controller.ipynb diff --git a/pybela/Controller.py b/pybela/Controller.py index f0fe8ff..345b9d2 100644 --- a/pybela/Controller.py +++ b/pybela/Controller.py @@ -6,7 +6,7 @@ class Controller(Watcher): def __init__(self, ip="192.168.7.2", port=5555, data_add="gui_data", control_add="gui_control"): """Controller class - Note: All values set with the controller class will be only visible through the "get_controlled_value" function, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected. + Note: All values set with the controller class will be only visible through the "get_value()" method, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected. Args: ip (str, optional): Remote address IP. If using internet over USB, the IP won't work, pass "bela.local". Defaults to "192.168.7.2". @@ -20,7 +20,7 @@ def __init__(self, ip="192.168.7.2", port=5555, data_add="gui_data", control_add def start_controlling(self, variables=[]): """Starts controlling given variables. This function will block until all requested variables are set to 'controlled' in the list. - Note: All values set with the controller class will be only visible through the "get_controlled_value" function, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected. + Note: All values set with the controller class will be only visible through the "get_value()" method, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected. Args: variables (list, optional): List of variables to control. If no variables are specified, stream all watcher variables (default). @@ -45,7 +45,7 @@ async def async_wait_for_control_mode_to_be_set(variables=variables): def stop_controlling(self, variables=[]): """Stops controlling given variables. This function will block until all requested variables are set to 'uncontrolled' in the list. - Note: All values set with the controller class will be only visible through the "get_controlled_value" function, or the "value" field in the list() function. + Note: All values set with the controller class will be only visible through the "get_value()" method, or the "value" field in the list() function. Args: variables (list, optional): List of variables to control. If no variables are specified, stream all watcher variables (default). @@ -67,9 +67,9 @@ async def async_wait_for_control_mode_to_be_set(variables=variables): print_info(f"Stopped controlling variables {variables}.") - def send_ctrl_value(self, variables, values): + def send_value(self, variables, values): """Send a value to the given variables. - Note: All values set with this function will be only visible through the "get_controlled_value" function, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected. + Note: All values set with this function will be only visible through the "get_value()" method, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected. Args: variables (list, required): List of variables to control. @@ -89,31 +89,33 @@ def send_ctrl_value(self, variables, values): value = values[variables.index(var)] - if value % 1 is not 0 and _type in ["i", "j"]: + if value % 1 != 0 and _type in ["i", "j"]: print_warning( f"Value {value} is not an integer, but the variable {var} is of type {_type}. Only the integer part will be sent.") self.send_ctrl_msg( {"watcher": [{"cmd": "set", "watchers": variables, "values": values}]}) - def get_controlled_status(self, variables): + def get_controlled_status(self, variables=[]): """Gets the controlled status (controlled or uncontrolled) of the variables Args: - variables (list of str): List of variables to check + variables (list of str, optional): List of variables to check. Defaults to all variables in the watcher. Returns: list of str: List of controlled status of the variables """ + variables = self._var_arg_checker(variables) return {var['name']: var['controlled'] for var in self.list()['watchers'] if var['name'] in variables} - def get_controlled_value(self, variables): - """ Gets the controlled value of the variables + def get_value(self, variables=[]): + """ Gets the value of the variables Args: - variables (list of str): List of variables to get the controlled value + variables (list of str, optional): List of variables to get the value from. Defaults to all variables in the watcher. Returns: list of numbers: List of controlled values of the variables """ + variables = self._var_arg_checker(variables) return {var['name']: var['value'] for var in self.list()['watchers'] if var['name'] in variables} diff --git a/readme.md b/readme.md index ac2896a..d013c24 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,13 @@ # pybela -pybela allows interfacing with [Bela](https://bela.io/), the embedded audio platform, using Python. pybela provides a convenient way to stream, log, and monitor sensor data from your Bela device to your laptop. +pybela allows interfacing with [Bela](https://bela.io/), the embedded audio platform, using Python. pybela provides a convenient way to stream, log, monitor sensor data from your Bela device to your laptop. It also allows you to control the value of variables in your Bela code from your laptop. -This library is currently under development and has been tested with Bela at `dev` branch commit `69cdf75a` and watcher at `main` commit `903573a`. +Below, you can find instructions to install pybela. You can find code examples at `tutorials/` and `test/`. + +pybela was developed with a machine learning use case in mind. For a complete pipeline including data acquisition, processing, model training, and deployment (including rapid cross-compilation) check the [pybela-pytorch-xc-tutorial](https://github.com/pelinski/pybela-pytorch-xc-tutorial). ## [Installation and set up](#installation) +You will need to (1) install the python package in your laptop, (2) set the Bela branch to `dev` and (3) add the watcher library to your Bela project. ### 1. Installing the python package @@ -95,9 +98,10 @@ pybela has three different modes of operation: - **Streaming**: continuously send data from your Bela device to your laptop. - **Logging**: log data in your Bela device and then retrieve it from your laptop. -- **Monitoring**: monitor the state of variables in the Bela code from your laptop. +- **Monitoring**: monitor the value of variables in the Bela code from your laptop. +- **Controlling**: control the value of variables in the Bela code from your laptop. -You can check the **tutorials** at `tutorials/` for more detailed information and usage of each of the modes. +You can check the **tutorials** at `tutorials/` for more detailed information and usage of each of the modes. You can also check the `test/test.py` for a quick overview of the library. ### Running the examples @@ -173,6 +177,8 @@ streamer.stop_streaming() ## Testing +This library has been tested with Bela at `dev` branch commit `69cdf75a` and watcher at `main` commit `903573a`. + To run pybela's tests first copy the `bela-test` code into your Bela, compile and run it: ```bash @@ -198,7 +204,7 @@ pipenv run python -m build --sdist # builds the .tar.gz file ## To do and known issues -- [ ] **Issue:** Monitor and streamer can't be used simultaneously –  This is due to both monitor and streamer both using the same websocket connection and message format. This could be fixed by having a different message format for the monitor and the streamer (e.g., adding a header to the message) +- [ ] **Issue:** Monitor and streamer/controller can't be used simultaneously –  This is due to both monitor and streamer both using the same websocket connection and message format. This could be fixed by having a different message format for the monitor and the streamer (e.g., adding a header to the message) - [ ] **Issue:** The plotting routine does not work when variables are updated at different rates. - [ ] **Issue**: The plotting routine does not work for the monitor (it only works for the streamer) - [ ] **Code refactor:** There are two routines for generating filenames (for Streamer and for Logger). This should be unified. diff --git a/test/test.py b/test/test.py index 1435cca..ccc22b9 100644 --- a/test/test.py +++ b/test/test.py @@ -379,18 +379,18 @@ async def async_test_start_stop_controlling(): asyncio.run(async_test_start_stop_controlling()) - def test_send_ctrl_value(self): - async def async_test_send_ctrl_value(): + def test_send_value(self): + async def async_test_send_value(): # TODO add streamer to check values are being sent self.controller.start_controlling(variables=self.controlled_vars) set_value = 4.6 - self.controller.send_ctrl_value( + self.controller.send_value( variables=self.controlled_vars, values=[set_value]*len(self.controlled_vars)) await asyncio.sleep(0.1) # wait for the values to be set - _controlled_values = self.controller.get_controlled_value( + _controlled_values = self.controller.get_value( variables=self.controlled_vars) # avoid multiple calls to list integer_types = ["i", "j"] @@ -401,7 +401,7 @@ async def async_test_send_ctrl_value(): self.assertTrue( _controlled_values[var] == expected_values[idx], "The controlled value should be 4") - asyncio.run(async_test_send_ctrl_value()) + asyncio.run(async_test_send_value()) def remove_file(file_path): @@ -443,7 +443,7 @@ def remove_file(file_path): if 1: suite.addTest(test_Controller('test_start_stop_controlling')) - suite.addTest(test_Controller('test_send_ctrl_value')) + suite.addTest(test_Controller('test_send_value')) runner = unittest.TextTestRunner(verbosity=2) runner.run(suite) diff --git a/tutorials/notebooks/5_Controller.ipynb b/tutorials/notebooks/5_Controller.ipynb new file mode 100644 index 0000000..d4e9bfc --- /dev/null +++ b/tutorials/notebooks/5_Controller.ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# pybela Tutorial 5: Controller\n", + "This notebook is a tutorial for the Controller class in the pybela python library. The Controller class allows you to control the variables in the Bela program using python. \n", + "\n", + "The Controller class has some limitations: you can only send one value at a time (no buffers) and you can not control the exact frame at which the values will be updated in the Bela program. Moreover, you can't use it at the same time as the Monitor. However, it is still a useful tool if you want to modify variable values in the Bela program without caring too much about the rate and exact timing of the updates.\n", + "\n", + "As with the previous tutorials, you will need to run the `potentiometers` project in Bela. If you haven't done it yet, copy the project onto Bela:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!rsync -rvL ../bela-code/potentiometers root@bela.local:Bela/projects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And compile and run the project using either the IDE or by running the following command in the Terminal:\n", + "```bash\n", + "ssh root@bela.local \"make -C Bela stop Bela PROJECT=potentiometers run\" \n", + "```\n", + "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)\n", + "\n", + "First, let's import the `Controller` class from the `pybela` library and create a `Controller` object. Remember to run `.connect()` every time you instantiate a `pybela` object to connect to the Bela program. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pybela import Controller\n", + "\n", + "controller = Controller()\n", + "controller.connect()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run `.start_controlling()` to start controlling the variables in the Bela program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "controller.start_controlling(variables=['pot1', 'pot2'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can check which variables are being controlled with the `.get_controlled_status()` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "controller.get_controlled_status()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's check what their current value is:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "controller.get_value(variables=['pot1', 'pot2'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now send a value to `pot1` and `pot2` using the `.send_value()` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "controller.send_value(variables=['pot1', 'pot2'], values=[0.5, 0.5])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can check if the variable values have been updated:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "controller.get_value(variables=['pot1', 'pot2'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The controlled value will stay so until we send a new value or stop controlling the variable. We can stop controlling the variables with the `.stop_controlling()` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "controller.stop_controlling(variables=['pot1', 'pot2'])\n", + "controller.get_value(variables=['pot1', 'pot2']) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You should note that the values modified with the Controller class will only be visible through the Controller `get_value())` method and not through the Monitor, Streamer or Logger. The values in the Bela program will be updated with the values sent by the Controller, but the Monitor, Streamer or Logger will instead send the value of the variable in the Bela program if it hadn't been modified by the Controller. The reason behind this is that the Controller class has a different use case than the Monitor, Streamer or Logger (controlling variables in the code vs. collecting data), and it is not meant to be used at the same time as the other classes." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "pybela-rZFcaNs6", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 02d7adc2fec00c177836725d9fc440c4563345e3 Mon Sep 17 00:00:00 2001 From: Teresa Pelinski Date: Fri, 8 Mar 2024 12:47:42 +0000 Subject: [PATCH 7/7] updated package version --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index b77d45e..37dddc0 100644 --- a/setup.py +++ b/setup.py @@ -5,10 +5,10 @@ setuptools.setup( name="pybela", - version="0.0.1", + version="0.1.0", author="Teresa Pelinski", author_email="teresapelinski@gmail.com", - description="pybela allows interfacing with Bela, the embedded audio platform, using Python. pybela provides a convenient way to stream, log, and monitor sensor data from Bela to the host machine.", + description="pybela allows interfacing with Bela, the embedded audio platform, using Python. pybela provides a convenient way to stream, log, and monitor sensor data from your Bela device to your laptop, or alternatively, to send values to a Bela program from your laptop.", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/BelaPlatform/pybela",