Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/editor/editor.py: 63%

379 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-05 17:29 +0100

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

2# This python module will launch the schematic editor (creating the top level window) 

3# The run_editor() function is called from '__main__.py' if the package is run as 

4# a module (-m) - or can be called externally (useful for running in a pyhon IDE) 

5# This module contains all the functions to process the main menubar selections 

6#  

7# External API functions intended for use by other editor modules: 

8# run_editor() - Start the application 

9# 

10# Makes the following external API calls to other editor modules: 

11# objects.save_schematic_state() - Save the state following save or load 

12# objects.set_all(new_objects) - Set the dict of objects following a load 

13# objects.get_all() - Retrieve the dict of objects for saving to file 

14# objects.reset_objects() - Reset the schematic back to its default state 

15# objects.mqtt_update_signals(pub_list, sub_list) - configure MQTT networking 

16# objects.mqtt_update_sections(pub_list, sub_list) - configure MQTT networking 

17# objects.mqtt_update_instruments(pub_list, sub_list) - configure MQTT networking 

18# schematic.initialise(root, callback, width, height, grid, snap) - Create the canvas 

19# schematic.delete_all_objects() - For deleting all objects (on new/load) 

20# schematic.update_canvas(width,height,grid,snap) - Update the canvas following reload/resizing 

21# schematic.enable_editing() - On mode toggle or load (if loaded file is in edit mode and not already in edit mode) 

22# schematic.disable_editing() - On mode toggle or load (if loaded file is in run mode and not already in run mode) 

23# run_layout.configure_automation() - On automation toggle or load 

24# run_layout.configure_spad_popups() - On settings update or load 

25# settings.get_all() - Get all settings (for save) 

26# settings.set_all() - Set all settings (following load) 

27# settings.get_canvas() - Get default/loaded canvas settings (for resizing) 

28# settings.get_version() - Get the Application version (for the Arg Parser) 

29# settings.get_general() - Get the current filename and editor mode 

30# settings.set_general() - Set the filename and editor mode 

31# settings.get_logging() - Set the default log level 

32# settings.get_sprog() - to get the current SPROG settings 

33# settings.get_gpio() - to get the current track sensor GPIO mappings 

34# settings.restore_defaults() - Following user selection of "new" 

35# common.scrollable_text_box - to display a list of warnings on file load 

36# menubar_windows.edit_mqtt_settings(root, mqtt_connect_callback, mqtt_update_callback) 

37# menubar_windows.edit_sprog_settings(root, sprog_connect_callback, sprog_update_callback) 

38# menubar_windows.edit_logging_settings(root, logging_update_callback) 

39# menubar_windows.edit_canvas_settings(root, canvas_update_callback) 

40# menubar_windows.edit_gpio_settings(root, gpio_update_callback) 

41# menubar_windows.display_help(parent_window) - opens the config window 

42# menubar_windows.display_about(parent_window) - opens the config window 

43# menubar_windows.edit_layout_info(parent_window) - opens the config window 

44# utilities.dcc_programming(root, dcc_power_off_callback, dcc_power_on_callback) 

45# utilities.dcc_mappings(root) 

46# 

47# Makes the following external API calls to library modules: 

48# library_common.find_root_window (widget) - To set the root window 

49# library_common.on_closing (ask_to_save_state) - To shutdown gracefully 

50# file_interface.load_schematic - To load all settings and objects 

51# file_interface.save_schematic - To save all settings and objects 

52# file_interface.purge_loaded_state_information - Called following a re-load 

53# pi_sprog_interface.initialise_pi_sprog - After update of Pi Sprog Settings 

54# pi_sprog_interface.sprog_shutdown - Disconnect from the Pi-SPROG 

55# pi_sprog_interface.request_dcc_power_off - To turn off the track power 

56# pi_sprog_interface.request_dcc_power_on - To turn on the track power 

57# mqtt_interface.mqtt_broker_connect - MQTT Broker connection configuration 

58# mqtt_interface.mqtt_broker_disconnect - disconnect prior to reconfiguration 

59# mqtt_interface.configure_mqtt_client - configure client network details 

60# dcc_control.reset_mqtt_configuration - reset all publish/subscribe 

61# dcc_control.set_node_to_publish_dcc_commands - set note to publish DCC 

62# dcc_control.subscribe_to_dcc_command_feed - subscribe to DCC from other nodes 

63# gpio_sensors.running_on_raspberry_pi - is the app running on a Raspberry Pi 

64# 

65#------------------------------------------------------------------------------------ 

66 

67import os 

68import tkinter as Tk 

69import logging 

70import argparse 

71 

72from . import objects 

73from . import settings 

74from . import schematic 

75from . import run_layout 

76from . import menubar_windows 

77from . import utilities 

78from ..library import file_interface 

79from ..library import pi_sprog_interface 

80from ..library import mqtt_interface 

81from ..library import gpio_sensors 

82from ..library import dcc_control 

83from ..library import common as library_common 

84 

85# The following imports are only used for the advanced debugging functions 

86import linecache 

87import tracemalloc 

88 

89#------------------------------------------------------------------------------------ 

90# Top level class for the toolbar window 

91#------------------------------------------------------------------------------------ 

92 

93class main_menubar: 

94 def __init__(self, root): 

95 self.root = root 

96 # Configure the logger (log level gets set later) 

97 logging.basicConfig(format='%(levelname)s: %(message)s') 

98 # Create the menu bar 

99 self.mainmenubar = Tk.Menu(self.root) 

100 self.root.configure(menu=self.mainmenubar) 

101 # Create the various menubar items for the File Dropdown 

102 self.file_menu = Tk.Menu(self.mainmenubar, tearoff=False) 

103 self.file_menu.add_command(label=" New", command=self.new_schematic) 

104 self.file_menu.add_command(label=" Open...", command=self.load_schematic) 

105 self.file_menu.add_command(label=" Save", command=lambda:self.save_schematic(False)) 105 ↛ exitline 105 didn't run the lambda on line 105

106 self.file_menu.add_command(label=" Save as...", command=lambda:self.save_schematic(True)) 106 ↛ exitline 106 didn't run the lambda on line 106

107 self.file_menu.add_separator() 

108 self.file_menu.add_command(label=" Quit",command=lambda:self.quit_schematic()) 108 ↛ exitline 108 didn't run the lambda on line 108

109 self.mainmenubar.add_cascade(label="File", menu=self.file_menu) 

110 # Create the various menubar items for the Mode Dropdown 

111 self.mode_label = "Mode:xxx" 

112 self.mode_menu = Tk.Menu(self.mainmenubar,tearoff=False) 

113 self.mode_menu.add_command(label=" Edit ", command=self.edit_mode) 

114 self.mode_menu.add_command(label=" Run ", command=self.run_mode) 

115 self.mode_menu.add_command(label=" Reset", command=self.reset_layout) 

116 self.mainmenubar.add_cascade(label=self.mode_label, menu=self.mode_menu) 

117 # Create the various menubar items for the Automation Dropdown 

118 self.auto_label = "Automation:xxx" 

119 self.auto_menu = Tk.Menu(self.mainmenubar,tearoff=False) 

120 self.auto_menu.add_command(label=" Enable ", command=self.automation_enable) 

121 self.auto_menu.add_command(label=" Disable", command=self.automation_disable) 

122 self.mainmenubar.add_cascade(label=self.auto_label, menu=self.auto_menu) 

123 self.mainmenubar.entryconfigure(self.auto_label, state="disabled") 

124 # Create the various menubar items for the SPROG Connection Dropdown 

125 self.sprog_label = "SPROG:Disconnected" 

126 self.sprog_menu = Tk.Menu(self.mainmenubar,tearoff=False) 

127 self.sprog_menu.add_command(label=" Connect ", command=self.sprog_connect) 

128 self.sprog_menu.add_command(label=" Disconnect ", command=self.sprog_disconnect) 

129 self.mainmenubar.add_cascade(label=self.sprog_label, menu=self.sprog_menu) 

130 # Create the various menubar items for the DCC Power Dropdown 

131 self.power_label = "DCC Power:???" 

132 self.power_menu = Tk.Menu(self.mainmenubar,tearoff=False) 

133 self.power_menu.add_command(label=" OFF ", command=self.dcc_power_off) 

134 self.power_menu.add_command(label=" ON ", command=self.dcc_power_on) 

135 self.mainmenubar.add_cascade(label=self.power_label, menu=self.power_menu) 

136 self.mainmenubar.entryconfigure(self.power_label, state="disabled") 

137 # Create the various menubar items for the MQTT Connection Dropdown 

138 self.mqtt_label = "MQTT:Disconnected" 

139 self.mqtt_menu = Tk.Menu(self.mainmenubar,tearoff=False) 

140 self.mqtt_menu.add_command(label=" Connect ", command=self.mqtt_connect) 

141 self.mqtt_menu.add_command(label=" Disconnect ", command=self.mqtt_disconnect) 

142 self.mainmenubar.add_cascade(label=self.mqtt_label, menu=self.mqtt_menu) 

143 # Create the various menubar items for the Utilities Dropdown 

144 self.utilities_menu = Tk.Menu(self.mainmenubar,tearoff=False) 

145 self.utilities_menu.add_command(label =" DCC Programming...", 145 ↛ exitline 145 didn't jump to the function exit

146 command=lambda:utilities.dcc_programming(self.root, self.dcc_programming_enabled, 

147 self.dcc_power_off, self.dcc_power_on)) 

148 self.utilities_menu.add_command(label =" DCC Mappings...", 148 ↛ exitline 148 didn't jump to the function exit

149 command=lambda:utilities.dcc_mappings(self.root)) 

150 self.mainmenubar.add_cascade(label = "Utilities", menu=self.utilities_menu) 

151 # Create the various menubar items for the Settings Dropdown 

152 self.settings_menu = Tk.Menu(self.mainmenubar,tearoff=False) 

153 self.settings_menu.add_command(label =" Canvas...", 153 ↛ exitline 153 didn't jump to the function exit

154 command=lambda:menubar_windows.edit_canvas_settings(self.root, self.canvas_update)) 

155 self.settings_menu.add_command(label =" General...", 155 ↛ exitline 155 didn't jump to the function exit

156 command=lambda:menubar_windows.edit_general_settings(self.root, self.general_settings_update)) 

157 self.settings_menu.add_command(label =" GPIO...", 157 ↛ exitline 157 didn't jump to the function exit

158 command=lambda:menubar_windows.edit_gpio_settings(self.root, self.gpio_update)) 

159 self.settings_menu.add_command(label =" Logging...", 159 ↛ exitline 159 didn't jump to the function exit

160 command=lambda:menubar_windows.edit_logging_settings(self.root, self.logging_update)) 

161 self.settings_menu.add_command(label =" MQTT...", 161 ↛ exitline 161 didn't jump to the function exit

162 command=lambda:menubar_windows.edit_mqtt_settings(self.root, self.mqtt_connect, self.mqtt_update)) 

163 self.settings_menu.add_command(label =" SPROG...", 163 ↛ exitline 163 didn't jump to the function exit

164 command=lambda:menubar_windows.edit_sprog_settings(self.root, self.sprog_connect, self.sprog_update)) 

165 self.mainmenubar.add_cascade(label = "Settings", menu=self.settings_menu) 

166 # Create the various menubar items for the Help Dropdown 

167 self.help_menu = Tk.Menu(self.mainmenubar,tearoff=False) 

168 self.help_menu.add_command(label =" Help...", command=lambda:menubar_windows.display_help(self.root)) 168 ↛ exitline 168 didn't run the lambda on line 168

169 self.help_menu.add_command(label =" About...", command=lambda:menubar_windows.display_about(self.root)) 169 ↛ exitline 169 didn't run the lambda on line 169

170 self.help_menu.add_command(label =" Info...", command=lambda:menubar_windows.edit_layout_info(self.root)) 170 ↛ exitline 170 didn't run the lambda on line 170

171 self.mainmenubar.add_cascade(label = "Help", menu=self.help_menu) 

172 # Flag to track whether the new configuration has been saved or not 

173 # Used to enforce a "save as" dialog on the initial save of a new layout 

174 self.file_has_been_saved = False 

175 # Initialise the schematic - the Edit Mode flag is the 2nd param in the returned tuple from get_general 

176 width, height, grid, snap_to_grid = settings.get_canvas() 

177 edit_mode = settings.get_general()[1] 

178 schematic.initialise(self.root, self.handle_canvas_event, width, height, grid, snap_to_grid, edit_mode) 

179 # Parse the command line arguments 

180 parser = argparse.ArgumentParser(description = "Model railway signalling "+settings.get_general()[2], 

181 formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=27)) 

182 parser.add_argument("-d","--debug",dest="debug_mode",action='store_true',help="run editor with debug functions") 

183 parser.add_argument("-f","--file",dest="filename",metavar="FILE",help="schematic file to load on startup") 

184 parser.add_argument("-l","--log",dest="log_level",metavar="LEVEL", 

185 help="log level (DEBUG, INFO, WARNING, ERROR)") 

186 args = parser.parse_args() 

187 # Set the log level (default unless one has been specified as a command line argument) 

188 if args.log_level == "ERROR": settings.set_logging(1) 188 ↛ 192line 188 didn't jump to line 192

189 elif args.log_level == "WARNING": settings.set_logging(2) 189 ↛ 192line 189 didn't jump to line 192

190 elif args.log_level == "INFO": settings.set_logging(3) 190 ↛ 192line 190 didn't jump to line 192

191 elif args.log_level == "DEBUG": settings.set_logging(4) 

192 self.logging_update() 

193 # Initialise the editor configuration at startup (using the default settings) 

194 self.initialise_editor() 

195 # If a filename has been specified as a command line argument then load it. The loaded 

196 # settings will overwrite the default settings and initialise_editor will be called again 

197 if args.filename is not None: self.load_schematic(args.filename) 

198 # The following code is to help with advanced debugging (start the app with the -d flag) 

199 if args.debug_mode: 199 ↛ 200line 199 didn't jump to line 200, because the condition on line 199 was never true

200 self.debug_menu = Tk.Menu(self.mainmenubar,tearoff=False) 

201 self.debug_menu.add_command(label =" Start memory allocation reporting", command=self.start_memory_monitoring) 

202 self.debug_menu.add_command(label =" Stop memory allocation reporting", command=self.stop_memory_monitoring) 

203 self.debug_menu.add_command(label =" Report the top 10 users of memory", command=self.report_highest_memory_users) 

204 self.mainmenubar.add_cascade(label = "Debug ", menu=self.debug_menu) 

205 tracemalloc.start() 

206 self.monitor_memory_usage = False 

207 

208 # -------------------------------------------------------------------------------------- 

209 # Advanced debugging functions (memory allocation monitoring/reporting) 

210 # Full acknowledgements to stack overflow for the reporting functions used here 

211 # -------------------------------------------------------------------------------------- 

212 

213 def start_memory_monitoring(self): 

214 if not self.monitor_memory_usage: 

215 self.monitor_memory_usage=True 

216 self.report_memory_usage() 

217 

218 def stop_memory_monitoring(self): 

219 self.monitor_memory_usage=False 

220 

221 def report_memory_usage(self): 

222 current, peak = tracemalloc.get_traced_memory() 

223 print(f"Current memory usage is {current / 10**3}KB; Peak was {peak / 10**3}KB; Diff = {(peak - current) / 10**3}KB") 

224 if self.monitor_memory_usage: self.root.after(5000,lambda:self.report_memory_usage()) 

225 

226 def report_highest_memory_users(self): 

227 key_type='lineno' 

228 limit=10 

229 snapshot = tracemalloc.take_snapshot() 

230 snapshot = snapshot.filter_traces((tracemalloc.Filter(False, "<frozen importlib._bootstrap>"), 

231 tracemalloc.Filter(False, "<unknown>"),)) 

232 top_stats = snapshot.statistics(key_type) 

233 print("Top %s users of memory (lines of python code)" % limit) 

234 for index, stat in enumerate(top_stats[:limit], 1): 

235 frame = stat.traceback[0] 

236 # replace "/path/to/module/file.py" with "module/file.py" 

237 filename = os.sep.join(frame.filename.split(os.sep)[-2:]) 

238 print("#%s: %s:%s: %.1f KiB" % (index, filename, frame.lineno, stat.size / 1024)) 

239 line = linecache.getline(frame.filename, frame.lineno).strip() 

240 if line: print(' %s' % line) 

241 other = top_stats[limit:] 

242 if other: 

243 size = sum(stat.size for stat in other) 

244 print("%s other: %.1f KiB" % (len(other), size / 1024)) 

245 total = sum(stat.size for stat in top_stats) 

246 print("Total allocated size: %.1f KiB" % (total / 1024)) 

247 

248 # -------------------------------------------------------------------------------------- 

249 # Common initialisation functions (called on editor start or layout load or new layout) 

250 # -------------------------------------------------------------------------------------- 

251 

252 def initialise_editor(self): 

253 # Set the root window label to the name of the current file (split from the dir path) 

254 # The fully qualified filename is the first parameter provided by 'get_general' 

255 path, name = os.path.split(settings.get_general()[0]) 

256 self.root.title(name) 

257 # Re-size the canvas to reflect the new schematic size 

258 self.canvas_update() 

259 # Reset the SPROG and MQTT connecions to their default states - the MQTT and SPROG 

260 # configuration settings in the loaded file may be completely different so we 

261 # want to close down everything before re-opening everything from scratch 

262 if self.power_label == "DCC Power:On": self.dcc_power_off() 

263 if self.sprog_label == "SPROG:Connected":self.sprog_disconnect() 

264 if self.mqtt_label == "MQTT:Connected": self.mqtt_disconnect() 

265 # Initialise the SPROG (if configured). Note that we use the menubar functions 

266 # for connection and the DCC power so these are correctly reflected in the UI 

267 # The "connect" and "power" flags are the 4th and 5th parameter returned 

268 if settings.get_sprog()[3]: 268 ↛ 269line 268 didn't jump to line 269, because the condition on line 268 was never true

269 sprog_connected = self.sprog_connect() 

270 if sprog_connected and settings.get_sprog()[4]: 

271 self.dcc_power_on() 

272 # Initialise the MQTT networking (if configured). Note that we use the menubar  

273 # function for connection so the state is correctly reflected in the UI 

274 # The "connect on startup" flag is the 8th parameter returned 

275 self.mqtt_reconfigure_client() 

276 if settings.get_mqtt()[7]: self.mqtt_connect() 

277 self.mqtt_reconfigure_pub_sub() 

278 # Set the Automation Mode (5th param in the returned tuple) 

279 # Either of these calls will update 'run_layout' 

280 if settings.get_general()[4]: self.automation_enable() 280 ↛ 281line 280 didn't jump to line 281, because the condition on line 280 was never false

281 else: self.automation_disable() 

282 # Set the edit mode (2nd param in the returned tuple) 

283 # Either of these calls will update run layout 

284 if settings.get_general()[1]: self.edit_mode() 

285 else: self.run_mode() 

286 # Create all the track sensor objects that have been defined 

287 self.gpio_update() 

288 # Apply any other general settings 

289 self.general_settings_update() 

290 

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

292 # Callback function to handle the Toggle Mode Event ('m' key) from schematic.py 

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

294 

295 def handle_canvas_event(self, event=None): 

296 # Note that event.keysym returns the character (event.state would be 'Control') 

297 if event.keysym == 'm': 

298 # the Edit mode flag is the second parameter returned 

299 if settings.get_general()[1]: self.run_mode() 299 ↛ exitline 299 didn't return from function 'handle_canvas_event'

300 else: self.edit_mode() 

301 elif event.keysym == 's': 

302 # the Snap to Grid flag is the fourth parameter returned 

303 if settings.get_canvas()[3]: settings.set_canvas(snap_to_grid=False) 

304 else: settings.set_canvas(snap_to_grid=True) 

305 # Apply the new canvas settings 

306 self.canvas_update() 

307 elif event.keysym == 'a': 307 ↛ exitline 307 didn't return from function 'handle_canvas_event', because the condition on line 307 was never false

308 # the Automation flag is the fifth parameter returned 

309 if settings.get_general()[4]: self.automation_disable() 309 ↛ 310line 309 didn't jump to line 310, because the condition on line 309 was never false

310 else: self.automation_enable() 

311 

312 #------------------------------------------------------------------------------------------ 

313 # Mode menubar functions 

314 #------------------------------------------------------------------------------------------ 

315 

316 def automation_enable(self): 

317 new_label = "Automation:On" 

318 self.mainmenubar.entryconfigure(self.auto_label, label=new_label) 

319 self.auto_label = new_label 

320 settings.set_general(automation=True) 

321 run_layout.configure_automation(True) 

322 

323 def automation_disable(self): 

324 new_label = "Automation:Off" 

325 self.mainmenubar.entryconfigure(self.auto_label, label=new_label) 

326 self.auto_label = new_label 

327 settings.set_general(automation=False) 

328 run_layout.configure_automation(False) 

329 

330 def edit_mode(self): 

331 if self.mode_label != "Mode:Edit": 

332 new_label = "Mode:Edit" 

333 self.mainmenubar.entryconfigure(self.mode_label, label=new_label) 

334 self.mode_label = new_label 

335 settings.set_general(editmode=True) 

336 schematic.enable_editing() 

337 # Disable the automation menubar selection and set to "off" (automation is always disabled 

338 # in Run mode so we just need to update the indication (no need to update 'run_layout') 

339 new_label1 = "Automation:N/A" 

340 self.mainmenubar.entryconfigure(self.auto_label, state="disabled") 

341 self.mainmenubar.entryconfigure(self.auto_label, label=new_label1) 

342 self.auto_label = new_label1 

343 

344 def run_mode(self): 

345 if self.mode_label != "Mode:Run": 

346 new_label = "Mode:Run" 

347 self.mainmenubar.entryconfigure(self.mode_label, label=new_label) 

348 self.mode_label = new_label 

349 settings.set_general(editmode=False) 

350 schematic.disable_editing() 

351 # Enable the the automation menubar selection and update to reflect the current setting 

352 # Note that automation is only enbled in Run mode so we just need to update the indication 

353 # no need to update 'run_layout'. Note the Automation flag is the fifth parameter returned 

354 if settings.get_general()[4]: new_label1 = "Automation:On" 354 ↛ 355line 354 didn't jump to line 355, because the condition on line 354 was never false

355 else: new_label1 = "Automation:Off" 

356 self.mainmenubar.entryconfigure(self.auto_label, state="normal") 

357 self.mainmenubar.entryconfigure(self.auto_label, label=new_label1) 

358 self.auto_label = new_label1 

359 

360 def reset_layout(self, ask_for_confirm:bool=True): 

361 if ask_for_confirm: 361 ↛ 362line 361 didn't jump to line 362, because the condition on line 361 was never true

362 if Tk.messagebox.askokcancel(parent=self.root, title="Reset Schematic", 

363 message="Are you sure you want to reset all signals, points and " 

364 +"track occupancy sections back to their default state"): 

365 objects.reset_objects() 

366 else: 

367 objects.reset_objects() 

368 

369 #------------------------------------------------------------------------------------------ 

370 # SPROG menubar functions 

371 #------------------------------------------------------------------------------------------ 

372 

373 def update_sprog_menubar_controls(self, desired_state:bool, connected:bool, show_popup:bool): 

374 if connected: 

375 new_label = "SPROG:Connected" 

376 self.mainmenubar.entryconfigure(self.power_label, state="normal") 

377 if show_popup and connected != desired_state: 

378 Tk.messagebox.showerror(parent=self.root, title="SPROG Error", 

379 message="Error disconnecting from Serial Port - try rebooting") 

380 else: 

381 new_label = "SPROG:Disconnected" 

382 self.mainmenubar.entryconfigure(self.power_label, state="disabled") 

383 if show_popup and connected != desired_state: 

384 Tk.messagebox.showerror(parent=self.root, title="SPROG Error", 

385 message="SPROG connection failure - Check SPROG settings") 

386 self.mainmenubar.entryconfigure(self.sprog_label, label=new_label) 

387 self.sprog_label = new_label 

388 

389 def update_power_menubar_controls(self, desired_state:bool, power_on:bool): 

390 if power_on: 

391 new_label = "DCC Power:On" 

392 if power_on != desired_state: 

393 Tk.messagebox.showerror(parent=self.root, title="SPROG Error", 

394 message="DCC power off failed - Check SPROG settings") 

395 else: 

396 new_label = "DCC Power:Off" 

397 if power_on != desired_state: 

398 Tk.messagebox.showerror(parent=self.root, title="SPROG Error", 

399 message="DCC power on failed - Check SPROG settings") 

400 self.mainmenubar.entryconfigure(self.power_label, label=new_label) 

401 self.power_label = new_label 

402 

403 def sprog_connect(self, show_popup:bool=True): 

404 # The connect request returns True if successful 

405 port, baud, debug, startup, power = settings.get_sprog() 

406 connected = pi_sprog_interface.sprog_connect(port, baud, debug) 

407 self.update_sprog_menubar_controls(True, connected, show_popup) 

408 return(connected) 

409 

410 def sprog_disconnect(self): 

411 # The disconnect request returns True if successful 

412 connected = not pi_sprog_interface.sprog_disconnect() 

413 self.update_sprog_menubar_controls(False, connected, True) 

414 

415 def sprog_update(self): 

416 # Only update the configuration if we are already connected - otherwise  

417 # do nothing (wait until the next time the user attempts to connect) 

418 if self.sprog_label == "SPROG:Connected": self.sprog_connect() 

419 

420 def dcc_power_off(self): 

421 # The power off request returns True if successful 

422 power_on = not pi_sprog_interface.request_dcc_power_off() 

423 self.update_power_menubar_controls(False, power_on) 

424 

425 def dcc_power_on(self): 

426 # The power on request returns True if successful 

427 power_on = pi_sprog_interface.request_dcc_power_on() 

428 self.update_power_menubar_controls(True, power_on) 

429 

430 def dcc_programming_enabled(self): 

431 return (self.power_label=="DCC Power:On" and self.sprog_label=="SPROG:Connected") 

432 

433 #------------------------------------------------------------------------------------------ 

434 # MQTT menubar functions 

435 #------------------------------------------------------------------------------------------ 

436 

437 def update_mqtt_menubar_controls(self, desired_state:bool, connected:bool, show_popup:bool): 

438 if connected: 

439 new_label = "MQTT:Connected" 

440 if show_popup and connected != desired_state: 440 ↛ 441line 440 didn't jump to line 441, because the condition on line 440 was never true

441 Tk.messagebox.showerror(parent=self.root, title="MQTT Error", 

442 message="Broker disconnection failure - try rebooting") 

443 else: 

444 new_label = "MQTT:Disconnected" 

445 if show_popup and connected != desired_state: 445 ↛ 446line 445 didn't jump to line 446, because the condition on line 445 was never true

446 Tk.messagebox.showerror(parent=self.root, title="MQTT Error", 

447 message="Broker connection failure- Check MQTT settings") 

448 self.mainmenubar.entryconfigure(self.mqtt_label, label=new_label) 

449 self.mqtt_label = new_label 

450 

451 def mqtt_connect(self, show_popup:bool=True): 

452 url = settings.get_mqtt()[0] 

453 port = settings.get_mqtt()[1] 

454 username = settings.get_mqtt()[4] 

455 password = settings.get_mqtt()[5] 

456 # The connect request returns True if successful 

457 connected = mqtt_interface.mqtt_broker_connect(url, port, username, password) 

458 self.update_mqtt_menubar_controls(True, connected, show_popup) 

459 return(connected) 

460 

461 def mqtt_disconnect(self): 

462 # The disconnect request returns True if successful 

463 connected = not mqtt_interface.mqtt_broker_disconnect() 

464 self.update_mqtt_menubar_controls(False, connected, True) 

465 

466 def mqtt_update(self): 

467 # Apply the new signalling network confguration 

468 self.mqtt_reconfigure_client() 

469 # Only reset the broker connection if we are already connected - otherwise  

470 # do nothing (wait until the next time the user attempts to connect) 

471 if self.mqtt_label == "MQTT:Connected" : self.mqtt_connect() 

472 # Reconfigure all publish and subscribe settings 

473 self.mqtt_reconfigure_pub_sub() 

474 

475 def mqtt_reconfigure_client(self): 

476 network = settings.get_mqtt()[2] 

477 node = settings.get_mqtt()[3] 

478 debug = settings.get_mqtt()[6] 

479 publish_shutdown = settings.get_mqtt()[8] 

480 act_on_shutdown = settings.get_mqtt()[9] 

481 mqtt_interface.configure_mqtt_client(network, node, debug, publish_shutdown, act_on_shutdown, 481 ↛ exitline 481 didn't jump to the function exit

482 shutdown_callback = lambda:self.quit_schematic(ask_for_confirm=False)) 

483 

484 def mqtt_reconfigure_pub_sub(self): 

485 dcc_control.reset_mqtt_configuration() 

486 dcc_control.set_node_to_publish_dcc_commands(settings.get_pub_dcc()) 

487 dcc_control.subscribe_to_dcc_command_feed(*settings.get_sub_dcc_nodes()) 

488 objects.mqtt_update_gpio_sensors(settings.get_pub_sensors(), settings.get_sub_sensors()) 

489 objects.mqtt_update_signals(settings.get_pub_signals(), settings.get_sub_signals()) 

490 objects.mqtt_update_sections(settings.get_pub_sections(), settings.get_sub_sections()) 

491 objects.mqtt_update_instruments(settings.get_pub_instruments(), settings.get_sub_instruments()) 

492 

493 #------------------------------------------------------------------------------------------ 

494 # OTHER menubar functions 

495 #------------------------------------------------------------------------------------------ 

496 

497 def canvas_update(self): 

498 width, height, grid, snap_to_grid = settings.get_canvas() 

499 schematic.update_canvas(width, height, grid, snap_to_grid) 

500 

501 def logging_update(self): 

502 log_level = settings.get_logging() 

503 if log_level == 1: logging.getLogger().setLevel(logging.ERROR) 503 ↛ exitline 503 didn't return from function 'logging_update'

504 elif log_level == 2: logging.getLogger().setLevel(logging.WARNING) 504 ↛ 505line 504 didn't jump to line 505, because the condition on line 504 was never false

505 elif log_level == 3: logging.getLogger().setLevel(logging.INFO) 

506 elif log_level == 4: logging.getLogger().setLevel(logging.DEBUG) 

507 

508 def gpio_update(self): 

509 trigger, timeout, mappings = settings.get_gpio() 

510 # Generate a pop-up warning if mappings have been defined but we are not running on a Pi 

511 if len(mappings)>0 and not gpio_sensors.running_on_raspberry_pi: 511 ↛ 512line 511 didn't jump to line 512, because the condition on line 511 was never true

512 Tk.messagebox.showwarning(parent=self.root, title="GPIO Warning", 

513 message="Not running on Raspberry Pi - no track sensors will be active") 

514 # Delete all track sensor objects and then re-create from the updated settings - we do this 

515 # even if not running on a Raspberry Pi (to enable transfer of layout files between platforms) 

516 objects.update_local_gpio_sensors(trigger, timeout, mappings) 

517 

518 def general_settings_update(self): 

519 # The spad popups enabled flag is the 6th parameter returned 

520 run_layout.configure_spad_popups(settings.get_general()[5]) 

521 

522 #------------------------------------------------------------------------------------------ 

523 # FILE menubar functions 

524 #------------------------------------------------------------------------------------------ 

525 

526 def quit_schematic(self, ask_for_confirm:bool=True): 

527 # Note that 'confirmation' is defaulted to 'True' for normal use (i.e. when this function 

528 # is called as a result of a menubar selection) to enforce the confirmation dialog. If 

529 # 'confirmation' is False (system_test_harness use case) then the dialogue is surpressed 

530 if not ask_for_confirm or Tk.messagebox.askokcancel(parent=self.root, title="Quit Schematic", 530 ↛ 538line 530 didn't jump to line 538, because the condition on line 530 was never false

531 message="Are you sure you want to discard all changes and quit the application"): 

532 # Kill off the PhotoImage objects so we don't get spurious exceptions on window close and 

533 # perform an orderly shutdown (cleanup and disconnect from the MQTT broker, Switch off DCC 

534 # power and disconnect from the serial port, Revert all GPIO ports to their default states 

535 # and then wait until all scheduled Tkinter tasks have completed before destroying root 

536 schematic.shutdown() 

537 library_common.shutdown() 

538 return() 

539 

540 def new_schematic(self, ask_for_confirm:bool=True): 

541 # Note that 'confirmation' is defaulted to 'True' for normal use (i.e. when this function 

542 # is called as a result of a menubar selection) to enforce the confirmation dialog. If 

543 if not ask_for_confirm or Tk.messagebox.askokcancel(parent=self.root, title="New Schematic", 543 ↛ 553line 543 didn't jump to line 553, because the condition on line 543 was never false

544 message="Are you sure you want to discard all changes and create a new blank schematic"): 

545 # Delete all existing objects, restore the default settings and re-initialise the editor 

546 schematic.delete_all_objects() 

547 settings.restore_defaults() 

548 self.initialise_editor() 

549 # Save the current state (for undo/redo) - deleting all previous history 

550 objects.save_schematic_state(reset_pointer=True) 

551 # Set the file saved flag back to false (to force a "save as" on next save) 

552 self.file_has_been_saved = False 

553 return() 

554 

555 def save_schematic(self, save_as:bool=False): 

556 settings_to_save = settings.get_all() 

557 objects_to_save = objects.get_all() 

558 # Filename is the first parameter returned from settings.get_general 

559 filename_to_save = settings.get_general()[0] 

560 # If the filename is the default "new_schematic.sig" then we force a 'save as' 

561 if not self.file_has_been_saved: 

562 save_as = True 

563 filename_to_save = "" 

564 # Call the library function to load the base configuration file 

565 saved_filename = file_interface.save_schematic(settings_to_save, objects_to_save, 

566 filename_to_save, save_as=save_as) 

567 # Reset the filename / root window title to the name of the file we have saved 

568 if saved_filename is not None: 

569 settings.set_general(filename=saved_filename) 

570 path, name = os.path.split(saved_filename) 

571 self.root.title(name) 

572 self.file_has_been_saved = True 

573 return() 

574 

575 # Helper function to convert the version string into a Tuple to allow comparison 

576 # Removes the leading "Version " prefix and handles version numbers without a patch number  

577 def tuple_version(self, version:str): 

578 if " " in version: version = version.split(" ")[1] 

579 if len(version)==3: version += ".0" 

580 return tuple(map(int,(version.split(".")))) 

581 

582 def load_schematic(self, filename=None): 

583 # Note that 'filename' is defaulted to 'None' for normal use (i.e. when this function 

584 # is called as a result of a menubar selection) to enforce the file selection dialog. If 

585 # a filename is specified (system_test_harness use case) then the dialogue is surpressed 

586 file_loaded, layout_state = file_interface.load_schematic(filename) 

587 # the 'file_loaded' will be the name of the file loaded or None (if not loaded) 

588 if file_loaded is not None: 588 ↛ 636line 588 didn't jump to line 636, because the condition on line 588 was never false

589 # Do some basic validation that the file has the elements we need 

590 if "settings" in layout_state.keys() and "objects" in layout_state.keys(): 590 ↛ 633line 590 didn't jump to line 633, because the condition on line 590 was never false

591 # Compare the version of the application to the version the file was saved under 

592 sig_file_version = layout_state["settings"]["general"]["version"] 

593 application_version = settings.get_general()[2] 

594 if self.tuple_version(sig_file_version) > self.tuple_version(application_version): 

595 # We don't provide forward compatibility (too difficult) - so fail fast 

596 logging.error("LOAD LAYOUT - File was saved by "+sig_file_version) 

597 logging.error("LOAD LAYOUT - Current version of the application is "+application_version) 

598 Tk.messagebox.showerror(parent=self.root, title="Load Error", 

599 message="File was saved by "+sig_file_version+". Upgrade application to "+ 

600 sig_file_version+" or later to support this layout file.") 

601 elif self.tuple_version(sig_file_version) < self.tuple_version("3.5.0"): 

602 # We only provide backward compatibility for a few versions - before that, fail fast 

603 logging.error("LOAD LAYOUT - File was saved by application "+sig_file_version) 

604 logging.error("LOAD LAYOUT - Current version of the application is "+application_version) 

605 Tk.messagebox.showerror(parent=self.root, title="Load Error", 

606 message="File was saved by "+sig_file_version+". "+ 

607 "This version of the application only supports files saved by version "+ 

608 "3.5.0 or later. Try loading/saving with an intermediate version first.") 

609 else: 

610 # We should now be OK to attempt the load, but if the file was saved under a 

611 # previous version then we still want to flag a warning message to the user 

612 if self.tuple_version(sig_file_version) < self.tuple_version(application_version): 

613 logging.warning("LOAD LAYOUT - File was saved by application "+sig_file_version) 

614 logging.warning("LOAD LAYOUT - Current version of the application is "+application_version) 

615 Tk.messagebox.showwarning(parent=self.root, title="Load Warning", 

616 message="File was saved by "+sig_file_version+". "+ 

617 "Re-save with current version to ensure forward compatibility.") 

618 # Delete all existing objects 

619 schematic.delete_all_objects() 

620 settings.set_all(layout_state["settings"]) 

621 # Set the filename to reflect that actual name of the loaded file 

622 settings.set_general(filename=file_loaded) 

623 # Re-initialise the editor for the new settings to take effect 

624 self.initialise_editor() 

625 # Create the loaded layout objects then purge the loaded state information 

626 objects.set_all(layout_state["objects"]) 

627 # Purge the loaded state (to stope it being erroneously inherited 

628 # when items are deleted and then new items created with the same IDs) 

629 file_interface.purge_loaded_state_information() 

630 # Set the flag so we don't enforce a "save as" on next save 

631 self.file_has_been_saved = True 

632 else: 

633 logging.error("LOAD LAYOUT - File does not contain all required elements") 

634 Tk.messagebox.showerror(parent=self.root, title="Load Error", 

635 message="File does not contain\nall required elements") 

636 return() 

637 

638#------------------------------------------------------------------------------------ 

639# This is the main function to run up the schematic editor application  

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

641 

642def run_editor(): 

643 print("Starting Model Railway Signalling application") 

644 # Create the Main Root Window 

645 root = Tk.Tk() 

646 # Configure Tkinter to not show hidden files in the file open/save dialogs 

647 # Full credit to Stack Overflow for the solution to this problem 

648 try: 

649 try: 

650 root.tk.call('tk_getOpenFile', '-foobarbaz') 

651 except Tk.TclError: 

652 pass 

653 root.tk.call('set', '::tk::dialog::file::showHiddenVar', '0') 

654 except: 

655 pass 

656 # Limit the maximum window size to the size of the screen (layout can be scrolled in this) 

657 # Note the slight adjustment for the window title bar - this makes it a perfect fit on the Pi 

658 screen_width = root.winfo_screenwidth() 

659 screen_height = root.winfo_screenheight()-30 

660 root.maxsize(screen_width,screen_height) 

661 # Store the root window reference for use by the library functions 

662 library_common.set_root_window(root) 

663 # Create the menubar and editor canvas (canvas size will be set on creation) 

664 main_window_menubar = main_menubar(root) 

665 # Bind the close window event to the editor quit function to perform an orderly shutdown 

666 root.protocol("WM_DELETE_WINDOW", main_window_menubar.quit_schematic) 

667 # Enter the TKinter main loop (with exception handling for keyboardinterrupt) 

668 try: root.mainloop() 

669 except KeyboardInterrupt: 

670 logging.info("Keyboard Interrupt - Shutting down") 

671 # Kill off the PhotoImage objects so we don't get spurious exceptions on window close and 

672 # perform an orderly shutdown (cleanup and disconnect from the MQTT broker, Switch off DCC 

673 # power and disconnect from the serial port, Revert all GPIO ports to their default states 

674 # and then wait until all scheduled Tkinter tasks have completed before destroying root 

675 schematic.shutdown() 

676 library_common.shutdown() 

677 print("Exiting Model Railway Signalling application") 

678 

679####################################################################################