Skip to content

Commit 068287b

Browse files
author
CUAUV
committed
Merge pull request #141 in SOF/subcode from control-helm-activity to master
1 parent d3fc2e9 commit 068287b

File tree

5 files changed

+243
-96
lines changed

5 files changed

+243
-96
lines changed
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Activity Tracking
2+
3+
## Motivation
4+
It seems a common occurrence at pool tests is for one person to begin to drive the sub, only for it to behave unexpectedly because someone else was already controlling it. The Activity Tracking system attempts to eliminate this minor frustration by making clearly apparent in Control Helm anyone else who might be driving the sub.
5+
6+
## What it does
7+
The rightmost column on Control Helm contains 3 panels:
8+
* The "Active" panel displays a list of people who have issued a command to the sub (through Control Helm) within the past 30 seconds.
9+
* The "Idle" panel lists people who have Control Helm open, but who have not issued a command within the past 30 seconds.
10+
* The "Mission" panel displays up to one name of someone who is currently running a mission on the sub.
11+
12+
## How it works
13+
### "Active" and "Idle"
14+
A file called "activity.csv" stores all the information necessary for generating the "Active" and "Idle" panels. The format is simple: Each line contains the name of a team member who has Control Helm open, the time (in seconds since the epoch) that they last interacted with the Helm, and the number of Helms they currently have open, separated by commas.
15+
16+
When a user opens Control Helm, their name is added to the file next to the number zero, as if the last time they interacted was the epoch, which guarantees they show up originally as "Idle." Whenever they press a key which corresponds to a command, the time next to their name in the file is updated. And when they quit the Helm, if the number of helms they have opened is decreased to 0, the line containing their name is removed. Then the "Active" and "Idle" panels simply select names from the list based on their recency.
17+
18+
### Mission
19+
The Mission Runner uses a lock to prevent any two missions from running on the sub simultaneously. It also writes to a file called "mission.csv" to tell control helm when a mission is running. Specifically, it writes the name of the user who runs a mission to the file when the mission begins, and empties the file when the mission ends (and the lock is released). Then Control Helm need only read this file to see if someone is running a mission.
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
import sys
3+
import time
4+
5+
activity_filepath = '/home/software/cuauv/workspaces/worktrees/master/control/control_helm2/activity/activity.csv'
6+
7+
# Read the activity file, returning the raw list of lines as well
8+
# as the number of the line which holds the current user's name
9+
# (-1 if the current user's name does not appear).
10+
def read_file():
11+
if not os.path.exists(activity_filepath):
12+
open(activity_filepath, 'w')
13+
with open(activity_filepath, 'r') as f:
14+
lines = f.readlines()
15+
line_num = -1
16+
for num, line in enumerate(lines):
17+
if line.split(',')[0] == os.getenv('AUV_ENV_ALIAS'):
18+
line_num = num
19+
return (lines, line_num)
20+
21+
# Add the user to the list of connections stored
22+
# in connections.csv: their name followed by
23+
# a timestamp representing when they were last
24+
# active in the control helm.
25+
def log(new_helm=False):
26+
lines, line_num = read_file()
27+
if line_num == -1:
28+
lines.append('')
29+
helms = 1
30+
else:
31+
helms = int(lines[line_num].split(',')[2]) + (1 if new_helm else 0)
32+
lines[line_num] = os.getenv('AUV_ENV_ALIAS') + ',' + str(time.time()) + ',' + str(helms) + '\n'
33+
with open(activity_filepath, 'w') as f:
34+
f.writelines(lines)
35+
36+
# Add a call to 'log()' to each key callback
37+
# so the last time the user is marked as
38+
# having been active always stays up to date.
39+
def add_logging(callbacks, key):
40+
value = callbacks[key]
41+
def func():
42+
log()
43+
return value()
44+
callbacks[key] = func
45+
46+
# Remove the user from the activity list if closing
47+
# their last Helm, otherwise simply decrease their
48+
# helm count by 1.
49+
def close_helm(signum, frame):
50+
lines, line_num = read_file()
51+
helms = int(lines[line_num].split(',')[2])
52+
if helms == 1:
53+
lines.remove(lines[line_num])
54+
else:
55+
time = float(lines[line_num].split(',')[1])
56+
lines[line_num] = os.getenv('AUV_ENV_ALIAS') + ',' + str(time) + ',' + str(helms - 1) + '\n'
57+
with open(activity_filepath, 'w') as f:
58+
f.writelines(lines)
59+
sys.exit(0)
60+
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import math
2+
import time
3+
import os
4+
5+
from helm_basis import Panel
6+
7+
mission_filepath = '/home/software/cuauv/workspaces/worktrees/master/control/control_helm2/activity/mission.csv'
8+
9+
# A panel which displays names from the list of connections
10+
class ConnectionsPanel(Panel):
11+
def __init__(self, title, height, min_recency=0, max_recency=math.inf):
12+
self.min_recency = min_recency
13+
self.max_recency = max_recency
14+
super().__init__(title=title, width=3, height=height)
15+
16+
def get_cols_lines(self, width, height):
17+
with open('/home/software/cuauv/workspaces/worktrees/master/control/control_helm2/activity/activity.csv', 'r') as f:
18+
lines = f.readlines()
19+
return [[line.split(',')[0] for line in filter(lambda x: time.time() - float(x.split(',')[1]) > self.min_recency and time.time() - float(x.split(',')[1]) <= self.max_recency, lines)]]
20+
21+
# A panel which displays up to one name from the mission file
22+
class MissionPanel(Panel):
23+
def __init__(self):
24+
super().__init__(title="Mission", width=7, height=4)
25+
26+
def get_cols_lines(self, width, height):
27+
if not os.path.exists(mission_filepath):
28+
open(mission_filepath, 'w')
29+
with open(mission_filepath, 'r') as f:
30+
lines = f.readlines()
31+
if len(lines) != 0:
32+
return [[lines[0]]]
33+
return [[]]
34+

control/control_helm2/control_helm2.py

+124-96
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import sys
44
import time
5+
import os
6+
import signal
57

68
from helm_basis import *
9+
import activity.panels, activity.logging
710

811
import shm
912

@@ -71,102 +74,110 @@ def get_battery_status():
7174
status_line = ' Voltages nominal.'
7275
return (voltage_line, status_line)
7376

74-
drive_panels = Vbox(
75-
Hbox(
76-
LineLambdaPanel([
77-
lambda: ' PORT: {:4} '.format(
78-
shm.motor_desires.port.get()),
79-
lambda: ' STAR: {:4} '.format(
80-
shm.motor_desires.starboard.get()),
81-
lambda: ' FORE:{:3}:{:3} '.format(
82-
shm.motor_desires.fore_port.get(),
83-
shm.motor_desires.fore_starboard.get()),
84-
lambda: ' AFT:{:3}:{:3} '.format(
85-
shm.motor_desires.aft_port.get(),
86-
shm.motor_desires.aft_starboard.get()),
87-
lambda: ' SFOR: {:4} '.format(
88-
shm.motor_desires.sway_fore.get()),
89-
lambda: ' SAFT: {:4} '.format(
90-
shm.motor_desires.sway_aft.get()),
91-
], width=16, padding=False),
92-
93-
ShmPanel(shm.navigation_desires, width=20, title=None,
94-
padding=False,
95-
select_vars=['heading', 'depth', 'pitch',
96-
'roll', 'speed', 'sway_speed'],
97-
var_names=[' DES HEAD', ' DES DPTH', ' DES PTCH',
98-
' DES ROLL', ' DES VELX', ' DES VELY']),
99-
100-
ShmPanel(shm.kalman, width=16, title=None, padding=False,
101-
select_vars=['heading', 'depth', 'pitch',
102-
'roll', 'velx', 'vely'],
103-
var_names=[' HEAD', ' DPTH', ' PTCH',
104-
' ROLL', ' VELX', ' VELY']),
105-
106-
LineLambdaPanel([
107-
lambda: (' DVL ALTD:', dvl_fmt(shm.dvl.savg_altitude.get())),
108-
lambda: (' DVL TEMP:', dvl_fmt(shm.dvl.temperature.get())),
109-
lambda: (StyledString.highlight_if(
110-
' DVL BEAM 1', shm.dvl.low_amp_1.get()
111-
or shm.dvl.low_correlation_1.get()), ' FWRD:'),
112-
lambda: (StyledString.highlight_if(
113-
' DVL BEAM 2', shm.dvl.low_amp_2.get()
114-
or shm.dvl.low_correlation_2.get()),
115-
dvl_fmt(shm.kalman.forward.get())),
116-
lambda: (StyledString.highlight_if(
117-
' DVL BEAM 3', shm.dvl.low_amp_3.get()
118-
or shm.dvl.low_correlation_3.get()), ' SWAY:'),
119-
lambda: (StyledString.highlight_if(
120-
' DVL BEAM 4', shm.dvl.low_amp_4.get()
121-
or shm.dvl.low_correlation_4.get()),
122-
dvl_fmt(shm.kalman.sway.get())),
123-
], width=20, columns=True, padding=False),
124-
125-
LineLambdaPanel([
126-
lambda: StyledString.highlight_if(
127-
' HK ',shm.switches.hard_kill.get()),
128-
lambda: StyledString.highlight_if(
129-
' SK ', shm.switches.soft_kill.get()),
130-
lambda: StyledString.highlight_if(
131-
' DV ', shm.dvl.vel_x_invalid.get()
132-
or shm.dvl.vel_y_invalid.get()
133-
or shm.dvl.vel_z_invalid.get()),
134-
lambda: StyledString.highlight_if(
135-
' PC ', shm.navigation_settings.position_controls.get()),
136-
lambda: StyledString.highlight_if(
137-
' OT ', shm.navigation_settings.optimize.get()),
138-
lambda: StyledString.highlight_if(
139-
' EN ', shm.settings_control.enabled.get()),
140-
], width=6, padding=False),
141-
height=8, min_height=8
142-
),
143-
144-
# PID loop panels
145-
Hbox(
146-
pid_panel('heading', 'heading_rate', 'heading'),
147-
pid_panel('pitch', 'pitch_rate', 'pitch'),
148-
pid_panel('roll', 'roll_rate', 'roll'),
149-
height=8,
150-
),
151-
Hbox(
152-
pid_panel('velx', 'accelx', 'speed'),
153-
pid_panel('vely', 'accely', 'sway_speed'),
154-
pid_panel('depth', 'depth_rate', 'depth'),
155-
height=8,
156-
),
157-
158-
Hbox(
159-
LineLambdaPanel([
160-
lambda: get_battery_status()[0],
161-
lambda: get_battery_status()[1],
162-
], width=26),
163-
LineLambdaPanel([
164-
lambda: msg,
165-
lambda: buf,
166-
], width=26),
167-
Panel(width=26),
168-
height=4,
77+
drive_panels = Hbox(
78+
Vbox(
79+
Hbox(
80+
LineLambdaPanel([
81+
lambda: ' PORT: {:4} '.format(
82+
shm.motor_desires.port.get()),
83+
lambda: ' STAR: {:4} '.format(
84+
shm.motor_desires.starboard.get()),
85+
lambda: ' FORE:{:3}:{:3} '.format(
86+
shm.motor_desires.fore_port.get(),
87+
shm.motor_desires.fore_starboard.get()),
88+
lambda: ' AFT:{:3}:{:3} '.format(
89+
shm.motor_desires.aft_port.get(),
90+
shm.motor_desires.aft_starboard.get()),
91+
lambda: ' SFOR: {:4} '.format(
92+
shm.motor_desires.sway_fore.get()),
93+
lambda: ' SAFT: {:4} '.format(
94+
shm.motor_desires.sway_aft.get()),
95+
], width=16, padding=False),
96+
97+
ShmPanel(shm.navigation_desires, width=20, title=None,
98+
padding=False,
99+
select_vars=['heading', 'depth', 'pitch',
100+
'roll', 'speed', 'sway_speed'],
101+
var_names=[' DES HEAD', ' DES DPTH', ' DES PTCH',
102+
' DES ROLL', ' DES VELX', ' DES VELY']),
103+
104+
ShmPanel(shm.kalman, width=16, title=None, padding=False,
105+
select_vars=['heading', 'depth', 'pitch',
106+
'roll', 'velx', 'vely'],
107+
var_names=[' HEAD', ' DPTH', ' PTCH',
108+
' ROLL', ' VELX', ' VELY']),
109+
110+
LineLambdaPanel([
111+
lambda: (' DVL ALTD:', dvl_fmt(shm.dvl.savg_altitude.get())),
112+
lambda: (' DVL TEMP:', dvl_fmt(shm.dvl.temperature.get())),
113+
lambda: (StyledString.highlight_if(
114+
' DVL BEAM 1', shm.dvl.low_amp_1.get()
115+
or shm.dvl.low_correlation_1.get()), ' FWRD:'),
116+
lambda: (StyledString.highlight_if(
117+
' DVL BEAM 2', shm.dvl.low_amp_2.get()
118+
or shm.dvl.low_correlation_2.get()),
119+
dvl_fmt(shm.kalman.forward.get())),
120+
lambda: (StyledString.highlight_if(
121+
' DVL BEAM 3', shm.dvl.low_amp_3.get()
122+
or shm.dvl.low_correlation_3.get()), ' SWAY:'),
123+
lambda: (StyledString.highlight_if(
124+
' DVL BEAM 4', shm.dvl.low_amp_4.get()
125+
or shm.dvl.low_correlation_4.get()),
126+
dvl_fmt(shm.kalman.sway.get())),
127+
], width=20, columns=True, padding=False),
128+
129+
LineLambdaPanel([
130+
lambda: StyledString.highlight_if(
131+
' HK ',shm.switches.hard_kill.get()),
132+
lambda: StyledString.highlight_if(
133+
' SK ', shm.switches.soft_kill.get()),
134+
lambda: StyledString.highlight_if(
135+
' DV ', shm.dvl.vel_x_invalid.get()
136+
or shm.dvl.vel_y_invalid.get()
137+
or shm.dvl.vel_z_invalid.get()),
138+
lambda: StyledString.highlight_if(
139+
' PC ', shm.navigation_settings.position_controls.get()),
140+
lambda: StyledString.highlight_if(
141+
' OT ', shm.navigation_settings.optimize.get()),
142+
lambda: StyledString.highlight_if(
143+
' EN ', shm.settings_control.enabled.get()),
144+
], width=6, padding=False),
145+
height=8, min_height=8
146+
),
147+
148+
# PID loop panels
149+
Hbox(
150+
pid_panel('heading', 'heading_rate', 'heading'),
151+
pid_panel('pitch', 'pitch_rate', 'pitch'),
152+
pid_panel('roll', 'roll_rate', 'roll'),
153+
height=8,
154+
),
155+
Hbox(
156+
pid_panel('velx', 'accelx', 'speed'),
157+
pid_panel('vely', 'accely', 'sway_speed'),
158+
pid_panel('depth', 'depth_rate', 'depth'),
159+
height=8,
160+
),
161+
Hbox(
162+
LineLambdaPanel([
163+
lambda: get_battery_status()[0],
164+
lambda: get_battery_status()[1],
165+
], width=26),
166+
LineLambdaPanel([
167+
lambda: msg,
168+
lambda: buf,
169+
], width=26),
170+
Panel(width=26),
171+
height=4
172+
),
173+
174+
width=26 * 3,
169175
),
176+
Vbox(
177+
activity.panels.ConnectionsPanel("Active", height=8, max_recency=30),
178+
activity.panels.ConnectionsPanel("Idle", height=16, min_recency=30),
179+
activity.panels.MissionPanel()
180+
)
170181
)
171182

172183
def toggle_shm(var, set_msg=None):
@@ -412,8 +423,25 @@ def commit_pos(name):
412423
lambda: commit_pos('north'), allowable_chars)
413424

414425
add_positional_controls()
415-
426+
427+
# Add activity logging to each callback, so every keypress
428+
# updates the user's entry in the activity list.
429+
for key in drive_callbacks:
430+
activity.logging.add_logging(drive_callbacks, key)
431+
for mode in drive_modal_callbacks:
432+
for key in drive_modal_callbacks[mode]:
433+
activity.logging.add_logging(drive_modal_callbacks[mode], key)
434+
416435
return drive_panels, drive_callbacks, drive_modal_callbacks
417436

418437
if __name__ == '__main__':
438+
# When Control Helm is exited through any of these
439+
# means, remove the user from the activity list.
440+
signal.signal(signal.SIGTERM, activity.logging.close_helm)
441+
signal.signal(signal.SIGINT, activity.logging.close_helm)
442+
signal.signal(signal.SIGHUP, activity.logging.close_helm)
443+
444+
# Add the user to the activity list.
445+
activity.logging.log(new_helm=True)
446+
419447
start_helm(*build_control_helm(expert='-e' in sys.argv))

mission/runner.py

+6
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@
107107
lock_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), LOCK_NAME)
108108
try:
109109
os.mkdir(lock_dir)
110+
# Inform control helm that the current user is running a mission.
111+
with open('/home/software/cuauv/workspaces/worktrees/master/control/control_helm2/activity/mission.csv', 'w') as f:
112+
f.write(os.getenv('AUV_ENV_ALIAS'))
110113
except OSError:
111114
logger("A MISSION IS ALREADY RUNNING! Aborting...", copy_to_stdout=True)
112115
logger("If I am mistaken, delete %s or check permissions" % lock_dir,
@@ -136,6 +139,9 @@
136139

137140
def release_lock():
138141
os.rmdir(lock_dir)
142+
# Inform control helm that the no mission is currently running.
143+
with open('/home/software/cuauv/workspaces/worktrees/master/control/control_helm2/activity/mission.csv', 'w') as f:
144+
f.truncate(0)
139145

140146
initially_killed = shm.switches.hard_kill.get()
141147
was_ever_unkilled = False

0 commit comments

Comments
 (0)