Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/library/signals_semaphores.py: 84%
461 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 semaphore 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 semaphore signals
16# -------------------------------------------------------------------------
18# Define the superset of signal sub types that can be created
19class semaphore_sub_type(enum.Enum):
20 home = 1
21 distant = 2
23# ---------------------------------------------------------------------------------
24# Externally called Function to create a Semaphore Signal 'object'. The Signal is
25# normally set to "NOT CLEAR" = RED unless its fully automatic - when its set to "CLEAR"
26# All attributes (that need to be tracked) are stored as a dictionary which is then
27# stored in the common dictionary of signals. Note that some elements in the dictionary
28# are MANDATORY across all signal types (to allow mixing and matching of signal types)
29# ---------------------------------------------------------------------------------
31def create_semaphore_signal (canvas, sig_id: int, x:int, y:int,
32 signal_subtype=semaphore_sub_type.home,
33 associated_home:int = 0,
34 sig_callback = None,
35 orientation:int = 0,
36 sig_passed_button:bool=False,
37 approach_release_button:bool=False,
38 main_signal:bool=True,
39 lh1_signal:bool=False,
40 lh2_signal:bool=False,
41 rh1_signal:bool=False,
42 rh2_signal:bool=False,
43 main_subsidary:bool=False,
44 lh1_subsidary:bool=False,
45 lh2_subsidary:bool=False,
46 rh1_subsidary:bool=False,
47 rh2_subsidary:bool=False,
48 theatre_route_indicator:bool=False,
49 refresh_immediately:bool = True,
50 fully_automatic:bool=False):
52 # Do some basic validation on the parameters we have been given
53 logging.info ("Signal "+str(sig_id)+": Creating Semaphore Signal")
55 has_subsidary = main_subsidary or lh1_subsidary or lh2_subsidary or rh1_subsidary or rh2_subsidary
56 has_junction_arms = (lh1_subsidary or lh2_subsidary or rh1_subsidary or rh2_subsidary or
57 lh1_signal or lh2_signal or rh1_signal or rh2_signal )
59 if signals_common.sig_exists(sig_id): 59 ↛ 60line 59 didn't jump to line 60, because the condition on line 59 was never true
60 logging.error ("Signal "+str(sig_id)+": Signal already exists")
61 elif sig_id < 1: 61 ↛ 62line 61 didn't jump to line 62, because the condition on line 61 was never true
62 logging.error ("Signal "+str(sig_id)+": Signal ID must be greater than zero")
63 elif orientation != 0 and orientation != 180: 63 ↛ 64line 63 didn't jump to line 64, because the condition on line 63 was never true
64 logging.error ("Signal "+str(sig_id)+": Invalid orientation angle - only 0 and 180 currently supported")
65 elif has_junction_arms and theatre_route_indicator: 65 ↛ 66line 65 didn't jump to line 66, because the condition on line 65 was never true
66 logging.error ("Signal "+str(sig_id)+": Signal can only have junction arms OR a Theatre Route Indicator")
67 elif signal_subtype == semaphore_sub_type.distant and theatre_route_indicator: 67 ↛ 68line 67 didn't jump to line 68, because the condition on line 67 was never true
68 logging.error ("Signal "+str(sig_id)+": Distant signals should not have a Theatre Route Indicator")
69 elif signal_subtype == semaphore_sub_type.distant and has_subsidary: 69 ↛ 70line 69 didn't jump to line 70, because the condition on line 69 was never true
70 logging.error ("Signal "+str(sig_id)+": Distant signals should not have subsidary signals")
71 elif signal_subtype == semaphore_sub_type.distant and approach_release_button: 71 ↛ 72line 71 didn't jump to line 72, because the condition on line 71 was never true
72 logging.error ("Signal "+str(sig_id)+": Distant signals should not have Approach Release Control")
73 elif associated_home > 0 and signal_subtype == semaphore_sub_type.home: 73 ↛ 74line 73 didn't jump to line 74, because the condition on line 73 was never true
74 logging.error ("Signal "+str(sig_id)+": Can only specify an associated signal for a distant signal")
75 elif associated_home > 0 and not signals_common.sig_exists(associated_home): 75 ↛ 76line 75 didn't jump to line 76, because the condition on line 75 was never true
76 logging.error ("Signal "+str(sig_id)+": Associated signal "+str(associated_home)+" does not exist")
77 elif associated_home > 0 and signals_common.signals[str(associated_home)]["sigtype"] != signals_common.sig_type.semaphore: 77 ↛ 78line 77 didn't jump to line 78, because the condition on line 77 was never true
78 logging.error ("Signal "+str(sig_id)+": Associated signal "+str(associated_home)+" is not a semaphore type")
79 elif associated_home > 0 and signals_common.signals[str(associated_home)]["subtype"] == semaphore_sub_type.distant: 79 ↛ 80line 79 didn't jump to line 80, because the condition on line 79 was never true
80 logging.error ("Signal "+str(sig_id)+": Associated signal "+str(associated_home)+" is not a home signal")
81 elif associated_home > 0 and sig_passed_button: 81 ↛ 82line 81 didn't jump to line 82, because the condition on line 81 was never true
82 logging.error ("Signal "+str(sig_id)+": Cannot create a signal passed button if associated with another signal")
83 elif associated_home == 0 and not main_signal: 83 ↛ 84line 83 didn't jump to line 84, because the condition on line 83 was never true
84 logging.error ("Signal "+str(sig_id)+": Normal home and distant signals must have a signal arm for the main route")
85 else:
86 # Define the "Tag" for all drawing objects for this signal instance
87 # If it is an associated distant then set the tag the same as the home signal
88 if associated_home > 0: sig_id_tag = "signal"+str(associated_home)
89 else: sig_id_tag = "signal"+str(sig_id)
90 # Work out the offset for the post depending on the combination of signal arms. Note that if
91 # this is a distant signal associated with another home signal then we'll use the post offset
92 # for the existing signal (as there may be a different combination of home arms specified)
93 # This to cater for the situation where not all home arms have an associated distant arm
94 if associated_home > 0:
95 postoffset = signals_common.signals[str(associated_home)]["postoffset"]
96 elif (not rh2_signal and not rh2_subsidary and not rh1_signal and not rh1_subsidary ):
97 if lh2_signal or lh2_subsidary:
98 postoffset = -8
99 elif lh1_signal or lh1_subsidary:
100 postoffset = -15
101 else:
102 postoffset = -20
103 elif rh2_signal or rh2_subsidary:
104 postoffset = -37
105 elif (not rh2_signal and not rh2_subsidary) and (not lh2_signal and not lh2_subsidary): 105 ↛ 108line 105 didn't jump to line 108, because the condition on line 105 was never false
106 postoffset = -22
107 else:
108 postoffset = -22
109 lh2offset = postoffset-28
110 lh1offset = postoffset-14
111 rh1offset = postoffset+14
112 rh2offset = postoffset+28
114 # Draw the signal base & signal post (unless this is a distant associated with an existing home signal
115 # in which case the signal base & post will already have been drawn when the home signal was created
116 # and we therefore only need to add the additional distant arms to the existing posts
117 if associated_home == 0:
118 line_coords = common.rotate_line(x,y,0,0,0,postoffset,orientation)
119 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
120 line_coords = common.rotate_line(x,y,0,postoffset,+70,postoffset,orientation)
121 canvas.create_line(line_coords,width=3,fill="white",tags=sig_id_tag)
122 # Draw the rest of the gantry to support other arms as required
123 if lh2_signal or lh2_subsidary:
124 line_coords = common.rotate_line(x,y,30,postoffset,30,lh2offset,orientation)
125 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
126 line_coords = common.rotate_line(x,y,30,lh2offset,40,lh2offset,orientation)
127 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
128 if lh2_signal: 128 ↛ 131line 128 didn't jump to line 131, because the condition on line 128 was never false
129 line_coords = common.rotate_line(x,y,40,lh2offset,65,lh2offset,orientation)
130 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
131 if lh1_signal or lh1_subsidary:
132 line_coords = common.rotate_line(x,y,30,postoffset,30,lh1offset,orientation)
133 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
134 line_coords = common.rotate_line(x,y,30,lh1offset,40,lh1offset,orientation)
135 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
136 if lh1_signal:
137 line_coords = common.rotate_line(x,y,40,lh1offset,65,lh1offset,orientation)
138 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
139 if rh2_signal or rh2_subsidary:
140 line_coords = common.rotate_line(x,y,30,postoffset,30,rh2offset,orientation)
141 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
142 line_coords = common.rotate_line(x,y,30,rh2offset,40,rh2offset,orientation)
143 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
144 if rh2_signal: 144 ↛ 147line 144 didn't jump to line 147, because the condition on line 144 was never false
145 line_coords = common.rotate_line(x,y,40,rh2offset,65,rh2offset,orientation)
146 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
147 if rh1_signal or rh1_subsidary:
148 line_coords = common.rotate_line(x,y,30,postoffset,30,rh1offset,orientation)
149 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
150 line_coords = common.rotate_line(x,y,30,rh1offset,40,rh1offset,orientation)
151 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
152 if rh1_signal: 152 ↛ 157line 152 didn't jump to line 157, because the condition on line 152 was never false
153 line_coords = common.rotate_line(x,y,40,rh1offset,65,rh1offset,orientation)
154 canvas.create_line(line_coords,width=2,fill="white",tags=sig_id_tag)
156 # set the colour of the signal arm according to the signal type
157 if signal_subtype == semaphore_sub_type.distant: arm_colour="yellow"
158 else: arm_colour = "red"
160 # If this is a distant signal associated with an existing home signal then the distant arms need
161 # to be created underneath the main home signal arms - we therefore need to apply a vertical offset
162 if associated_home > 0: armoffset = -10
163 else: armoffset = 0
164 # Draw the signal arm for the main route
165 line_coords = common.rotate_line(x,y,65+armoffset,postoffset+3,65+armoffset,postoffset-8,orientation)
166 mainsigon = canvas.create_line(line_coords,fill=arm_colour,width=4,tags=sig_id_tag)
167 line_coords = common.rotate_line(x,y,65+armoffset,postoffset+3,72+armoffset,postoffset-8,orientation)
168 mainsigoff = canvas.create_line(line_coords,fill=arm_colour,width=4,state='hidden',tags=sig_id_tag)
169 # Draw the subsidary arm for the main route
170 line_coords = common.rotate_line(x,y,+43,postoffset+3,+43,postoffset-6,orientation)
171 mainsubon = canvas.create_line(line_coords,fill=arm_colour,width=3,tags=sig_id_tag)
172 line_coords = common.rotate_line(x,y,+43,postoffset+3,+48,postoffset-6,orientation)
173 mainsuboff = canvas.create_line(line_coords,fill=arm_colour,width=3,state='hidden',tags=sig_id_tag)
174 # Draw the signal arms for the RH routes
175 line_coords = common.rotate_line(x,y,60+armoffset,rh1offset+2,60+armoffset,rh1offset-8,orientation)
176 rh1sigon = canvas.create_line(line_coords,fill=arm_colour,width=4,tags=sig_id_tag)
177 line_coords = common.rotate_line(x,y,60+armoffset,rh1offset+2,67+armoffset,rh1offset-8,orientation)
178 rh1sigoff = canvas.create_line(line_coords,fill=arm_colour,width=4,state='hidden',tags=sig_id_tag)
179 line_coords = common.rotate_line(x,y,60+armoffset,rh2offset+2,60+armoffset,rh2offset-8,orientation)
180 rh2sigon = canvas.create_line(line_coords,fill=arm_colour,width=4,tags=sig_id_tag)
181 line_coords = common.rotate_line(x,y,60+armoffset,rh2offset+2,67+armoffset,rh2offset-8,orientation)
182 rh2sigoff = canvas.create_line(line_coords,fill=arm_colour,width=4,state='hidden',tags=sig_id_tag)
183 # Draw the subsidary arms for the RH routes
184 line_coords = common.rotate_line(x,y,+38,rh1offset+2,+38,rh1offset-6,orientation)
185 rh1subon = canvas.create_line(line_coords,fill=arm_colour,width=3,tags=sig_id_tag)
186 line_coords = common.rotate_line(x,y,+38,rh1offset+2,+43,rh1offset-6,orientation)
187 rh1suboff = canvas.create_line(line_coords,fill=arm_colour,width=3,state='hidden',tags=sig_id_tag)
188 line_coords = common.rotate_line(x,y,+38,rh2offset+2,+38,rh2offset-6,orientation)
189 rh2subon = canvas.create_line(line_coords,fill=arm_colour,width=3,tags=sig_id_tag)
190 line_coords = common.rotate_line(x,y,+38,rh2offset+2,+43,rh2offset-6,orientation)
191 rh2suboff = canvas.create_line(line_coords,fill=arm_colour,width=3,state='hidden',tags=sig_id_tag)
192 # Draw the signal arms for the LH routes
193 line_coords = common.rotate_line(x,y,60+armoffset,lh1offset+2,60+armoffset,lh1offset-8,orientation)
194 lh1sigon = canvas.create_line(line_coords,fill=arm_colour,width=4,tags=sig_id_tag)
195 line_coords = common.rotate_line(x,y,60+armoffset,lh1offset+2,67+armoffset,lh1offset-8,orientation)
196 lh1sigoff = canvas.create_line(line_coords,fill=arm_colour,width=4,state='hidden',tags=sig_id_tag)
197 line_coords = common.rotate_line(x,y,60+armoffset,lh2offset+2,60+armoffset,lh2offset-8,orientation)
198 lh2sigon = canvas.create_line(line_coords,fill=arm_colour,width=4,tags=sig_id_tag)
199 line_coords = common.rotate_line(x,y,60+armoffset,lh2offset+2,67+armoffset,lh2offset-8,orientation)
200 lh2sigoff = canvas.create_line(line_coords,fill=arm_colour,width=4,state='hidden',tags=sig_id_tag)
201 # Draw the subsidary arms for the LH routes
202 line_coords = common.rotate_line(x,y,+38,lh1offset+2,+38,lh1offset-6,orientation)
203 lh1subon = canvas.create_line(line_coords,fill=arm_colour,width=3,tags=sig_id_tag)
204 line_coords = common.rotate_line(x,y,+38,lh1offset+2,+43,lh1offset-6,orientation)
205 lh1suboff = canvas.create_line(line_coords,fill=arm_colour,width=3,state='hidden',tags=sig_id_tag)
206 line_coords = common.rotate_line(x,y,+38,lh2offset+2,+38,lh2offset-6,orientation)
207 lh2subon = canvas.create_line(line_coords,fill=arm_colour,width=3,tags=sig_id_tag)
208 line_coords = common.rotate_line(x,y,+38,lh2offset+2,+43,lh2offset-6,orientation)
209 lh2suboff = canvas.create_line(line_coords,fill=arm_colour,width=3,state='hidden',tags=sig_id_tag)
210 # Hide any otherdrawing objects we don't need for this particular signal
211 if not main_signal: canvas.itemconfigure(mainsigon,state='hidden')
212 if not main_subsidary: canvas.itemconfigure(mainsubon,state='hidden')
213 if not lh1_subsidary: canvas.itemconfigure(lh1subon,state='hidden')
214 if not rh1_subsidary: canvas.itemconfigure(rh1subon,state='hidden')
215 if not lh2_subsidary: canvas.itemconfigure(lh2subon,state='hidden')
216 if not rh2_subsidary: canvas.itemconfigure(rh2subon,state='hidden')
217 if not lh1_signal: canvas.itemconfigure(lh1sigon,state='hidden')
218 if not rh1_signal: canvas.itemconfigure(rh1sigon,state='hidden')
219 if not lh2_signal: canvas.itemconfigure(lh2sigon,state='hidden')
220 if not rh2_signal: canvas.itemconfigure(rh2sigon,state='hidden')
222 # Set the initial state of the signal Arms. We use True/False to represent the current
223 # state of the signal arm, or a value of 'None' if the arm doesn't exist. We set the
224 # arms that do exist in the "wrong" state initially, so that when they are first updated
225 # they get "changed" to the correct initial state (causing the appropriate DCC commands
226 # to be sent out to the external signal). So, bearing in mind that the parameters passed
227 # into this function were either True or False (with False representing no signal arm)
228 # Signal arm specified - corresponding arm will bee set to True (OFF) initially
229 # No signal arm specified - corresponding arm will be set to None
230 if not main_signal: main_signal = None
231 if not main_subsidary: main_subsidary = None
232 if not lh1_subsidary: lh1_subsidary = None
233 if not rh1_subsidary: rh1_subsidary = None
234 if not lh2_subsidary: lh2_subsidary = None
235 if not rh2_subsidary: rh2_subsidary = None
236 if not lh1_signal: lh1_signal = None
237 if not rh1_signal: rh1_signal = None
238 if not lh2_signal: lh2_signal = None
239 if not rh2_signal: rh2_signal = None
241 # If this is a distant signal associated with another home signal then we need to adjust the
242 # position of the signal button to "deconflict" with the buttons of the home signal
243 if associated_home > 0:
244 if signals_common.signals[str(associated_home)]["hassubsidary"]:
245 button_offset = -55
246 else:
247 button_offset = -37
248 else:
249 button_offset = 0
251 # Create all of the signal elements common to all signal types
252 signals_common.create_common_signal_elements (canvas, sig_id, x, y,
253 ext_callback = sig_callback,
254 signal_type = signals_common.sig_type.semaphore,
255 orientation = orientation,
256 subsidary = has_subsidary,
257 sig_passed_button = sig_passed_button,
258 automatic = fully_automatic,
259 distant_button_offset = button_offset,
260 tag = sig_id_tag)
262 # Create the signal elements for a Theatre Route indicator
263 signals_common.create_theatre_route_elements (canvas, sig_id, x, y, xoff=25, yoff = postoffset,
264 orientation = orientation,has_theatre = theatre_route_indicator)
266 # Create the signal elements to support Approach Control
267 signals_common.create_approach_control_elements (canvas, sig_id, x, y, orientation = orientation,
268 approach_button = approach_release_button)
270 # Compile a dictionary of everything we need to track for the signal
271 # Note that all MANDATORY attributes are signals_common to ALL signal types
272 # All SHARED attributes are signals_common to more than one signal Types
273 signals_common.signals[str(sig_id)]["refresh"] = refresh_immediately # Type-specific - if signal should be refreshed on a change
274 signals_common.signals[str(sig_id)]["subtype"] = signal_subtype # Type-specific - subtype of the signal (home/distant)
275 signals_common.signals[str(sig_id)]["associatedsignal"] = associated_home # Type-specific - subtype of the signal (home/distant)
276 signals_common.signals[str(sig_id)]["main_subsidary"] = main_subsidary # Type-specific - details of the signal configuration
277 signals_common.signals[str(sig_id)]["lh1_subsidary"] = lh1_subsidary # Type-specific - details of the signal configuration
278 signals_common.signals[str(sig_id)]["rh1_subsidary"] = rh1_subsidary # Type-specific - details of the signal configuration
279 signals_common.signals[str(sig_id)]["lh2_subsidary"] = lh2_subsidary # Type-specific - details of the signal configuration
280 signals_common.signals[str(sig_id)]["rh2_subsidary"] = rh2_subsidary # Type-specific - details of the signal configuration
281 signals_common.signals[str(sig_id)]["main_signal"] = main_signal # Type-specific - details of the signal configuration
282 signals_common.signals[str(sig_id)]["lh1_signal"] = lh1_signal # Type-specific - details of the signal configuration
283 signals_common.signals[str(sig_id)]["rh1_signal"] = rh1_signal # Type-specific - details of the signal configuration
284 signals_common.signals[str(sig_id)]["lh2_signal"] = lh2_signal # Type-specific - details of the signal configuration
285 signals_common.signals[str(sig_id)]["rh2_signal"] = rh2_signal # Type-specific - details of the signal configuration
286 signals_common.signals[str(sig_id)]["postoffset"] = postoffset # Type-specific - used for drawing associated distants
287 signals_common.signals[str(sig_id)]["mainsigon"] = mainsigon # Type-specific - drawing object
288 signals_common.signals[str(sig_id)]["mainsigoff"] = mainsigoff # Type-specific - drawing object
289 signals_common.signals[str(sig_id)]["lh1sigon"] = lh1sigon # Type-specific - drawing object
290 signals_common.signals[str(sig_id)]["lh1sigoff"] = lh1sigoff # Type-specific - drawing object
291 signals_common.signals[str(sig_id)]["lh2sigon"] = lh2sigon # Type-specific - drawing object
292 signals_common.signals[str(sig_id)]["lh2sigoff"] = lh2sigoff # Type-specific - drawing object
293 signals_common.signals[str(sig_id)]["rh1sigon"] = rh1sigon # Type-specific - drawing object
294 signals_common.signals[str(sig_id)]["rh1sigoff"] = rh1sigoff # Type-specific - drawing object
295 signals_common.signals[str(sig_id)]["rh2sigon"] = rh2sigon # Type-specific - drawing object
296 signals_common.signals[str(sig_id)]["rh2sigoff"] = rh2sigoff # Type-specific - drawing object
297 signals_common.signals[str(sig_id)]["mainsubon"] = mainsubon # Type-specific - drawing object
298 signals_common.signals[str(sig_id)]["mainsuboff"] = mainsuboff # Type-specific - drawing object
299 signals_common.signals[str(sig_id)]["lh1subon"] = lh1subon # Type-specific - drawing object
300 signals_common.signals[str(sig_id)]["lh1suboff"] = lh1suboff # Type-specific - drawing object
301 signals_common.signals[str(sig_id)]["lh2subon"] = lh2subon # Type-specific - drawing object
302 signals_common.signals[str(sig_id)]["lh2suboff"] = lh2suboff # Type-specific - drawing object
303 signals_common.signals[str(sig_id)]["rh1subon"] = rh1subon # Type-specific - drawing object
304 signals_common.signals[str(sig_id)]["rh1suboff"] = rh1suboff # Type-specific - drawing object
305 signals_common.signals[str(sig_id)]["rh2subon"] = rh2subon # Type-specific - drawing object
306 signals_common.signals[str(sig_id)]["rh2suboff"] = rh2suboff # Type-specific - drawing object
308 # Create the timed sequence class instances for the signal (one per route)
309 signals_common.signals[str(sig_id)]["timedsequence"] = []
310 for route in signals_common.route_type:
311 signals_common.signals[str(sig_id)]["timedsequence"].append(timed_sequence(sig_id,route))
313 # if there is an associated signal then we also need to update that signal to refer back to this one
314 if associated_home > 0: signals_common.signals[str(associated_home)]["associatedsignal"] = sig_id
316 # Get the initial state for the signal (if layout state has been successfully loaded)
317 # Note that each element of 'loaded_state' will be 'None' if no data was loaded
318 loaded_state = file_interface.get_initial_item_state("signals",sig_id)
319 # Note that for Enum types we load the value - need to turn this back into the Enum
320 if loaded_state["routeset"] is not None:
321 loaded_state["routeset"] = signals_common.route_type(loaded_state["routeset"])
322 # Set the initial state from the "loaded" state
323 if loaded_state["releaseonred"]: signals_common.set_approach_control(sig_id,release_on_yellow=False)
324 if loaded_state["releaseonyel"]: signals_common.set_approach_control(sig_id,release_on_yellow=True)
325 if loaded_state["theatretext"]: signals_common.update_theatre_route_indication(sig_id,loaded_state["theatretext"])
326 if loaded_state["routeset"]: signals_common.signals[str(sig_id)]["routeset"]=loaded_state["routeset"]
327 if loaded_state["override"]: signals_common.set_signal_override(sig_id)
328 # If no state was loaded we still need to toggle fully automatic signals to OFF
329 # Note that we also need to Set the signal Arms to the "wrong" initial state so that when they
330 # are first updated they get "changed" to the correct aspects and the correct DCC commands sent out
331 # We test to see if there is a arm for each route (as the signal may not have one for all the routes)
332 if loaded_state["sigclear"] or fully_automatic:
333 if (signals_common.signals[str(sig_id)]["routeset"]==signals_common.route_type.MAIN
334 and signals_common.signals[str(sig_id)]["main_signal"]==True ):
335 signals_common.signals[str(sig_id)]["main_signal"] = False
336 elif (signals_common.signals[str(sig_id)]["routeset"]==signals_common.route_type.LH1
337 and signals_common.signals[str(sig_id)]["lh1_signal"]==True ):
338 signals_common.signals[str(sig_id)]["lh1_signal"] = False
339 elif (signals_common.signals[str(sig_id)]["routeset"]==signals_common.route_type.LH2 339 ↛ 341line 339 didn't jump to line 341, because the condition on line 339 was never true
340 and signals_common.signals[str(sig_id)]["lh2_signal"]==True ):
341 signals_common.signals[str(sig_id)]["lh2_signal"] = False
342 elif (signals_common.signals[str(sig_id)]["routeset"]==signals_common.route_type.RH1 342 ↛ 344line 342 didn't jump to line 344, because the condition on line 342 was never true
343 and signals_common.signals[str(sig_id)]["rh1_signal"]==True ):
344 signals_common.signals[str(sig_id)]["rh1_signal"] = False
345 elif (signals_common.signals[str(sig_id)]["routeset"]==signals_common.route_type.RH2 345 ↛ 347line 345 didn't jump to line 347, because the condition on line 345 was never true
346 and signals_common.signals[str(sig_id)]["rh2_signal"]==True ):
347 signals_common.signals[str(sig_id)]["rh2_signal"] = False
348 signals_common.toggle_signal(sig_id)
349 # Update the signal to show the initial aspect (and send out DCC commands)
350 # We only refresh the signal if it is set to refresh immediately
351 if signals_common.signals[str(sig_id)]["refresh"]: update_semaphore_signal(sig_id)
352 # finally Lock the signal if required
353 if loaded_state["siglocked"]: signals_common.lock_signal(sig_id)
355 # Set the initial state of the subsidary from the "loaded" state
356 # Note that we also need to Set the signal Arms to the "wrong" initial state so that when they
357 # are first updated they get "changed" to the correct aspects and the correct DCC commands sent out
358 # We test to see if there is a subsidary arm (as the signal may not have one for all the routes)
359 if has_subsidary:
360 if loaded_state["subclear"]: 360 ↛ 361line 360 didn't jump to line 361, because the condition on line 360 was never true
361 signals_common.toggle_subsidary(sig_id)
362 if (signals_common.signals[str(sig_id)]["routeset"]==signals_common.route_type.MAIN
363 and signals_common.signals[str(sig_id)]["main_subsidary"]==True ):
364 signals_common.signals[str(sig_id)]["main_subsidary"] = False
365 elif (signals_common.signals[str(sig_id)]["routeset"]==signals_common.route_type.LH1
366 and signals_common.signals[str(sig_id)]["lh1_subsidary"]==True ):
367 signals_common.signals[str(sig_id)]["lh1_subsidary"] = False
368 elif (signals_common.signals[str(sig_id)]["routeset"]==signals_common.route_type.LH2
369 and signals_common.signals[str(sig_id)]["lh2_subsidary"]==True ):
370 signals_common.signals[str(sig_id)]["lh2_subsidary"] = False
371 elif (signals_common.signals[str(sig_id)]["routeset"]==signals_common.route_type.RH1
372 and signals_common.signals[str(sig_id)]["rh1_subsidary"]==True ):
373 signals_common.signals[str(sig_id)]["rh1_subsidary"] = False
374 elif (signals_common.signals[str(sig_id)]["routeset"]==signals_common.route_type.RH2
375 and signals_common.signals[str(sig_id)]["rh2_subsidary"]==True ):
376 signals_common.signals[str(sig_id)]["rh2_subsidary"] = False
377 # Update the signal to show the initial aspect (and send out DCC commands)
378 update_semaphore_subsidary_arms(sig_id)
379 # finally Lock the subsidary if required
380 if loaded_state["sublocked"]: signals_common.lock_subsidary(sig_id)
382 # Publish the initial state to the broker (for other nodes to consume). Note that changes will
383 # only be published if the MQTT interface has been configured for publishing updates for this
384 # signal. This allows publish/subscribe to be configured prior to signal creation
385 signals_common.publish_signal_state(sig_id)
387 return ()
389#-------------------------------------------------------------------
390# Internal Function to update the drawing objects for a specified signal Arm
391# to represent the state of the signal arm (either ON or OFF). If the signal
392# was not created with the specificed signal arm then the state of the signal
393# arm will be "None" and the function will have no effect. All changes in the
394# signal arm state are logged (together with the additional log message passed
395# into the function as to WHY the signal Arm is being changed to its new state
396#------------------------------------------------------------------
398def update_signal_arm (sig_id, signal_arm, off_element, on_element, set_to_clear, log_message = ""):
399 # We explicitly test for True or False as "None" signifies the signal arm does not exist
400 if set_to_clear and signals_common.signals[str(sig_id)][signal_arm]==False:
401 logging.info ("Signal "+str(sig_id)+": Changing \'"+signal_arm+"\' arm to OFF"+log_message)
402 signals_common.signals[str(sig_id)]["canvas"].itemconfigure(signals_common.signals[str(sig_id)][off_element],state='normal')
403 signals_common.signals[str(sig_id)]["canvas"].itemconfigure(signals_common.signals[str(sig_id)][on_element],state='hidden')
404 dcc_control.update_dcc_signal_element(sig_id,True,element=signal_arm)
405 signals_common.signals[str(sig_id)][signal_arm]=True
406 elif not set_to_clear and signals_common.signals[str(sig_id)][signal_arm]==True:
407 logging.info ("Signal "+str(sig_id)+": Changing \'"+ signal_arm +"\' arm to ON"+log_message)
408 signals_common.signals[str(sig_id)]["canvas"].itemconfigure(signals_common.signals[str(sig_id)][off_element],state='hidden')
409 signals_common.signals[str(sig_id)]["canvas"].itemconfigure(signals_common.signals[str(sig_id)][on_element],state='normal')
410 dcc_control.update_dcc_signal_element(sig_id,False,element=signal_arm)
411 signals_common.signals[str(sig_id)][signal_arm]=False
412 return()
414#-------------------------------------------------------------------
415# Helper function to determine if the signal has any diverging route arms
416#-------------------------------------------------------------------
418def has_diverging_route_arms(sig_id:int):
419 has_route_arms = (signals_common.signals[str(sig_id)]["lh1_subsidary"] is not None or
420 signals_common.signals[str(sig_id)]["lh2_subsidary"] is not None or
421 signals_common.signals[str(sig_id)]["rh1_subsidary"] is not None or
422 signals_common.signals[str(sig_id)]["rh1_subsidary"] is not None or
423 signals_common.signals[str(sig_id)]["lh1_signal"] is not None or
424 signals_common.signals[str(sig_id)]["lh2_signal"] is not None or
425 signals_common.signals[str(sig_id)]["rh1_signal"] is not None or
426 signals_common.signals[str(sig_id)]["rh2_signal"] is not None )
427 return (has_route_arms)
429#-------------------------------------------------------------------
430# Internal Function to update each of the subsidary signal arms supported by
431# a signal to reflect the current state of the subsidary (either ON or OFF)
432# and the route set for the signal (i.e the actual subsidary arm that is changed
433# will depend on the route that the particular subsidary arm is controlling
434# Calls the Update_Signal_Arm function to update the state of each arm
435#------------------------------------------------------------------
437def update_semaphore_subsidary_arms (sig_id:int, log_message:str=""):
438 # We explicitly test for True and False as a state of 'None' signifies the signal was created without a subsidary
439 if signals_common.signals[str(sig_id)]["subclear"] == True:
440 # If the route has been set to signals_common.route_type.NONE then we assume MAIN and change the MAIN arm
441 # We also change the MAIN subsidary arm for Home signals without any diverging route arms (main signal or
442 # subsidary signal) to cover the case of a single subsidary signal arm controlling multiple routes
443 if ( signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.MAIN or
444 signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.NONE or
445 not has_diverging_route_arms(sig_id)):
446 update_signal_arm (sig_id, "main_subsidary", "mainsuboff", "mainsubon", True, log_message)
447 update_signal_arm (sig_id, "lh1_subsidary", "lh1suboff", "lh1subon", False, log_message)
448 update_signal_arm (sig_id, "lh2_subsidary", "lh2suboff", "lh2subon", False, log_message)
449 update_signal_arm (sig_id, "rh1_subsidary", "rh1suboff", "rh1subon", False, log_message)
450 update_signal_arm (sig_id, "rh2_subsidary", "rh2suboff", "rh2subon", False, log_message)
451 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.LH1:
452 if signals_common.signals[str(sig_id)]["lh1_subsidary"] is None: 452 ↛ 453line 452 didn't jump to line 453, because the condition on line 452 was never true
453 logging.info ("Signal "+str(sig_id)+": No subsidary arm exists for route LH1")
454 update_signal_arm (sig_id, "main_subsidary", "mainsuboff", "mainsubon", False, log_message)
455 update_signal_arm (sig_id, "lh1_subsidary", "lh1suboff", "lh1subon", True, log_message)
456 update_signal_arm (sig_id, "lh2_subsidary", "lh2suboff", "lh2subon", False, log_message)
457 update_signal_arm (sig_id, "rh1_subsidary", "rh1suboff", "rh1subon", False, log_message)
458 update_signal_arm (sig_id, "rh2_subsidary", "rh2suboff", "rh2subon", False, log_message)
459 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.LH2:
460 if signals_common.signals[str(sig_id)]["lh2_subsidary"] is None: 460 ↛ 461line 460 didn't jump to line 461, because the condition on line 460 was never true
461 logging.info ("Signal "+str(sig_id)+": No subsidary arm exists for route LH2")
462 update_signal_arm (sig_id, "main_subsidary", "mainsuboff", "mainsubon", False, log_message)
463 update_signal_arm (sig_id, "lh1_subsidary", "lh1suboff", "lh1subon", False, log_message)
464 update_signal_arm (sig_id, "lh2_subsidary", "lh2suboff", "lh2subon", True, log_message)
465 update_signal_arm (sig_id, "rh1_subsidary", "rh1suboff", "rh1subon", False, log_message)
466 update_signal_arm (sig_id, "rh2_subsidary", "rh2suboff", "rh2subon", False, log_message)
467 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.RH1:
468 if signals_common.signals[str(sig_id)]["rh1_subsidary"] is None: 468 ↛ 469line 468 didn't jump to line 469, because the condition on line 468 was never true
469 logging.info ("Signal "+str(sig_id)+": No subsidary arm exists for route RH1")
470 update_signal_arm (sig_id, "main_subsidary", "mainsuboff", "mainsubon", False, log_message)
471 update_signal_arm (sig_id, "lh1_subsidary", "lh1suboff", "lh1subon", False, log_message)
472 update_signal_arm (sig_id, "lh2_subsidary", "lh2suboff", "lh2subon", False, log_message)
473 update_signal_arm (sig_id, "rh1_subsidary", "rh1suboff", "rh1subon", True, log_message)
474 update_signal_arm (sig_id, "rh2_subsidary", "rh2suboff", "rh2subon", False, log_message)
475 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.RH2: 475 ↛ 490line 475 didn't jump to line 490, because the condition on line 475 was never false
476 if signals_common.signals[str(sig_id)]["rh2_subsidary"] is None: 476 ↛ 477line 476 didn't jump to line 477, because the condition on line 476 was never true
477 logging.info ("Signal "+str(sig_id)+": No subsidary arm exists for route RH2")
478 update_signal_arm (sig_id, "main_subsidary", "mainsuboff", "mainsubon", False, log_message)
479 update_signal_arm (sig_id, "lh1_subsidary", "lh1suboff", "lh1subon", False, log_message)
480 update_signal_arm (sig_id, "lh2_subsidary", "lh2suboff", "lh2subon", False, log_message)
481 update_signal_arm (sig_id, "rh1_subsidary", "rh1suboff", "rh1subon", False, log_message)
482 update_signal_arm (sig_id, "rh2_subsidary", "rh2suboff", "rh2subon", True, log_message)
483 elif signals_common.signals[str(sig_id)]["subclear"] == False: 483 ↛ 490line 483 didn't jump to line 490, because the condition on line 483 was never false
484 # The subsidary signal is at danger
485 update_signal_arm (sig_id, "main_subsidary", "mainsuboff", "mainsubon", False, log_message)
486 update_signal_arm (sig_id, "lh1_subsidary", "lh1suboff", "lh1subon", False, log_message)
487 update_signal_arm (sig_id, "lh2_subsidary", "lh2suboff", "lh2subon", False, log_message)
488 update_signal_arm (sig_id, "rh1_subsidary", "rh1suboff", "rh1subon", False, log_message)
489 update_signal_arm (sig_id, "rh2_subsidary", "rh2suboff", "rh2subon", False, log_message)
490 return ()
492# -------------------------------------------------------------------------
493# Internal Function to update each of the Main signal arms supported by
494# a signal to reflect the current state of the main signal (either ON or OFF)
495# and the route set for the signal (i.e the actual signal arm that is changed
496# will depend on the route that the particular signal arm is controlling
497# Calls the Update_Signal_Arm function to update the state of each arm
498# -------------------------------------------------------------------------
500def update_main_signal_arms(sig_id:int, log_message:str=""):
501 # When Home/Distant signal is set to PROCEED - the main signal arms will reflect the route
502 # Also the case of a home signal associated with a distant signal (i.e on the same post). In
503 # this case if the home signal is at DANGER and the distant signal is at CAUTION then the state
504 # of the Home signal will be set to caution - in this case we need to set the home arms to OFF
505 if (signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.PROCEED or
506 (signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.CAUTION and
507 signals_common.signals[str(sig_id)]["subtype"] == semaphore_sub_type.home) ):
508 # If the route has been set to signals_common.route_type.NONE then we assume MAIN and change the MAIN arm
509 # We also change the MAIN signal arm for (1) Home signals without any diverging route arms (main signal or
510 # subsidary signal) to cover the case of a single subsidary signal arm controlling multiple routes, and
511 # (2) Associated Distant signals where the associated home signal has no diverging route arms
512 if ( signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.MAIN or
513 signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.NONE or
514 (not has_diverging_route_arms(sig_id) and not (signals_common.signals[str(sig_id)]["associatedsignal"] > 0
515 and has_diverging_route_arms(signals_common.signals[str(sig_id)]["associatedsignal"])))):
516 update_signal_arm (sig_id, "main_signal", "mainsigoff", "mainsigon", True, log_message)
517 update_signal_arm (sig_id, "lh1_signal", "lh1sigoff", "lh1sigon", False, log_message)
518 update_signal_arm (sig_id, "lh2_signal", "lh2sigoff", "lh2sigon", False, log_message)
519 update_signal_arm (sig_id, "rh1_signal", "rh1sigoff", "rh1sigon", False, log_message)
520 update_signal_arm (sig_id, "rh2_signal", "rh2sigoff", "rh2sigon", False, log_message)
521 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.LH1:
522 if signals_common.signals[str(sig_id)]["lh1_signal"] is None: 522 ↛ 523line 522 didn't jump to line 523, because the condition on line 522 was never true
523 logging.info ("Signal "+str(sig_id)+": No main signal arm exists for route LH1")
524 update_signal_arm (sig_id, "main_signal", "mainsigoff", "mainsigon", False, log_message)
525 update_signal_arm (sig_id, "lh1_signal", "lh1sigoff", "lh1sigon", True, log_message)
526 update_signal_arm (sig_id, "lh2_signal", "lh2sigoff", "lh2sigon", False, log_message)
527 update_signal_arm (sig_id, "rh1_signal", "rh1sigoff", "rh1sigon", False, log_message)
528 update_signal_arm (sig_id, "rh2_signal", "rh2sigoff", "rh2sigon", False, log_message)
529 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.LH2:
530 if signals_common.signals[str(sig_id)]["lh2_signal"] is None: 530 ↛ 531line 530 didn't jump to line 531, because the condition on line 530 was never true
531 logging.info ("Signal "+str(sig_id)+": No main signal arm exists for route LH2")
532 update_signal_arm (sig_id, "main_signal", "mainsigoff", "mainsigon", False, log_message)
533 update_signal_arm (sig_id, "lh1_signal", "lh1sigoff", "lh1sigon", False, log_message)
534 update_signal_arm (sig_id, "lh2_signal", "lh2sigoff", "lh2sigon", True, log_message)
535 update_signal_arm (sig_id, "rh1_signal", "rh1sigoff", "rh1sigon", False, log_message)
536 update_signal_arm (sig_id, "rh2_signal", "rh2sigoff", "rh2sigon", False, log_message)
537 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.RH1:
538 if signals_common.signals[str(sig_id)]["rh1_signal"] is None: 538 ↛ 539line 538 didn't jump to line 539, because the condition on line 538 was never true
539 logging.info ("Signal "+str(sig_id)+": No main signal arm exists for route RH1")
540 update_signal_arm (sig_id, "main_signal", "mainsigoff", "mainsigon", False, log_message)
541 update_signal_arm (sig_id, "lh1_signal", "lh1sigoff", "lh1sigon", False, log_message)
542 update_signal_arm (sig_id, "lh2_signal", "lh2sigoff", "lh2sigon", False, log_message)
543 update_signal_arm (sig_id, "rh1_signal", "rh1sigoff", "rh1sigon", True, log_message)
544 update_signal_arm (sig_id, "rh2_signal", "rh2sigoff", "rh2sigon", False, log_message)
545 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.RH2: 545 ↛ 561line 545 didn't jump to line 561, because the condition on line 545 was never false
546 if signals_common.signals[str(sig_id)]["rh2_signal"] is None: 546 ↛ 547line 546 didn't jump to line 547, because the condition on line 546 was never true
547 logging.info ("Signal "+str(sig_id)+": No main signal arm exists for route RH2")
548 update_signal_arm (sig_id, "main_signal", "mainsigoff", "mainsigon", False, log_message)
549 update_signal_arm (sig_id, "lh1_signal", "lh1sigoff", "lh1sigon", False, log_message)
550 update_signal_arm (sig_id, "lh2_signal", "lh2sigoff", "lh2sigon", False, log_message)
551 update_signal_arm (sig_id, "rh1_signal", "rh1sigoff", "rh1sigon", False, log_message)
552 update_signal_arm (sig_id, "rh2_signal", "rh2sigoff", "rh2sigon", True, log_message)
553 else:
554 # Its either a Home signal at DANGER or a Distant Signal at CAUTION
555 # In either case - all the main signal arms should be set to ON
556 update_signal_arm (sig_id, "main_signal", "mainsigoff", "mainsigon", False, log_message)
557 update_signal_arm (sig_id, "lh1_signal", "lh1sigoff", "lh1sigon", False, log_message)
558 update_signal_arm (sig_id, "lh2_signal", "lh2sigoff", "lh2sigon", False, log_message)
559 update_signal_arm (sig_id, "rh1_signal", "rh1sigoff", "rh1sigon", False, log_message)
560 update_signal_arm (sig_id, "rh2_signal", "rh2sigoff", "rh2sigon", False, log_message)
561 return()
563# -------------------------------------------------------------------------
564# Function to Refresh the displayed signal aspect according the signal state
565# Also takes into account the state of the signal ahead if one is specified
566# to ensure the correct aspect is displayed for 3/4 aspect types and 2 aspect
567# distant signals - e.g. for a 3/4 aspect signal - if the signal ahead is ON
568# and this signal is OFF then we want to change it to YELLOW rather than GREEN
569# This function assumes the Sig_ID has been validated by the calling programme
570# -------------------------------------------------------------------------
572def update_semaphore_signal (sig_id:int, sig_ahead_id:Union[int,str]=None, updating_associated_signal:bool=False):
574 route = signals_common.signals[str(sig_id)]["routeset"]
576 # Get the ID of the associated signal (to make the following code more readable)
577 associated_signal = signals_common.signals[str(sig_id)]["associatedsignal"]
578 # Establish what the signal should be displaying based on the state
579 if signals_common.signals[str(sig_id)]["subtype"] == semaphore_sub_type.distant:
580 if not signals_common.signals[str(sig_id)]["sigclear"]:
581 new_aspect = signals_common.signal_state_type.CAUTION
582 log_message = " (CAUTION) - signal is ON"
583 elif signals_common.signals[str(sig_id)]["override"]:
584 new_aspect = signals_common.signal_state_type.CAUTION
585 log_message = " (CAUTION) - signal is OVERRIDDEN"
586 elif signals_common.signals[str(sig_id)]["overcaution"]:
587 new_aspect = signals_common.signal_state_type.CAUTION
588 log_message = " (CAUTION) - signal is OVERRIDDEN to CAUTION"
589 elif signals_common.signals[str(sig_id)]["timedsequence"][route.value].sequence_in_progress: 589 ↛ 590line 589 didn't jump to line 590, because the condition on line 589 was never true
590 new_aspect = signals_common.signal_state_type.CAUTION
591 log_message = " (CAUTION) - signal is on a timed sequence"
592 elif associated_signal > 0 and signals_common.signals[str(associated_signal)]["sigstate"] == signals_common.signal_state_type.DANGER:
593 new_aspect = signals_common.signal_state_type.CAUTION
594 log_message = (" (CAUTION) - signal is OFF but slotted with home signal "+str(associated_signal)+" at DANGER")
595 elif sig_ahead_id is not None and signals_common.signals[str(sig_ahead_id)]["sigstate"] == signals_common.signal_state_type.DANGER: 595 ↛ 596line 595 didn't jump to line 596, because the condition on line 595 was never true
596 new_aspect = signals_common.signal_state_type.CAUTION
597 log_message = (" (CAUTION) - distant signal is OFF but signal ahead "+str(sig_ahead_id)+" is at DANGER")
598 else:
599 new_aspect = signals_common.signal_state_type.PROCEED
600 log_message = (" (PROCEED) - signal is OFF - route is set to " +
601 str(signals_common.signals[str(sig_id)]["routeset"]).rpartition('.')[-1] +")")
602 else:
603 if not signals_common.signals[str(sig_id)]["sigclear"]:
604 new_aspect = signals_common.signal_state_type.DANGER
605 log_message = " (DANGER) - signal is ON"
606 elif signals_common.signals[str(sig_id)]["override"]:
607 new_aspect = signals_common.signal_state_type.DANGER
608 log_message = " (DANGER) - signal is OVERRIDDEN"
609 elif signals_common.signals[str(sig_id)]["timedsequence"][route.value].sequence_in_progress:
610 new_aspect = signals_common.signal_state_type.DANGER
611 log_message = " (DANGER) - signal is on a timed sequence"
612 elif signals_common.signals[str(sig_id)]["releaseonred"]:
613 new_aspect = signals_common.signal_state_type.DANGER
614 log_message = " (DANGER) - signal is subject to \'release on red\' approach control"
615 elif associated_signal > 0 and signals_common.signals[str(associated_signal)]["sigstate"] == signals_common.signal_state_type.CAUTION:
616 new_aspect = signals_common.signal_state_type.CAUTION
617 log_message = (" (CAUTION) - signal is OFF but associated distant "+str(associated_signal)+" is at CAUTION")
618 else:
619 new_aspect = signals_common.signal_state_type.PROCEED
620 log_message = (" (PROCEED) - signal is OFF - route is set to " +
621 str(signals_common.signals[str(sig_id)]["routeset"]).rpartition('.')[-1] +")")
623 current_aspect = signals_common.signals[str(sig_id)]["sigstate"]
625 # Now refresh the displayed aspect (passing in the log message to be displayed) if the aspect has changed
626 if new_aspect != current_aspect:
627 signals_common.signals[str(sig_id)]["sigstate"] = new_aspect
628 update_main_signal_arms (sig_id,log_message)
629 # If this signal is an associated with another signal then we also need to refresh the other signal
630 # Associated distant signals need to be updated as they are "slotted" with the home signal - i.e. if the
631 # home signal is set to DANGER then the distant signal (on the same arm) should show CAUTION
632 # Associated Home signals need to be updated as the internal state of home signals relies on the state of
633 # the distant signal and the home signal (i.e. Home is OFF but distant is ON - State is therefore CAUTION
634 # We set a flag for the recursive call so we don't end up in a circular recursion
635 if associated_signal > 0 and not updating_associated_signal:
636 if signals_common.signals[str(associated_signal)]["refresh"]: 636 ↛ 640line 636 didn't jump to line 640, because the condition on line 636 was never false
637 update_semaphore_signal(associated_signal,updating_associated_signal=True)
638 # Call the common function to update the theatre route indicator elements
639 # (if the signal has a theatre route indicator - otherwise no effect)
640 signals_common.enable_disable_theatre_route_indication(sig_id)
641 # Publish the signal changes to the broker (for other nodes to consume). Note that state changes will only
642 # be published if the MQTT interface has been successfully configured for publishing updates for this signal
643 signals_common.publish_signal_state(sig_id)
644 # For associated distant signals we need to take into account the state of both the home and distant signals
645 # to set the correct "state" for the associated home/distant signal - and we will only know this state once
646 # both home and distant signals have been updated (to take into account any "slotting")
647 if ( signals_common.signals[str(sig_id)]["subtype"] == semaphore_sub_type.home and associated_signal > 0 and
648 signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.CAUTION and
649 signals_common.signals[str(associated_signal)]["sigstate"] == signals_common.signal_state_type.PROCEED ):
650 logging.info ("Signal "+str(sig_id)+": Updating signal state to PROCEED - associated (slotted) distant is now OFF")
651 signals_common.signals[str(sig_id)]["sigstate"] = signals_common.signal_state_type.PROCEED
652 return()
654# -------------------------------------------------------------------------
655# Function to set (and update) the route indication for the signal
656# Calls the internal functions to update the route feathers and the
657# theatre route indication. This Function assumes the Sig_ID has
658# already been validated by the calling programme
659# -------------------------------------------------------------------------
661def update_semaphore_route_indication (sig_id,route_to_set = None):
662 # Only update the respective route indication if the route has been changed and has actively
663 # been set (a route of 'NONE' signifies that the particular route indication isn't used)
664 if route_to_set is not None and signals_common.signals[str(sig_id)]["routeset"] != route_to_set:
665 logging.info ("Signal "+str(sig_id)+": Setting semaphore route to "+str(route_to_set).rpartition('.')[-1])
666 signals_common.signals[str(sig_id)]["routeset"] = route_to_set
667 # Refresh the signal drawing objects (which will also send the DCC commands to change the arms accordingly)
668 # Log messages will also be generated for each change - so we don't need lo log anything extra here
669 update_main_signal_arms(sig_id," (route has been changed to "+str(route_to_set).rpartition('.')[-1]+")")
670 # Also update the subsidary aspects for route changes (as these may be represented by different subsidary arms)
671 update_semaphore_subsidary_arms(sig_id," (route has been changed to "+str(route_to_set).rpartition('.')[-1]+")")
672 # If this is a home signal with an associated distant signal then we also need to set the route for
673 # the distant signal as it is effectively on the same post and "slotted" with the home signal
674 # Get the ID of the associated signal (to make the following code more readable)
675 associated_signal = signals_common.signals[str(sig_id)]["associatedsignal"]
676 if signals_common.signals[str(sig_id)]["subtype"] == semaphore_sub_type.home and associated_signal > 0:
677 update_semaphore_route_indication (associated_signal,route_to_set)
678 # Refresh the signal aspect (a catch-all to ensure the signal displays the correct aspect
679 # in case the signal is in the middle of a timed sequence for the old route or the new route
680 if signals_common.signals[str(sig_id)]["refresh"]: update_semaphore_signal(sig_id)
681 return()
684# -------------------------------------------------------------------------
685# Class for a timed signal sequence. A class instance is created for each
686# route for each signal. When a timed signal is triggered the existing
687# instance is first aborted. A new instance is then created/started
688# -------------------------------------------------------------------------
690class timed_sequence():
691 def __init__(self, sig_id:int, route, start_delay:int=0, time_delay:int=0):
692 self.sig_id = sig_id
693 self.sig_route = route
694 self.start_delay = start_delay
695 self.time_delay = time_delay
696 self.sequence_abort_flag = False
697 self.sequence_in_progress = False
699 def abort(self):
700 self.sequence_abort_flag = True
702 def start(self):
703 if self.sequence_abort_flag or not signals_common.sig_exists(self.sig_id): 703 ↛ 704line 703 didn't jump to line 704, because the condition on line 703 was never true
704 self.sequence_in_progress = False
705 else:
706 self.sequence_in_progress = True
707 # For a start delay of zero we assume the intention is not to make a callback (on the basis
708 # that the user has triggered the timed signal in the first place). For start delays > 0 the
709 # sequence is initiated after the specified delay and this will trigger a callback
710 # Note that we only change the aspect and generate the callback if the same route is set
711 if signals_common.signals[str(self.sig_id)]["routeset"] == self.sig_route: 711 ↛ 722line 711 didn't jump to line 722, because the condition on line 711 was never false
712 if self.start_delay > 0: 712 ↛ 713line 712 didn't jump to line 713, because the condition on line 712 was never true
713 logging.info("Signal "+str(self.sig_id)+": Timed Signal - Signal Passed Event **************************")
714 update_semaphore_signal(self.sig_id)
715 # Publish the signal passed event via the mqtt interface. Note that the event will only be published if the
716 # mqtt interface has been successfully configured and the signal has been set to publish passed events
717 signals_common.publish_signal_passed_event(self.sig_id)
718 signals_common.signals[str(self.sig_id)]["extcallback"] (self.sig_id,signals_common.sig_callback_type.sig_passed)
719 else:
720 update_semaphore_signal(self.sig_id)
721 # We need to schedule the sequence completion (i.e. back to clear
722 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_end())
724 def timed_signal_sequence_end(self):
725 # We've finished - Set the signal back to its "normal" condition
726 self.sequence_in_progress = False
727 if signals_common.sig_exists(self.sig_id): 727 ↛ exitline 727 didn't return from function 'timed_signal_sequence_end', because the condition on line 727 was never false
728 logging.info("Signal "+str(self.sig_id)+": Timed Signal - Signal Updated Event *************************")
729 update_semaphore_signal(self.sig_id)
730 signals_common.signals[str(self.sig_id)]["extcallback"] (self.sig_id, signals_common.sig_callback_type.sig_updated)
732# -------------------------------------------------------------------------
733# Function to initiate a timed signal sequence - setting the signal initially to ON
734# and then returning to OFF (assuming the signal is clear and nor overridden) after
735# the specified time delay. Intended for automation of 'exit' signals on a layout.
736# The start_delay is the initial delay (in seconds) before the signal is set to ON
737# and the time_delay is the delay before the signal returns to OFF. A 'sig_passed'
738# callback event will be generated when the signal is set to ON if if a start delay
739# is specified. When returning to OFF, a 'sig_updated' callback event will be generated.
740# -------------------------------------------------------------------------
742def trigger_timed_semaphore_signal (sig_id:int,start_delay:int=0,time_delay:int=5):
744 def delayed_sequence_start(sig_id:int, sig_route):
745 if signals_common.sig_exists(sig_id):
746 signals_common.signals[str(sig_id)]["timedsequence"][route.value].start()
748 # Don't initiate a timed signal sequence if a shutdown has already been initiated
749 if common.shutdown_initiated: 749 ↛ 750line 749 didn't jump to line 750, because the condition on line 749 was never true
750 logging.warning("Signal "+str(sig_id)+": Timed Signal - Shutdown initiated - not triggering timed signal")
751 else:
752 # Abort any timed signal sequences already in progess
753 route = signals_common.signals[str(sig_id)]["routeset"]
754 signals_common.signals[str(sig_id)]["timedsequence"][route.value].abort()
755 # Create a new instnce of the time signal class - this should have the effect of "destroying"
756 # the old instance when it goes out of scope, leaving us with the newly created instance
757 signals_common.signals[str(sig_id)]["timedsequence"][route.value] = timed_sequence(sig_id, route, start_delay, time_delay)
758 # Schedule the start of the sequence (i.e. signal to danger) if the start delay is greater than zero
759 # Otherwise initiate the sequence straight away (so the signal state is updated immediately)
760 if start_delay > 0: 760 ↛ 761line 760 didn't jump to line 761, because the condition on line 760 was never true
761 common.root_window.after(start_delay*1000,lambda:delayed_sequence_start(sig_id,route))
762 else:
763 signals_common.signals[str(sig_id)]["timedsequence"][route.value].start()
764 return()
766###############################################################################