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
« 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#----------------------------------------------------------------------------------------------------
170from . import signals_common
171from . import pi_sprog_interface
172from . import mqtt_interface
174import enum
175import logging
177#----------------------------------------------------------------------------------------------------
178# Global definitions
179#----------------------------------------------------------------------------------------------------
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
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 = {}
194# Define the Flag to control whether DCC Commands are published to the MQTT Broker
195publish_dcc_commands_to_mqtt_broker:bool = False
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 = {}
203#----------------------------------------------------------------------------------------------------
204# API function to return a dictionary of all DCC Address mappings (to signals/points)
205#----------------------------------------------------------------------------------------------------
207def get_dcc_address_mappings():
208 return(dcc_address_mappings)
210#----------------------------------------------------------------------------------------------------
211# API function to return an existing DCC address mapping if one exists (otherwise None)
212#----------------------------------------------------------------------------------------------------
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)
224#----------------------------------------------------------------------------------------------------
225# Internal function to test if a DCC mapping already exists for a signal
226#----------------------------------------------------------------------------------------------------
228def sig_mapped(sig_id:int):
229 return (str(sig_id) in dcc_signal_mappings.keys())
231#----------------------------------------------------------------------------------------------------
232# Internal function to test if a DCC mapping already exists for a point
233#----------------------------------------------------------------------------------------------------
235def point_mapped(point_id:int):
236 return (str(point_id) in dcc_point_mappings.keys())
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
437#----------------------------------------------------------------------------------------------------
438# Function to send the appropriate DCC command to set the state of a DCC Point
439#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
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#----------------------------------------------------------------------------------------------------
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()
752#####################################################################################################