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

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

307 

308from . import signals_common 

309from . import signals_colour_lights 

310from . import signals_semaphores 

311from . import mqtt_interface 

312 

313from typing import Union 

314import logging 

315 

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

324 

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) 

337 

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

345 

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) 

354 

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

361 

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) 

377 

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

384 

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

393 

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

400 

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

409 

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

418 

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

429 

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

438 

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

449 

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

459 

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

470 

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

478 

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

489 

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

498 

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

514 

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

522 

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

538 

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

546 

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

558 

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

567 

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

586 

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

594 

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

627 

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

635 

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

651 

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

661 

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

679 

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

687 

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

710 

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

725 

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

741 

742#----------------------------------------------------------------------------------------------- 

743# Public API Function to "subscribe" to signal updates published by another MQTT"Node" 

744#----------------------------------------------------------------------------------------------- 

745 

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

764 

765#----------------------------------------------------------------------------------------------- 

766# Public API Function to set all aspect changes to be "published" for a signal 

767#----------------------------------------------------------------------------------------------- 

768 

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

781 

782##########################################################################################