11#!/usr/bin/env python3
2+ import datetime
23import json
34import urllib .request
45
@@ -21,96 +22,137 @@ def send_alerts(alert_config, message):
2122
2223def main ():
2324 rospy .init_node ('seatrac_starlink_manager' )
24- shutdown_reminder = rospy .get_param ('~shutdown_reminder' , None )
25- force_on_threshold = rospy .get_param ('~soc_force_threshold' , 95 )
26- on_threshold = rospy .get_param ('~soc_threshold' , 80 )
27- off_delay = rospy .get_param ('~off_delay' , 86400 )
25+
26+ # Read parameters
2827 alerts = rospy .get_param ('/alerts' , [])
28+ high_threshold = rospy .get_param ('~soc_threshold/high' , 95 )
29+ medium_threshold = rospy .get_param ('~soc_threshold/medium' , 80 )
30+
31+ shutdown_reminder = rospy .get_param ('~shutdown_reminder' , None )
32+ if shutdown_reminder is not None :
33+ shutdown_reminder = rospy .Duration (shutdown_reminder )
34+
35+ scheduled_on = rospy .get_param ('~scheduled_on' , None )
36+ if scheduled_on is not None :
37+ scheduled_on = datetime .datetime .strptime (scheduled_on , '%H:%M' ).time ()
2938
39+
40+ # Create a publisher for controlling the Starlink outlet
3041 outlet_pub = rospy .Publisher ('/seatrac/outlet/starlink/control' , StdBool ,
3142 queue_size = 1 )
3243
33- manual_shutdown = False
34- last_off = None
35- nag_timer = None
3644
37- enable_sent = False
45+ # Subscribe to power level messages and record the state of charge, and
46+ # turn Starlink on automatically if the SOC is high enough.
47+ charging = False
48+
49+ already_sent = False
3850 last_state = None
3951 soc = 0.0
4052
53+ def power_level_cb (msg : PowerLevel ) -> None :
54+ nonlocal already_sent , charging , soc
55+ if last_state is None :
56+ return
57+
58+ soc = msg .soc_percentage
59+ charging = msg .pack_current < 0
60+
61+ if soc >= high_threshold and charging and \
62+ not last_state and not already_sent :
63+ rospy .loginfo (
64+ f'Enabling Starlink: SOC { soc } % >= { high_threshold } % and charging'
65+ )
66+ outlet_pub .publish (StdBool (True ))
67+ already_sent = True
68+ elif soc < medium_threshold :
69+ already_sent = False
70+
71+ rospy .Subscriber ('/seatrac/power' , PowerLevel , power_level_cb )
72+
73+
74+ # Nag the operators if Starlink has been active for too long
75+ nag_timer = None
76+
4177 def nag_operator () -> None :
42- nonlocal alerts , nag_timer
78+ nonlocal nag_timer
4379 nag_timer = None
4480 send_alerts (alerts , 'Starlink has been active for a while' )
4581
4682 def outlet_status_cb (msg : OutletStatus ) -> None :
47- nonlocal manual_shutdown , last_off , nag_timer , last_state
48- now = rospy .Time .now ().to_sec ()
49- if last_state is None :
50- if not msg .is_active :
51- manual_shutdown = True
52- last_off = now
53- elif last_state and not msg .is_active :
54- manual_shutdown = True
55- last_off = now
56- elif manual_shutdown and msg .is_active :
57- manual_shutdown = False
83+ nonlocal nag_timer , last_state
5884 last_state = msg .is_active
5985
60- if shutdown_reminder is not None and last_state and nag_timer is None :
86+ # Set up the nag timer if Starlink is active (except if we are at a
87+ # high state of charge and charging). When the outlet is off, cancel
88+ # the nag timer.
89+ if shutdown_reminder is None :
90+ return
91+
92+ if msg .is_active and nag_timer is None and \
93+ not (soc >= high_threshold and charging ):
94+ # Assume we'll be getting outlet status messages regularly, so we
95+ # can do a one-shot timer.
6196 nag_timer = rospy .Timer (
62- rospy . Duration ( shutdown_reminder ) ,
97+ shutdown_reminder ,
6398 nag_operator ,
6499 oneshot = True
65100 )
66101 elif not msg .is_active and nag_timer :
67102 nag_timer .shutdown ()
68103 nag_timer = None
69104
70- def power_level_cb (msg : PowerLevel ) -> None :
71- nonlocal enable_sent , last_state , soc , manual_shutdown , last_off
72- if last_state is None :
105+ rospy .Subscriber ('/seatrac/outlet/starlink/status' , OutletStatus ,
106+ outlet_status_cb )
107+
108+
109+ # Schedule Starlink to turn on at a specific time every day
110+ daily_timer = None
111+
112+ def schedule_daily_timer ():
113+ nonlocal daily_timer
114+ now = datetime .datetime .now (datetime .timezone .utc )
115+ target = now .replace (hour = scheduled_on .hour ,
116+ minute = scheduled_on .minute ,
117+ second = 0 , microsecond = 0 )
118+ if target <= now :
119+ target += datetime .timedelta (days = 1 )
120+
121+ daily_timer = rospy .Timer (
122+ rospy .Duration ((target - now ).total_seconds ()),
123+ daily_timer_cb ,
124+ oneshot = True
125+ )
126+
127+ def daily_timer_cb (event ):
128+ nonlocal already_sent
129+
130+ # Starlink is already enabled, no need to enable it again. We don't
131+ # check already_sent because the operator may have manually turned it
132+ # off yesterday, and we want to turn it on today.
133+ if last_state :
73134 return
74135
75- soc = msg .soc_percentage
76- is_charging = msg .pack_current < 0
77- now = rospy .Time .now ().to_sec ()
78-
79- if (
80- soc >= force_on_threshold
81- and is_charging
82- and not enable_sent
83- and not manual_shutdown
84- ):
136+ if soc >= medium_threshold :
85137 rospy .loginfo (
86- f'Enabling Starlink: SOC { soc } % > { force_on_threshold } and charging'
138+ f'Enabling Starlink: SOC { soc :.1f} % >= { medium_threshold } % '
139+ 'at scheduled time'
87140 )
88141 outlet_pub .publish (StdBool (True ))
89- enable_sent = True
90- elif (
91- soc >= on_threshold
92- and not enable_sent
93- and not manual_shutdown
94- and (last_off is None or now - last_off > off_delay )
95- ):
96- rospy .loginfo (
97- f'Enabling Starlink: SOC { soc } % > { on_threshold } '
98- f' and last_off { int (now - last_off )} s > { off_delay } '
142+ already_sent = True
143+ elif soc < medium_threshold :
144+ send_alerts (
145+ alerts ,
146+ 'Not enabling Starlink at scheduled time because '
147+ f'SOC { soc :.1f} % below medium threshold { medium_threshold } %'
99148 )
100- outlet_pub .publish (StdBool (True ))
101- enable_sent = True
102- elif soc < on_threshold :
103- enable_sent = False
149+ schedule_daily_timer ()
104150
105- rospy .Subscriber ('/seatrac/outlet/starlink/status' , OutletStatus ,
106- outlet_status_cb )
107- rospy .Subscriber ('/seatrac/power' , PowerLevel , power_level_cb )
151+ if scheduled_on is not None :
152+ schedule_daily_timer ()
108153
109154 rospy .spin ()
110155
111156
112157if __name__ == '__main__' :
113- try :
114- main ()
115- except rospy .ROSInterruptException :
116- pass
158+ main ()
0 commit comments