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

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

69 

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 

79 

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

87 

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

96 

97# ------------------------------------------------------------------------- 

98# Classes used by external functions when calling create_instrument 

99# ------------------------------------------------------------------------- 

100 

101class instrument_type(enum.Enum): 

102 single_line = 1 

103 double_line = 2 

104 

105class block_callback_type(enum.Enum): 

106 block_section_ahead_updated = 51 # The instrument has been updated 

107 

108# -------------------------------------------------------------------------------- 

109# Block Instruments are to be added to a global dictionary when created 

110# -------------------------------------------------------------------------------- 

111 

112instruments = {} 

113 

114# -------------------------------------------------------------------------------- 

115# Global variable to indicate whether a Bell Code window is already open or not 

116# -------------------------------------------------------------------------------- 

117 

118bell_code_hints_open = False 

119 

120# -------------------------------------------------------------------------------- 

121# Global list of block instruments to publish to the MQTT Broker 

122# -------------------------------------------------------------------------------- 

123 

124list_of_instruments_to_publish = [] 

125 

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

132 

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

139 

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

177 

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

183 

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) 

191 

192# -------------------------------------------------------------------------------- 

193# Internal Callbacks for handling button push events 

194# -------------------------------------------------------------------------------- 

195 

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

200 

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

205 

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

210 

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

227 

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

232 

233def reset_telegraph_button (inst_id:int): 

234 if instrument_exists(inst_id): instruments[str(inst_id)]["bellbutton"].config(bg="black") 

235 

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

248 

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

253 

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

273 

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

290 

291# -------------------------------------------------------------------------------- 

292# Internal functions for building and publishing MQTT messages (to a remote instrument) 

293# -------------------------------------------------------------------------------- 

294 

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

304 

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

313 

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

318 

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

339 

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

344 

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

365 

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

370 

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

391 

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

398 

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

417 

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

424 

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

443 

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

450 

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

469 

470# -------------------------------------------------------------------------------- 

471# Internal function to update the REPEATER display on a linked block instrument 

472# -------------------------------------------------------------------------------- 

473 

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

486 

487# -------------------------------------------------------------------------------- 

488# Internal function to create the Indicator component of a Block Instrument 

489# -------------------------------------------------------------------------------- 

490 

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) 

502 

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

508 

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) 

529 

530# -------------------------------------------------------------------------------- 

531# Public API function to create a Block Instrument (drawing objects and internal state) 

532# -------------------------------------------------------------------------------- 

533 

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) 

646 

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

651 

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) 

663 

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

669 

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

688 

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

696 

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

722 

723# ------------------------------------------------------------------------------------------ 

724# Internal common function to validate instrument linking (raising warnings as required) 

725# ------------------------------------------------------------------------------------------ 

726 

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

744 

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

750 

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

768 

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

774 

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

791 

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

798 

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

821 

822###############################################################################