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

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#------------------------------------------------------------------------------------ 

65 

66import logging 

67import tkinter as Tk 

68from typing import Union 

69 

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 

78 

79from . import objects 

80 

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#------------------------------------------------------------------------------------ 

85 

86canvas = None 

87run_mode = None 

88automation_enabled = None 

89spad_popups = False 

90enhanced_debugging = False # Switch this on to enable 'info' 

91 

92#------------------------------------------------------------------------------------ 

93# The set_canvas function is called at application startup (on canvas creation) 

94#------------------------------------------------------------------------------------ 

95 

96def initialise(canvas_object): 

97 global canvas 

98 canvas = canvas_object 

99 return() 

100 

101#------------------------------------------------------------------------------------ 

102# The behavior of the layout processing will change depending on what mode we are in 

103#------------------------------------------------------------------------------------ 

104 

105def enable_editing(): 

106 global run_mode 

107 run_mode = False 

108 initialise_layout() 

109 return() 

110 

111def disable_editing(): 

112 global run_mode 

113 run_mode = True 

114 initialise_layout() 

115 return() 

116 

117def configure_automation(automation:bool): 

118 global automation_enabled 

119 automation_enabled = automation 

120 initialise_layout() 

121 return() 

122 

123def configure_spad_popups(popups:bool): 

124 global spad_popups 

125 spad_popups = popups 

126 return() 

127 

128#------------------------------------------------------------------------------------ 

129# Internal helper Function to find if an ID is a local (int) or remote (str) Item ID 

130#------------------------------------------------------------------------------------ 

131 

132def is_local_id(item_id:Union[int,str]): 

133 return( isinstance(item_id, int) or (isinstance(item_id, str) and item_id.isdigit()) ) 

134 

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#------------------------------------------------------------------------------------ 

139 

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] ) 

148 

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#------------------------------------------------------------------------------------ 

153 

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] ) 

161 

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#------------------------------------------------------------------------------------ 

166 

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) ) 

173 

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#------------------------------------------------------------------------------------ 

178 

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 ) ) 

185 

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#------------------------------------------------------------------------------------ 

196 

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) 

217 

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#------------------------------------------------------------------------------------ 

223 

224def find_valid_route(object_id, dict_key:str): 

225 route, locked = find_route(object_id, dict_key) 

226 return(route) 

227 

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) 

232 

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#------------------------------------------------------------------------------------ 

240 

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) 

249 

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#------------------------------------------------------------------------------------ 

256 

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) 

265 

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#------------------------------------------------------------------------------------ 

277 

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) 

293 

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#------------------------------------------------------------------------------------ 

302 

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) 

315 

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#------------------------------------------------------------------------------------ 

323 

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() 

340 

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#------------------------------------------------------------------------------------ 

348 

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() 

364 

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#------------------------------------------------------------------------------------ 

370 

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() 

385 

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#------------------------------------------------------------------------------------ 

390 

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() 

410 

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#------------------------------------------------------------------------------------ 

419 

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() 

449 

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#------------------------------------------------------------------------------------ 

459 

460list_of_movements = [] 

461 

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#------------------------------------------------------------------------------------ 

483 

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() 

493 

494#------------------------------------------------------------------------------------ 

495# Signal specific logic for track occupancy updates 

496#------------------------------------------------------------------------------------ 

497 

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() 

551 

552#------------------------------------------------------------------------------------ 

553# Track Sensor specific logic for track occupancy updates 

554#------------------------------------------------------------------------------------ 

555 

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() 

579 

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#------------------------------------------------------------------------------------ 

584 

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) 

626 

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() 

651 

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################################################################################################ 

656 

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() 

685 

686################################################################################################ 

687 

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#------------------------------------------------------------------------------------ 

693 

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() 

777 

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#------------------------------------------------------------------------------------ 

783 

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() 

803 

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#------------------------------------------------------------------------------------ 

808 

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) 

816 

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) 

823 

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() 

843 

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#------------------------------------------------------------------------------------ 

850 

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() 

879 

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#------------------------------------------------------------------------------------ 

891 

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() 

899 

900#------------------------------------------------------------------------------------ 

901# Function to clear all signal overrides (LOCAL signals only) 

902#------------------------------------------------------------------------------------ 

903 

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() 

908 

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() 

918 

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() 

926 

927#------------------------------------------------------------------------------------ 

928# Function to Process all route updates on the schematic (LOCAL signals only) 

929#------------------------------------------------------------------------------------ 

930 

931def set_all_signal_routes(): 

932 for str_signal_id in objects.signal_index: 

933 set_signal_route(int(str_signal_id)) 

934 return() 

935 

936################################################################################################ 

937# Function to Update all LOCAL mirrored track sections - To move into Library eventually ####### 

938################################################################################################ 

939 

940def update_all_mirrored_sections(): 

941 for str_section_id in objects.section_index: 

942 update_mirrored_section(int(str_section_id)) 

943 

944################################################################################################ 

945 

946#------------------------------------------------------------------------------------ 

947# Function to Update all signal aspects (based on signals ahead) 

948#------------------------------------------------------------------------------------ 

949 

950def process_all_aspect_updates(): 

951 for str_signal_id in objects.signal_index: 

952 process_aspect_updates(int(str_signal_id)) 

953 return() 

954 

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#------------------------------------------------------------------------------------ 

961 

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() 

1061 

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#------------------------------------------------------------------------------------ 

1067 

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() 

1118 

1119########################################################################################