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
« 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# --------------------------------------------------------------------------------------------
79import threading
80import serial
81import time
82import logging
83import queue
85# Global class for the Serial Port (port is configured/opened later)
86serial_port = serial.Serial()
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)
93# Global Flag to enable enhanced debug logging for the Pi-SPROG interface
94debug = False # Enhanced Debug logging - set by the sprog_connect call
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)
107# Global 'one up' session ID (to match up the CV programming responses with the requests)
108session_id = 1
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
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()
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#------------------------------------------------------------------------------
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)
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#------------------------------------------------------------------------------
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()
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#------------------------------------------------------------------------------
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()
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#------------------------------------------------------------------------------
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()
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#------------------------------------------------------------------------------
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()
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()
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#------------------------------------------------------------------------------
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()
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#------------------------------------------------------------------------------
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()
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#------------------------------------------------------------------------------
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()
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#------------------------------------------------------------------------------
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)
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#------------------------------------------------------------------------------
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)
463#------------------------------------------------------------------------------
464# Function called on shutdown to turn off DCC bus power and close the comms port
465#------------------------------------------------------------------------------
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()
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#------------------------------------------------------------------------------
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)
504#------------------------------------------------------------------------------
505# Externally Called Function to turn on the track power
506#------------------------------------------------------------------------------
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)
534#------------------------------------------------------------------------------
535# Externally Called Function to turn off the track power
536#------------------------------------------------------------------------------
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)
562#------------------------------------------------------------------------------
563# Externally Called Function to send an Accessory Short CBUS On/Off Event
564#------------------------------------------------------------------------------
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 ()
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#------------------------------------------------------------------------------
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)
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#------------------------------------------------------------------------------
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)
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######################################################################################