motan: Pass dataset parameters in parenthesis

Replace names like "trapq:toolhead:x" with "trapq(toolhead,x)".

Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
This commit is contained in:
Kevin O'Connor 2021-08-24 13:40:47 -04:00
parent 5fd1c9853d
commit 1e4041a96b
4 changed files with 97 additions and 70 deletions

View File

@ -122,7 +122,7 @@ Graphs can be generated with a command like the following:
One can use the `-g` option to specify the datasets to graph (it takes One can use the `-g` option to specify the datasets to graph (it takes
a Python literal containing a list of lists). For example: a Python literal containing a list of lists). For example:
``` ```
~/klipper/scripts/motan/motan_graph.py mylog -g '[["trapq:toolhead:velocity"], ["trapq:toolhead:accel"]]' ~/klipper/scripts/motan/motan_graph.py mylog -g '[["trapq(toolhead,velocity)"], ["trapq(toolhead,accel)"]]'
``` ```
The list of available datasets can be found using the `-l` option - The list of available datasets can be found using the `-l` option -
@ -134,7 +134,7 @@ for example:
It is also possible to specify matplotlib plot options for each It is also possible to specify matplotlib plot options for each
dataset: dataset:
``` ```
~/klipper/scripts/motan/motan_graph.py mylog -g '[["trapq:toolhead:velocity?color=red"]]' ~/klipper/scripts/motan/motan_graph.py mylog -g '[["trapq(toolhead,velocity)?color=red&alpha=0.4"]]'
``` ```
Many matplotlib options are available; some examples are "color", Many matplotlib options are available; some examples are "color",
"label", "alpha", and "linestyle". "label", "alpha", and "linestyle".

View File

@ -4,6 +4,7 @@
# #
# This file may be distributed under the terms of the GNU GPLv3 license. # This file may be distributed under the terms of the GNU GPLv3 license.
import collections import collections
import readlog
###################################################################### ######################################################################
@ -15,12 +16,13 @@ AHandlers = {}
# Calculate a derivative (position to velocity, or velocity to accel) # Calculate a derivative (position to velocity, or velocity to accel)
class GenDerivative: class GenDerivative:
ParametersMin = ParametersMax = 1
DataSets = [ DataSets = [
('derivative:<dataset>', 'Derivative of the given dataset'), ('derivative(<dataset>)', 'Derivative of the given dataset'),
] ]
def __init__(self, amanager, params): def __init__(self, amanager, name_parts):
self.amanager = amanager self.amanager = amanager
self.source = params self.source = name_parts[1]
amanager.setup_dataset(self.source) amanager.setup_dataset(self.source)
def get_label(self): def get_label(self):
label = self.amanager.get_label(self.source) label = self.amanager.get_label(self.source)
@ -46,28 +48,30 @@ AHandlers["derivative"] = GenDerivative
# Calculate a kinematic stepper position from the toolhead requested position # Calculate a kinematic stepper position from the toolhead requested position
class GenKinematicPosition: class GenKinematicPosition:
ParametersMin = ParametersMax = 1
DataSets = [ DataSets = [
('kin:<stepper>', 'Stepper position derived from toolhead kinematics'), ('kin(<stepper>)', 'Stepper position derived from toolhead kinematics'),
] ]
def __init__(self, amanager, params): def __init__(self, amanager, name_parts):
self.amanager = amanager self.amanager = amanager
stepper = name_parts[1]
status = self.amanager.get_initial_status() status = self.amanager.get_initial_status()
kin = status['configfile']['settings']['printer']['kinematics'] kin = status['configfile']['settings']['printer']['kinematics']
if kin not in ['cartesian', 'corexy']: if kin not in ['cartesian', 'corexy']:
raise amanager.error("Unsupported kinematics '%s'" % (kin,)) raise amanager.error("Unsupported kinematics '%s'" % (kin,))
if params not in ['stepper_x', 'stepper_y', 'stepper_z']: if stepper not in ['stepper_x', 'stepper_y', 'stepper_z']:
raise amanager.error("Unknown stepper '%s'" % (params,)) raise amanager.error("Unknown stepper '%s'" % (stepper,))
if kin == 'corexy' and params in ['stepper_x', 'stepper_y']: if kin == 'corexy' and stepper in ['stepper_x', 'stepper_y']:
self.source1 = 'trapq:toolhead:x' self.source1 = 'trapq(toolhead,x)'
self.source2 = 'trapq:toolhead:y' self.source2 = 'trapq(toolhead,y)'
if params == 'stepper_x': if stepper == 'stepper_x':
self.generate_data = self.generate_data_corexy_plus self.generate_data = self.generate_data_corexy_plus
else: else:
self.generate_data = self.generate_data_corexy_minus self.generate_data = self.generate_data_corexy_minus
amanager.setup_dataset(self.source1) amanager.setup_dataset(self.source1)
amanager.setup_dataset(self.source2) amanager.setup_dataset(self.source2)
else: else:
self.source1 = 'trapq:toolhead:' + params[-1:] self.source1 = 'trapq(toolhead,%s)' % (stepper[-1:],)
self.source2 = None self.source2 = None
self.generate_data = self.generate_data_passthrough self.generate_data = self.generate_data_passthrough
amanager.setup_dataset(self.source1) amanager.setup_dataset(self.source1)
@ -89,15 +93,13 @@ AHandlers["kin"] = GenKinematicPosition
# Calculate a position deviation # Calculate a position deviation
class GenDeviation: class GenDeviation:
ParametersMin = ParametersMax = 2
DataSets = [ DataSets = [
('deviation:<dataset1>-<dataset2>', 'Difference between datasets'), ('deviation(<dataset1>,<dataset2>)', 'Difference between datasets'),
] ]
def __init__(self, amanager, params): def __init__(self, amanager, name_parts):
self.amanager = amanager self.amanager = amanager
parts = params.split('-') self.source1, self.source2 = name_parts[1:]
if len(parts) != 2:
raise amanager.error("Invalid deviation '%s'" % (params,))
self.source1, self.source2 = parts
amanager.setup_dataset(self.source1) amanager.setup_dataset(self.source1)
amanager.setup_dataset(self.source2) amanager.setup_dataset(self.source2)
def get_label(self): def get_label(self):
@ -117,20 +119,16 @@ AHandlers["deviation"] = GenDeviation
###################################################################### ######################################################################
# List datasets # Analyzer management and data generation
###################################################################### ######################################################################
# Return a description of available analyzers
def list_datasets(): def list_datasets():
datasets = [] datasets = []
for ah in sorted(AHandlers.keys()): for ah in sorted(AHandlers.keys()):
datasets += AHandlers[ah].DataSets datasets += AHandlers[ah].DataSets
return datasets return datasets
######################################################################
# Data generation
######################################################################
# Manage raw and generated data samples # Manage raw and generated data samples
class AnalyzerManager: class AnalyzerManager:
error = None error = None
@ -159,15 +157,18 @@ class AnalyzerManager:
return self.raw_datasets[name] return self.raw_datasets[name]
if name in self.gen_datasets: if name in self.gen_datasets:
return self.gen_datasets[name] return self.gen_datasets[name]
nparts = name.split(':') name_parts = readlog.name_split(name)
if nparts[0] in self.lmanager.available_dataset_types(): if name_parts[0] in self.lmanager.available_dataset_types():
hdl = self.lmanager.setup_dataset(name) hdl = self.lmanager.setup_dataset(name)
self.raw_datasets[name] = hdl self.raw_datasets[name] = hdl
else: else:
cls = AHandlers.get(nparts[0]) cls = AHandlers.get(name_parts[0])
if cls is None: if cls is None:
raise self.error("Unknown dataset '%s'" % (name,)) raise self.error("Unknown dataset '%s'" % (name,))
hdl = cls(self, ':'.join(nparts[1:])) num_param = len(name_parts) - 1
if num_param < cls.ParametersMin or num_param > cls.ParametersMax:
raise self.error("Invalid parameters to dataset '%s'" % (name,))
hdl = cls(self, name_parts)
self.gen_datasets[name] = hdl self.gen_datasets[name] = hdl
self.datasets[name] = [] self.datasets[name] = []
return hdl return hdl

View File

@ -125,9 +125,9 @@ def main():
# Default graphs to draw # Default graphs to draw
graph_descs = [ graph_descs = [
["trapq:toolhead:velocity?color=green"], ["trapq(toolhead,velocity)?color=green"],
["trapq:toolhead:accel?color=green"], ["trapq(toolhead,accel)?color=green"],
["deviation:stepq:stepper_x-kin:stepper_x?color=blue"], ["deviation(stepq(stepper_x),kin(stepper_x))?color=blue"],
] ]
if options.graph is not None: if options.graph is not None:
graph_descs = ast.literal_eval(options.graph) graph_descs = ast.literal_eval(options.graph)

View File

@ -18,20 +18,21 @@ LogHandlers = {}
# Extract requested position, velocity, and accel from a trapq log # Extract requested position, velocity, and accel from a trapq log
class HandleTrapQ: class HandleTrapQ:
ParametersSubscriptionId = 2 SubscriptionIdParts = 2
ParametersMin = ParametersMax = 3 ParametersMin = ParametersMax = 2
DataSets = [ DataSets = [
('trapq:<name>:velocity', 'Requested velocity for the given trapq'), ('trapq(<name>,velocity)', 'Requested velocity for the given trapq'),
('trapq:<name>:<axis>', 'Requested axis (x, y, or z) position'), ('trapq(<name>,accel)', 'Requested acceleration for the given trapq'),
('trapq:<name>:<axis>_velocity', 'Requested axis velocity'), ('trapq(<name>,<axis>)', 'Requested axis (x, y, or z) position'),
('trapq:<name>:<axis>_accel', 'Requested axis acceleration'), ('trapq(<name>,<axis>_velocity)', 'Requested axis velocity'),
('trapq(<name>,<axis>_accel)', 'Requested axis acceleration'),
] ]
def __init__(self, lmanager, name): def __init__(self, lmanager, name, name_parts):
self.name = name self.name = name
self.jdispatch = lmanager.get_jdispatch() self.jdispatch = lmanager.get_jdispatch()
self.cur_data = [(0., 0., 0., 0., (0., 0., 0.), (0., 0., 0.))] self.cur_data = [(0., 0., 0., 0., (0., 0., 0.), (0., 0., 0.))]
self.data_pos = 0 self.data_pos = 0
tq, trapq_name, datasel = name.split(':') tq, trapq_name, datasel = name_parts
ptypes = {} ptypes = {}
ptypes['velocity'] = { ptypes['velocity'] = {
'label': '%s velocity' % (trapq_name,), 'label': '%s velocity' % (trapq_name,),
@ -113,26 +114,27 @@ LogHandlers["trapq"] = HandleTrapQ
# Extract positions from queue_step log # Extract positions from queue_step log
class HandleStepQ: class HandleStepQ:
ParametersSubscriptionId = 2 SubscriptionIdParts = 2
ParametersMin = 2 ParametersMin = 1
ParametersMax = 3 ParametersMax = 2
DataSets = [ DataSets = [
('stepq:<stepper>', 'Commanded position of the given stepper'), ('stepq(<stepper>)', 'Commanded position of the given stepper'),
('stepq:<stepper>:raw', 'Commanded position without smoothing'), ('stepq(<stepper>,<time>)', 'Commanded position with smooth time'),
] ]
def __init__(self, lmanager, name): def __init__(self, lmanager, name, name_parts):
self.name = name self.name = name
self.stepper_name = name_parts[1]
self.jdispatch = lmanager.get_jdispatch() self.jdispatch = lmanager.get_jdispatch()
self.step_data = [(0., 0., 0.), (0., 0., 0.)] # [(time, half_pos, pos)] self.step_data = [(0., 0., 0.), (0., 0., 0.)] # [(time, half_pos, pos)]
self.data_pos = 0 self.data_pos = 0
self.smooth_time = 0.010 self.smooth_time = 0.010
name_parts = name.split(':')
if len(name_parts) == 3: if len(name_parts) == 3:
if name_parts[2] != 'raw': try:
raise error("Unknown stepq data selection '%s'" % (name,)) self.smooth_time = float(name_parts[2])
self.smooth_time = 0. except ValueError:
raise error("Invalid stepq smooth time '%s'" % (name_parts[2],))
def get_label(self): def get_label(self):
label = '%s position' % (self.name.split(':')[1],) label = '%s position' % (self.stepper_name,)
return {'label': label, 'units': 'Position\n(mm)'} return {'label': label, 'units': 'Position\n(mm)'}
def pull_data(self, req_time): def pull_data(self, req_time):
smooth_time = self.smooth_time smooth_time = self.smooth_time
@ -204,17 +206,6 @@ class HandleStepQ:
LogHandlers["stepq"] = HandleStepQ LogHandlers["stepq"] = HandleStepQ
######################################################################
# List datasets
######################################################################
def list_datasets():
datasets = []
for lh in sorted(LogHandlers.keys()):
datasets += LogHandlers[lh].DataSets
return datasets
###################################################################### ######################################################################
# Log reading # Log reading
###################################################################### ######################################################################
@ -279,6 +270,40 @@ class JsonDispatcher:
for mq in self.queues.get(qid, []): for mq in self.queues.get(qid, []):
mq.append(json_msg['params']) mq.append(json_msg['params'])
######################################################################
# Dataset and log tracking
######################################################################
# 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 # Main log access management
class LogManager: class LogManager:
error = error error = error
@ -325,15 +350,16 @@ class LogManager:
def setup_dataset(self, name): def setup_dataset(self, name):
if name in self.datasets: if name in self.datasets:
return self.datasets[name] return self.datasets[name]
parts = name.split(':') name_parts = name_split(name)
cls = LogHandlers.get(parts[0]) cls = LogHandlers.get(name_parts[0])
if cls is None: if cls is None:
raise error("Unknown dataset '%s'" % (parts[0],)) raise error("Unknown dataset '%s'" % (name_parts[0],))
if len(parts) < cls.ParametersMin or len(parts) > cls.ParametersMax: len_pp = len(name_parts) - 1
raise error("Invalid number of parameters for %s" % (parts[0],)) if len_pp < cls.ParametersMin or len_pp > cls.ParametersMax:
subscription_id = ":".join(parts[:cls.ParametersSubscriptionId]) raise error("Invalid number of parameters for '%s'" % (name,))
subscription_id = ":".join(name_parts[:cls.SubscriptionIdParts])
if subscription_id not in self.log_subscriptions: if subscription_id not in self.log_subscriptions:
raise error("Dataset '%s' not in capture" % (subscription_id,)) raise error("Dataset '%s' not in capture" % (subscription_id,))
self.datasets[name] = hdl = cls(self, name) self.datasets[name] = hdl = cls(self, name, name_parts)
self.jdispatch.add_handler(name, subscription_id) self.jdispatch.add_handler(name, subscription_id)
return hdl return hdl