mirror of
				https://github.com/Klipper3d/klipper.git
				synced 2025-10-31 18:36:09 +01:00 
			
		
		
		
	The mcu_phase_offset should be added not subtracted. Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
		
			
				
	
	
		
			699 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			699 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # Code for reading data logs produced by data_logger.py
 | |
| #
 | |
| # Copyright (C) 2021  Kevin O'Connor <kevin@koconnor.net>
 | |
| #
 | |
| # This file may be distributed under the terms of the GNU GPLv3 license.
 | |
| import json, zlib
 | |
| 
 | |
| class error(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| ######################################################################
 | |
| # Log data handlers
 | |
| ######################################################################
 | |
| 
 | |
| # Log data handlers: {name: class, ...}
 | |
| LogHandlers = {}
 | |
| 
 | |
| # Extract status fields from log
 | |
| class HandleStatusField:
 | |
|     SubscriptionIdParts = 0
 | |
|     ParametersMin = ParametersMax = 1
 | |
|     DataSets = [
 | |
|         ('status(<field>)', 'A get_status field name (separate by periods)'),
 | |
|     ]
 | |
|     def __init__(self, lmanager, name, name_parts):
 | |
|         self.status_tracker = lmanager.get_status_tracker()
 | |
|         self.field_name = name_parts[1]
 | |
|         self.field_parts = name_parts[1].split('.')
 | |
|         self.next_update_time = 0.
 | |
|         self.result = None
 | |
|     def get_label(self):
 | |
|         label = '%s field' % (self.field_name,)
 | |
|         return {'label': label, 'units': 'Unknown'}
 | |
|     def pull_data(self, req_time):
 | |
|         if req_time < self.next_update_time:
 | |
|             return self.result
 | |
|         db, next_update_time = self.status_tracker.pull_status(req_time)
 | |
|         for fp in self.field_parts[:-1]:
 | |
|             db = db.get(fp, {})
 | |
|         self.result = db.get(self.field_parts[-1], 0.)
 | |
|         self.next_update_time = next_update_time
 | |
|         return self.result
 | |
| LogHandlers["status"] = HandleStatusField
 | |
| 
 | |
| # Extract requested position, velocity, and accel from a trapq log
 | |
| class HandleTrapQ:
 | |
|     SubscriptionIdParts = 2
 | |
|     ParametersMin = ParametersMax = 2
 | |
|     DataSets = [
 | |
|         ('trapq(<name>,velocity)', 'Requested velocity for the given trapq'),
 | |
|         ('trapq(<name>,accel)', 'Requested acceleration for the given trapq'),
 | |
|         ('trapq(<name>,<axis>)', 'Requested axis (x, y, or z) position'),
 | |
|         ('trapq(<name>,<axis>_velocity)', 'Requested axis velocity'),
 | |
|         ('trapq(<name>,<axis>_accel)', 'Requested axis acceleration'),
 | |
|     ]
 | |
|     def __init__(self, lmanager, name, name_parts):
 | |
|         self.name = name
 | |
|         self.jdispatch = lmanager.get_jdispatch()
 | |
|         self.cur_data = [(0., 0., 0., 0., (0., 0., 0.), (0., 0., 0.))]
 | |
|         self.data_pos = 0
 | |
|         tq, trapq_name, datasel = name_parts
 | |
|         ptypes = {}
 | |
|         ptypes['velocity'] = {
 | |
|             'label': '%s velocity' % (trapq_name,),
 | |
|             'units': 'Velocity\n(mm/s)', 'func': self._pull_velocity
 | |
|         }
 | |
|         ptypes['accel'] = {
 | |
|             'label': '%s acceleration' % (trapq_name,),
 | |
|             'units': 'Acceleration\n(mm/s^2)', 'func': self._pull_accel
 | |
|         }
 | |
|         for axis, name in enumerate("xyz"):
 | |
|             ptypes['%s' % (name,)] = {
 | |
|                 'label': '%s %s position' % (trapq_name, name), 'axis': axis,
 | |
|                 'units': 'Position\n(mm)', 'func': self._pull_axis_position
 | |
|             }
 | |
|             ptypes['%s_velocity' % (name,)] = {
 | |
|                 'label': '%s %s velocity' % (trapq_name, name), 'axis': axis,
 | |
|                 'units': 'Velocity\n(mm/s)', 'func': self._pull_axis_velocity
 | |
|             }
 | |
|             ptypes['%s_accel' % (name,)] = {
 | |
|                 'label': '%s %s acceleration' % (trapq_name, name),
 | |
|                 'axis': axis, 'units': 'Acceleration\n(mm/s^2)',
 | |
|                 'func': self._pull_axis_accel
 | |
|             }
 | |
|         pinfo = ptypes.get(datasel)
 | |
|         if pinfo is None:
 | |
|             raise error("Unknown trapq data selection '%s'" % (datasel,))
 | |
|         self.label = {'label': pinfo['label'], 'units': pinfo['units']}
 | |
|         self.axis = pinfo.get('axis')
 | |
|         self.pull_data = pinfo['func']
 | |
|     def get_label(self):
 | |
|         return self.label
 | |
|     def _find_move(self, req_time):
 | |
|         data_pos = self.data_pos
 | |
|         while 1:
 | |
|             move = self.cur_data[data_pos]
 | |
|             print_time, move_t, start_v, accel, start_pos, axes_r = move
 | |
|             if req_time <= print_time + move_t:
 | |
|                 return move, req_time >= print_time
 | |
|             data_pos += 1
 | |
|             if data_pos < len(self.cur_data):
 | |
|                 self.data_pos = data_pos
 | |
|                 continue
 | |
|             jmsg = self.jdispatch.pull_msg(req_time, self.name)
 | |
|             if jmsg is None:
 | |
|                 return move, False
 | |
|             self.cur_data = jmsg['data']
 | |
|             self.data_pos = data_pos = 0
 | |
|     def _pull_axis_position(self, req_time):
 | |
|         move, in_range = self._find_move(req_time)
 | |
|         print_time, move_t, start_v, accel, start_pos, axes_r = move
 | |
|         mtime = max(0., min(move_t, req_time - print_time))
 | |
|         dist = (start_v + .5 * accel * mtime) * mtime;
 | |
|         return start_pos[self.axis] + axes_r[self.axis] * dist
 | |
|     def _pull_axis_velocity(self, req_time):
 | |
|         move, in_range = self._find_move(req_time)
 | |
|         if not in_range:
 | |
|             return 0.
 | |
|         print_time, move_t, start_v, accel, start_pos, axes_r = move
 | |
|         return (start_v + accel * (req_time - print_time)) * axes_r[self.axis]
 | |
|     def _pull_axis_accel(self, req_time):
 | |
|         move, in_range = self._find_move(req_time)
 | |
|         if not in_range:
 | |
|             return 0.
 | |
|         print_time, move_t, start_v, accel, start_pos, axes_r = move
 | |
|         return accel * axes_r[self.axis]
 | |
|     def _pull_velocity(self, req_time):
 | |
|         move, in_range = self._find_move(req_time)
 | |
|         if not in_range:
 | |
|             return 0.
 | |
|         print_time, move_t, start_v, accel, start_pos, axes_r = move
 | |
|         return start_v + accel * (req_time - print_time)
 | |
|     def _pull_accel(self, req_time):
 | |
|         move, in_range = self._find_move(req_time)
 | |
|         if not in_range:
 | |
|             return 0.
 | |
|         print_time, move_t, start_v, accel, start_pos, axes_r = move
 | |
|         return accel
 | |
| LogHandlers["trapq"] = HandleTrapQ
 | |
| 
 | |
| # Extract positions from queue_step log
 | |
| class HandleStepQ:
 | |
|     SubscriptionIdParts = 2
 | |
|     ParametersMin = 1
 | |
|     ParametersMax = 2
 | |
|     DataSets = [
 | |
|         ('stepq(<stepper>)', 'Commanded position of the given stepper'),
 | |
|         ('stepq(<stepper>,<time>)', 'Commanded position with smooth time'),
 | |
|     ]
 | |
|     def __init__(self, lmanager, name, name_parts):
 | |
|         self.name = name
 | |
|         self.stepper_name = name_parts[1]
 | |
|         self.jdispatch = lmanager.get_jdispatch()
 | |
|         self.step_data = [(0., 0., 0.), (0., 0., 0.)] # [(time, half_pos, pos)]
 | |
|         self.data_pos = 0
 | |
|         self.smooth_time = 0.010
 | |
|         if len(name_parts) == 3:
 | |
|             try:
 | |
|                 self.smooth_time = float(name_parts[2])
 | |
|             except ValueError:
 | |
|                 raise error("Invalid stepq smooth time '%s'" % (name_parts[2],))
 | |
|     def get_label(self):
 | |
|         label = '%s position' % (self.stepper_name,)
 | |
|         return {'label': label, 'units': 'Position\n(mm)'}
 | |
|     def pull_data(self, req_time):
 | |
|         smooth_time = self.smooth_time
 | |
|         while 1:
 | |
|             data_pos = self.data_pos
 | |
|             step_data = self.step_data
 | |
|             # Find steps before and after req_time
 | |
|             next_time, next_halfpos, next_pos = step_data[data_pos + 1]
 | |
|             if req_time >= next_time:
 | |
|                 if data_pos + 2 < len(step_data):
 | |
|                     self.data_pos = data_pos + 1
 | |
|                     continue
 | |
|                 self._pull_block(req_time)
 | |
|                 continue
 | |
|             last_time, last_halfpos, last_pos = step_data[data_pos]
 | |
|             # Perform step smoothing
 | |
|             rtdiff = req_time - last_time
 | |
|             stime = next_time - last_time
 | |
|             if stime <= smooth_time:
 | |
|                 pdiff = next_halfpos - last_halfpos
 | |
|                 return last_halfpos + rtdiff * pdiff / stime
 | |
|             stime = .5 * smooth_time
 | |
|             if rtdiff < stime:
 | |
|                 pdiff = last_pos - last_halfpos
 | |
|                 return last_halfpos + rtdiff * pdiff / stime
 | |
|             rtdiff = next_time - req_time
 | |
|             if rtdiff < stime:
 | |
|                 pdiff = last_pos - next_halfpos
 | |
|                 return next_halfpos + rtdiff * pdiff / stime
 | |
|             return last_pos
 | |
|     def _pull_block(self, req_time):
 | |
|         step_data = self.step_data
 | |
|         del step_data[:-1]
 | |
|         self.data_pos = 0
 | |
|         # Read data block containing requested time frame
 | |
|         while 1:
 | |
|             jmsg = self.jdispatch.pull_msg(req_time, self.name)
 | |
|             if jmsg is None:
 | |
|                 last_time, last_halfpos, last_pos = step_data[0]
 | |
|                 self.step_data.append((req_time + .1, last_pos, last_pos))
 | |
|                 return
 | |
|             last_time = jmsg['last_step_time']
 | |
|             if req_time <= last_time:
 | |
|                 break
 | |
|         # Process block into (time, half_position, position) 3-tuples
 | |
|         first_time = step_time = jmsg['first_step_time']
 | |
|         first_clock = jmsg['first_clock']
 | |
|         step_clock = first_clock - jmsg['data'][0][0]
 | |
|         cdiff = jmsg['last_clock'] - first_clock
 | |
|         tdiff = last_time - first_time
 | |
|         inv_freq = 0.
 | |
|         if cdiff:
 | |
|             inv_freq = tdiff / cdiff
 | |
|         step_dist = jmsg['step_distance']
 | |
|         step_pos = jmsg['start_position']
 | |
|         if not step_data[0][0]:
 | |
|             step_data[0] = (0., step_pos, step_pos)
 | |
|         for interval, raw_count, add in jmsg['data']:
 | |
|             qs_dist = step_dist
 | |
|             count = raw_count
 | |
|             if count < 0:
 | |
|                 qs_dist = -qs_dist
 | |
|                 count = -count
 | |
|             for i in range(count):
 | |
|                 step_clock += interval
 | |
|                 interval += add
 | |
|                 step_time = first_time + (step_clock - first_clock) * inv_freq
 | |
|                 step_halfpos = step_pos + .5 * qs_dist
 | |
|                 step_pos += qs_dist
 | |
|                 step_data.append((step_time, step_halfpos, step_pos))
 | |
| LogHandlers["stepq"] = HandleStepQ
 | |
| 
 | |
| # Extract stepper motor phase position
 | |
| class HandleStepPhase:
 | |
|     SubscriptionIdParts = 0
 | |
|     ParametersMin = 1
 | |
|     ParametersMax = 2
 | |
|     DataSets = [
 | |
|         ('step_phase(<driver>)', 'Stepper motor phase of the given stepper'),
 | |
|         ('step_phase(<driver>,microstep)', 'Microstep position for stepper'),
 | |
|     ]
 | |
|     def __init__(self, lmanager, name, name_parts):
 | |
|         self.name = name
 | |
|         self.driver_name = name_parts[1]
 | |
|         self.stepper_name = " ".join(self.driver_name.split()[1:])
 | |
|         config = lmanager.get_initial_status()['configfile']['settings']
 | |
|         if self.driver_name not in config or self.stepper_name not in config:
 | |
|             raise error("Unable to find stepper driver '%s' config"
 | |
|                         % (self.driver_name,))
 | |
|         if len(name_parts) == 3 and name_parts[2] != "microstep":
 | |
|             raise error("Unknown step_phase selection '%s'" % (name_parts[2],))
 | |
|         self.report_microsteps = len(name_parts) == 3
 | |
|         sconfig = config[self.stepper_name]
 | |
|         self.phases = sconfig["microsteps"]
 | |
|         if not self.report_microsteps:
 | |
|             self.phases *= 4
 | |
|         self.jdispatch = lmanager.get_jdispatch()
 | |
|         self.jdispatch.add_handler(name, "stepq:" + self.stepper_name)
 | |
|         # stepq tracking
 | |
|         self.step_data = [(0., 0), (0., 0)] # [(time, mcu_pos)]
 | |
|         self.data_pos = 0
 | |
|         # driver phase tracking
 | |
|         self.status_tracker = lmanager.get_status_tracker()
 | |
|         self.next_status_time = 0.
 | |
|         self.mcu_phase_offset = 0
 | |
|     def get_label(self):
 | |
|         if self.report_microsteps:
 | |
|             return {'label': '%s microstep' % (self.stepper_name,),
 | |
|                     'units': 'Microstep'}
 | |
|         return {'label': '%s phase' % (self.stepper_name,), 'units': 'Phase'}
 | |
|     def _pull_phase_offset(self, req_time):
 | |
|         db, self.next_status_time = self.status_tracker.pull_status(req_time)
 | |
|         mcu_phase_offset = db.get(self.driver_name, {}).get('mcu_phase_offset')
 | |
|         if mcu_phase_offset is None:
 | |
|             mcu_phase_offset = 0
 | |
|         self.mcu_phase_offset = mcu_phase_offset
 | |
|     def pull_data(self, req_time):
 | |
|         if req_time >= self.next_status_time:
 | |
|             self._pull_phase_offset(req_time)
 | |
|         while 1:
 | |
|             data_pos = self.data_pos
 | |
|             step_data = self.step_data
 | |
|             # Find steps before and after req_time
 | |
|             next_time, next_pos = step_data[data_pos + 1]
 | |
|             if req_time >= next_time:
 | |
|                 if data_pos + 2 < len(step_data):
 | |
|                     self.data_pos = data_pos + 1
 | |
|                     continue
 | |
|                 self._pull_block(req_time)
 | |
|                 continue
 | |
|             step_pos = step_data[data_pos][1]
 | |
|             return (step_pos + self.mcu_phase_offset) % self.phases
 | |
|     def _pull_block(self, req_time):
 | |
|         step_data = self.step_data
 | |
|         del step_data[:-1]
 | |
|         self.data_pos = 0
 | |
|         # Read data block containing requested time frame
 | |
|         while 1:
 | |
|             jmsg = self.jdispatch.pull_msg(req_time, self.name)
 | |
|             if jmsg is None:
 | |
|                 last_time, last_pos = step_data[0]
 | |
|                 self.step_data.append((req_time + .1, last_pos))
 | |
|                 return
 | |
|             last_time = jmsg['last_step_time']
 | |
|             if req_time <= last_time:
 | |
|                 break
 | |
|         # Process block into (time, position) 2-tuples
 | |
|         first_time = step_time = jmsg['first_step_time']
 | |
|         first_clock = jmsg['first_clock']
 | |
|         step_clock = first_clock - jmsg['data'][0][0]
 | |
|         cdiff = jmsg['last_clock'] - first_clock
 | |
|         tdiff = last_time - first_time
 | |
|         inv_freq = 0.
 | |
|         if cdiff:
 | |
|             inv_freq = tdiff / cdiff
 | |
|         step_pos = jmsg['start_mcu_position']
 | |
|         if not step_data[0][0]:
 | |
|             step_data[0] = (0., step_pos)
 | |
|         for interval, raw_count, add in jmsg['data']:
 | |
|             qs_dist = 1
 | |
|             count = raw_count
 | |
|             if count < 0:
 | |
|                 qs_dist = -1
 | |
|                 count = -count
 | |
|             for i in range(count):
 | |
|                 step_clock += interval
 | |
|                 interval += add
 | |
|                 step_time = first_time + (step_clock - first_clock) * inv_freq
 | |
|                 step_pos += qs_dist
 | |
|                 step_data.append((step_time, step_pos))
 | |
| LogHandlers["step_phase"] = HandleStepPhase
 | |
| 
 | |
| # Extract accelerometer data
 | |
| class HandleADXL345:
 | |
|     SubscriptionIdParts = 2
 | |
|     ParametersMin = ParametersMax = 2
 | |
|     DataSets = [
 | |
|         ('adxl345(<name>,<axis>)', 'Accelerometer for given axis (x, y, or z)'),
 | |
|     ]
 | |
|     def __init__(self, lmanager, name, name_parts):
 | |
|         self.name = name
 | |
|         self.adxl_name = name_parts[1]
 | |
|         self.jdispatch = lmanager.get_jdispatch()
 | |
|         self.next_accel_time = self.last_accel_time = 0.
 | |
|         self.next_accel = self.last_accel = (0., 0., 0.)
 | |
|         self.cur_data = []
 | |
|         self.data_pos = 0
 | |
|         if name_parts[2] not in 'xyz':
 | |
|             raise error("Unknown adxl345 data selection '%s'" % (name,))
 | |
|         self.axis = 'xyz'.index(name_parts[2])
 | |
|     def get_label(self):
 | |
|         label = '%s %s acceleration' % (self.adxl_name, 'xyz'[self.axis])
 | |
|         return {'label': label, 'units': 'Acceleration\n(mm/s^2)'}
 | |
|     def pull_data(self, req_time):
 | |
|         axis = self.axis
 | |
|         while 1:
 | |
|             if req_time <= self.next_accel_time:
 | |
|                 adiff = self.next_accel[axis] - self.last_accel[axis]
 | |
|                 tdiff = self.next_accel_time - self.last_accel_time
 | |
|                 rtdiff = req_time - self.last_accel_time
 | |
|                 return self.last_accel[axis] + rtdiff * adiff / tdiff
 | |
|             if self.data_pos >= len(self.cur_data):
 | |
|                 # Read next data block
 | |
|                 jmsg = self.jdispatch.pull_msg(req_time, self.name)
 | |
|                 if jmsg is None:
 | |
|                     return 0.
 | |
|                 self.cur_data = jmsg['data']
 | |
|                 self.data_pos = 0
 | |
|                 continue
 | |
|             self.last_accel = self.next_accel
 | |
|             self.last_accel_time = self.next_accel_time
 | |
|             self.next_accel_time, x, y, z = self.cur_data[self.data_pos]
 | |
|             self.next_accel = (x, y, z)
 | |
|             self.data_pos += 1
 | |
| LogHandlers["adxl345"] = HandleADXL345
 | |
| 
 | |
| # Extract positions from magnetic angle sensor
 | |
| class HandleAngle:
 | |
|     SubscriptionIdParts = 2
 | |
|     ParametersMin = ParametersMax = 1
 | |
|     DataSets = [
 | |
|         ('angle(<name>)', 'Angle sensor position'),
 | |
|     ]
 | |
|     def __init__(self, lmanager, name, name_parts):
 | |
|         self.name = name
 | |
|         self.angle_name = name_parts[1]
 | |
|         self.jdispatch = lmanager.get_jdispatch()
 | |
|         self.next_angle_time = self.last_angle_time = 0.
 | |
|         self.next_angle = self.last_angle = 0.
 | |
|         self.cur_data = []
 | |
|         self.data_pos = 0
 | |
|         self.position_offset = 0.
 | |
|         self.angle_dist = 1.
 | |
|         # Determine angle distance from associated stepper's rotation_distance
 | |
|         config = lmanager.get_initial_status()['configfile']['settings']
 | |
|         aname = 'angle %s' % (self.angle_name,)
 | |
|         stepper_name = config.get(aname, {}).get('stepper')
 | |
|         if stepper_name is not None:
 | |
|             sconfig = config.get(stepper_name, {})
 | |
|             rotation_distance = sconfig.get('rotation_distance', 1.)
 | |
|             gear_ratio = sconfig.get('gear_ratio', ())
 | |
|             if type(gear_ratio) == str: # XXX
 | |
|                 gear_ratio = [[float(v.strip()) for v in gr.split(':')]
 | |
|                               for gr in gear_ratio.split(',')]
 | |
|             for n, d in gear_ratio:
 | |
|                 rotation_distance *= d / n
 | |
|             self.angle_dist = rotation_distance / 65536.
 | |
|     def get_label(self):
 | |
|         label = '%s position' % (self.angle_name,)
 | |
|         return {'label': label, 'units': 'Position\n(mm)'}
 | |
|     def pull_data(self, req_time):
 | |
|         while 1:
 | |
|             if req_time <= self.next_angle_time:
 | |
|                 pdiff = self.next_angle - self.last_angle
 | |
|                 tdiff = self.next_angle_time - self.last_angle_time
 | |
|                 rtdiff = req_time - self.last_angle_time
 | |
|                 po = rtdiff * pdiff / tdiff
 | |
|                 return ((self.last_angle + po) * self.angle_dist
 | |
|                         + self.position_offset)
 | |
|             if self.data_pos >= len(self.cur_data):
 | |
|                 # Read next data block
 | |
|                 jmsg = self.jdispatch.pull_msg(req_time, self.name)
 | |
|                 if jmsg is None:
 | |
|                     return (self.next_angle * self.angle_dist
 | |
|                             + self.position_offset)
 | |
|                 self.cur_data = jmsg['data']
 | |
|                 position_offset = jmsg.get('position_offset')
 | |
|                 if position_offset is not None:
 | |
|                     self.position_offset = position_offset
 | |
|                 self.data_pos = 0
 | |
|                 continue
 | |
|             self.last_angle = self.next_angle
 | |
|             self.last_angle_time = self.next_angle_time
 | |
|             self.next_angle_time, self.next_angle = self.cur_data[self.data_pos]
 | |
|             self.data_pos += 1
 | |
| LogHandlers["angle"] = HandleAngle
 | |
| 
 | |
| def interpolate(next_val, prev_val, next_time, prev_time, req_time):
 | |
|     vdiff = next_val - prev_val
 | |
|     tdiff = next_time - prev_time
 | |
|     rtdiff = req_time - prev_time
 | |
|     return prev_val + rtdiff * vdiff / tdiff
 | |
| 
 | |
| # Extract eddy current data
 | |
| class HandleEddyCurrent:
 | |
|     SubscriptionIdParts = 2
 | |
|     ParametersMin = 1
 | |
|     ParametersMax = 2
 | |
|     DataSets = [
 | |
|         ('ldc1612(<name>)', 'Coil resonant frequency'),
 | |
|         ('ldc1612(<name>,period)', 'Coil resonant period'),
 | |
|         ('ldc1612(<name>,z)', 'Estimated Z height'),
 | |
|     ]
 | |
|     def __init__(self, lmanager, name, name_parts):
 | |
|         self.name = name
 | |
|         self.sensor_name = name_parts[1]
 | |
|         if len(name_parts) == 3 and name_parts[2] not in ("period", "z"):
 | |
|             raise error("Unknown ldc1612 selection '%s'" % (name_parts[2],))
 | |
|         self.report_frequency = len(name_parts) == 2
 | |
|         self.report_z = len(name_parts) == 3 and name_parts[2] == "z"
 | |
|         self.jdispatch = lmanager.get_jdispatch()
 | |
|         self.next_samp = self.prev_samp = [0., 0., 0.]
 | |
|         self.cur_data = []
 | |
|         self.data_pos = 0
 | |
|     def get_label(self):
 | |
|         if self.report_frequency:
 | |
|             label = '%s frequency' % (self.sensor_name,)
 | |
|             return {'label': label, 'units': 'Frequency\n(Hz)'}
 | |
|         if self.report_z:
 | |
|             label = '%s height' % (self.sensor_name,)
 | |
|             return {'label': label, 'units': 'Position\n(mm)'}
 | |
|         label = '%s period' % (self.sensor_name,)
 | |
|         return {'label': label, 'units': 'Period\n(s)'}
 | |
|     def pull_data(self, req_time):
 | |
|         while 1:
 | |
|             next_time, next_freq, next_z = self.next_samp
 | |
|             if req_time <= next_time:
 | |
|                 prev_time, prev_freq, prev_z = self.prev_samp
 | |
|                 if self.report_frequency:
 | |
|                     next_val = next_freq
 | |
|                     prev_val = prev_freq
 | |
|                 elif self.report_z:
 | |
|                     next_val = next_z
 | |
|                     prev_val = prev_z
 | |
|                 else:
 | |
|                     next_val = 1. / next_freq
 | |
|                     prev_val = 1. / prev_freq
 | |
|                 return interpolate(next_val, prev_val, next_time, prev_time,
 | |
|                                    req_time)
 | |
|             if self.data_pos >= len(self.cur_data):
 | |
|                 # Read next data block
 | |
|                 jmsg = self.jdispatch.pull_msg(req_time, self.name)
 | |
|                 if jmsg is None:
 | |
|                     return 0.
 | |
|                 self.cur_data = jmsg['data']
 | |
|                 self.data_pos = 0
 | |
|                 continue
 | |
|             self.prev_samp = self.next_samp
 | |
|             self.next_samp = self.cur_data[self.data_pos]
 | |
|             self.data_pos += 1
 | |
| LogHandlers["ldc1612"] = HandleEddyCurrent
 | |
| 
 | |
| 
 | |
| ######################################################################
 | |
| # Log reading
 | |
| ######################################################################
 | |
| 
 | |
| # Read, uncompress, and parse messages in a log built by data_logger.py
 | |
| class JsonLogReader:
 | |
|     def __init__(self, filename):
 | |
|         self.file = open(filename, "rb")
 | |
|         self.comp = zlib.decompressobj(31)
 | |
|         self.msgs = [b""]
 | |
|     def seek(self, pos):
 | |
|         self.file.seek(pos)
 | |
|         self.comp = zlib.decompressobj(-15)
 | |
|     def pull_msg(self):
 | |
|         msgs = self.msgs
 | |
|         while 1:
 | |
|             if len(msgs) > 1:
 | |
|                 msg = msgs.pop(0)
 | |
|                 try:
 | |
|                     json_msg = json.loads(msg)
 | |
|                 except:
 | |
|                     logging.exception("Unable to parse line")
 | |
|                     continue
 | |
|                 return json_msg
 | |
|             raw_data = self.file.read(8192)
 | |
|             if not raw_data:
 | |
|                 return None
 | |
|             data = self.comp.decompress(raw_data)
 | |
|             parts = data.split(b'\x03')
 | |
|             parts[0] = msgs[0] + parts[0]
 | |
|             self.msgs = msgs = parts
 | |
| 
 | |
| # Store messages in per-subscription queues until handlers are ready for them
 | |
| class JsonDispatcher:
 | |
|     def __init__(self, log_prefix):
 | |
|         self.names = {}
 | |
|         self.queues = {}
 | |
|         self.last_read_time = 0.
 | |
|         self.log_reader = JsonLogReader(log_prefix + ".json.gz")
 | |
|         self.is_eof = False
 | |
|     def check_end_of_data(self):
 | |
|         return self.is_eof and not any(self.queues.values())
 | |
|     def add_handler(self, name, subscription_id):
 | |
|         self.names[name] = q = []
 | |
|         self.queues.setdefault(subscription_id, []).append(q)
 | |
|     def pull_msg(self, req_time, name):
 | |
|         q = self.names[name]
 | |
|         while 1:
 | |
|             if q:
 | |
|                 return q.pop(0)
 | |
|             if req_time + 1. < self.last_read_time:
 | |
|                 return None
 | |
|             json_msg = self.log_reader.pull_msg()
 | |
|             if json_msg is None:
 | |
|                 self.is_eof = True
 | |
|                 return None
 | |
|             qid = json_msg.get('q')
 | |
|             if qid == 'status':
 | |
|                 pt = json_msg.get('toolhead', {}).get('estimated_print_time')
 | |
|                 if pt is not None:
 | |
|                     self.last_read_time = pt
 | |
|             for mq in self.queues.get(qid, []):
 | |
|                 mq.append(json_msg['params'])
 | |
| 
 | |
| 
 | |
| ######################################################################
 | |
| # Dataset and log tracking
 | |
| ######################################################################
 | |
| 
 | |
| # Tracking of get_status messages
 | |
| class TrackStatus:
 | |
|     def __init__(self, lmanager, name, start_status):
 | |
|         self.name = name
 | |
|         self.jdispatch = lmanager.get_jdispatch()
 | |
|         self.next_status_time = 0.
 | |
|         self.status = dict(start_status)
 | |
|         self.next_update = {}
 | |
|     def pull_status(self, req_time):
 | |
|         status = self.status
 | |
|         while 1:
 | |
|             if req_time < self.next_status_time:
 | |
|                 return status, self.next_status_time
 | |
|             for k, v in self.next_update.items():
 | |
|                 status.setdefault(k, {}).update(v)
 | |
|             jmsg = self.jdispatch.pull_msg(req_time, self.name)
 | |
|             if jmsg is None:
 | |
|                 self.next_status_time = req_time + 0.100
 | |
|                 self.next_update = {}
 | |
|                 return status, self.next_status_time
 | |
|             self.next_update = jmsg['status']
 | |
|             th = self.next_update.get('toolhead', {})
 | |
|             self.next_status_time = th.get('estimated_print_time', 0.)
 | |
| 
 | |
| # Split a string by commas while keeping parenthesis intact
 | |
| def param_split(line):
 | |
|     out = []
 | |
|     level = prev = 0
 | |
|     for i, c in enumerate(line):
 | |
|         if not level and c == ',':
 | |
|             out.append(line[prev:i])
 | |
|             prev = i+1
 | |
|         elif c == '(':
 | |
|             level += 1
 | |
|         elif level and c== ')':
 | |
|             level -= 1
 | |
|     out.append(line[prev:])
 | |
|     return out
 | |
| 
 | |
| # Split a dataset name (eg, "abc(def,ghi)") into parts
 | |
| def name_split(name):
 | |
|     if '(' not in name or not name.endswith(')'):
 | |
|         raise error("Malformed dataset name '%s'" % (name,))
 | |
|     aname, aparams = name.split('(', 1)
 | |
|     return [aname] + param_split(aparams[:-1])
 | |
| 
 | |
| # Return a description of possible datasets
 | |
| def list_datasets():
 | |
|     datasets = []
 | |
|     for lh in sorted(LogHandlers.keys()):
 | |
|         datasets += LogHandlers[lh].DataSets
 | |
|     return datasets
 | |
| 
 | |
| # Main log access management
 | |
| class LogManager:
 | |
|     error = error
 | |
|     def __init__(self, log_prefix):
 | |
|         self.index_reader = JsonLogReader(log_prefix + ".index.gz")
 | |
|         self.jdispatch = JsonDispatcher(log_prefix)
 | |
|         self.initial_start_time = self.start_time = 0.
 | |
|         self.datasets = {}
 | |
|         self.initial_status = {}
 | |
|         self.start_status = {}
 | |
|         self.log_subscriptions = {}
 | |
|         self.status_tracker = None
 | |
|     def setup_index(self):
 | |
|         fmsg = self.index_reader.pull_msg()
 | |
|         self.initial_status = status = fmsg['status']
 | |
|         self.start_status = dict(status)
 | |
|         start_time = status['toolhead']['estimated_print_time']
 | |
|         self.initial_start_time = self.start_time = start_time
 | |
|         self.log_subscriptions = fmsg.get('subscriptions', {})
 | |
|     def get_initial_status(self):
 | |
|         return self.initial_status
 | |
|     def available_dataset_types(self):
 | |
|         return {name: None for name in LogHandlers}
 | |
|     def get_jdispatch(self):
 | |
|         return self.jdispatch
 | |
|     def seek_time(self, req_time):
 | |
|         self.start_time = req_start_time = self.initial_start_time + req_time
 | |
|         start_status = self.start_status
 | |
|         seek_time = max(self.initial_start_time, req_start_time - 1.)
 | |
|         file_position = 0
 | |
|         while 1:
 | |
|             fmsg = self.index_reader.pull_msg()
 | |
|             if fmsg is None:
 | |
|                 break
 | |
|             th = fmsg['status']['toolhead']
 | |
|             ptime = max(th['estimated_print_time'], th.get('print_time', 0.))
 | |
|             if ptime > seek_time:
 | |
|                 break
 | |
|             for k, v in fmsg["status"].items():
 | |
|                 start_status.setdefault(k, {}).update(v)
 | |
|             file_position = fmsg['file_position']
 | |
|         if file_position:
 | |
|             self.jdispatch.log_reader.seek(file_position)
 | |
|     def get_initial_start_time(self):
 | |
|         return self.initial_start_time
 | |
|     def get_start_time(self):
 | |
|         return self.start_time
 | |
|     def get_status_tracker(self):
 | |
|         if self.status_tracker is None:
 | |
|             self.status_tracker = TrackStatus(self, "status", self.start_status)
 | |
|             self.jdispatch.add_handler("status", "status")
 | |
|         return self.status_tracker
 | |
|     def setup_dataset(self, name):
 | |
|         if name in self.datasets:
 | |
|             return self.datasets[name]
 | |
|         name_parts = name_split(name)
 | |
|         cls = LogHandlers.get(name_parts[0])
 | |
|         if cls is None:
 | |
|             raise error("Unknown dataset '%s'" % (name_parts[0],))
 | |
|         len_pp = len(name_parts) - 1
 | |
|         if len_pp < cls.ParametersMin or len_pp > cls.ParametersMax:
 | |
|             raise error("Invalid number of parameters for '%s'" % (name,))
 | |
|         if cls.SubscriptionIdParts:
 | |
|             subscription_id = ":".join(name_parts[:cls.SubscriptionIdParts])
 | |
|             if subscription_id not in self.log_subscriptions:
 | |
|                 raise error("Dataset '%s' not in capture" % (subscription_id,))
 | |
|             self.jdispatch.add_handler(name, subscription_id)
 | |
|         self.datasets[name] = hdl = cls(self, name, name_parts)
 | |
|         return hdl
 |