Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/library/dcc_control.py: 99%

258 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-10 15:08 +0100

1#---------------------------------------------------------------------------------------------------- 

2# This module enables DCC mappings to be created for the Signal and Point library objects, which 

3# are then processed on signal/route changes to send out the required DCC commands for the layout 

4#---------------------------------------------------------------------------------------------------- 

5#  

6# For Colour Light signals, either "Event Driven" or "Command Sequence" mappings can be defined. 

7# An "Event Driven" mapping would send out a single DCC command (address/state) to change the signal 

8# to the required aspect (as used by the TrainTech DCC signals). A "Command Sequence" mapping would 

9# send out a sequence of DCC commands (address/state), which the signal would then map to the required 

10# aspect to display (as used by signal decoders such as the Signalist SC1). Note that if the signal 

11# has a subsidary aspect then the subsidary aspect is mapped to its own unique DCC address. 

12# 

13# Semaphore signal mappings are more straightforward as each semaphore arm on the signal is mapped 

14# to its own unique DCC address (to switch the arm either ON or OFF) 

15# 

16# The feather route indications (Colour Light Signals only) or Theatre Route indications (supported by 

17# Semaphore or colour light signals also support "Event Driven" or "Command Sequence" mappings. 

18# 

19# Points mappings consist of a single DCC address (either 'Switched' or 'Normal') although this logic 

20# can be reversed at mapping creation time if required. 

21#  

22# Not all signals/points that exist on the layout need to have a DCC Mapping configured - If no DCC mapping  

23# has been defined, then no DCC commands will be sent. This provides flexibility for including signals on the  

24# schematic which are "off-scene" or for progressively "working up" the signalling scheme for a layout. 

25#  

26#---------------------------------------------------------------------------------------------------- 

27# 

28# External API - the classes and functions (used by the Schematic Editor): 

29# 

30# get_dcc_address_mappings() - returns a sorted dictionary of DCC addresses and details of their mappings 

31# Keys are DCC addresses, Each element comprises [item, item_id] - item is either 'Signal' or 'Point' 

32# 

33# dcc_address_mapping(dcc_address:int) - Reteturns an existing DCC address mapping if one exists (otherwise None) 

34# If not None, the returned value is [item, item_id] - item is either 'Signal' or 'Point' 

35# 

36# map_dcc_signal - Generate DCC mappings for a semaphore signal 

37# Mandatory Parameters: 

38# sig_id:int - The ID for the signal to create a DCC mapping for 

39# Optional Parameters: 

40# auto_route_inhibit:bool (default = False) - does the signal inhibit route indications at DANGER? 

41# proceed[[add:int,state:bool],] - List of DCC Commands for "Green" 

42# danger [[add:int,state:bool],] - List of DCC Commands for "Red" 

43# caution[[add:int,state:bool],] - List of DCC Commands for "Yellow" 

44# prelim_caution[[add:int,state:bool],] - List of DCC Commands for "Double Yellow" 

45# flash_caution[[add:int,state:bool],] - List of DCC Commands for "Flashing Yellow" 

46# flash_prelim_caution[[add:int,state:bool],] - List of DCC Commands for "Flashing Double Yellow" 

47# LH1[[add:int,state:bool],] - List of DCC Commands for the LH1 Feather 

48# LH2[[add:int,state:bool],] - List of DCC Commands for the LH2 Feather 

49# RH1[[add:int,state:bool],] - List of DCC Commands for the RH1 Feather 

50# RH2[[add:int,state:bool],] - List of DCC Commands for the RH2 Feather 

51# MAIN[[add:int,state:bool],] - List of DCC Commands for the MAIN Feather 

52# NONE[[add:int,state:bool],] - List of DCC Commands to inhibit all Feathers 

53# THEATRE[["char",[add:int,state:bool],],] - List of Theatre states and their DCC command sequences 

54# subsidary:int - DCC address for the "subsidary" signal 

55#  

56# map_semaphore_signal - Generate DCC mappings for a semaphore signal 

57# Mandatory Parameters: 

58# sig_id:int - The ID for the signal to create a DCC mapping for 

59# main_signal:int - DCC address for the main signal arm 

60# Optional Parameters: 

61# main_subsidary:int - DCC address for main subsidary arm 

62# lh1_signal:int - DCC address for LH1 signal arm 

63# lh1_subsidary:int - DCC address for LH1 subsidary arm 

64# lh2_signal:int - DCC address for LH2 signal arm  

65# lh2_subsidary:int - DCC address for LH2 subsidary arm 

66# rh1_signal:int - DCC address for RH1 signal arm 

67# rh1_subsidary:int - DCC address for RH1 subsidary arm 

68# rh2_signal:int - DCC address for RH2 signal arm 

69# rh2_subsidary:int - DCC address for RH2 subsidary arm 

70# THEATRE[["char",[add:int,state:bool],],] - List of Theatre states and their DCC command sequences 

71# 

72# map_dcc_point - Generate DCC mappings for a point 

73# Mandatory Parameters: 

74# point_id:int - The ID for the point to create a DCC mapping for 

75# address:int - the single DCC address to use for the point 

76# Optional Parameters: 

77# state_reversed:bool - Set to True to reverse the DCC logic (default = false) 

78# 

79# delete_point_mapping(point_id:int) - Delete a DCC mapping (called when the Point is deleted) 

80# 

81# delete_signal_mapping(sig_id:int) - Delete a DCC mapping (called when the Signal is deleted) 

82# 

83# The following API functions are for configuring the pub/sub of DCC command feeds. The functions are called 

84# by the editor on 'Apply' of the MQTT settings. First, 'reset_mqtt_configuration' is called to clear down 

85# the existing pub/sub configuration, followed by 'set_node_to_publish_dcc_commands' (either True or False) 

86# and 'subscribe_to_dcc_command_feed' for each REMOTE DCC Node (DCC Command feed subscribed). 

87# 

88# reset_mqtt_configuration() - Clears down the current DCC Command feed pub/sub configuration 

89# 

90# set_node_to_publish_dcc_commands(publish_dcc_commands:bool) - Enable publishing of DCC command feed 

91# All DCC commands wil lthen be published to the MQTT broker for consumption by other nodes 

92#  

93# subscribe_to_dcc_command_feed(*nodes:str) - Subcribes to DCC command feeds from other nodes on the network. 

94# All received DCC commands will then be automatically forwarded to the local Pi-Sprog interface. 

95# 

96# External API - classes and functions (used by the other library modules): 

97# 

98# update_dcc_point(point_id:int,state:bool) - Called on state change of a point 

99# 

100# update_dcc_signal_aspects(sig_id:int, sig_state:signals_common.signal_state_type) - called on change of a Colour Light Signal 

101# 

102# update_dcc_signal_element(sig_id:int, state:bool, element:str)- called on update of a Semaphore Signal 

103# (also called for Colour Light signals to change the 'main_subsidary' element when this changes) 

104# 

105# update_dcc_signal_route(sig_id:int, route:signals_common.route_type, signal_change:bool=False ,sig_at_danger:bool=False) 

106# 

107# update_dcc_signal_theatre(sig_id:int, character_to_display:str, signal_change:bool=False, sig_at_danger:bool=False): 

108# 

109# handle_mqtt_dcc_accessory_short_event(message) - Called on reciept of a 'dcc_accessory_short_events' message 

110# 

111#---------------------------------------------------------------------------------------------------- 

112# DCC Mapping Examples 

113# 

114# An "Event Driven" example - a 4 aspect signal, where 2 addresses are used (the base address 

115# to select the Red or Green aspect and the base+1 address to set the Yellow or Double Yellow 

116# Aspect). A single DCC command is then used to change the signal to the required state 

117# 

118# map_dcc_signal(sig_id = 2, 

119# danger = [[1,False]], 

120# proceed = [[1,True]], 

121# caution = [[2,True]], 

122# prelim_caution = [[2,False]]) 

123# 

124# An example mapping for a Signalist SC1 decoder with a base address of 1 is included below. This assumes 

125# the decoder is configured in "8 individual output" Mode (CV38=8). In this example we are using outputs 

126# A,B,C,D to drive our signal with E & F each driving a route feather. The Signallist SC1 uses 8 consecutive 

127# addresses in total (which equate to DCC addresses 1 to 8 for this example). The DCC addresses for each LED 

128# are therefore : RED = 1, Green = 2, YELLOW1 = 3, YELLOW2 = 4, Feather1 = 5, Feather2 = 6. 

129#  

130# map_dcc_signal(sig_id = 2, 

131# danger = [[1,True],[2,False],[3,False],[4,False]], 

132# proceed = [[1,False],[2,True],[3,False],[4,False]], 

133# caution = [[1,False],[2,False],[3,True],[4,False]], 

134# prelim_caution = [[1,False],[2,False],[3,True],[4,True]], 

135# LH1 = [[5,True],[6,False]],  

136# MAIN = [[6,True],[5,False]],  

137# NONE = [[5,False],[6,False]] ) 

138#  

139# A another example DCC mapping, but this time with a Theatre Route Indication, is shown below. The main signal 

140# aspects are configured in the same way to the example above, the only difference being the THEATRE mapping, 

141# where a display of "A" is enabled by DCC Address 5 and "B" by DCC Address 6. 

142#  

143# map_dcc_signal(sig_id = 2, 

144# danger = [[1,True],[2,False],[3,False],[4,False]], 

145# proceed = [[1,False],[2,True],[3,False],[4,False]], 

146# caution = [[1,False],[2,False],[3,True],[4,False]], 

147# prelim_caution = [[1,False],[2,False],[3,True],[4,True]], 

148# THEATRE = [ ["#",[[5,False],[6,False]]], 

149# ["1",[[6,False],[5,True]]], 

150# ["2",[[5,False],[6,True]]] ] ) 

151# 

152# For the Theatre Route indicator, Each entry comprises the character to display and the list of DCC Commands 

153# [address,state] needed to get the theatre indicator to display the character. Note that "#" is a special 

154# character - which means inhibit all route indications. You should ALWAYS provide mappings for '#' unless 

155# the signal automatically inhibits route indications when at DANGER (see 'auto_route_inhibit' flag above). 

156# 

157# Similarly, if you are using route feathers, you should ALWAYS provide mappings for 'NONE' unless 

158# the signal automatically inhibits route indications when at DANGER. 

159# 

160# Semaphore signal DCC mappings assume that each main/subsidary signal arm is mapped to its own DCC address. 

161# In this example, we are mapping a signal with MAIN and LH signal arms and a subsidary arm for the MAIN route. 

162#  

163# map_semaphore_signal(sig_id = 2,  

164# main_signal = 1 ,  

165# lh1_signal = 2 ,  

166# main_subsidary = 3) 

167# 

168#---------------------------------------------------------------------------------------------------- 

169 

170from . import signals_common 

171from . import pi_sprog_interface 

172from . import mqtt_interface 

173 

174import enum 

175import logging 

176 

177#---------------------------------------------------------------------------------------------------- 

178# Global definitions 

179#---------------------------------------------------------------------------------------------------- 

180 

181# Define the internal Type for the DCC Signal mappings 

182class mapping_type(enum.Enum): 

183 SEMAPHORE = 1 # One to one mapping of single DCC Addresses to each signal element  

184 COLOUR_LIGHT = 2 # Each aspect is mapped to a sequence of one or more DCC Addresses/states 

185 

186# The DCC commands for Signals and Points are held in global dictionaries where the dictionary 

187# 'key' is the ID of the signal or point. Each entry is another dictionary, with each element 

188# holding the DCC commands (or sequences) needed to put the signal/point into the desired state. 

189# Note that the mappings are completely different for Colour Light or Semaphore signals, so the 

190# common 'mapping_type' value is used by the software to differentiate between the two types 

191dcc_signal_mappings:dict = {} 

192dcc_point_mappings:dict = {} 

193 

194# Define the Flag to control whether DCC Commands are published to the MQTT Broker 

195publish_dcc_commands_to_mqtt_broker:bool = False 

196 

197# List of DCC Mappings - The key is the address, with each element a list of [item,item_id] 

198# Note that we use the DCC Address as an INTEGER for the key - so we can sort on the key 

199# Item - either "Signal" or "Point" to identify the type of item the address is mapped to 

200# Item ID - the ID of the Signal or Point that the DCC address is mapped to 

201dcc_address_mappings:dict = {} 

202 

203#---------------------------------------------------------------------------------------------------- 

204# API function to return a dictionary of all DCC Address mappings (to signals/points) 

205#---------------------------------------------------------------------------------------------------- 

206 

207def get_dcc_address_mappings(): 

208 return(dcc_address_mappings) 

209 

210#---------------------------------------------------------------------------------------------------- 

211# API function to return an existing DCC address mapping if one exists (otherwise None) 

212#---------------------------------------------------------------------------------------------------- 

213 

214def dcc_address_mapping(dcc_address:int): 

215 if not isinstance(dcc_address, int) or dcc_address < 0 or dcc_address > 2047: 

216 logging.error("DCC Control: dcc_address_mapping - Invalid DCC Address "+str(dcc_address)) 

217 dcc_address_mapping = None 

218 elif dcc_address not in dcc_address_mappings.keys(): 

219 dcc_address_mapping = None 

220 else: 

221 dcc_address_mapping = dcc_address_mappings[dcc_address] 

222 return(dcc_address_mapping) 

223 

224#---------------------------------------------------------------------------------------------------- 

225# Internal function to test if a DCC mapping already exists for a signal 

226#---------------------------------------------------------------------------------------------------- 

227 

228def sig_mapped(sig_id:int): 

229 return (str(sig_id) in dcc_signal_mappings.keys()) 

230 

231#---------------------------------------------------------------------------------------------------- 

232# Internal function to test if a DCC mapping already exists for a point 

233#---------------------------------------------------------------------------------------------------- 

234 

235def point_mapped(point_id:int): 

236 return (str(point_id) in dcc_point_mappings.keys()) 

237 

238#---------------------------------------------------------------------------------------------------- 

239# Function to "map" a Colour Light signal object to a series of DCC addresses/command sequences 

240# Note we allow DCC addresses of zero to be specified (i.e. no mapping for that element) 

241#---------------------------------------------------------------------------------------------------- 

242 

243def map_dcc_signal(sig_id:int, 

244 auto_route_inhibit:bool = False, 

245 danger = [[0,False],], 

246 proceed = [[0,False],], 

247 caution = [[0,False],], 

248 prelim_caution = [[0,False],], 

249 flash_caution = [[0,False],], 

250 flash_prelim_caution = [[0,False],], 

251 LH1 = [[0,False],], 

252 LH2 = [[0,False],], 

253 RH1 = [[0,False],], 

254 RH2 = [[0,False],], 

255 MAIN = [[0,False],], 

256 NONE = [[0,False],], 

257 THEATRE = [["#", [[0,False],]],], 

258 subsidary:int=0): 

259 global dcc_signal_mappings 

260 global dcc_address_mappings 

261 # Do some basic validation on the parameters we have been given 

262 if not isinstance(sig_id,int) or sig_id < 1: 

263 logging.error ("DCC Control: map_dcc_signal - Signal "+str(sig_id)+" - Signal ID must be a positive integer") 

264 elif sig_mapped(sig_id): 

265 logging.error ("DCC Control: map_dcc_signal - Signal "+str(sig_id)+" - already has a DCC mapping") 

266 else: 

267 # Create a list of DCC addresses [address,state] to validate 

268 addresses = ( danger + proceed + caution + prelim_caution + flash_caution + 

269 flash_prelim_caution + LH1 + LH2 + RH1 + RH2 + MAIN + NONE ) 

270 # Add the Theatre route indicator addresses - these are the form [char,[[address,state],]] 

271 for theatre_state in THEATRE: 

272 addresses = addresses + theatre_state[1] 

273 # Add the subsidary signal DCC address into the list (this is a single DCC address) 

274 addresses = addresses + [[subsidary,True]] 

275 # Validate the DCC Addresses we have been given are either 0 (i.e. don't send anything) or 

276 # within the valid DCC accessory address range of 1 and 2047. 

277 addresses_valid = True 

278 for entry in addresses: 

279 if not isinstance(entry,list) or not len(entry) == 2: 

280 logging.error ("DCC Control: map_dcc_signal - Signal "+str(sig_id)+" - Invalid DCC command: "+str(entry)) 

281 addresses_valid = False 

282 elif not isinstance(entry[1],bool): 

283 logging.error ("DCC Control: map_dcc_signal - Signal "+str(sig_id)+" - Invalid DCC state: " +str(entry[1])) 

284 addresses_valid = False 

285 elif not isinstance(entry[0],int) or entry[0] < 0 or entry[0] > 2047: 

286 logging.error ("DCC Control: map_dcc_signal - Signal "+str(sig_id)+" - Invalid DCC address: "+str(entry[0])) 

287 addresses_valid = False 

288 elif dcc_address_mapping(entry[0]) is not None: 

289 logging.error ("DCC Control: map_dcc_signal - Signal "+str(sig_id)+" - DCC Address "+str(entry[0])+ 

290 " is already assigned to "+dcc_address_mapping(entry[0])[0]+" "+str(dcc_address_mapping(entry[0])[1])) 

291 addresses_valid = False 

292 # We now know if all the DCC addresses we have been given are valid 

293 if addresses_valid: 

294 logging.debug ("DCC Control - Creating DCC Address mapping for Colour Light Signal "+str(sig_id)) 

295 # Create the DCC Mapping entry for the signal 

296 new_dcc_mapping = { 

297 "mapping_type" : mapping_type.COLOUR_LIGHT, # Common to Colour_Light & Semaphore Mappings 

298 "auto_route_inhibit" : auto_route_inhibit, # Common to Colour_Light & Semaphore Mappings 

299 "main_subsidary" : subsidary, # Common to Colour_Light & Semaphore Mappings  

300 "THEATRE" : THEATRE, # Common to Colour_Light & Semaphore Mappings  

301 str(signals_common.signal_state_type.DANGER) : danger, # Specific to Colour_Light Mappings 

302 str(signals_common.signal_state_type.PROCEED) : proceed, # Specific to Colour_Light Mappings 

303 str(signals_common.signal_state_type.CAUTION) : caution, # Specific to Colour_Light Mappings 

304 str(signals_common.signal_state_type.CAUTION_APP_CNTL) : caution, # Specific to Colour_Light Mappings 

305 str(signals_common.signal_state_type.PRELIM_CAUTION) : prelim_caution, # Specific to Colour_Light Mappings 

306 str(signals_common.signal_state_type.FLASH_CAUTION) : flash_caution, # Specific to Colour_Light Mappings 

307 str(signals_common.signal_state_type.FLASH_PRELIM_CAUTION) : flash_prelim_caution, # Specific to Colour_Light Mappings 

308 str(signals_common.route_type.LH1) : LH1, # Specific to Colour_Light Mappings 

309 str(signals_common.route_type.LH2) : LH2, # Specific to Colour_Light Mappings 

310 str(signals_common.route_type.RH1) : RH1, # Specific to Colour_Light Mappings 

311 str(signals_common.route_type.RH2) : RH2, # Specific to Colour_Light Mappings 

312 str(signals_common.route_type.MAIN) : MAIN, # Specific to Colour_Light Mappings 

313 str(signals_common.route_type.NONE) : NONE } # Specific to Colour_Light Mappings 

314 dcc_signal_mappings[str(sig_id)] = new_dcc_mapping 

315 # Update the DCC mappings dictionary (note the key is an INTEGER) 

316 for entry in addresses: 

317 if entry[0] > 0 and entry[0] not in dcc_address_mappings.keys(): 

318 dcc_address_mappings[int(entry[0])] = ["Signal",sig_id] 

319 return() 

320 

321#---------------------------------------------------------------------------------------------------- 

322# Function to "map" a semaphore signal to the appropriate DCC addresses/commands using 

323# a simple one-to-one mapping of each signal arm to a single DCC accessory address (apart 

324# from the theatre route display where we send a sequence of DCC commands) 

325# Note we allow DCC addresses of zero to be specified (i.e. no mapping for that element) 

326#---------------------------------------------------------------------------------------------------- 

327 

328def map_semaphore_signal(sig_id:int, 

329 main_signal:int = 0, 

330 lh1_signal:int = 0, 

331 lh2_signal:int = 0, 

332 rh1_signal:int = 0, 

333 rh2_signal:int = 0, 

334 main_subsidary:int = 0, 

335 lh1_subsidary:int = 0, 

336 lh2_subsidary:int = 0, 

337 rh1_subsidary:int = 0, 

338 rh2_subsidary:int = 0, 

339 THEATRE = [["#", [[0,False],]],]): 

340 global dcc_signal_mappings 

341 global dcc_address_mappings 

342 # Do some basic validation on the parameters we have been given 

343 if not isinstance(sig_id,int) or sig_id < 1: 

344 logging.error ("DCC Control: map_semaphore_signal - Signal "+str(sig_id)+" - Signal ID must be a positive integer") 

345 elif sig_mapped(sig_id): 

346 logging.error ("DCC Control: map_semaphore_signal - Signal "+str(sig_id)+" - already has a DCC Address mapping") 

347 else: 

348 # Create a list of DCC addresses to validate 

349 addresses = [main_signal,main_subsidary,lh1_signal,lh1_subsidary,rh1_signal,rh1_subsidary, 

350 lh2_signal,lh2_subsidary,rh2_signal,rh2_subsidary] 

351 # Validate the DCC Addresses we have been given are either 0 (i.e. don't send anything) or 

352 # within the valid DCC accessory address range of 1 and 2047. 

353 addresses_valid = True 

354 for entry in addresses: 

355 if not isinstance(entry,int) or entry < 0 or entry > 2047: 

356 logging.error ("DCC Control: map_semaphore_signal - Signal "+str(sig_id)+" - Invalid DCC address: "+str(entry)) 

357 addresses_valid = False 

358 elif dcc_address_mapping(entry) is not None: 

359 logging.error ("DCC Control: map_semaphore_signal - Signal "+str(sig_id)+" - DCC Address "+str(entry)+ 

360 " is already assigned to "+dcc_address_mapping(entry)[0]+" "+str(dcc_address_mapping(entry)[1])) 

361 addresses_valid = False 

362 # Validate the Theatre route indicator addresses - these are the form [char,[address,state] 

363 for theatre_state in THEATRE: 

364 for entry in theatre_state[1]: 

365 if not isinstance(entry,list) or not len(entry) == 2: 

366 logging.error ("DCC Control: map_semaphore_signal - Signal "+str(sig_id)+" - Invalid DCC command: "+str(entry)) 

367 addresses_valid = False 

368 elif not isinstance(entry[1],bool): 

369 logging.error ("DCC Control: map_semaphore_signal - Signal "+str(sig_id)+" - Invalid DCC state: "+str(entry[1])) 

370 addresses_valid = False 

371 elif not isinstance(entry[0],int) or entry[0] < 0 or entry[0] > 2047: 

372 logging.error ("DCC Control: map_semaphore_signal - Signal "+str(sig_id)+" - Invalid DCC address "+str(entry[0])) 

373 addresses_valid = False 

374 elif dcc_address_mapping(entry[0]) is not None: 

375 logging.error ("DCC Control: map_semaphore_signal - Signal "+str(sig_id)+" - DCC Address "+str(entry[0])+ 

376 " is already assigned to "+dcc_address_mapping(entry[0])[0]+" "+str(dcc_address_mapping(entry[0])[1])) 

377 addresses_valid = False 

378 else: 

379 # Add to the list of addresses (so we can add to the mappings later on) 

380 addresses.append(entry[0]) 

381 # We now know if all the DCC addresses we have been given are valid 

382 if addresses_valid: 

383 logging.debug("Signal "+str(sig_id)+": Creating DCC Address mapping for a Semaphore Signal") 

384 # Create the DCC Mapping entry for the signal. 

385 new_dcc_mapping = { 

386 "mapping_type" : mapping_type.SEMAPHORE, # Common to Colour_Light & Semaphore Mappings 

387 "auto_route_inhibit" : False, # Common to Colour_Light & Semaphore Mappings 

388 "main_subsidary" : main_subsidary, # Common to Colour_Light & Semaphore Mappings  

389 "THEATRE" : THEATRE, # Common to Colour_Light & Semaphore Mappings 

390 "main_signal" : main_signal, # Specific to Semaphore Signal Mappings 

391 "lh1_signal" : lh1_signal, # Specific to Semaphore Signal Mappings 

392 "lh1_subsidary" : lh1_subsidary, # Specific to Semaphore Signal Mappings 

393 "lh2_signal" : lh2_signal, # Specific to Semaphore Signal Mappings 

394 "lh2_subsidary" : lh2_subsidary, # Specific to Semaphore Signal Mappings 

395 "rh1_signal" : rh1_signal, # Specific to Semaphore Signal Mappings 

396 "rh1_subsidary" : rh1_subsidary, # Common to both Semaphore and Colour Lights 

397 "rh2_signal" : rh2_signal, # Specific to Semaphore Signal Mappings 

398 "rh2_subsidary" : rh2_subsidary } # Finally save the DCC mapping into the dictionary of mappings  

399 dcc_signal_mappings[str(sig_id)] = new_dcc_mapping 

400 # Update the DCC mappings dictionary (note the key is an INTEGER) 

401 for entry in addresses: 

402 if entry > 0 and entry not in dcc_address_mappings.keys(): 

403 dcc_address_mappings[int(entry)] = ["Signal",sig_id] 

404 return() 

405 

406#---------------------------------------------------------------------------------------------------- 

407# Externally called unction to "map" a particular point object to a DCC address/command 

408# This is much simpler than the signals as we only need to map a signle DCC address for 

409# each point to be controlled - with an appropriate state (either switched or not_switched) 

410# Note we allow DCC addresses of zero to be specified (i.e. no mapping for that element) 

411#---------------------------------------------------------------------------------------------------- 

412 

413def map_dcc_point(point_id:int, address:int, state_reversed:bool=False): 

414 # Do some basic validation on the parameters we have been given 

415 if not isinstance(point_id,int) or point_id < 1: 

416 logging.error ("DCC Control: map_dcc_point - Point "+str(point_id)+" - Point ID must be a positive integer") 

417 elif point_mapped(point_id): 

418 logging.error ("DCC Control: map_dcc_point - Point "+str(point_id)+" - already has a DCC Address mapping") 

419 elif not isinstance(address,int) or address < 0 or address > 2047: 

420 logging.error ("DCC Control: map_dcc_point - Point "+str(point_id)+" - Invalid DCC address "+str(address)) 

421 elif not isinstance(state_reversed,bool): 

422 logging.error ("DCC Control: map_dcc_point - Point "+str(point_id)+" - Invalid state_reversed flag") 

423 elif dcc_address_mapping(address) is not None: 

424 logging.error ("DCC Control: map_dcc_point - Point "+str(point_id)+" - DCC Address "+str(address)+ 

425 " is already assigned to "+dcc_address_mapping(address)[0]+" "+str(dcc_address_mapping(address)[1])) 

426 else: 

427 logging.debug("Point "+str(point_id)+": Creating DCC Address mapping for Point") 

428 # Create the DCC Mapping entry for the point 

429 new_dcc_mapping = { 

430 "address" : address, 

431 "reversed" : state_reversed } 

432 dcc_point_mappings[str(point_id)] = new_dcc_mapping 

433 # Update the DCC mappings dictionary (note the key is an INTEGER) 

434 if address > 0: dcc_address_mappings[int(address)] = ["Point",point_id] 

435 return() 

436 

437#---------------------------------------------------------------------------------------------------- 

438# Function to send the appropriate DCC command to set the state of a DCC Point 

439#---------------------------------------------------------------------------------------------------- 

440 

441def update_dcc_point(point_id:int, state:bool): 

442 if point_mapped(point_id): 

443 logging.debug ("Point "+str(point_id)+": Looking up DCC commands to switch point") 

444 dcc_mapping = dcc_point_mappings[str(point_id)] 

445 if dcc_mapping["reversed"]: state = not state 

446 if dcc_mapping["address"] > 0: 

447 # Send the DCC commands to change the state 

448 pi_sprog_interface.send_accessory_short_event (dcc_mapping["address"],state) 

449 # Publish the DCC commands to a remote pi-sprog "node" via an external MQTT broker. 

450 # Commands will only be published if networking is configured and publishing is enabled 

451 publish_accessory_short_event(dcc_mapping["address"],state) 

452 return() 

453 

454#---------------------------------------------------------------------------------------------------- 

455# Function to send the appropriate DCC commands to set the state of a DCC Colour Light  

456# Signal. The commands to be sent will depend on the displayed aspect of the signal. 

457#---------------------------------------------------------------------------------------------------- 

458 

459def update_dcc_signal_aspects(sig_id:int, sig_state:signals_common.signal_state_type): 

460 if sig_mapped(sig_id): 

461 # Retrieve the DCC mappings for our signal and validate its the correct mapping 

462 # This function should only be called for Colour Light Signal Types 

463 dcc_mapping = dcc_signal_mappings[str(sig_id)] 

464 if dcc_mapping["mapping_type"] != mapping_type.COLOUR_LIGHT: 

465 logging.error ("Signal "+str(sig_id)+": Incorrect DCC Mapping Type for signal - Expecting a Colour Light signal") 

466 else: 

467 logging.debug ("Signal "+str(sig_id)+": Looking up DCC commands to change main signal aspect") 

468 for entry in dcc_mapping[str(sig_state)]: 

469 if entry[0] > 0: 

470 # Send the DCC commands to change the state via the serial port to the Pi-Sprog. 

471 # Note that the commands will only be sent if the pi-sprog interface is configured 

472 pi_sprog_interface.send_accessory_short_event(entry[0],entry[1]) 

473 # Publish the DCC commands to a remote pi-sprog "node" via an external MQTT broker. 

474 # Commands will only be published if networking is configured and publishing is enabled 

475 publish_accessory_short_event(entry[0],entry[1]) 

476 return() 

477 

478#---------------------------------------------------------------------------------------------------- 

479# Function to send the appropriate DCC commands to change a single element of a signal 

480# This function primarily used for semaphore signals where each signal "arm" is normally 

481# mapped to a single DCC address. Also used for the subsidary aspect of main colour light 

482# signals where this subsidary aspect is normally mapped to a single DCC Address 

483#---------------------------------------------------------------------------------------------------- 

484 

485def update_dcc_signal_element(sig_id:int, state:bool, element:str="main_subsidary"): 

486 if sig_mapped(sig_id): 486 ↛ 501line 486 didn't jump to line 501, because the condition on line 486 was never false

487 # Retrieve the DCC mappings for our signal and validate its the correct mapping 

488 # This function should only be called for anything other than the "main_subsidary" for Semaphore Signal Types 

489 dcc_mapping = dcc_signal_mappings[str(sig_id)] 

490 if element != "main_subsidary" and dcc_mapping["mapping_type"] != mapping_type.SEMAPHORE: 

491 logging.error ("Signal "+str(sig_id)+": Incorrect DCC Mapping Type for signal - Expecting a Semaphore signal") 

492 else: 

493 logging.debug ("Signal "+str(sig_id)+": Looking up DCC commands to change \'"+element+"\' ") 

494 if dcc_mapping[element] > 0: 

495 # Send the DCC commands to change the state via the serial port to the Pi-Sprog. 

496 # Note that the commands will only be sent if the pi-sprog interface is configured 

497 pi_sprog_interface.send_accessory_short_event(dcc_mapping[element],state) 

498 # Publish the DCC commands to a remote pi-sprog "node" via an external MQTT broker. 

499 # Commands will only be published if networking is configured and publishing is enabled 

500 publish_accessory_short_event(dcc_mapping[element],state) 

501 return() 

502 

503#---------------------------------------------------------------------------------------------------- 

504# Function to send the appropriate DCC commands to change the route indication 

505# Whether we need to send out DCC commands to actually change the route indication will 

506# depend on the DCC signal type and WHY we are changing the route indication - Some DCC 

507# signals automatically disable/enable the route indications when the signal is switched 

508# to/from DANGER - In this case we only need to command it when the ROUTE has been changed. 

509# For signals that don't do this, we need to send out commands every time we need to change 

510# the route display - i.e. on all Signal Changes (to/from DANGER) to enable/disable the 

511# display, and for all ROUTE changes when the signal is not at DANGER 

512#---------------------------------------------------------------------------------------------------- 

513 

514def update_dcc_signal_route(sig_id:int,route:signals_common.route_type, 

515 signal_change:bool=False,sig_at_danger:bool=False): 

516 if sig_mapped(sig_id): 516 ↛ 539line 516 didn't jump to line 539, because the condition on line 516 was never false

517 # Retrieve the DCC mappings for our signal and validate its the correct mapping 

518 # This function should only be called for Colour Light Signal Types 

519 dcc_mapping = dcc_signal_mappings[str(sig_id)] 

520 if dcc_mapping["mapping_type"] != mapping_type.COLOUR_LIGHT: 

521 logging.error ("Signal "+str(sig_id)+": Incorrect DCC Mapping Type for signal - Expecting a Colour Light signal") 

522 else: 

523 # Only send commands to enable/disable route if we need to: 

524 # All signals - Any route change when the signal is not at DANGER 

525 # Auto inhibit signals - additionally route changes when signal is at DANGER 

526 # Non auto inhibit signals - additionally all signal changes to/from DANGER 

527 if ( (dcc_mapping["auto_route_inhibit"] and not signal_change) or 

528 (not dcc_mapping["auto_route_inhibit"] and signal_change) or 

529 (not sig_at_danger and not signal_change) ): 

530 logging.debug ("Signal "+str(sig_id)+": Looking up DCC commands to change route display") 

531 for entry in dcc_mapping[str(route)]: 

532 if entry[0] > 0: 

533 # Send the DCC commands to change the state via the serial port to the Pi-Sprog. 

534 # Note that the commands will only be sent if the pi-sprog interface is configured 

535 pi_sprog_interface.send_accessory_short_event(entry[0],entry[1]) 

536 # Publish the DCC commands to a remote pi-sprog "node" via an external MQTT broker. 

537 # Commands will only be published if networking is configured and publishing is enabled 

538 publish_accessory_short_event(entry[0],entry[1]) 

539 return() 

540 

541#---------------------------------------------------------------------------------------------------- 

542# Function to send the appropriate DCC commands to change the Theatre indication 

543# Whether we need to send out DCC commands to actually change the route indication will 

544# depend on the DCC signal type and WHY we are changing the route indication - Some DCC 

545# signals automatically disable/enable the route indications when the signal is switched 

546# to/from DANGER - In this case we only need to command it when the ROUTE has been changed. 

547# For signals that don't do this, we need to send out commands every time we need to change 

548# the route display - i.e. on all Signal Changes (to/from DANGER) to enable/disable the 

549# display, and for all ROUTE changes when the signal is not at DANGER 

550#---------------------------------------------------------------------------------------------------- 

551 

552def update_dcc_signal_theatre(sig_id:int, character_to_display:str, 

553 signal_change:bool=False, sig_at_danger:bool=False): 

554 if sig_mapped(sig_id): 554 ↛ 577line 554 didn't jump to line 577, because the condition on line 554 was never false

555 # Retrieve the DCC mappings for our signal. We don't need to validate the mapping type 

556 # as Theatre route displays are supported by both Colour Light and Semaphore signal types  

557 dcc_mapping = dcc_signal_mappings[str(sig_id)] 

558 # Only send commands to enable/disable route if we need to: 

559 # All signals - Any route change when the signal is not at DANGER 

560 # Auto inhibit signals - additionally route changes when signal is at DANGER 

561 # Non auto inhibit signals - additionally all signal changes to/from DANGER 

562 if ( (dcc_mapping["auto_route_inhibit"] and not signal_change) or 

563 (not dcc_mapping["auto_route_inhibit"] and signal_change) or 

564 (not sig_at_danger and not signal_change) ): 

565 logging.debug ("Signal "+str(sig_id)+": Looking up DCC commands to change Theatre display") 

566 # Send the DCC commands to change the state if required 

567 for entry in dcc_mapping["THEATRE"]: 

568 if entry[0] == character_to_display: 

569 for command in entry[1]: 

570 if command[0] > 0: 

571 # Send the DCC commands to change the state via the serial port to the Pi-Sprog. 

572 # Note that the commands will only be sent if the pi-sprog interface is configured 

573 pi_sprog_interface.send_accessory_short_event(command[0],command[1]) 

574 # Publish the DCC commands to a remote pi-sprog "node" via an external MQTT broker. 

575 # Commands will only be published if networking is configured and publishing is enabled 

576 publish_accessory_short_event(command[0],command[1]) 

577 return() 

578 

579#---------------------------------------------------------------------------------------------------- 

580# Callback for handling received MQTT messages from a remote DCC-command-producer Node 

581# Note that this function will already be running in the main Tkinter thread 

582#---------------------------------------------------------------------------------------------------- 

583 

584def handle_mqtt_dcc_accessory_short_event(message): 

585 if "sourceidentifier" not in message.keys() or "dccaddress" not in message.keys() or "dccstate" not in message.keys(): 

586 logging.error ("DCC Control: Unhandled MQTT Message - "+str(message)) 

587 else: 

588 source_node = message["sourceidentifier"] 

589 dcc_address = message["dccaddress"] 

590 dcc_state = message["dccstate"] 

591 if dcc_state: 

592 logging.debug ("DCC Control: Received ASON command from \'"+source_node+"\' for DCC address: "+str(dcc_address)) 

593 else: 

594 logging.debug ("DCC Control: Received ASOF command from \'"+source_node+"\' for DCC address: "+str(dcc_address)) 

595 # Forward the received DCC command on to the Pi-Sprog Interface (for transmission on the DCC Bus) 

596 pi_sprog_interface.send_accessory_short_event(dcc_address,dcc_state) 

597 return() 

598 

599#---------------------------------------------------------------------------------------------------- 

600# Internal function for building and sending MQTT messages - but only if this 

601# particular node has been configured to publish DCC commands viathe mqtt broker 

602#---------------------------------------------------------------------------------------------------- 

603 

604def publish_accessory_short_event(address:int,active:bool): 

605 if publish_dcc_commands_to_mqtt_broker: 

606 data = {} 

607 data["dccaddress"] = address 

608 data["dccstate"] = active 

609 if active: log_message = "DCC Control: Publishing DCC command ASON with DCC address: "+str(address)+" to MQTT broker" 

610 else: log_message = "DCC Control: Publishing DCC command ASOF with DCC address: "+str(address)+" to MQTT broker" 

611 # Publish as "retained" messages so remote nodes that subscribe later will always pick up the latest state 

612 mqtt_interface.send_mqtt_message("dcc_accessory_short_events",0,data=data, 

613 log_message=log_message,subtopic = str(address),retain=True) 

614 return() 

615 

616#---------------------------------------------------------------------------------------------------- 

617# API function for deleting a DCC Point mapping and removing the DCC address 

618# associated with the point from the dcc_address_mappings. This is used by the 

619# schematic editor for deleting existing DCC mappings (before creating new ones) 

620#---------------------------------------------------------------------------------------------------- 

621 

622def delete_point_mapping(point_id:int): 

623 global dcc_point_mappings 

624 global dcc_address_mappings 

625 if not isinstance(point_id, int): 

626 logging.error("DCC Control: delete_point_mapping - Point "+str(point_id)+" - Point ID must be an integer") 

627 elif not point_mapped(point_id): 

628 logging.error("DCC Control: delete_point_mapping - Point "+str(point_id)+" - DCC Mapping does not exist") 

629 else: 

630 logging.debug("Point "+str(point_id)+": Deleting DCC Address mapping for Point") 

631 # Retrieve the DCC mapping address for the Point 

632 dcc_address = dcc_point_mappings[str(point_id)]["address"] 

633 # Remove the DCC address from the dcc_address_mappings dictionary (note the key is an INTEGER) 

634 if dcc_address in dcc_address_mappings.keys(): 

635 del dcc_address_mappings[int(dcc_address)] 

636 # Now delete the point mapping from the dcc_point_mappings dictionary 

637 del dcc_point_mappings[str(point_id)] 

638 return() 

639 

640#---------------------------------------------------------------------------------------------------- 

641# API function for deleting a DCC signal mapping and removing all DCC addresses 

642# associated with the signal from the dcc_address_mappings. This is used by the 

643# schematic editor for deleting existing DCC mappings (before creating new ones) 

644#---------------------------------------------------------------------------------------------------- 

645 

646def delete_signal_mapping(sig_id:int): 

647 global dcc_signal_mappings 

648 global dcc_address_mappings 

649 if not isinstance(sig_id, int): 

650 logging.error("DCC Control: delete_signal_mapping - Signal "+str(sig_id)+" - Signal ID must be an integer") 

651 elif not sig_mapped(sig_id): 

652 logging.error("DCC Control: delete_signal_mapping - Signal "+str(sig_id)+" - DCC Mapping does not exist") 

653 else: 

654 logging.debug("Signal "+str(sig_id)+": Deleting DCC Address mapping for signal") 

655 # Retrieve the DCC mappings for the signal and determine the mapping type 

656 dcc_signal_mapping = dcc_signal_mappings[str(sig_id)] 

657 # Colour Light Signal mappings 

658 if dcc_signal_mapping["mapping_type"] == mapping_type.COLOUR_LIGHT: 

659 # Compile a list of all DCC commands associated with the signal (aspects, feathers) 

660 # Note we don't need to add the 'CAUTION_APP_CNTL' list as this is the same as CAUTION 

661 dcc_command_list = [[dcc_signal_mapping["main_subsidary"],True]] 

662 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.signal_state_type.DANGER)]) 

663 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.signal_state_type.PROCEED)]) 

664 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.signal_state_type.CAUTION)]) 

665 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.signal_state_type.PRELIM_CAUTION)]) 

666 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.signal_state_type.FLASH_CAUTION)]) 

667 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.signal_state_type.FLASH_PRELIM_CAUTION)]) 

668 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.route_type.NONE)]) 

669 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.route_type.MAIN)]) 

670 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.route_type.LH1)]) 

671 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.route_type.LH2)]) 

672 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.route_type.RH1)]) 

673 dcc_command_list.extend(dcc_signal_mapping[str(signals_common.route_type.RH2)]) 

674 # Add the Theatre route indicator addresses - Each Route Element is [char,[[address,state],]] 

675 for theatre_route_element in dcc_signal_mapping["THEATRE"]: 

676 dcc_command_list.extend(theatre_route_element[1]) 

677 # List is now complete - remove all DCC addresses from the dcc_address_mappings dictionary 

678 # Note that the dictionary key is an INTEGER 

679 for dcc_command in dcc_command_list: 

680 if dcc_command[0] in dcc_address_mappings.keys(): 

681 del dcc_address_mappings[int(dcc_command[0])] 

682 # Semaphors Signal mappings 

683 elif dcc_signal_mapping["mapping_type"] == mapping_type.SEMAPHORE: 683 ↛ 705line 683 didn't jump to line 705, because the condition on line 683 was never false

684 # Compile a list of all DCC addresses associated with the signal (signal arms) 

685 dcc_address_list = [dcc_signal_mapping["main_signal"]] 

686 dcc_address_list.extend([dcc_signal_mapping["lh1_signal"]]) 

687 dcc_address_list.extend([dcc_signal_mapping["lh2_signal"]]) 

688 dcc_address_list.extend([dcc_signal_mapping["rh1_signal"]]) 

689 dcc_address_list.extend([dcc_signal_mapping["rh2_signal"]]) 

690 dcc_address_list.extend([dcc_signal_mapping["main_subsidary"]]) 

691 dcc_address_list.extend([dcc_signal_mapping["lh1_subsidary"]]) 

692 dcc_address_list.extend([dcc_signal_mapping["lh2_subsidary"]]) 

693 dcc_address_list.extend([dcc_signal_mapping["rh1_subsidary"]]) 

694 dcc_address_list.extend([dcc_signal_mapping["rh2_subsidary"]]) 

695 # Add the Theatre route indicator addresses - Each Route Element is [char,[[address,state],]] 

696 for theatre_route_element in dcc_signal_mapping["THEATRE"]: 

697 for dcc_command in theatre_route_element[1]: 

698 dcc_address_list.extend([dcc_command[0]]) 

699 # List is now complete - remove all DCC addresses from the dcc_address_mappings dictionary 

700 # Note that the dictionary key is an INTEGER 

701 for dcc_address in dcc_address_list: 

702 if dcc_address in dcc_address_mappings.keys(): 

703 del dcc_address_mappings[int(dcc_address)] 

704 # Now delete the signal mapping from the dcc_signal_mappings dictionary 

705 del dcc_signal_mappings[str(sig_id)] 

706 return() 

707 

708#---------------------------------------------------------------------------------------------------- 

709# API function to reset the published/subscribed DCC command feeds. This function is called by 

710# the editor on 'Apply' of the MQTT pub/sub configuration prior to applying the new configuration 

711# via the 'subscribe_to_dcc_command_feed' & 'set_node_to_publish_dcc_commands' functions. 

712#---------------------------------------------------------------------------------------------------- 

713 

714def reset_mqtt_configuration(): 

715 global publish_dcc_commands_to_mqtt_broker 

716 logging.debug("DCC Control: Resetting MQTT publish and subscribe configuration") 

717 publish_dcc_commands_to_mqtt_broker = False 

718 mqtt_interface.unsubscribe_from_message_type("dcc_accessory_short_events") 

719 return() 

720 

721#---------------------------------------------------------------------------------------------------- 

722# API Function to set this Signalling node to publish all DCC commands to remote MQTT 

723# nodes. This function is called by the editor on 'Apply' of the MQTT pub/sub configuration. 

724#---------------------------------------------------------------------------------------------------- 

725 

726def set_node_to_publish_dcc_commands (publish_dcc_commands:bool=False): 

727 global publish_dcc_commands_to_mqtt_broker 

728 if not isinstance(publish_dcc_commands, bool): 

729 logging.error("DCC Control: set_node_to_publish_dcc_commands - invalid publish_dcc_commands flag") 

730 else: 

731 if publish_dcc_commands: logging.debug("DCC Control: Configuring Application to publish DCC Commands to MQTT broker") 

732 else: logging.debug("DCC Control: Configuring Application NOT to publish DCC Commands to MQTT broker") 

733 publish_dcc_commands_to_mqtt_broker = publish_dcc_commands 

734 return() 

735 

736#---------------------------------------------------------------------------------------------------- 

737# API Function to "subscribe" to the published DCC command feed from other remote MQTT nodes 

738# This function is called by the editor on "Apply' of the MQTT pub/sub configuration. 

739#---------------------------------------------------------------------------------------------------- 

740 

741def subscribe_to_dcc_command_feed (*nodes:str): 

742 for node in nodes: 

743 if not isinstance(node, str): 

744 logging.error("DCC Control: subscribe_to_dcc_command_feed - invalid node "+str(node)) 

745 else: 

746 # For DCC addresses we need to subscribe to the optional Subtopics (with a wildcard) 

747 # as each DCC address will appear on a different topic from the remote MQTT node  

748 mqtt_interface.subscribe_to_mqtt_messages("dcc_accessory_short_events",node,0, 

749 handle_mqtt_dcc_accessory_short_event,subtopics=True) 

750 return() 

751 

752#####################################################################################################