mirror of
				https://github.com/Klipper3d/klipper.git
				synced 2025-10-31 02:15:52 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			287 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			287 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # Serial port management for firmware communication
 | |
| #
 | |
| # Copyright (C) 2016  Kevin O'Connor <kevin@koconnor.net>
 | |
| #
 | |
| # This file may be distributed under the terms of the GNU GPLv3 license.
 | |
| import time, logging, threading
 | |
| import serial
 | |
| 
 | |
| import msgproto, chelper
 | |
| 
 | |
| class SerialReader:
 | |
|     BITS_PER_BYTE = 10
 | |
|     def __init__(self, reactor, serialport, baud):
 | |
|         self.reactor = reactor
 | |
|         self.serialport = serialport
 | |
|         self.baud = baud
 | |
|         # Serial port
 | |
|         self.ser = None
 | |
|         self.msgparser = msgproto.MessageParser()
 | |
|         # C interface
 | |
|         self.ffi_main, self.ffi_lib = chelper.get_ffi()
 | |
|         self.serialqueue = None
 | |
|         self.default_cmd_queue = self.alloc_command_queue()
 | |
|         self.stats_buf = self.ffi_main.new('char[4096]')
 | |
|         # MCU time/clock tracking
 | |
|         self.last_ack_time = self.last_ack_rtt_time = 0.
 | |
|         self.last_ack_clock = self.last_ack_rtt_clock = 0
 | |
|         self.est_clock = 0.
 | |
|         # Threading
 | |
|         self.lock = threading.Lock()
 | |
|         self.background_thread = None
 | |
|         # Message handlers
 | |
|         self.status_timer = self.reactor.register_timer(
 | |
|             self._status_event, self.reactor.NOW)
 | |
|         self.status_cmd = None
 | |
|         handlers = {
 | |
|             '#unknown': self.handle_unknown, '#state': self.handle_state,
 | |
|             '#output': self.handle_output, 'status': self.handle_status,
 | |
|             'shutdown': self.handle_output, 'is_shutdown': self.handle_output
 | |
|         }
 | |
|         self.handlers = dict(((k, None), v) for k, v in handlers.items())
 | |
|     def _bg_thread(self):
 | |
|         response = self.ffi_main.new('struct pull_queue_message *')
 | |
|         while 1:
 | |
|             self.ffi_lib.serialqueue_pull(self.serialqueue, response)
 | |
|             count = response.len
 | |
|             if count <= 0:
 | |
|                 break
 | |
|             params = self.msgparser.parse(response.msg[0:count])
 | |
|             params['#sent_time'] = response.sent_time
 | |
|             params['#receive_time'] = response.receive_time
 | |
|             with self.lock:
 | |
|                 hdl = (params['#name'], params.get('oid'))
 | |
|                 hdl = self.handlers.get(hdl, self.handle_default)
 | |
|             try:
 | |
|                 hdl(params)
 | |
|             except:
 | |
|                 logging.exception("Exception in serial callback")
 | |
|     def connect(self):
 | |
|         logging.info("Starting serial connect")
 | |
|         self.ser = serial.Serial(self.serialport, self.baud, timeout=0)
 | |
|         stk500v2_leave(self.ser)
 | |
|         baud_adjust = float(self.BITS_PER_BYTE) / self.baud
 | |
|         self.serialqueue = self.ffi_lib.serialqueue_alloc(
 | |
|             self.ser.fileno(), baud_adjust, 0)
 | |
|         SerialBootStrap(self)
 | |
|         self.background_thread = threading.Thread(target=self._bg_thread)
 | |
|         self.background_thread.start()
 | |
|     def connect_file(self, debugoutput, dictionary, pace=False):
 | |
|         self.ser = debugoutput
 | |
|         self.msgparser.process_identify(dictionary, decompress=False)
 | |
|         baud_adjust = 0.
 | |
|         est_clock = 1000000000000.
 | |
|         if pace:
 | |
|             baud_adjust = float(self.BITS_PER_BYTE) / self.baud
 | |
|             est_clock = self.msgparser.config['CLOCK_FREQ']
 | |
|         self.serialqueue = self.ffi_lib.serialqueue_alloc(
 | |
|             self.ser.fileno(), baud_adjust, 1)
 | |
|         self.est_clock = est_clock
 | |
|         self.last_ack_time = time.time()
 | |
|         self.last_ack_clock = 0
 | |
|         self.ffi_lib.serialqueue_set_clock_est(
 | |
|             self.serialqueue, self.est_clock, self.last_ack_time
 | |
|             , self.last_ack_clock)
 | |
|     def disconnect(self):
 | |
|         self.send_flush()
 | |
|         time.sleep(0.010)
 | |
|         if self.ffi_lib is not None:
 | |
|             self.ffi_lib.serialqueue_exit(self.serialqueue)
 | |
|         if self.background_thread is not None:
 | |
|             self.background_thread.join()
 | |
|     def stats(self, eventtime):
 | |
|         if self.serialqueue is None:
 | |
|             return ""
 | |
|         sqstats = self.ffi_lib.serialqueue_get_stats(
 | |
|             self.serialqueue, self.stats_buf, len(self.stats_buf))
 | |
|         sqstats = self.ffi_main.string(self.stats_buf)
 | |
|         tstats = " est_clock=%.3f last_ack_time=%.3f last_ack_clock=%d" % (
 | |
|             self.est_clock, self.last_ack_time, self.last_ack_clock)
 | |
|         return sqstats + tstats
 | |
|     def _status_event(self, eventtime):
 | |
|         if self.status_cmd is None:
 | |
|             return eventtime + 0.1
 | |
|         self.send(self.status_cmd)
 | |
|         return eventtime + 1.0
 | |
|     # Serial response callbacks
 | |
|     def register_callback(self, callback, name, oid=None):
 | |
|         with self.lock:
 | |
|             self.handlers[name, oid] = callback
 | |
|     def unregister_callback(self, name, oid=None):
 | |
|         with self.lock:
 | |
|             del self.handlers[name, oid]
 | |
|     # Clock tracking
 | |
|     def get_clock(self, eventtime):
 | |
|         with self.lock:
 | |
|             return int(self.last_ack_clock
 | |
|                        + (eventtime - self.last_ack_time) * self.est_clock)
 | |
|     def translate_clock(self, raw_clock):
 | |
|         with self.lock:
 | |
|             last_ack_clock = self.last_ack_clock
 | |
|         clock_diff = (last_ack_clock - raw_clock) & 0xffffffff
 | |
|         if clock_diff & 0x80000000:
 | |
|             return last_ack_clock + 0x100000000 - clock_diff
 | |
|         return last_ack_clock - clock_diff
 | |
|     def get_last_clock(self):
 | |
|         with self.lock:
 | |
|             return self.last_ack_clock
 | |
|     # Command sending
 | |
|     def send(self, cmd, minclock=0, reqclock=0, cq=None):
 | |
|         if cq is None:
 | |
|             cq = self.default_cmd_queue
 | |
|         self.ffi_lib.serialqueue_send(
 | |
|             self.serialqueue, cq, cmd, len(cmd), minclock, reqclock)
 | |
|     def encode_and_send(self, data, minclock, reqclock, cq):
 | |
|         self.ffi_lib.serialqueue_encode_and_send(
 | |
|             self.serialqueue, cq, data, len(data), minclock, reqclock)
 | |
|     def send_with_response(self, cmd, callback, name):
 | |
|         SerialRetryCommand(self, cmd, callback, name)
 | |
|     def send_flush(self):
 | |
|         self.ffi_lib.serialqueue_flush_ready(self.serialqueue)
 | |
|     def alloc_command_queue(self):
 | |
|         return self.ffi_lib.serialqueue_alloc_commandqueue()
 | |
|     # Dumping debug lists
 | |
|     def dump_debug(self):
 | |
|         sdata = self.ffi_main.new('struct pull_queue_message[1024]')
 | |
|         rdata = self.ffi_main.new('struct pull_queue_message[1024]')
 | |
|         scount = self.ffi_lib.serialqueue_extract_old(
 | |
|             self.serialqueue, 1, sdata, len(sdata))
 | |
|         rcount = self.ffi_lib.serialqueue_extract_old(
 | |
|             self.serialqueue, 0, rdata, len(rdata))
 | |
|         logging.info("Dumping send queue %d messages" % (scount,))
 | |
|         for i in range(scount):
 | |
|             msg = sdata[i]
 | |
|             cmds = self.msgparser.dump(msg.msg[0:msg.len])
 | |
|             logging.info("Sent %d %f %f %d: %s" % (
 | |
|                 i, msg.receive_time, msg.sent_time, msg.len, ', '.join(cmds)))
 | |
|         logging.info("Dumping receive queue %d messages" % (rcount,))
 | |
|         for i in range(rcount):
 | |
|             msg = rdata[i]
 | |
|             cmds = self.msgparser.dump(msg.msg[0:msg.len])
 | |
|             logging.info("Receive: %d %f %f %d: %s" % (
 | |
|                 i, msg.receive_time, msg.sent_time, msg.len, ', '.join(cmds)))
 | |
|     # Default message handlers
 | |
|     def handle_status(self, params):
 | |
|         with self.lock:
 | |
|             # Update last_ack_time / last_ack_clock
 | |
|             ack_clock = (self.last_ack_clock & ~0xffffffff) | params['clock']
 | |
|             if ack_clock < self.last_ack_clock:
 | |
|                 ack_clock += 0x100000000
 | |
|             sent_time = params['#sent_time']
 | |
|             self.last_ack_time = receive_time = params['#receive_time']
 | |
|             self.last_ack_clock = ack_clock
 | |
|             # Update est_clock (if applicable)
 | |
|             if receive_time > self.last_ack_rtt_time + 1. and sent_time:
 | |
|                 if self.last_ack_rtt_time:
 | |
|                     timedelta = receive_time - self.last_ack_rtt_time
 | |
|                     clockdelta = ack_clock - self.last_ack_rtt_clock
 | |
|                     estclock = clockdelta / timedelta
 | |
|                     if estclock > self.est_clock and self.est_clock:
 | |
|                         self.est_clock = (self.est_clock * 63. + estclock) / 64.
 | |
|                     else:
 | |
|                         self.est_clock = estclock
 | |
|                 self.last_ack_rtt_time = sent_time
 | |
|                 self.last_ack_rtt_clock = ack_clock
 | |
|             self.ffi_lib.serialqueue_set_clock_est(
 | |
|                 self.serialqueue, self.est_clock, receive_time, ack_clock)
 | |
|     def handle_unknown(self, params):
 | |
|         logging.warn("Unknown message type %d: %s" % (
 | |
|             params['#msgid'], repr(params['#msg'])))
 | |
|     def handle_output(self, params):
 | |
|         logging.info("%s: %s" % (params['#name'], params['#msg']))
 | |
|     def handle_state(self, params):
 | |
|         state = params['#state']
 | |
|         if state  == 'connected':
 | |
|             logging.info("Loaded %d commands (%s)" % (
 | |
|                 len(self.msgparser.messages_by_id), self.msgparser.version))
 | |
|         else:
 | |
|             logging.info("State: %s" % (state,))
 | |
|     def handle_default(self, params):
 | |
|         logging.warn("got %s" % (params,))
 | |
| 
 | |
| # Class to retry sending of a query command until a given response is received
 | |
| class SerialRetryCommand:
 | |
|     RETRY_TIME = 0.500
 | |
|     def __init__(self, serial, cmd, callback, name):
 | |
|         self.serial = serial
 | |
|         self.cmd = cmd
 | |
|         self.callback = callback
 | |
|         self.name = name
 | |
|         self.serial.register_callback(self.handle_callback, self.name)
 | |
|         self.send_timer = self.serial.reactor.register_timer(
 | |
|             self.send_event, self.serial.reactor.NOW)
 | |
|     def send_event(self, eventtime):
 | |
|         if self.callback is None:
 | |
|             self.serial.reactor.unregister_timer(self.send_timer)
 | |
|             return self.serial.reactor.NEVER
 | |
|         self.serial.send(self.cmd)
 | |
|         return eventtime + self.RETRY_TIME
 | |
|     def handle_callback(self, params):
 | |
|         done = self.callback(params)
 | |
|         if done:
 | |
|             self.serial.unregister_callback(self.name)
 | |
|             self.callback = None
 | |
| 
 | |
| # Code to start communication and download message type dictionary
 | |
| class SerialBootStrap:
 | |
|     RETRY_TIME = 0.500
 | |
|     def __init__(self, serial):
 | |
|         self.serial = serial
 | |
|         self.identify_data = ""
 | |
|         self.identify_cmd = self.serial.msgparser.lookup_command(
 | |
|             "identify offset=%u count=%c")
 | |
|         self.is_done = False
 | |
|         self.serial.register_callback(self.handle_identify, 'identify_response')
 | |
|         self.serial.register_callback(self.handle_unknown, '#unknown')
 | |
|         self.send_timer = self.serial.reactor.register_timer(
 | |
|             self.send_event, self.serial.reactor.NOW)
 | |
|     def finalize(self):
 | |
|         self.is_done = True
 | |
|         self.serial.msgparser.process_identify(self.identify_data)
 | |
|         logging.info("MCU version: %s" % (self.serial.msgparser.version,))
 | |
|         self.serial.unregister_callback('identify_response')
 | |
|         self.serial.register_callback(self.serial.handle_unknown, '#unknown')
 | |
|         get_status = self.serial.msgparser.lookup_command('get_status')
 | |
|         self.serial.status_cmd = get_status.encode()
 | |
|         with self.serial.lock:
 | |
|             hdl = self.serial.handlers['#state', None]
 | |
|         statemsg = {'#name': '#state', '#state': 'connected'}
 | |
|         hdl(statemsg)
 | |
|     def handle_identify(self, params):
 | |
|         if self.is_done or params['offset'] != len(self.identify_data):
 | |
|             return
 | |
|         msgdata = params['data']
 | |
|         if not msgdata:
 | |
|             self.finalize()
 | |
|             return
 | |
|         self.identify_data += msgdata
 | |
|         imsg = self.identify_cmd.encode(len(self.identify_data), 40)
 | |
|         self.serial.send(imsg)
 | |
|     def send_event(self, eventtime):
 | |
|         if self.is_done:
 | |
|             self.serial.reactor.unregister_timer(self.send_timer)
 | |
|             return self.serial.reactor.NEVER
 | |
|         imsg = self.identify_cmd.encode(len(self.identify_data), 40)
 | |
|         self.serial.send(imsg)
 | |
|         return eventtime + self.RETRY_TIME
 | |
|     def handle_unknown(self, params):
 | |
|         logging.debug("Unknown message %d (len %d) while identifying" % (
 | |
|             params['#msgid'], len(params['#msg'])))
 | |
| 
 | |
| # Attempt to place an AVR stk500v2 style programmer into normal mode
 | |
| def stk500v2_leave(ser):
 | |
|     logging.debug("Starting stk500v2 leave programmer sequence")
 | |
|     origbaud = ser.baudrate
 | |
|     # Request a dummy speed first as this seems to help reset the port
 | |
|     ser.baudrate = 1200
 | |
|     ser.read(1)
 | |
|     # Send stk500v2 leave programmer sequence
 | |
|     ser.baudrate = 115200
 | |
|     time.sleep(0.100)
 | |
|     ser.read(4096)
 | |
|     ser.write('\x1b\x01\x00\x01\x0e\x11\x04')
 | |
|     time.sleep(0.050)
 | |
|     res = ser.read(4096)
 | |
|     logging.debug("Got %s from stk500v2" % (repr(res),))
 | |
|     ser.baudrate = origbaud
 |