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

339 statements  

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

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

2# This provides a basic CBUS interface fpor communicating with the Pi-SPROG3 via the Raspberry Pi  

3# UART. It does not provide a fully-functional interface for all DCC command and control functions, 

4# just the minimum set needed to support the driving of signals and points via a selection of common 

5# DCC Accessory decoders. Basic CV Programming is also supported - primarily as an aid to testing.  

6# For full decoder programming the recommendation is to use JRMI DecoderPro or similar. 

7#-------------------------------------------------------------------------------------------------- 

8# 

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

10#  

11# sprog_connect() - Opens and configures the serial comms port to the Pi Sprog and issues 

12# a 'Request Command Station Status' command to confirm connectivity. 

13# Returns True - if communication has been established (otherwise False) 

14# Optional Parameters: 

15# port_name:str - The serial port to use for the Pi-SPROG 3 - Default="/dev/serial0", 

16# baud_rate:int - The baud rate to use for the serial port - Default = 460800, 

17# dcc_debug_mode:bool - Set to 'True' for enhanced debug logging 

18# 

19# sprog_disconnect() - Performs an ordely shutdown of communications and closes the comms port  

20# Returns True - if the communications port has been closed (otherwise False) 

21# 

22# service_mode_read_cv - Queries a CV in direct bit mode and waits for response 

23# (events are only sent if the track power is currently switched on) 

24# (request times out after 5 secs if the request was unsuccessful) 

25# Mandatory Parameters: 

26# cv:int - The CV (Configuration Variable) to be read 

27# returns the current value of the CV if a response is received 

28# returns None - if the request fails or the request times out 

29#  

30# service_mode_write_cv - programmes a CV in direct bit mode and waits for response 

31# (events are only sent if the track power is currently switched on) 

32# (request times out after 5 secs if the request was unsuccessful) 

33# Mandatory Parameters: 

34# cv:int - The CV (Configuration Variable) to be programmed 

35# value:int - The value to programme 

36# returns True - if we have acknowledgement that the CV has been programmed 

37# returns False - if the CV programming fails or the request times out 

38#  

39# request_dcc_power_on - sends request to switch on the power and waits for acknowledgement 

40# (requests only sent if the Comms Port has been successfully opened/configured) 

41# returns True - if we have acknowledgement that Track Power has been turned on 

42# returns False - if the request times out 

43#  

44# request_dcc_power_off - sends request to switch off the power and waits for acknowledgement 

45# (requests only sent if the Comms Port has been successfully opened/configured) 

46# returns True - if we have acknowledgement that Track Power has been turned off 

47# returns False - if the request times out 

48# 

49# Classes and functions used by the other library modules: 

50# 

51# send_accessory_short_event(address:int, active:bool) - sends out a CBUS command to the 

52# Pi-Sprog to be translated into a DCC command for transmission on the DCC Bus 

53# 

54# sprog_shutdown() - performs an ordely DCC power off and disconnect 

55# 

56# -------------------------------------------------------------------------------------------- 

57# 

58# Note that the Pi-SPROG-3 needs the UART interfaces to be swapped so that 

59# serial0 is routed to the GPIO connector instead of being used for BlueTooth. 

60# The configuration procedure is documented below 

61# 

62# 1) Download the uart-rtscts overlay: 

63# wget https://raw.github.com/HiassofT/AtariSIO/master/contrib/rpi/uart- ctsrts.dtbo 

64# 2) Copy it to the required directory: 

65# sudo cp uart-ctsrts.dtbo /boot/overlays/ 

66# 3) Add the overlays to the end of config.txt: 

67# sudo nano /boot/config.txt - and then add the following lines: 

68# dtoverlay=miniuart-bt 

69# enable_uart=1 

70# dtoverlay=uart-ctsrts 

71# 4) Edit the command line to prevent the Kernel using the UART at startup: 

72# sudo nano /boot/cmdline.txt  

73# Remove ‘console=serial0,115200’ 

74# Note that this file must contain only one line 

75# 5) Reboot the Raspberry Pi 

76# 

77# -------------------------------------------------------------------------------------------- 

78 

79import threading 

80import serial 

81import time 

82import logging 

83import queue 

84 

85# Global class for the Serial Port (port is configured/opened later) 

86serial_port = serial.Serial() 

87 

88# Global constants used when transmitting CBUS messages 

89can_bus_id = 1 # The arbitary CANBUS ID we will use for the Pi 

90pi_cbus_node = 1 # The arbitary CBUS Node ID we will use for the Pi 

91transmit_delay = 0.02 # The delay between sending CBUS Messages (in seconds) 

92 

93# Global Flag to enable enhanced debug logging for the Pi-SPROG interface 

94debug = False # Enhanced Debug logging - set by the sprog_connect call 

95 

96# Global flags used to communicate between the Rx thread and the calling function (in the main thread) 

97ton_response = False # Flag to confirm that we have had a response that track power is on 

98tof_response = False # Flag to confirm that we have had a response that track power is off 

99rstat_response = False # Flag to confirm we have had a RSTAT response (on initialisation) 

100qnn_response = False # Flag to confirm we have had a PNN response 

101service_mode_response = None # The response code from the sstat response (program a CV) 

102service_mode_cv_value = None # The returned value from the pcvs response (query a CV) 

103service_mode_cv_address = None # The reported CV address from the pcvs response 

104service_mode_session_id = None # The reported session ID from the sstat/pcvs responses 

105dcc_power_on = None # What we think the status of the DCC Track power is (None=Unknown) 

106 

107# Global 'one up' session ID (to match up the CV programming responses with the requests) 

108session_id = 1 

109 

110# Global variables to coordinate serial port access between the various threads 

111port_close_initiated = False # Signal to the Rx/Tx threads to shut down 

112rx_thread_terminated = True # Rx thread terminated flag 

113tx_thread_terminated = True # Tx thread terminated flag 

114 

115# The global output buffer for messages sent from the main thread to the SPROG Tx Thread 

116# We use a seperate thread so we can throttle the Tx rate without blocking the main thread 

117output_buffer = queue.Queue() 

118 

119#------------------------------------------------------------------------------ 

120# Common function used by the main thread to wait for responses in other threads. 

121# When the specified function returns True, the function exits and returns True. 

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

123 

124def wait_for_response(timeout:float,test_for_response_function): 

125 response_received = False 

126 timeout_start = time.time() 

127 while time.time() < timeout_start + timeout: 127 ↛ 131line 127 didn't jump to line 131, because the condition on line 127 was never false

128 response_received = test_for_response_function() 

129 if response_received: break 

130 time.sleep(0.001) 

131 return(response_received) 

132 

133#------------------------------------------------------------------------------ 

134# Internal thread to write queued CBUS messages to the Serial Port with a 

135# short delay in between each message. We do this because some decoders don't 

136# seem to process all messages if sent to them in quick succession - and if the 

137# decoder "misses" an event the signal/point may end up in an erronous state. 

138#------------------------------------------------------------------------------ 

139 

140def thread_to_send_buffered_data(): 

141 global tx_thread_terminated 

142 # Reset the output buffer and clear down the queue to be on the safe side 

143 if serial_port.is_open: serial_port.reset_output_buffer() 

144 output_buffer.queue.clear() 

145 # The main thread triggers a shutdown of this thread by setting port_close_initiated to TRUE 

146 # Just before this thread exits, it sets tx_thread_active to FALSE to confirm thread is exiting 

147 tx_thread_terminated = False 

148 while not port_close_initiated: 

149 # Get the next message to transmit from the buffer 

150 if not output_buffer.empty(): 

151 command_string = output_buffer.get() 

152 # Write the CBUS Message to the serial port (as long as the port is open) 

153 if serial_port.is_open: 153 ↛ 162line 153 didn't jump to line 162, because the condition on line 153 was never false

154 try: 

155 serial_port.write(bytes(command_string,"Ascii")) 

156 # Print the Transmitted message (if the appropriate debug level is set) 

157 if debug: logging.debug("Pi-SPROG: Tx thread - Sent CBUS Message: "+command_string) 

158 except Exception as exception: 

159 logging.error("Pi-SPROG: Tx thread - Error sending CBUS Message: "+command_string+" - "+str(exception)) 

160 time.sleep(1.0) 

161 else: 

162 if debug: logging.debug("Pi-SPROG: Tx thread - Not sending CBUS Message: "+command_string+" - port is closed") 

163 # Sleep (transmit_delay) before sending the next CBUS message (to throttle the Tx rate).  

164 # This also ensures the thread doesn't hog all the CPU time 

165 time.sleep(transmit_delay) 

166 if debug: logging.debug("Pi-SPROG: Tx Thread - exiting") 

167 tx_thread_terminated = True 

168 return() 

169 

170#------------------------------------------------------------------------------ 

171# Internal thread to read CBUS messages from the Serial Port and process them 

172# We are only really interested in the response from byte_string [7] onwards 

173# byte_string[7] and byte_string[8] are the Hex representation of the 1 byte OPCODE  

174# The remaining characters represent the data bytes (0 to 7 associated with the OPCODE) 

175# Finally the string is terminated with a ';' (Note there is no '\r' required) 

176#------------------------------------------------------------------------------ 

177 

178def thread_to_read_received_data (): 

179 global rx_thread_terminated 

180 # Reset the input buffer to be on the safe side 

181 if serial_port.is_open: serial_port.reset_input_buffer() 

182 # The main thread triggers a shutdown of this thread by setting port_close_initiated to TRUE 

183 # Just before this thread exits, it sets rx_thread_active to FALSE to confirm thread is exiting 

184 rx_thread_terminated = False 

185 while not port_close_initiated: 

186 # Read the serial port to retrieve the bytes in the Rx buffer (as long as the port is open) 

187 if serial_port.is_open: 187 ↛ 225line 187 didn't jump to line 225, because the condition on line 187 was never false

188 # Read from the port until we get the GridConnect Protocol message termination character 

189 # Or we get notified to shutdown (main thread sets port_close_initiated to True) 

190 byte_string = bytearray() 

191 while not port_close_initiated: 

192 if serial_port.in_waiting > 0: 

193 try: 

194 received_data = serial_port.read() 

195 except Exception as exception: 

196 logging.error("Pi-SPROG: Rx thread - Error reading serial port - "+str(exception)) 

197 time.sleep(1.0) 

198 else: 

199 byte_string = byte_string + received_data 

200 if chr(byte_string[-1]) == ";": break 

201 # Ensure the loop doesn't hog all the CPU time 

202 time.sleep(0.001) 

203 # Process the GridConnect Protocol message (as long as it is complete) 

204 if len(byte_string) > 0 and chr(byte_string[-1]) == ";": 

205 # Log the Received message (if the appropriate debug level is set) 

206 if debug: logging.debug("Pi-SPROG: Rx thread - Received CBUS Message: "+byte_string.decode('Ascii')+"\r") 

207 # Note that there is exception handling around the decoding of the message to 

208 # deal with any "edge-case" exceptions we might get with corrupted messages 

209 try: 

210 # Extract the OpCode and Process the message (only a subset of messages is supported) 

211 op_code = int((chr(byte_string[7]) + chr(byte_string[8])),16) 

212 # Command Station Status Report (0xE3 = 227 decimal) 

213 if op_code == 227: process_stat_message(byte_string) 

214 # Response to confirm Track Power is OFF (0x04 = 4 decimal) 

215 elif op_code == 4: process_tof_message(byte_string) 

216 # Response to confirm Track Power is ON (0x05 = 5 decimal) 

217 elif op_code == 5: process_ton_message(byte_string) 

218 # Report CV value in service programming mode (0x85 = 133 decimal) 

219 elif op_code == 133: process_pcvs_message(byte_string) 

220 # Report Service Mode Status response (0x4C = 76 decimal)  

221 elif op_code == 76: process_sstat_message(byte_string) 

222 except: 

223 logging.warning("Pi-SPROG: Rx thread - Couldn't decode CBUS Message: "+byte_string.decode('Ascii')+"\r") 

224 # Ensure the thread doesn't hog all the CPU time 

225 time.sleep(0.001) 

226 if debug: logging.debug("Pi-SPROG: Rx Thread - exiting") 

227 rx_thread_terminated = True 

228 return() 

229 

230#------------------------------------------------------------------------------ 

231# Internal function to process a Command Station Status Report (STAT message) 

232# Sets the rstat_response flag - to signal back into the main thread 

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

234 

235def process_stat_message(byte_string): 

236 global rstat_response 

237 # Print out the status report (if the appropriate debug level is set) 

238 if debug: 

239 logging.debug ("Pi-SPROG: Rx thread - Received STAT (Command Station Status Report):") 

240 logging.debug (" Node Id :"+str(int(chr(byte_string[9]) + chr(byte_string[10]) 

241 + chr(byte_string[11]) + chr(byte_string[12]),16))) 

242 logging.debug (" CS Number :"+str(int(chr(byte_string[13]) + chr(byte_string[14]),16))) 

243 logging.debug (" Version :"+str(int(chr(byte_string[17]) + chr(byte_string[18]),16))+"." 

244 +str(int(chr(byte_string[19]) + chr(byte_string[20]),16))+"." 

245 +str(int(chr(byte_string[21]) + chr(byte_string[22]),16))) 

246 # Get the Flags - we only need the last hex character (to get the 4 bits) 

247 flags = int(chr(byte_string[16]),16) 

248 logging.debug (" Reserved :"+str((flags & 0x080)==0x80)) 

249 logging.debug (" Service Mode :"+str((flags & 0x040)==0x40)) 

250 logging.debug (" Reset Done :"+str((flags & 0x02)==0x20)) 

251 logging.debug (" Emg Stop Perf :"+str((flags & 0x10)==0x10)) 

252 logging.debug (" Bus On :"+str((flags & 0x08)==0x08)) 

253 logging.debug (" Track On :"+str((flags & 0x04)==0x04)) 

254 logging.debug (" Track Error :"+str((flags & 0x02)==0x02)) 

255 logging.debug (" H/W Error :"+str((flags & 0x01)==0x01)+"\r") 

256 # Respond to the trigger function (waiting in the main thread for a response) 

257 rstat_response = True 

258 return() 

259 

260#------------------------------------------------------------------------------ 

261# Internal function to process a Track power responses (TOF/TON messages) 

262# Sets the appropriate acknowledge flag - to signal back into the main thread 

263#------------------------------------------------------------------------------ 

264 

265def process_tof_message(byte_string): 

266 global tof_response 

267 global dcc_power_on 

268 if debug: logging.debug ("Pi-SPROG: Rx thread - Received TOF (Track OFF) acknowledgement") 

269 # Respond to the trigger function (waiting in the main thread for a response) 

270 tof_response = True 

271 dcc_power_on = False 

272 return() 

273 

274def process_ton_message(byte_string): 

275 global ton_response 

276 global dcc_power_on 

277 if debug: logging.debug ("Pi-SPROG: Rx thread - Received TON (Track ON) acknowledgement") 

278 # Respond to the trigger function (waiting in the main thread for a response) 

279 ton_response = True 

280 dcc_power_on = True 

281 return() 

282 

283#------------------------------------------------------------------------------ 

284# Internal function to process a Report CV response (PCVS message) 

285# Sets the service_mode_cv_value - to signal back into the main thread 

286#------------------------------------------------------------------------------ 

287 

288def process_pcvs_message(byte_string): 

289 global service_mode_cv_value 

290 global service_mode_cv_address 

291 global service_mode_session_id 

292 # Response contains [header]<85><Session><High CV#><Low CV#><Val> 

293 session_id = int(chr(byte_string[9]) + chr(byte_string[10]),16) 

294 cv = ( int(chr(byte_string[11]) + chr(byte_string[12]),16) + 

295 int(chr(byte_string[13]) + chr(byte_string[14]),16) ) 

296 value = int(chr(byte_string[15]) + chr(byte_string[16]),16) 

297 if debug: logging.debug ("Pi-SPROG: Rx thread - Received PCVS (Report CV) - Session:"+ 

298 str(session_id)+", CV:"+str(cv)+", Value:"+str(value)) 

299 # Respond to the trigger function (waiting in the main thread for a response) 

300 service_mode_session_id = session_id 

301 service_mode_cv_address = cv 

302 service_mode_cv_value = value 

303 return() 

304 

305#------------------------------------------------------------------------------ 

306# Internal function to process a service mode status response (SSTAT message) 

307# Sets the service_mode_response - to signal back into the main thread 

308#------------------------------------------------------------------------------ 

309 

310def process_sstat_message(byte_string): 

311 global service_mode_response 

312 global service_mode_session_id 

313 session_id = int(chr(byte_string[9]) + chr(byte_string[10]),16) 

314 service_mode_status = int(chr(byte_string[11]) + chr(byte_string[12]),16) 

315 if service_mode_status == 0: status = "Reserved" 315 ↛ 322line 315 didn't jump to line 322

316 elif service_mode_status == 1: status = "No Acknowledge" 316 ↛ 322line 316 didn't jump to line 322

317 elif service_mode_status == 2: status = "Overload on Programming Track" 317 ↛ 322line 317 didn't jump to line 322

318 elif service_mode_status == 3: status = "Write Acknowledge" 318 ↛ 319line 318 didn't jump to line 319, because the condition on line 318 was never false

319 elif service_mode_status == 4: status = "Busy" 

320 elif service_mode_status == 5: status = "CV Out of Range" 

321 else: status = "Unrecognised response code" + str (service_mode_status) 

322 if debug: logging.debug ("Pi-SPROG: Rx thread - Received SSTAT (Service Mode Status) - Session:" 

323 + str(session_id)+", Status:" + status) 

324 # Respond to the trigger function (waiting in the main thread for a response) 

325 service_mode_session_id = session_id 

326 service_mode_response = service_mode_status 

327 return() 

328 

329#------------------------------------------------------------------------------ 

330# Internal function to encode a CBUS Command in the GridConnect protocol and send to 

331# the specified comms port. The format of a CBUS Command is summarised as follows: 

332# 

333# CBUS Commands are sent as an ASCII strings starting with ':' and followed by 'S' 

334# The next 4 characters are the Hex representation of the 11 bit CAN Header 

335# Major Priority - Bits 9 & 10 (00 = Emergency; 01 = High Priority, 10 = Normal) 

336# Minor Priority - Bits 7 & 8 (00 = High, 01 = Above Normal, 10 = Normal, 11 = Low) 

337# CAN BUS ID - Bits 0-6 (CAN segment unique ID) 

338# Note that The 11 CAN Header Bits are encoded into the 2 Bytes LEFT JUSTIFIED 

339# So the Header bytes end up as: P P P P A A A A | A A A 0 0 0 0 0 

340# The next character is 'N' - specifying a 'Normal' CBUS Frame (all we need to use) 

341# The next 2 characters are the Hex representation of the 1 byte OPCODE  

342# The remaining characters represent the data bytes (0 to 7 associated with the OPCODE) 

343# Finally the string is terminated with a ';' (Note there is no '\r' required) 

344# 

345# References for Header Encoding - CBUS Developers Guide - Section 6.1, 6.4, 12.2 

346# 

347# Example - can_id=99 , mj_pri=2, min_pri=2, op_code=9 (RTON - request track on) 

348# encodes into a CBUS Command 'SAC60N09;' 

349#------------------------------------------------------------------------------ 

350 

351def send_cbus_command (mj_pri:int, min_pri:int, op_code:int, *data_bytes:int): 

352 if (mj_pri < 0 or mj_pri > 2): 352 ↛ 353line 352 didn't jump to line 353, because the condition on line 352 was never true

353 logging.error("Pi-SPROG: CBUS Command - Invalid Major Priority "+str(mj_pri)) 

354 elif (min_pri < 0 or min_pri > 3): 354 ↛ 355line 354 didn't jump to line 355, because the condition on line 354 was never true

355 logging.error("Pi-SPROG: CBUS Command - Invalid Minor Priority "+str(min_pri)) 

356 elif (op_code < 0 or op_code > 255): 356 ↛ 357line 356 didn't jump to line 357, because the condition on line 356 was never true

357 logging.error("Pi-SPROG: CBUS Command - Op Code out of range "+str(op_code)) 

358 else: 

359 # Encode the CAN Header  

360 header_byte1 = (mj_pri << 6) | (min_pri <<4) | (can_bus_id >> 3) 

361 header_byte2 = (0x1F & can_bus_id) << 5 

362 # Start building the GridConnect Protocol string for the CBUS command 

363 command_string = (":S" + format(header_byte1,"02X") + format(header_byte2,"02X") 

364 + "N" + format (op_code,"02X")) 

365 # Add the Data Bytes associated with the OpCode (if there are any) 

366 for data_byte in data_bytes: command_string = command_string + format(data_byte,"02X") 

367 # Finally - add the command string termination character 

368 command_string = command_string + ";" 

369 # Add the command to the output buffer (to be picked up by the Tx thread) 

370 output_buffer.put(command_string) 

371 return() 

372 

373#------------------------------------------------------------------------------ 

374# Externally Called Function to establish basic comms with the PI-SPROG 

375# (opening the port and sending an RSTAT command to confirm connectivity) 

376# With dcc_debug_mode=True an "enhanced" level of debug logging is enabled 

377# namely logging of all the CBUS commands sent to the Pi SPROG and other 

378# log messages associated with internal state of the threads 

379#------------------------------------------------------------------------------ 

380 

381def sprog_connect (port_name:str="/dev/serial0", 

382 baud_rate:int = 115200, 

383 dcc_debug_mode:bool = False): 

384 global debug 

385 pi_sprog_connected = False 

386 if not isinstance(port_name, str): 

387 logging.error("Pi-SPROG: sprog_connect - Port name must be specified as a string") 

388 elif not isinstance(baud_rate, int): 

389 logging.error("Pi-SPROG: sprog_connect - Baud rate must be specified as an integer") 

390 elif not isinstance(dcc_debug_mode, bool): 

391 logging.error("Pi-SPROG: sprog_connect - Enhanced debug flag must be specified as a boolean") 

392 else: 

393 # If the serial port is already open then close it before re-configuring 

394 if serial_port.is_open: sprog_disconnect() 

395 # Assign the global "enhanced debugging" flag 

396 debug = dcc_debug_mode 

397 # Configure the port - note the zero timeout so the Rx thread does not block 

398 # The Rx thread combines the data read from the port into 'complete' CBUS messages 

399 serial_port.port = port_name 

400 serial_port.baudrate = baud_rate 

401 serial_port.bytesize = 8 

402 serial_port.timeout = 0 # Non blocking - returns immediately 

403 serial_port.parity = serial.PARITY_NONE 

404 serial_port.stopbits = serial.STOPBITS_ONE 

405 # Try to open the serial port (catching any exceptions) 

406 logging.debug("Pi-SPROG: Opening Serial Port: "+port_name+" - baud: "+str(baud_rate)) 

407 try: 

408 serial_port.open() 

409 except Exception as exception: 

410 # If the attempt to open the serial port fails then we catch the exception (and return) 

411 logging.error("Pi-SPROG: Error opening Serial Port - "+str(exception)) 

412 else: 

413 # The port has been successfully opened. We now start the Rx and Tx threads. 

414 # These are shut down in a controlled manner by the sprog_disconnect function but 

415 # if all else fails we set to Daemon so they will terminate with the main programme 

416 if rx_thread_terminated: 416 ↛ 421line 416 didn't jump to line 421, because the condition on line 416 was never false

417 if debug: logging.debug("Pi-SPROG: Starting Rx Thread") 

418 rx_thread = threading.Thread (target=thread_to_read_received_data) 

419 rx_thread.setDaemon(True) 

420 rx_thread.start() 

421 if tx_thread_terminated: 421 ↛ 427line 421 didn't jump to line 427, because the condition on line 421 was never false

422 if debug: logging.debug("Pi-SPROG: Starting Tx Thread") 

423 tx_thread = threading.Thread (target=thread_to_send_buffered_data) 

424 tx_thread.setDaemon(True) 

425 tx_thread.start() 

426 # Short delay to allow the threads to fully start up before we continue 

427 time.sleep(0.1) 

428 # To verify full connectivity, we query the command station status 

429 # query_command_station_status will return TRUE if a response was received 

430 pi_sprog_connected = query_command_station_status() 

431 if pi_sprog_connected: logging.info("Pi-SPROG: Successfully connected to Pi-SPROG") 

432 return(pi_sprog_connected) 

433 

434#------------------------------------------------------------------------------ 

435# Externally Called Function to disconnect from the PI-SPROG (close the port) 

436# so that the port is free for other applications to use if required. 

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

438 

439def sprog_disconnect(): 

440 global port_close_initiated 

441 def response_received(): return(rx_thread_terminated and tx_thread_terminated) 

442 pi_sprog_disconnected = False 

443 if serial_port.is_open: 

444 if debug: logging.debug("Pi-SPROG: Shutting down Tx and Rx Threads") 

445 port_close_initiated = True 

446 # Wait until we get confirmation the Threads have been terminated 

447 wait_for_response(0.5, response_received) 

448 if not tx_thread_terminated: logging.error("Pi-SPROG: Tx thread failed to terminate") 

449 if not rx_thread_terminated: logging.error("Pi-SPROG: Rx thread failed to terminate") 

450 # Try to close the serial port (with exception handling) 

451 if debug: logging.debug ("Pi-SPROG: Closing Serial Port") 

452 try: 

453 serial_port.close() 

454 except Exception as exception: 

455 logging.error("Pi-SPROG: Error closing Serial Port - "+str(exception)) 

456 if not serial_port.is_open: 456 ↛ 460line 456 didn't jump to line 460, because the condition on line 456 was never false

457 logging.info("Pi-SPROG: Successfully disconnected from Pi-SPROG") 

458 pi_sprog_disconnected = True 

459 # Reset the port_close_initiated flag (ready for the next time) 

460 port_close_initiated = False 

461 return(pi_sprog_disconnected) 

462 

463#------------------------------------------------------------------------------ 

464# Function called on shutdown to turn off DCC bus power and close the comms port 

465#------------------------------------------------------------------------------ 

466 

467def sprog_shutdown(): 

468 # Ensure the track power is turned off 

469 if serial_port.is_open: request_dcc_power_off() 

470 # Now close the comms port and exit 

471 sprog_disconnect() 

472 return() 

473 

474#------------------------------------------------------------------------------ 

475# Function to send a RSTAT (Request command Station Status) command (response logged) 

476# Returns True if successful and False if no response is received (timeout) 

477# Results in a character string of ':SA020N0C' being sent out to the Pi-SPROG 

478# The only bit we really care about is the bit after the 'N', which is the op code 

479# we are seding out (in this case '0C' ) - see above for info on the header 

480#------------------------------------------------------------------------------ 

481 

482def query_command_station_status(): 

483 global rstat_response 

484 def response_received(): return(rstat_response) 

485 rstat_response = False 

486 # Only bother sending commands to the Pi Sprog if the serial port has been opened 

487 if serial_port.is_open: 487 ↛ 501line 487 didn't jump to line 501, because the condition on line 487 was never false

488 # Retry sending the command (3 attempts) if we don't get a response 

489 attempts = 0 

490 while attempts < 3: 490 ↛ 498line 490 didn't jump to line 498, because the condition on line 490 was never false

491 # Query the status of the command station to confirm connectivity (0x0C = 12 decimal) 

492 logging.debug("Pi-SPROG: Sending RSTAT command (Request Command Station Status)") 

493 send_cbus_command(mj_pri=2, min_pri=2, op_code=12) 

494 # Wait for the response (with a 1 second timeout) 

495 if wait_for_response(1.0, response_received): break 495 ↛ 496line 495 didn't jump to line 496, because the condition on line 495 was never false

496 attempts = attempts + 1 

497 logging.warning("Pi-SPROG: Request Command Station Status timeout - retrying") 

498 if rstat_response: logging.debug ("Pi-SPROG: Received STAT (Command Station Status Report)") 498 ↛ 499line 498 didn't jump to line 499, because the condition on line 498 was never false

499 else: logging.error("Pi-SPROG: Request Command Station Status failed") 

500 else: 

501 logging.warning("Pi-SPROG: Cannot Request Command Station Status - SPROG is disconnected") 

502 return(rstat_response) 

503 

504#------------------------------------------------------------------------------ 

505# Externally Called Function to turn on the track power 

506#------------------------------------------------------------------------------ 

507 

508def request_dcc_power_on(): 

509 global ton_response 

510 def response_received(): return(ton_response) 

511 ton_response = False 

512 # Only bother sending commands to the Pi Sprog if the serial port has been opened 

513 if serial_port.is_open: 

514 # Retry sending the command (3 attempts) if we don't get a response 

515 attempts = 0 

516 while attempts < 3: 516 ↛ 524line 516 didn't jump to line 524, because the condition on line 516 was never false

517 # Send the command to switch on the Track Supply (to the DCC Bus) 

518 logging.debug ("Pi-SPROG: Sending RTON command (Request Track Power On)") 

519 send_cbus_command (mj_pri=2, min_pri=2, op_code=9) 

520 # Wait for the response (with a 1 second timeout) 

521 if wait_for_response(1.0, response_received): break 521 ↛ 522line 521 didn't jump to line 522, because the condition on line 521 was never false

522 attempts = attempts + 1 

523 logging.warning("Pi-SPROG: Request Track Power On timeout - retrying") 

524 if ton_response: 524 ↛ 527line 524 didn't jump to line 527, because the condition on line 524 was never false

525 logging.debug("Pi-SPROG: Received TON (Track ON) acknowledgement") 

526 logging.info("Pi-SPROG: Track power has been turned ON") 

527 else: logging.error("Pi-SPROG: Request to turn on Track Power failed") 

528 # Give things time to get established before sending out any commands 

529 time.sleep (0.1) 

530 else: 

531 logging.warning("Pi-SPROG: Cannot Request Track Power On - SPROG is disconnected") 

532 return(ton_response) 

533 

534#------------------------------------------------------------------------------ 

535# Externally Called Function to turn off the track power 

536#------------------------------------------------------------------------------ 

537 

538def request_dcc_power_off(): 

539 global tof_response 

540 def response_received(): return(tof_response) 

541 tof_response = False 

542 # Only bother sending commands to the Pi Sprog if the serial port has been opened 

543 if serial_port.is_open: 

544 # Retry sending the command (3 attempts) if we don't get a response 

545 attempts = 0 

546 while attempts < 3: 546 ↛ 554line 546 didn't jump to line 554, because the condition on line 546 was never false

547 # Send the command to switch on the Track Supply (to the DCC Bus) 

548 logging.debug("Pi-SPROG: Sending RTOF command (Request Track Power Off)") 

549 send_cbus_command(mj_pri=2, min_pri=2, op_code=8) 

550 # Wait for the response (with a 1 second timeout) 

551 if wait_for_response(1.0, response_received): break 551 ↛ 552line 551 didn't jump to line 552, because the condition on line 551 was never false

552 attempts = attempts + 1 

553 logging.warning("Pi-SPROG: Request Track Power Off timeout - retrying") 

554 if tof_response: 554 ↛ 557line 554 didn't jump to line 557, because the condition on line 554 was never false

555 logging.debug("Pi-SPROG: Received TOF (Track OFF) acknowledgement") 

556 logging.info("Pi-SPROG: Track power has been turned OFF") 

557 else: logging.error("Pi-SPROG: Request to turn off Track Power failed") 

558 else: 

559 logging.warning("Pi-SPROG: Cannot Request Track Power Off - SPROG is disconnected") 

560 return(tof_response) 

561 

562#------------------------------------------------------------------------------ 

563# Externally Called Function to send an Accessory Short CBUS On/Off Event 

564#------------------------------------------------------------------------------ 

565 

566def send_accessory_short_event(address:int, active:bool): 

567 if not isinstance(address, int): 

568 logging.error("Pi-SPROG: send_accessory_short_event - Address must be specified as an integer") 

569 elif not isinstance(active, bool): 

570 logging.error("Pi-SPROG: send_accessory_short_event - State must be specified as a boolean") 

571 elif (address < 1 or address > 2047): 

572 logging.error("Pi-SPROG: send_accessory_short_event - Invalid address specified: "+ str(address)) 

573 # Only bother sending commands to the Pi Sprog if the serial port has been opened 

574 elif serial_port.is_open and dcc_power_on: 

575 # Encode the message into the required number of bytes 

576 byte1 = (pi_cbus_node & 0xff00) >> 8 

577 byte2 = (pi_cbus_node & 0x00ff) 

578 byte3 = (address & 0xff00) >> 8 

579 byte4 = (address & 0x00ff) 

580 # Send a ASON or ASOF Command (Accessoy Short On or Accessory Short Off) 

581 if active: 

582 logging.debug("Pi-SPROG: Sending DCC command ASON (Accessory Short ON) to DCC address: "+ str(address)) 

583 send_cbus_command(2, 3, 152, byte1, byte2, byte3, byte4) 

584 else: 

585 logging.debug("Pi-SPROG: Sending DCC command ASOF (Accessory Short OFF) to DCC address: "+ str(address)) 

586 send_cbus_command(2, 3, 153, byte1, byte2, byte3, byte4) 

587 elif debug: 

588 # Note we only log the discard messages in enhanced debugging mode (to reduce the spam in the logs) 

589 if active: log_string ="Discarding ASON command to DCC address: "+ str(address) 

590 else: log_string = "Discarding ASOF command to DCC address: "+ str(address) 

591 logging.debug("Pi-SPROG: "+log_string+" - SPROG is disconnected or DCC power is OFF") 

592 return () 

593 

594#------------------------------------------------------------------------------ 

595# Externally Called Function to read a single CV (used for testing) 

596# Returns (Value) if successfull or (None) if the request timed out 

597#------------------------------------------------------------------------------ 

598 

599def service_mode_read_cv(cv:int): 

600 global service_mode_cv_value 

601 global service_mode_cv_address 

602 global service_mode_session_id 

603 global session_id 

604 def response_received(): return(service_mode_cv_value is not None) 

605 service_mode_cv_value = None 

606 service_mode_cv_address = None 

607 if not isinstance(cv, int): 

608 logging.error("Pi-SPROG: service_mode_read_cv - CV to read must be specified as an integer") 

609 elif (cv < 0 or cv > 1023): 

610 logging.error("Pi-SPROG: service_mode_read_cv - Invalid CV specified: "+str(cv)) 

611 # Only bother sending commands to the Pi Sprog if the serial port has been opened 

612 elif serial_port.is_open and dcc_power_on: 

613 # Encode the message into the required number of bytes 

614 byte1 = session_id # Session ID 

615 byte2 = (cv & 0xff00) >> 8 # High CV 

616 byte3 = (cv & 0x00ff) # Low CV 

617 byte4 = 1 # Mode (1 = Direct bit) 

618 # Sending the QCVS command (without any re-tries) 

619 logging.debug ("Pi-SPROG: Sending QCVS (Read CV in Service Mode) - Session:"+str(byte1)+", CV:"+str(cv)) 

620 # Command to send is 0x84 (=132 Decimal) - Read CV in Service Mode (QCVS)  

621 send_cbus_command(2, 2, 132, byte1, byte2, byte3, byte4) 

622 # Wait for the response (with a 5 second timeout - this takes a long time) 

623 if wait_for_response(5.0, response_received): 623 ↛ 636line 623 didn't jump to line 636, because the condition on line 623 was never false

624 logging.debug("Pi-SPROG: Received PCVS (Report CV) - Session:"+ str(service_mode_session_id)+ 

625 ", CV:"+str(service_mode_cv_address)+", Value:"+str(service_mode_cv_value)) 

626 if service_mode_cv_address != cv: 626 ↛ 627line 626 didn't jump to line 627, because the condition on line 626 was never true

627 logging.error("Pi-SPROG: Failed to read CV "+str(cv)+" - Responded with incorrect CV address") 

628 service_mode_cv_value = None 

629 elif service_mode_session_id != session_id: 629 ↛ 630line 629 didn't jump to line 630, because the condition on line 629 was never true

630 logging.error("Pi-SPROG: Failed to read CV "+str(cv)+" - Responded with incorrect Session ID") 

631 service_mode_cv_value = None 

632 else: 

633 logging.info("Pi-SPROG: Successfully read CV"+str(service_mode_cv_address)+ 

634 " - value:"+str(service_mode_cv_value)) 

635 else: 

636 logging.error("Pi-SPROG: Failed to read CV "+str(cv)+" - Timeout awaiting response") 

637 # Increment the 'one up' session Id for the next time 

638 session_id = session_id + 1 

639 if session_id > 255: session_id = 1 

640 else: 

641 logging.warning("Pi-SPROG: Failed to read CV "+str(cv)+" - SPROG is disconnected or DCC power is off") 

642 return (service_mode_cv_value) 

643 

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

645# Externally Called Function to programme a single CV (used for testing) 

646# Returns True if successful and False if no response is received (timeout) 

647#------------------------------------------------------------------------------ 

648 

649def service_mode_write_cv(cv:int, value:int): 

650 global service_mode_response 

651 global service_mode_session_id 

652 global session_id 

653 def response_received(): return(service_mode_response is not None) 

654 service_mode_response = None 

655 service_mode_session_id = None 

656 if not isinstance(cv, int): 

657 logging.error("Pi-SPROG: service_mode_write_cv - CV to write must be specified as an integer") 

658 elif not isinstance(value, int): 

659 logging.error("Pi-SPROG: service_mode_write_cv - Value to write must be specified as an integer") 

660 elif (cv < 0 or cv > 1023): 

661 logging.error("Pi-SPROG: service_mode_write_cv - Invalid CV specified: "+str(cv)) 

662 elif (value < 0 or value > 255): 

663 logging.error("Pi-SPROG: service_mode_write_cv - CV "+str(cv)+" - Invalid value specified: "+str(value)) 

664 # Only try to send the command if the PI-SPROG-3 has initialised correctly 

665 elif serial_port.is_open and dcc_power_on: 

666 # Encode the message into the required number of bytes 

667 byte1 = session_id # Session ID 

668 byte2 = (cv & 0xff00) >> 8 # High CV 

669 byte3 = (cv & 0x00ff) # Low CV 

670 byte4 = 1 # Mode (1 = Direct bit) 

671 byte5 = value # value to write 

672 # Sending the WCVS command (without any re-tries) 

673 logging.debug("Pi-SPROG: Sending WCVS (Write CV in Service Mode) command - Session:" 

674 +str(byte1)+", CV:"+str(cv)+", Value:"+str(value)) 

675 # Command to send is 0xA2 (=162 Decimal) - Write CV in Service mode (WCVS) 

676 send_cbus_command(2, 2, 162, byte1, byte2, byte3, byte4, byte5) 

677 # Wait for the response (with a 5 second timeout) 

678 if wait_for_response(5.0, response_received): 678 ↛ 690line 678 didn't jump to line 690, because the condition on line 678 was never false

679 logging.debug("Pi-SPROG: Received SSTAT (Service Mode Status) - Session:" 

680 +str(service_mode_session_id)+", Status:"+str(service_mode_response)) 

681 if service_mode_session_id != session_id: 681 ↛ 682line 681 didn't jump to line 682, because the condition on line 681 was never true

682 logging.error("Pi-SPROG: Failed to write CV "+str(cv)+" - Responded with incorrect Session ID") 

683 service_mode_response = None 

684 elif service_mode_response != 3: 684 ↛ 685line 684 didn't jump to line 685, because the condition on line 684 was never true

685 logging.error("Pi-SPROG: Failed to write CV "+str(cv)+" - Error Code: "+str(service_mode_response)) 

686 service_mode_response = None 

687 else: 

688 logging.info("Pi-SPROG: Successfully programmed CV"+str(cv)+" with value:"+str(value)) 

689 else: 

690 logging.error("Pi-SPROG: Failed to write CV "+str(cv)+" - Timeout awaiting response") 

691 # Increment the 'one up' session Id for the next time 

692 session_id = session_id + 1 

693 if session_id > 255: session_id = 1 

694 else: 

695 logging.warning("Pi-SPROG: Failed to write CV "+str(cv)+" - SPROG is disconnected or DCC power is off") 

696 return(service_mode_response == 3) 

697 

698#------------------------------------------------------------------------------ 

699# Function to encode a standard 3-byte DCC Accessory Decoder Packet into 3 bytes 

700# for transmission to the PI-SPROG as a RDCC3 Command (Request 3-byte DCC Packet). 

701# Calls the 'send_cbus_command' function to actually encode and send the command 

702# The DCC Packet is sent <repeat> times - but not refreshed on a regular basis 

703# Acknowledgement to Java NMRA implementation (which this function follows closely) 

704#  

705# Packets are represented by an array of bytes. Preamble/postamble not included. 

706# Note that this is a data representation, NOT a representation of the waveform! 

707# From the NMRA RP: 0 10AAAAAA 0 1AAACDDD 0 EEEEEEEE 1, Where 

708# A = Address bits 

709# D = the output channel to set 

710# C = the State (1 = ON, 0 = OFF)  

711# E = the error detection bits 

712# 

713# Accessory Digital Decoders can control momentary or constant-on devices, the duration 

714# of time that each output is active being pre-configured by CVs #515 through #518. 

715# Bit 3 of the second byte "C" is used to activate or deactivate the addressed device. 

716# (Note if the duration the device is intended to be on is less than or equal the 

717# pre-configured duration, no deactivation is necessary) 

718# 

719# Since most devices are paired, the convention is that bit "0" of the second byte 

720# is used to distinguish between which of a pair of outputs the accessory decoder is 

721# activating or deactivating. 

722# 

723# Bits 1 and 2 of the second byte is used to indicate which of 4 pairs of outputs the 

724# accessory decoder is activating or deactivating 

725# 

726# The significant bits of the 9 bit address are bits 4-6 of the second data byte. 

727# By convention these three bits are in ones complement. The use of bit 7 of the second 

728# byte is reserved for future use. 

729# 

730# NOTE - This function is currently untested as I have been unable to confirm 

731# (either via research or Test) whether the Pi-SPROG-3 supports the RDDC3 Command  

732#------------------------------------------------------------------------------ 

733#  

734# def send_DCC_accessory_decoder_packet (address:int, active:bool, output_channel:int = 0, repeat:int = 3): 

735# global track_power_on 

736# if (address < 1 or address > 511): 

737# logging.info("Error: send_accessory_decoder_packet - Invalid address "+str(address)) 

738# elif (output_channel < 0 or output_channel > 7): 

739# logging.info("Error: send_accessory_decoder_packet - Invalid output channel " + 

740# str(output_channel)+" for address "+str(address))  

741# elif (repeat < 0 or repeat > 255): 

742# logging.info("Error: send_accessory_decoder_packet - Invalid Repeat Value " + 

743# str(repeat)+" for address "+str(address)) 

744# # Only try to send the command if the PI-SPROG-3 has initialised correctly 

745# elif track_power_on: 

746# low_addr = address & 0x3F 

747# high_addr = (( ~ address) >> 6) & 0x07 

748# byte1 = (0x80 | low_addr) 

749# byte2 = (0x80 | (high_addr << 4) | (active << 3) | output_channel & 0x07) 

750# byte3 = (byte1 ^ byte2) 

751# # Send a RDCC3 Command (Request 3-Byte DCC Packet) via the CBUS 

752# logging.debug ("PI >> SPROG - RDCC3 (Send 3 Byte DCC Packet) : Address:" 

753# + str(address) + " Channel:" + str(output_channel) +" State:" + str(active)) 

754# send_cbus_command (2, 2, 128, repeat, byte1, byte2, byte3) 

755# return () 

756# 

757#------------------------------------------------------------------------------ 

758# Function to encode a standard Extended DCC Accessory Decoder Packet into 4 bytes 

759# for transmission to the PI-SPROG as a RDCC4 Command (Request 4-byte DCC Packet). 

760# Calls the 'send_cbus_command' function to actually encode and send the command 

761# The DCC Packet is sent <repeat> times - but not refreshed on a regular basis 

762# Acknowledgement to Java NMRA implementation (which this function follows closely) 

763# 

764# Packets are represented by an array of bytes. Preamble/postamble not included. 

765# From the NMRA RP: 10AAAAAA 0 0AAA0AA1 0 000XXXXX 0 EEEEEEEE 1} 

766# A = Address bits 

767# X = The Aspect to display 

768# E = the error detection bits 

769# 

770# The addressing is not clear in te NRMA standard - Two interpretations are provided 

771# in the code (albeit one commented out) - thanks again to the Java NMRA implementation 

772# 

773# NOTE - This function is currently untested as I have been unable to confirm 

774# (either via research or Test) whether the Pi-SPROG-3 supports the RDDC4 Command  

775#------------------------------------------------------------------------------ 

776#  

777# def send_extended_DCC_accessory_decoder_packet (address:int, aspect:int, repeat:int = 3, alt_address = False): 

778# global track_power_on 

779# if (address < 1 or address > 2044): 

780# logging.info("Error: send_extended_DCC_accessory_decoder_packet - Invalid address "+str(address)) 

781# elif (aspect < 0 or aspect > 31): 

782# logging.info("Error: send_extended_DCC_accessory_decoder_packet - Invalid aspect "+str(aspect)) 

783# elif track_power_on: 

784# # DCC Address interpretation 1 and 2 

785# address -= 1 

786# low_addr = (address & 0x03) 

787# board_addr = (address >> 2) 

788# if alt_address: board_addr = board_addr+1 

789# mid_addr = board_addr & 0x3F 

790# high_addr = ((~board_addr) >> 6) & 0x07 

791# byte1 = (0x80 | mid_addr) 

792# byte2 = (0x01 | (high_addr << 4) | (low_addr << 1)) 

793# byte3 = (0x1F & aspect) 

794# byte4 = (byte1 ^ byte2 ^ byte3) 

795# # Send a RDCC4 Command (Request 4-Byte DCC Packet) via the CBUS 

796# logging.debug ("PI >> SPROG - RDCC4 (Send 4 Byte DCC Packet) : Address:" 

797# + str(address) + " Aspect:" + str(aspect)) 

798# send_cbus_command (2, 2, 160, repeat, byte1, byte2, byte3, byte4) 

799# return() 

800# 

801#------------------------------------------------------------------------------ 

802# Function to send a QNN (Query Node Number) command (response will be logged) 

803# Returns True if successful and False if no response is received (timeout) 

804#------------------------------------------------------------------------------ 

805#  

806# def query_node_number(): 

807# global qnn_response 

808# def response_received(): return(qnn_response) 

809# qnn_response = False 

810# # Only bother sending commands to the Pi Sprog if the serial port has been opened 

811# if serial_port.is_open: 

812# # Retry sending the command (3 attempts) if we don't get a response 

813# attempts = 0 

814# while attempts < 3: 

815# logging.debug ("Pi-SPROG: Sending QNN command (Query Node Number)") 

816# send_cbus_command (mj_pri=2, min_pri=3, op_code=13) 

817# # Wait for the response (with a 1 second timeout) 

818# if wait_for_response(1.0, response_received): break 

819# attempts = attempts + 1 

820# logging.warning("Pi-SPROG: Query Node Number timeout - retrying") 

821# if qnn_response: logging.debug ("Pi-SPROG: Received PNN (Response to Query Node)") 

822# else: logging.error("Pi-SPROG: Query Node Number failed") 

823# else: 

824# logging.warning("Pi-SPROG: Cannot Query Node Number - port is closed") 

825# return(qnn_response) 

826# 

827#------------------------------------------------------------------------------ 

828# Internal function to process a Query Node response (PNN message) 

829# Sets the qnn_response flag - to signal back into the main thread 

830# Response to Query Node op code is (0xB6 = 182 decimal) so the following 

831# line would need to be added into the receive data thread: 

832# elif op_code == 182: process_pnn_message(byte_string) 

833#------------------------------------------------------------------------------ 

834#  

835# def process_pnn_message(byte_string): 

836# global qnn_response 

837# # Print out the status report (if the appropriate debug level is set) 

838# if debug: 

839# logging.debug ("Pi-SPROG: Rx thread - Received PNN (Response to Query Node):") 

840# logging.debug (" Node Id :"+str(int(chr(byte_string[9]) + chr(byte_string[10]) 

841# + chr(byte_string[11]) + chr(byte_string[12]),16))) 

842# logging.debug (" Mfctre ID :"+str(int(chr(byte_string[13]) + chr(byte_string[14]),16))) 

843# logging.debug (" Module ID :"+str(int(chr(byte_string[15]) + chr(byte_string[16]),16))) 

844# # Get the Flags - we only need the last hex character (to get the 4 bits) 

845# flags = int(chr(byte_string[18]),16) 

846# logging.debug (" Bldr Comp :"+str((flags & 0x08)==0x08)) 

847# logging.debug (" FLiM Mode :"+str((flags & 0x04)==0x04)) 

848# logging.debug (" Prod Node :"+str((flags & 0x02)==0x02)) 

849# logging.debug (" Cons Node :"+str((flags & 0x01)==0x01)+"\r") 

850# # Respond to the trigger function (waiting in the main thread for a response) 

851# qnn_response = True 

852# return() 

853# 

854###################################################################################### 

855 

856