Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/editor/run_layout.py: 98%
511 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 contains all the functions to "run" the layout
3#
4# External API functions intended for use by other editor modules:
5# initialise(canvas) - sets a global reference to the tkinter canvas object
6# initialise_layout() - call after object changes/deletions or load of a new schematic
7# schematic_callback(item_id,callback_type) - the callback for all library objects
8# enable_editing() - Call when 'Edit' Mode is selected (from Schematic Module)
9# disable_editing() - Call when 'Run' Mode is selected (from Schematic Module)
10# configure_automation(auto_enabled) - Call to set automation mode (from Editor Module)
11# configure_spad_popups(spad_enabled) - Call to set SPAD popup warnings (from Editor Module)
12#
13# Makes the following external API calls to other editor modules:
14# objects.signal(signal_id) - To get the object_id for a given signal_id
15# objects.point(point_id) - To get the object_id for a given point_id
16# objects.section(section_id) - To get the object_id for a given section_id
17# objects.track_sensor(sensor_id) - To get the object_id for a given sensor_id
18#
19# Accesses the following external editor objects directly:
20# objects.schematic_objects - the dict holding descriptions for all objects
21# objects.object_type - used to establish the type of the schematic objects
22# objects.signal_index - To iterate through all the signal objects
23# objects.point_index - To iterate through all the point objects
24# objects.section_index - To iterate through all the section objects
25#
26# Accesses the following external library objects directly:
27# signals_common.route_type - for accessing the enum value
28# signals_common.sig_type - for accessing the enum value
29# signals_common.signal_state_type - for accessing the enum value
30# signals_common.sig_callback_type - for accessing the enum value
31# points.point_callback_type - for accessing the enum value
32# track_sections.section_callback_type - for accessing the enum value
33# block_instruments.block_callback_type - for accessing the enum value
34# signals_colour_lights.signal_sub_type - for accessing the enum value
35# signals_semaphores.semaphore_sub_type - for accessing the enum value
36# track_sensors.track_sensor_callback_type - for accessing the enum value
37#
38# Makes the following external API calls to library modules:
39# signals.signal_state(sig_id) - For testing the current displayed aspect
40# signals.update_signal(sig_id, sig_ahead_id) - To update the signal aspect
41# signals.signal_clear(sig_id, sig_route) - To test if a signal is clear
42# signals.subsidary_clear(sig_id) - to test if a subsidary is clear
43# signals.lock_signal(sig_id) - To lock a signal
44# signals.unlock_signal(sig_id) - To unlock a signal
45# signals.lock_subsidary(sig_id) - To lock a subsidary signal
46# signals.unlock_subsidary(sig_id) - To unlock a subsidary signal
47# signals.set_approach_control - Enable approach control mode for the signal
48# signals.clear_approach_control - Clear approach control mode for the signal
49# signals.set_route(sig_id, sig_route, theatre) - To set the route for the signal
50# signals.trigger_timed_signal(sig_id, T1, T2) - Trigger timed signal sequence
51# signals.set_signal_override - Override the signal to DANGER
52# signals.clear_signal_override - Clear the Signal override DANGER mode
53# signals.set_signal_override_caution - Override the signal to CAUTION
54# signals.clear_signal_override_caution - Clear the Signal override CAUTION mode
55# points.fpl_active(point_id) - Test if the FPL is active (for interlocking)
56# points.point_switched(point_id) - Test if the point is switched (for interlocking)
57# points.lock_point(point_id) - Lock a point (for interlocking)
58# points.unlock_point(point_id) - Unlock a point (for interlocking)
59# block_instruments.block_section_ahead_clear(inst_id) - Get the state (for interlocking)
60# track_sections.set_section_occupied (section_id) - Set Track Section to "Occupied"
61# track_sections.clear_section_occupied (section_id) - Set Track Section to "Clear"
62# track_sections.section_occupied (section_id) - To test if a section is occupied
63# track_sections.section_label - get the current label for an occupied section
64#------------------------------------------------------------------------------------
66import logging
67import tkinter as Tk
68from typing import Union
70from ..library import signals
71from ..library import points
72from ..library import block_instruments
73from ..library import signals_common
74from ..library import signals_semaphores
75from ..library import signals_colour_lights
76from ..library import track_sections
77from ..library import track_sensors
79from . import objects
81#------------------------------------------------------------------------------------
82# The Tkinter Canvas Object is saved as a global variable for easy referencing
83# The editing_enabled and run_mode flags control the behavior of run_layout
84#------------------------------------------------------------------------------------
86canvas = None
87run_mode = None
88automation_enabled = None
89spad_popups = False
90enhanced_debugging = False # Switch this on to enable 'info'
92#------------------------------------------------------------------------------------
93# The set_canvas function is called at application startup (on canvas creation)
94#------------------------------------------------------------------------------------
96def initialise(canvas_object):
97 global canvas
98 canvas = canvas_object
99 return()
101#------------------------------------------------------------------------------------
102# The behavior of the layout processing will change depending on what mode we are in
103#------------------------------------------------------------------------------------
105def enable_editing():
106 global run_mode
107 run_mode = False
108 initialise_layout()
109 return()
111def disable_editing():
112 global run_mode
113 run_mode = True
114 initialise_layout()
115 return()
117def configure_automation(automation:bool):
118 global automation_enabled
119 automation_enabled = automation
120 initialise_layout()
121 return()
123def configure_spad_popups(popups:bool):
124 global spad_popups
125 spad_popups = popups
126 return()
128#------------------------------------------------------------------------------------
129# Internal helper Function to find if an ID is a local (int) or remote (str) Item ID
130#------------------------------------------------------------------------------------
132def is_local_id(item_id:Union[int,str]):
133 return( isinstance(item_id, int) or (isinstance(item_id, str) and item_id.isdigit()) )
135#------------------------------------------------------------------------------------
136# Internal helper Function to find if a signal has a subsidary
137# Note the function should only be called for local signals (sig ID is an integer)
138#------------------------------------------------------------------------------------
140def has_subsidary(int_signal_id:int):
141 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
142 return (signal_object["subsidary"][0] or
143 signal_object["sigarms"][0][1][0] or
144 signal_object["sigarms"][1][1][0] or
145 signal_object["sigarms"][2][1][0] or
146 signal_object["sigarms"][3][1][0] or
147 signal_object["sigarms"][4][1][0] )
149#------------------------------------------------------------------------------------
150# Internal helper Function to find if a signal has a distant arms
151# Note the function should only be called for local signals (sig ID is an integer)
152#------------------------------------------------------------------------------------
154def has_distant_arms(int_signal_id:int):
155 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
156 return (signal_object["sigarms"][0][2][0] or
157 signal_object["sigarms"][1][2][0] or
158 signal_object["sigarms"][2][2][0] or
159 signal_object["sigarms"][3][2][0] or
160 signal_object["sigarms"][4][2][0] )
162#------------------------------------------------------------------------------------
163# Internal helper Function to find if a signal is a home signal (semaphore or colour light)
164# Note the function should only be called for local signals (sig ID is an integer)
165#------------------------------------------------------------------------------------
167def is_home_signal(int_signal_id:int):
168 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
169 return ( ( signal_object["itemtype"] == signals_common.sig_type.colour_light.value and
170 signal_object["itemsubtype"] == signals_colour_lights.signal_sub_type.home.value ) or
171 ( signal_object["itemtype"] == signals_common.sig_type.semaphore.value and
172 signal_object["itemsubtype"] == signals_semaphores.semaphore_sub_type.home.value) )
174#------------------------------------------------------------------------------------
175# Internal helper Function to find if a signal is a distant signal (semaphore or colour light)
176# Note the function should only be called for local signals (sig ID is an integer)
177#------------------------------------------------------------------------------------
179def is_distant_signal(int_signal_id:int):
180 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
181 return( ( signal_object["itemtype"] == signals_common.sig_type.colour_light.value and
182 signal_object["itemsubtype"] == signals_colour_lights.signal_sub_type.distant.value ) or
183 ( signal_object["itemtype"] == signals_common.sig_type.semaphore.value and
184 signal_object["itemsubtype"] == signals_colour_lights.signal_sub_type.distant.value ) )
186#------------------------------------------------------------------------------------
187# Common Function to find the first valid route (all points set correctly) for a Signal or Track Sensor
188# The 'locked' flag is also returned to signify whether all facing point locks or active. This allows
189# most functions to use just the returned route - the interlocking functions care about the FPLs.
190# For both signals and track sensors, a route table comprises a list of routes: [MAIN, LH1, LH2, RH1, RH2]
191# For a signal, each route entry comprises: [[p1, p2, p3, p4, p5, p6, p7] signal_id, block_inst_id]
192# For a Track Sensor, each route entry comprises: [[p1, p2, p3, p4, p5, p6, p7] section_id]
193# Each route comprises: [[p1, p2, p3, p4, p5, p6, p7] signal, block_inst]
194# Each point element comprises [point_id, point_state]
195#------------------------------------------------------------------------------------
197def find_route(object_id, dict_key:str):
198 route_to_return = None
199 # Iterate through each route in the specified table
200 for index, route_entry in enumerate(objects.schematic_objects[object_id][dict_key]):
201 route_has_points, valid_route, points_locked = False, True, True
202 # Iterate through the points to see if they are set and locked for the route
203 for point_entry in route_entry[0]:
204 if point_entry[0] > 0:
205 route_has_points = True
206 if not points.point_switched(point_entry[0]) == point_entry[1]:
207 valid_route = False
208 if not points.fpl_active(point_entry[0]):
209 points_locked = False
210 if not valid_route: break
211 # Valid route if all points on the route are set and locked correctly
212 # Or if the route is MAIN and no points have been specified for the route
213 if (index == 0 and not route_has_points) or (route_has_points and valid_route):
214 route_to_return = signals_common.route_type(index+1)
215 break
216 return(route_to_return, points_locked)
218#------------------------------------------------------------------------------------
219# The following two functions build on the above. The first function just returns the route
220# and is used by most of the Run Layout functions. The second function only returns the route
221# if all FPLs for the route are active. This is used by the interlocking functions
222#------------------------------------------------------------------------------------
224def find_valid_route(object_id, dict_key:str):
225 route, locked = find_route(object_id, dict_key)
226 return(route)
228def find_locked_route(object_id, dict_key:str):
229 route, locked = find_route(object_id, dict_key)
230 if not locked: route = None
231 return(route)
233#------------------------------------------------------------------------------------
234# Internal common Function to find the 'signal ahead' of a signal object (based on
235# the route that has been set (points correctly set and locked for the route)
236# Note the function should only be called for local signals (sig ID is an integer)
237# but can return either local or remote IDs (int or str) - both returned as a str
238# If no route is set/locked or no sig ahead is specified then 'None' is returned
239#------------------------------------------------------------------------------------
241def find_signal_ahead(int_signal_id:int):
242 str_signal_ahead_id = None
243 signal_route = find_valid_route(objects.signal(int_signal_id),"pointinterlock")
244 if signal_route is not None:
245 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
246 str_signal_ahead_id = signal_object["pointinterlock"][signal_route.value-1][1]
247 if str_signal_ahead_id == "": str_signal_ahead_id = None
248 return(str_signal_ahead_id)
250#------------------------------------------------------------------------------------
251# Internal common Function to find the 'signal behind' a signal object by testing each
252# of the other signal objects in turn to find the route that has been set and then see
253# if the 'signal ahead' on the set route matches the signal passed into the function
254# Note the function should only be called for local signals (sig ID is an integer)
255#------------------------------------------------------------------------------------
257def find_signal_behind(int_signal_id:int):
258 int_signal_behind_id = None
259 for str_signal_id_to_test in objects.signal_index:
260 str_signal_ahead_id = find_signal_ahead(int(str_signal_id_to_test))
261 if str_signal_ahead_id == str(int_signal_id):
262 int_signal_behind_id = int(str_signal_id_to_test)
263 break
264 return(int_signal_behind_id)
266#------------------------------------------------------------------------------------
267# Internal Function to walk the route ahead of a distant signal to see if any
268# signals are at DANGER (will return True as soon as this is the case). The
269# forward search will be aborted as soon as a "non-home" signal type is found
270# (this includes the case where a home semaphore also has secondary distant arms)
271# The forward search will also be aborted if the signal ahead is a remote signal
272# on the assumption that the remote signal is in the next block section and
273# should therefore be the distant signal protecting that block section.
274# A maximum recursion depth provides a level of protection from mis-configuration
275# Note the function should only be called for local signals (sig ID is an integer)
276#------------------------------------------------------------------------------------
278def home_signal_ahead_at_danger(int_signal_id:int, recursion_level:int=0):
279 home_signal_at_danger = False
280 if recursion_level < 20: 280 ↛ 291line 280 didn't jump to line 291, because the condition on line 280 was never false
281 str_signal_ahead_id = find_signal_ahead(int_signal_id)
282 if str_signal_ahead_id is not None and is_local_id(str_signal_ahead_id):
283 int_signal_ahead_id = int(str_signal_ahead_id)
284 if ( is_home_signal(int_signal_ahead_id) and
285 signals.signal_state(int_signal_ahead_id) == signals_common.signal_state_type.DANGER):
286 home_signal_at_danger = True
287 elif is_home_signal(int_signal_ahead_id) and not has_distant_arms(int_signal_ahead_id):
288 # Call the function recursively to find the next signal ahead
289 home_signal_at_danger = home_signal_ahead_at_danger(int_signal_ahead_id, recursion_level+1)
290 else:
291 logging.error("RUN LAYOUT - Interlock with Signal ahead - Maximum recursion level reached")
292 return(home_signal_at_danger)
294#------------------------------------------------------------------------------------
295# Internal Function to test if the signal ahead of the specified signal is a
296# distant signal and if that distant signal is displaying a caution aspect.
297# In the case that the signal ahead is a remote signal we have to assume that
298# the remote signal is in the next block section and should therefore be the
299# distant signal protecting that block section (i.e we don't test the type)
300# Note the function should only be called for local signals (sig ID is an integer)
301#------------------------------------------------------------------------------------
303def distant_signal_ahead_at_caution(int_signal_id:int):
304 distant_signal_at_caution = False
305 str_signal_ahead_id = find_signal_ahead(int_signal_id)
306 if str_signal_ahead_id is not None:
307 if is_local_id(str_signal_ahead_id):
308 int_signal_ahead_id = int(str_signal_ahead_id)
309 if ( is_distant_signal(int_signal_ahead_id) and
310 signals.signal_state(int_signal_ahead_id) == signals_common.signal_state_type.CAUTION ):
311 distant_signal_at_caution = True
312 elif signals.signal_state(str_signal_ahead_id) == signals_common.signal_state_type.CAUTION:
313 distant_signal_at_caution = True
314 return (distant_signal_at_caution)
316#------------------------------------------------------------------------------------
317# Internal function to find any colour light signals which are configured to update aspects
318# based on the aspect of the signal that has changed (i.e. signals "behind"). The function
319# is recursive and keeps working back along the route until there are no further changes
320# that need propagating backwards. A maximum recursion depth provides a level of protection.
321# Note the function should only be called for local signals (sig ID is an integer)
322#------------------------------------------------------------------------------------
324def update_signal_behind(int_signal_id:int, recursion_level:int=0):
325 if recursion_level < 20: 325 ↛ 338line 325 didn't jump to line 338, because the condition on line 325 was never false
326 int_signal_behind_id = find_signal_behind(int_signal_id)
327 if int_signal_behind_id is not None:
328 signal_behind_object = objects.schematic_objects[objects.signal(int_signal_behind_id)]
329 if signal_behind_object["itemtype"] == signals_common.sig_type.colour_light.value:
330 # Fnd the displayed aspect of the signal (before any changes)
331 initial_signal_aspect = signals.signal_state(int_signal_behind_id)
332 # Update the signal behind based on the signal we called into the function with
333 signals.update_signal(int_signal_behind_id, int_signal_id)
334 # If the aspect has changed then we need to continute working backwards
335 if signals.signal_state(int_signal_behind_id) != initial_signal_aspect:
336 update_signal_behind(int_signal_behind_id, recursion_level+1)
337 else:
338 logging.error("RUN LAYOUT - Update Signal Behind - Maximum recursion level reached")
339 return()
341#------------------------------------------------------------------------------------
342# Functions to update a signal aspect based on the signal ahead and then to work back
343# along the set route to update any other signals that need changing. Called on Called
344# on sig_switched or sig_updated events. The Signal that has changed could either be a
345# local signal (sig ID is an integer) or a remote signal (Signal ID is a string)
346# Note the function should only be called for local signals (sig ID is an integer)
347#------------------------------------------------------------------------------------
349def process_aspect_updates(int_signal_id:int):
350 # First update on the signal ahead (only if its a colour light signal)
351 # Other signal types are updated automatically when switched
352 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
353 if signal_object["itemtype"] == signals_common.sig_type.colour_light.value:
354 str_signal_ahead_id = find_signal_ahead(int_signal_id)
355 if str_signal_ahead_id is not None:
356 # The update signal function works with local and remote signal IDs
357 signals.update_signal(int_signal_id, str_signal_ahead_id)
358 else:
359 signals.update_signal(int_signal_id)
360 # Now work back along the route to update signals behind. Note that we do this for
361 # all signal types as there could be colour light signals behind this signal
362 update_signal_behind(int_signal_id)
363 return()
365#------------------------------------------------------------------------------------
366# Function to update the signal route based on the 'interlocking routes' configuration
367# of the signal and the current setting of the points (and FPL) on the schematic
368# Note the function should only be called for local signals (sig ID is an integer)
369#------------------------------------------------------------------------------------
371def set_signal_route(int_signal_id:int):
372 signal_route = find_valid_route(objects.signal(int_signal_id),"pointinterlock")
373 if signal_route is not None:
374 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
375 # Set the Route (and any associated route indication) for the signal
376 # Note that the main route is the second element (the first element is the dark aspect)
377 theatre_text = signal_object["dcctheatre"][signal_route.value][0]
378 signals.set_route(int_signal_id, route=signal_route, theatre_text=theatre_text)
379 # For Semaphore Signals with secondary distant arms we also need
380 # to set the route for the associated semaphore distant signal
381 if has_distant_arms(int_signal_id):
382 int_associated_distant_sig_id = int_signal_id + 100
383 signals.set_route(int_associated_distant_sig_id, route=signal_route)
384 return()
386#------------------------------------------------------------------------------------
387# Function to trigger any timed signal sequences (from the signal 'passed' event)
388# Note the function should only be called for local signals (sig ID is an integer)
389#------------------------------------------------------------------------------------
391def trigger_timed_sequence(int_signal_id:int):
392 signal_route = find_valid_route(objects.signal(int_signal_id),"pointinterlock")
393 if signals.signal_clear(int_signal_id) and signal_route is not None:
394 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
395 # Get the details of the timed signal sequence to initiate
396 # Each route comprises a list of [selected, sig_id,start_delay, time_delay)
397 trigger_signal = signal_object["timedsequences"][signal_route.value-1][0]
398 int_sig_id_to_trigger = signal_object["timedsequences"][signal_route.value-1][1]
399 start_delay = signal_object["timedsequences"][signal_route.value-1][2]
400 time_delay = signal_object["timedsequences"][signal_route.value-1][3]
401 # If the signal to trigger is the same as the current signal then we enforce
402 # a start delay of Zero - otherwise, every time the signal changes to RED
403 # (after the start delay) a "signal passed" event will be generated which
404 # would then trigger another timed signal sequence and so on and so on
405 if int_sig_id_to_trigger == int_signal_id: start_delay = 0
406 # Trigger the timed sequence
407 if trigger_signal and int_sig_id_to_trigger !=0:
408 signals.trigger_timed_signal(int_sig_id_to_trigger, start_delay, time_delay)
409 return()
411#------------------------------------------------------------------------------------
412# Function to SET or CLEAR a signal's approach control state and refresh the displayed
413# aspect. The function then recursively calls itself to work backwards along the route
414# updating the approach control state (and displayed aspect)of preceding signals
415# Note that Approach control won't be set in the period between signal released and
416# signal passed events unless the 'force_set' flag is set
417# Note the function should only be called for local signals (sig ID is an integer)
418#------------------------------------------------------------------------------------
420def update_signal_approach_control(int_signal_id:int, force_set:bool, recursion_level:int=0):
421 if recursion_level < 20: 421 ↛ 447line 421 didn't jump to line 447, because the condition on line 421 was never false
422 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
423 if (signal_object["itemtype"] == signals_common.sig_type.colour_light.value or
424 signal_object["itemtype"] == signals_common.sig_type.semaphore.value):
425 signal_route = find_valid_route(objects.signal(int_signal_id),"pointinterlock")
426 if signal_route is not None:
427 # The "approachcontrol" element is a list of routes [Main, Lh1, Lh2, Rh1, Rh2]
428 # Each element represents the approach control mode that has been set
429 # release_on_red=1, release_on_yel=2, released_on_red_home_ahead=3
430 if signal_object["approachcontrol"][signal_route.value-1] == 1:
431 signals.set_approach_control(int_signal_id, release_on_yellow=False, force_set=force_set)
432 elif signal_object["approachcontrol"][signal_route.value-1] == 2:
433 signals.set_approach_control(int_signal_id, release_on_yellow=True, force_set=force_set)
434 elif (signal_object["approachcontrol"][signal_route.value-1] == 3 and home_signal_ahead_at_danger(int_signal_id) ):
435 signals.set_approach_control(int_signal_id, release_on_yellow=False, force_set=force_set)
436 else:
437 signals.clear_approach_control(int_signal_id)
438 else:
439 signals.clear_approach_control(int_signal_id)
440 # Update the signal aspect and work back along the route to see if any other signals need
441 # approach control to be set/cleared depending on the updated aspect of this signal
442 process_aspect_updates(int_signal_id)
443 int_signal_behind_id = find_signal_behind(int_signal_id)
444 if int_signal_behind_id is not None:
445 update_signal_approach_control(int_signal_behind_id, False, recursion_level+1)
446 else:
447 logging.error("RUN LAYOUT - Update Approach Control on signals ahead - Maximum recursion level reached")
448 return()
450#------------------------------------------------------------------------------------
451# Functions to Update track occupancy (from the signal or Track Sensor 'passed' events)
452#
453# For signals, we ignore secondary 'signal passed' events - This is the case of a train passing
454# a signal (and getting passed from one Track Section to another) and then immediately passing an
455# opposing signal on the route ahead (where we don't want to erroneously pass the train back)
456# To enable this, all train movements (from one track section to the next) are stored in the
457# global list_of_movements and then deleted once a secondary 'signal passed' event occurs
458#------------------------------------------------------------------------------------
460list_of_movements = []
462#------------------------------------------------------------------------------------
463# For both Signals and Track Sensors, we also ignore any events where we can't find a valid route
464# in the signal / Track Sensor configuration to identify the Track Sections either side
465#
466# Common logic that applies to all Signals and Track Sensor Types:
467# - Section AHEAD = OCCUPIED and section BEHIND = CLEAR - Pass train from AHEAD to BEHIND
468# - Section BEHIND = OCCUPIED and section AHEAD = CLEAR - Pass train from BEHIND to AHEAD
469# - (but raise SPAD warning if passing a signal and signal is displaying DANGER)
470# - Section AHEAD = CLEAR - section BEHIND doesn't exist - set section AHEAD to OCCUPIED
471# - (but raise SPAD warning if passing a signal and signal is displaying DANGER)
472# - Section BEHIND = CLEAR - section AHEAD doesn't exist - set section BEHIND to OCCUPIED
473# - Section AHEAD = OCCUPIED - section BEHIND doesn't exist - set section AHEAD to CLEAR
474# - Section BEHIND = OCCUPIED - section AHEAD doesn't exist -set section BEHIND to CLEAR
475# - (but raise SPAD warning if passing a signal and signal is displaying DANGER)
476# - Section AHEAD = CLEAR and section BEHIND = CLEAR - No action (but raise a warning)
477# - Section AHEAD = OCCUPIED and section BEHIND = OCCUPIED
478# - If passing a Signal that is CLEAR - Pass train from BEHIND to AHEAD
479# - Otherwise, no action (no idea) - but raise a warning
480# - Section BEHIND doesn't exist and section AHEAD doesn't exist - No action
481#
482#------------------------------------------------------------------------------------
484def update_track_occupancy(object_id):
485 schematic_object = objects.schematic_objects[object_id]
486 item_type = schematic_object["item"]
487 # The track occupancy logic to apply will depend on the item type (and if a signal, its state)
488 if item_type == objects.object_type.signal:
489 update_track_occupancy_for_signal(object_id)
490 elif item_type == objects.object_type.track_sensor: 490 ↛ 492line 490 didn't jump to line 492, because the condition on line 490 was never false
491 update_track_occupancy_for_track_sensor(object_id)
492 return()
494#------------------------------------------------------------------------------------
495# Signal specific logic for track occupancy updates
496#------------------------------------------------------------------------------------
498def update_track_occupancy_for_signal(object_id):
499 global list_of_movements
500 schematic_object = objects.schematic_objects[object_id]
501 item_id = schematic_object["itemid"]
502 item_text = "Signal "+str(item_id)
503 # Find the section ahead and section behind the signal (0 = No section). If the returned route is
504 # None for a semaphore distant signal then we assume a default route of MAIN. This is to cater for a
505 # train passing the semaphore distant where the route (controlling the distant arms) may not be set
506 # and locked for the home signal ahead - it is still perfectly valid to pass the distant at caution
507 section_behind = schematic_object["tracksections"][0]
508 route = find_valid_route(object_id, "pointinterlock")
509 if route is not None:
510 section_ahead = schematic_object["tracksections"][1][route.value-1][0]
511 elif is_distant_signal(item_id):
512 route = signals_common.route_type.MAIN
513 section_ahead = schematic_object["tracksections"][1][0][0]
514 else:
515 # There is no valid route for the signal so we cannot make any assumptions about the train movement.
516 section_ahead = 0
517 # However, note that the movement may be a possible "secondary event" - e.g. A train passes a signal
518 # protecting a trailing crossover (the primary event) and then the opposing signal controlling a
519 # movement back over the crossover (the secondary event). It may be that the second signal is only
520 # configured for the crossover move (there is no valid signal route back down the main line). In this
521 # case we don't want to raise a warning to the user - so we fail silently if the 'section_behind'
522 # matches a 'section_ahead' in the list of movements.
523 if True in list(element[1] == section_behind for element in list_of_movements): 523 ↛ 527line 523 didn't jump to line 527, because the condition on line 523 was never false
524 logging.debug("RUN LAYOUT: "+item_text+" 'passed' - no valid route ahead of the Signal "+
525 "but ignoring as this is a possible secondary event")
526 else:
527 log_text = item_text+" has been 'passed' but unable to determine train movement as there is no valid route ahead of the Signal"
528 logging.warning("RUN LAYOUT: "+log_text)
529 if spad_popups: Tk.messagebox.showwarning(parent=canvas, title="Occupancy Error", message=log_text)
530 # Establish if this is a primary event or a secondary event (to a previous train movement). This is the
531 # case of a train passing a signal and then immediately passing an opposing signal on the route ahead
532 # The second event should be ignored as we don't want to pass the train back to the previous section.
533 is_secondary_event = False
534 if section_ahead > 0 and section_behind > 0:
535 if [section_ahead, section_behind] in list_of_movements:
536 list_of_movements.remove([section_ahead, section_behind])
537 is_secondary_event = True
538 elif [section_behind, section_ahead] not in list_of_movements:
539 list_of_movements.append([section_behind, section_ahead])
540 # Establish the state of the signal - if the subsidary aspect is clear or the main aspect not showing
541 # DANGER then we can assume any movement from the sectiion_behind to the section_ahead is valid.
542 # Otherwise we may need to raise a Signal Passed at Danger warning later on in the code
543 if ( (signals.signal_state(item_id) != signals_common.signal_state_type.DANGER) or
544 (has_subsidary(item_id) and signals.subsidary_clear(item_id)) ):
545 signal_clear = True
546 else:
547 signal_clear = False
548 if route is not None and not is_secondary_event:
549 process_track_occupancy(section_ahead, section_behind, item_text, signal_clear)
550 return()
552#------------------------------------------------------------------------------------
553# Track Sensor specific logic for track occupancy updates
554#------------------------------------------------------------------------------------
556def update_track_occupancy_for_track_sensor(object_id):
557 schematic_object = objects.schematic_objects[object_id]
558 item_id = schematic_object["itemid"]
559 item_text = "Track Sensor "+str(item_id)
560 # Find the section ahead and section behind the Track Sensor (0 = No section). If either of
561 # the returned routes are None we can't really assume anything so don't process any changes.
562 route_ahead = find_valid_route(object_id, "routeahead")
563 if route_ahead is None:
564 log_text = item_text+" has been 'passed' but unable to determine train movement as there is no valid route ahead of the Track Sensor"
565 logging.warning("RUN LAYOUT: "+log_text)
566 if spad_popups: Tk.messagebox.showwarning(parent=canvas, title="Occupancy Error", message=log_text)
567 else:
568 section_ahead = schematic_object["routeahead"][route_ahead.value-1][1]
569 route_behind = find_valid_route(object_id, "routebehind")
570 if route_behind is None:
571 log_text=item_text+" has been 'passed' but unable to determine train movement as there is no valid route behind of the Track Sensor"
572 logging.warning("RUN LAYOUT: "+log_text)
573 if spad_popups: Tk.messagebox.showwarning(parent=canvas, title="Occupancy Error", message=log_text)
574 else:
575 section_behind = schematic_object["routebehind"][route_behind.value-1][1]
576 if route_ahead is not None and route_behind is not None:
577 process_track_occupancy(section_ahead, section_behind, item_text)
578 return()
580#------------------------------------------------------------------------------------
581# Common Track Occupancy logic - Track Sensors and Signals. If this function is
582# called for a track sensor then the sig_clear will default to None
583#------------------------------------------------------------------------------------
585def process_track_occupancy(section_ahead:int, section_behind:int, item_text:str, sig_clear:bool=None):
586 if ( section_ahead > 0 and track_sections.section_occupied(section_ahead) and
587 section_behind > 0 and not track_sections.section_occupied(section_behind) ):
588 # Section AHEAD = OCCUPIED and section BEHIND = CLEAR - Pass train from AHEAD to BEHIND
589 train_descriptor = track_sections.clear_section_occupied(section_ahead)
590 track_sections.set_section_occupied (section_behind, train_descriptor)
591 elif ( section_ahead > 0 and not track_sections.section_occupied(section_ahead) and
592 section_behind > 0 and track_sections.section_occupied(section_behind) ):
593 # Section BEHIND = OCCUPIED and section AHEAD = CLEAR - Pass train from BEHIND to AHEAD
594 train_descriptor = track_sections.clear_section_occupied(section_behind)
595 track_sections.set_section_occupied (section_ahead, train_descriptor)
596 if sig_clear == False:
597 log_text = item_text+" has been Passed at Danger by '"+train_descriptor+"'"
598 logging.warning("RUN LAYOUT: "+log_text)
599 if spad_popups: Tk.messagebox.showwarning(parent=canvas, title="SPAD Warning", message=log_text)
600 elif section_ahead > 0 and section_behind == 0 and not track_sections.section_occupied(section_ahead):
601 # Section AHEAD = CLEAR - section BEHIND doesn't exist - set section ahead to OCCUPIED
602 track_sections.set_section_occupied(section_ahead)
603 if sig_clear == False:
604 log_text = item_text+" has been Passed at Danger by an unidentified train"
605 logging.warning("RUN LAYOUT: "+log_text)
606 if spad_popups: Tk.messagebox.showwarning(parent=canvas, title="SPAD Warning", message=log_text)
607 elif section_behind > 0 and section_ahead == 0 and not track_sections.section_occupied(section_behind):
608 # Section BEHIND = CLEAR - section AHEAD doesn't exist - set section behind to OCCUPIED
609 track_sections.set_section_occupied(section_behind)
610 elif section_ahead > 0 and section_behind == 0 and track_sections.section_occupied(section_ahead):
611 # Section AHEAD = OCCUPIED - section BEHIND doesn't exist - set section ahead to CLEAR
612 track_sections.clear_section_occupied(section_ahead)
613 elif section_behind > 0 and section_ahead == 0 and track_sections.section_occupied(section_behind):
614 # Section BEHIND = OCCUPIED - section AHEAD doesn't exist -set section behind to CLEAR
615 train_descriptor = track_sections.clear_section_occupied(section_behind)
616 if sig_clear == False:
617 log_text = item_text+" has been Passed at Danger by '"+train_descriptor+"'"
618 logging.warning("RUN LAYOUT: "+log_text)
619 if spad_popups: Tk.messagebox.showwarning(parent=canvas, title="SPAD Warning", message=log_text)
620 elif ( section_ahead > 0 and not track_sections.section_occupied(section_ahead) and
621 section_behind > 0 and not track_sections.section_occupied(section_behind) ):
622 # Section BEHIND = CLEAR and section AHEAD = CLEAR - No idea
623 log_text = item_text+" has been 'passed' but unable to determine train movement as Track Sections ahead and behind are both CLEAR"
624 logging.warning("RUN LAYOUT: "+log_text)
625 if spad_popups: Tk.messagebox.showwarning(parent=canvas, title="Occupancy Error", message=log_text)
627 elif ( section_ahead > 0 and track_sections.section_occupied(section_ahead) and
628 section_behind > 0 and track_sections.section_occupied(section_behind) ):
629 # Section BEHIND = OCCUPIED and section AHEAD = OCCUPIED
630 if sig_clear == True:
631 # Assume that the train BEHIND the signal will move into the section AHEAD
632 train_descriptor = track_sections.clear_section_occupied(section_behind)
633 train_ahead_descriptor = track_sections.section_label(section_ahead)
634 track_sections.set_section_occupied (section_ahead, train_descriptor)
635 log_text = (item_text+" has been Passed at Clear by '"+train_descriptor+"' and has entered Section occupied by '"
636 +train_ahead_descriptor+ "'. Check and update train descriptor as required")
637 logging.info("RUN LAYOUT: "+log_text)
638 if spad_popups: Tk.messagebox.showinfo(parent=canvas, title="Occupancy Update", message=log_text)
639 else:
640 # We have no idea what train has passed the Signal / Track Section
641 log_text = item_text+" has been 'passed' but unable to determine train movement as Track Sections ahead and behind are both OCCUPIED"
642 logging.warning("RUN LAYOUT: "+log_text)
643 if spad_popups: Tk.messagebox.showwarning(parent=canvas, title="Occupancy Error", message=log_text)
644 ############################################################################################
645 # Propagate changes to any mirrored track sections - To move into Library eventually #######
646 ############################################################################################
647 if section_ahead > 0: update_mirrored_section(section_ahead)
648 if section_behind > 0: update_mirrored_section(section_behind)
649 ############################################################################################
650 return()
652################################################################################################
653# Function to Update a mirrored track sections on a change to one track section. Note ##########
654# the Track Section ID is a string (local or remote) - To move into Library eventually #########
655################################################################################################
657def update_mirrored_section(int_or_str_section_id:Union[int,str], str_section_id_just_set:str="0", recursion_level:int=0):
658 if recursion_level < 20: 658 ↛ 683line 658 didn't jump to line 683, because the condition on line 658 was never false
659 # Iterate through the other sections to see if any are set to mirror this section
660 for str_section_id_to_test in objects.section_index:
661 section_object_to_test = objects.schematic_objects[objects.section(str_section_id_to_test)]
662 str_mirrored_section_id_of_object_to_test = section_object_to_test["mirror"]
663 # Note that the use case of trwo sections set to mirror each other is valid
664 # For this, we just update the first mirrored section and then exit
665 if str(int_or_str_section_id) == str_mirrored_section_id_of_object_to_test:
666 current_label = track_sections.section_label(str_section_id_to_test)
667 current_state = track_sections.section_occupied(str_section_id_to_test)
668 label_to_set = track_sections.section_label(int_or_str_section_id)
669 state_to_set = track_sections.section_occupied(int_or_str_section_id)
670 if state_to_set:
671 track_sections.set_section_occupied(str_section_id_to_test,label_to_set,publish=False)
672 else:
673 track_sections.clear_section_occupied(str_section_id_to_test,label_to_set,publish=False)
674 # See if there are any other sections set to mirror this section - but only bother if the
675 # state or label of this section have actually changed (otherwise there is no point). We
676 # also don't bother looping back on ourselves (if 2 sections are set to mirror each other)
677 if ((current_label != label_to_set or current_state != state_to_set) and
678 str_section_id_to_test != str_section_id_just_set ):
679 update_mirrored_section(str_section_id_to_test,
680 str_mirrored_section_id_of_object_to_test,
681 recursion_level= recursion_level+1)
682 else:
683 logging.error("RUN LAYOUT - Update Mirrored Section - Maximum recursion level reached")
684 return()
686################################################################################################
688#-------------------------------------------------------------------------------------
689# Function to update the Signal interlocking (against points & instruments). Called on
690# sig/sub_switched, point_switched fpl_switched or block_section_ahead_updated events
691# Note that this function processes updates for all local signals on the schematic
692#------------------------------------------------------------------------------------
694def process_all_signal_interlocking():
695 for str_signal_id in objects.signal_index:
696 int_signal_id = int(str_signal_id)
697 # Note that the ID of any associated distant signal is sig_id+100
698 int_associated_distant_id = int_signal_id + 100
699 distant_arms_can_be_unlocked = has_distant_arms(int_signal_id)
700 signal_can_be_unlocked = False
701 subsidary_can_be_unlocked = False
702 # Find the signal route (all points are set and locked by their FPLs)
703 signal_route = find_locked_route(objects.signal(int_signal_id),"pointinterlock")
704 # If there is a set/locked route then the signal/subsidary can be unlocked
705 if signal_route is not None:
706 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
707 # 'sigroutes' and 'subroutes' represent the routes supported by the
708 # signal (and its subsidary) - of the form [main, lh1, lh2, rh1, rh2]
709 if signal_object["sigroutes"][signal_route.value-1]:
710 signal_can_be_unlocked = True
711 if signal_object["subroutes"][signal_route.value-1]:
712 subsidary_can_be_unlocked = True
713 # 'siginterlock' comprises a list of routes [main, lh1, lh2, rh1, rh2]
714 # Each route element comprises a list of signals [sig1, sig2, sig3, sig4]
715 # Each signal element comprises [sig_id, [main, lh1, lh2, rh1, rh2]]
716 # Where each route element is a boolean value (True or False)
717 signal_route_to_test = signal_object["siginterlock"][signal_route.value-1]
718 for opposing_signal_to_test in signal_route_to_test:
719 int_opposing_signal_id = opposing_signal_to_test[0]
720 opposing_sig_routes = opposing_signal_to_test[1]
721 for index, opposing_sig_route in enumerate(opposing_sig_routes):
722 if opposing_sig_route:
723 if ( signals.signal_clear(int_opposing_signal_id, signals_common.route_type(index+1)) or
724 ( has_subsidary(int_opposing_signal_id) and
725 signals.subsidary_clear(int_opposing_signal_id, signals_common.route_type(index+1)))):
726 subsidary_can_be_unlocked = False
727 signal_can_be_unlocked = False
728 # See if the signal is interlocked with a block instrument on the route ahead
729 # Each route comprises: [[p1, p2, p3, p4, p5, p6, p7] signal, block_inst]
730 # The block instrument is the local block instrument - ID is an integer
731 int_block_instrument = signal_object["pointinterlock"][signal_route.value-1][2]
732 if int_block_instrument != 0:
733 block_clear = block_instruments.block_section_ahead_clear(int_block_instrument)
734 if not block_clear and not signals.signal_clear(signal_object["itemid"]):
735 signal_can_be_unlocked = False
736 # The "interlockedahead" flag will only be True if selected and it can only be selected for
737 # a semaphore distant, a colour light distant or a semaphore home with secondary distant arms
738 # In the latter case then a call to "has_distant_arms" will be true (false for all other types)
739 if signal_object["interlockahead"] and home_signal_ahead_at_danger(int_signal_id):
740 if has_distant_arms(int_signal_id):
741 # Must be a home semaphore signal with secondary distant arms
742 if not signals.signal_clear(signal_object["itemid"]+100):
743 distant_arms_can_be_unlocked = False
744 else:
745 # Must be a distant signal (colour light or semaphore)
746 if not signals.signal_clear(signal_object["itemid"]):
747 signal_can_be_unlocked = False
748 # Interlock against track sections on the route ahead - note that this is the
749 # one bit of interlocking functionality that we can only do in RUN mode as
750 # track section objects dont 'exist' as such in EDIT mode
751 if run_mode:
752 interlocked_sections = signal_object["trackinterlock"][signal_route.value-1]
753 for section in interlocked_sections:
754 if section > 0 and track_sections.section_occupied(section):
755 # Only lock the signal if it is already ON (we always need to allow the
756 # signalman to return the signal to ON if it is currently OFF
757 if not signals.signal_clear(signal_object["itemid"]):
758 signal_can_be_unlocked = False
759 break
760 # Interlock the main signal with the subsidary
761 if signals.signal_clear(int_signal_id):
762 subsidary_can_be_unlocked = False
763 if has_subsidary(int_signal_id) and signals.subsidary_clear(int_signal_id):
764 signal_can_be_unlocked = False
765 # Lock/unlock the signal as required
766 if signal_can_be_unlocked: signals.unlock_signal(int_signal_id)
767 else: signals.lock_signal(int_signal_id)
768 # Lock/unlock the subsidary as required (if the signal has one)
769 if has_subsidary(int_signal_id):
770 if subsidary_can_be_unlocked: signals.unlock_subsidary(int_signal_id)
771 else: signals.lock_subsidary(int_signal_id)
772 # lock/unlock the associated distant arms (if the signal has any)
773 if has_distant_arms(int_signal_id):
774 if distant_arms_can_be_unlocked: signals.unlock_signal(int_associated_distant_id)
775 else: signals.lock_signal(int_associated_distant_id)
776 return()
778#------------------------------------------------------------------------------------
779# Function to update the Point interlocking (against signals). Called on sig/sub
780# switched events. Note that this function processes updates for all local points
781# on the schematic
782#------------------------------------------------------------------------------------
784def process_all_point_interlocking():
785 for str_point_id in objects.point_index:
786 int_point_id = int(str_point_id)
787 point_object = objects.schematic_objects[objects.point(int_point_id)]
788 point_locked = False
789 # siginterlock comprises a variable length list of interlocked signals
790 # Each signal entry comprises [sig_id, [main, lh1, lh2, rh1, rh2]]
791 # Each route element is a boolean value (True or False)
792 for interlocked_signal in point_object["siginterlock"]:
793 for index, interlocked_route in enumerate(interlocked_signal[1]):
794 if interlocked_route:
795 if ( signals.signal_clear(interlocked_signal[0], signals_common.route_type(index+1)) or
796 ( has_subsidary(interlocked_signal[0]) and
797 signals.subsidary_clear(interlocked_signal[0], signals_common.route_type(index+1)) )):
798 point_locked = True
799 break
800 if point_locked: points.lock_point(int_point_id)
801 else: points.unlock_point(int_point_id)
802 return()
804#------------------------------------------------------------------------------------
805# Function to Set/Clear all signal overrides based on track occupancy
806# Note that this function processes updates for all local signals on the schematic
807#------------------------------------------------------------------------------------
809def update_all_signal_overrides():
810 # Sub-function to set a signal override
811 def set_signal_override(int_signal_id:int):
812 if objects.schematic_objects[objects.signal(int_signal_id)]["overridesignal"]:
813 signals.set_signal_override(int_signal_id)
814 if has_distant_arms(int_signal_id):
815 signals.set_signal_override(int_signal_id + 100)
817 # Sub-function to Clear a signal override
818 def clear_signal_override(int_signal_id:int):
819 if objects.schematic_objects[objects.signal(int_signal_id)]["overridesignal"]:
820 signals.clear_signal_override(int_signal_id)
821 if has_distant_arms(int_signal_id):
822 signals.clear_signal_override(int_signal_id + 100)
824 # Start of main function
825 for str_signal_id in objects.signal_index:
826 int_signal_id = int(str_signal_id)
827 signal_route = find_valid_route(objects.signal(int_signal_id),"pointinterlock")
828 # Override/clear the current signal based on the section ahead
829 override_signal = False
830 if signal_route is not None:
831 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
832 list_of_sections_ahead = signal_object["tracksections"][1][signal_route.value-1]
833 for section_ahead in list_of_sections_ahead:
834 if (section_ahead > 0 and track_sections.section_occupied(section_ahead)
835 and signal_object["sigroutes"][signal_route.value-1] ):
836 override_signal = True
837 break
838 if override_signal: set_signal_override(int_signal_id)
839 else: clear_signal_override(int_signal_id)
840 else:
841 clear_signal_override(int_signal_id)
842 return()
844#------------------------------------------------------------------------------------
845# Function to override any distant signals that have been configured to be overridden
846# to CAUTION if any of the home signals on the route ahead are at DANGER. If this
847# results in an aspect change then we also work back to update any dependent signals
848# Note that this function processes updates for all LOCAL signals on the schematic
849#------------------------------------------------------------------------------------
851def update_all_distant_overrides():
852 for str_signal_id in objects.signal_index:
853 int_signal_id = int(str_signal_id)
854 signal_object = objects.schematic_objects[objects.signal(int_signal_id)]
855 # The "overrideahead" flag will only be True if selected and it can only be selected for
856 # a semaphore distant, a colour light distant or a semaphore home with secondary distant arms
857 # In the latter case then a call to "has_distant_arms" will be true (false for all other types)
858 if signal_object["overrideahead"]:
859 # The Override on signals ahead function is designed for two use cases
860 # 1) Override signal to CAUTION if ANY home signals in the block section are at danger
861 # 2) Override signal to CAUTION if a distant signal is ahead and at CAUTION - this is to
862 # allow distant signals controlled by one block section to be 'mirrored' on another block
863 # section - e.g. A home signal with an secondary distant arm. In this case the distant
864 # arm would be under the control of the next block section (on that block section schematic)
865 # but you might still want to show the signal (and its state) on your own block schematic
866 if distant_signal_ahead_at_caution(int_signal_id) or home_signal_ahead_at_danger(int_signal_id):
867 if has_distant_arms(int_signal_id):
868 signals.set_signal_override_caution(int_signal_id+100)
869 else:
870 signals.set_signal_override_caution(int_signal_id)
871 else:
872 if has_distant_arms(int_signal_id):
873 signals.clear_signal_override_caution(int_signal_id+100)
874 else:
875 signals.clear_signal_override_caution(int_signal_id)
876 # Update the signal aspect and propogate any aspect updates back along the route
877 process_aspect_updates(int_signal_id)
878 return()
880#------------------------------------------------------------------------------------
881# Function to Update the approach control state of all signals (LOCAL signals only)
882# Note that the 'force_set' flag is set for the signal that has been switched (this
883# is passed in on a signal switched event only) to enforce a "reset" of the Approach
884# control mode in the period between signal released and signal passed events.
885# Note that this function can be called following many callback types and hence
886# the item_id can refer to different item types (points, sections, signals etc)
887# The function therefore has to handle both local or remote item_ids being passed
888# in - but this is only used for matching a signal_switched event (which would
889# match a local signal on the schematic (i.e. the item_id would be an int)
890#------------------------------------------------------------------------------------
892def update_all_signal_approach_control(int_or_str_item_id:Union[int,str]=None, callback_type=None):
893 for str_signal_id in objects.signal_index:
894 if (callback_type == signals_common.sig_callback_type.sig_switched and
895 str_signal_id == str(int_or_str_item_id) ): force_set = True
896 else: force_set = False
897 update_signal_approach_control(int(str_signal_id), force_set)
898 return()
900#------------------------------------------------------------------------------------
901# Function to clear all signal overrides (LOCAL signals only)
902#------------------------------------------------------------------------------------
904def clear_all_signal_overrides():
905 for str_signal_id in objects.signal_index:
906 signals.clear_signal_override(int(str_signal_id))
907 return()
909def clear_all_distant_overrides():
910 for str_signal_id in objects.signal_index:
911 signal_object = objects.schematic_objects[objects.signal(int(str_signal_id))]
912 if signal_object["overrideahead"]:
913 if has_distant_arms(int(str_signal_id)):
914 signals.clear_signal_override_caution(int(str_signal_id)+100)
915 else:
916 signals.clear_signal_override_caution(int(str_signal_id))
917 return()
919def clear_all_approach_control():
920 for str_signal_id in objects.signal_index:
921 signal_object = objects.schematic_objects[objects.signal(int(str_signal_id))]
922 if (signal_object["itemtype"] == signals_common.sig_type.colour_light.value or
923 signal_object["itemtype"] == signals_common.sig_type.semaphore.value):
924 signals.clear_approach_control(int(str_signal_id))
925 return()
927#------------------------------------------------------------------------------------
928# Function to Process all route updates on the schematic (LOCAL signals only)
929#------------------------------------------------------------------------------------
931def set_all_signal_routes():
932 for str_signal_id in objects.signal_index:
933 set_signal_route(int(str_signal_id))
934 return()
936################################################################################################
937# Function to Update all LOCAL mirrored track sections - To move into Library eventually #######
938################################################################################################
940def update_all_mirrored_sections():
941 for str_section_id in objects.section_index:
942 update_mirrored_section(int(str_section_id))
944################################################################################################
946#------------------------------------------------------------------------------------
947# Function to Update all signal aspects (based on signals ahead)
948#------------------------------------------------------------------------------------
950def process_all_aspect_updates():
951 for str_signal_id in objects.signal_index:
952 process_aspect_updates(int(str_signal_id))
953 return()
955#------------------------------------------------------------------------------------
956# Main callback function for when anything on the layout changes
957# Note that the returned item_id could be a remote ID (str) for the following events:
958# track_sections.section_callback_type.section_updated
959# signals_common.sig_callback_type.sig_updated
960#------------------------------------------------------------------------------------
962def schematic_callback(item_id:Union[int,str], callback_type):
963 if enhanced_debugging: logging.info("RUN LAYOUT - Callback - Item: "+str(item_id)+" - Callback Type: "+str(callback_type))
964 # 'signal_passed' events (from LOCAL SIGNALS) can trigger changes in track occupancy
965 # Track Occupancy changes are enabled ONLY IN RUN MODE (as Track section library objects only 'exist'
966 # in Run mode) - and are enabled in RUN MODE whether automation is ENABLED or DISABLED
967 if callback_type == signals_common.sig_callback_type.sig_passed and run_mode:
968 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Track Section occupancy (signal passed event):")
969 update_track_occupancy(objects.signal(item_id))
970 # Timed signal sequences can be triggered by 'signal_passed' events (from LOCAL SIGNALS)
971 # Timed sequences are only Enabled in RUN Mode when Automation is ENABLED
972 if (callback_type == signals_common.sig_callback_type.sig_passed and run_mode and automation_enabled):
973 if enhanced_debugging: logging.info("RUN LAYOUT - Triggering any Timed Signal sequences (signal passed event):")
974 trigger_timed_sequence(item_id)
975 # 'sensor_passed' events can trigger changes in track occupancy - LOCAL TRACK SENSORS ONLY
976 # Track Occupancy changes are enabled ONLY IN RUN MODE (as Track section library objects only 'exist'
977 # in Run mode) - but remain enabled in Run Mode whether automation is Enabled or Disabled
978 if callback_type == track_sensors.track_sensor_callback_type.sensor_triggered and run_mode:
979 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Track Section occupancy (Track Sensor passed event):")
980 update_track_occupancy(objects.track_sensor(item_id))
981 # Signal routes are updated on 'point_switched' or 'fpl_switched' events
982 # Route Setting is ENABLED in both Run and Edit Modes, whether automation is Enabled or Disabled
983 if ( callback_type == points.point_callback_type.point_switched or
984 callback_type == points.point_callback_type.fpl_switched ):
985 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Signal Routes based on Point settings:")
986 set_all_signal_routes()
987 # Any 'Mirrored' track sections are updated following changes to track occupancy as part of the
988 # 'update_track_occupancy' function call above. The 'section_updated' callback is generated when a
989 # track section is manually changed (only possible in RUN Mode) or if an update is received from
990 # a remote track section (which could happen in either Run and Edit Mode). As Track sections (the
991 # library objects) only "exist" in run mode this event is only processed in RUN mode, whether
992 # automation is Enabled or Disabled. Note that the Item ID could local (int) or remote (str).
993 if callback_type == track_sections.section_callback_type.section_updated and run_mode:
994 if enhanced_debugging: logging.info("RUN LAYOUT - Updating any Mirrored Track Sections:")
995 update_mirrored_section(item_id) # Could be an int (local) or str (remote) ####################################################
996 # Signal aspects need to be updated on 'sig_switched'(where a signal state has been manually
997 # changed via the UI), 'sig_updated' (either a timed signal sequence or a remote signal update),
998 # changes to signal overides (see above for events) or changes to the approach control state
999 # of a signal ('sig_passed' or 'sig_released' events - or any changes to the signal routes)
1000 # any signal overrides have been SET or CLEARED (as a result of track sections ahead
1001 # being occupied/cleared following a signal passed event) or if any signal junction
1002 # approach control states have been SET or CLEARED - including the case of the signal
1003 # being RELEASED (as signified by the 'sig_released' event) or the approach control
1004 # being RESET (following a 'sig_passed' event)
1005 if ( callback_type == signals_common.sig_callback_type.sig_updated or
1006 callback_type == signals_common.sig_callback_type.sig_released or
1007 callback_type == signals_common.sig_callback_type.sig_passed or
1008 callback_type == signals_common.sig_callback_type.sig_switched or
1009 callback_type == points.point_callback_type.point_switched or
1010 callback_type == points.point_callback_type.fpl_switched or
1011 callback_type == track_sections.section_callback_type.section_updated ):
1012 if run_mode and automation_enabled:
1013 # First we update all signal overrides based on track occupancy, but ONLY IN RUN MODE
1014 # (as track sections only exist in RUN Mode), if Automation is ENABLED
1015 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Signal Overrides to reflect Track Occupancy:")
1016 update_all_signal_overrides()
1017 # Approach control is made complex by the need to support the case of setting approach
1018 # control on the state of home signals ahead (for layout automation). We therefore have
1019 # to process these changes here (which also updates the aspects of all signals).
1020 # Note that the item_id is only used in conjunction with the signal_passed event
1021 # so the function will not 'break' if the item-id is an int or a str
1022 # Approach Control is only ENABLED in RUN Mode if automation is ENABLED
1023 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Signal Approach Control to reflect Signals ahead:")
1024 update_all_signal_approach_control(item_id, callback_type)
1025 # Finally process any distant signal overrides on home signals ahead (walks the home signals
1026 # ahead and will override the distant signal to CAUTION if any of the home signals are at DANGER
1027 # This is a seperate override function to the main signal override (works an an OR function)
1028 # Distant Overrides are only ENABLED in RUN Mode if automation is ENABLED
1029 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Signal CAUTION Overrides to reflect Signals ahead:")
1030 update_all_distant_overrides()
1031 else:
1032 # If we are in EDIT mode and/or Automation is DISABLED, we still want to update the
1033 # signals to reflect the displayed aspects of the signal ahead
1034 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Signal Aspects to reflect Signals ahead:")
1035 process_all_aspect_updates()
1036 # Signal interlocking is updated on point, signal or block instrument switched events
1037 # We also need to process signal interlocking on any event which may have changed the
1038 # displayed aspect of a signal (when interlocking signals against home signals ahead)
1039 # Interlocking is ENABLED in Run and Edit Modes, whether automation is Enabled or Disabled
1040 if ( callback_type == block_instruments.block_callback_type.block_section_ahead_updated or
1041 callback_type == signals_common.sig_callback_type.sub_switched or
1042 callback_type == signals_common.sig_callback_type.sig_updated or
1043 callback_type == signals_common.sig_callback_type.sig_released or
1044 callback_type == signals_common.sig_callback_type.sig_passed or
1045 callback_type == signals_common.sig_callback_type.sig_switched or
1046 callback_type == points.point_callback_type.point_switched or
1047 callback_type == points.point_callback_type.fpl_switched or
1048 callback_type == track_sections.section_callback_type.section_updated ):
1049 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Signal Interlocking:")
1050 process_all_signal_interlocking()
1051 # Point interlocking is updated on signal (or subsidary signal) switched events
1052 # Interlocking is ENABLED in Run and Edit Modes, whether automation is Enabled or Disabled
1053 if ( callback_type == signals_common.sig_callback_type.sig_switched or
1054 callback_type == signals_common.sig_callback_type.sub_switched):
1055 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Point Interlocking:")
1056 process_all_point_interlocking()
1057 if enhanced_debugging: logging.info("**************************************************************************************")
1058 # Refocus back on the canvas to ensure that any keypress events function
1059 canvas.focus_set()
1060 return()
1062#------------------------------------------------------------------------------------
1063# Function to "initialise" the layout - Called on change of Edit/Run Mode, Automation
1064# Enable/Disable, layout reset, layout load, object deletion (from the schematic) or
1065# the configuration change of any schematic object
1066#------------------------------------------------------------------------------------
1068def initialise_layout():
1069 global list_of_movements
1070 if enhanced_debugging: logging.info("RUN LAYOUT - Initialising Schematic **************************************************")
1071 # Reset the list of track occupancy movements
1072 list_of_movements = []
1073 # We always process signal routes - for all modes whether automation is enabled/disabled
1074 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Signal Routes based on Point settings:")
1075 set_all_signal_routes()
1076 if run_mode and not automation_enabled:
1077 # Run Mode (Track Sections exist) with Automation Disabled. Note that we need to call
1078 # the process_all_aspect_updates function (as we are not making the other update calls)
1079 if enhanced_debugging: logging.info("RUN LAYOUT - Updating all Mirrored Track Sections:") ####################################
1080 update_all_mirrored_sections() ###############################################################################################
1081 if enhanced_debugging: logging.info("RUN LAYOUT - Clearing down all Signal Overrides (automation disabled):")
1082 clear_all_signal_overrides()
1083 clear_all_distant_overrides()
1084 if enhanced_debugging: logging.info("RUN LAYOUT - Clearing down all Approach Control (automation disabled):")
1085 clear_all_approach_control()
1086 if enhanced_debugging: logging.info("RUN LAYOUT - Updating signal aspects to reflect the signals ahead:")
1087 process_all_aspect_updates()
1088 elif run_mode and automation_enabled:
1089 # Run Mode (Track Sections exist) with Automation Enabled. Note that aspects are
1090 # updated by update_all_signal_approach_control and update_all_distant_overrides
1091 if enhanced_debugging: logging.info("RUN LAYOUT - Updating all Mirrored Track Sections:") ####################################
1092 update_all_mirrored_sections() ###############################################################################################
1093 if enhanced_debugging: logging.info("RUN LAYOUT - Overriding Signals to reflect Track Occupancy:")
1094 update_all_signal_overrides()
1095 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Signal Approach Control and updating signal aspects:")
1096 update_all_signal_approach_control()
1097 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Distant Signal Overrides based on Home Signals ahead:")
1098 update_all_distant_overrides()
1099 else:
1100 # Edit mode (automation disabled by default - we don't care about the user selection)
1101 # Note that we need to call the process_all_aspect_updates function (see above)
1102 if enhanced_debugging: logging.info("RUN LAYOUT - Clearing down all Signal Overrides (automation disabled):")
1103 clear_all_signal_overrides()
1104 clear_all_distant_overrides()
1105 if enhanced_debugging: logging.info("RUN LAYOUT - Clearing down all Approach Control (automation disabled):")
1106 clear_all_approach_control()
1107 if enhanced_debugging: logging.info("RUN LAYOUT - Updating signal aspects to reflect the signals ahead:")
1108 process_all_aspect_updates()
1109 # We always process interlocking - for all modes whether automation is enabled/disabled
1110 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Signal Interlocking:")
1111 process_all_signal_interlocking()
1112 if enhanced_debugging: logging.info("RUN LAYOUT - Updating Point Interlocking:")
1113 process_all_point_interlocking()
1114 if enhanced_debugging: logging.info("**************************************************************************************")
1115 # Refocus back on the canvas to ensure that any keypress events function
1116 canvas.focus_set()
1117 return()
1119########################################################################################