• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

simonsobs / socs / 15356518725

30 May 2025 09:59PM UTC coverage: 38.156% (-0.01%) from 38.167%
15356518725

Pull #889

github

web-flow
Merge 7e6ad62b8 into 99d37db7a
Pull Request #889: hwp-gripper: fix alarm message handling

0 of 1 new or added line in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

7165 of 18778 relevant lines covered (38.16%)

0.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.14
/socs/agents/hwp_gripper/agent.py
1
import argparse
1✔
2
import time
1✔
3
from typing import Optional
1✔
4

5
from ocs import ocs_agent, site_config
1✔
6
from ocs.ocs_twisted import TimeoutLock
1✔
7

8
import socs.agents.hwp_gripper.drivers.gripper_client as cli
1✔
9
from socs.agents.hwp_supervisor.agent import get_op_data
1✔
10

11

12
class HWPGripperAgent:
1✔
13
    """
14
    Agent for controlling/monitoring the HWP's three LEY32C-30 linear actuators.
15
    This interfaces with the GripperServer running on the beaglebone
16
    microcontroller (https://github.com/simonsobs/sobonelib/blob/main/hwp_gripper/control/gripper_server.py).
17

18
    This agent will issue commands to the microcontroller via OCS, and publish
19
    gripper position and limit switch states to grafana.
20

21
    Args:
22
        agent (ocs.ocs_agent.OCSAgent):
23
            Agent instance
24
        args (argparse.Namespace):
25
            Parsed command line arguments namespace.
26
    """
27

28
    def __init__(self, agent, args):
1✔
29
        self.agent = agent
1✔
30
        self.log = agent.log
1✔
31
        self.client_lock = TimeoutLock()
1✔
32

33
        self._initialized = False
1✔
34
        self.mcu_ip = args.mcu_ip
1✔
35
        self.control_port = args.control_port
1✔
36
        self.warm_grip_distance = args.warm_grip_distance
1✔
37
        self.adjustment_distance = args.adjustment_distance
1✔
38

39
        self.shutdown_mode = False
1✔
40
        self.supervisor_id = args.supervisor_id
1✔
41

42
        self.client: Optional[cli.GripperClient] = None
1✔
43

44
        self.decoded_alarm_group = {False: 'No alarm was detected',
1✔
45
                                    'A': 'Unrecognized error',
46
                                    'B': 'Controller saved parameter issue',
47
                                    'C': 'Issue with position calibration',
48
                                    'D': 'Could not reach position within time limit',
49
                                    'E': 'General controller erorr'}
50

51
        agg_params = {'frame_length': 60}
1✔
52
        self.agent.register_feed('hwp_gripper', record=True, agg_params=agg_params)
1✔
53
        self.agent.register_feed('gripper_action', record=True)
1✔
54

55
    def _run_client_func(self, func, *args, lock_timeout=10,
1✔
56
                         job=None, check_shutdown=True, **kwargs):
57
        """
58
        Args
59
        ----
60
        func (function):
61
            Function to run
62
        *args (positional args):
63
            Additional args to pass to function
64
        lock_timeout (float):
65
            Time (sec) to wait to acquire client-lock before throwing an error.
66
        job (str):
67
            Name of job to attach to lock
68
        check_shutdown (bool):
69
            If true, will block the client function and throw an error if
70
            shutdown mode is in effect.
71
        """
72
        if self.shutdown_mode and check_shutdown:
1✔
73
            raise RuntimeError(
×
74
                'Cannot run client function, shutdown mode is in effect'
75
            )
76

77
        lock_kw = {'timeout': lock_timeout}
1✔
78
        if job is not None:
1✔
79
            lock_kw['job'] = job
1✔
80
        with self.client_lock.acquire_timeout(**lock_kw) as acquired:
1✔
81
            if not acquired:
1✔
82
                self.log.error(
×
83
                    f"Could not acquire lock! Job {self.client_lock.job} is "
84
                    "already running."
85
                )
86
                raise TimeoutError('Could not acquire lock')
×
87

88
            return_dict = func(*args, **kwargs)
1✔
89

90
        for line in return_dict['log']:
1✔
91
            self.log.debug(line)
×
92
        return return_dict
1✔
93

94
    def _get_hwp_freq(self):
1✔
95
        if self.supervisor_id is None:
1✔
96
            raise ValueError("No Supervisor ID set")
×
97

98
        res = get_op_data(self.supervisor_id, 'monitor')
1✔
99
        return res['data']['hwp_state']['pid_current_freq']
1✔
100

101
    def _check_stopped(self):
1✔
102
        return self._get_hwp_freq() < 0.1
×
103

104
    def init_connection(self, session, params):
1✔
105
        """init_connection()
106

107
        **Task** - Initialize connection to the GripperServer on the BeagleBone
108
        micro-controller
109
        """
110
        if self._initialized:
1✔
111
            self.log.info('Connection already initialized. Returning...')
×
112
            return True, 'Connection already initialized'
×
113

114
        self.client = cli.GripperClient(self.mcu_ip, self.control_port)
1✔
115
        self.log.info("Initialized Client")
1✔
116
        self._initialized = True
1✔
117
        return True, 'Initialized connection to GripperServer'
1✔
118

119
    @ocs_agent.param('state', default=True, type=bool)
1✔
120
    def power(self, session, params=None):
1✔
121
        """power(state=True)
122

123
        **Task** - If turning on, will power on the linear actuators and disengage
124
        brakes. If turning off, will cut power to the linear actuators and engage
125
        brakes.
126

127
        Parameters:
128
            state (bool): State to set the actuator power to.
129

130
        Notes:
131
            The most recent data collected is stored in session data in the
132
            structure:
133

134
                >>> response.session['response']
135
                {'result': True,
136
                 'log': ["SVON turned on in Control.ON()",
137
                         "Turned off BRAKE for axis 1 in Control.BRAKE()",
138
                         "Turned off BRAKE for axis 2 in Control.BRAKE()",
139
                         "Turned off BRAKE for axis 3 in Control.BRAKE()",
140
                         "Successfully turned off BRAKE for axis 1 in Control.BRAKE()",
141
                         "Successfully turned off BRAKE for axis 2 in Control.BRAKE()",
142
                         "Successfully turned off BRAKE for axis 3 in Control.BRAKE()",
143
                         "Disengaged brakes in Control.ON()"]}
144
        """
145
        return_dict = self._run_client_func(
×
146
            self.client.power, params['state'], job='power'
147
        )
148
        session.data['response'] = return_dict
×
149
        return return_dict['result'], f"Success: {return_dict['result']}"
×
150

151
    @ocs_agent.param('state', default=True, type=bool)
1✔
152
    @ocs_agent.param('actuator', default=0, type=int, check=lambda x: 0 <= x <= 3)
1✔
153
    def brake(self, session, params=None):
1✔
154
        """brake(state=True, actuator=0)
155

156
        **Task** - Controls actuator brakes
157

158
        Parameters:
159
            state (bool):
160
                State to set the actuator brake to. Takes bool input
161
            actuator (int):
162
                Actuator number. Takes input of 0-3 with 1-3 controlling and
163
                individual actuator and 0 controlling all three
164

165
        Notes:
166
            The most recent data collected is stored in session data in the
167
            structure:
168

169
                >>> response.session['response']
170
                {'result': True,
171
                 'log': ["Turned off BRAKE for axis 1 in Control.BRAKE()",
172
                         "Successfully turned off BRAKE for axis 1 in Control.BRAKE()"]}
173
        """
174
        return_dict = self._run_client_func(
×
175
            self.client.brake, params['state'], params['actuator'], job='brake'
176
        )
177
        session.data['response'] = return_dict
×
178
        return return_dict['result'], f"Success: {return_dict['result']}"
×
179

180
    @ocs_agent.param('mode', default='push', type=str, choices=['push', 'pos'])
1✔
181
    @ocs_agent.param('actuator', default=1, type=int, check=lambda x: 1 <= x <= 3)
1✔
182
    @ocs_agent.param('distance', default=0, type=float)
1✔
183
    def move(self, session, params=None):
1✔
184
        """move(mode='push', actuator=1, distance=0)
185

186
        **Task** - Move an actuator a specific distance. If the HWP is spinning,
187
        this task will not run.
188

189
        Parameters:
190
            mode (str):
191
                Movement mode. Takes inputs of 'pos' (positioning) or 'push'
192
                (pushing)
193
            actuator (int):
194
                Actuator number 1-3
195
            distance (float):
196
                Distance to move (mm). Takes positive and negative numbers for
197
                'pos' mode. Takes only positive numbers for 'push' mode. Value
198
                should be a multiple of 0.1.
199

200
        Notes:
201
            Positioning mode is used when you want to position the actuators without
202
            gripping the rotor. Pushing mode is used when you want the grip the
203
            rotor.
204

205
            The most recent data collected is stored in session data in the
206
            structure:
207

208
                >>> response.session['response']
209
                {'result': True,
210
                 'log': ["Control.STEP() operation finished for step 1",
211
                         "MOVE in Gripper.MOVE() completed successfully"]}
212
        """
213

214
        if self._get_hwp_freq() > 0.1:
×
215
            self.log.warn("Not moving actuators while HWP is spinning")
×
216
            return False, "HWP is spinning, not moving actuators"
×
217

218
        return_dict = self._run_client_func(
×
219
            self.client.move, params['mode'], params['actuator'],
220
            params['distance'], job='move'
221
        )
222
        session.data['response'] = return_dict
×
223
        return return_dict['result'], f"Success: {return_dict['result']}"
×
224

225
    def home(self, session, params=None):
1✔
226
        """home()
227
        **Task** - Homes and recalibrates the position of the actuators
228

229
        Note:
230
            This action must be done first after a power cycle. Otherwise the
231
            controller will throw an error.
232

233
            The most recent data collected is stored in session data in the
234
            structure:
235

236
                >>> response.session['response']
237
                {'result': True,
238
                 'log': ["SVON turned on in Control.ON()",
239
                         "'HOME' operation finished in Control.HOME()",
240
                         "HOME operation in Gripper.HOME() completed"]}
241
        """
242
        return_dict = self._run_client_func(self.client.home, job='home')
×
243
        session.data['response'] = return_dict
×
244
        return return_dict['result'], f"Success: {return_dict['result']}"
×
245

246
    def inp(self, session, params=None):
1✔
247
        """inp()
248

249
        **Task** - Queries whether the actuators are in a known position. This
250
        tells you whether the windows software has detected that the actuator
251
        has been homed.
252

253
        Notes:
254
            The most recent data collected is stored in session data in the
255
            structure:
256

257
                >>> response.session['response']
258
                {'result': True,
259
                 'log': []}
260
        """
261
        return_dict = self._run_client_func(
×
262
            self.client.inp, job='INP', check_shutdown=False)
263
        session.data['response'] = return_dict
×
264
        return return_dict['result'], f"Success: {return_dict['result']}"
×
265

266
    def alarm(self, session, params=None):
1✔
267
        """alarm()
268

269
        **Task** - Queries the actuator controller alarm state
270

271
        Notes:
272
            The most recent data collected is stored in session data in the
273
            structure:
274

275
                >>> response.session['response']
276
                {'result': True,
277
                 'log': ["ALARM = False"]}
278
        """
279
        return_dict = self._run_client_func(
×
280
            self.client.alarm, job='alarm', check_shutdown=False)
281
        session.data['response'] = return_dict
×
282
        return return_dict['result'], f"Success: {return_dict['result']}"
×
283

284
    def alarm_group(self, session, params=None):
1✔
285
        """alarm_group()
286

287
        **Task** - Queries the actuator controller alarm group
288

289
        Notes:
290
            Return one of six values depending on the controller alarm state
291
                False: No alarm was detected
292
                'A': Unrecognized error
293
                'B': Controller saved parameter issue
294
                'C': Issue with position calibration
295
                'D': Could not reach position within time limit
296
                'E': General controller error
297

298
            The most recent data collected is stored in session data in the
299
            structure:
300

301
                >>> response.session['response']
302
                {'result': 'B',
303
                 'log': ["ALARM GROUP B detected"]}
304
        """
305
        state_return_dict = self._run_client_func(
×
306
            self.client.get_state, job='get_state', check_shutdown=False)
307

308
        if not bool(state_return_dict['result']['jxc']['alarm']):
×
309
            return_dict = {'result': False, 'log': ['Alarm not triggered']}
×
310
        else:
311
            return_dict = self._run_client_func(
×
312
                self.client.alarm_group, job='alarm_group', check_shutdown=False)
313

314
            if return_dict['result'] is None:
×
315
                return_dict['result'] = 'A'
×
316

317
        session.data['response'] = return_dict
×
318
        session.data['decoded_alarm_group'] = self.decoded_alarm_group[return_dict['result']]
×
319
        return return_dict['result'], f"Success: {return_dict['result']}"
×
320

321
    def reset(self, session, params=None):
1✔
322
        """reset()
323
        **Task** - Resets the current active controller alarm
324

325
        Notes:
326
            The most recent data collected is stored in session data in the
327
            structure:
328

329
                >>> response.session['response']
330
                {'result': True,
331
                 'log': ["Ignored Control.ALARM_GROUP(). No ALARM detected",
332
                         "RESET aborted in Gripper.RESET() due to no detected alarm"]}
333
        """
334
        return_dict = self._run_client_func(self.client.reset, job='reset')
×
335
        session.data['response'] = return_dict
×
336
        return return_dict['result'], f"Success: {return_dict['result']}"
×
337

338
    @ocs_agent.param('actuator', default=1, type=int, check=lambda x: 1 <= x <= 3)
1✔
339
    def act(self, session, params=None):
1✔
340
        """act(actuator=1)
341

342
        **Task** - Queries whether an actuator is connected
343

344
        Parameters:
345
            actuator (int): Actuator number 1-3
346

347
        Notes:
348
            The most recent data collected is stored in session data in the
349
            structure:
350

351
                >>> response.session['response']
352
                {'result': True,
353
                 'log': ["Actuator 1 is in state 1"]}
354
        """
355
        return_dict = self._run_client_func(
×
356
            self.client.act, params['actuator'], job='act', check_shutdown=False)
357
        session.data['response'] = return_dict
×
358
        return return_dict['result'], f"Success: {return_dict['result']}"
×
359

360
    @ocs_agent.param('value', type=bool)
1✔
361
    def is_cold(self, session, params=None):
1✔
362
        """is_cold(value=False)
363

364
        **Task** - Set the limit switches to warm/cold grip configuration
365

366
        Parameters:
367
            value (bool): Set to warm grip (False) or cold grip (True)
368

369
        Notes:
370
            Configures the software to query the correct set of limit switches.
371
            Warm grip configuration enables both warm and cold limit switches.
372
            Cold grip configuration enables only cold limit switches. The maximum
373
            extension of the actuators depends on the cryostat temperature.
374

375
            The most recent data collected is stored in session data in the
376
            structure:
377

378
                >>> response.session['response']
379
                {'result': True,
380
                 'log': ["Received request to change is_cold to True",
381
                         "is_cold successfully changed"]}
382
        """
383
        return_dict = self._run_client_func(
×
384
            self.client.is_cold, params['value'], job='is_cold')
385
        session.data['response'] = return_dict
×
386
        return return_dict['result'], f"Success: {return_dict['result']}"
×
387

388
    @ocs_agent.param('value', default=False, type=bool)
1✔
389
    def force(self, session, params=None):
1✔
390
        """force(value=False)
391

392
        **Task** - Enable or disable force mode in the GripperServer,
393
        which will ignore limit switch information.
394

395
        Parameters:
396
            value (bool): Use limit switch information (False) or ignore limit
397
                switch information (True)
398

399
        Notes:
400
            By default the code is configured to prevent actuator movement if
401
            on of the limit switches has been triggered. This function can be
402
            called to forcibly move the actuators even with a limit switch
403
            trigger.
404

405
            The most recent data collected is stored in session data in the
406
            structure:
407

408
                >>> response.session['response']
409
                {'result': True,
410
                 'log': ["Received request to change force to True",
411
                         "force successfully changed"]}
412
        """
413
        return_dict = self._run_client_func(
×
414
            self.client.force, params['value'], job='force')
415
        session.data['response'] = return_dict
×
416
        return return_dict['result'], f"Success: {return_dict['result']}"
×
417

418
    def shutdown(self, session, params=None):
1✔
419
        """shutdown()
420

421
        **Task** - Series of commands executed during a shutdown.
422
        This will enable shutdown mode, which locks out other agent operations.
423

424
        Notes:
425
            This function is called once a shutdown trigger has been given.
426
        """
427
        self.log.info("Enabling shutdown-mode, which will prevent grip "
×
428
                      "operations from being performed")
429
        self.shutdown_mode = True
×
430
        return True, 'Shutdown completed'
×
431

432
    def grip(self, session, params=None):
1✔
433
        """grip()
434

435
        **Task** - Series of commands to automatically warm grip the HWP. This
436
        assumes that HWP is cold. This will return grippers to their home position,
437
        then move them each inwards incrementally until warm limit switches are
438
        tiggered. If the HWP is spinning, this will not run. If this fails to grip
439
        hwp, this will return grippers to their home position.
440

441
        Notes:
442
            The most recent data collected is stored in session data in the
443
            structure:
444

445
                >>> response.session['responses']
446
                [{'result': True, 'log': [.., .., etc]},
447
                                    ..
448
                 {'result': True, 'log': [.., .., etc]}]
449
        """
450
        if self._get_hwp_freq() > 0.1:
1✔
451
            return False, "Not gripping HWP because HWP is spinning"
×
452

453
        result, session.data = self._grip_hwp(check_shutdown=True)
1✔
454
        return result, "Finished gripping procedure"
1✔
455

456
    def _grip_hwp(self, check_shutdown=True):
1✔
457
        """
458
        Helper function to grip the HWP that can be called by the grip_hwp
459
        task or by the shutdown procedure.  This will return grippers to their
460
        home position, then move them each inwards incrementally.
461
        """
462
        data = {
1✔
463
            'responses': [],
464
        }
465

466
        def run_and_append(func, *args, **kwargs):
1✔
467
            return_dict = self._run_client_func(func, *args, **kwargs)
1✔
468
            data['responses'].append(return_dict)
1✔
469
            return return_dict
1✔
470

471
        # Check controller alarm
472
        return_dict = run_and_append(self.client.get_state, job='grip',
1✔
473
                                     check_shutdown=check_shutdown)
474
        if return_dict['result']['jxc']['alarm']:
1✔
475
            return_dict = self._run_client_func(
×
476
                self.client.alarm_group, job='alarm_group', check_shutdown=False)
477
            if return_dict['result'] is None:
×
478
                return_dict['result'] = 'A'
×
479
            alarm_message = self.decoded_alarm_group[return_dict['result']]
×
480
            self.log.error(
×
481
                f"Abort grip. Detected contoller alarm: {alarm_message}")
482
            return False, data
×
483

484
        # Check if hwp is already gripper or not
485
        act_results = return_dict['result']['actuators']
1✔
486
        limit_switch_state = act_results[0]['limits']['warm_grip']['state'] | \
1✔
487
            act_results[1]['limits']['warm_grip']['state'] | \
488
            act_results[2]['limits']['warm_grip']['state']
489
        if limit_switch_state:
1✔
490
            self.log.warn("HWP is already gripped. Do nothing.")
×
491
            return True, data
×
492

493
        # Reset alarms
494
        run_and_append(self.client.reset, job='grip', check_shutdown=check_shutdown)
1✔
495
        time.sleep(1)
1✔
496

497
        # Enable power to actuators
498
        run_and_append(self.client.power, True, job='grip', check_shutdown=check_shutdown)
1✔
499
        time.sleep(1)
1✔
500

501
        # Disable brake
502
        run_and_append(self.client.brake, False, job='grip', check_shutdown=check_shutdown)
1✔
503
        time.sleep(1)
1✔
504

505
        # Ignore limit switches to move grippers backwards
506
        run_and_append(self.client.force, True, job='grip', check_shutdown=check_shutdown)
1✔
507

508
        # Ensure that the limit switches are in warm configuration
509
        run_and_append(self.client.is_cold, False, job='grip', check_shutdown=check_shutdown)
1✔
510

511
        # Send grippers to their home position
512
        run_and_append(self.client.home, job='grip', check_shutdown=check_shutdown)
1✔
513

514
        # Ensure that we do not ignore limit switches
515
        run_and_append(self.client.force, False, job='grip', check_shutdown=check_shutdown)
1✔
516

517
        finished = [False, False, False]
1✔
518

519
        # Move to the warm_grip_distance - 5 mm.
520
        for actuator in range(3):
1✔
521
            distance = self.warm_grip_distance[actuator] - 5.
1✔
522
            return_dict = run_and_append(self.client.move, 'POS', actuator + 1, distance,
1✔
523
                                         job='grip', check_shutdown=check_shutdown)
524
            time.sleep(1)
1✔
525

526
        # Move actator inwards by 0.1 mm step, warm_grip_distance + 1 is absolute maximum
527
        for i in range(60):
1✔
528
            if all(finished):
1✔
529
                break
1✔
530
            for actuator, _ in enumerate(finished):
1✔
531
                if finished[actuator]:
1✔
532
                    continue
×
533

534
                # Move actuator inwards until warm-limit is hit
535
                # If the warm limit is hit, return_dict['result'] will be False
536
                return_dict = run_and_append(self.client.move, 'POS', actuator + 1, 0.1,
1✔
537
                                             job='grip', check_shutdown=check_shutdown)
538

539
                if not return_dict['result']:
1✔
540
                    # Reset alarms.
541
                    run_and_append(self.client.reset, job='grip',
1✔
542
                                   check_shutdown=check_shutdown)
543
                    time.sleep(1)
1✔
544

545
                    # If the warm-limit is hit, move the actuator outwards by 0.5 mm
546
                    # to un-trigger the warm-limit. This is because gripper sligthly
547
                    # overshoot the limit switches. The outward movement compensates
548
                    # for the overshoot and hysteresys of the limit switches.
549
                    run_and_append(self.client.is_cold, True, job='grip',
1✔
550
                                   check_shutdown=check_shutdown)
551
                    time.sleep(1)
1✔
552

553
                    run_and_append(self.client.move, 'POS', actuator + 1, -0.5,
1✔
554
                                   job='grip', check_shutdown=check_shutdown)
555

556
                    run_and_append(self.client.is_cold, False, job='grip',
1✔
557
                                   check_shutdown=check_shutdown)
558
                    time.sleep(1)
1✔
559

560
                    finished[actuator] = True
1✔
561

562
        # Return grippers back to home is something is wrong
563
        if not all(finished):
1✔
564
            self.log.error('Failed to grip HWP. Retract grippers.')
×
565
            run_and_append(self.client.force, True, job='grip',
×
566
                           check_shutdown=check_shutdown)
567
            time.sleep(1)
×
568

569
            run_and_append(self.client.home, job='grip',
×
570
                           check_shutdown=check_shutdown)
571
            time.sleep(1)
×
572

573
            run_and_append(self.client.force, False, job='grip',
×
574
                           check_shutdown=check_shutdown)
575

576
        # Adjust gripper position if you need
577
        else:
578
            run_and_append(self.client.is_cold, True, job='grip',
1✔
579
                           check_shutdown=check_shutdown)
580
            time.sleep(1)
1✔
581

582
            for actuator in range(3):
1✔
583
                run_and_append(self.client.move, 'POS', actuator + 1,
1✔
584
                               self.adjustment_distance[actuator],
585
                               job='grip', check_shutdown=check_shutdown)
586

587
            run_and_append(self.client.is_cold, False, job='grip',
1✔
588
                           check_shutdown=check_shutdown)
589
            time.sleep(1)
1✔
590

591
        # Enable breaks
592
        run_and_append(self.client.brake, True, job='grip',
1✔
593
                       check_shutdown=check_shutdown)
594
        time.sleep(1)
1✔
595

596
        # Disable power to actuators
597
        run_and_append(self.client.power, False, job='grip',
1✔
598
                       check_shutdown=check_shutdown)
599
        time.sleep(1)
1✔
600

601
        # We should stop schedule if we have an error in this task
602
        if not all(finished):
1✔
603
            self.log.error('Failed to grip HWP. Grippers are retracted.')
×
604
            return False, data
×
605

606
        return True, data
1✔
607

608
    def ungrip(self, session, params=None):
1✔
609
        """ungrip()
610

611
        **Task** - Series of commands to automatically ungrip the HWP.
612
        This will return grippers to their home position, and retract
613
        grippers as much as possible.
614

615
        Notes:
616
            The most recent data collected is stored in session data in the
617
            structure:
618

619
                >>> response.session['responses']
620
                [{'result': True, 'log': [.., .., etc]},
621
                                    ..
622
                 {'result': True, 'log': [.., .., etc]}]
623
        """
624

625
        result, session.data = self._ungrip_hwp(check_shutdown=True)
×
626
        return result, "Finished ungripping procedure"
×
627

628
    def _ungrip_hwp(self, check_shutdown=True):
1✔
629
        """
630
        Helper function to ungrip the HWP that can be called by the ungrip_hwp
631
        task or by the shutdown procedure.  This will return grippers to their
632
        home position, and retract as much as possible.
633
        """
634
        data = {
×
635
            'responses': [],
636
        }
637

638
        def run_and_append(func, *args, **kwargs):
×
639
            return_dict = self._run_client_func(func, *args, **kwargs)
×
640
            data['responses'].append(return_dict)
×
641
            return return_dict
×
642

643
        # Check controller alarm
644
        return_dict = run_and_append(self.client.get_state, job='ungrip',
×
645
                                     check_shutdown=check_shutdown)
646
        if return_dict['result']['jxc']['alarm']:
×
647
            return_dict = self._run_client_func(
×
648
                self.client.alarm_group, job='alarm_group', check_shutdown=False)
649
            if return_dict['result'] is None:
×
650
                return_dict['result'] = 'A'
×
651
            alarm_message = self.decoded_alarm_group[return_dict['result']]
×
652
            self.log.error(
×
653
                f"Abort ungrip. Detected contoller alarm: {alarm_message}")
654
            return False, data
×
655

656
        run_and_append(self.client.reset, job='ungrip', check_shutdown=check_shutdown)
×
657
        # Enable power to actuators
658
        run_and_append(self.client.power, True, job='ungrip', check_shutdown=check_shutdown)
×
659
        # Disable breaks
660
        run_and_append(self.client.brake, False, job='ungrip', check_shutdown=check_shutdown)
×
661
        # Ignore limit switches
662
        run_and_append(self.client.force, True, job='ungrip', check_shutdown=check_shutdown)
×
663

664
        # Send grippers to their home position
665
        run_and_append(self.client.home, job='ungrip', check_shutdown=check_shutdown)
×
666

667
        # Move actuator outwards as much as possible
668
        for actuator in range(1, 4):
×
669
            run_and_append(self.client.move, 'POS', actuator, -1.9,
×
670
                           job='ungrip', check_shutdown=check_shutdown)
671
        time.sleep(1)
×
672

673
        # Enable brake
674
        run_and_append(self.client.brake, True, job='ungrip', check_shutdown=check_shutdown)
×
675
        # Power off actuators
676
        run_and_append(self.client.power, False, job='ungrip', check_shutdown=check_shutdown)
×
677
        # Enable limit switches
678
        run_and_append(self.client.force, False, job='ungrip', check_shutdown=check_shutdown)
×
679
        time.sleep(1)
×
680

681
        # check limit switch state
682
        return_dict = run_and_append(self.client.get_state, job='ungrip',
×
683
                                     check_shutdown=check_shutdown)
684
        act_results = return_dict['result']['actuators']
×
685
        limit_switch_state = act_results[0]['limits']['warm_grip']['state'] | \
×
686
            act_results[1]['limits']['warm_grip']['state'] | \
687
            act_results[2]['limits']['warm_grip']['state']
688

689
        # Stop schedule if the limit switch state is wrong
690
        if limit_switch_state:
×
691
            self.log.error("Failed to ungrip HWP. Limit switch state is not as expected.")
×
692
            return False, data
×
693

694
        return True, data
×
695

696
    def cancel_shutdown(self, session, params=None):
1✔
697
        """cancel_shutdown()
698

699
        **Task** - Take the gripper agent out of shutdown mode
700
        """
701
        self.shutdown_mode = False
×
702
        return True, 'Cancelled shutdown mode'
×
703

704
    def restart(self, session, params=None):
1✔
705
        """restart()
706

707
        **Task** - Restarts the beaglebone processes and the socket connection
708

709
        Notes:
710
            The most recent data collected is stored in session data in the
711
            structure:
712

713
                >>> response.session['response']
714
                {'result': True,
715
                 'log': ["Previous connection closed",
716
                         "Restart command response: Success",
717
                         "Control socket reconnected"]}
718
        """
719
        return_dict = self._run_client_func(
×
720
            self.client.restart, job='restart', check_shutdown=False)
721
        session.data['response'] = return_dict
×
722
        return return_dict['result'], f"Success: {return_dict['result']}"
×
723

724
    def monitor_state(self, session, params=None):
1✔
725
        """monitor_state()
726

727
        **Process** - Process to monitor the gripper state
728

729
        Notes:
730
            The most recent data collected is stored in session data in the
731
            structure:
732

733
                >>> response.session
734
                {'last_updated': 1649085992.719602,
735
                 'state': {'last_packet_received': 1649085992.719602,
736
                           'jxc_setup': False,
737
                           'jxc_svon': False,
738
                           'jxc_busy': False,
739
                           'jxc_seton': False,
740
                           'jxc_inp': False,
741
                           'jxc_svre': False,
742
                           'jxc_alarm': False,
743
                           'jxc_alarm_message': 'No alarm was detected',
744
                           'jxc_out': 0,
745
                           'act{axis}_pos': 0,
746
                           'act{axis}_limit_warm_grip_state': False,
747
                           'act{axis}_limit_cold_grip_state': False,
748
                           'act{axis}_emg': False,
749
                           'act{axis}_brake': True}}
750
        """
751
        sleep_time = 5
1✔
752
        while session.status in ['starting', 'running']:
1✔
753
            if self.client is None:
1✔
754
                self.log.warn("Client not initialized")
1✔
755
                time.sleep(1)
1✔
756
                continue
1✔
757

758
            try:
1✔
759
                return_dict = self._run_client_func(
1✔
760
                    self.client.get_state, job='get_state', check_shutdown=False
761
                )
762
            except TimeoutError:
×
763
                self.log.warn('monitor_state: Query Timeout')
×
764

765
            now = time.time()
1✔
766

767
            # Dict of the 'GripperState' class from the pru_monitor
768
            state = return_dict['result']
1✔
769
            data = {
1✔
770
                'last_packet_received': state['last_packet_received'],
771
            }
772

773
            data.update({
1✔
774
                'jxc_setup': int(state['jxc']['setup']),
775
                'jxc_svon': int(state['jxc']['svon']),
776
                'jxc_busy': int(state['jxc']['busy']),
777
                'jxc_seton': int(state['jxc']['seton']),
778
                'jxc_inp': int(state['jxc']['inp']),
779
                'jxc_svre': int(state['jxc']['svre']),
780
                'jxc_alarm': int(state['jxc']['alarm']),
781
                'jxc_out': int(state['jxc']['out']),
782
            })
783

784
            alarm_group_mapping = {4: 'B',
1✔
785
                                   2: 'C',
786
                                   1: 'D',
787
                                   0: 'E'}
788

789
            if bool(state['jxc']['alarm']):
1✔
790
                out_value = int(state['jxc']['out']) % 16
×
791
                if out_value in alarm_group_mapping.keys():
×
NEW
792
                    alarm_message = self.decoded_alarm_group[alarm_group_mapping[out_value]]
×
793
                else:
794
                    alarm_message = self.decoded_alarm_group['A']
×
795
            else:
796
                alarm_message = self.decoded_alarm_group[False]
1✔
797

798
            data.update({'jxc_alarm_message': alarm_message})
1✔
799

800
            for act in state['actuators']:
1✔
801
                axis = act['axis']
1✔
802
                data.update({
1✔
803
                    f'act{axis}_pos': act['pos'],
804
                    f'act{axis}_limit_cold_grip_state': int(act['limits']['cold_grip']['state']),
805
                    f'act{axis}_limit_warm_grip_state': int(act['limits']['warm_grip']['state']),
806
                    f'act{axis}_brake': int(act['brake']),
807
                    f'act{axis}_emg': int(act['emg']),
808
                })
809

810
            session.data = {
1✔
811
                'state': data,
812
                'last_updated': now,
813
            }
814

815
            _data = {
1✔
816
                'block_name': 'gripper_state',
817
                'timestamp': now,
818
                'data': data,
819
            }
820

821
            self.agent.publish_to_feed('hwp_gripper', _data)
1✔
822
            time.sleep(sleep_time)
1✔
823

824
    def _stop_monitor_state(self, session, params=None):
1✔
825
        session.set_status('stopping')
1✔
826
        return True, "Requesting monitor_state process to stop"
1✔
827

828
    @ocs_agent.param('no_data_warn_time', default=60, type=float)
1✔
829
    @ocs_agent.param('no_data_shutdown_time', default=300, type=float)
1✔
830
    def monitor_supervisor(self, session, params=None):
1✔
831
        """monitor_supervisor(no_data_warn_time=60, no_data_shutdown_time=300)
832

833
        **Process** - Monitor the hwp-supervisor state. If supervisor sends shutdown
834
        signal, or if communication wtih supervisor is dropped for longer than
835
        ``no_data_shutdown_time``, this will begin agent shutdown and disable access
836
        to other dangerous gripper operations.
837

838
        Parameters:
839
            no_data_warn_time (int): Time in seconds to wait after communication failure
840
                                     before generating a warning
841
            no_data_shutdown_time (int): Time in seconds to wait after communication failure
842
                                         before initiating shutdown
843

844
        Notes:
845
            The most recent data collected is stored in session data in the
846
            structure:
847

848
                >>> response.session
849
                {'time': 1649085992.719602,
850
                 'gripper_action': 'ok'}
851
        """
852
        last_ok_time = time.time()
1✔
853

854
        if self.supervisor_id is None:
1✔
855
            return False, 'No supervisor ID set'
×
856

857
        warning_issued = False
1✔
858
        while session.status in ['starting', 'running']:
1✔
859
            res = get_op_data(self.supervisor_id, 'monitor')
1✔
860
            if res['status'] != 'ok':
1✔
861
                action = 'no_data'
1✔
862
            else:
863
                action = res['data']['actions']['gripper']
1✔
864

865
            if action == 'ok':
1✔
866
                warning_issued = False
1✔
867
                last_ok_time = time.time()
1✔
868
            elif action == 'stop':
1✔
869
                if not self.shutdown_mode:
×
870
                    self.agent.start('shutdown')
×
871

872
            time_since_ok = time.time() - last_ok_time
1✔
873
            if time_since_ok > params['no_data_warn_time'] and not warning_issued:
1✔
874
                self.log.error(
×
875
                    f"Have not received 'ok' in {time_since_ok / 60:.2f} minutes."
876
                    f"Will issue shutdown in "
877
                    f"{params['no_data_shutdown_time'] / 60:.2f} minutes."
878
                )
879
                warning_issued = True
×
880

881
            if time_since_ok > params['no_data_shutdown_time']:
1✔
882
                self.log.error(
×
883
                    f"Have not received 'ok' in "
884
                    f"{params['no_data_shutdown_time'] / 60:.2f} minutes. "
885
                    "Issuing shutdown"
886
                )
887
                self.agent.start('shutdown')
×
888

889
            data = {
1✔
890
                'data': {'gripper_action': action},
891
                'block_name': 'gripper_action',
892
                'timestamp': time.time()
893
            }
894

895
            self.agent.publish_to_feed('gripper_action', data)
1✔
896
            session.data = {
1✔
897
                'gripper_action': action,
898
                'time': time.time()
899
            }
900

901
            time.sleep(1)
1✔
902
        return True, 'Gripper monitor exited cleanly'
1✔
903

904
    def _stop_monitor_supervisor(self, session, params=None):
1✔
905
        session.set_status('stopping')
1✔
906
        return True, "Requesting monitor_supervisor process to stop"
1✔
907

908

909
def make_parser(parser=None):
1✔
910
    if parser is None:
1✔
911
        parser = argparse.ArgumentParser()
1✔
912

913
    pgroup = parser.add_argument_group('Agent Options')
1✔
914
    pgroup.add_argument('--mcu-ip', type=str,
1✔
915
                        help='IP of Gripper Beaglebone')
916
    pgroup.add_argument('--control-port', type=int, default=8041,
1✔
917
                        help='Port for actuator control as set by the Beaglebone code')
918
    pgroup.add_argument('--warm-grip-distance', action='store', type=float, nargs=3,
1✔
919
                        default=[10.0, 10.0, 10.0],
920
                        help='Nominal distance for warm grip position (mm). This needs'
921
                             'to be multiple of 0.1')
922
    pgroup.add_argument('--adjustment-distance', action='store', type=float, nargs=3,
1✔
923
                        default=[0, 0, 0],
924
                        help='Adjustment distance to compensate the misalignment of '
925
                             'limit switches (mm). This needs to be multiple of 0.1')
926
    pgroup.add_argument('--supervisor-id', type=str,
1✔
927
                        help='Instance ID for HWP Supervisor agent')
928
    pgroup.add_argument('--no-data-warn-time', type=float, default=60,
1✔
929
                        help='Time (seconds) since last supervisor-ok signal to '
930
                             'wait before issuing a warning')
931
    pgroup.add_argument('--no-data-shutdown-time', type=float, default=300,
1✔
932
                        help='Time (seconds) since last supervisor-ok signal to '
933
                             'wait before entering shutdown mode')
934
    return parser
1✔
935

936

937
def main(args=None):
1✔
938
    parser = make_parser()
1✔
939
    args = site_config.parse_args(agent_class='HWPGripperAgent',
1✔
940
                                  parser=parser,
941
                                  args=args)
942

943
    agent, runner = ocs_agent.init_site_agent(args)
1✔
944
    gripper_agent = HWPGripperAgent(agent, args)
1✔
945
    agent.register_task('init_connection', gripper_agent.init_connection,
1✔
946
                        startup=True)
947
    agent.register_process('monitor_state', gripper_agent.monitor_state,
1✔
948
                           gripper_agent._stop_monitor_state, startup=True)
949
    agent.register_process('monitor_supervisor', gripper_agent.monitor_supervisor,
1✔
950
                           gripper_agent._stop_monitor_supervisor,
951
                           startup={'no_data_warn_time': args.no_data_warn_time,
952
                                    'no_data_shutdown_time': args.no_data_shutdown_time})
953
    agent.register_task('power', gripper_agent.power)
1✔
954
    agent.register_task('brake', gripper_agent.brake)
1✔
955
    agent.register_task('move', gripper_agent.move)
1✔
956
    agent.register_task('home', gripper_agent.home)
1✔
957
    agent.register_task('inp', gripper_agent.inp)
1✔
958
    agent.register_task('alarm', gripper_agent.alarm)
1✔
959
    agent.register_task('alarm_group', gripper_agent.alarm_group)
1✔
960
    agent.register_task('reset', gripper_agent.reset)
1✔
961
    agent.register_task('act', gripper_agent.act)
1✔
962
    agent.register_task('is_cold', gripper_agent.is_cold)
1✔
963
    agent.register_task('force', gripper_agent.force)
1✔
964
    agent.register_task('shutdown', gripper_agent.shutdown)
1✔
965
    agent.register_task('grip', gripper_agent.grip)
1✔
966
    agent.register_task('ungrip', gripper_agent.ungrip)
1✔
967
    agent.register_task('cancel_shutdown', gripper_agent.cancel_shutdown)
1✔
968
    agent.register_task('restart', gripper_agent.restart)
1✔
969

970
    runner.run(agent, auto_reconnect=True)
1✔
971

972

973
if __name__ == '__main__':
1✔
974
    main()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc