Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/library/signals.py: 65%
203 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 (and its dependent packages) is used for creating and managing signal objects
3# ------------------------------------------------------------------------------------------
4#
5# Currently supported signal types:
6#
7# Colour Light Signals - 3 or 4 aspect or 2 aspect (home, distant or red/ylw)
8# - with / without a position light subsidary signal
9# - with / without route indication feathers (maximum of 5)
10# - with / without a theatre type route indicator
11# - With / without a "Signal Passed" Button
12# - With / without a "Approach Release" Button
13# - With / without control buttons (manual / fully automatic)
14# Semaphore Signals - Home or Distant
15# - with / without junction arms (RH1, RH2, LH1, LH2)
16# - with / without subsidary arms (Main, LH1, LH2, RH1, RH2) (Home signals only)
17# - with / without a theatre type route indicator (Home signals only)
18# - With / without a "Signal Passed" Button
19# - With / without a "Approach Release" Button
20# - With / without control buttons (manual / fully automatic)
21# - Home and Distant signals can be co-located
22# Ground Position Light Signals
23# - normal ground position light or shunt ahead position light
24# - either early or modern (post 1996) types
25# Ground Disc Signals
26# - normal ground disc (red banner) or shunt ahead ground disc (yellow banner)
27#
28# Summary of features supported by each signal type:
29#
30# Colour Light signals
31# - set_route_indication (Route Type and theatre text)
32# - update_signal (based on a signal Ahead) - not 2 Aspect Home or Red/Yellow
33# - toggle_signal / toggle_subsidary
34# - lock_subsidary / unlock_subsidary
35# - lock_signal / unlock_signal
36# - set_signal_override / clear_signal_override
37# - set_signal_override_caution / clear_signal_override_caution (not Home)
38# - set_approach_control (Release on Red or Yellow) / clear_approach_control
39# - trigger_timed_signal
40# - query signal state (signal_clear, signal_state, subsidary_clear)
41# Semaphore signals:
42# - set_route_indication (Route Type and theatre text)
43# - update_signal (based on a signal Ahead) - (distant signals only)
44# - toggle_signal / toggle_subsidary
45# - lock_subsidary / unlock_subsidary
46# - lock_signal / unlock_signal
47# - set_signal_override / clear_signal_override
48# - set_signal_override_caution / clear_signal_override_caution (not Home)
49# - set_approach_control (Release on Red only) / clear_approach_control
50# - trigger_timed_signal
51# - query signal state (signal_clear, signal_state, subsidary_clear)
52# Ground Position Colour Light signals:
53# - toggle_signal
54# - lock_signal / unlock_signal
55# - set_signal_override / clear_signal_override
56# - query signal state (signal_clear, signal_state)
57# Ground Disc signals
58# - toggle_signal
59# - lock_signal / unlock_signal
60# - set_signal_override / clear_signal_override
61# - query signal state (signal_clear, signal_state)
62#
63# Public types and functions:
64#
65# signal_sub_type (use when creating colour light signals):
66# signal_sub_type.home (2 aspect - Red/Green)
67# signal_sub_type.distant (2 aspect - Yellow/Green
68# signal_sub_type.red_ylw (2 aspect - Red/Yellow
69# signal_sub_type.three_aspect (3 aspect - Red/Yellow/Green)
70# signal_sub_type.four_aspect (4 aspect - Red/Yellow/Double-Yellow/Green)
71#
72# semaphore_sub_type (use when creating semaphore signals):
73# semaphore_sub_type.home
74# semaphore_sub_type.distant
75#
76# ground_pos_sub_type(enum.Enum):
77# ground_pos_sub_type.standard (post 1996 type)
78# ground_pos_sub_type.shunt_ahead (post 1996 type)
79# ground_pos_sub_type.early_standard
80# ground_pos_sub_type.early_shunt_ahead
81#
82# ground_disc_sub_type(enum.Enum):
83# ground_disc_sub_type.standard
84# ground_disc_sub_type.shunt_ahead
85#
86# route_type (use for specifying the route):
87# route_type.NONE (no route indication)
88# route_type.MAIN (main route)
89# route_type.LH1 (immediate left)
90# route_type.LH2 (far left)
91# route_type.RH1 (immediate right)
92# route_type.RH2 (rar right)
93# These equate to the feathers for colour light signals or the Sempahore junction "arms"
94#
95# signal_state_type(enum.Enum):
96# DANGER (colour light & semaphore signals)
97# PROCEED (colour light & semaphore signals)
98# CAUTION (colour light & semaphore signals)
99# PRELIM_CAUTION (colour light signals only)
100# CAUTION_APP_CNTL (colour light signals only - CAUTION but subject to RELEASE ON YELLOW)
101# FLASH_CAUTION (colour light signals only- when the signal ahead is CAUTION_APP_CNTL)
102# FLASH_PRELIM_CAUTION (colour light signals only- when the signal ahead is FLASH_CAUTION)
103#
104# sig_callback_type (tells the calling program what has triggered the callback):
105# sig_callback_type.sig_switched (signal has been switched)
106# sig_callback_type.sub_switched (subsidary signal has been switched)
107# sig_callback_type.sig_passed ("signal passed" event - or triggered by a Timed signal)
108# sig_callback_type.sig_updated (signal aspect updated as part of a timed sequence)
109# sig_callback_type.sig_released (signal "approach release" event)
110#
111# create_colour_light_signal - Creates a colour light signal
112# Mandatory Parameters:
113# Canvas - The Tkinter Drawing canvas on which the signal is to be displayed
114# sig_id:int - The ID for the signal - also displayed on the signal button
115# x:int, y:int - Position of the signal on the canvas (in pixels)
116# Optional Parameters:
117# signal_subtype:sig_sub_type - subtype of signal - Default = four_aspect
118# orientation:int- Orientation in degrees (0 or 180) - Default = zero
119# sig_callback:name - Function to call when a signal event happens - Default = None
120# Note that the callback function returns (item_id, callback type)
121# sig_passed_button:bool - Creates a "signal Passed" button - Default = False
122# approach_release_button:bool - Creates an "Approach Release" button - Default = False
123# position_light:bool - Creates a subsidary position light signal - Default = False
124# lhfeather45:bool - Creates a LH route feather at 45 degrees - Default = False
125# lhfeather90:bool - Creates a LH route feather at 90 degrees - Default = False
126# rhfeather45:bool - Creates a RH route feather at 45 degrees - Default = False
127# rhfeather90:bool - Creates a RH route feather at 90 degrees - Default = False
128# mainfeather:bool - Creates a MAIN route feather - Default = False
129# theatre_route_indicator:bool - Creates a Theatre route indicator - Default = False
130# refresh_immediately:bool - When set to False the signal aspects will NOT be automatically
131# updated when the signal is changed and the external programme will need to call
132# the seperate 'update_signal' function. Primarily intended for use with 3/4
133# aspect signals, where the displayed aspect will depend on the displayed aspect
134# of the signal ahead if the signal is clear - Default = True
135# fully_automatic:bool - Creates a signal without a manual controls - Default = False
136#
137# create_semaphore_signal - Creates a Semaphore signal
138# Mandatory Parameters:
139# Canvas - The Tkinter Drawing canvas on which the signal is to be displayed
140# sig_id:int - The ID for the signal - also displayed on the signal button
141# x:int, y:int - Position of the signal on the canvas (in pixels)
142# Optional Parameters:
143# signal_subtype - subtype of the signal - default = semaphore_sub_type.home
144# associated_home:int - Option only valid when creating distant signals - Provide the ID of
145# a previously created home signal (and use the same x and y coords)
146# to create the distant signal on the same post as the home signal
147# with appropriate "slotting" between the signal arms - Default = False
148# orientation:int - Orientation in degrees (0 or 180) - Default = zero
149# sig_callback:name - Function to call when a signal event happens - Default = None
150# Note that the callback function returns (item_id, callback type)
151# sig_passed_button:bool - Creates a "signal Passed" button - Default = False
152# approach_release_button:bool - Creates an "Approach Release" button - Default = False
153# main_signal:bool - To create a signal arm for the main route - default = True
154# (Only set this to False when creating an "associated" distant signal
155# for a situation where a distant arm for the main route is not required)
156# lh1_signal:bool - create a LH1 post with a main (junction) arm - default = False
157# lh2_signal:bool - create a LH2 post with a main (junction) arm - default = False
158# rh1_signal:bool - create a RH1 post with a main (junction) arm - default = False
159# rh2_signal:bool - create a RH2 post with a main (junction) arm - default = False
160# main_subsidary:bool - create a subsidary signal under the "main" signal - default = False
161# lh1_subsidary:bool - create a LH1 post with a subsidary arm - default = False
162# lh2_subsidary:bool - create a LH2 post with a subsidary arm - default = False
163# rh1_subsidary:bool - create a RH1 post with a subsidary arm - default = False
164# rh2_subsidary:bool - create a RH2 post with a subsidary arm - default = False
165# theatre_route_indicator:bool - Creates a Theatre route indicator - Default = False
166# refresh_immediately:bool - When set to False the signal aspects will NOT be automatically
167# updated when the signal is changed and the external programme will need to call
168# the seperate 'update_signal' function. Primarily intended for fully automatic
169# distant signals to reflect the state of the home signal ahead - Default = True
170# fully_automatic:bool - Creates a signal without a manual control button - Default = False
171#
172# create_ground_position_signal - create a ground position light signal
173# Mandatory Parameters:
174# Canvas - The Tkinter Drawing canvas on which the signal is to be displayed
175# sig_id:int - The ID for the signal - also displayed on the signal button
176# x:int, y:int - Position of the signal on the canvas (in pixels)
177# Optional Parameters:
178# signal_subtype - subtype of the signal - default = ground_pos_sub_type.early_standard
179# orientation:int- Orientation in degrees (0 or 180) - default is zero
180# sig_callback:name - Function to call when a signal event happens - default = None
181# Note that the callback function returns (item_id, callback type)
182# sig_passed_button:bool - Creates a "signal Passed" button - default =False
183#
184# create_ground_disc_signal - Creates a ground disc type signal
185# Mandatory Parameters:
186# Canvas - The Tkinter Drawing canvas on which the signal is to be displayed
187# sig_id:int - The ID for the signal - also displayed on the signal button
188# x:int, y:int - Position of the signal on the canvas (in pixels)
189# Optional Parameters:
190# signal_subtype - subtype of the signal - default = ground_disc_sub_type.standard
191# orientation:int- Orientation in degrees (0 or 180) - Default is zero
192# sig_callback:name - Function to call when a signal event happens - Default = none
193# Note that the callback function returns (item_id, callback type)
194# sig_passed_button:bool - Creates a "signal Passed" button - Default = False
195#
196# set_route - Set (and change) the route indication (either feathers or theatre text)
197# Mandatory Parameters:
198# sig_id:int - The ID for the signal
199# Optional Parameters:
200# route:signals_common.route_type - MAIN, LH1, LH2, RH1 or RH2 - default = 'NONE'
201# theatre_text:str - The text to display in the theatre indicator - default = "NONE"
202#
203# update_signal - update the signal aspect based on the aspect of a signal ahead - Primarily
204# intended for 3/4 aspect colour light signals but can also be used to update
205# 2-aspect distant signals (semaphore or colour light) on the home signal ahead
206# Mandatory Parameters:
207# sig_id:int - The ID for the signal
208# Optional Parameters:
209# sig_ahead_id:int/str - The ID for the signal "ahead" of the one we want to update.
210# Either an integer representing the ID of the signal created on our schematic,
211# or a string representing the compound identifier of a remote signal on an
212# external MQTT node. Default = "None" (no signal ahead to take into account)
213#
214# toggle_signal(sig_id:int) - for route setting (use 'signal_clear' to find the state)
215#
216# toggle_subsidary(sig_id:int) - forroute setting (use 'subsidary_clear' to find the state)
217#
218# lock_signal(*sig_id:int) - for interlocking (multiple Signal_IDs can be specified)
219#
220# unlock_signal(*sig_id:int) - for interlocking (multiple Signal_IDs can be specified)
221#
222# lock_subsidary(*sig_id:int) - for interlocking (multiple Signal_IDs can be specified)
223#
224# unlock_subsidary(*sig_id:int) - for interlocking (multiple Signal_IDs can be specified)
225#
226# signal_clear - returns the SWITCHED state of the signal - i.e the state of the
227# signal manual control button (True='OFF', False = 'ON'). If a route
228# is specified then the function also tests against the specified route
229# Mandatory Parameters:
230# sig_id:int - The ID for the signal
231# Optional Parameters:
232# route:signals_common.route_type - MAIN, LH1, LH2, RH1 or RH2 - default = 'NONE'
233#
234# subsidary_clear - returns the SWITCHED state of the subsidary i.e the state of the
235# signal manual control button (True='OFF', False = 'ON'). If a route
236# is specified then the function also tests against the specified route
237# Mandatory Parameters:
238# sig_id:int - The ID for the signal
239# Optional Parameters:
240# route:signals_common.route_type - MAIN, LH1, LH2, RH1 or RH2 - default = 'NONE'
241#
242# signal_state(sig_id:int/str) - returns the DISPLAYED state of the signal. This can be different
243# to the SWITCHED state if the signal is OVERRIDDEN or subject to APPROACH
244# CONTROL. Use this function when you need to get the actual state (in terms
245# of aspect) that the signal is displaying - returns 'signal_state_type'.
246# - Note that for this function, the sig_id can be specified either as an
247# integer (representing the ID of a signal on the local schematic), or a
248# string (representing the identifier of an signal on an external MQTT node)
249#
250# set_signal_override (sig_id*:int) - Overrides the signal to display the most restrictive aspect
251# (Distant signals will display CAUTION - all other types will display DANGER)
252#
253# clear_signal_override (sig_id*:int) - Clears the signal Override (can specify multiple sig_ids)
254#
255# set_signal_override_caution (sig_id*:int) - Overrides the signal to display CAUTION
256# (Applicable to all main signal types apart from home signals)
257#
258# clear_signal_override_caution (sig_id*:int) - Clears the signal Override
259# (Applicable to all main signal types apart from home signals)
260#
261# trigger_timed_signal - Sets the signal to DANGER and cycles through the aspects back to PROCEED.
262# If start delay > 0 then a 'sig_passed' callback event is generated when
263# the signal is changed to DANGER - For each subsequent aspect change
264# (back to PROCEED) a 'sig_updated' callback event will be generated.
265# Mandatory Parameters:
266# sig_id:int - The ID for the signal
267# Optional Parameters:
268# start_delay:int - Delay (in seconds) before changing to DANGER (default = 5)
269# time_delay:int - Delay (in seconds) for cycling through the aspects (default = 5)
270#
271# set_approach_control - Normally used when a diverging route has a lower speed restriction.
272# Puts the signal into "Approach Control" Mode where the signal will display a more
273# restrictive aspect/state (either DANGER or CAUTION) to approaching trains. As the
274# Train approaches, the signal will then be "released" to display its "normal" aspect.
275# When a signal is in "approach control" mode the signals behind will display the
276# appropriate aspects (when updated based on the signal ahead). These would be the
277# normal aspects for "Release on Red" but for "Release on Yellow", the colour light
278# signals behind would show flashing yellow / double-yellow aspects as appropriate.
279# Mandatory Parameters:
280# sig_id:int - The ID for the signal
281# Optional Parameters:
282# release_on_yellow:Bool - True for Release on Yellow - default = False (Release on Red)
283# force_set:Bool - If False then this function will have no effect in the period between
284# the signal being 'released' and the signal being 'passed' (default True)
285#
286# clear_approach_control (sig_id:int) - This "releases" the signal to display the normal aspect.
287# Signals are also automatically released when the"release button" (displayed just
288# in front of the signal if specified when the signal was created) is activated,
289# either manually or via an external sensor event.
290#
291# ------------------------------------------------------------------------------------------
292#
293# The following functions are associated with the MQTT networking Feature:
294#
295# subscribe_to_remote_signal - Subscribes to a remote signal object
296# Mandatory Parameters:
297# remote_identifier:str - the remote identifier for the signal in the form 'node-id'
298# Optional Parameters:
299# signal_callback - Function to call when a signal update is received - default = None
300#
301# set_signals_to_publish_state - Enable the publication of state updates for signals.
302# All subsequent changes will be automatically published to remote subscribers
303# Mandatory Parameters:
304# *sig_ids:int - The signals to publish (multiple Signal_IDs can be specified)
305#
306# ------------------------------------------------------------------------------------------
308from . import signals_common
309from . import signals_colour_lights
310from . import signals_semaphores
311from . import mqtt_interface
313from typing import Union
314import logging
316# -------------------------------------------------------------------------
317# Externally called function to Return the current SWITCHED state of the signal
318# (i.e. the state of the signal button - Used to enable interlocking functions)
319# Note that the DISPLAYED state of the signal may not be CLEAR if the signal is
320# overridden or subject to release on RED - See "signal_displaying_clear"
321# Function applicable to ALL signal types created on the local schematic
322# Function does not support REMOTE Signals (with a compound Sig-ID)
323# -------------------------------------------------------------------------
325def signal_clear (sig_id:int,route:signals_common.route_type = None):
326 # Validate the signal exists
327 if not signals_common.sig_exists(sig_id): 327 ↛ 328line 327 didn't jump to line 328, because the condition on line 327 was never true
328 logging.error ("Signal "+str(sig_id)+": signal_clear - Signal does not exist")
329 sig_clear = False
330 else:
331 if route is None:
332 sig_clear = signals_common.signals[str(sig_id)]["sigclear"]
333 else:
334 sig_clear = (signals_common.signals[str(sig_id)]["sigclear"] and
335 signals_common.signals[str(sig_id)]["routeset"] == route)
336 return (sig_clear)
338# -------------------------------------------------------------------------
339# Externally called function to Return the displayed state of the signal
340# (i.e. whether the signal is actually displaying a CLEAR aspect). Note that
341# this can be different to the state the signal has been manually set to (via
342# the signal button) - as it could be overridden or subject to Release on Red
343# Function applicable to ALL signal types - Including REMOTE SIGNALS
344# -------------------------------------------------------------------------
346def signal_state (sig_id:Union[int,str]):
347 # Validate the signal exists
348 if not signals_common.sig_exists(sig_id): 348 ↛ 349line 348 didn't jump to line 349, because the condition on line 348 was never true
349 logging.error ("Signal "+str(sig_id)+": signal_state - Signal does not exist")
350 sig_state = signals_common.signal_state_type.DANGER
351 else:
352 sig_state = signals_common.signals[str(sig_id)]["sigstate"]
353 return (sig_state)
355# -------------------------------------------------------------------------
356# Externally called function to Return the current state of the subsidary
357# signal - if the signal does not have one then the return will be FALSE
358# Function applicable to ALL signal types created on the local schematic
359# Function does not support REMOTE Signals (with a compound Sig-ID)
360# -------------------------------------------------------------------------
362def subsidary_clear (sig_id:int,route:signals_common.route_type = None):
363 # Validate the signal exists
364 if not signals_common.sig_exists(sig_id): 364 ↛ 365line 364 didn't jump to line 365, because the condition on line 364 was never true
365 logging.error ("Signal "+str(sig_id)+": subsidary_clear - Signal does not exist")
366 sig_clear = False
367 elif not signals_common.signals[str(sig_id)]["hassubsidary"]: 367 ↛ 368line 367 didn't jump to line 368, because the condition on line 367 was never true
368 logging.error ("Signal "+str(sig_id)+": subsidary_clear - Signal does not have a subsidary")
369 sig_clear = False
370 else:
371 if route is None:
372 sig_clear = signals_common.signals[str(sig_id)]["subclear"]
373 else:
374 sig_clear = (signals_common.signals[str(sig_id)]["subclear"] and
375 signals_common.signals[str(sig_id)]["routeset"] == route)
376 return (sig_clear)
378# -------------------------------------------------------------------------
379# Externally called function to Lock the signal (preventing it being cleared)
380# Multiple signal IDs can be specified in the call
381# Function applicable to ALL signal types created on the local schematic
382# Function does not support REMOTE Signals (with a compound Sig-ID)
383# -------------------------------------------------------------------------
385def lock_signal (*sig_ids:int):
386 for sig_id in sig_ids:
387 # Validate the signal exists
388 if not signals_common.sig_exists(sig_id): 388 ↛ 389line 388 didn't jump to line 389, because the condition on line 388 was never true
389 logging.error ("Signal "+str(sig_id)+": lock_signal - Signal does not exist")
390 else:
391 signals_common.lock_signal(sig_id)
392 return()
394# -------------------------------------------------------------------------
395# Externally called function to Unlock the main signal
396# Multiple signal IDs can be specified in the call
397# Function applicable to ALL signal types created on the local schematic
398# Function does not support REMOTE Signals (with a compound Sig-ID)
399# -------------------------------------------------------------------------
401def unlock_signal (*sig_ids:int):
402 for sig_id in sig_ids:
403 # Validate the signal exists
404 if not signals_common.sig_exists(sig_id): 404 ↛ 405line 404 didn't jump to line 405, because the condition on line 404 was never true
405 logging.error ("Signal "+str(sig_id)+": unlock_signal - Signal does not exist")
406 else:
407 signals_common.unlock_signal(sig_id)
408 return()
410# -------------------------------------------------------------------------
411# Externally called function to Lock the subsidary signal
412# This is effectively a seperate signal from the main aspect
413# Multiple signal IDs can be specified in the call
414# Function applicable to ALL signal types created on the local schematic
415# (will report an error if the specified signal does not have a subsidary)
416# Function does not support REMOTE Signals (with a compound Sig-ID)
417# -------------------------------------------------------------------------
419def lock_subsidary (*sig_ids:int):
420 for sig_id in sig_ids:
421 # Validate the signal exists
422 if not signals_common.sig_exists(sig_id): 422 ↛ 423line 422 didn't jump to line 423, because the condition on line 422 was never true
423 logging.error ("Signal "+str(sig_id)+": lock_subsidary - Signal does not exist")
424 elif not signals_common.signals[str(sig_id)]["hassubsidary"]: 424 ↛ 425line 424 didn't jump to line 425, because the condition on line 424 was never true
425 logging.error ("Signal "+str(sig_id)+": lock_subsidary - Signal does not have a subsidary")
426 else:
427 signals_common.lock_subsidary(sig_id)
428 return()
430# -------------------------------------------------------------------------
431# Externally called function to Unlock the subsidary signal
432# This is effectively a seperate signal from the main aspect
433# Multiple signal IDs can be specified in the call
434# Function applicable to ALL signal types created on the local schematic
435# (will report an error if the specified signal does not have a subsidary)
436# Function does not support REMOTE Signals (with a compound Sig-ID)
437# -------------------------------------------------------------------------
439def unlock_subsidary (*sig_ids:int):
440 for sig_id in sig_ids:
441 # Validate the signal exists
442 if not signals_common.sig_exists(sig_id): 442 ↛ 443line 442 didn't jump to line 443, because the condition on line 442 was never true
443 logging.error ("Signal "+str(sig_id)+": unlock_subsidary - Signal does not exist")
444 elif not signals_common.signals[str(sig_id)]["hassubsidary"]: 444 ↛ 445line 444 didn't jump to line 445, because the condition on line 444 was never true
445 logging.error ("Signal "+str(sig_id)+": unlock_subsidary - Signal does not have a subsidary")
446 else:
447 signals_common.unlock_subsidary(sig_id)
448 return()
450# -------------------------------------------------------------------------
451# Externally called function to Override a signal - effectively setting it
452# to RED (apart from 2 aspect distance signals - which are set to YELLOW)
453# Signal will display the overriden aspect no matter what its current setting is
454# Used to support automation - e.g. set a signal to Danger once a train has passed
455# Multiple signal IDs can be specified in the call
456# Function applicable to ALL signal types created on the local schematic
457# Function does not support REMOTE Signals (with a compound Sig-ID)
458# -------------------------------------------------------------------------
460def set_signal_override (*sig_ids:int):
461 for sig_id in sig_ids:
462 # Validate the signal exists
463 if not signals_common.sig_exists(sig_id): 463 ↛ 464line 463 didn't jump to line 464, because the condition on line 463 was never true
464 logging.error ("Signal "+str(sig_id)+": set_signal_override - Signal does not exist")
465 else:
466 # Set the override and refresh the signal following the change in state
467 signals_common.set_signal_override(sig_id)
468 signals_common.auto_refresh_signal(sig_id)
469 return()
471# -------------------------------------------------------------------------
472# Externally called function to Clear a Signal Override
473# Signal will revert to its current manual setting (on/off) and aspect
474# Multiple signal IDs can be specified in the call
475# Function applicable to ALL signal types created on the local schematic
476# Function does not support REMOTE Signals (with a compound Sig-ID)
477# -------------------------------------------------------------------------
479def clear_signal_override (*sig_ids:int):
480 for sig_id in sig_ids:
481 # Validate the signal exists
482 if not signals_common.sig_exists(sig_id): 482 ↛ 483line 482 didn't jump to line 483, because the condition on line 482 was never true
483 logging.error ("Signal "+str(sig_id)+": clear_signal_override - Signal does not exist")
484 else:
485 # Clear the override and refresh the signal following the change in state
486 signals_common.clear_signal_override(sig_id)
487 signals_common.auto_refresh_signal(sig_id)
488 return()
490# -------------------------------------------------------------------------
491# Externally called function to Override a signal to CAUTION. The signal will
492# display CAUTION irrespective of its current setting. Used to support automation
493# e.g. set a signal to CAUTION if any Home signals ahead are at DANGER.
494# Multiple signal IDs can be specified in the call
495# Function applicable to all signal types apart from HOME signals
496# Function does not support REMOTE Signals (with a compound Sig-ID)
497# -------------------------------------------------------------------------
499def set_signal_override_caution (*sig_ids:int):
500 for sig_id in sig_ids:
501 # Validate the signal exists
502 if not signals_common.sig_exists(sig_id): 502 ↛ 503line 502 didn't jump to line 503, because the condition on line 502 was never true
503 logging.error ("Signal "+str(sig_id)+": set_signal_override_caution - Signal does not exist")
504 elif ( ( signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.colour_light and 504 ↛ 512line 504 didn't jump to line 512, because the condition on line 504 was never false
505 signals_common.signals[str(sig_id)]["subtype"] != signals_colour_lights.signal_sub_type.home ) or
506 ( signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.semaphore and
507 signals_common.signals[str(sig_id)]["subtype"] != signals_semaphores.semaphore_sub_type.home ) ):
508 # Set the override and refresh the signal following the change in state
509 signals_common.set_signal_override_caution(sig_id)
510 signals_common.auto_refresh_signal(sig_id)
511 else:
512 logging.error("Signal "+str(sig_id)+": - set_signal_override_caution - Function not supported by signal type")
513 return()
515# -------------------------------------------------------------------------
516# Externally called function to Clear a Signal Override
517# Signal will revert to its current manual setting (on/off) and aspect
518# Multiple signal IDs can be specified in the call
519# Function applicable to ALL signal types created on the local schematic
520# Function does not support REMOTE Signals (with a compound Sig-ID)
521# -------------------------------------------------------------------------
523def clear_signal_override_caution (*sig_ids:int):
524 for sig_id in sig_ids:
525 # Validate the signal exists
526 if not signals_common.sig_exists(sig_id): 526 ↛ 527line 526 didn't jump to line 527, because the condition on line 526 was never true
527 logging.error ("Signal "+str(sig_id)+": clear_signal_override_caution - Signal does not exist")
528 elif ( ( signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.colour_light and 528 ↛ 536line 528 didn't jump to line 536, because the condition on line 528 was never false
529 signals_common.signals[str(sig_id)]["subtype"] != signals_colour_lights.signal_sub_type.home ) or
530 ( signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.semaphore and
531 signals_common.signals[str(sig_id)]["subtype"] != signals_semaphores.semaphore_sub_type.home ) ):
532 # Set the override and refresh the signal following the change in state
533 signals_common.clear_signal_override_caution(sig_id)
534 signals_common.auto_refresh_signal(sig_id)
535 else:
536 logging.error("Signal "+str(sig_id)+": - clear_signal_override_caution - Function not supported by signal type")
537 return()
539# -------------------------------------------------------------------------
540# Externally called function to Toggle the state of a main signal
541# to enable automated route setting from the external programme.
542# Use in conjunction with 'signal_clear' to find the state first
543# Function applicable to ALL signal types created on the local schematic
544# Function does not support REMOTE Signals (with a compound Sig-ID)
545# -------------------------------------------------------------------------
547def toggle_signal (sig_id:int):
548 # Validate the signal exists
549 if not signals_common.sig_exists(sig_id):
550 logging.error ("Signal "+str(sig_id)+": toggle_signal - Signal does not exist")
551 else:
552 if signals_common.signals[str(sig_id)]["siglocked"]:
553 logging.warning ("Signal "+str(sig_id)+": toggle_signal - Signal is locked - Toggling anyway")
554 # Toggle the signal and refresh the signal following the change in state
555 signals_common.toggle_signal(sig_id)
556 signals_common.auto_refresh_signal(sig_id)
557 return()
559# -------------------------------------------------------------------------
560# Externally called function to Toggle the state of a subsidary signal
561# to enable automated route setting from the external programme. Use
562# in conjunction with 'subsidary_signal_clear' to find the state first
563# Function applicable to ALL signal types created on the local schematic
564# (will report an error if the specified signal does not have a subsidary)
565# Function does not support REMOTE Signals (with a compound Sig-ID)
566# -------------------------------------------------------------------------
568def toggle_subsidary (sig_id:int):
569 # Validate the signal exists
570 if not signals_common.sig_exists(sig_id):
571 logging.error ("Signal "+str(sig_id)+": toggle_subsidary - Signal does not exist")
572 elif not signals_common.signals[str(sig_id)]["hassubsidary"]:
573 logging.error ("Signal "+str(sig_id)+": toggle_subsidary - Signal does not have a subsidary")
574 else:
575 if signals_common.signals[str(sig_id)]["sublocked"]:
576 logging.warning ("Signal "+str(sig_id)+": toggle_subsidary - Subsidary signal is locked - Toggling anyway")
577 # Toggle the subsidary and refresh the signal following the change in state
578 signals_common.toggle_subsidary(sig_id)
579 if signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.colour_light:
580 signals_colour_lights.update_colour_light_subsidary(sig_id)
581 elif signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.semaphore:
582 signals_semaphores.update_semaphore_subsidary_arms(sig_id)
583 else:
584 logging.error ("Signal "+str(sig_id)+": toggle_subsidary - Function not supported by signal type")
585 return()
587# -------------------------------------------------------------------------
588# Externally called function to set the "approach conrol" for the signal
589# Calls the signal type-specific functions depending on the signal type
590# Function applicable to Colour Light and Semaphore signal types created on
591# the local schematic (will report an error if the particular signal type not
592# supported) Function does not support REMOTE Signals (with a compound Sig-ID)
593# -------------------------------------------------------------------------
595def set_approach_control (sig_id:int, release_on_yellow:bool = False, force_set:bool = True):
596 # Validate the signal exists
597 if not signals_common.sig_exists(sig_id): 597 ↛ 598line 597 didn't jump to line 598, because the condition on line 597 was never true
598 logging.error ("Signal "+str(sig_id)+": set_approach_control - Signal does not exist")
599 else:
600 # call the signal type-specific functions to update the signal (note that we only update
601 # Semaphore and colour light signals if they are configured to update immediately)
602 if signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.colour_light:
603 # do some additional validation specific to this function for colour light signals
604 if signals_common.signals[str(sig_id)]["subtype"]==signals_colour_lights.signal_sub_type.distant: 604 ↛ 605line 604 didn't jump to line 605, because the condition on line 604 was never true
605 logging.error("Signal "+str(sig_id)+": Can't set approach control for a 2 aspect distant signal")
606 elif release_on_yellow and signals_common.signals[str(sig_id)]["subtype"]==signals_colour_lights.signal_sub_type.home: 606 ↛ 607line 606 didn't jump to line 607, because the condition on line 606 was never true
607 logging.error("Signal "+str(sig_id)+": Can't set \'release on yellow\' approach control for a 2 aspect home signal")
608 elif release_on_yellow and signals_common.signals[str(sig_id)]["subtype"]==signals_colour_lights.signal_sub_type.red_ylw: 608 ↛ 609line 608 didn't jump to line 609, because the condition on line 608 was never true
609 logging.error("Signal "+str(sig_id)+": Can't set \'release on yellow\' approach control for a 2 aspect red/yellow signal")
610 else:
611 # Set approach control and refresh the signal following the change in state
612 signals_common.set_approach_control(sig_id, release_on_yellow, force_set)
613 signals_common.auto_refresh_signal(sig_id)
614 elif signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.semaphore: 614 ↛ 625line 614 didn't jump to line 625, because the condition on line 614 was never false
615 # Do some additional validation specific to this function for semaphore signals
616 if signals_common.signals[str(sig_id)]["subtype"] == signals_semaphores.semaphore_sub_type.distant: 616 ↛ 617line 616 didn't jump to line 617, because the condition on line 616 was never true
617 logging.error("Signal "+str(sig_id)+": Can't set approach control for semaphore distant signals")
618 elif release_on_yellow: 618 ↛ 619line 618 didn't jump to line 619, because the condition on line 618 was never true
619 logging.error("Signal "+str(sig_id)+": Can't set \'release on yellow\' approach control for home signals")
620 else:
621 # Set approach control and refresh the signal following the change in state
622 signals_common.set_approach_control(sig_id, release_on_yellow, force_set)
623 signals_common.auto_refresh_signal(sig_id)
624 else:
625 logging.error ("Signal "+str(sig_id)+": set_approach_control - Function not supported by signal type")
626 return()
628# -------------------------------------------------------------------------
629# Externally called function to clear the "approach control" for the signal
630# Calls the signal type-specific functions depending on the signal type
631# Function applicable to Colour Light and Semaphore signal types created on
632# the local schematic (will have no effect on other signal types
633# Function does not support REMOTE Signals (with a compound Sig-ID)
634# -------------------------------------------------------------------------
636def clear_approach_control (sig_id:int):
637 # Validate the signal exists
638 if not signals_common.sig_exists(sig_id): 638 ↛ 639line 638 didn't jump to line 639, because the condition on line 638 was never true
639 logging.error ("Signal "+str(sig_id)+": clear_approach_control - Signal does not exist")
640 else:
641 # call the signal type-specific functions to update the signal (note that we only update
642 # Semaphore and colour light signals if they are configured to update immediately)
643 if ( signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.colour_light or 643 ↛ 649line 643 didn't jump to line 649, because the condition on line 643 was never false
644 signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.semaphore ):
645 # Clear approach control and refresh the signal following the change in state
646 signals_common.clear_approach_control (sig_id)
647 signals_common.auto_refresh_signal(sig_id)
648 else:
649 logging.error ("Signal "+str(sig_id)+": clear_approach_control - Function not supported by signal type")
650 return()
652# -------------------------------------------------------------------------
653# Externally called Function to update a signal according the state of the
654# Signal ahead - Intended mainly for Coulour Light Signal types so we can
655# ensure the "CLEAR" aspect reflects the aspect of ths signal ahead
656# Calls the signal type-specific functions depending on the signal type
657# Function applicable only to Main colour Light and semaphore signal types
658# created on the local schematic - but either locally-created or REMOTE
659# Signals can be specified as the signal ahead
660# -------------------------------------------------------------------------
662def update_signal (sig_id:int, sig_ahead_id:Union[int,str]=None):
663 # Validate the signal exists (and the one ahead if specified)
664 if not signals_common.sig_exists(sig_id): 664 ↛ 665line 664 didn't jump to line 665, because the condition on line 664 was never true
665 logging.error ("Signal "+str(sig_id)+": update_signal - Signal does not exist")
666 elif sig_ahead_id != None and not signals_common.sig_exists(sig_ahead_id): 666 ↛ 667line 666 didn't jump to line 667, because the condition on line 666 was never true
667 logging.error ("Signal "+str(sig_id)+": update_signal - Signal ahead "+str(sig_ahead_id)+" does not exist")
668 elif sig_id == sig_ahead_id: 668 ↛ 669line 668 didn't jump to line 669, because the condition on line 668 was never true
669 logging.error ("Signal "+str(sig_id)+": update_signal - Signal ahead "+str(sig_ahead_id)+" is the same ID")
670 else:
671 # call the signal type-specific functions to update the signal
672 if signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.colour_light: 672 ↛ 674line 672 didn't jump to line 674, because the condition on line 672 was never false
673 signals_colour_lights.update_colour_light_signal (sig_id,sig_ahead_id)
674 elif signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.semaphore:
675 signals_semaphores.update_semaphore_signal (sig_id,sig_ahead_id)
676 else:
677 logging.error ("Signal "+str(sig_id)+": update_signal - Function not supported by signal type")
678 return()
680# -------------------------------------------------------------------------
681# Externally called function to set the route indication for the signal
682# Calls the signal type-specific functions depending on the signal type
683# Function only applicable to Main Colour Light and Semaphore signal types
684# created on the local schematic (will raise an error if signal type not
685# supported. Function does not support REMOTE Signals (with a compound Sig-ID)
686# -------------------------------------------------------------------------
688def set_route (sig_id:int, route:signals_common.route_type = None, theatre_text:str = None):
689 # Validate the signal exists
690 if not signals_common.sig_exists(sig_id): 690 ↛ 691line 690 didn't jump to line 691, because the condition on line 690 was never true
691 logging.error ("Signal "+str(sig_id)+": set_route - Signal does not exist")
692 else:
693 if route is not None:
694 # call the signal type-specific functions to update the signal
695 if signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.colour_light:
696 signals_colour_lights.update_feather_route_indication (sig_id,route)
697 elif signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.semaphore:
698 signals_semaphores.update_semaphore_route_indication (sig_id,route)
699 # Even if the signal does not support route indications we still allow the route
700 # element to be set. This is useful for interlocking where a signal without a route
701 # display (e.g. ground signal) can support more than one interlocked routes
702 signals_common.signals[str(sig_id)]["routeset"] = route
703 if theatre_text is not None:
704 # call the signal type-specific functions to update the signal
705 if signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.colour_light:
706 signals_common.update_theatre_route_indication(sig_id,theatre_text)
707 elif signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.semaphore:
708 signals_common.update_theatre_route_indication(sig_id,theatre_text)
709 return()
711# -------------------------------------------------------------------------
712# Externally called Function to 'override' a signal (changing it to 'ON') after
713# a specified time delay and then clearing the override the signal after another
714# specified time delay. In the case of colour light signals, this will cause the
715# signal to cycle through the supported aspects all the way back to GREEN. When
716# the Override is cleared, the signal will revert to its previously displayed aspect
717# This is to support the automation of 'exit' signals on a layout
718# A 'sig_passed' callback event will be generated when the signal is overriden if
719# and only if a start delay (> 0) is specified. For each subsequent aspect change
720# a'sig_updated' callback event will be generated
721# Function only applicable to Main Colour Light and Semaphore signal types
722# created on the local schematic (will raise an error if signal type not
723# supported. Function does not support REMOTE Signals (with a compound Sig-ID)
724# -------------------------------------------------------------------------
726def trigger_timed_signal (sig_id:int,start_delay:int=0,time_delay:int=5):
727 # Validate the signal exists
728 if not signals_common.sig_exists(sig_id): 728 ↛ 729line 728 didn't jump to line 729, because the condition on line 728 was never true
729 logging.error ("Signal "+str(sig_id)+": trigger_timed_signal - Signal does not exist")
730 else:
731 # call the signal type-specific functions to update the signal
732 if signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.colour_light:
733 logging.info ("Signal "+str(sig_id)+": Triggering Timed Signal")
734 signals_colour_lights.trigger_timed_colour_light_signal (sig_id,start_delay,time_delay)
735 elif signals_common.signals[str(sig_id)]["sigtype"] == signals_common.sig_type.semaphore: 735 ↛ 739line 735 didn't jump to line 739, because the condition on line 735 was never false
736 logging.info ("Signal "+str(sig_id)+": Triggering Timed Signal")
737 signals_semaphores.trigger_timed_semaphore_signal (sig_id,start_delay,time_delay)
738 else:
739 logging.error ("Signal "+str(sig_id)+": trigger_timed_signal - Function not supported by signal type")
740 return()
742#-----------------------------------------------------------------------------------------------
743# Public API Function to "subscribe" to signal updates published by another MQTT"Node"
744#-----------------------------------------------------------------------------------------------
746def subscribe_to_remote_signal (remote_identifier:str,signal_callback):
747 # Validate the remote identifier (must be 'node-id' where id is an int between 1 and 99)
748 if mqtt_interface.split_remote_item_identifier(remote_identifier) is None: 748 ↛ 749line 748 didn't jump to line 749, because the condition on line 748 was never true
749 logging.error ("MQTT-Client: Signal "+remote_identifier+": The remote identifier must be in the form of 'Node-ID'")
750 logging.error ("with the 'Node' element a non-zero length string and the 'ID' element an integer between 1 and 99")
751 else:
752 if signals_common.sig_exists(remote_identifier): 752 ↛ 753line 752 didn't jump to line 753, because the condition on line 752 was never true
753 logging.warning("MQTT-Client: Signal "+remote_identifier+" - has already been subscribed to via MQTT networking")
754 signals_common.signals[remote_identifier] = {}
755 signals_common.signals[remote_identifier]["sigtype"] = signals_common.sig_type.remote_signal
756 signals_common.signals[remote_identifier]["sigstate"] = signals_common.signal_state_type.DANGER
757 signals_common.signals[remote_identifier]["routeset"] = signals_common.route_type.NONE
758 signals_common.signals[remote_identifier]["extcallback"] = signal_callback
759 # Subscribe to updates from the remote signal (even if we have already subscribed)
760 [node_id,item_id] = mqtt_interface.split_remote_item_identifier(remote_identifier)
761 mqtt_interface.subscribe_to_mqtt_messages("signal_updated_event",node_id,item_id,
762 signals_common.handle_mqtt_signal_updated_event)
763 return()
765#-----------------------------------------------------------------------------------------------
766# Public API Function to set all aspect changes to be "published" for a signal
767#-----------------------------------------------------------------------------------------------
769def set_signals_to_publish_state(*sig_ids:int):
770 for sig_id in sig_ids:
771 logging.debug("MQTT-Client: Configuring signal "+str(sig_id)+" to publish state changes via MQTT broker")
772 # Add the signal ID to the list of signals to publish
773 if sig_id in signals_common.list_of_signals_to_publish_state_changes: 773 ↛ 774line 773 didn't jump to line 774, because the condition on line 773 was never true
774 logging.warning("MQTT-Client: Signal "+str(sig_id)+" - is already configured to publish state changes")
775 else:
776 signals_common.list_of_signals_to_publish_state_changes.append(sig_id)
777 # Publish the initial state now this has been added to the list of signals to publish
778 # This allows the publish/subscribe functions to be configured after signal creation
779 if str(sig_id) in signals_common.signals.keys(): signals_common.publish_signal_state(sig_id)
780 return()
782##########################################################################################