From d6a12a406bf71de54d7fba20f3f87e35252ad23e Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Tue, 21 Jul 2026 13:54:01 -0700 Subject: [PATCH 01/26] Add JackhammerAgent for OCS-driven SMuRF crate resets New agent wrapping sodetlib's jackhammer hammer() function, exposing it as an OCS task with the same options (slots, no_reboot, no_dump, skip_setup, dump_rogue). Co-Authored-By: Claude Opus 4.6 --- socs/agents/jackhammer/__init__.py | 0 socs/agents/jackhammer/agent.py | 111 +++++++++++++++++++++++++++++ socs/plugin.py | 1 + 3 files changed, 112 insertions(+) create mode 100644 socs/agents/jackhammer/__init__.py create mode 100644 socs/agents/jackhammer/agent.py diff --git a/socs/agents/jackhammer/__init__.py b/socs/agents/jackhammer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py new file mode 100644 index 000000000..ef397aa48 --- /dev/null +++ b/socs/agents/jackhammer/agent.py @@ -0,0 +1,111 @@ +import argparse +import os + +import txaio +from ocs import ocs_agent, site_config +from ocs.ocs_twisted import TimeoutLock + +txaio.use_twisted() + + +class JackhammerAgent: + """Agent to execute the sodetlib jackhammer hammer sequence. + + Parameters + ---------- + agent : OCSAgent + OCSAgent object which forms this Agent. + """ + + def __init__(self, agent): + self.agent = agent + self.log = agent.log + self.lock = TimeoutLock() + + @ocs_agent.param('slots', default=None) + @ocs_agent.param('no_reboot', default=False, type=bool) + @ocs_agent.param('no_dump', default=False, type=bool) + @ocs_agent.param('skip_setup', default=False, type=bool) + @ocs_agent.param('dump_rogue', default=False, type=bool) + def hammer(self, session, params): + """hammer(slots=None, no_reboot=False, no_dump=False, \ + skip_setup=False, dump_rogue=False) + + **Task** - Execute the jackhammer hammer sequence to reset and + reconfigure SMuRF slots. This replicates the ``jackhammer hammer`` + CLI command. + + Parameters + ---------- + slots : list of int, optional + Slot numbers to hammer. Defaults to all slots defined in + the sys_config. + no_reboot : bool + If True, perform a soft reset without rebooting the carriers. + no_dump : bool + If True, skip dumping docker logs before hammering. + skip_setup : bool + If True, skip pysmurf setup after reboot. + dump_rogue : bool + If True, dump the rogue tree before hammering. + + Notes + ----- + The session data object reports the current step of the hammer + sequence:: + + >>> response.session['data'] + {'status': 'done', + 'slots': [2, 3], + 'reboot': True} + """ + from sodetlib.hammers.jackhammer import hammer + + with self.lock.acquire_timeout(10, job='hammer') as acquired: + if not acquired: + return False, "Could not acquire lock" + + session.data = { + 'status': 'running', + 'slots': params.get('slots'), + 'reboot': not params['no_reboot'], + } + + hammer( + slots=params.get('slots'), + no_reboot=params['no_reboot'], + no_dump=params['no_dump'], + skip_setup=params['skip_setup'], + dump_rogue=params['dump_rogue'], + ) + + session.data['status'] = 'done' + + reboot_str = "soft" if params['no_reboot'] else "hard" + return True, f"Successfully {reboot_str}-hammered slots {params.get('slots')}" + + +def add_agent_args(parser=None): + if parser is None: + parser = argparse.ArgumentParser() + return parser + + +def main(args=None): + txaio.start_logging(level=os.environ.get("LOGLEVEL", "info")) + + parser = add_agent_args() + args = site_config.parse_args(agent_class='JackhammerAgent', + parser=parser, + args=args) + + agent, runner = ocs_agent.init_site_agent(args) + p = JackhammerAgent(agent) + + agent.register_task('hammer', p.hammer) + + runner.run(agent, auto_reconnect=True) + + +if __name__ == "__main__": + main() diff --git a/socs/plugin.py b/socs/plugin.py index 9f7873e3b..c8a6f7daf 100644 --- a/socs/plugin.py +++ b/socs/plugin.py @@ -22,6 +22,7 @@ 'HWPPMXAgent': {'module': 'socs.agents.hwp_pmx.agent', 'entry_point': 'main'}, 'HWPSupervisor': {'module': 'socs.agents.hwp_supervisor.agent', 'entry_point': 'main'}, 'ibootbarAgent': {'module': 'socs.agents.ibootbar.agent', 'entry_point': 'main'}, + 'JackhammerAgent': {'module': 'socs.agents.jackhammer.agent', 'entry_point': 'main'}, 'LabJackAgent': {'module': 'socs.agents.labjack.agent', 'entry_point': 'main'}, 'Lakeshore240Agent': {'module': 'socs.agents.lakeshore240.agent', 'entry_point': 'main'}, 'Lakeshore336Agent': {'module': 'socs.agents.lakeshore336.agent', 'entry_point': 'main'}, From 1643d6507b739d6a8840771faca4cc5918f07fb9 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Tue, 21 Jul 2026 13:56:21 -0700 Subject: [PATCH 02/26] Add exception handling to JackhammerAgent hammer task Store the traceback in session.data['error'] on failure so clients can inspect what went wrong. Co-Authored-By: Claude Opus 4.6 --- socs/agents/jackhammer/agent.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py index ef397aa48..13fb5b921 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/jackhammer/agent.py @@ -1,5 +1,6 @@ import argparse import os +import traceback import txaio from ocs import ocs_agent, site_config @@ -69,15 +70,22 @@ def hammer(self, session, params): 'status': 'running', 'slots': params.get('slots'), 'reboot': not params['no_reboot'], + 'error': None, } - hammer( - slots=params.get('slots'), - no_reboot=params['no_reboot'], - no_dump=params['no_dump'], - skip_setup=params['skip_setup'], - dump_rogue=params['dump_rogue'], - ) + try: + hammer( + slots=params.get('slots'), + no_reboot=params['no_reboot'], + no_dump=params['no_dump'], + skip_setup=params['skip_setup'], + dump_rogue=params['dump_rogue'], + ) + except Exception as e: + self.log.error("Hammer failed: {error}", error=e) + session.data['status'] = 'error' + session.data['error'] = traceback.format_exc() + return False, f"Hammer failed: {e}" session.data['status'] = 'done' From 847b2ccd214f5e3dba85570f5af82042fdee56cf Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Wed, 22 Jul 2026 10:09:31 -0400 Subject: [PATCH 03/26] fix(agents/jackhammer): Set up logging --- socs/agents/jackhammer/agent.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py index 13fb5b921..b0c95cf0c 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/jackhammer/agent.py @@ -6,8 +6,6 @@ from ocs import ocs_agent, site_config from ocs.ocs_twisted import TimeoutLock -txaio.use_twisted() - class JackhammerAgent: """Agent to execute the sodetlib jackhammer hammer sequence. @@ -100,6 +98,9 @@ def add_agent_args(parser=None): def main(args=None): + # set up logging + txaio.use_twisted() + log = txaio.make_logger() txaio.start_logging(level=os.environ.get("LOGLEVEL", "info")) parser = add_agent_args() From 38aee9814b6d0e861632c5b9332983240ef7a381 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Wed, 22 Jul 2026 10:10:26 -0400 Subject: [PATCH 04/26] fix(agents/jackhammer): Simplify agent setup and status. --- socs/agents/jackhammer/agent.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py index b0c95cf0c..35adb1285 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/jackhammer/agent.py @@ -65,7 +65,6 @@ def hammer(self, session, params): return False, "Could not acquire lock" session.data = { - 'status': 'running', 'slots': params.get('slots'), 'reboot': not params['no_reboot'], 'error': None, @@ -73,7 +72,7 @@ def hammer(self, session, params): try: hammer( - slots=params.get('slots'), + slots=params['slots'], no_reboot=params['no_reboot'], no_dump=params['no_dump'], skip_setup=params['skip_setup'], @@ -81,20 +80,10 @@ def hammer(self, session, params): ) except Exception as e: self.log.error("Hammer failed: {error}", error=e) - session.data['status'] = 'error' session.data['error'] = traceback.format_exc() return False, f"Hammer failed: {e}" - session.data['status'] = 'done' - - reboot_str = "soft" if params['no_reboot'] else "hard" - return True, f"Successfully {reboot_str}-hammered slots {params.get('slots')}" - - -def add_agent_args(parser=None): - if parser is None: - parser = argparse.ArgumentParser() - return parser + return True, f"Successfully hammered slots {params.get('slots')}" def main(args=None): @@ -103,9 +92,7 @@ def main(args=None): log = txaio.make_logger() txaio.start_logging(level=os.environ.get("LOGLEVEL", "info")) - parser = add_agent_args() args = site_config.parse_args(agent_class='JackhammerAgent', - parser=parser, args=args) agent, runner = ocs_agent.init_site_agent(args) From 1d5eb59f08688a2ae75398886c0d851f2ecea3d8 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Wed, 22 Jul 2026 11:02:57 -0400 Subject: [PATCH 05/26] Report per-slot hammer results in JackhammerAgent Use the new hammer() return value to populate session.data with succeeded_slots and failed_slots. Distinguish full success, partial success, and total failure in the task return message. Co-Authored-By: Claude Opus 4.6 --- socs/agents/jackhammer/agent.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py index 35adb1285..deac06689 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/jackhammer/agent.py @@ -34,6 +34,9 @@ def hammer(self, session, params): reconfigure SMuRF slots. This replicates the ``jackhammer hammer`` CLI command. + Individual slot failures are isolated so that the remaining + slots can still complete successfully. + Parameters ---------- slots : list of int, optional @@ -50,13 +53,14 @@ def hammer(self, session, params): Notes ----- - The session data object reports the current step of the hammer - sequence:: + The session data object reports per-slot results:: >>> response.session['data'] - {'status': 'done', - 'slots': [2, 3], - 'reboot': True} + {'slots': [2, 3], + 'reboot': True, + 'succeeded_slots': [2], + 'failed_slots': {3: 'EPICS connection timed out ...'}, + 'error': None} """ from sodetlib.hammers.jackhammer import hammer @@ -67,11 +71,13 @@ def hammer(self, session, params): session.data = { 'slots': params.get('slots'), 'reboot': not params['no_reboot'], + 'succeeded_slots': [], + 'failed_slots': {}, 'error': None, } try: - hammer( + result = hammer( slots=params['slots'], no_reboot=params['no_reboot'], no_dump=params['no_dump'], @@ -83,7 +89,20 @@ def hammer(self, session, params): session.data['error'] = traceback.format_exc() return False, f"Hammer failed: {e}" - return True, f"Successfully hammered slots {params.get('slots')}" + session.data['succeeded_slots'] = result['succeeded'] + session.data['failed_slots'] = result['failed'] + + succeeded = result['succeeded'] + failed = result['failed'] + + if not succeeded: + return False, f"All slots failed: {failed}" + if failed: + return True, ( + f"Partial success: slots {succeeded} succeeded, " + f"slots {list(failed.keys())} failed" + ) + return True, f"Successfully hammered slots {succeeded}" def main(args=None): From 5c61f63123f86ae388648142f86c15acd91a699d Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Wed, 22 Jul 2026 11:37:53 -0400 Subject: [PATCH 06/26] fix(agents/jackhammer): dump_logs parameter default to False. --- socs/agents/jackhammer/agent.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py index deac06689..72b56a9b2 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/jackhammer/agent.py @@ -23,11 +23,11 @@ def __init__(self, agent): @ocs_agent.param('slots', default=None) @ocs_agent.param('no_reboot', default=False, type=bool) - @ocs_agent.param('no_dump', default=False, type=bool) + @ocs_agent.param('dump_logs', default=False, type=bool) @ocs_agent.param('skip_setup', default=False, type=bool) @ocs_agent.param('dump_rogue', default=False, type=bool) def hammer(self, session, params): - """hammer(slots=None, no_reboot=False, no_dump=False, \ + """hammer(slots=None, no_reboot=False, dump_logs=False, \ skip_setup=False, dump_rogue=False) **Task** - Execute the jackhammer hammer sequence to reset and @@ -44,8 +44,8 @@ def hammer(self, session, params): the sys_config. no_reboot : bool If True, perform a soft reset without rebooting the carriers. - no_dump : bool - If True, skip dumping docker logs before hammering. + dump_logs : bool + If True, dump docker logs before hammering. skip_setup : bool If True, skip pysmurf setup after reboot. dump_rogue : bool @@ -80,7 +80,7 @@ def hammer(self, session, params): result = hammer( slots=params['slots'], no_reboot=params['no_reboot'], - no_dump=params['no_dump'], + no_dump=not params['dump_logs'], skip_setup=params['skip_setup'], dump_rogue=params['dump_rogue'], ) From 6a6293e5674c2da0a4e9f08a678d73620f0fadbb Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Wed, 22 Jul 2026 11:44:24 -0400 Subject: [PATCH 07/26] feat(agents/jackhammer): restrict to access level 2. --- socs/agents/jackhammer/agent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py index 72b56a9b2..ff57b5ff5 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/jackhammer/agent.py @@ -117,7 +117,8 @@ def main(args=None): agent, runner = ocs_agent.init_site_agent(args) p = JackhammerAgent(agent) - agent.register_task('hammer', p.hammer) + # restrict access to level 2 + agent.register_task('hammer', p.hammer, min_privs=2) runner.run(agent, auto_reconnect=True) From e1dd5ee37c4df3def634e5ba1a7d0cbdf06a45d9 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Wed, 22 Jul 2026 12:16:24 -0400 Subject: [PATCH 08/26] fix(agents/jackhammer): Add timestamp to session data. --- socs/agents/jackhammer/agent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py index ff57b5ff5..bd2fd620b 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/jackhammer/agent.py @@ -1,6 +1,7 @@ import argparse import os import traceback +import time import txaio from ocs import ocs_agent, site_config @@ -74,6 +75,7 @@ def hammer(self, session, params): 'succeeded_slots': [], 'failed_slots': {}, 'error': None, + 'timestamp': time.time(), } try: From bd688659eb367db2b37ad0c530a26dc843be7c29 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:16:46 +0000 Subject: [PATCH 09/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- socs/agents/jackhammer/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py index bd2fd620b..760fbae5c 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/jackhammer/agent.py @@ -1,7 +1,7 @@ import argparse import os -import traceback import time +import traceback import txaio from ocs import ocs_agent, site_config From 0b1b76d43629af080284d76f2b47a8a5b3f4b429 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Wed, 22 Jul 2026 14:24:01 -0400 Subject: [PATCH 10/26] fix(agents/jackhammer): Remove unused import, variable. --- socs/agents/jackhammer/agent.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py index 760fbae5c..1998ead4e 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/jackhammer/agent.py @@ -1,4 +1,3 @@ -import argparse import os import time import traceback @@ -110,7 +109,7 @@ def hammer(self, session, params): def main(args=None): # set up logging txaio.use_twisted() - log = txaio.make_logger() + _ = txaio.make_logger() txaio.start_logging(level=os.environ.get("LOGLEVEL", "info")) args = site_config.parse_args(agent_class='JackhammerAgent', From 30f841402b8d5cb4e01a13d225165601351e425c Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Thu, 23 Jul 2026 09:56:56 -0400 Subject: [PATCH 11/26] feat(agents/jackhammer): Add monitor process for slot configuration status Poll SystemConfigured and ConfiguringInProgress EPICS registers per slot every 10s via epics.caget. Publishes to a system_configured OCS feed and populates session.data for ocs-web display. Auto-starts on agent launch; suppress with --no-processes. Co-Authored-By: Claude Opus 4.6 --- socs/agents/jackhammer/agent.py | 126 +++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 2 deletions(-) diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/jackhammer/agent.py index 1998ead4e..e28807fc3 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/jackhammer/agent.py @@ -1,14 +1,17 @@ +import argparse import os import time import traceback +import epics import txaio from ocs import ocs_agent, site_config from ocs.ocs_twisted import TimeoutLock class JackhammerAgent: - """Agent to execute the sodetlib jackhammer hammer sequence. + """Agent to execute the sodetlib jackhammer hammer sequence and + monitor the configured status of each SMuRF slot. Parameters ---------- @@ -20,6 +23,15 @@ def __init__(self, agent): self.agent = agent self.log = agent.log self.lock = TimeoutLock() + self._monitor_running = False + + # get slots from config file + from sodetlib.hammers.jackhammer import sys_config + self.slot_order = list(sys_config['slot_order']) + + self.agent.register_feed('system_configured', + record=True, + buffer_time=0) @ocs_agent.param('slots', default=None) @ocs_agent.param('no_reboot', default=False, type=bool) @@ -105,19 +117,129 @@ def hammer(self, session, params): ) return True, f"Successfully hammered slots {succeeded}" + @ocs_agent.param('_') + def monitor(self, session, params): + """monitor() + + **Process** - Continuously monitor the configuration status of + each SMuRF slot by querying two EPICS registers: + + - ``AMCc.SmurfApplication.SystemConfigured`` — whether setup + has completed successfully. + - ``AMCc.SmurfApplication.ConfiguringInProgress`` — whether + setup is currently running. + + Notes + ----- + The session data object reports per-slot status:: + + >>> response.session['data'] + {'timestamp': 1721234567.0, + 'slots': { + 2: {'configured': True, 'configuring': False}, + 3: {'configured': False, 'configuring': True}, + 5: {'configured': None, 'configuring': None}, + }} + + For each register, ``True``/``False`` reflect the register + value and ``None`` means the EPICS query timed out (slot + unreachable). + """ + self._monitor_running = True + session.data = {} + + while self._monitor_running: + slot_status = {} + start = time.time() + feed_data = { + 'block_name': 'system_configured', + 'timestamp': start, + 'data': {}, + } + + for slot in self.slot_order: + epics_root = f'smurf_server_s{slot}' + pv_configured = f'{epics_root}:AMCc:SmurfApplication:SystemConfigured' + pv_configuring = f'{epics_root}:AMCc:SmurfApplication:ConfiguringInProgress' + + val_configured = epics.caget(pv_configured, timeout=5) + val_configuring = epics.caget(pv_configuring, timeout=5) + + if val_configured is None: + configured = None + feed_data['data'][f'configured_s{slot}'] = -1 + else: + configured = bool(int(val_configured)) + feed_data['data'][f'configured_s{slot}'] = int(configured) + + if val_configuring is None: + configuring = None + feed_data['data'][f'configuring_s{slot}'] = -1 + else: + configuring = bool(int(val_configuring)) + feed_data['data'][f'configuring_s{slot}'] = int(configuring) + + slot_status[slot] = { + 'configured': configured, + 'configuring': configuring, + } + + session.data = { + 'timestamp': feed_data['timestamp'], + 'slots': slot_status, + } + + if feed_data['data']: + self.agent.publish_to_feed('system_configured', feed_data) + + self.log.debug( + "Slot status: {status}", status=slot_status + ) + + # aim for 10s between samples + wait = 10 - (time.time() - start) + if wait > 0: + time.sleep(wait) + + return True, 'Monitor exited cleanly.' + + def _stop_monitor(self, session, params): + self._monitor_running = False + session.set_status('stopping') + return True, 'Stopping monitor.' + + +def add_agent_args(parser_in=None): + if parser_in is None: + parser_in = argparse.ArgumentParser() + pgroup = parser_in.add_argument_group('Agent Options') + pgroup.add_argument('--no-processes', action='store_true', + default=False, + help="Do not auto-start the monitor process.") + return parser_in + def main(args=None): # set up logging txaio.use_twisted() - _ = txaio.make_logger() txaio.start_logging(level=os.environ.get("LOGLEVEL", "info")) + parser = add_agent_args() args = site_config.parse_args(agent_class='JackhammerAgent', + parser=parser, args=args) + startup = not args.no_processes + agent, runner = ocs_agent.init_site_agent(args) p = JackhammerAgent(agent) + agent.register_process('monitor', + p.monitor, + p._stop_monitor, + blocking=False, + startup=startup) + # restrict access to level 2 agent.register_task('hammer', p.hammer, min_privs=2) From 95f9a0675d062ab2c7951aa95e7f01032541d12d Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Fri, 7 Aug 2026 11:46:10 -0700 Subject: [PATCH 12/26] fix(agents/jackhammer): Rename to smurf_hammer. --- socs/agents/{jackhammer => smurf_hammer}/__init__.py | 0 socs/agents/{jackhammer => smurf_hammer}/agent.py | 6 +++--- socs/plugin.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename socs/agents/{jackhammer => smurf_hammer}/__init__.py (100%) rename socs/agents/{jackhammer => smurf_hammer}/agent.py (98%) diff --git a/socs/agents/jackhammer/__init__.py b/socs/agents/smurf_hammer/__init__.py similarity index 100% rename from socs/agents/jackhammer/__init__.py rename to socs/agents/smurf_hammer/__init__.py diff --git a/socs/agents/jackhammer/agent.py b/socs/agents/smurf_hammer/agent.py similarity index 98% rename from socs/agents/jackhammer/agent.py rename to socs/agents/smurf_hammer/agent.py index e28807fc3..1b31c3e82 100644 --- a/socs/agents/jackhammer/agent.py +++ b/socs/agents/smurf_hammer/agent.py @@ -9,7 +9,7 @@ from ocs.ocs_twisted import TimeoutLock -class JackhammerAgent: +class SmurfHammerAgent: """Agent to execute the sodetlib jackhammer hammer sequence and monitor the configured status of each SMuRF slot. @@ -225,14 +225,14 @@ def main(args=None): txaio.start_logging(level=os.environ.get("LOGLEVEL", "info")) parser = add_agent_args() - args = site_config.parse_args(agent_class='JackhammerAgent', + args = site_config.parse_args(agent_class='SmurfHammerAgent', parser=parser, args=args) startup = not args.no_processes agent, runner = ocs_agent.init_site_agent(args) - p = JackhammerAgent(agent) + p = SmurfHammerAgent(agent) agent.register_process('monitor', p.monitor, diff --git a/socs/plugin.py b/socs/plugin.py index c8a6f7daf..18a6a485b 100644 --- a/socs/plugin.py +++ b/socs/plugin.py @@ -22,7 +22,6 @@ 'HWPPMXAgent': {'module': 'socs.agents.hwp_pmx.agent', 'entry_point': 'main'}, 'HWPSupervisor': {'module': 'socs.agents.hwp_supervisor.agent', 'entry_point': 'main'}, 'ibootbarAgent': {'module': 'socs.agents.ibootbar.agent', 'entry_point': 'main'}, - 'JackhammerAgent': {'module': 'socs.agents.jackhammer.agent', 'entry_point': 'main'}, 'LabJackAgent': {'module': 'socs.agents.labjack.agent', 'entry_point': 'main'}, 'Lakeshore240Agent': {'module': 'socs.agents.lakeshore240.agent', 'entry_point': 'main'}, 'Lakeshore336Agent': {'module': 'socs.agents.lakeshore336.agent', 'entry_point': 'main'}, @@ -42,6 +41,7 @@ 'RTSPCameraAgent': {'module': 'socs.agents.rtsp_camera.agent', 'entry_point': 'main'}, 'ScpiPsuAgent': {'module': 'socs.agents.scpi_psu.agent', 'entry_point': 'main'}, 'SmurfFileEmulator': {'module': 'socs.agents.smurf_file_emulator.agent', 'entry_point': 'main'}, + 'SmurfHammerAgent': {'module': 'socs.agents.smurf_hammer.agent', 'entry_point': 'main'}, 'SmurfStreamSimulator': {'module': 'socs.agents.smurf_stream_simulator.agent', 'entry_point': 'main'}, 'SmurfTimingCardAgent': {'module': 'socs.agents.smurf_timing_card.agent', 'entry_point': 'main'}, 'SRSCG635Agent': {'module': 'socs.agents.srs_cg635.agent', 'entry_point': 'main'}, From cdf00b3c0409d35b856b79f5e4d5cd29ed08f344 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Fri, 7 Aug 2026 11:47:11 -0700 Subject: [PATCH 13/26] docs: Add SmurfHammerAgent documentation Co-Authored-By: Claude Opus 4.6 --- docs/agents/smurf_hammer.rst | 89 ++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 90 insertions(+) create mode 100644 docs/agents/smurf_hammer.rst diff --git a/docs/agents/smurf_hammer.rst b/docs/agents/smurf_hammer.rst new file mode 100644 index 000000000..648633709 --- /dev/null +++ b/docs/agents/smurf_hammer.rst @@ -0,0 +1,89 @@ +.. highlight:: rst + +.. _smurf_hammer: + +================== +SMuRF Hammer Agent +================== + +The SMuRF Hammer Agent wraps sodetlib's ``jackhammer hammer`` CLI command as an +OCS agent. It operates on the crate controlled by the SMuRF server to which it +is deployed and accepts that usual ``jackhammer`` options. Slots for which the +hammer fails will be reported in the session data. There is also a monitoring +process to expose the configuration state of each slot. + +.. argparse:: + :filename: ../socs/agents/smurf_hammer/agent.py + :func: add_agent_args + :prog: python3 agent.py + +Dependencies +-------------- + +The SMuRF Hammer Agent requires the following packages: + + - `sodetlib `_ + +Additionally, ``socs`` should be installed with the ``pysmurf`` group: + +.. code-block:: bash + + $ pip install -U socs[pysmurf] + +Configuration File Examples +------------------------------ +Below are configuration examples for the ocs config file and for the +docker compose service. + +OCS Site Config +````````````````` +Here is an example of an agent configuration block for the ocs-site-config +file:: + + {'agent-class': 'SmurfHammerAgent', + 'instance-id': 'smurf-hammer', + 'arguments': []}, + +To suppress the auto-starting monitor process:: + + {'agent-class': 'SmurfHammerAgent', + 'instance-id': 'smurf-hammer', + 'arguments': ['--no-processes']}, + +If managed by ``HostManager``, this agent should be run on the host (not in a +Docker container) because it needs access to the Docker daemon to start and +stop SMuRF containers during the hammer sequence. + +Description +-------------- + +The agent exposes two operations: + +**hammer** (task) + Resets and reconfigures the specified SMuRF slots by calling + ``sodetlib.hammers.jackhammer.hammer()``. The sequence reboots the + carriers, waits for EPICS connectivity, and runs pysmurf setup. Failures + at each stage are caught per-slot, and the remaining slots continue. The + task is protected by a ``TimeoutLock`` and requires privilege level 2. + +**monitor** (process) + Continuously polls each slot's EPICS registers using ``epics.caget()`` + directly (bypassing pysmurf, so it works even when the system is + degraded). Two registers are queried per slot every 10 seconds: + + - ``AMCc.SmurfApplication.SystemConfigured`` -- whether pysmurf setup + has completed. + - ``AMCc.SmurfApplication.ConfiguringInProgress`` -- whether setup is + currently running. + + Results are published to the ``system_configured`` OCS feed for HK + archival and Grafana. The monitor auto-starts on agent launch unless + ``--no-processes`` is passed. + + Unreachable slots are recorded as ``None`` in session data and ``-1`` + in the feed. + +Agent API +----------- +.. autoclass:: socs.agents.smurf_hammer.agent.SmurfHammerAgent + :members: diff --git a/docs/index.rst b/docs/index.rst index 6cbf27e65..dca8b6a31 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -78,6 +78,7 @@ API Reference Full API documentation for core parts of the SOCS library. agents/rtsp_camera agents/scpi_psu agents/smurf_crate_monitor + agents/smurf_hammer agents/smurf_timing_card agents/srs_cg635 agents/stimulator_encoder From 4a68f73738bfebe4b31b8d5f1c026ab875227065 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Fri, 7 Aug 2026 13:08:59 -0700 Subject: [PATCH 14/26] fix(agents/smurf_hammer): Don't require access level for now. --- socs/agents/smurf_hammer/agent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/socs/agents/smurf_hammer/agent.py b/socs/agents/smurf_hammer/agent.py index 1b31c3e82..aefd5dc55 100644 --- a/socs/agents/smurf_hammer/agent.py +++ b/socs/agents/smurf_hammer/agent.py @@ -241,7 +241,8 @@ def main(args=None): startup=startup) # restrict access to level 2 - agent.register_task('hammer', p.hammer, min_privs=2) + # TODO: not supported yet + #agent.register_task('hammer', p.hammer, min_privs=2) runner.run(agent, auto_reconnect=True) From dd41e49bdd44594357b2d861e3d94202e5a08174 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:10:08 +0000 Subject: [PATCH 15/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- socs/agents/smurf_hammer/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socs/agents/smurf_hammer/agent.py b/socs/agents/smurf_hammer/agent.py index aefd5dc55..d76411242 100644 --- a/socs/agents/smurf_hammer/agent.py +++ b/socs/agents/smurf_hammer/agent.py @@ -242,7 +242,7 @@ def main(args=None): # restrict access to level 2 # TODO: not supported yet - #agent.register_task('hammer', p.hammer, min_privs=2) + # agent.register_task('hammer', p.hammer, min_privs=2) runner.run(agent, auto_reconnect=True) From 6c7470d37ca14925b96b6598cd6e9b0496d2922d Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Mon, 24 Aug 2026 17:25:38 +0000 Subject: [PATCH 16/26] fix(docker/pysmurf_controller): Add docker client for hammer agent. --- docker/pysmurf_controller/Dockerfile | 11 +++++++++++ docs/agents/smurf_hammer.rst | 4 ---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docker/pysmurf_controller/Dockerfile b/docker/pysmurf_controller/Dockerfile index c5500f161..26f396216 100644 --- a/docker/pysmurf_controller/Dockerfile +++ b/docker/pysmurf_controller/Dockerfile @@ -1,5 +1,16 @@ FROM simonsobs/so_smurf_base:v0.0.9 +# Download and extract just the Docker CLI binary +RUN curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-24.0.2.tgz \ + | tar -xz -C /tmp \ + && mv /tmp/docker/docker /usr/local/bin/ \ + && rm -rf /tmp/docker + +# Create the docker group +ARG DOCKER_GID=999 +RUN groupadd -g ${DOCKER_GID} docker && \ + usermod -aG docker cryo + ################################################################# # sodetlib Install ################################################################# diff --git a/docs/agents/smurf_hammer.rst b/docs/agents/smurf_hammer.rst index 648633709..bf09d8c82 100644 --- a/docs/agents/smurf_hammer.rst +++ b/docs/agents/smurf_hammer.rst @@ -50,10 +50,6 @@ To suppress the auto-starting monitor process:: 'instance-id': 'smurf-hammer', 'arguments': ['--no-processes']}, -If managed by ``HostManager``, this agent should be run on the host (not in a -Docker container) because it needs access to the Docker daemon to start and -stop SMuRF containers during the hammer sequence. - Description -------------- From 0f637034800063304a70be06ae839b8abeaf70a0 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Tue, 25 Aug 2026 15:11:38 -0700 Subject: [PATCH 17/26] fix(agents/smurf_hammer): Restore register_task. --- socs/agents/smurf_hammer/agent.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/socs/agents/smurf_hammer/agent.py b/socs/agents/smurf_hammer/agent.py index d76411242..4e127f3aa 100644 --- a/socs/agents/smurf_hammer/agent.py +++ b/socs/agents/smurf_hammer/agent.py @@ -240,9 +240,7 @@ def main(args=None): blocking=False, startup=startup) - # restrict access to level 2 - # TODO: not supported yet - # agent.register_task('hammer', p.hammer, min_privs=2) + agent.register_task('hammer', p.hammer) runner.run(agent, auto_reconnect=True) From 0e355f7f1a2c74fbce1726877e64acd6bf4648b1 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Tue, 25 Aug 2026 15:33:56 -0700 Subject: [PATCH 18/26] fix(agents/smurf_hammer): Set blocking flag for monitor. --- socs/agents/smurf_hammer/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socs/agents/smurf_hammer/agent.py b/socs/agents/smurf_hammer/agent.py index 4e127f3aa..c0dbc9dfa 100644 --- a/socs/agents/smurf_hammer/agent.py +++ b/socs/agents/smurf_hammer/agent.py @@ -237,7 +237,7 @@ def main(args=None): agent.register_process('monitor', p.monitor, p._stop_monitor, - blocking=False, + blocking=True, startup=startup) agent.register_task('hammer', p.hammer) From a7ddb963fe43cce226149da6b5c98da3cc0a2da6 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Tue, 25 Aug 2026 15:45:40 -0700 Subject: [PATCH 19/26] fix(agents/smurf_hammer): Query slot status concurrently. --- socs/agents/smurf_hammer/agent.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/socs/agents/smurf_hammer/agent.py b/socs/agents/smurf_hammer/agent.py index c0dbc9dfa..8ea6edfc0 100644 --- a/socs/agents/smurf_hammer/agent.py +++ b/socs/agents/smurf_hammer/agent.py @@ -157,13 +157,17 @@ def monitor(self, session, params): 'data': {}, } + pv_names = [] for slot in self.slot_order: epics_root = f'smurf_server_s{slot}' - pv_configured = f'{epics_root}:AMCc:SmurfApplication:SystemConfigured' - pv_configuring = f'{epics_root}:AMCc:SmurfApplication:ConfiguringInProgress' + pv_names.append(f'{epics_root}:AMCc:SmurfApplication:SystemConfigured') + pv_names.append(f'{epics_root}:AMCc:SmurfApplication:ConfiguringInProgress') - val_configured = epics.caget(pv_configured, timeout=5) - val_configuring = epics.caget(pv_configuring, timeout=5) + values = epics.caget_many(pv_names, timeout=5, connection_timeout=5) + + for i, slot in enumerate(self.slot_order): + val_configured = values[2 * i] + val_configuring = values[2 * i + 1] if val_configured is None: configured = None From b67d365df6e5c773302f0e37a04351d7651b130f Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Fri, 28 Aug 2026 00:27:07 +0000 Subject: [PATCH 20/26] fix(smurf_hammer): Handle exception for epics query. --- socs/agents/smurf_hammer/agent.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/socs/agents/smurf_hammer/agent.py b/socs/agents/smurf_hammer/agent.py index 8ea6edfc0..9b73d316c 100644 --- a/socs/agents/smurf_hammer/agent.py +++ b/socs/agents/smurf_hammer/agent.py @@ -163,7 +163,11 @@ def monitor(self, session, params): pv_names.append(f'{epics_root}:AMCc:SmurfApplication:SystemConfigured') pv_names.append(f'{epics_root}:AMCc:SmurfApplication:ConfiguringInProgress') - values = epics.caget_many(pv_names, timeout=5, connection_timeout=5) + try: + values = epics.caget_many(pv_names, timeout=5, connection_timeout=5) + except Exception as e: + self.log.error(f"EPICS query failed with: {e}") + values = [None] * len(pv_names) for i, slot in enumerate(self.slot_order): val_configured = values[2 * i] From e36885d1704605596b58a22b163e193df43dc69f Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Thu, 27 Aug 2026 17:39:41 -0700 Subject: [PATCH 21/26] revert(docker/pysmurf_controller): Revert changes to Dockerfile. --- docker/pysmurf_controller/Dockerfile | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docker/pysmurf_controller/Dockerfile b/docker/pysmurf_controller/Dockerfile index 26f396216..c5500f161 100644 --- a/docker/pysmurf_controller/Dockerfile +++ b/docker/pysmurf_controller/Dockerfile @@ -1,16 +1,5 @@ FROM simonsobs/so_smurf_base:v0.0.9 -# Download and extract just the Docker CLI binary -RUN curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-24.0.2.tgz \ - | tar -xz -C /tmp \ - && mv /tmp/docker/docker /usr/local/bin/ \ - && rm -rf /tmp/docker - -# Create the docker group -ARG DOCKER_GID=999 -RUN groupadd -g ${DOCKER_GID} docker && \ - usermod -aG docker cryo - ################################################################# # sodetlib Install ################################################################# From 25df41aef9fe2fc28b056e1a2b288f7cf6764671 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Thu, 17 Sep 2026 10:03:48 -0700 Subject: [PATCH 22/26] docs(smurf_hammer): Add instructions for installing EPICS. --- docs/agents/smurf_hammer.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/agents/smurf_hammer.rst b/docs/agents/smurf_hammer.rst index bf09d8c82..df0c2f00c 100644 --- a/docs/agents/smurf_hammer.rst +++ b/docs/agents/smurf_hammer.rst @@ -23,6 +23,25 @@ Dependencies The SMuRF Hammer Agent requires the following packages: - `sodetlib `_ + - `pyepics `_ + +pyepics requires EPICS to be installed on the host. The easiest +way to do this is to follow the installation steps present in the +`Dockerfile from smurf_docker `_ +To build that version of EPICS I found I needed to use the following +compiler flags: + +.. code-block:: bash + + -std=c++98 -fno-access-control + +Note that it is also necessary to set the EPICS environment variables +from the Dockerfile in the host's .bashrc and also the following: + +.. code-block:: bash + + EPICS_CA_ADDR_LIST=127.255.255.255 + EPICS_CA_MAX_ARRAY_BYTES=80000000 Additionally, ``socs`` should be installed with the ``pysmurf`` group: From c8273c0be999e56d80801772e61856a600bbbc38 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Thu, 17 Sep 2026 11:38:51 -0700 Subject: [PATCH 23/26] fix(smurf_hammer): Address PR comments. - remove TimeoutLock - register feed with buffer time - partial success returns False - log exceptions, don't put in session data - tweak docs --- docs/agents/smurf_hammer.rst | 8 ++-- socs/agents/smurf_hammer/agent.py | 61 +++++++++++++++---------------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/docs/agents/smurf_hammer.rst b/docs/agents/smurf_hammer.rst index df0c2f00c..ea228a673 100644 --- a/docs/agents/smurf_hammer.rst +++ b/docs/agents/smurf_hammer.rst @@ -8,7 +8,7 @@ SMuRF Hammer Agent The SMuRF Hammer Agent wraps sodetlib's ``jackhammer hammer`` CLI command as an OCS agent. It operates on the crate controlled by the SMuRF server to which it -is deployed and accepts that usual ``jackhammer`` options. Slots for which the +is deployed and accepts the usual ``jackhammer`` options. Slots for which the hammer fails will be reported in the session data. There is also a monitoring process to expose the configuration state of each slot. @@ -23,9 +23,8 @@ Dependencies The SMuRF Hammer Agent requires the following packages: - `sodetlib `_ - - `pyepics `_ -pyepics requires EPICS to be installed on the host. The easiest +jackhammer requires EPICS to be installed on the host. The easiest way to do this is to follow the installation steps present in the `Dockerfile from smurf_docker `_ To build that version of EPICS I found I needed to use the following @@ -78,8 +77,7 @@ The agent exposes two operations: Resets and reconfigures the specified SMuRF slots by calling ``sodetlib.hammers.jackhammer.hammer()``. The sequence reboots the carriers, waits for EPICS connectivity, and runs pysmurf setup. Failures - at each stage are caught per-slot, and the remaining slots continue. The - task is protected by a ``TimeoutLock`` and requires privilege level 2. + at each stage are caught per-slot, and the remaining slots continue. **monitor** (process) Continuously polls each slot's EPICS registers using ``epics.caget()`` diff --git a/socs/agents/smurf_hammer/agent.py b/socs/agents/smurf_hammer/agent.py index 9b73d316c..bc18bd31e 100644 --- a/socs/agents/smurf_hammer/agent.py +++ b/socs/agents/smurf_hammer/agent.py @@ -7,6 +7,7 @@ import txaio from ocs import ocs_agent, site_config from ocs.ocs_twisted import TimeoutLock +from sodetlib.hammers.jackhammer import sys_config, hammer class SmurfHammerAgent: @@ -26,12 +27,11 @@ def __init__(self, agent): self._monitor_running = False # get slots from config file - from sodetlib.hammers.jackhammer import sys_config self.slot_order = list(sys_config['slot_order']) self.agent.register_feed('system_configured', record=True, - buffer_time=0) + buffer_time=1) @ocs_agent.param('slots', default=None) @ocs_agent.param('no_reboot', default=False, type=bool) @@ -74,36 +74,35 @@ def hammer(self, session, params): 'failed_slots': {3: 'EPICS connection timed out ...'}, 'error': None} """ - from sodetlib.hammers.jackhammer import hammer - with self.lock.acquire_timeout(10, job='hammer') as acquired: - if not acquired: - return False, "Could not acquire lock" - - session.data = { - 'slots': params.get('slots'), - 'reboot': not params['no_reboot'], - 'succeeded_slots': [], - 'failed_slots': {}, - 'error': None, - 'timestamp': time.time(), - } - - try: - result = hammer( - slots=params['slots'], - no_reboot=params['no_reboot'], - no_dump=not params['dump_logs'], - skip_setup=params['skip_setup'], - dump_rogue=params['dump_rogue'], - ) - except Exception as e: - self.log.error("Hammer failed: {error}", error=e) - session.data['error'] = traceback.format_exc() - return False, f"Hammer failed: {e}" + session.data = { + 'slots': params.get('slots'), + 'reboot': not params['no_reboot'], + 'succeeded_slots': [], + 'failed_slots': {}, + 'error': None, + 'timestamp': time.time(), + } + + try: + result = hammer( + slots=params['slots'], + no_reboot=params['no_reboot'], + no_dump=not params['dump_logs'], + skip_setup=params['skip_setup'], + dump_rogue=params['dump_rogue'], + ) + except Exception as e: + self.log.error( + "Hammer failed: {error}\n{traceback}", + error=e, + traceback=traceback.format_exc() + ) + session.data['error'] = f"{e}" + return False, f"Hammer failed: {e}" - session.data['succeeded_slots'] = result['succeeded'] - session.data['failed_slots'] = result['failed'] + session.data['succeeded_slots'] = result['succeeded'] + session.data['failed_slots'] = result['failed'] succeeded = result['succeeded'] failed = result['failed'] @@ -111,7 +110,7 @@ def hammer(self, session, params): if not succeeded: return False, f"All slots failed: {failed}" if failed: - return True, ( + return False, ( f"Partial success: slots {succeeded} succeeded, " f"slots {list(failed.keys())} failed" ) From 43f0828dbc1f0dfb05ea7f15de1d81c491a58c14 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:40:53 +0000 Subject: [PATCH 24/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- socs/agents/smurf_hammer/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socs/agents/smurf_hammer/agent.py b/socs/agents/smurf_hammer/agent.py index bc18bd31e..004d3b35b 100644 --- a/socs/agents/smurf_hammer/agent.py +++ b/socs/agents/smurf_hammer/agent.py @@ -7,7 +7,7 @@ import txaio from ocs import ocs_agent, site_config from ocs.ocs_twisted import TimeoutLock -from sodetlib.hammers.jackhammer import sys_config, hammer +from sodetlib.hammers.jackhammer import hammer, sys_config class SmurfHammerAgent: From 7eaf24431fe648a32f818e282a8ba28feaa5e5e7 Mon Sep 17 00:00:00 2001 From: Tristan Pinsonneault-Marotte Date: Thu, 17 Sep 2026 11:51:21 -0700 Subject: [PATCH 25/26] fix(docs): Mock import for smurf_hammer agent. --- docs/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 3cf1f0150..298ba11eb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -108,6 +108,8 @@ 'sodetlib', 'sodetlib.operations', 'sodetlib.det_config', + 'sodetlib.hammers', + 'sodetlib.hammers.jackhammer', 'src', 'src.pid_controller', 'cv2', From 8148838911e0eef65541018f7cfda0461735d9ce Mon Sep 17 00:00:00 2001 From: Brian Koopman Date: Tue, 22 Sep 2026 14:57:25 -0400 Subject: [PATCH 26/26] Add AI assitance notices --- docs/agents/smurf_hammer.rst | 3 +++ socs/agents/smurf_hammer/agent.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/docs/agents/smurf_hammer.rst b/docs/agents/smurf_hammer.rst index ea228a673..3efe61ed7 100644 --- a/docs/agents/smurf_hammer.rst +++ b/docs/agents/smurf_hammer.rst @@ -6,6 +6,9 @@ SMuRF Hammer Agent ================== +.. image:: https://img.shields.io/badge/AI-assisted-orange + :alt: Agent written with AI assitance + The SMuRF Hammer Agent wraps sodetlib's ``jackhammer hammer`` CLI command as an OCS agent. It operates on the crate controlled by the SMuRF server to which it is deployed and accepts the usual ``jackhammer`` options. Slots for which the diff --git a/socs/agents/smurf_hammer/agent.py b/socs/agents/smurf_hammer/agent.py index 004d3b35b..340feeac4 100644 --- a/socs/agents/smurf_hammer/agent.py +++ b/socs/agents/smurf_hammer/agent.py @@ -1,3 +1,5 @@ +# Agent developed with AI assistance. + import argparse import os import time