Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/library/block_instruments.py: 89%
403 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 is used for creating and managing Block instruments. Both single line (bi directional)
3# and twin line instruments are supported. Sound files credited to https://www.soundjay.com/tos.html
4# -----------------------------------------------------------------------------------------------
5#
6# External API - classes and functions (used by the Schematic Editor):
7#
8# instrument_type (use when creating instruments)
9# instrument_type.single_line
10# instrument_type.double_line
11#
12# block_callback_type (tells the calling program what has triggered the callback)
13# block_callback_type. block_section_ahead_updated - The block section AHEAD has been updated
14# (i.e. the block section state of the linked block instrument)
15#
16# create_instrument - Creates an instrument and returns the "tag" for all tkinter canvas drawing objects
17# This allows the editor to move the point object on the schematic as required
18# Mandatory Parameters:
19# Canvas - The Tkinter Drawing canvas on which the instrument is to be displayed
20# inst_id:int - The local identifier to be used for the Block Instrument
21# inst_type:instrument_type - either instrument_type.single_line or instrument_type.double_line
22# x:int, y:int - Position of the instrument on the canvas (in pixels)
23# callback - The function to call on block section ahead updated events
24# Note that the callback function returns (item_id, callback type)
25# Optional Parameters:
26# bell_sound_file:str - The filename of the soundfile (in the local package resources
27# folder) to use for the bell sound (default "bell-ring-01.wav")
28# telegraph_sound_file:str - The filename of the soundfile (in the local package resources)
29# to use for the Telegraph key sound (default "telegraph-key-01.wav")
30# linked_to:int/str - the identifier for the "paired" block instrument - can be specified
31# either as an integer (representing the ID of a Block Instrument on the
32# the local schematic), or a string representing a Block Instrument
33# running on a remote node - see MQTT networking (default = None)
34#
35# instrument_exists(inst_id:int/str) - returns true if the Block Instrument 'exists' (either a block instrument
36# has been created on the local schematic or the block_instrument has been subscribed to via MQTT networking)
37#
38# update_linked_instrument(inst_id:int, linked_to:str) - updated the linked instrument in a block Instrument
39# configuration (updating the repeater on the new linked instrument as required)
40#
41# delete_instrument(inst_id:int) - To delete the specified Block Instrument from the schematic
42#
43# block_section_ahead_clear(inst_id:int) - Returns the state of the ASSOCIATED block instrument
44# (i.e. the linked instrument controlling the state of the block section ahead)
45#
46# The following API functions are for configuring the pub/sub of Block Instrument events. The functions are
47# called by the editor on 'Apply' of the MQTT settings. First, 'reset_mqtt_configuration' is called to clear down
48# the existing pub/sub configuration, followed by 'set_instruments_to_publish_state' (with the list of LOCAL Block
49# Instruments to publish) and 'subscribe_to_remote_instrument' for each REMOTE Block Instrument that has been subscribed.
50#
51# reset_mqtt_configuration() - Clears down the current Block Instrument pub/sub configuration
52#
53# set_instruments_to_publish_state(*sensor_ids:int) - Enable the publication of Block Instrument events.
54#
55# subscribe_to_remote_instrument(remote_id:str) - Subscribes to a remote Block Instrument
56#
57# Classes and functions used by the other library modules:
58#
59# handle_mqtt_instrument_updated_event(message) - called on receipt of a remote 'instrument_updated' event
60#
61# handle_mqtt_ring_section_bell_event(message) - called on receipt of a remote 'telegraph key' event
62#
63# ------------------------------------------------------------------------------------------
64# To use Block Instruments with full sound enabled (bell rings and telegraph key sounds) then
65# the 'simpleaudio' package will need to be installed. Note that for Windows it has a dependency
66# on Microsoft Visual C++ 14.0 or greater (so Visual Studio 2015 should be installed first)
67# If 'simpleaudio' is not installed then the software will function without sound.
68# ------------------------------------------------------------------------------------------
70from . import common
71from . import mqtt_interface
72from . import file_interface
73from typing import Union
74import tkinter as Tk
75import enum
76import os
77import logging
78import importlib.resources
80# -------------------------------------------------------------------------
81# We can only use audio for the block instruments if 'simpleaudio' is installed
82# Although this package is supported across different platforms, for Windows
83# it has a dependency on Visual C++ 14.0. As this is quite a faff to install I
84# haven't made audio a hard and fast dependency for the 'model_railway_signals'
85# package as a whole - its up to the user to install if required
86# -------------------------------------------------------------------------
88def is_simpleaudio_installed():
89 global simpleaudio
90 try:
91 import simpleaudio
92 return (True)
93 except Exception: pass
94 return (False)
95audio_enabled = is_simpleaudio_installed()
97# -------------------------------------------------------------------------
98# Classes used by external functions when calling create_instrument
99# -------------------------------------------------------------------------
101class instrument_type(enum.Enum):
102 single_line = 1
103 double_line = 2
105class block_callback_type(enum.Enum):
106 block_section_ahead_updated = 51 # The instrument has been updated
108# --------------------------------------------------------------------------------
109# Block Instruments are to be added to a global dictionary when created
110# --------------------------------------------------------------------------------
112instruments = {}
114# --------------------------------------------------------------------------------
115# Global variable to indicate whether a Bell Code window is already open or not
116# --------------------------------------------------------------------------------
118bell_code_hints_open = False
120# --------------------------------------------------------------------------------
121# Global list of block instruments to publish to the MQTT Broker
122# --------------------------------------------------------------------------------
124list_of_instruments_to_publish = []
126# --------------------------------------------------------------------------------
127# Internal Function to Open a window containing a list of common signal box bell
128# codes on the right click of the TELEGRAPH Button on any Block Instrument. The
129# window will always be displayed on top of the other Tkinter windows until closed.
130# Only one Telegraph Bell Codes window can be open at a time
131# --------------------------------------------------------------------------------
133# Function to close the Telegraph Bell Codes window (when "X" is clicked)
134def close_bell_code_hints(hints_window):
135 global bell_code_hints_open
136 bell_code_hints_open = False
137 hints_window.destroy()
138 return()
140# Function to Open the Telegraph Bell Codes window (right click of TELEGRAPH button)
141def open_bell_code_hints():
142 global bell_code_hints_open
143 if not bell_code_hints_open:
144 # List of common bell codes (additional codes can be added to the list as required)
145 bell_codes = []
146 bell_codes.append ([" 1"," Call attention"])
147 bell_codes.append ([" 2"," Train entering section"])
148 bell_codes.append ([" 2 - 3"," Is line clear for light engine"])
149 bell_codes.append ([" 2 - 2"," Is line clear for stopping freight train"])
150 bell_codes.append ([" 2 - 2 - 1"," Is line clear for empty coaching stock train"])
151 bell_codes.append ([" 3"," Is line clear for stopping freight train"])
152 bell_codes.append ([" 3 - 1"," Is line clear for stopping passenger train"])
153 bell_codes.append ([" 3 - 1 - 1"," Is line clear for express freight train"])
154 bell_codes.append ([" 4"," Is line clear for express passenger train"])
155 bell_codes.append ([" 4 - 1"," Is line clear for mineral or empty waggon train"])
156 bell_codes.append ([" 2 - 1"," Train arrived"])
157 bell_codes.append ([" 6"," Obstruction danger"])
158 hints_window = Tk.Toplevel(common.root_window)
159 hints_window.attributes('-topmost',True)
160 hints_window.title("Common signal box bell codes")
161 hints_window.protocol("WM_DELETE_WINDOW", lambda:close_bell_code_hints(hints_window))
162 bell_code_hints_open = True
163 for row, item1 in enumerate (bell_codes, start=1):
164 text_entry_box1 = Tk.Entry(hints_window,width=8)
165 text_entry_box1.insert(0,item1[0])
166 text_entry_box1.grid(row=row,column=1)
167 text_entry_box1.config(state='disabled')
168 text_entry_box1.config({'disabledbackground':'white'})
169 text_entry_box1.config({'disabledforeground':'black'})
170 text_entry_box2 = Tk.Entry(hints_window,width=40)
171 text_entry_box2.insert(0,item1[1])
172 text_entry_box2.grid(row=row,column=2)
173 text_entry_box2.config(state='disabled')
174 text_entry_box2.config({'disabledbackground':'white'})
175 text_entry_box2.config({'disabledforeground':'black'})
176 return()
178# --------------------------------------------------------------------------------
179# API Function to check if a Block Instrument exists in the list of Instruments
180# Used in most externally-called functions to validate the Block instrument ID
181# Note the function will take in either local or (subscribed to) remote IDs
182# --------------------------------------------------------------------------------
184def instrument_exists(inst_id:Union[int,str]):
185 if not isinstance(inst_id, int) and not isinstance(inst_id, str):
186 logging.error("Instrument "+str(inst_id)+": instrument_exists - Instrument ID must be an int or str")
187 instrument_exists = False
188 else:
189 instrument_exists = str(inst_id) in instruments.keys()
190 return(instrument_exists)
192# --------------------------------------------------------------------------------
193# Internal Callbacks for handling button push events
194# --------------------------------------------------------------------------------
196def occup_button_event(inst_id:int):
197 logging.info ("Instrument "+str(inst_id)+": Occup button event ***********************************************")
198 set_section_occupied(inst_id)
199 return()
201def clear_button_event(inst_id:int):
202 logging.info ("Instrument "+str(inst_id)+": Clear button event ***********************************************")
203 set_section_clear(inst_id)
204 return()
206def blocked_button_event(inst_id:int):
207 logging.info ("Instrument "+str(inst_id)+": Blocked button event *********************************************")
208 set_section_blocked(inst_id)
209 return()
211def telegraph_key_button(inst_id:int):
212 logging.debug ("Instrument "+str(inst_id)+": Telegraph key operated ************************************")
213 # Provide a visual indication of the key being pressed
214 instruments[str(inst_id)]["bellbutton"].config(relief="sunken")
215 common.root_window.after(10,lambda:instruments[str(inst_id)]["bellbutton"].config(relief="raised"))
216 # Sound the "clack" of the telegraph key - We put exception handling around this as the function can raise
217 # exceptions if you try to play too many sounds simultaneously (if the button is clicked too quickly/frequently)
218 if instruments[str(inst_id)]["telegraphsound"] is not None: 218 ↛ 224line 218 didn't jump to line 224, because the condition on line 218 was never false
219 try: instruments[str(inst_id)]["telegraphsound"].play()
220 except: pass
221 # If linked to another instrument then call the function to ring the bell on the other instrument or
222 # Publish the "bell ring event" to the broker (for other nodes to consume). Note that events will only
223 # be published if the MQTT interface has been configured and we are connected to the broker
224 if instruments[str(inst_id)]["linkedto"].isdigit(): ring_section_bell(instruments[str(inst_id)]["linkedto"])
225 elif instruments[str(inst_id)]["linkedto"] != "": send_mqtt_ring_section_bell_event(inst_id)
226 return()
228# --------------------------------------------------------------------------------
229# Internal Function to receive bell rings from another instrument and
230# sound a bell "ting" on the local instrument (with a visual indication)
231# --------------------------------------------------------------------------------
233def reset_telegraph_button (inst_id:int):
234 if instrument_exists(inst_id): instruments[str(inst_id)]["bellbutton"].config(bg="black")
236def ring_section_bell(inst_id:int):
237 logging.debug ("Instrument "+str(inst_id)+": Ringing Bell")
238 # Provide a visual indication and sound the bell (not if shutdown has been initiated)
239 if not common.shutdown_initiated: 239 ↛ 247line 239 didn't jump to line 247, because the condition on line 239 was never false
240 instruments[str(inst_id)]["bellbutton"].config(bg="yellow")
241 common.root_window.after(100,lambda:reset_telegraph_button(inst_id))
242 # Sound the Bell - We put exception handling around this as I've seen this function raise exceptions
243 # if you try to play too many sounds simultaneously (if the button is clicked too quickly/frequently)
244 if instruments[str(inst_id)]["bellsound"] is not None: 244 ↛ 247line 244 didn't jump to line 247, because the condition on line 244 was never false
245 try: instruments[str(inst_id)]["bellsound"].play()
246 except: pass
247 return()
249# --------------------------------------------------------------------------------
250# API callback function for handling received MQTT messages from a remote instrument
251# Note that this function will already be running in the main Tkinter thread
252# --------------------------------------------------------------------------------
254def handle_mqtt_instrument_updated_event(message):
255 if ("sourceidentifier" not in message.keys() or "sectionstate" not in message.keys() or
256 "instrumentid" not in message.keys() or not instrument_exists(message["sourceidentifier"]) or
257 mqtt_interface.split_remote_item_identifier(message["instrumentid"]) is None):
258 logging.warning("Instruments: handle_mqtt_instrument_updated_event - Unhandled MQTT message - "+str(message))
259 else:
260 node_id, inst_id = mqtt_interface.split_remote_item_identifier(message["instrumentid"])
261 if instrument_exists(inst_id):
262 logging.info("Instrument "+str(inst_id)+": State update from linked instrument "+
263 message["sourceidentifier"]+" ********************")
264 if message["sectionstate"] == True: set_repeater_clear(inst_id)
265 elif message["sectionstate"] == False: set_repeater_occupied(inst_id)
266 elif message["sectionstate"] == None: set_repeater_blocked(inst_id)
267 # Note that if the specified block instrument does not exist then we fail silently for the instrument updated event.
268 # This could arise from either layout misconfiguration (a non existant 'linked instrument' specified for the Block
269 # instrument on the oither network node), or the race condition that will arise when loading the layouts (one of the
270 # layouts will always be created first and will therefore end up sending the initial block instrument state message
271 # before the linked instrument (on the other layout) has been created. We therefore accept this as a limitation.
272 return()
274def handle_mqtt_ring_section_bell_event(message):
275 if ("sourceidentifier" not in message.keys() or "instrumentid" not in message.keys() or not instrument_exists(message["sourceidentifier"])
276 or mqtt_interface.split_remote_item_identifier(message["instrumentid"]) is None):
277 logging.warning("Instruments: handle_mqtt_ring_section_bell_event - Unhandled MQTT message - "+str(message))
278 else:
279 node_id, inst_id = mqtt_interface.split_remote_item_identifier(message["instrumentid"])
280 if instrument_exists(inst_id):
281 logging.debug("Instrument "+str(inst_id)+": Telegraph key event from linked instrument"+
282 message["sourceidentifier"]+" ********************")
283 ring_section_bell(inst_id)
284 # Note that if the specified block instrument does not exist then we fail silently for the ring section bell event.
285 # This could arise from either layout misconfiguration (a non existant 'linked instrument' specified for the Block
286 # instrument on the oither network node), or the race condition that will arise when loading the layouts (one of the
287 # layouts will always be created first and will therefore end up sending the initial block instrument state message
288 # before the linked instrument (on the other layout) has been created. We therefore accept this as a limitation.
289 return()
291# --------------------------------------------------------------------------------
292# Internal functions for building and publishing MQTT messages (to a remote instrument)
293# --------------------------------------------------------------------------------
295def send_mqtt_instrument_updated_event(inst_id:int):
296 if inst_id in list_of_instruments_to_publish:
297 data = {}
298 data["instrumentid"] = instruments[str(inst_id)]["linkedto"]
299 data["sectionstate"] = instruments[str(inst_id)]["sectionstate"]
300 log_message = "Instrument "+str(inst_id)+": Publishing instrument state to MQTT Broker"
301 # Publish as "retained" messages so remote items that subscribe later will always pick up the latest state
302 mqtt_interface.send_mqtt_message("instrument_updated_event", inst_id, data=data, log_message=log_message, retain=True)
303 return()
305def send_mqtt_ring_section_bell_event(inst_id:int):
306 if inst_id in list_of_instruments_to_publish:
307 data = {}
308 data["instrumentid"] = instruments[str(inst_id)]["linkedto"]
309 log_message = "Instrument "+str(inst_id)+": Publishing telegraph key event to MQTT Broker"
310 # These are transitory events so we do not publish as "retained" messages (if they get missed, they get missed)
311 mqtt_interface.send_mqtt_message("instrument_telegraph_event", inst_id, data=data, log_message=log_message, retain=False)
312 return()
314# --------------------------------------------------------------------------------
315# Internal Function to set the repeater indicator (linked to another block section)
316# to BLOCKED - if its a single line instrument then we set the main indication
317# --------------------------------------------------------------------------------
319def set_repeater_blocked(inst_id:int,make_callback:bool=True):
320 global instruments
321 if instruments[str(inst_id)]["repeaterstate"] is not None:
322 logging.info ("Instrument "+str(inst_id)+": Changing block section repeater to LINE BLOCKED")
323 # Set the internal repeater state and the repeater indicator to BLOCKED
324 instruments[str(inst_id)]["repeaterstate"] = None
325 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["repeatindicatoroccup"],state = "hidden")
326 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["repeatindicatorclear"],state = "hidden")
327 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["repeatindicatorblock"],state = "normal")
328 # For single line instruments we set the local instrument buttons to mirror the remote instrument
329 # and also enable the local buttons (as the remote instrument has been returned to LINE BLOCKED)
330 if instruments[str(inst_id)]["insttype"] == instrument_type.single_line:
331 instruments[str(inst_id)]["blockbutton"].config(state="normal",relief="sunken",bg=common.bgsunken)
332 instruments[str(inst_id)]["clearbutton"].config(state="normal",relief="raised",bg=common.bgraised)
333 instruments[str(inst_id)]["occupbutton"].config(state="normal",relief="raised",bg=common.bgraised)
334 # Make an external callback (if one was specified) to notify that the block section AHEAD has been updated
335 # This enables full block section interlocking to be implemented for the starter signal in OUR block section
336 if make_callback: 336 ↛ 338line 336 didn't jump to line 338, because the condition on line 336 was never false
337 instruments[str(inst_id)]["extcallback"] (inst_id,block_callback_type.block_section_ahead_updated)
338 return ()
340# --------------------------------------------------------------------------------
341# Internal Function to set the repeater indicator (linked to another block section)
342# to LINE CLEAR - if its a single line instrument then we set the main indication
343# --------------------------------------------------------------------------------
345def set_repeater_clear(inst_id:int,make_callback:bool=True):
346 global instruments
347 if instruments[str(inst_id)]["repeaterstate"] != True:
348 logging.info ("Instrument "+str(inst_id)+": Changing block section repeater to LINE CLEAR")
349 # Set the internal repeater state and the repeater indicator to CLEAR
350 instruments[str(inst_id)]["repeaterstate"] = True
351 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["repeatindicatoroccup"],state = "hidden")
352 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["repeatindicatorclear"],state = "normal")
353 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["repeatindicatorblock"],state = "hidden")
354 # For single line instruments we set the local instrument buttons to mirror the remote instrument
355 # and also inhibit the local buttons (until the remote instrument is returned to LINE BLOCKED)
356 if instruments[str(inst_id)]["insttype"] == instrument_type.single_line:
357 instruments[str(inst_id)]["blockbutton"].config(state="disabled",relief="raised",bg=common.bgraised)
358 instruments[str(inst_id)]["clearbutton"].config(state="disabled",relief="sunken",bg=common.bgsunken)
359 instruments[str(inst_id)]["occupbutton"].config(state="disabled",relief="raised",bg=common.bgraised)
360 # Make an external callback (if one was specified) to notify that the block section AHEAD has been updated
361 # This enables full block section interlocking to be implemented for the starter signal in OUR block section
362 if make_callback:
363 instruments[str(inst_id)]["extcallback"] (inst_id,block_callback_type.block_section_ahead_updated)
364 return ()
366# --------------------------------------------------------------------------------
367# Internal Function to set the repeater indicator (linked to another block section)
368# to OCCUPIED - if its a single line instrument then we set the main indication
369# --------------------------------------------------------------------------------
371def set_repeater_occupied(inst_id:int,make_callback:bool=True):
372 global instruments
373 if instruments[str(inst_id)]["repeaterstate"] != False:
374 logging.info ("Instrument "+str(inst_id)+": Changing block section repeater to TRAIN ON LINE")
375 # Set the internal repeater state and the repeater indicator to OCCUPIED
376 instruments[str(inst_id)]["repeaterstate"] = False
377 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["repeatindicatoroccup"],state = "normal")
378 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["repeatindicatorclear"],state = "hidden")
379 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["repeatindicatorblock"],state = "hidden")
380 # For single line instruments we set the local instrument buttons to mirror the remote instrument
381 # and also inhibit the local buttons (until the remote instrument is returned to LINE BLOCKED)
382 if instruments[str(inst_id)]["insttype"] == instrument_type.single_line:
383 instruments[str(inst_id)]["blockbutton"].config(state="disabled",relief="raised",bg=common.bgraised)
384 instruments[str(inst_id)]["clearbutton"].config(state="disabled",relief="raised",bg=common.bgraised)
385 instruments[str(inst_id)]["occupbutton"].config(state="disabled",relief="sunken",bg=common.bgsunken)
386 # Make an external callback (if one was specified) to notify that the block section AHEAD has been updated
387 # This enables full block section interlocking to be implemented for the starter signal in OUR block section
388 if make_callback:
389 instruments[str(inst_id)]["extcallback"] (inst_id,block_callback_type.block_section_ahead_updated)
390 return ()
392# --------------------------------------------------------------------------------
393# Internal Function to set the main block section indicator to BLOCKED
394# called when the "BLOCKED" button is clicked on the local block instrument
395# Also called for single-line block instruments (without a repeater display)
396# following a state change of the linked remote block instrument
397# --------------------------------------------------------------------------------
399def set_section_blocked(inst_id:int,update_remote_instrument:bool=True):
400 global instruments
401 if instruments[str(inst_id)]["sectionstate"] is not None:
402 logging.info ("Instrument "+str(inst_id)+": Changing block section indicator to LINE BLOCKED")
403 # Set the state of the buttons accordingly
404 instruments[str(inst_id)]["blockbutton"].config(relief="sunken",bg=common.bgsunken)
405 instruments[str(inst_id)]["clearbutton"].config(relief="raised",bg=common.bgraised)
406 instruments[str(inst_id)]["occupbutton"].config(relief="raised",bg=common.bgraised)
407 # Set the internal state of the block instrument and the local indicator
408 instruments[str(inst_id)]["sectionstate"] = None
409 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["myindicatoroccup"],state = "hidden")
410 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["myindicatorclear"],state = "hidden")
411 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["myindicatorblock"],state = "normal")
412 # If linked to another instrument then update the repeater indicator on the other instrument or
413 # Publish the initial state to the broker (for other nodes to consume). Note that state will only
414 # be published if the MQTT interface has been configured and we are connected to the broker
415 if update_remote_instrument: refresh_linked_instrument(inst_id, instruments[str(inst_id)]["linkedto"])
416 return ()
418# --------------------------------------------------------------------------------
419# Internal Function to set the main block section indicator to CLEAR
420# called when the "CLEAR" button is clicked on the local block instrument
421# Also called for single-line block instruments (without a repeater display)
422# following a state change of the linked remote block instrument
423# --------------------------------------------------------------------------------
425def set_section_clear(inst_id:int,update_remote_instrument:bool=True):
426 global instruments
427 if instruments[str(inst_id)]["sectionstate"] != True:
428 logging.info ("Instrument "+str(inst_id)+": Changing block section indicator to LINE CLEAR")
429 # Set the state of the buttons accordingly
430 instruments[str(inst_id)]["blockbutton"].config(relief="raised",bg=common.bgraised)
431 instruments[str(inst_id)]["clearbutton"].config(relief="sunken",bg=common.bgsunken)
432 instruments[str(inst_id)]["occupbutton"].config(relief="raised",bg=common.bgraised)
433 # Set the internal state of the block instrument and the local indicator
434 instruments[str(inst_id)]["sectionstate"] = True
435 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["myindicatoroccup"],state = "hidden")
436 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["myindicatorclear"],state = "normal")
437 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["myindicatorblock"],state = "hidden")
438 # If linked to another instrument then update the repeater indicator on the other instrument or
439 # Publish the initial state to the broker (for other nodes to consume). Note that state will only
440 # be published if the MQTT interface has been configured and we are connected to the broker
441 if update_remote_instrument: refresh_linked_instrument(inst_id, instruments[str(inst_id)]["linkedto"])
442 return ()
444# --------------------------------------------------------------------------------
445# Internal Function to set the main block section indicator to OCCUPIED
446# called when the "OCCUP" button is clicked on the local block instrument
447# Also called for single-line block instruments (without a repeater display)
448# following a state change of the linked remote block instrument
449# --------------------------------------------------------------------------------
451def set_section_occupied(inst_id:int,update_remote_instrument:bool=True):
452 global instruments
453 if instruments[str(inst_id)]["sectionstate"] != False:
454 logging.info ("Instrument "+str(inst_id)+": Changing block section indicator to TRAIN ON LINE")
455 # Set the state of the buttons accordingly
456 instruments[str(inst_id)]["blockbutton"].config(relief="raised",bg=common.bgraised)
457 instruments[str(inst_id)]["clearbutton"].config(relief="raised",bg=common.bgraised)
458 instruments[str(inst_id)]["occupbutton"].config(relief="sunken",bg=common.bgsunken)
459 # Set the internal state of the block instrument and the local indicator
460 instruments[str(inst_id)]["sectionstate"] = False
461 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["myindicatoroccup"],state = "normal")
462 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["myindicatorclear"],state = "hidden")
463 instruments[str(inst_id)]["canvas"].itemconfigure(instruments[str(inst_id)]["myindicatorblock"],state = "hidden")
464 # If linked to another instrument then update the repeater indicator on the other instrument or
465 # Publish the initial state to the broker (for other nodes to consume). Note that state will only
466 # be published if the MQTT interface has been configured and we are connected to the broker
467 if update_remote_instrument: refresh_linked_instrument(inst_id, instruments[str(inst_id)]["linkedto"])
468 return ()
470# --------------------------------------------------------------------------------
471# Internal function to update the REPEATER display on a linked block instrument
472# --------------------------------------------------------------------------------
474def refresh_linked_instrument(inst_id:int, linked_to:str):
475 # Block State is as follows: True = Line Clear, False = Train On Line, None = Line Blocked
476 if instruments[str(inst_id)]["sectionstate"] == True:
477 if linked_to.isdigit() and instrument_exists(linked_to): set_repeater_clear(linked_to)
478 elif linked_to != "": send_mqtt_instrument_updated_event(inst_id)
479 elif instruments[str(inst_id)]["sectionstate"] == False:
480 if linked_to.isdigit() and instrument_exists(linked_to): set_repeater_occupied(linked_to)
481 elif linked_to != "": send_mqtt_instrument_updated_event(inst_id)
482 else:
483 if linked_to.isdigit() and instrument_exists(linked_to): set_repeater_blocked(linked_to)
484 elif linked_to != "": send_mqtt_instrument_updated_event(inst_id)
485 return()
487# --------------------------------------------------------------------------------
488# Internal function to create the Indicator component of a Block Instrument
489# --------------------------------------------------------------------------------
491def create_block_indicator(canvas:int, x:int, y:int, canvas_tag):
492 canvas.create_rectangle (x-40, y-10, x+40, y+45, fill="gray90", outline='black', width=1, tags=canvas_tag)
493 canvas.create_arc(x-40, y-40, x+40, y+40, fill='tomato', outline='black', start=-150, extent=40, width=0, tags=canvas_tag)
494 canvas.create_arc(x-40, y-40, x+40, y+40, fill='yellow', outline='black', start=-110, extent=40, width=0, tags=canvas_tag)
495 canvas.create_arc(x-40, y-40, x+40, y+40, fill='green yellow', outline='black', start=-70, extent=40, width=0, tags=canvas_tag)
496 canvas.create_arc(x-20, y-20, x+20, y+20, fill='gray90', outline="gray90", start=-155, extent=130, width=0, tags=canvas_tag)
497 canvas.create_oval(x-5, y-5, x+5, y+5, fill='black', outline="black", width=0, tags=canvas_tag)
498 block = canvas.create_line (x+0, y-5, x+0, y + 35, fill='black', width = 3, state='normal', tags=canvas_tag)
499 clear = canvas.create_line (x-3, y-3, x+25, y + 25, fill='black', width = 3, state='hidden', tags=canvas_tag)
500 occup = canvas.create_line (x+3, y-3, x-25, y + 25, fill='black', width = 3 , state='hidden', tags=canvas_tag)
501 return (block, clear, occup)
503# --------------------------------------------------------------------------------
504# Internal function to load a specified audio file for the bell / telegraph key sounds.
505# If these fail to load for any reason then no sounds will be produced on these events
506# If the filename isn't fully qualified then it assume a file in the resources folder
507# --------------------------------------------------------------------------------
509def load_audio_file(audio_filename):
510 audio_object = None
511 if os.path.split(audio_filename)[1] == audio_filename:
512 try:
513 with importlib.resources.path ('model_railway_signals.library.resources',audio_filename) as audio_file:
514 audio_object = simpleaudio.WaveObject.from_wave_file(str(audio_file))
515 except Exception as exception:
516 Tk.messagebox.showerror(parent=common.root_window, title="Load Error",
517 message="Error loading audio resource file '"+str(audio_filename)+"'")
518 logging.error("Block Instruments: Error loading audio resource file '"+str(audio_filename)+"'"+
519 " \nReported Exception: "+str(exception))
520 else:
521 try:
522 audio_object = simpleaudio.WaveObject.from_wave_file(str(audio_filename))
523 except Exception as exception:
524 Tk.messagebox.showerror(parent=common.root_window, title="Load Error",
525 message="Error loading audio file '"+str(audio_filename)+"'")
526 logging.error("Block Instruments: Error loading audio file '"+str(audio_filename)+"'"+
527 " \nReported Exception: "+str(exception))
528 return(audio_object)
530# --------------------------------------------------------------------------------
531# Public API function to create a Block Instrument (drawing objects and internal state)
532# --------------------------------------------------------------------------------
534def create_instrument (canvas, inst_id:int, inst_type:instrument_type, x:int, y:int, callback,
535 bell_sound_file:str = "bell-ring-01.wav",
536 telegraph_sound_file:str = "telegraph-key-01.wav",
537 linked_to:str = ""):
538 global instruments
539 # Set a unique 'tag' to reference the tkinter drawing objects
540 canvas_tag = "instrument"+str(inst_id)
541 # Validate the parameters we have been given as this is a library API function
542 if not isinstance(inst_id, int) or inst_id < 1:
543 logging.error("Instrument "+str(inst_id)+": create_instrument - Instrument ID must be a positive integer")
544 elif instrument_exists(inst_id):
545 logging.error("Instrument "+str(inst_id)+": create_instrument - Instrument ID already exists")
546 elif not isinstance(linked_to, str):
547 logging.error("Instrument "+str(inst_id)+": create_instrument - Linked Instrument ID must be a str")
548 elif linked_to == str(inst_id): 548 ↛ 549line 548 didn't jump to line 549, because the condition on line 548 was never true
549 logging.error("Instrument "+str(inst_id)+": create_instrument - Linked Instrument ID is the same as the Instrument ID")
550 elif linked_to !="" and not linked_to.isdigit() and mqtt_interface.split_remote_item_identifier(linked_to) is None:
551 logging.error("Instrument "+str(inst_id)+": create_instrument - Remote identifier for linked instrument is invalid format")
552 elif inst_type not in instrument_type:
553 logging.error("Instrument "+str(inst_id)+": create_instrument - Invalid Instrument Type specified")
554 else:
555 logging.debug("Instrument "+str(inst_id)+": Creating library object on the schematic")
556 # Validate the linked instrument config - this won't prevent the instrument being created
557 # but does raise warnings for potential misconfigurations (that won't break the system)
558 validate_linked_instrument(inst_id, linked_to)
559 # Create the Instrument background - this will vary in size depending on single or double line
560 if inst_type == instrument_type.single_line:
561 canvas.create_rectangle (x-48, y-18, x+48, y+120, fill = "saddle brown",tags=canvas_tag)
562 else:
563 canvas.create_rectangle (x-48, y-73, x+48, y+120, fill = "saddle brown",tags=canvas_tag)
564 # Create the button objects and their callbacks
565 occup_button = Tk.Button (canvas, text="OCCUP", padx=common.xpadding, pady=common.ypadding, 565 ↛ exitline 565 didn't jump to the function exit
566 state="normal", relief="raised", font=('Courier',common.fontsize,"normal"),
567 bg=common.bgraised, command = lambda:occup_button_event(inst_id))
568 clear_button = Tk.Button (canvas, text="CLEAR", padx=common.xpadding, pady=common.ypadding, 568 ↛ exitline 568 didn't jump to the function exit
569 state="normal", relief="raised", font=('Courier',common.fontsize,"normal"),
570 bg=common.bgraised, command = lambda:clear_button_event(inst_id))
571 block_button = Tk.Button (canvas, text="LINE BLOCKED", padx=common.xpadding, pady=common.ypadding, 571 ↛ exitline 571 didn't jump to the function exit
572 state="normal", relief="sunken", font=('Courier',common.fontsize,"normal"),
573 bg=common.bgsunken, command = lambda:blocked_button_event(inst_id))
574 bell_button = Tk.Button (canvas, text="TELEGRAPH", padx=common.xpadding, pady=common.ypadding, 574 ↛ exitline 574 didn't jump to the function exit
575 state="normal", relief="raised", font=('Courier',common.fontsize,"normal"),
576 bg="black", fg="white", activebackground="black", activeforeground="white",
577 command = lambda:telegraph_key_button(inst_id))
578 # Bind a right click on the Telegraph button to open the bell code hints
579 bell_button.bind('<Button-2>', lambda event:open_bell_code_hints()) 579 ↛ exitline 579 didn't run the lambda on line 579
580 bell_button.bind('<Button-3>', lambda event:open_bell_code_hints()) 580 ↛ exitline 580 didn't run the lambda on line 580
581 # Create the windows (on the canvas) for the buttons
582 canvas.create_window(x, y+70, window=occup_button, anchor=Tk.SE, tags=canvas_tag)
583 canvas.create_window(x, y+70, window=clear_button, anchor=Tk.SW, tags=canvas_tag)
584 canvas.create_window(x, y+70, window=block_button, anchor=Tk.N, tags=canvas_tag)
585 canvas.create_window(x, y+95, window=bell_button, anchor=Tk.N, tags=canvas_tag)
586 # Create the main block section indicator for our instrument
587 my_ind_block, my_ind_clear, my_ind_occup = create_block_indicator(canvas, x, y, canvas_tag)
588 # If this is a double line indicator then create the repeater indicator
589 # For a single line indicator, we use the main indicator as the repeater
590 if inst_type == instrument_type.single_line:
591 rep_ind_block, rep_ind_clear, rep_ind_occup = my_ind_block, my_ind_clear, my_ind_occup
592 else:
593 rep_ind_block, rep_ind_clear, rep_ind_occup = create_block_indicator(canvas, x, y-55, canvas_tag)
594 # Try to Load the specified audio files for the bell rings and telegraph key if audio is enabled
595 # if these fail to load for any reason then no sounds will be produced on these events
596 if audio_enabled: 596 ↛ 600line 596 didn't jump to line 600, because the condition on line 596 was never false
597 bell_audio = load_audio_file(bell_sound_file)
598 telegraph_audio = load_audio_file(telegraph_sound_file)
599 else:
600 logging.warning ("Instruments - Audio is not enabled - To enable: 'python3 -m pip install simpleaudio'")
601 bell_audio = None
602 telegraph_audio = None
603 # Create the dictionary of elements that we need to track
604 instruments[str(inst_id)] = {}
605 instruments[str(inst_id)]["canvas"] = canvas # Tkinter drawing canvas
606 instruments[str(inst_id)]["extcallback"] = callback # External callback to make
607 instruments[str(inst_id)]["linkedto"] = linked_to # Id of the instrument this one is linked to
608 instruments[str(inst_id)]["insttype"] = inst_type # Block Instrument Type
609 instruments[str(inst_id)]["sectionstate"] = None # State of this instrument (None = "BLOCKED")
610 instruments[str(inst_id)]["repeaterstate"] = None # State of repeater display (None = "BLOCKED")
611 instruments[str(inst_id)]["blockbutton"] = block_button # Tkinter Button object
612 instruments[str(inst_id)]["clearbutton"] = clear_button # Tkinter Button object
613 instruments[str(inst_id)]["occupbutton"] = occup_button # Tkinter Button object
614 instruments[str(inst_id)]["bellbutton"] = bell_button # Tkinter Button object
615 instruments[str(inst_id)]["myindicatorclear"] = my_ind_clear # Tkinter Drawing object
616 instruments[str(inst_id)]["myindicatoroccup"] = my_ind_occup # Tkinter Drawing object
617 instruments[str(inst_id)]["myindicatorblock"] = my_ind_block # Tkinter Drawing object
618 instruments[str(inst_id)]["repeatindicatorclear"] = rep_ind_clear # Tkinter Drawing object
619 instruments[str(inst_id)]["repeatindicatoroccup"] = rep_ind_occup # Tkinter Drawing object
620 instruments[str(inst_id)]["repeatindicatorblock"] = rep_ind_block # Tkinter Drawing object
621 instruments[str(inst_id)]["telegraphsound"] = telegraph_audio # Sound file for the telegraph "clack"
622 instruments[str(inst_id)]["bellsound"] = bell_audio # Sound file for the bell "Ting"
623 instruments[str(inst_id)]["tags"] = canvas_tag # Canvas Tags for all drawing objects
624 # Get the initial state for the instrument (if layout state has been loaded)
625 # if nothing has been loaded then the default state (of LINE BLOCKED) will be applied
626 loaded_state = file_interface.get_initial_item_state("instruments",inst_id)
627 # Set the initial block-state for the instrument (values will be 'None' for No state loaded)
628 if loaded_state["sectionstate"] == True: set_section_clear(inst_id, update_remote_instrument=False)
629 elif loaded_state["sectionstate"] == False: set_section_occupied(inst_id, update_remote_instrument=False)
630 else: set_section_blocked(inst_id, update_remote_instrument=False)
631 # Set the initial repeater state (values will be 'None' for No state loaded)
632 if loaded_state["repeaterstate"] == True: set_repeater_clear(inst_id, make_callback=False)
633 elif loaded_state["repeaterstate"] == False: set_repeater_occupied(inst_id, make_callback=False)
634 else: set_repeater_blocked(inst_id, make_callback=False)
635 # Update the repeater display of the linked instrument (if one is specified). This will update
636 # local instruments (inst_id is an int) if they have already been created on the schematic or
637 # send an MQTT event to update remote instruments (inst_id is a str) if the current instrument
638 # has already been configured to publish state to the MQTT broker.
639 refresh_linked_instrument(inst_id, linked_to)
640 # If an instrument already exists that is already linked to this instrument then we need
641 # to set the repeater display of 'our' instrument to reflect the state of that instrument.
642 for other_instrument in instruments:
643 if instruments[other_instrument]['linkedto'] == str(inst_id):
644 refresh_linked_instrument(int(other_instrument), str(inst_id))
645 return(canvas_tag)
647# --------------------------------------------------------------------------------
648# Public API function to find out if the block section ahead is clear.
649# This is represented by the current status of the REPEATER Indicator
650# --------------------------------------------------------------------------------
652def block_section_ahead_clear(inst_id:int):
653 # Validate the parameters we have been given as this is a library API function
654 if not isinstance(inst_id, int) :
655 logging.error("Instrument "+str(inst_id)+": block_section_ahead_clear - Instrument ID must be an integer")
656 section_ahead_clear = False
657 if not instrument_exists(inst_id):
658 logging.error ("Instrument "+str(inst_id)+": block_section_ahead_clear - Instrument ID does not exist")
659 section_ahead_clear = False
660 else:
661 section_ahead_clear = instruments[str(inst_id)]["repeaterstate"]
662 return(section_ahead_clear)
664# ------------------------------------------------------------------------------------------
665# API function for deleting an instrument library object (including all the drawing objects)
666# This is used by the schematic editor for changing instrument types where we delete the existing
667# instrument with all its data and then recreate it (with the same ID) in its new configuration.
668# ------------------------------------------------------------------------------------------
670def delete_instrument(inst_id:int):
671 global instruments
672 # Validate the parameters we have been given as this is a library API function
673 if not isinstance(inst_id, int):
674 logging.error("Instrument "+str(inst_id)+": delete_instrument - Instrument ID must be an integer")
675 elif not instrument_exists(inst_id):
676 logging.error("Instrument "+str(inst_id)+": delete_instrument - Instrument ID does not exist")
677 else:
678 logging.debug("Instrument "+str(inst_id)+": Deleting library object from the schematic")
679 # Delete all the tkinter drawing objects associated with the instrument
680 instruments[str(inst_id)]["canvas"].delete(instruments[str(inst_id)]["tags"])
681 instruments[str(inst_id)]["blockbutton"].destroy()
682 instruments[str(inst_id)]["clearbutton"].destroy()
683 instruments[str(inst_id)]["occupbutton"].destroy()
684 instruments[str(inst_id)]["bellbutton"].destroy()
685 # Delete the instrument entry from the dictionary of instruments
686 del instruments[str(inst_id)]
687 return()
689# ------------------------------------------------------------------------------------------
690# Non public API function for updating the ID of the linked block instrument without
691# needing to delete the block instrument and then create it in its new state. The main
692# use case is when bulk deleting objects via the schematic editor, where we want to avoid
693# interleaving tkinter 'create' commands in amongst the 'delete' commands outside of the
694# main tkinter loop as this can lead to problems with artefacts persisting on the canvas
695# ------------------------------------------------------------------------------------------
697def update_linked_instrument(inst_id:int, linked_to:str):
698 global instruments
699 # Validate the parameters we have been given as this is a library API function
700 if not isinstance(inst_id, int):
701 logging.error("Instrument "+str(inst_id)+": update_linked_instrument - Instrument ID must be an integer")
702 elif not instrument_exists(inst_id):
703 logging.error("Instrument "+str(inst_id)+": update_linked_instrument - Instrument ID does not exist")
704 elif not isinstance(linked_to, str):
705 logging.error("Instrument "+str(inst_id)+": update_linked_instrument - Linked ID must be a string")
706 elif linked_to == str(inst_id):
707 logging.error("Instrument "+str(inst_id)+": update_linked_instrument - Linked Instrument ID is the same as the Instrument ID")
708 elif linked_to !="" and not linked_to.isdigit() and mqtt_interface.split_remote_item_identifier(linked_to) is None:
709 logging.error("Instrument "+str(inst_id)+": create_instrument - Remote identifier for linked instrument is invalid format")
710 else:
711 if linked_to == "":
712 logging.debug("Instrument "+str(inst_id)+": Un-linking Block Instrument "+instruments[str(inst_id)]["linkedto"])
713 else:
714 logging.debug("Instrument "+str(inst_id)+": Updating linked Block Instrument to "+linked_to)
715 # Validate the config to generate any warnings as required:
716 validate_linked_instrument(inst_id, linked_to)
717 # Update the "linkedto" element of the Instrument configuration
718 instruments[str(inst_id)]["linkedto"] = linked_to
719 # Ensure the repeater on the new linked instrument reflects the state of our instrument
720 refresh_linked_instrument(inst_id, linked_to)
721 return()
723# ------------------------------------------------------------------------------------------
724# Internal common function to validate instrument linking (raising warnings as required)
725# ------------------------------------------------------------------------------------------
727def validate_linked_instrument(inst_id:int, linked_to:str):
728 for other_instrument in instruments:
729 link_from_other_instrument = instruments[other_instrument]['linkedto']
730 if link_from_other_instrument == str(inst_id) and linked_to != "" and linked_to != other_instrument:
731 # We've found an instrument already 'linked back to' the instrument we have just created
732 # but 'our instrument' points to a completely different instrument - Raise a warning
733 logging.warning("Instrument "+str(inst_id)+": linking to instrument "+linked_to+
734 " - but instrument "+link_from_other_instrument+" is linked from instrument "+other_instrument)
735 elif other_instrument != str(inst_id) and link_from_other_instrument != "" and link_from_other_instrument == linked_to:
736 # We've found another instrument linked to the instrument we are trying to link to
737 logging.warning("Instrument "+str(inst_id)+": linking to instrument "+linked_to+
738 " - but instrument "+ other_instrument+" is also linked to instrument "+linked_to)
739 # Raise a warning if the instrument we are linked to is already linked to another instrument
740 if instrument_exists(linked_to) and instruments[linked_to]['linkedto'] not in ("",str(inst_id)):
741 logging.warning("Instrument "+str(inst_id)+": linking to instrument "+linked_to+" - but instrument "
742 +linked_to+" is already linked to instrument "+instruments[linked_to]['linkedto'])
743 return()
745# ------------------------------------------------------------------------------------------
746# API function to reset the list of published/subscribed Instruments. This function is called by
747# the editor on 'Apply' of the MQTT pub/sub configuration prior to applying the new configuration
748# via the 'set_instruments_to_publish_state' & 'subscribe_to_remote_instrument' functions.
749# ------------------------------------------------------------------------------------------
751def reset_mqtt_configuration():
752 global instruments
753 global list_of_instruments_to_publish
754 logging.debug("Block Instruments: Resetting MQTT publish and subscribe configuration")
755 # We only need to clear the list to stop any further instrument events being published
756 list_of_instruments_to_publish.clear()
757 # For subscriptions we unsubscribe from all topics associated with the message_type
758 mqtt_interface.unsubscribe_from_message_type("instrument_updated_event")
759 mqtt_interface.unsubscribe_from_message_type("instrument_telegraph_event")
760 # Finally remove all "remote" instruments from the dictionary of instruments - these
761 # will be re-created if they are subsequently re-subscribed to. Note we don't iterate
762 # through the dictionary of instruments to remove items as it will change under us
763 new_instruments = {}
764 for key in instruments:
765 if key.isdigit(): new_instruments[key] = instruments[key]
766 instruments = new_instruments
767 return()
769#-----------------------------------------------------------------------------------------------
770# API function to configure local Block Instruments to publish state changes to remote MQTT
771# nodes. This function is called by the editor on 'Apply' of the MQTT pub/sub configuration.
772# Note the configuration can be applied independently to whether the gpio sensors 'exist' or not.
773#-----------------------------------------------------------------------------------------------
775def set_instruments_to_publish_state(*inst_ids:int):
776 global list_of_instruments_to_publish
777 for inst_id in inst_ids:
778 # Validate the parameters we have been given as this is a library API function
779 if not isinstance(inst_id, int) or inst_id < 1:
780 logging.error("Instrument "+str(inst_id)+": set_instruments_to_publish_state - ID must be a positive integer")
781 elif inst_id in list_of_instruments_to_publish:
782 logging.warning("Instrument "+str(inst_id)+": set_instruments_to_publish_state -"
783 +" Instrument is already configured to publish state to MQTT broker")
784 else:
785 logging.debug("Instrument "+str(inst_id)+": Configuring to publish state changes and telegraph events to MQTT broker")
786 list_of_instruments_to_publish.append(inst_id)
787 # Publish the initial state now this has been added to the list of instruments to publish
788 # This allows the pub/sub config to be configured independently to instrument creation
789 if instrument_exists(inst_id): send_mqtt_instrument_updated_event(inst_id)
790 return()
792#---------------------------------------------------------------------------------------------------
793# API Function to "subscribe" to remote Block Instrument events (published by other MQTT Nodes)
794# and map the appropriate internal callback (for the linked block instrument object on the local
795# schematic). This function is called by the editor on "Apply' of the MQTT pub/sub configuration
796# for all subscribed Block Instruments.
797#---------------------------------------------------------------------------------------------------
799def subscribe_to_remote_instrument(remote_id:str):
800 global instruments
801 # Validate the parameters we have been given as this is a library API function
802 if not isinstance(remote_id,str):
803 logging.error("Instrument "+str(remote_id)+": subscribe_to_remote_instrument - Remote ID must be a string")
804 elif mqtt_interface.split_remote_item_identifier(remote_id) is None:
805 logging.error("Instrument "+remote_id+": subscribe_to_remote_instrument - Remote ID is an invalid format")
806 elif instrument_exists(remote_id):
807 logging.warning("Instrument "+remote_id+": subscribe_to_remote_instrument - Already subscribed")
808 else:
809 logging.debug("Instrument "+remote_id+": Subscribing to remote Block Instrument")
810 # Create a dummy instrument object to enable 'instrument_exists' validation checks
811 # Note that this does not hold state - as state is reflected on the local repeater indicator
812 instruments[remote_id] = {}
813 instruments[remote_id]["linkedto"] = ""
814 # Subscribe to updates from the remote block instrument
815 [node_id, item_id] = mqtt_interface.split_remote_item_identifier(remote_id)
816 mqtt_interface.subscribe_to_mqtt_messages("instrument_updated_event", node_id,
817 item_id, handle_mqtt_instrument_updated_event)
818 mqtt_interface.subscribe_to_mqtt_messages("instrument_telegraph_event", node_id,
819 item_id, handle_mqtt_ring_section_bell_event)
820 return()
822###############################################################################