From 5c684a0360b23e7e22e4d724c39fcab93c0cb007 Mon Sep 17 00:00:00 2001 From: MathiasMahn <53939819+MathiasMahn@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:41:39 +0100 Subject: [PATCH 1/3] adding event type decoding to FSMThread --- .gitignore | 3 + bpod_core/bpod.py | 7 +- examples/hello_world.ipynb | 222 ++++++++++++++++++++++++++++++++++++ examples/minimal_example.py | 67 +++++++++++ 4 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 examples/hello_world.ipynb create mode 100644 examples/minimal_example.py diff --git a/.gitignore b/.gitignore index 65de09cc..24aa61a4 100644 --- a/.gitignore +++ b/.gitignore @@ -193,6 +193,9 @@ target/ profile_default/ ipython_config.py +# LLM files +.repotomdrc + # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: diff --git a/bpod_core/bpod.py b/bpod_core/bpod.py index 151f062f..85cf40ee 100644 --- a/bpod_core/bpod.py +++ b/bpod_core/bpod.py @@ -137,6 +137,7 @@ def __init__( # noqa: PLR0913 softcode_handler: Callable, state_transitions: NDArray[np.uint8], use_back_op: bool, + event_names: list[str], ) -> None: """ Initialize the FSMThread. @@ -168,6 +169,7 @@ def __init__( # noqa: PLR0913 self._softcode_handler = softcode_handler self._state_transitions = state_transitions self._use_back_op = use_back_op + self._event_names = event_names def stop(self): self._stop_event.set() @@ -228,7 +230,9 @@ def run(self) -> None: events = event_data_view[:param] for event in events: if debug: - logger.debug('%d µs: Event %d', micros, event) + if not(event == 255): # exit event + event_name = self._event_names[event] if event < len(self._event_names) else "Unknown" + logger.debug('%d µs: Event: %s (%d)', micros, event_name, event) # TODO: handle event # handle state transitions / exit event @@ -1292,6 +1296,7 @@ def _run_state_machine(self, *, blocking: bool) -> None: self._softcode_handler, self._state_transitions, self._use_back_op, + self.event_names, ) self._fsm_thread.start() self._waiting_for_confirmation = False diff --git a/examples/hello_world.ipynb b/examples/hello_world.ipynb new file mode 100644 index 00000000..e8a80b05 --- /dev/null +++ b/examples/hello_world.ipynb @@ -0,0 +1,222 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "e8a973cd", + "metadata": {}, + "outputs": [], + "source": [ + "from bpod_core.fsm import StateMachine" + ] + }, + { + "cell_type": "markdown", + "id": "09331e75", + "metadata": {}, + "source": [ + "### Define a state machine" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1dfc3950", + "metadata": {}, + "outputs": [], + "source": [ + "fsm = StateMachine()\n", + "\n", + "fsm.set_global_timer(\n", + " index=1,\n", + " duration=5\n", + ")\n", + "\n", + "\n", + "fsm.add_state(\n", + " name='StartGlobalTimer',\n", + " timer=0.25,\n", + " transitions={'Tup': 'Port1Light'},\n", + " actions={'GlobalTimerTrig': 1},\n", + ")\n", + "fsm.add_state(\n", + " name='Port1Light',\n", + " timer=0.25,\n", + " transitions={\n", + " 'Tup': 'Port3Light',\n", + " 'GlobalTimer1_End': '>exit',\n", + " },\n", + " actions={'PWM1': 255},\n", + ")\n", + "fsm.add_state(\n", + " name='Port3Light',\n", + " timer=0.25,\n", + " transitions={\n", + " 'Tup': 'Port1Light',\n", + " 'GlobalTimer1_End': '>exit',\n", + " },\n", + " actions={'PWM3': 255},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a32cfda8", + "metadata": {}, + "source": [ + "### Display the statemechine for easy verification" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2c77e81e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "State Machine\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "StartGlobalTimer\n", + "\n", + "\n", + "StartGlobalTimer  \n", + "\n", + "0.25 s\n", + "GlobalTimerTrig\n", + "1\n", + "\n", + "\n", + "\n", + "\n", + "->StartGlobalTimer\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Port1Light\n", + "\n", + "\n", + "Port1Light  \n", + "\n", + "0.25 s\n", + "PWM1\n", + "255\n", + "\n", + "\n", + "\n", + "\n", + "StartGlobalTimer->Port1Light\n", + "\n", + "\n", + "Tup\n", + "\n", + "\n", + "\n", + "exit\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Port1Light->exit\n", + "\n", + "\n", + "GlobalTimer1_End\n", + "\n", + "\n", + "\n", + "Port3Light\n", + "\n", + "\n", + "Port3Light  \n", + "\n", + "0.25 s\n", + "PWM3\n", + "255\n", + "\n", + "\n", + "\n", + "\n", + "Port1Light->Port3Light\n", + "\n", + "\n", + "Tup\n", + "\n", + "\n", + "\n", + "Port3Light->exit\n", + "\n", + "\n", + "GlobalTimer1_End\n", + "\n", + "\n", + "\n", + "Port3Light->Port1Light\n", + "\n", + "\n", + "Tup\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fsm.to_digraph() " + ] + }, + { + "cell_type": "markdown", + "id": "ecf44170", + "metadata": {}, + "source": [ + "### Running a state machine / bpod communication does not seem to work from a jupyter notebook..." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "bpod-core", + "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.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/minimal_example.py b/examples/minimal_example.py new file mode 100644 index 00000000..6e1559f1 --- /dev/null +++ b/examples/minimal_example.py @@ -0,0 +1,67 @@ +from bpod_core.fsm import StateMachine +from bpod_core.bpod import Bpod +import logging +import sys + + +LOG_FILE = "bpod_debug.log" + +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8'), + logging.StreamHandler(sys.stdout) # This also keeps logs appearing in the terminal + ] +) + +# 3. Specifically ensure the bpod_core library is set to DEBUG +logging.getLogger('bpod_core').setLevel(logging.DEBUG) + +print(f"Logging initialized. All Bpod traffic will be saved to: {LOG_FILE}") + +fsm = StateMachine() + +fsm.set_global_timer( + index=1, + duration=5 +) + + +fsm.add_state( + name='StartGlobalTimer', + timer=0.25, + transitions={'Tup': 'Port1Light'}, + actions={'GlobalTimerTrig': 1}, +) +fsm.add_state( + name='Port1Light', + timer=0.25, + transitions={ + 'Tup': 'Port3Light', + 'GlobalTimer1_End': '>exit', + }, + actions={'PWM1': 255}, +) +fsm.add_state( + name='Port3Light', + timer=0.25, + transitions={ + 'Tup': '>exit', + 'GlobalTimer1_End': '>exit', + }, + actions={'PWM3': 255}, +) + + + +with Bpod() as bpod: + print(f"Connected to Bpod!") + print(f"Found Bpod on port {bpod.serial0.port}") + print(f"Firmware Version: {bpod.version.firmware}") + print(f"Hardware Version: {bpod.version.machine}") + print("Send State machine.") + bpod.send_state_machine(fsm) + print("Run State machine.") + bpod.run_state_machine() + print("State machine finished.") \ No newline at end of file From 39e58145dd9c207287d1dc1a817ad7b072d2f0ec Mon Sep 17 00:00:00 2001 From: MathiasMahn <53939819+MathiasMahn@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:23:02 +0100 Subject: [PATCH 2/3] additional debugging and working global counter SM and run added --- bpod_core/bpod.py | 10 +++++ examples/global_timers.py | 39 +++++++++++++++++++ ...=> minimal_example_run_global_timer_SM.py} | 10 ++--- 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 examples/global_timers.py rename examples/{minimal_example.py => minimal_example_run_global_timer_SM.py} (85%) diff --git a/bpod_core/bpod.py b/bpod_core/bpod.py index 85cf40ee..a8c866aa 100644 --- a/bpod_core/bpod.py +++ b/bpod_core/bpod.py @@ -818,6 +818,8 @@ def _compile_output_actions(self) -> None: if self.version.machine == 4: self.actions.extend(['AnalogThreshEnable', 'AnalogThreshDisable']) + + @property def port(self) -> str | None: """The port of the Bpod's primary serial device.""" @@ -899,6 +901,12 @@ def update_modules(self) -> None: self._compile_event_names() self._compile_output_actions() + def load_serial_message(self, module_index, message_id, message_bytes): + # Format: ord('L'), module_index(0-based), n_messages(1), msg_id, msg_len, msg_payload + header = struct.pack(' None: """ Validate the provided state machine for compatibility with the hardware. @@ -1256,6 +1264,8 @@ def pack_values(values: list[int], format_str: str) -> None: f'exit', # this is 1 indexed + }, + actions={'PWM1': 255}, +) +fsm.add_state( + name='Port3Light', + timer=0.25, + transitions={ + 'Tup': 'Port1Light', + 'GlobalTimer1_End': '>exit', # this is 1 indexed + }, + actions={'PWM3': 255}, +) \ No newline at end of file diff --git a/examples/minimal_example.py b/examples/minimal_example_run_global_timer_SM.py similarity index 85% rename from examples/minimal_example.py rename to examples/minimal_example_run_global_timer_SM.py index 6e1559f1..36e2dfbe 100644 --- a/examples/minimal_example.py +++ b/examples/minimal_example_run_global_timer_SM.py @@ -23,7 +23,7 @@ fsm = StateMachine() fsm.set_global_timer( - index=1, + index=0, # this is 0 indexed duration=5 ) @@ -32,14 +32,14 @@ name='StartGlobalTimer', timer=0.25, transitions={'Tup': 'Port1Light'}, - actions={'GlobalTimerTrig': 1}, + actions={'GlobalTimerTrig': 1}, # this is 1 indexed ) fsm.add_state( name='Port1Light', timer=0.25, transitions={ 'Tup': 'Port3Light', - 'GlobalTimer1_End': '>exit', + 'GlobalTimer1_End': '>exit', # this is 1 indexed }, actions={'PWM1': 255}, ) @@ -47,8 +47,8 @@ name='Port3Light', timer=0.25, transitions={ - 'Tup': '>exit', - 'GlobalTimer1_End': '>exit', + 'Tup': 'Port1Light', + 'GlobalTimer1_End': '>exit', # this is 1 indexed }, actions={'PWM3': 255}, ) From 9a26b385ee8a179dfca0a04cd7b6b044f607c3c6 Mon Sep 17 00:00:00 2001 From: MathiasMahn <53939819+MathiasMahn@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:04:20 +0100 Subject: [PATCH 3/3] extended debug logging to include event_names. Fixed global counter index in example --- bpod_core/bpod.py | 1 + bpod_core/fsm.py | 2 +- ...ld.ipynb => Graph_global_counter_SM.ipynb} | 0 examples/global_counters.py | 2 +- examples/global_timers.py | 8 +-- .../minimal_example_run_global_counter_SM.py | 72 +++++++++++++++++++ 6 files changed, 79 insertions(+), 6 deletions(-) rename examples/{hello_world.ipynb => Graph_global_counter_SM.ipynb} (100%) create mode 100644 examples/minimal_example_run_global_counter_SM.py diff --git a/bpod_core/bpod.py b/bpod_core/bpod.py index a8c866aa..288f3081 100644 --- a/bpod_core/bpod.py +++ b/bpod_core/bpod.py @@ -792,6 +792,7 @@ def _compile_event_names(self) -> None: ]: self.event_names.extend(event_name.format(i + 1) for i in range(n)) self.event_names.append('Tup') + logger.debug('Compiled event names: %s', self.event_names) def _compile_output_actions(self) -> None: """Compile the list of output actions supported by the Bpod hardware.""" diff --git a/bpod_core/fsm.py b/bpod_core/fsm.py index f2ea482e..cb16d003 100644 --- a/bpod_core/fsm.py +++ b/bpod_core/fsm.py @@ -462,7 +462,7 @@ def set_global_counter( threshold: GlobalCounterThreshold, ) -> None: """ - Configure a global timer with the specified parameters. + Configure a global counter with the specified parameters. Parameters ---------- diff --git a/examples/hello_world.ipynb b/examples/Graph_global_counter_SM.ipynb similarity index 100% rename from examples/hello_world.ipynb rename to examples/Graph_global_counter_SM.ipynb diff --git a/examples/global_counters.py b/examples/global_counters.py index 77534b58..38d0a36b 100644 --- a/examples/global_counters.py +++ b/examples/global_counters.py @@ -9,7 +9,7 @@ fsm = StateMachine() fsm.set_global_counter( - index=1, + index=0, # this is zero based, so this is Global Counter 1 event='Port1High', threshold=5, ) diff --git a/examples/global_timers.py b/examples/global_timers.py index 0df414dc..69890c87 100644 --- a/examples/global_timers.py +++ b/examples/global_timers.py @@ -8,7 +8,7 @@ fsm = StateMachine() fsm.set_global_timer( - index=0, # this is 0 indexed + index=0, # this is zero based, so this is Global Timer 1 duration=5 ) @@ -17,14 +17,14 @@ name='StartGlobalTimer', timer=0.25, transitions={'Tup': 'Port1Light'}, - actions={'GlobalTimerTrig': 1}, # this is 1 indexed + actions={'GlobalTimerTrig': 1}, # this is 1 based ) fsm.add_state( name='Port1Light', timer=0.25, transitions={ 'Tup': 'Port3Light', - 'GlobalTimer1_End': '>exit', # this is 1 indexed + 'GlobalTimer1_End': '>exit', # this is 1 based }, actions={'PWM1': 255}, ) @@ -33,7 +33,7 @@ timer=0.25, transitions={ 'Tup': 'Port1Light', - 'GlobalTimer1_End': '>exit', # this is 1 indexed + 'GlobalTimer1_End': '>exit', # this is 1 based }, actions={'PWM3': 255}, ) \ No newline at end of file diff --git a/examples/minimal_example_run_global_counter_SM.py b/examples/minimal_example_run_global_counter_SM.py new file mode 100644 index 00000000..f275fc53 --- /dev/null +++ b/examples/minimal_example_run_global_counter_SM.py @@ -0,0 +1,72 @@ +from bpod_core.fsm import StateMachine +from bpod_core.bpod import Bpod +import logging +import sys + + +LOG_FILE = "bpod_debug.log" + +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8'), + logging.StreamHandler(sys.stdout) # This also keeps logs appearing in the terminal + ] +) + +# 3. Specifically ensure the bpod_core library is set to DEBUG +logging.getLogger('bpod_core').setLevel(logging.DEBUG) + +print(f"Logging initialized. All Bpod traffic will be saved to: {LOG_FILE}") + +fsm = StateMachine() + +fsm.set_global_counter( + index=0, # this is zero based, so this is Global Counter 1 + event='BNC1_High', + threshold=5, +) + +fsm.add_state( + name='InitialDelay', + timer=2, + transitions={'Tup': 'ResetGlobalCounter'}, + actions={'PWM2': 255}, +) +fsm.add_state( + name='ResetGlobalCounter', + transitions={'Tup': 'Port1Light'}, + actions={'GlobalCounterReset': 1}, +) +fsm.add_state( + name='Port1Light', + timer=0.25, + transitions={ + 'Tup': 'Port3Light', + 'GlobalCounter1_End': '>exit', + }, + actions={'PWM1': 255}, +) +fsm.add_state( + name='Port3Light', + timer=0.25, + transitions={ + 'Tup': 'Port1Light', + 'GlobalCounter1_End': '>exit', + }, + actions={'PWM3': 255}, +) + + + +with Bpod() as bpod: + print(f"Connected to Bpod!") + print(f"Found Bpod on port {bpod.serial0.port}") + print(f"Firmware Version: {bpod.version.firmware}") + print(f"Hardware Version: {bpod.version.machine}") + print("Send State machine.") + bpod.send_state_machine(fsm) + print("Run State machine.") + bpod.run_state_machine() + print("State machine finished.") \ No newline at end of file