Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/library/signals_colour_lights.py: 88%
353 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-10 15:08 +0100
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-10 15:08 +0100
1# --------------------------------------------------------------------------------
2# This module is used for creating and managing colour light signal types
3# --------------------------------------------------------------------------------
5from . import common
6from . import signals_common
7from . import dcc_control
8from . import file_interface
10from typing import Union
11import logging
12import enum
14# -------------------------------------------------------------------------
15# Classes used externally when creating/updating colour light signals
16# -------------------------------------------------------------------------
18# Define the superset of signal sub types that can be created
19class signal_sub_type(enum.Enum):
20 home = 1 # 2 aspect - Red/Grn
21 distant = 2 # 2 aspect - Ylw/Grn
22 red_ylw = 3 # 2 aspect - Red/Ylw
23 three_aspect = 4
24 four_aspect = 5
26# ---------------------------------------------------------------------------------
27# Public API Function to create a Colour Light Signal 'object'. The Signal is
28# normally set to "NOT CLEAR" = RED (or YELLOW if its a 2 aspect distant signal)
29# unless its fully automatic - when its set to "CLEAR" (with the appropriate aspect)
30# ---------------------------------------------------------------------------------
32def create_colour_light_signal (canvas, sig_id: int, x:int, y:int,
33 signal_subtype = signal_sub_type.four_aspect,
34 sig_callback = None,
35 orientation:int = 0,
36 sig_passed_button:bool=False,
37 approach_release_button:bool=False,
38 position_light:bool=False,
39 mainfeather:bool=False,
40 lhfeather45:bool=False,
41 lhfeather90:bool=False,
42 rhfeather45:bool=False,
43 rhfeather90:bool=False,
44 theatre_route_indicator:bool=False,
45 refresh_immediately = True,
46 fully_automatic:bool=False):
47 logging.info ("Signal "+str(sig_id)+": Creating Colour Light Signal")
48 # Do some basic validation on the parameters we have been given
49 signal_has_feathers = mainfeather or lhfeather45 or lhfeather90 or rhfeather45 or rhfeather90
50 if signals_common.sig_exists(sig_id): 50 ↛ 51line 50 didn't jump to line 51, because the condition on line 50 was never true
51 logging.error ("Signal "+str(sig_id)+": Signal already exists")
52 elif sig_id < 1: 52 ↛ 53line 52 didn't jump to line 53, because the condition on line 52 was never true
53 logging.error ("Signal "+str(sig_id)+": Signal ID must be greater than zero")
54 elif orientation != 0 and orientation != 180: 54 ↛ 55line 54 didn't jump to line 55, because the condition on line 54 was never true
55 logging.error ("Signal "+str(sig_id)+": Invalid orientation angle - only 0 and 180 currently supported")
56 elif signal_has_feathers and theatre_route_indicator: 56 ↛ 57line 56 didn't jump to line 57, because the condition on line 56 was never true
57 logging.error ("Signal "+str(sig_id)+": Signal can only have Feathers OR a Theatre Route Indicator")
58 elif (signal_has_feathers or theatre_route_indicator) and signal_subtype == signal_sub_type.distant: 58 ↛ 59line 58 didn't jump to line 59, because the condition on line 58 was never true
59 logging.error ("Signal "+str(sig_id)+": 2 Aspect distant signals should not have Route Indicators")
60 elif approach_release_button and signal_subtype == signal_sub_type.distant: 60 ↛ 61line 60 didn't jump to line 61, because the condition on line 60 was never true
61 logging.error ("Signal "+str(sig_id)+": 2 Aspect distant signals should not have Approach Release Control")
62 else:
63 # Define the "Tag" for all drawing objects for this signal instance
64 sig_id_tag = "signal"+str(sig_id)
65 # Draw the signal base line & signal post
66 line_coords = common.rotate_line (x,y,0,0,0,-20,orientation)
67 canvas.create_line (line_coords,width=2,tags=sig_id_tag)
68 line_coords = common.rotate_line (x,y,0,-20,+30,-20,orientation)
69 canvas.create_line (line_coords,width=3,tags=sig_id_tag)
71 # Draw the body of the position light - only if a position light has been specified
72 if position_light:
73 point_coords1 = common.rotate_point (x,y,+13,-12,orientation)
74 point_coords2 = common.rotate_point (x,y,+13,-28,orientation)
75 point_coords3 = common.rotate_point (x,y,+26,-28,orientation)
76 point_coords4 = common.rotate_point (x,y,+26,-24,orientation)
77 point_coords5 = common.rotate_point (x,y,+19,-12,orientation)
78 points = point_coords1, point_coords2, point_coords3, point_coords4, point_coords5
79 canvas.create_polygon (points, outline="black", fill="black",tags=sig_id_tag)
81 # Draw the position light aspects (but hide then if the signal doesn't have a subsidary)
82 line_coords = common.rotate_line (x,y,+18,-27,+24,-21,orientation)
83 poslight1 = canvas.create_oval (line_coords,fill="grey",outline="black",tags=sig_id_tag)
84 line_coords = common.rotate_line (x,y,+14,-14,+20,-20,orientation)
85 poslight2 = canvas.create_oval (line_coords,fill="grey",outline="black",tags=sig_id_tag)
86 if not position_light:
87 canvas.itemconfigure(poslight1,state='hidden')
88 canvas.itemconfigure(poslight2,state='hidden')
90 # Draw all aspects for a 4-aspect signal (running from bottom to top)
91 # Unused spects (if its a 2 or 3 aspect signal) get 'hidden' later
92 line_coords = common.rotate_line (x,y,+40,-25,+30,-15,orientation)
93 red = canvas.create_oval (line_coords,fill="grey",tags=sig_id_tag)
94 line_coords = common.rotate_line (x,y,+50,-25,+40,-15,orientation)
95 yel = canvas.create_oval (line_coords,fill="grey",tags=sig_id_tag)
96 line_coords = common.rotate_line (x,y,+60,-25,+50,-15,orientation)
97 grn = canvas.create_oval (line_coords,fill="grey",tags=sig_id_tag)
98 line_coords = common.rotate_line (x,y,+70,-25,+60,-15,orientation)
99 yel2 = canvas.create_oval (line_coords,fill="grey",tags=sig_id_tag)
100 # Hide the aspects we don't need and define the 'offset' for the route indications based on
101 # the signal type - so that the feathers and theatre route indicator sit on top of the signal
102 # If its a 2 aspect signal we need to hide the green and the 2nd yellow aspect
103 # We also need to 'reassign" the other aspects if its a Home or Distant signal
104 if signal_subtype in (signal_sub_type.home, signal_sub_type.distant, signal_sub_type.red_ylw):
105 offset = -20
106 canvas.itemconfigure(yel2,state='hidden')
107 canvas.itemconfigure(grn,state='hidden')
108 if signal_subtype == signal_sub_type.home:
109 grn = yel # Reassign the green aspect to aspect#2 (normally yellow in 3/4 aspect signals)
110 elif signal_subtype == signal_sub_type.distant:
111 grn = yel # Reassign the green aspect to aspect#2 (normally yellow in 3/4 aspect signals)
112 yel = red # Reassign the Yellow aspect to aspect#1 (normally red in 3/4 aspect signals)
113 # If its a 3 aspect signal we need to hide the 2nd yellow aspect
114 elif signal_subtype == signal_sub_type.three_aspect:
115 canvas.itemconfigure(yel2,state='hidden')
116 offset = -10
117 else: # its a 4 aspect signal
118 offset = 0
120 # Now draw the feathers (x has been adjusted for the no of aspects)
121 line_coords = common.rotate_line (x,y,offset+71,-20,offset+85,-20,orientation)
122 main = canvas.create_line (line_coords,width=3,fill="black",tags=sig_id_tag)
123 line_coords = common.rotate_line (x,y,offset+71,-20,offset+81,-10,orientation)
124 rhf45 = canvas.create_line (line_coords,width=3,fill="black",tags=sig_id_tag)
125 line_coords = common.rotate_line (x,y,offset+71,-20,offset+71,-5,orientation)
126 rhf90 = canvas.create_line (line_coords,width=3,fill="black",tags=sig_id_tag)
127 line_coords = common.rotate_line (x,y,offset+71,-20,offset+81,-30,orientation)
128 lhf45 = canvas.create_line (line_coords,width=3,fill="black",tags=sig_id_tag)
129 line_coords = common.rotate_line (x,y,offset+71,-20,offset+71,-35,orientation)
130 lhf90 = canvas.create_line (line_coords,width=3,fill="black",tags=sig_id_tag)
131 # Hide any feather drawing objects we don't need for this particular signal
132 if not mainfeather: canvas.itemconfigure(main,state='hidden')
133 if not lhfeather45: canvas.itemconfigure(lhf45,state='hidden')
134 if not lhfeather90: canvas.itemconfigure(lhf90,state='hidden')
135 if not rhfeather45: canvas.itemconfigure(rhf45,state='hidden')
136 if not rhfeather90: canvas.itemconfigure(rhf90,state='hidden')
138 # Set the "Override" Aspect - this is the default aspect that will be displayed
139 # by the signal when it is overridden - This will be RED apart from 2 aspect
140 # Distant signals where it will be YELLOW
141 if signal_subtype == signal_sub_type.distant:
142 override_aspect = signals_common.signal_state_type.CAUTION
143 else:
144 override_aspect = signals_common.signal_state_type.DANGER
146 # Create all of the signal elements common to all signal types
147 signals_common.create_common_signal_elements (canvas, sig_id, x, y,
148 signal_type = signals_common.sig_type.colour_light,
149 ext_callback = sig_callback,
150 orientation = orientation,
151 subsidary = position_light,
152 sig_passed_button = sig_passed_button,
153 automatic = fully_automatic,
154 tag = sig_id_tag)
156 # Create the signal elements for a Theatre Route indicator
157 signals_common.create_theatre_route_elements (canvas, sig_id, x, y, xoff=offset+80, yoff = -20,
158 orientation = orientation,has_theatre = theatre_route_indicator)
160 # Create the signal elements to support Approach Control
161 signals_common.create_approach_control_elements (canvas, sig_id, x, y, orientation = orientation,
162 approach_button = approach_release_button)
164 # Add all of the signal-specific elements we need to manage colour light signal types
165 # Note that setting a "sigstate" of RED is valid for all 2 aspect signals
166 # as the associated drawing objects have been "swapped" by the code above
167 # All SHARED attributes are signals_common to more than one signal Types
168 signals_common.signals[str(sig_id)]["overriddenaspect"] = override_aspect # Type-specific - The 'Overridden' aspect
169 signals_common.signals[str(sig_id)]["subtype"] = signal_subtype # Type-specific - subtype of the signal
170 signals_common.signals[str(sig_id)]["refresh"] = refresh_immediately # Type-specific - controls when aspects are updated
171 signals_common.signals[str(sig_id)]["hasfeathers"] = signal_has_feathers # Type-specific - If there is a Feather Route display
172 signals_common.signals[str(sig_id)]["featherenabled"] = None # Type-specific - State of the Feather Route display
173 signals_common.signals[str(sig_id)]["grn"] = grn # Type-specific - drawing object
174 signals_common.signals[str(sig_id)]["yel"] = yel # Type-specific - drawing object
175 signals_common.signals[str(sig_id)]["red"] = red # Type-specific - drawing object
176 signals_common.signals[str(sig_id)]["yel2"] = yel2 # Type-specific - drawing object
177 signals_common.signals[str(sig_id)]["pos1"] = poslight1 # Type-specific - drawing object
178 signals_common.signals[str(sig_id)]["pos2"] = poslight2 # Type-specific - drawing object
179 signals_common.signals[str(sig_id)]["mainf"] = main # Type-specific - drawing object
180 signals_common.signals[str(sig_id)]["lhf45"] = lhf45 # Type-specific - drawing object
181 signals_common.signals[str(sig_id)]["lhf90"] = lhf90 # Type-specific - drawing object
182 signals_common.signals[str(sig_id)]["rhf45"] = rhf45 # Type-specific - drawing object
183 signals_common.signals[str(sig_id)]["rhf90"] = rhf90 # Type-specific - drawing object
185 # Create the timed sequence class instances for the signal (one per route)
186 signals_common.signals[str(sig_id)]["timedsequence"] = []
187 for route in signals_common.route_type:
188 signals_common.signals[str(sig_id)]["timedsequence"].append(timed_sequence(sig_id,route))
190 # Get the initial state for the signal (if layout state has been successfully loaded)
191 # Note that each element of 'loaded_state' will be 'None' if no data was loaded
192 loaded_state = file_interface.get_initial_item_state("signals",sig_id)
193 # Note that for Enum types we load the value - need to turn this back into the Enum
194 if loaded_state["routeset"] is not None:
195 loaded_state["routeset"] = signals_common.route_type(loaded_state["routeset"])
196 # Set the initial state from the "loaded" state
197 if loaded_state["releaseonred"]: signals_common.set_approach_control(sig_id,release_on_yellow=False)
198 if loaded_state["releaseonyel"]: signals_common.set_approach_control(sig_id,release_on_yellow=True)
199 if loaded_state["theatretext"]: signals_common.update_theatre_route_indication(sig_id,loaded_state["theatretext"])
200 if loaded_state["routeset"]: update_feather_route_indication(sig_id,loaded_state["routeset"])
201 if loaded_state["override"]: signals_common.set_signal_override(sig_id)
202 # If no state was loaded we still need to toggle fully automatic signals to OFF
203 if loaded_state["sigclear"] or fully_automatic: signals_common.toggle_signal(sig_id)
204 # Update the signal to show the initial aspect (and send out DCC commands)
205 # We only refresh the signal if it is set to refresh immediately
206 if signals_common.signals[str(sig_id)]["refresh"]: update_colour_light_signal(sig_id)
207 # finally Lock the signal if required
208 if loaded_state["siglocked"]: signals_common.lock_signal(sig_id)
210 if position_light:
211 # Set the initial state of the subsidary from the "loaded" state
212 if loaded_state["subclear"]: signals_common.toggle_subsidary(sig_id)
213 # Update the signal to show the initial aspect (and send out DCC commands)
214 update_colour_light_subsidary(sig_id)
215 # finally Lock the subsidary if required
216 if loaded_state["sublocked"]: signals_common.lock_subsidary(sig_id)
218 # Publish the initial state to the broker (for other nodes to consume). Note that changes will
219 # only be published if the MQTT interface has been configured for publishing updates for this
220 # signal. This allows publish/subscribe to be configured prior to signal creation
221 signals_common.publish_signal_state(sig_id)
223 return ()
225#-------------------------------------------------------------------
226# Internal Function to update the current state of the Subsidary signal
227# (on/off). If a Subsidary was not specified at creation time then the
228# objects are hidden' and the function will have no effect.
229#------------------------------------------------------------------
231def update_colour_light_subsidary (sig_id:int):
232 if signals_common.signals[str(sig_id)]["subclear"]:
233 logging.info ("Signal "+str(sig_id)+": Changing subsidary aspect to PROCEED")
234 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["pos1"],fill="white")
235 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["pos2"],fill="white")
236 dcc_control.update_dcc_signal_element(sig_id,True,element="main_subsidary")
237 else:
238 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["pos1"],fill="grey")
239 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["pos2"],fill="grey")
240 logging.info ("Signal "+str(sig_id)+": Changing subsidary aspect to UNLIT")
241 dcc_control.update_dcc_signal_element(sig_id,False,element="main_subsidary")
242 return ()
244# -------------------------------------------------------------------------
245# Function to Refresh the displayed signal aspect according the signal state
246# Also takes into account the state of the signal ahead if one is specified
247# to ensure the correct aspect is displayed (for 3/4 aspect types and 2 aspect
248# distant signals). E.g. for a 3/4 aspect signal - if the signal ahead is ON
249# and this signal is OFF then we want to change it to YELLOW rather than GREEN
250# -------------------------------------------------------------------------
252def update_colour_light_signal (sig_id:int, sig_ahead_id:Union[str,int]=None):
254 route = signals_common.signals[str(sig_id)]["routeset"]
256 # ---------------------------------------------------------------------------------
257 # First deal with the Signal ON, Overridden or "Release on Red" cases
258 # as they will apply to all colour light signal types (2, 3 or 4 aspect)
259 # ---------------------------------------------------------------------------------
261 # If signal is set to "ON" then its DANGER (or CAUTION if its a 2 aspect distant)
262 if not signals_common.signals[str(sig_id)]["sigclear"]:
263 if signals_common.signals[str(sig_id)]["subtype"] == signal_sub_type.distant:
264 new_aspect = signals_common.signal_state_type.CAUTION
265 log_message = " (signal is ON and 2-aspect distant)"
266 else:
267 new_aspect = signals_common.signal_state_type.DANGER
268 log_message = " (signal is ON)"
270 # If signal is Overriden the set the signal to its overriden aspect
271 elif signals_common.signals[str(sig_id)]["override"]:
272 new_aspect = signals_common.signals[str(sig_id)]["overriddenaspect"]
273 log_message = " (signal is OVERRIDEN)"
275 # If signal is Overriden to CAUTION set the signal to display CAUTION
276 # Note we are relying on the public API function to only allow this to
277 # be set for signal types apart from 2 aspect home signals
278 elif signals_common.signals[str(sig_id)]["overcaution"]:
279 new_aspect = signals_common.signal_state_type.CAUTION
280 log_message = " (signal is OVERRIDDEN to CAUTION)"
282 # If signal is triggered on a timed sequence then set to the sequence aspect
283 elif signals_common.signals[str(sig_id)]["timedsequence"][route.value].sequence_in_progress:
284 new_aspect = signals_common.signals[str(sig_id)]["timedsequence"][route.value].aspect
285 log_message = " (signal is on a timed sequence)"
287 # Set to DANGER if the signal is subject to "Release on Red" approach control
288 # Note that this state should never apply to 2 aspect distant signals
289 elif signals_common.signals[str(sig_id)]["releaseonred"]: 289 ↛ 290line 289 didn't jump to line 290, because the condition on line 289 was never true
290 new_aspect = signals_common.signal_state_type.DANGER
291 log_message = " (signal is OFF - but subject to \'release on red\' approach control)"
293 # ---------------------------------------------------------------------------------
294 # From here, the Signal is Displaying "OFF" - but could still be of any type
295 # ---------------------------------------------------------------------------------
297 # If the signal is a 2 aspect home signal or a 2 aspect red/yellow signal
298 # we can ignore the signal ahead and set it to its "clear" aspect
299 elif signals_common.signals[str(sig_id)]["subtype"] == signal_sub_type.home:
300 new_aspect = signals_common.signal_state_type.PROCEED
301 log_message = " (signal is OFF and 2-aspect home)"
303 elif signals_common.signals[str(sig_id)]["subtype"] == signal_sub_type.red_ylw:
304 new_aspect = signals_common.signal_state_type.CAUTION
305 log_message = " (signal is OFF and 2-aspect R/Y)"
307 # ---------------------------------------------------------------------------------
308 # From here, the Signal is CLEAR and is a 2 aspect Distant or a 3/4 aspect signal
309 # ---------------------------------------------------------------------------------
311 # Set to CAUTION if the signal is subject to "Release on YELLOW" approach control
312 # We use the special CAUTION_APPROACH_CONTROL for "update on signal ahead" purposes
313 elif signals_common.signals[str(sig_id)]["releaseonyel"]:
314 new_aspect = signals_common.signal_state_type.CAUTION_APP_CNTL
315 log_message = " (signal is OFF - but subject to \'release on yellow\' approach control)"
317 # ---------------------------------------------------------------------------------
318 # From here Signal the Signal is CLEAR and is a 2 aspect Distant or 3/4 aspect signal
319 # and will display the "normal" aspects based on the signal ahead (if one has been specified)
320 # ---------------------------------------------------------------------------------
322 # If no signal ahead has been specified then we can set the signal to its "clear" aspect
323 # (Applies to 2 aspect distant signals as well as the remaining 3 and 4 aspect signals types)
324 elif sig_ahead_id is None:
325 new_aspect = signals_common.signal_state_type.PROCEED
326 log_message = " (signal is OFF and no signal ahead specified)"
328 # ---------------------------------------------------------------------------------
329 # From here Signal the Signal is CLEAR and is a 2 aspect Distant or 3/4 aspect signal
330 # and will display the "normal" aspects based on the signal ahead (one has been specified
331 # ---------------------------------------------------------------------------------
333 else:
335 if signals_common.signals[str(sig_ahead_id)]["sigstate"] == signals_common.signal_state_type.DANGER:
336 # All remaining signal types (3/4 aspects and 2 aspect distants) should display CAUTION
337 new_aspect = signals_common.signal_state_type.CAUTION
338 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying DANGER)")
340 elif signals_common.signals[str(sig_ahead_id)]["sigstate"] == signals_common.signal_state_type.CAUTION_APP_CNTL:
341 # All remaining signal types (3/4 aspects and 2 aspect distants) should display FLASHING CAUTION
342 new_aspect = signals_common.signal_state_type.FLASH_CAUTION
343 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+
344 " is subject to \'release on yellow\' approach control)")
346 elif signals_common.signals[str(sig_ahead_id)]["sigstate"] == signals_common.signal_state_type.CAUTION:
347 if signals_common.signals[str(sig_id)]["subtype"] == signal_sub_type.four_aspect: 347 ↛ 353line 347 didn't jump to line 353, because the condition on line 347 was never false
348 # 4 aspect signals should display a PRELIM_CAUTION aspect
349 new_aspect = signals_common.signal_state_type.PRELIM_CAUTION
350 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying CAUTION)")
351 else:
352 # 3 aspect signals and 2 aspect distant signals should display PROCEED
353 new_aspect = signals_common.signal_state_type.PROCEED
354 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying CAUTION)")
356 elif signals_common.signals[str(sig_ahead_id)]["sigstate"] == signals_common.signal_state_type.FLASH_CAUTION:
357 if signals_common.signals[str(sig_id)]["subtype"] == signal_sub_type.four_aspect: 357 ↛ 363line 357 didn't jump to line 363, because the condition on line 357 was never false
358 # 4 aspect signals will display a FLASHING PRELIM CAUTION aspect
359 new_aspect = signals_common.signal_state_type.FLASH_PRELIM_CAUTION
360 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying FLASHING_CAUTION)")
361 else:
362 # 3 aspect signals and 2 aspect distant signals should display PROCEED
363 new_aspect = signals_common.signal_state_type.PROCEED
364 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying PROCEED)")
365 else:
366 # A signal ahead state is either PRELIM_CAUTION, FLASH PRELIM CAUTION or PROCEED
367 # These states have have no effect on the signal we are updating - Signal will show PROCEED
368 new_aspect = signals_common.signal_state_type.PROCEED
369 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying "
370 + str(signals_common.signals[str(sig_ahead_id)]["sigstate"]).rpartition('.')[-1] + ")")
372 current_aspect = signals_common.signals[str(sig_id)]["sigstate"]
374 # Only refresh the signal if the aspect has been changed
375 if new_aspect != current_aspect:
376 logging.info ("Signal "+str(sig_id)+": Changing aspect to " + str(new_aspect).rpartition('.')[-1] + log_message)
377 # Update the current aspect - note that this dictionary element is also used by the Flash Aspects Thread
378 signals_common.signals[str(sig_id)]["sigstate"] = new_aspect
379 refresh_signal_aspects (sig_id)
380 # Update the Theatre & Feather route indications as these are inhibited/enabled for transitions to/from DANGER
381 enable_disable_feather_route_indication(sig_id)
382 signals_common.enable_disable_theatre_route_indication(sig_id)
383 # Send the required DCC bus commands to change the signal to the desired aspect. Note that commands will only
384 # be sent if the Pi-SPROG interface has been successfully configured and a DCC mapping exists for the signal
385 dcc_control.update_dcc_signal_aspects(sig_id, new_aspect)
386 # Publish the signal changes to the broker (for other nodes to consume). Note that state changes will only
387 # be published if the MQTT interface has been successfully configured for publishing updates for this signal
388 signals_common.publish_signal_state(sig_id)
390 return ()
392# -------------------------------------------------------------------------
393# Internal Functions for cycling the flashing aspects. Rather than using a
394# Thread to do this, we use the tkinter 'after' method to scedule the next
395# update via the tkinter event queue (as Tkinter is not Threadsafe)
396# -------------------------------------------------------------------------
398def flash_aspect_off(sig_id):
399 if not common.shutdown_initiated: 399 ↛ 405line 399 didn't jump to line 405, because the condition on line 399 was never false
400 if (signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_CAUTION
401 or signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_PRELIM_CAUTION):
402 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="grey")
403 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="grey")
404 common.root_window.after(250,lambda:flash_aspect_on(sig_id))
405 return()
407def flash_aspect_on(sig_id):
408 if not common.shutdown_initiated: 408 ↛ 417line 408 didn't jump to line 417, because the condition on line 408 was never false
409 if signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_CAUTION:
410 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="yellow")
411 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="grey")
412 common.root_window.after(250,lambda:flash_aspect_off(sig_id))
413 if signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_PRELIM_CAUTION:
414 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="yellow")
415 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="yellow")
416 common.root_window.after(250,lambda:flash_aspect_off(sig_id))
417 return()
419# -------------------------------------------------------------------------
420# Internal function to Refresh the displayed signal aspect by
421# updating the signal drawing objects associated with each aspect
422# -------------------------------------------------------------------------
424def refresh_signal_aspects (sig_id:int):
426 if signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.DANGER:
427 # Change the signal to display the RED aspect
428 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="red")
429 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="grey")
430 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="grey")
431 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="grey")
433 elif (signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.CAUTION
434 or signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.CAUTION_APP_CNTL):
435 # Change the signal to display the Yellow aspect
436 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="grey")
437 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="yellow")
438 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="grey")
439 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="grey")
441 elif signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.PRELIM_CAUTION:
442 # Change the signal to display the Double Yellow aspect
443 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="grey")
444 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="yellow")
445 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="grey")
446 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="yellow")
448 elif signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_CAUTION:
449 # The flash_aspect_on function will start the flashing aspect so just turn off the other aspects
450 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="grey")
451 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="grey")
452 flash_aspect_on(sig_id)
454 elif signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_PRELIM_CAUTION:
455 # The flash_aspect_on function will start the flashing aspect so just turn off the other aspects
456 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="grey")
457 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="grey")
458 flash_aspect_on(sig_id)
460 elif signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.PROCEED: 460 ↛ 467line 460 didn't jump to line 467, because the condition on line 460 was never false
461 # Change the signal to display the Green aspect
462 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="grey")
463 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="grey")
464 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="green")
465 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="grey")
467 return ()
469# -------------------------------------------------------------------------
470# Function to change the feather route indication (on route change)
471# -------------------------------------------------------------------------
473def update_feather_route_indication (sig_id:int,route_to_set):
474 # Only Change the route indication if the signal has feathers
475 if signals_common.signals[str(sig_id)]["hasfeathers"]:
476 # Deal with route changes - but only if the Route has actually been changed
477 if route_to_set != signals_common.signals[str(sig_id)]["routeset"]:
478 signals_common.signals[str(sig_id)]["routeset"] = route_to_set
479 if signals_common.signals[str(sig_id)]["featherenabled"] == True: 479 ↛ 480line 479 didn't jump to line 480, because the condition on line 479 was never true
480 logging.info ("Signal "+str(sig_id)+": Changing feather route display to "+ str(route_to_set).rpartition('.')[-1])
481 dcc_control.update_dcc_signal_route (sig_id, signals_common.signals[str(sig_id)]["routeset"],
482 signal_change = False, sig_at_danger = False)
483 else:
484 logging.info ("Signal "+str(sig_id)+": Setting signal route to "+str(route_to_set).rpartition('.')[-1])
485 # We always call the function to update the DCC route indication on a change in route even if the signal
486 # is at Danger to cater for DCC signal types that automatically enable/disable the route indication
487 dcc_control.update_dcc_signal_route (sig_id, signals_common.signals[str(sig_id)]["routeset"],
488 signal_change = False, sig_at_danger = True)
489 # Refresh the signal aspect (a catch-all to ensure the signal displays the correct aspect
490 # in case the signal is in the middle of a timed sequence for the old route or the new route
491 if signals_common.signals[str(sig_id)]["refresh"]: update_colour_light_signal(sig_id)
492 # Update the feathers on the display
493 update_feathers(sig_id)
494 return()
496# -------------------------------------------------------------------------
497# Internal Function that gets called on a signal aspect change - will
498# Enable/disable the feather route indication on a change to/from DANGER
499# -------------------------------------------------------------------------
501def enable_disable_feather_route_indication (sig_id:int):
502 # Only Enable/Disable the route indication if the signal has feathers
503 if signals_common.signals[str(sig_id)]["hasfeathers"]:
504 # We test for !True and !False to support the initial state when the signal is created (state = None)
505 if (signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.DANGER
506 and signals_common.signals[str(sig_id)]["featherenabled"] != False):
507 logging.info ("Signal "+str(sig_id)+": Disabling feather route display (signal is at RED)")
508 signals_common.signals[str(sig_id)]["featherenabled"] = False
509 dcc_control.update_dcc_signal_route(sig_id,signals_common.route_type.NONE,
510 signal_change=True,sig_at_danger=True)
512 elif (signals_common.signals[str(sig_id)]["sigstate"] != signals_common.signal_state_type.DANGER
513 and signals_common.signals[str(sig_id)]["featherenabled"] != True):
514 logging.info ("Signal "+str(sig_id)+": Enabling feather route display for "
515 + str(signals_common.signals[str(sig_id)]["routeset"]).rpartition('.')[-1])
516 signals_common.signals[str(sig_id)]["featherenabled"] = True
517 dcc_control.update_dcc_signal_route(sig_id,signals_common.signals[str(sig_id)]["routeset"],
518 signal_change=True,sig_at_danger=False)
519 # Update the feathers on the display
520 update_feathers(sig_id)
521 return()
523# -------------------------------------------------------------------------
524# Internal Function to update the drawing objects for the feather indicators.
525# -------------------------------------------------------------------------
527def update_feathers(sig_id:int):
528 # initially set all the indications to OFF - we'll then set what we need
529 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["lhf45"],fill="black")
530 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["lhf90"],fill="black")
531 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["rhf45"],fill="black")
532 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["rhf90"],fill="black")
533 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["mainf"],fill="black")
534 # Only display the route indication if the signal is not at RED
535 if signals_common.signals[str(sig_id)]["sigstate"] != signals_common.signal_state_type.DANGER:
536 if signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.LH1:
537 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["lhf45"],fill="white")
538 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.LH2:
539 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["lhf90"],fill="white")
540 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.RH1:
541 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["rhf45"],fill="white")
542 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.RH2:
543 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["rhf90"],fill="white")
544 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.MAIN: 544 ↛ 546line 544 didn't jump to line 546, because the condition on line 544 was never false
545 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["mainf"],fill="white")
546 return()
548# -------------------------------------------------------------------------
549# Class for a timed signal sequence. A class instance is created for each
550# route for each signal. When a timed signal is triggered the existing
551# instance is first aborted. A new instance is then created/started
552# -------------------------------------------------------------------------
554class timed_sequence():
555 def __init__(self, sig_id:int, route, start_delay:int=0, time_delay:int=0):
556 self.sig_id = sig_id
557 self.sig_route = route
558 self.aspect = signals_common.signals[str(sig_id)]["overriddenaspect"]
559 self.start_delay = start_delay
560 self.time_delay = time_delay
561 self.sequence_abort_flag = False
562 self.sequence_in_progress = False
564 def abort(self):
565 self.sequence_abort_flag = True
567 def start(self):
568 if self.sequence_abort_flag or not signals_common.sig_exists(self.sig_id): 568 ↛ 569line 568 didn't jump to line 569, because the condition on line 568 was never true
569 self.sequence_in_progress = False
570 else:
571 self.sequence_in_progress = True
572 # For a start delay of zero we assume the intention is not to make a callback (on the basis
573 # that the user has triggered the timed signal in the first place). For start delays > 0 the
574 # sequence is initiated after the specified delay and this will trigger a callback
575 # Note that we only change the aspect and generate the callback if the same route is set
576 if signals_common.signals[str(self.sig_id)]["routeset"] == self.sig_route: 576 ↛ 588line 576 didn't jump to line 588, because the condition on line 576 was never false
577 if self.start_delay > 0: 577 ↛ 578line 577 didn't jump to line 578, because the condition on line 577 was never true
578 logging.info("Signal "+str(self.sig_id)+": Timed Signal - Signal Passed Event **************************")
579 # Update the signal for automatic "signal passed" events as Signal is OVERRIDDEN
580 update_colour_light_signal(self.sig_id)
581 # Publish the signal passed event via the mqtt interface. Note that the event will only be published if the
582 # mqtt interface has been successfully configured and the signal has been set to publish passed events
583 signals_common.publish_signal_passed_event(self.sig_id)
584 signals_common.signals[str(self.sig_id)]["extcallback"] (self.sig_id, signals_common.sig_callback_type.sig_passed)
585 else:
586 update_colour_light_signal(self.sig_id)
587 # We only need to schedule the next YELLOW aspect for 3 and 4 aspect signals - otherwise schedule sequence completion
588 if signals_common.signals[str(self.sig_id)]["subtype"] in (signal_sub_type.three_aspect, signal_sub_type.four_aspect): 588 ↛ 591line 588 didn't jump to line 591, because the condition on line 588 was never false
589 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_yellow())
590 else:
591 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_end())
593 def timed_signal_sequence_yellow(self):
594 if self.sequence_abort_flag or not signals_common.sig_exists(self.sig_id): 594 ↛ 595line 594 didn't jump to line 595, because the condition on line 594 was never true
595 self.sequence_in_progress = False
596 else:
597 # This sequence step only applicable to 3 and 4 aspect signals
598 self.aspect = signals_common.signal_state_type.CAUTION
599 # Only change the aspect and generate the callback if the same route is set
600 if signals_common.signals[str(self.sig_id)]["routeset"] == self.sig_route: 600 ↛ 605line 600 didn't jump to line 605, because the condition on line 600 was never false
601 logging.info("Signal "+str(self.sig_id)+": Timed Signal - Signal Updated Event *************************")
602 update_colour_light_signal(self.sig_id)
603 signals_common.signals[str(self.sig_id)]["extcallback"] (self.sig_id, signals_common.sig_callback_type.sig_updated)
604 # We only need to schedule the next DOUBLE YELLOW aspect for 4 aspect signals - otherwise schedule sequence completion
605 if signals_common.signals[str(self.sig_id)]["subtype"] == signal_sub_type.four_aspect: 605 ↛ 608line 605 didn't jump to line 608, because the condition on line 605 was never false
606 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_double_yellow())
607 else:
608 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_end())
610 def timed_signal_sequence_double_yellow(self):
611 if self.sequence_abort_flag or not signals_common.sig_exists(self.sig_id): 611 ↛ 612line 611 didn't jump to line 612, because the condition on line 611 was never true
612 self.sequence_in_progress = False
613 else:
614 # This sequence step only applicable to 4 aspect signals
615 self.aspect = signals_common.signal_state_type.PRELIM_CAUTION
616 # Only change the aspect and generate the callback if the same route is set
617 if signals_common.signals[str(self.sig_id)]["routeset"] == self.sig_route: 617 ↛ 622line 617 didn't jump to line 622, because the condition on line 617 was never false
618 logging.info("Signal "+str(self.sig_id)+": Timed Signal - Signal Updated Event *************************")
619 update_colour_light_signal(self.sig_id)
620 signals_common.signals[str(self.sig_id)]["extcallback"] (self.sig_id, signals_common.sig_callback_type.sig_updated)
621 # Schedule the next aspect change (which will be the sequence completion)
622 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_end())
624 def timed_signal_sequence_end(self):
625 # We've finished - Set the signal back to its "normal" condition
626 self.sequence_in_progress = False
627 if signals_common.sig_exists(self.sig_id): 627 ↛ exitline 627 didn't return from function 'timed_signal_sequence_end', because the condition on line 627 was never false
628 # Only change the aspect and generate the callback if the same route is set
629 if signals_common.signals[str(self.sig_id)]["routeset"] == self.sig_route: 629 ↛ exitline 629 didn't return from function 'timed_signal_sequence_end', because the condition on line 629 was never false
630 logging.info("Signal "+str(self.sig_id)+": Timed Signal - Signal Updated Event *************************")
631 update_colour_light_signal(self.sig_id)
632 signals_common.signals[str(self.sig_id)]["extcallback"] (self.sig_id, signals_common.sig_callback_type.sig_updated)
634# -------------------------------------------------------------------------
635# Function to initiate a timed signal sequence - setting the signal to RED and then
636# cycling through all of the supported aspects all the way back to GREEN (or YELLOW
637# in the case of a RED/YELLOW 2-aspect signal). Intended for automation of 'exit'
638# signals on a layout. The start_delay is the initial delay (in seconds) before the
639# signal is changed to RED and the time_delay is the delay (in seconds) between each
640# aspect change. A 'sig_passed' callback event will be generated when the signal is
641# overriden if a start delay (> 0) is specified. For each subsequent aspect change
642# a 'sig_updated' callback event will be generated.
643# -------------------------------------------------------------------------
645def trigger_timed_colour_light_signal (sig_id:int,start_delay:int=0,time_delay:int=5):
647 def delayed_sequence_start(sig_id:int, sig_route):
648 if signals_common.sig_exists(sig_id):
649 signals_common.signals[str(sig_id)]["timedsequence"][route.value].start()
651 # Don't initiate a timed signal sequence if a shutdown has already been initiated
652 if common.shutdown_initiated: 652 ↛ 653line 652 didn't jump to line 653, because the condition on line 652 was never true
653 logging.warning("Signal "+str(sig_id)+": Timed Signal - Shutdown initiated - not triggering timed signal")
654 else:
655 # Abort any timed signal sequences already in progess
656 route = signals_common.signals[str(sig_id)]["routeset"]
657 signals_common.signals[str(sig_id)]["timedsequence"][route.value].abort()
658 # Create a new instnce of the time signal class - this should have the effect of "destroying"
659 # the old instance when it goes out of scope, leaving us with the newly created instance
660 signals_common.signals[str(sig_id)]["timedsequence"][route.value] = timed_sequence(sig_id, route, start_delay, time_delay)
661 # Schedule the start of the sequence (i.e. signal to danger) if the start delay is greater than zero
662 # Otherwise initiate the sequence straight away (so the signal state is updated immediately)
663 if start_delay > 0: 663 ↛ 664line 663 didn't jump to line 664, because the condition on line 663 was never true
664 common.root_window.after(start_delay*1000,lambda:delayed_sequence_start(sig_id,route))
665 else:
666 signals_common.signals[str(sig_id)]["timedsequence"][route.value].start()
668###############################################################################