mirror of
https://github.com/QIDITECH/klipper.git
synced 2026-01-30 15:38:42 +03:00
Add files via upload
This commit is contained in:
44
scripts/Dockerfile
Normal file
44
scripts/Dockerfile
Normal file
@@ -0,0 +1,44 @@
|
||||
# This is an example Dockerfile showing how it's possible to install Klipper in Docker.
|
||||
# IMPORTANT: The docker build must be run from the root of the repo, either copy the
|
||||
# Dockerfile to the root, or run docker build with "-f", for example:
|
||||
# docker build . -f scripts/Dockerfile -t klipper
|
||||
# Note that the host still needs to run Linux to connect the printers serial port to
|
||||
# the container.
|
||||
# When running, the serial port of your printer should be connected, including an
|
||||
# argument such as:
|
||||
# --device /dev/ttyUSB0:/dev/ttyUSB0
|
||||
# It's also required that your control program (eg: OctoPrint) be included in the same
|
||||
# container as Docker does not allow sharing of the virtual serial port outside the
|
||||
# container.
|
||||
# The config should be in a file named "printer.cfg" in a directory mounted at:
|
||||
# /home/klippy/.config/
|
||||
# For more Dockerfile examples with Klipper (and Octoprint) see:
|
||||
# https://github.com/sillyfrog/OctoPrint-Klipper-mjpg-Dockerfile
|
||||
FROM ubuntu:18.04
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y sudo
|
||||
|
||||
# Create user
|
||||
RUN useradd -ms /bin/bash klippy && adduser klippy dialout
|
||||
USER klippy
|
||||
|
||||
#This fixes issues with the volume command setting wrong permissions
|
||||
RUN mkdir /home/klippy/.config
|
||||
VOLUME /home/klippy/.config
|
||||
|
||||
### Klipper setup ###
|
||||
WORKDIR /home/klippy
|
||||
|
||||
COPY . klipper/
|
||||
USER root
|
||||
RUN echo 'klippy ALL=(ALL:ALL) NOPASSWD: ALL' > /etc/sudoers.d/klippy && \
|
||||
chown klippy:klippy -R klipper
|
||||
# This is to allow the install script to run without error
|
||||
RUN ln -s /bin/true /bin/systemctl
|
||||
USER klippy
|
||||
RUN ./klipper/scripts/install-ubuntu-18.04.sh
|
||||
# Clean up install script workaround
|
||||
RUN sudo rm -f /bin/systemctl
|
||||
|
||||
CMD ["/home/klippy/klippy-env/bin/python", "/home/klippy/klipper/klippy/klippy.py", "/home/klippy/.config/printer.cfg"]
|
||||
245
scripts/avrsim.py
Normal file
245
scripts/avrsim.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
# Script to interact with simulavr by simulating a serial port.
|
||||
#
|
||||
# Copyright (C) 2015-2018 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import sys, optparse, time, os, pty, fcntl, termios, errno
|
||||
import pysimulavr
|
||||
|
||||
SERIALBITS = 10 # 8N1 = 1 start, 8 data, 1 stop
|
||||
SIMULAVR_FREQ = 10**9
|
||||
|
||||
# Class to read serial data from AVR serial transmit pin.
|
||||
class SerialRxPin(pysimulavr.PySimulationMember, pysimulavr.Pin):
|
||||
def __init__(self, baud, terminal):
|
||||
pysimulavr.Pin.__init__(self)
|
||||
pysimulavr.PySimulationMember.__init__(self)
|
||||
self.terminal = terminal
|
||||
self.sc = pysimulavr.SystemClock.Instance()
|
||||
self.delay = SIMULAVR_FREQ // baud
|
||||
self.current = 0
|
||||
self.pos = -1
|
||||
def SetInState(self, pin):
|
||||
pysimulavr.Pin.SetInState(self, pin)
|
||||
self.state = pin.outState
|
||||
if self.pos < 0 and pin.outState == pin.LOW:
|
||||
self.pos = 0
|
||||
self.sc.Add(self)
|
||||
def DoStep(self, trueHwStep):
|
||||
ishigh = self.state == self.HIGH
|
||||
self.current |= ishigh << self.pos
|
||||
self.pos += 1
|
||||
if self.pos == 1:
|
||||
return int(self.delay * 1.5)
|
||||
if self.pos >= SERIALBITS:
|
||||
data = bytearray([(self.current >> 1) & 0xff])
|
||||
self.terminal.write(data)
|
||||
self.pos = -1
|
||||
self.current = 0
|
||||
return -1
|
||||
return self.delay
|
||||
|
||||
# Class to send serial data to AVR serial receive pin.
|
||||
class SerialTxPin(pysimulavr.PySimulationMember, pysimulavr.Pin):
|
||||
def __init__(self, baud, terminal):
|
||||
pysimulavr.Pin.__init__(self)
|
||||
pysimulavr.PySimulationMember.__init__(self)
|
||||
self.terminal = terminal
|
||||
self.SetPin('H')
|
||||
self.sc = pysimulavr.SystemClock.Instance()
|
||||
self.delay = SIMULAVR_FREQ // baud
|
||||
self.current = 0
|
||||
self.pos = 0
|
||||
self.queue = bytearray()
|
||||
self.sc.Add(self)
|
||||
def DoStep(self, trueHwStep):
|
||||
if not self.pos:
|
||||
if not self.queue:
|
||||
data = self.terminal.read()
|
||||
if not data:
|
||||
return self.delay * 100
|
||||
self.queue.extend(data)
|
||||
self.current = (self.queue.pop(0) << 1) | 0x200
|
||||
newstate = 'L'
|
||||
if self.current & (1 << self.pos):
|
||||
newstate = 'H'
|
||||
self.SetPin(newstate)
|
||||
self.pos += 1
|
||||
if self.pos >= SERIALBITS:
|
||||
self.pos = 0
|
||||
return self.delay
|
||||
|
||||
# Support for creating VCD trace files
|
||||
class Tracing:
|
||||
def __init__(self, filename, signals):
|
||||
self.filename = filename
|
||||
self.signals = signals
|
||||
if not signals:
|
||||
self.dman = None
|
||||
return
|
||||
self.dman = pysimulavr.DumpManager.Instance()
|
||||
self.dman.SetSingleDeviceApp()
|
||||
def show_help(self):
|
||||
ostr = pysimulavr.ostringstream()
|
||||
self.dman.save(ostr)
|
||||
sys.stdout.write(ostr.str())
|
||||
sys.exit(1)
|
||||
def load_options(self):
|
||||
if self.dman is None:
|
||||
return
|
||||
if self.signals.strip() == '?':
|
||||
self.show_help()
|
||||
sigs = "\n".join(["+ " + s for s in self.signals.split(',')])
|
||||
self.dman.addDumpVCD(self.filename, sigs, "ns", False, False)
|
||||
def start(self):
|
||||
if self.dman is not None:
|
||||
self.dman.start()
|
||||
def finish(self):
|
||||
if self.dman is not None:
|
||||
self.dman.stopApplication()
|
||||
|
||||
# Pace the simulation scaled to real time
|
||||
class Pacing(pysimulavr.PySimulationMember):
|
||||
def __init__(self, rate):
|
||||
pysimulavr.PySimulationMember.__init__(self)
|
||||
self.sc = pysimulavr.SystemClock.Instance()
|
||||
self.pacing_rate = 1. / (rate * SIMULAVR_FREQ)
|
||||
self.next_check_clock = 0
|
||||
self.rel_time = time.time()
|
||||
self.best_offset = 0.
|
||||
self.delay = SIMULAVR_FREQ // 10000
|
||||
self.sc.Add(self)
|
||||
def DoStep(self, trueHwStep):
|
||||
curtime = time.time()
|
||||
clock = self.sc.GetCurrentTime()
|
||||
offset = clock * self.pacing_rate - (curtime - self.rel_time)
|
||||
self.best_offset = max(self.best_offset, offset)
|
||||
if offset > 0.000050:
|
||||
time.sleep(offset - 0.000040)
|
||||
if clock >= self.next_check_clock:
|
||||
self.rel_time -= min(self.best_offset, 0.)
|
||||
self.next_check_clock = clock + self.delay * 500
|
||||
self.best_offset = -999999999.
|
||||
return self.delay
|
||||
|
||||
# Forward data from a terminal device to the serial port pins
|
||||
class TerminalIO:
|
||||
def __init__(self):
|
||||
self.fd = -1
|
||||
def run(self, fd):
|
||||
self.fd = fd
|
||||
def write(self, data):
|
||||
os.write(self.fd, data)
|
||||
def read(self):
|
||||
try:
|
||||
return os.read(self.fd, 64)
|
||||
except os.error as e:
|
||||
if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK):
|
||||
pysimulavr.SystemClock.Instance().stop()
|
||||
return ""
|
||||
|
||||
# Support for creating a pseudo-tty for emulating a serial port
|
||||
def create_pty(ptyname):
|
||||
mfd, sfd = pty.openpty()
|
||||
try:
|
||||
os.unlink(ptyname)
|
||||
except os.error:
|
||||
pass
|
||||
os.symlink(os.ttyname(sfd), ptyname)
|
||||
fcntl.fcntl(mfd, fcntl.F_SETFL
|
||||
, fcntl.fcntl(mfd, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
tcattr = termios.tcgetattr(mfd)
|
||||
tcattr[0] &= ~(
|
||||
termios.IGNBRK | termios.BRKINT | termios.PARMRK | termios.ISTRIP |
|
||||
termios.INLCR | termios.IGNCR | termios.ICRNL | termios.IXON)
|
||||
tcattr[1] &= ~termios.OPOST
|
||||
tcattr[3] &= ~(
|
||||
termios.ECHO | termios.ECHONL | termios.ICANON | termios.ISIG |
|
||||
termios.IEXTEN)
|
||||
tcattr[2] &= ~(termios.CSIZE | termios.PARENB)
|
||||
tcattr[2] |= termios.CS8
|
||||
tcattr[6][termios.VMIN] = 0
|
||||
tcattr[6][termios.VTIME] = 0
|
||||
termios.tcsetattr(mfd, termios.TCSAFLUSH, tcattr)
|
||||
return mfd
|
||||
|
||||
def main():
|
||||
usage = "%prog [options] <program.elf>"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-m", "--machine", type="string", dest="machine",
|
||||
default="atmega644", help="type of AVR machine to simulate")
|
||||
opts.add_option("-s", "--speed", type="int", dest="speed", default=16000000,
|
||||
help="machine speed")
|
||||
opts.add_option("-r", "--rate", type="float", dest="pacing_rate",
|
||||
default=0., help="real-time pacing rate")
|
||||
opts.add_option("-b", "--baud", type="int", dest="baud", default=250000,
|
||||
help="baud rate of the emulated serial port")
|
||||
opts.add_option("-t", "--trace", type="string", dest="trace",
|
||||
help="signals to trace (? for help)")
|
||||
opts.add_option("-p", "--port", type="string", dest="port",
|
||||
default="/tmp/pseudoserial",
|
||||
help="pseudo-tty device to create for serial port")
|
||||
deffile = os.path.splitext(os.path.basename(sys.argv[0]))[0] + ".vcd"
|
||||
opts.add_option("-f", "--tracefile", type="string", dest="tracefile",
|
||||
default=deffile, help="filename to write signal trace to")
|
||||
options, args = opts.parse_args()
|
||||
if len(args) != 1:
|
||||
opts.error("Incorrect number of arguments")
|
||||
elffile = args[0]
|
||||
proc = options.machine
|
||||
ptyname = options.port
|
||||
speed = options.speed
|
||||
baud = options.baud
|
||||
|
||||
# launch simulator
|
||||
sc = pysimulavr.SystemClock.Instance()
|
||||
trace = Tracing(options.tracefile, options.trace)
|
||||
dev = pysimulavr.AvrFactory.instance().makeDevice(proc)
|
||||
dev.Load(elffile)
|
||||
dev.SetClockFreq(SIMULAVR_FREQ // speed)
|
||||
sc.Add(dev)
|
||||
pysimulavr.cvar.sysConHandler.SetUseExit(False)
|
||||
trace.load_options()
|
||||
|
||||
# Do optional real-time pacing
|
||||
if options.pacing_rate:
|
||||
pacing = Pacing(options.pacing_rate)
|
||||
|
||||
# Setup terminal
|
||||
io = TerminalIO()
|
||||
|
||||
# Setup rx pin
|
||||
rxpin = SerialRxPin(baud, io)
|
||||
net = pysimulavr.Net()
|
||||
net.Add(rxpin)
|
||||
net.Add(dev.GetPin("D1"))
|
||||
|
||||
# Setup tx pin
|
||||
txpin = SerialTxPin(baud, io)
|
||||
net2 = pysimulavr.Net()
|
||||
net2.Add(dev.GetPin("D0"))
|
||||
net2.Add(txpin)
|
||||
|
||||
# Display start banner
|
||||
msg = "Starting AVR simulation: machine=%s speed=%d\n" % (proc, speed)
|
||||
msg += "Serial: port=%s baud=%d\n" % (ptyname, baud)
|
||||
if options.trace:
|
||||
msg += "Trace file: %s\n" % (options.tracefile,)
|
||||
sys.stdout.write(msg)
|
||||
sys.stdout.flush()
|
||||
|
||||
# Create terminal device
|
||||
fd = create_pty(ptyname)
|
||||
|
||||
# Run loop
|
||||
try:
|
||||
io.run(fd)
|
||||
trace.start()
|
||||
sc.RunTimeRange(0x7fff0000ffff0000)
|
||||
trace.finish()
|
||||
finally:
|
||||
os.unlink(ptyname)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
618
scripts/buildcommands.py
Normal file
618
scripts/buildcommands.py
Normal file
@@ -0,0 +1,618 @@
|
||||
#!/usr/bin/env python2
|
||||
# Script to handle build time requests embedded in C code.
|
||||
#
|
||||
# Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import sys, os, subprocess, optparse, logging, shlex, socket, time, traceback
|
||||
import json, zlib
|
||||
sys.path.append('./klippy')
|
||||
import msgproto
|
||||
|
||||
FILEHEADER = """
|
||||
/* DO NOT EDIT! This is an autogenerated file. See scripts/buildcommands.py. */
|
||||
|
||||
#include "board/irq.h"
|
||||
#include "board/pgm.h"
|
||||
#include "command.h"
|
||||
#include "compiler.h"
|
||||
#include "initial_pins.h"
|
||||
"""
|
||||
|
||||
def error(msg):
|
||||
sys.stderr.write(msg + "\n")
|
||||
sys.exit(-1)
|
||||
|
||||
Handlers = []
|
||||
|
||||
|
||||
######################################################################
|
||||
# C call list generation
|
||||
######################################################################
|
||||
|
||||
# Create dynamic C functions that call a list of other C functions
|
||||
class HandleCallList:
|
||||
def __init__(self):
|
||||
self.call_lists = {'ctr_run_initfuncs': []}
|
||||
self.ctr_dispatch = { '_DECL_CALLLIST': self.decl_calllist }
|
||||
def decl_calllist(self, req):
|
||||
funcname, callname = req.split()[1:]
|
||||
self.call_lists.setdefault(funcname, []).append(callname)
|
||||
def update_data_dictionary(self, data):
|
||||
pass
|
||||
def generate_code(self, options):
|
||||
code = []
|
||||
for funcname, funcs in self.call_lists.items():
|
||||
func_code = [' extern void %s(void);\n %s();' % (f, f)
|
||||
for f in funcs]
|
||||
if funcname == 'ctr_run_taskfuncs':
|
||||
add_poll = ' irq_poll();\n'
|
||||
func_code = [add_poll + fc for fc in func_code]
|
||||
func_code.append(add_poll)
|
||||
fmt = """
|
||||
void
|
||||
%s(void)
|
||||
{
|
||||
%s
|
||||
}
|
||||
"""
|
||||
code.append(fmt % (funcname, "\n".join(func_code).strip()))
|
||||
return "".join(code)
|
||||
|
||||
Handlers.append(HandleCallList())
|
||||
|
||||
|
||||
######################################################################
|
||||
# Enumeration and static string generation
|
||||
######################################################################
|
||||
|
||||
STATIC_STRING_MIN = 2
|
||||
|
||||
# Generate a dynamic string to integer mapping
|
||||
class HandleEnumerations:
|
||||
def __init__(self):
|
||||
self.static_strings = []
|
||||
self.enumerations = {}
|
||||
self.ctr_dispatch = {
|
||||
'_DECL_STATIC_STR': self.decl_static_str,
|
||||
'DECL_ENUMERATION': self.decl_enumeration,
|
||||
'DECL_ENUMERATION_RANGE': self.decl_enumeration_range
|
||||
}
|
||||
def add_enumeration(self, enum, name, value):
|
||||
enums = self.enumerations.setdefault(enum, {})
|
||||
if name in enums and enums[name] != value:
|
||||
error("Conflicting definition for enumeration '%s %s'" % (
|
||||
enum, name))
|
||||
enums[name] = value
|
||||
def decl_enumeration(self, req):
|
||||
enum, name, value = req.split()[1:]
|
||||
self.add_enumeration(enum, name, int(value, 0))
|
||||
def decl_enumeration_range(self, req):
|
||||
enum, name, value, count = req.split()[1:]
|
||||
self.add_enumeration(enum, name, (int(value, 0), int(count, 0)))
|
||||
def decl_static_str(self, req):
|
||||
msg = req.split(None, 1)[1]
|
||||
if msg not in self.static_strings:
|
||||
self.static_strings.append(msg)
|
||||
def update_data_dictionary(self, data):
|
||||
for i, s in enumerate(self.static_strings):
|
||||
self.add_enumeration("static_string_id", s, i + STATIC_STRING_MIN)
|
||||
data['enumerations'] = self.enumerations
|
||||
def generate_code(self, options):
|
||||
code = []
|
||||
for i, s in enumerate(self.static_strings):
|
||||
code.append(' if (__builtin_strcmp(str, "%s") == 0)\n'
|
||||
' return %d;\n' % (s, i + STATIC_STRING_MIN))
|
||||
fmt = """
|
||||
uint8_t __always_inline
|
||||
ctr_lookup_static_string(const char *str)
|
||||
{
|
||||
%s
|
||||
return 0xff;
|
||||
}
|
||||
"""
|
||||
return fmt % ("".join(code).strip(),)
|
||||
|
||||
HandlerEnumerations = HandleEnumerations()
|
||||
Handlers.append(HandlerEnumerations)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Constants
|
||||
######################################################################
|
||||
|
||||
# Allow adding build time constants to the data dictionary
|
||||
class HandleConstants:
|
||||
def __init__(self):
|
||||
self.constants = {}
|
||||
self.ctr_dispatch = {
|
||||
'DECL_CONSTANT': self.decl_constant,
|
||||
'DECL_CONSTANT_STR': self.decl_constant_str,
|
||||
}
|
||||
def set_value(self, name, value):
|
||||
if name in self.constants and self.constants[name] != value:
|
||||
error("Conflicting definition for constant '%s'" % name)
|
||||
self.constants[name] = value
|
||||
def decl_constant(self, req):
|
||||
name, value = req.split()[1:]
|
||||
self.set_value(name, int(value, 0))
|
||||
def decl_constant_str(self, req):
|
||||
name, value = req.split(None, 2)[1:]
|
||||
value = value.strip()
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
self.set_value(name, value)
|
||||
def update_data_dictionary(self, data):
|
||||
data['config'] = self.constants
|
||||
def generate_code(self, options):
|
||||
return ""
|
||||
|
||||
HandlerConstants = HandleConstants()
|
||||
Handlers.append(HandlerConstants)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Initial pins
|
||||
######################################################################
|
||||
|
||||
class HandleInitialPins:
|
||||
def __init__(self):
|
||||
self.initial_pins = []
|
||||
self.ctr_dispatch = { 'DECL_INITIAL_PINS': self.decl_initial_pins }
|
||||
def decl_initial_pins(self, req):
|
||||
pins = req.split(None, 1)[1].strip()
|
||||
if pins.startswith('"') and pins.endswith('"'):
|
||||
pins = pins[1:-1]
|
||||
if pins:
|
||||
self.initial_pins = [p.strip() for p in pins.split(',')]
|
||||
HandlerConstants.decl_constant_str(
|
||||
"_DECL_CONSTANT_STR INITIAL_PINS "
|
||||
+ ','.join(self.initial_pins))
|
||||
def update_data_dictionary(self, data):
|
||||
pass
|
||||
def map_pins(self):
|
||||
if not self.initial_pins:
|
||||
return []
|
||||
mp = msgproto.MessageParser()
|
||||
mp.fill_enumerations(HandlerEnumerations.enumerations)
|
||||
pinmap = mp.get_enumerations().get('pin', {})
|
||||
out = []
|
||||
for p in self.initial_pins:
|
||||
flag = "IP_OUT_HIGH"
|
||||
if p.startswith('!'):
|
||||
flag = "0"
|
||||
p = p[1:].strip()
|
||||
if p not in pinmap:
|
||||
error("Unknown initial pin '%s'" % (p,))
|
||||
out.append("\n {%d, %s}, // %s" % (pinmap[p], flag, p))
|
||||
return out
|
||||
def generate_code(self, options):
|
||||
out = self.map_pins()
|
||||
fmt = """
|
||||
const struct initial_pin_s initial_pins[] PROGMEM = {%s
|
||||
};
|
||||
const int initial_pins_size PROGMEM = ARRAY_SIZE(initial_pins);
|
||||
"""
|
||||
return fmt % (''.join(out),)
|
||||
|
||||
Handlers.append(HandleInitialPins())
|
||||
|
||||
|
||||
######################################################################
|
||||
# ARM IRQ vector table generation
|
||||
######################################################################
|
||||
|
||||
# Create ARM IRQ vector table from interrupt handler declarations
|
||||
class Handle_arm_irq:
|
||||
def __init__(self):
|
||||
self.irqs = {}
|
||||
self.ctr_dispatch = { 'DECL_ARMCM_IRQ': self.decl_armcm_irq }
|
||||
def decl_armcm_irq(self, req):
|
||||
func, num = req.split()[1:]
|
||||
num = int(num, 0)
|
||||
if num in self.irqs and self.irqs[num] != func:
|
||||
error("Conflicting IRQ definition %d (old %s new %s)"
|
||||
% (num, self.irqs[num], func))
|
||||
self.irqs[num] = func
|
||||
def update_data_dictionary(self, data):
|
||||
pass
|
||||
def generate_code(self, options):
|
||||
armcm_offset = 16
|
||||
if 1 - armcm_offset not in self.irqs:
|
||||
# The ResetHandler was not defined - don't build VectorTable
|
||||
return ""
|
||||
max_irq = max(self.irqs.keys())
|
||||
table = [" DefaultHandler,\n"] * (max_irq + armcm_offset + 1)
|
||||
defs = []
|
||||
for num, func in self.irqs.items():
|
||||
if num < 1 - armcm_offset:
|
||||
error("Invalid IRQ %d (%s)" % (num, func))
|
||||
defs.append("extern void %s(void);\n" % (func,))
|
||||
table[num + armcm_offset] = " %s,\n" % (func,)
|
||||
table[0] = " &_stack_end,\n"
|
||||
fmt = """
|
||||
extern void DefaultHandler(void);
|
||||
extern uint32_t _stack_end;
|
||||
%s
|
||||
const void *VectorTable[] __visible __section(".vector_table") = {
|
||||
%s};
|
||||
"""
|
||||
return fmt % (''.join(defs), ''.join(table))
|
||||
|
||||
Handlers.append(Handle_arm_irq())
|
||||
|
||||
|
||||
######################################################################
|
||||
# Wire protocol commands and responses
|
||||
######################################################################
|
||||
|
||||
# Dynamic command and response registration
|
||||
class HandleCommandGeneration:
|
||||
def __init__(self):
|
||||
self.commands = {}
|
||||
self.encoders = []
|
||||
self.msg_to_id = dict(msgproto.DefaultMessages)
|
||||
self.messages_by_name = { m.split()[0]: m for m in self.msg_to_id }
|
||||
self.all_param_types = {}
|
||||
self.ctr_dispatch = {
|
||||
'DECL_COMMAND_FLAGS': self.decl_command,
|
||||
'_DECL_ENCODER': self.decl_encoder,
|
||||
'_DECL_OUTPUT': self.decl_output
|
||||
}
|
||||
def decl_command(self, req):
|
||||
funcname, flags, msgname = req.split()[1:4]
|
||||
if msgname in self.commands:
|
||||
error("Multiple definitions for command '%s'" % msgname)
|
||||
self.commands[msgname] = (funcname, flags, msgname)
|
||||
msg = req.split(None, 3)[3]
|
||||
m = self.messages_by_name.get(msgname)
|
||||
if m is not None and m != msg:
|
||||
error("Conflicting definition for command '%s'" % msgname)
|
||||
self.messages_by_name[msgname] = msg
|
||||
def decl_encoder(self, req):
|
||||
msg = req.split(None, 1)[1]
|
||||
msgname = msg.split()[0]
|
||||
m = self.messages_by_name.get(msgname)
|
||||
if m is not None and m != msg:
|
||||
error("Conflicting definition for message '%s'" % msgname)
|
||||
self.messages_by_name[msgname] = msg
|
||||
self.encoders.append((msgname, msg))
|
||||
def decl_output(self, req):
|
||||
msg = req.split(None, 1)[1]
|
||||
self.encoders.append((None, msg))
|
||||
def create_message_ids(self):
|
||||
# Create unique ids for each message type
|
||||
msgid = max(self.msg_to_id.values())
|
||||
mlist = list(self.commands.keys()) + [m for n, m in self.encoders]
|
||||
for msgname in mlist:
|
||||
msg = self.messages_by_name.get(msgname, msgname)
|
||||
if msg not in self.msg_to_id:
|
||||
msgid += 1
|
||||
self.msg_to_id[msg] = msgid
|
||||
if msgid >= 128:
|
||||
# The mcu currently assumes all message ids encode to one byte
|
||||
error("Too many message ids")
|
||||
def update_data_dictionary(self, data):
|
||||
# Handle message ids over 96 (they are decoded as negative numbers)
|
||||
msg_to_tag = {msg: msgid if msgid < 96 else msgid - 128
|
||||
for msg, msgid in self.msg_to_id.items()}
|
||||
command_tags = [msg_to_tag[msg]
|
||||
for msgname, msg in self.messages_by_name.items()
|
||||
if msgname in self.commands]
|
||||
response_tags = [msg_to_tag[msg]
|
||||
for msgname, msg in self.messages_by_name.items()
|
||||
if msgname not in self.commands]
|
||||
data['commands'] = { msg: msgtag for msg, msgtag in msg_to_tag.items()
|
||||
if msgtag in command_tags }
|
||||
data['responses'] = { msg: msgtag for msg, msgtag in msg_to_tag.items()
|
||||
if msgtag in response_tags }
|
||||
output = {msg: msgtag for msg, msgtag in msg_to_tag.items()
|
||||
if msgtag not in command_tags and msgtag not in response_tags}
|
||||
if output:
|
||||
data['output'] = output
|
||||
def build_parser(self, msgid, msgformat, msgtype):
|
||||
if msgtype == "output":
|
||||
param_types = msgproto.lookup_output_params(msgformat)
|
||||
comment = "Output: " + msgformat
|
||||
else:
|
||||
param_types = [t for name, t in msgproto.lookup_params(msgformat)]
|
||||
comment = msgformat
|
||||
params = '0'
|
||||
types = tuple([t.__class__.__name__ for t in param_types])
|
||||
if types:
|
||||
paramid = self.all_param_types.get(types)
|
||||
if paramid is None:
|
||||
paramid = len(self.all_param_types)
|
||||
self.all_param_types[types] = paramid
|
||||
params = 'command_parameters%d' % (paramid,)
|
||||
out = """
|
||||
// %s
|
||||
.msg_id=%d,
|
||||
.num_params=%d,
|
||||
.param_types = %s,
|
||||
""" % (comment, msgid, len(types), params)
|
||||
if msgtype == 'response':
|
||||
num_args = (len(types) + types.count('PT_progmem_buffer')
|
||||
+ types.count('PT_buffer'))
|
||||
out += " .num_args=%d," % (num_args,)
|
||||
else:
|
||||
max_size = min(msgproto.MESSAGE_MAX,
|
||||
(msgproto.MESSAGE_MIN + 1
|
||||
+ sum([t.max_length for t in param_types])))
|
||||
out += " .max_size=%d," % (max_size,)
|
||||
return out
|
||||
def generate_responses_code(self):
|
||||
encoder_defs = []
|
||||
output_code = []
|
||||
encoder_code = []
|
||||
did_output = {}
|
||||
for msgname, msg in self.encoders:
|
||||
msgid = self.msg_to_id[msg]
|
||||
if msgid in did_output:
|
||||
continue
|
||||
did_output[msgid] = True
|
||||
code = (' if (__builtin_strcmp(str, "%s") == 0)\n'
|
||||
' return &command_encoder_%s;\n' % (msg, msgid))
|
||||
if msgname is None:
|
||||
parsercode = self.build_parser(msgid, msg, 'output')
|
||||
output_code.append(code)
|
||||
else:
|
||||
parsercode = self.build_parser(msgid, msg, 'command')
|
||||
encoder_code.append(code)
|
||||
encoder_defs.append(
|
||||
"const struct command_encoder command_encoder_%s PROGMEM = {"
|
||||
" %s\n};\n" % (
|
||||
msgid, parsercode))
|
||||
fmt = """
|
||||
%s
|
||||
|
||||
const __always_inline struct command_encoder *
|
||||
ctr_lookup_encoder(const char *str)
|
||||
{
|
||||
%s
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const __always_inline struct command_encoder *
|
||||
ctr_lookup_output(const char *str)
|
||||
{
|
||||
%s
|
||||
return NULL;
|
||||
}
|
||||
"""
|
||||
return fmt % ("".join(encoder_defs).strip(),
|
||||
"".join(encoder_code).strip(),
|
||||
"".join(output_code).strip())
|
||||
def generate_commands_code(self):
|
||||
cmd_by_id = {
|
||||
self.msg_to_id[self.messages_by_name.get(msgname, msgname)]: cmd
|
||||
for msgname, cmd in self.commands.items()
|
||||
}
|
||||
max_cmd_msgid = max(cmd_by_id.keys())
|
||||
index = []
|
||||
externs = {}
|
||||
for msgid in range(max_cmd_msgid+1):
|
||||
if msgid not in cmd_by_id:
|
||||
index.append(" {\n},")
|
||||
continue
|
||||
funcname, flags, msgname = cmd_by_id[msgid]
|
||||
msg = self.messages_by_name[msgname]
|
||||
externs[funcname] = 1
|
||||
parsercode = self.build_parser(msgid, msg, 'response')
|
||||
index.append(" {%s\n .flags=%s,\n .func=%s\n}," % (
|
||||
parsercode, flags, funcname))
|
||||
index = "".join(index).strip()
|
||||
externs = "\n".join(["extern void "+funcname+"(uint32_t*);"
|
||||
for funcname in sorted(externs)])
|
||||
fmt = """
|
||||
%s
|
||||
|
||||
const struct command_parser command_index[] PROGMEM = {
|
||||
%s
|
||||
};
|
||||
|
||||
const uint8_t command_index_size PROGMEM = ARRAY_SIZE(command_index);
|
||||
"""
|
||||
return fmt % (externs, index)
|
||||
def generate_param_code(self):
|
||||
sorted_param_types = sorted(
|
||||
[(i, a) for a, i in self.all_param_types.items()])
|
||||
params = ['']
|
||||
for paramid, argtypes in sorted_param_types:
|
||||
params.append(
|
||||
'static const uint8_t command_parameters%d[] PROGMEM = {\n'
|
||||
' %s };' % (
|
||||
paramid, ', '.join(argtypes),))
|
||||
params.append('')
|
||||
return "\n".join(params)
|
||||
def generate_code(self, options):
|
||||
self.create_message_ids()
|
||||
parsercode = self.generate_responses_code()
|
||||
cmdcode = self.generate_commands_code()
|
||||
paramcode = self.generate_param_code()
|
||||
return paramcode + parsercode + cmdcode
|
||||
|
||||
Handlers.append(HandleCommandGeneration())
|
||||
|
||||
|
||||
######################################################################
|
||||
# Version generation
|
||||
######################################################################
|
||||
|
||||
# Run program and return the specified output
|
||||
def check_output(prog):
|
||||
logging.debug("Running %s" % (repr(prog),))
|
||||
try:
|
||||
process = subprocess.Popen(shlex.split(prog), stdout=subprocess.PIPE)
|
||||
output = process.communicate()[0]
|
||||
retcode = process.poll()
|
||||
except OSError:
|
||||
logging.debug("Exception on run: %s" % (traceback.format_exc(),))
|
||||
return ""
|
||||
logging.debug("Got (code=%s): %s" % (retcode, repr(output)))
|
||||
if retcode:
|
||||
return ""
|
||||
try:
|
||||
return str(output.decode('utf8'))
|
||||
except UnicodeError:
|
||||
logging.debug("Exception on decode: %s" % (traceback.format_exc(),))
|
||||
return ""
|
||||
|
||||
# Obtain version info from "git" program
|
||||
def git_version():
|
||||
if not os.path.exists('.git'):
|
||||
logging.debug("No '.git' file/directory found")
|
||||
return ""
|
||||
ver = check_output("git describe --always --tags --long --dirty").strip()
|
||||
logging.debug("Got git version: %s" % (repr(ver),))
|
||||
return ver
|
||||
|
||||
def build_version(extra, cleanbuild):
|
||||
version = git_version()
|
||||
if not version:
|
||||
cleanbuild = False
|
||||
version = "?"
|
||||
elif 'dirty' in version:
|
||||
cleanbuild = False
|
||||
if not cleanbuild:
|
||||
btime = time.strftime("%Y%m%d_%H%M%S")
|
||||
hostname = socket.gethostname()
|
||||
version = "%s-%s-%s" % (version, btime, hostname)
|
||||
return version + extra
|
||||
|
||||
# Run "tool --version" for each specified tool and extract versions
|
||||
def tool_versions(tools):
|
||||
tools = [t.strip() for t in tools.split(';')]
|
||||
versions = ['', '']
|
||||
success = 0
|
||||
for tool in tools:
|
||||
# Extract first line from "tool --version" output
|
||||
verstr = check_output("%s --version" % (tool,)).split('\n')[0]
|
||||
# Check if this tool looks like a binutils program
|
||||
isbinutils = 0
|
||||
if verstr.startswith('GNU '):
|
||||
isbinutils = 1
|
||||
verstr = verstr[4:]
|
||||
# Extract version information and exclude program name
|
||||
if ' ' not in verstr:
|
||||
continue
|
||||
prog, ver = verstr.split(' ', 1)
|
||||
if not prog or not ver:
|
||||
continue
|
||||
# Check for any version conflicts
|
||||
if versions[isbinutils] and versions[isbinutils] != ver:
|
||||
logging.debug("Mixed version %s vs %s" % (
|
||||
repr(versions[isbinutils]), repr(ver)))
|
||||
versions[isbinutils] = "mixed"
|
||||
continue
|
||||
versions[isbinutils] = ver
|
||||
success += 1
|
||||
cleanbuild = versions[0] and versions[1] and success == len(tools)
|
||||
return cleanbuild, "gcc: %s binutils: %s" % (versions[0], versions[1])
|
||||
|
||||
# Add version information to the data dictionary
|
||||
class HandleVersions:
|
||||
def __init__(self):
|
||||
self.ctr_dispatch = {}
|
||||
self.toolstr = self.version = ""
|
||||
def update_data_dictionary(self, data):
|
||||
data['version'] = self.version
|
||||
data['build_versions'] = self.toolstr
|
||||
def generate_code(self, options):
|
||||
cleanbuild, self.toolstr = tool_versions(options.tools)
|
||||
self.version = build_version(options.extra, cleanbuild)
|
||||
sys.stdout.write("Version: %s\n" % (self.version,))
|
||||
return "\n// version: %s\n// build_versions: %s\n" % (
|
||||
self.version, self.toolstr)
|
||||
|
||||
Handlers.append(HandleVersions())
|
||||
|
||||
|
||||
######################################################################
|
||||
# Identify data dictionary generation
|
||||
######################################################################
|
||||
|
||||
# Automatically generate the wire protocol data dictionary
|
||||
class HandleIdentify:
|
||||
def __init__(self):
|
||||
self.ctr_dispatch = {}
|
||||
def update_data_dictionary(self, data):
|
||||
pass
|
||||
def generate_code(self, options):
|
||||
# Generate data dictionary
|
||||
data = {}
|
||||
for h in Handlers:
|
||||
h.update_data_dictionary(data)
|
||||
datadict = json.dumps(data, separators=(',', ':'), sort_keys=True)
|
||||
|
||||
# Write data dictionary
|
||||
if options.write_dictionary:
|
||||
f = open(options.write_dictionary, 'w')
|
||||
f.write(datadict)
|
||||
f.close()
|
||||
|
||||
# Format compressed info into C code
|
||||
zdatadict = bytearray(zlib.compress(datadict.encode(), 9))
|
||||
out = []
|
||||
for i in range(len(zdatadict)):
|
||||
if i % 8 == 0:
|
||||
out.append('\n ')
|
||||
out.append(" 0x%02x," % (zdatadict[i],))
|
||||
fmt = """
|
||||
const uint8_t command_identify_data[] PROGMEM = {%s
|
||||
};
|
||||
|
||||
// Identify size = %d (%d uncompressed)
|
||||
const uint32_t command_identify_size PROGMEM
|
||||
= ARRAY_SIZE(command_identify_data);
|
||||
"""
|
||||
return fmt % (''.join(out), len(zdatadict), len(datadict))
|
||||
|
||||
Handlers.append(HandleIdentify())
|
||||
|
||||
|
||||
######################################################################
|
||||
# Main code
|
||||
######################################################################
|
||||
|
||||
def main():
|
||||
usage = "%prog [options] <cmd section file> <output.c>"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-e", "--extra", dest="extra", default="",
|
||||
help="extra version string to append to version")
|
||||
opts.add_option("-d", dest="write_dictionary",
|
||||
help="file to write mcu protocol dictionary")
|
||||
opts.add_option("-t", "--tools", dest="tools", default="",
|
||||
help="list of build programs to extract version from")
|
||||
opts.add_option("-v", action="store_true", dest="verbose",
|
||||
help="enable debug messages")
|
||||
|
||||
options, args = opts.parse_args()
|
||||
if len(args) != 2:
|
||||
opts.error("Incorrect arguments")
|
||||
incmdfile, outcfile = args
|
||||
if options.verbose:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# Parse request file
|
||||
ctr_dispatch = { k: v for h in Handlers for k, v in h.ctr_dispatch.items() }
|
||||
f = open(incmdfile, 'r')
|
||||
data = f.read()
|
||||
f.close()
|
||||
for req in data.split('\n'):
|
||||
req = req.lstrip()
|
||||
if not req:
|
||||
continue
|
||||
cmd = req.split()[0]
|
||||
if cmd not in ctr_dispatch:
|
||||
error("Unknown build time command '%s'" % cmd)
|
||||
ctr_dispatch[cmd](req)
|
||||
|
||||
# Write output
|
||||
code = "".join([FILEHEADER] + [h.generate_code(options) for h in Handlers])
|
||||
f = open(outcfile, 'w')
|
||||
f.write(code)
|
||||
f.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
174
scripts/calibrate_shaper.py
Normal file
174
scripts/calibrate_shaper.py
Normal file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
# Shaper auto-calibration script
|
||||
#
|
||||
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
from __future__ import print_function
|
||||
import importlib, optparse, os, sys
|
||||
from textwrap import wrap
|
||||
import numpy as np, matplotlib
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
'..', 'klippy'))
|
||||
shaper_calibrate = importlib.import_module('.shaper_calibrate', 'extras')
|
||||
|
||||
MAX_TITLE_LENGTH=65
|
||||
|
||||
def parse_log(logname):
|
||||
with open(logname) as f:
|
||||
for header in f:
|
||||
if not header.startswith('#'):
|
||||
break
|
||||
if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
|
||||
# Raw accelerometer data
|
||||
return np.loadtxt(logname, comments='#', delimiter=',')
|
||||
# Parse power spectral density data
|
||||
data = np.loadtxt(logname, skiprows=1, comments='#', delimiter=',')
|
||||
calibration_data = shaper_calibrate.CalibrationData(
|
||||
freq_bins=data[:,0], psd_sum=data[:,4],
|
||||
psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3])
|
||||
calibration_data.set_numpy(np)
|
||||
# If input shapers are present in the CSV file, the frequency
|
||||
# response is already normalized to input frequencies
|
||||
if 'mzv' not in header:
|
||||
calibration_data.normalize_to_frequencies()
|
||||
return calibration_data
|
||||
|
||||
######################################################################
|
||||
# Shaper calibration
|
||||
######################################################################
|
||||
|
||||
# Find the best shaper parameters
|
||||
def calibrate_shaper(datas, csv_output, max_smoothing):
|
||||
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
||||
if isinstance(datas[0], shaper_calibrate.CalibrationData):
|
||||
calibration_data = datas[0]
|
||||
for data in datas[1:]:
|
||||
calibration_data.add_data(data)
|
||||
else:
|
||||
# Process accelerometer data
|
||||
calibration_data = helper.process_accelerometer_data(datas[0])
|
||||
for data in datas[1:]:
|
||||
calibration_data.add_data(helper.process_accelerometer_data(data))
|
||||
calibration_data.normalize_to_frequencies()
|
||||
shaper, all_shapers = helper.find_best_shaper(
|
||||
calibration_data, max_smoothing, print)
|
||||
print("Recommended shaper is %s @ %.1f Hz" % (shaper.name, shaper.freq))
|
||||
if csv_output is not None:
|
||||
helper.save_calibration_data(
|
||||
csv_output, calibration_data, all_shapers)
|
||||
return shaper.name, all_shapers, calibration_data
|
||||
|
||||
######################################################################
|
||||
# Plot frequency response and suggested input shapers
|
||||
######################################################################
|
||||
|
||||
def plot_freq_response(lognames, calibration_data, shapers,
|
||||
selected_shaper, max_freq):
|
||||
freqs = calibration_data.freq_bins
|
||||
psd = calibration_data.psd_sum[freqs <= max_freq]
|
||||
px = calibration_data.psd_x[freqs <= max_freq]
|
||||
py = calibration_data.psd_y[freqs <= max_freq]
|
||||
pz = calibration_data.psd_z[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
|
||||
fig, ax = matplotlib.pyplot.subplots()
|
||||
ax.set_xlabel('Frequency, Hz')
|
||||
ax.set_xlim([0, max_freq])
|
||||
ax.set_ylabel('Power spectral density')
|
||||
|
||||
ax.plot(freqs, psd, label='X+Y+Z', color='purple')
|
||||
ax.plot(freqs, px, label='X', color='red')
|
||||
ax.plot(freqs, py, label='Y', color='green')
|
||||
ax.plot(freqs, pz, label='Z', color='blue')
|
||||
|
||||
title = "Frequency response and shapers (%s)" % (', '.join(lognames))
|
||||
ax.set_title("\n".join(wrap(title, MAX_TITLE_LENGTH)))
|
||||
ax.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(5))
|
||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0,0))
|
||||
ax.grid(which='major', color='grey')
|
||||
ax.grid(which='minor', color='lightgrey')
|
||||
|
||||
ax2 = ax.twinx()
|
||||
ax2.set_ylabel('Shaper vibration reduction (ratio)')
|
||||
best_shaper_vals = None
|
||||
for shaper in shapers:
|
||||
label = "%s (%.1f Hz, vibr=%.1f%%, sm~=%.2f, accel<=%.f)" % (
|
||||
shaper.name.upper(), shaper.freq,
|
||||
shaper.vibrs * 100., shaper.smoothing,
|
||||
round(shaper.max_accel / 100.) * 100.)
|
||||
linestyle = 'dotted'
|
||||
if shaper.name == selected_shaper:
|
||||
linestyle = 'dashdot'
|
||||
best_shaper_vals = shaper.vals
|
||||
ax2.plot(freqs, shaper.vals, label=label, linestyle=linestyle)
|
||||
ax.plot(freqs, psd * best_shaper_vals,
|
||||
label='After\nshaper', color='cyan')
|
||||
# A hack to add a human-readable shaper recommendation to legend
|
||||
ax2.plot([], [], ' ',
|
||||
label="Recommended shaper: %s" % (selected_shaper.upper()))
|
||||
|
||||
ax.legend(loc='upper left', prop=fontP)
|
||||
ax2.legend(loc='upper right', prop=fontP)
|
||||
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
######################################################################
|
||||
# Startup
|
||||
######################################################################
|
||||
|
||||
def setup_matplotlib(output_to_file):
|
||||
global matplotlib
|
||||
if output_to_file:
|
||||
matplotlib.rcParams.update({'figure.autolayout': True})
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||
import matplotlib.ticker
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
usage = "%prog [options] <logs>"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-o", "--output", type="string", dest="output",
|
||||
default=None, help="filename of output graph")
|
||||
opts.add_option("-c", "--csv", type="string", dest="csv",
|
||||
default=None, help="filename of output csv file")
|
||||
opts.add_option("-f", "--max_freq", type="float", default=200.,
|
||||
help="maximum frequency to graph")
|
||||
opts.add_option("-s", "--max_smoothing", type="float", default=None,
|
||||
help="maximum shaper smoothing to allow")
|
||||
options, args = opts.parse_args()
|
||||
if len(args) < 1:
|
||||
opts.error("Incorrect number of arguments")
|
||||
if options.max_smoothing is not None and options.max_smoothing < 0.05:
|
||||
opts.error("Too small max_smoothing specified (must be at least 0.05)")
|
||||
|
||||
# Parse data
|
||||
datas = [parse_log(fn) for fn in args]
|
||||
|
||||
# Calibrate shaper and generate outputs
|
||||
selected_shaper, shapers, calibration_data = calibrate_shaper(
|
||||
datas, options.csv, options.max_smoothing)
|
||||
|
||||
if not options.csv or options.output:
|
||||
# Draw graph
|
||||
setup_matplotlib(options.output is not None)
|
||||
|
||||
fig = plot_freq_response(args, calibration_data, shapers,
|
||||
selected_shaper, options.max_freq)
|
||||
|
||||
# Show graph
|
||||
if options.output is None:
|
||||
matplotlib.pyplot.show()
|
||||
else:
|
||||
fig.set_size_inches(8, 6)
|
||||
fig.savefig(options.output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
64
scripts/canbus_query.py
Normal file
64
scripts/canbus_query.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python2
|
||||
# Tool to query CAN bus uuids
|
||||
#
|
||||
# Copyright (C) 2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import sys, os, optparse, time
|
||||
import can
|
||||
|
||||
CANBUS_ID_ADMIN = 0x3f0
|
||||
CMD_QUERY_UNASSIGNED = 0x00
|
||||
RESP_NEED_NODEID = 0x20
|
||||
CMD_SET_KLIPPER_NODEID = 0x01
|
||||
CMD_SET_CANBOOT_NODEID = 0x11
|
||||
|
||||
def query_unassigned(canbus_iface):
|
||||
# Open CAN socket
|
||||
filters = [{"can_id": CANBUS_ID_ADMIN + 1, "can_mask": 0x7ff,
|
||||
"extended": False}]
|
||||
bus = can.interface.Bus(channel=canbus_iface, can_filters=filters,
|
||||
bustype='socketcan')
|
||||
# Send query
|
||||
msg = can.Message(arbitration_id=CANBUS_ID_ADMIN,
|
||||
data=[CMD_QUERY_UNASSIGNED], is_extended_id=False)
|
||||
bus.send(msg)
|
||||
# Read responses
|
||||
found_ids = {}
|
||||
start_time = curtime = time.time()
|
||||
while 1:
|
||||
tdiff = start_time + 2. - curtime
|
||||
if tdiff <= 0.:
|
||||
break
|
||||
msg = bus.recv(tdiff)
|
||||
curtime = time.time()
|
||||
if (msg is None or msg.arbitration_id != CANBUS_ID_ADMIN + 1
|
||||
or msg.dlc < 7 or msg.data[0] != RESP_NEED_NODEID):
|
||||
continue
|
||||
uuid = sum([v << ((5-i)*8) for i, v in enumerate(msg.data[1:7])])
|
||||
if uuid in found_ids:
|
||||
continue
|
||||
found_ids[uuid] = 1
|
||||
AppNames = {
|
||||
CMD_SET_KLIPPER_NODEID: "Klipper",
|
||||
CMD_SET_CANBOOT_NODEID: "CanBoot"
|
||||
}
|
||||
app_id = CMD_SET_KLIPPER_NODEID
|
||||
if msg.dlc > 7:
|
||||
app_id = msg.data[7]
|
||||
app_name = AppNames.get(app_id, "Unknown")
|
||||
sys.stdout.write("Found canbus_uuid=%012x, Application: %s\n"
|
||||
% (uuid, app_name))
|
||||
sys.stdout.write("Total %d uuids found\n" % (len(found_ids,)))
|
||||
|
||||
def main():
|
||||
usage = "%prog [options] <can interface>"
|
||||
opts = optparse.OptionParser(usage)
|
||||
options, args = opts.parse_args()
|
||||
if len(args) != 1:
|
||||
opts.error("Incorrect number of arguments")
|
||||
canbus_iface = args[0]
|
||||
query_unassigned(canbus_iface)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
18
scripts/check-gcc.sh
Normal file
18
scripts/check-gcc.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# This script checks for a broken Ubuntu 18.04 arm-none-eabi-gcc compile
|
||||
|
||||
f1="$1"
|
||||
f2="$2"
|
||||
|
||||
s1=`readelf -A "$f1" | grep "Tag_ARM_ISA_use"`
|
||||
s2=`readelf -A "$f2" | grep "Tag_ARM_ISA_use"`
|
||||
|
||||
if [ "$s1" != "$s2" ]; then
|
||||
echo ""
|
||||
echo "ERROR: The compiler failed to correctly compile Klipper"
|
||||
echo "It will be necessary to upgrade the compiler"
|
||||
echo "See: https://bugs.launchpad.net/ubuntu/+source/newlib/+bug/1767223"
|
||||
echo ""
|
||||
rm -f "$f1"
|
||||
exit 99
|
||||
fi
|
||||
70
scripts/check_whitespace.py
Normal file
70
scripts/check_whitespace.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
# Check files for whitespace problems
|
||||
#
|
||||
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import sys, os.path, unicodedata
|
||||
|
||||
HaveError = False
|
||||
|
||||
def report_error(filename, lineno, msg):
|
||||
global HaveError
|
||||
if not HaveError:
|
||||
sys.stderr.write("\n\nERROR:\nERROR: White space errors\nERROR:\n")
|
||||
HaveError = True
|
||||
sys.stderr.write("%s:%d: %s\n" % (filename, lineno + 1, msg))
|
||||
|
||||
def check_file(filename):
|
||||
# Open and read file
|
||||
try:
|
||||
f = open(filename, 'rb')
|
||||
data = f.read()
|
||||
f.close()
|
||||
except IOError:
|
||||
return
|
||||
if not data:
|
||||
# Empty files are okay
|
||||
return
|
||||
# Do checks
|
||||
is_source_code = any([filename.endswith(s) for s in ['.c', '.h', '.py']])
|
||||
lineno = 0
|
||||
for lineno, line in enumerate(data.split(b'\n')):
|
||||
# Verify line is valid utf-8
|
||||
try:
|
||||
line = line.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
report_error(filename, lineno, "Found non utf-8 character")
|
||||
continue
|
||||
# Check for control characters
|
||||
for c in line:
|
||||
if unicodedata.category(c).startswith('C'):
|
||||
char_name = repr(c)
|
||||
if c == '\t':
|
||||
if os.path.basename(filename).lower() == 'makefile':
|
||||
continue
|
||||
char_name = 'tab'
|
||||
report_error(filename, lineno, "Invalid %s character" % (
|
||||
char_name,))
|
||||
break
|
||||
# Check for trailing space
|
||||
if line.endswith(' ') or line.endswith('\t'):
|
||||
report_error(filename, lineno, "Line has trailing spaces")
|
||||
# Check for more than 80 characters
|
||||
if is_source_code and len(line) > 80:
|
||||
report_error(filename, lineno, "Line longer than 80 characters")
|
||||
if not data.endswith(b'\n'):
|
||||
report_error(filename, lineno, "No newline at end of file")
|
||||
if data.endswith(b'\n\n'):
|
||||
report_error(filename, lineno, "Extra newlines at end of file")
|
||||
|
||||
def main():
|
||||
files = sys.argv[1:]
|
||||
for filename in files:
|
||||
check_file(filename)
|
||||
if HaveError:
|
||||
sys.stderr.write("\n\n")
|
||||
sys.exit(-1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
18
scripts/check_whitespace.sh
Normal file
18
scripts/check_whitespace.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# Script to check whitespace in Klipper source code.
|
||||
|
||||
# Find SRCDIR from the pathname of this script
|
||||
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
cd ${SRCDIR}
|
||||
|
||||
# Run whitespace tool on all source files
|
||||
WS_DIRS="config/ docs/ klippy/ scripts/ src/ test/"
|
||||
WS_EXCLUDE="-path scripts/kconfig -prune"
|
||||
WS_FILES="-o -iname '*.[csh]' -o -name '*.py' -o -name '*.sh'"
|
||||
WS_FILES="$WS_FILES -o -name '*.md' -o -name '*.cfg' -o -name '*.txt'"
|
||||
WS_FILES="$WS_FILES -o -name '*.html' -o -name '*.css'"
|
||||
WS_FILES="$WS_FILES -o -name '*.yaml' -o -name '*.yml'"
|
||||
WS_FILES="$WS_FILES -o -name '*.css' -o -name '*.yaml' -o -name '*.yml'"
|
||||
WS_FILES="$WS_FILES -o -name '*.test' -o -name '*.config'"
|
||||
WS_FILES="$WS_FILES -o -iname '*.lds' -o -iname 'Makefile' -o -iname 'Kconfig'"
|
||||
eval find $WS_DIRS $WS_EXCLUDE $WS_FILES | xargs ./scripts/check_whitespace.py
|
||||
243
scripts/checkstack.py
Normal file
243
scripts/checkstack.py
Normal file
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python2
|
||||
# Script that tries to find how much stack space each function in an
|
||||
# object is using.
|
||||
#
|
||||
# Copyright (C) 2015 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
# Usage:
|
||||
# avr-objdump -d out/klipper.elf | scripts/checkstack.py
|
||||
|
||||
import sys
|
||||
import re
|
||||
|
||||
# Functions that change stacks
|
||||
STACKHOP = []
|
||||
# List of functions we can assume are never called.
|
||||
IGNORE = []
|
||||
|
||||
OUTPUTDESC = """
|
||||
#funcname1[preamble_stack_usage,max_usage_with_callers]:
|
||||
# insn_addr:called_function [usage_at_call_point+caller_preamble,total_usage]
|
||||
#
|
||||
#funcname2[p,m,max_usage_to_yield_point]:
|
||||
# insn_addr:called_function [u+c,t,usage_to_yield_point]
|
||||
"""
|
||||
|
||||
class function:
|
||||
def __init__(self, funcaddr, funcname):
|
||||
self.funcaddr = funcaddr
|
||||
self.funcname = funcname
|
||||
self.basic_stack_usage = 0
|
||||
self.max_stack_usage = None
|
||||
self.yield_usage = -1
|
||||
self.max_yield_usage = None
|
||||
self.total_calls = 0
|
||||
# called_funcs = [(insnaddr, calladdr, stackusage), ...]
|
||||
self.called_funcs = []
|
||||
self.subfuncs = {}
|
||||
# Update function info with a found "yield" point.
|
||||
def noteYield(self, stackusage):
|
||||
if self.yield_usage < stackusage:
|
||||
self.yield_usage = stackusage
|
||||
# Update function info with a found "call" point.
|
||||
def noteCall(self, insnaddr, calladdr, stackusage):
|
||||
if (calladdr, stackusage) in self.subfuncs:
|
||||
# Already noted a nearly identical call - ignore this one.
|
||||
return
|
||||
self.called_funcs.append((insnaddr, calladdr, stackusage))
|
||||
self.subfuncs[(calladdr, stackusage)] = 1
|
||||
|
||||
# Find out maximum stack usage for a function
|
||||
def calcmaxstack(info, funcs):
|
||||
if info.max_stack_usage is not None:
|
||||
return
|
||||
info.max_stack_usage = max_stack_usage = info.basic_stack_usage
|
||||
info.max_yield_usage = max_yield_usage = info.yield_usage
|
||||
total_calls = 0
|
||||
seenbefore = {}
|
||||
# Find max of all nested calls.
|
||||
for insnaddr, calladdr, usage in info.called_funcs:
|
||||
callinfo = funcs.get(calladdr)
|
||||
if callinfo is None:
|
||||
continue
|
||||
calcmaxstack(callinfo, funcs)
|
||||
if callinfo.funcname not in seenbefore:
|
||||
seenbefore[callinfo.funcname] = 1
|
||||
total_calls += callinfo.total_calls + 1
|
||||
funcnameroot = callinfo.funcname.split('.')[0]
|
||||
if funcnameroot in IGNORE:
|
||||
# This called function is ignored - don't contribute it to
|
||||
# the max stack.
|
||||
continue
|
||||
totusage = usage + callinfo.max_stack_usage
|
||||
totyieldusage = usage + callinfo.max_yield_usage
|
||||
if funcnameroot in STACKHOP:
|
||||
# Don't count children of this function
|
||||
totusage = totyieldusage = usage
|
||||
if totusage > max_stack_usage:
|
||||
max_stack_usage = totusage
|
||||
if callinfo.max_yield_usage >= 0 and totyieldusage > max_yield_usage:
|
||||
max_yield_usage = totyieldusage
|
||||
info.max_stack_usage = max_stack_usage
|
||||
info.max_yield_usage = max_yield_usage
|
||||
info.total_calls = total_calls
|
||||
|
||||
# Try to arrange output so that functions that call each other are
|
||||
# near each other.
|
||||
def orderfuncs(funcaddrs, availfuncs):
|
||||
l = [(availfuncs[funcaddr].total_calls
|
||||
, availfuncs[funcaddr].funcname, funcaddr)
|
||||
for funcaddr in funcaddrs if funcaddr in availfuncs]
|
||||
l.sort()
|
||||
l.reverse()
|
||||
out = []
|
||||
while l:
|
||||
count, name, funcaddr = l.pop(0)
|
||||
info = availfuncs.get(funcaddr)
|
||||
if info is None:
|
||||
continue
|
||||
calladdrs = [calls[1] for calls in info.called_funcs]
|
||||
del availfuncs[funcaddr]
|
||||
out = out + orderfuncs(calladdrs, availfuncs) + [info]
|
||||
return out
|
||||
|
||||
hex_s = r'[0-9a-f]+'
|
||||
re_func = re.compile(r'^(?P<funcaddr>' + hex_s + r') <(?P<func>.*)>:$')
|
||||
re_asm = re.compile(
|
||||
r'^[ ]*(?P<insnaddr>' + hex_s
|
||||
+ r'):\t[^\t]*\t(?P<insn>[^\t]+?)(?P<params>\t[^;]*)?'
|
||||
+ r'[ ]*(; (?P<calladdr>0x' + hex_s
|
||||
+ r') <(?P<ref>.*)>)?$')
|
||||
|
||||
def main():
|
||||
unknownfunc = function(None, "<unknown>")
|
||||
indirectfunc = function(-1, '<indirect>')
|
||||
unknownfunc.max_stack_usage = indirectfunc.max_stack_usage = 0
|
||||
unknownfunc.max_yield_usage = indirectfunc.max_yield_usage = -1
|
||||
funcs = {-1: indirectfunc}
|
||||
funcaddr = None
|
||||
datalines = {}
|
||||
cur = None
|
||||
atstart = 0
|
||||
stackusage = 0
|
||||
|
||||
# Parse input lines
|
||||
for line in sys.stdin.readlines():
|
||||
m = re_func.match(line)
|
||||
if m is not None:
|
||||
# Found function
|
||||
funcaddr = int(m.group('funcaddr'), 16)
|
||||
funcs[funcaddr] = cur = function(funcaddr, m.group('func'))
|
||||
stackusage = 0
|
||||
atstart = 1
|
||||
continue
|
||||
m = re_asm.match(line)
|
||||
if m is None:
|
||||
datalines.setdefault(funcaddr, []).append(line)
|
||||
#print("other", repr(line))
|
||||
continue
|
||||
insn = m.group('insn')
|
||||
|
||||
if insn == 'push':
|
||||
stackusage += 1
|
||||
continue
|
||||
if insn == 'rcall' and m.group('params').strip() == '.+0':
|
||||
stackusage += 2
|
||||
continue
|
||||
|
||||
if atstart:
|
||||
if insn in ['in', 'eor']:
|
||||
continue
|
||||
cur.basic_stack_usage = stackusage
|
||||
atstart = 0
|
||||
|
||||
insnaddr = m.group('insnaddr')
|
||||
calladdr = m.group('calladdr')
|
||||
if calladdr is None:
|
||||
if insn == 'ijmp':
|
||||
# Indirect tail call
|
||||
cur.noteCall(insnaddr, -1, 0)
|
||||
elif insn == 'icall':
|
||||
cur.noteCall(insnaddr, -1, stackusage + 2)
|
||||
else:
|
||||
# misc instruction
|
||||
continue
|
||||
else:
|
||||
# Jump or call insn
|
||||
calladdr = int(calladdr, 16)
|
||||
ref = m.group('ref')
|
||||
if '+' in ref:
|
||||
# Inter-function jump.
|
||||
continue
|
||||
elif insn.startswith('ld') or insn.startswith('st'):
|
||||
# memory access
|
||||
continue
|
||||
elif insn in ('rjmp', 'jmp', 'brne', 'brcs'):
|
||||
# Tail call
|
||||
cur.noteCall(insnaddr, calladdr, 0)
|
||||
elif insn in ('rcall', 'call'):
|
||||
cur.noteCall(insnaddr, calladdr, stackusage + 2)
|
||||
else:
|
||||
print("unknown call", ref)
|
||||
cur.noteCall(insnaddr, calladdr, stackusage)
|
||||
# Reset stack usage to preamble usage
|
||||
stackusage = cur.basic_stack_usage
|
||||
|
||||
# Update for known indirect functions
|
||||
funcsbyname = {}
|
||||
for info in funcs.values():
|
||||
funcnameroot = info.funcname.split('.')[0]
|
||||
funcsbyname[funcnameroot] = info
|
||||
cmdfunc = funcsbyname.get('sched_main')
|
||||
command_index = funcsbyname.get('command_index')
|
||||
if command_index is not None and cmdfunc is not None:
|
||||
for line in datalines[command_index.funcaddr]:
|
||||
parts = line.split()
|
||||
if len(parts) < 9:
|
||||
continue
|
||||
calladdr = int(parts[8]+parts[7], 16) * 2
|
||||
numparams = int(parts[2], 16)
|
||||
stackusage = cmdfunc.basic_stack_usage + 2 + numparams * 4
|
||||
cmdfunc.noteCall(0, calladdr, stackusage)
|
||||
if len(parts) < 17:
|
||||
continue
|
||||
calladdr = int(parts[16]+parts[15], 16) * 2
|
||||
numparams = int(parts[10], 16)
|
||||
stackusage = cmdfunc.basic_stack_usage + 2 + numparams * 4
|
||||
cmdfunc.noteCall(0, calladdr, stackusage)
|
||||
eventfunc = funcsbyname.get('__vector_13', funcsbyname.get('__vector_17'))
|
||||
for funcnameroot, info in funcsbyname.items():
|
||||
if funcnameroot.endswith('_event') and eventfunc is not None:
|
||||
eventfunc.noteCall(0, info.funcaddr, eventfunc.basic_stack_usage+2)
|
||||
|
||||
# Calculate maxstackusage
|
||||
for info in funcs.values():
|
||||
calcmaxstack(info, funcs)
|
||||
|
||||
# Sort functions for output
|
||||
funcinfos = orderfuncs(funcs.keys(), funcs.copy())
|
||||
|
||||
# Show all functions
|
||||
print(OUTPUTDESC)
|
||||
for info in funcinfos:
|
||||
if info.max_stack_usage == 0 and info.max_yield_usage < 0:
|
||||
continue
|
||||
yieldstr = ""
|
||||
if info.max_yield_usage >= 0:
|
||||
yieldstr = ",%d" % info.max_yield_usage
|
||||
print("\n%s[%d,%d%s]:" % (info.funcname, info.basic_stack_usage
|
||||
, info.max_stack_usage, yieldstr))
|
||||
for insnaddr, calladdr, stackusage in info.called_funcs:
|
||||
callinfo = funcs.get(calladdr, unknownfunc)
|
||||
yieldstr = ""
|
||||
if callinfo.max_yield_usage >= 0:
|
||||
yieldstr = ",%d" % (stackusage + callinfo.max_yield_usage)
|
||||
print(" %04s:%-40s [%d+%d,%d%s]" % (
|
||||
insnaddr, callinfo.funcname, stackusage
|
||||
, callinfo.basic_stack_usage
|
||||
, stackusage+callinfo.max_stack_usage, yieldstr))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
81
scripts/ci-build.sh
Normal file
81
scripts/ci-build.sh
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
# Test script for continuous integration.
|
||||
|
||||
# Stop script early on any error; check variables
|
||||
set -eu
|
||||
|
||||
# Paths to tools installed by ci-install.sh
|
||||
MAIN_DIR=${PWD}
|
||||
BUILD_DIR=${PWD}/ci_build
|
||||
export PATH=${BUILD_DIR}/pru-gcc/bin:${PATH}
|
||||
PYTHON=${BUILD_DIR}/python-env/bin/python
|
||||
PYTHON2=${BUILD_DIR}/python2-env/bin/python
|
||||
|
||||
|
||||
######################################################################
|
||||
# Section grouping output message helpers
|
||||
######################################################################
|
||||
|
||||
start_test()
|
||||
{
|
||||
echo "::group::=============== $1 $2"
|
||||
set -x
|
||||
}
|
||||
|
||||
finish_test()
|
||||
{
|
||||
set +x
|
||||
echo "=============== Finished $2"
|
||||
echo "::endgroup::"
|
||||
}
|
||||
|
||||
|
||||
######################################################################
|
||||
# Check for whitespace errors
|
||||
######################################################################
|
||||
|
||||
start_test check_whitespace "Check whitespace"
|
||||
./scripts/check_whitespace.sh
|
||||
finish_test check_whitespace "Check whitespace"
|
||||
|
||||
|
||||
######################################################################
|
||||
# Run compile tests for several different MCU types
|
||||
######################################################################
|
||||
|
||||
DICTDIR=${BUILD_DIR}/dict
|
||||
mkdir -p ${DICTDIR}
|
||||
|
||||
for TARGET in test/configs/*.config ; do
|
||||
start_test mcu_compile "$TARGET"
|
||||
make clean
|
||||
make distclean
|
||||
unset CC
|
||||
cp ${TARGET} .config
|
||||
make olddefconfig
|
||||
make V=1
|
||||
size out/*.elf
|
||||
finish_test mcu_compile "$TARGET"
|
||||
cp out/klipper.dict ${DICTDIR}/$(basename ${TARGET} .config).dict
|
||||
done
|
||||
|
||||
|
||||
######################################################################
|
||||
# Verify klippy host software
|
||||
######################################################################
|
||||
|
||||
start_test klippy "Test klippy import (Python3)"
|
||||
$PYTHON klippy/klippy.py --import-test
|
||||
finish_test klippy "Test klippy import (Python3)"
|
||||
|
||||
start_test klippy "Test klippy import (Python2)"
|
||||
$PYTHON2 klippy/klippy.py --import-test
|
||||
finish_test klippy "Test klippy import (Python2)"
|
||||
|
||||
start_test klippy "Test invoke klippy (Python3)"
|
||||
$PYTHON scripts/test_klippy.py -d ${DICTDIR} test/klippy/*.test
|
||||
finish_test klippy "Test invoke klippy (Python3)"
|
||||
|
||||
start_test klippy "Test invoke klippy (Python2)"
|
||||
$PYTHON2 scripts/test_klippy.py -d ${DICTDIR} test/klippy/*.test
|
||||
finish_test klippy "Test invoke klippy (Python2)"
|
||||
68
scripts/ci-install.sh
Normal file
68
scripts/ci-install.sh
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# Build setup script for continuous integration testing.
|
||||
# See ci-build.sh for the actual test steps.
|
||||
|
||||
# Stop script early on any error; check variables; be verbose
|
||||
set -eux
|
||||
|
||||
MAIN_DIR=${PWD}
|
||||
BUILD_DIR=${PWD}/ci_build
|
||||
CACHE_DIR=${PWD}/ci_cache
|
||||
mkdir -p ${BUILD_DIR} ${CACHE_DIR}
|
||||
|
||||
|
||||
######################################################################
|
||||
# Install system dependencies
|
||||
######################################################################
|
||||
|
||||
echo -e "\n\n=============== Install system dependencies\n\n"
|
||||
PKGS="virtualenv python-dev libffi-dev build-essential"
|
||||
PKGS="${PKGS} gcc-avr avr-libc"
|
||||
PKGS="${PKGS} libnewlib-arm-none-eabi gcc-arm-none-eabi binutils-arm-none-eabi"
|
||||
PKGS="${PKGS} pv libmpfr-dev libgmp-dev libmpc-dev texinfo bison flex"
|
||||
sudo apt-get install ${PKGS}
|
||||
|
||||
|
||||
######################################################################
|
||||
# Install (or build) pru gcc
|
||||
######################################################################
|
||||
|
||||
echo -e "\n\n=============== Install embedded pru gcc\n\n"
|
||||
PRU_FILE=${CACHE_DIR}/gnupru.tar.gz
|
||||
PRU_DIR=${BUILD_DIR}/pru-gcc
|
||||
|
||||
if [ ! -f ${PRU_FILE} ]; then
|
||||
cd ${BUILD_DIR}
|
||||
git config --global user.email "you@example.com"
|
||||
git config --global user.name "Your Name"
|
||||
git clone https://github.com/dinuxbg/gnupru -b 2018.03-beta-rc3 --depth 1
|
||||
cd gnupru
|
||||
export PREFIX=${PRU_DIR}
|
||||
./download-and-patch.sh 2>&1 | pv -nli 30 > ${BUILD_DIR}/gnupru-build.log
|
||||
./build.sh 2>&1 | pv -nli 30 >> ${BUILD_DIR}/gnupru-build.log
|
||||
cd ${BUILD_DIR}
|
||||
tar cfz ${PRU_FILE} pru-gcc/
|
||||
else
|
||||
cd ${BUILD_DIR}
|
||||
tar xfz ${PRU_FILE}
|
||||
fi
|
||||
|
||||
|
||||
######################################################################
|
||||
# Create python3 virtualenv environment
|
||||
######################################################################
|
||||
|
||||
echo -e "\n\n=============== Install python3 virtualenv\n\n"
|
||||
cd ${MAIN_DIR}
|
||||
virtualenv -p python3 ${BUILD_DIR}/python-env
|
||||
${BUILD_DIR}/python-env/bin/pip install -r ${MAIN_DIR}/scripts/klippy-requirements.txt
|
||||
|
||||
|
||||
######################################################################
|
||||
# Create python2 virtualenv environment
|
||||
######################################################################
|
||||
|
||||
echo -e "\n\n=============== Install python2 virtualenv\n\n"
|
||||
cd ${MAIN_DIR}
|
||||
virtualenv -p python2 ${BUILD_DIR}/python2-env
|
||||
${BUILD_DIR}/python2-env/bin/pip install -r ${MAIN_DIR}/scripts/klippy-requirements.txt
|
||||
26
scripts/flash-linux.sh
Normal file
26
scripts/flash-linux.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# This script installs the Linux MCU code to /usr/local/bin/
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "This script must be run as root"
|
||||
exit -1
|
||||
fi
|
||||
set -e
|
||||
|
||||
# Install new micro-controller code
|
||||
echo "Installing micro-controller code to /usr/local/bin/"
|
||||
rm -f /usr/local/bin/klipper_mcu
|
||||
cp out/klipper.elf /usr/local/bin/klipper_mcu
|
||||
sync
|
||||
|
||||
# Restart (if system install script present)
|
||||
if [ -f /etc/init.d/klipper_pru ]; then
|
||||
echo "Attempting host PRU restart..."
|
||||
service klipper_pru restart
|
||||
fi
|
||||
|
||||
# Restart (if system install script present)
|
||||
if [ -f /etc/init.d/klipper_mcu ]; then
|
||||
echo "Attempting host MCU restart..."
|
||||
service klipper_mcu restart
|
||||
fi
|
||||
20
scripts/flash-pru.sh
Normal file
20
scripts/flash-pru.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# This script installs the PRU firmware on a beaglebone machine.
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "This script must be run as root"
|
||||
exit -1
|
||||
fi
|
||||
set -e
|
||||
|
||||
# Install new firmware
|
||||
echo "Installing firmware to /lib/firmware/"
|
||||
cp out/pru0.elf /lib/firmware/am335x-pru0-fw
|
||||
cp out/pru1.elf /lib/firmware/am335x-pru1-fw
|
||||
sync
|
||||
|
||||
# Restart (if system install script present)
|
||||
if [ -f /etc/init.d/klipper_pru ]; then
|
||||
echo "Attempting PRU restart..."
|
||||
service klipper_pru restart
|
||||
fi
|
||||
85
scripts/flash-sdcard.sh
Normal file
85
scripts/flash-sdcard.sh
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# This script launches flash_sdcard.py, a utitlity that enables
|
||||
# unattended firmware updates on boards with "SD Card" bootloaders
|
||||
|
||||
# Non-standard installations may need to change this location
|
||||
KLIPPY_ENV="${HOME}/klippy-env/bin/python"
|
||||
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
KLIPPER_BIN="${SRCDIR}/out/klipper.bin"
|
||||
KLIPPER_BIN_DEFAULT=$KLIPPER_BIN
|
||||
KLIPPER_DICT_DEFAULT="${SRCDIR}/out/klipper.dict"
|
||||
SPI_FLASH="${SRCDIR}/scripts/spi_flash/spi_flash.py"
|
||||
BAUD_ARG=""
|
||||
# Force script to exit if an error occurs
|
||||
set -e
|
||||
|
||||
print_help_message()
|
||||
{
|
||||
echo "SD Card upload utility for Klipper"
|
||||
echo
|
||||
echo "usage: flash_sdcard.sh [-h] [-l] [-b <baud>] [-f <firmware>] [-d <dictionary>]"
|
||||
echo " <device> <board>"
|
||||
echo
|
||||
echo "positional arguments:"
|
||||
echo " <device> device serial port"
|
||||
echo " <board> board type"
|
||||
echo
|
||||
echo "optional arguments:"
|
||||
echo " -h show this message"
|
||||
echo " -l list available boards"
|
||||
echo " -b <baud> serial baud rate (default is 250000)"
|
||||
echo " -f <firmware> path to klipper.bin"
|
||||
echo " -d <dictionary> path to klipper.dict for firmware validation"
|
||||
}
|
||||
|
||||
# Parse command line "optional args"
|
||||
while getopts "hlb:f:d:" arg; do
|
||||
case $arg in
|
||||
h)
|
||||
print_help_message
|
||||
exit 0
|
||||
;;
|
||||
l)
|
||||
${KLIPPY_ENV} ${SPI_FLASH} -l
|
||||
exit 0
|
||||
;;
|
||||
b) BAUD_ARG="-b ${OPTARG}";;
|
||||
f) KLIPPER_BIN=$OPTARG;;
|
||||
d) KLIPPER_DICT=$OPTARG;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Make sure that we have the correct number of positional args
|
||||
if [ $(($# - $OPTIND + 1)) -ne 2 ]; then
|
||||
echo "Invalid number of args: $(($# - $OPTIND + 1))"
|
||||
exit -1
|
||||
fi
|
||||
|
||||
DEVICE=${@:$OPTIND:1}
|
||||
BOARD=${@:$OPTIND+1:1}
|
||||
|
||||
if [ ! -f $KLIPPER_BIN ]; then
|
||||
echo "No file found at '${KLIPPER_BIN}'"
|
||||
exit -1
|
||||
fi
|
||||
|
||||
if [ ! -e $DEVICE ]; then
|
||||
echo "No device found at '${DEVICE}'"
|
||||
exit -1
|
||||
fi
|
||||
|
||||
if [ ! $KLIPPER_DICT ] && [ $KLIPPER_BIN == $KLIPPER_BIN_DEFAULT ] ; then
|
||||
KLIPPER_DICT=$KLIPPER_DICT_DEFAULT
|
||||
fi
|
||||
|
||||
if [ $KLIPPER_DICT ]; then
|
||||
if [ ! -f $KLIPPER_DICT ]; then
|
||||
echo "No file found at '${KLIPPER_BIN}'"
|
||||
exit -1
|
||||
fi
|
||||
KLIPPER_DICT="-d ${KLIPPER_DICT}"
|
||||
fi
|
||||
|
||||
# Run Script
|
||||
echo "Flashing ${KLIPPER_BIN} to ${DEVICE}"
|
||||
${KLIPPY_ENV} ${SPI_FLASH} ${BAUD_ARG} ${KLIPPER_DICT} ${DEVICE} ${BOARD} ${KLIPPER_BIN}
|
||||
380
scripts/flash_usb.py
Normal file
380
scripts/flash_usb.py
Normal file
@@ -0,0 +1,380 @@
|
||||
#!/usr/bin/env python3
|
||||
# Tool to enter a USB bootloader and flash Klipper
|
||||
#
|
||||
# Copyright (C) 2019 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import sys, os, re, subprocess, optparse, time, fcntl, termios, struct
|
||||
|
||||
class error(Exception):
|
||||
pass
|
||||
|
||||
# Attempt to enter bootloader via 1200 baud request
|
||||
def enter_bootloader(device):
|
||||
try:
|
||||
f = open(device, 'rb')
|
||||
fd = f.fileno()
|
||||
fcntl.ioctl(fd, termios.TIOCMBIS, struct.pack('I', termios.TIOCM_DTR))
|
||||
t = termios.tcgetattr(fd)
|
||||
t[4] = t[5] = termios.B1200
|
||||
sys.stderr.write("Entering bootloader on %s\n" % (device,))
|
||||
termios.tcsetattr(fd, termios.TCSANOW, t)
|
||||
fcntl.ioctl(fd, termios.TIOCMBIC, struct.pack('I', termios.TIOCM_DTR))
|
||||
f.close()
|
||||
except (IOError, OSError) as e:
|
||||
pass
|
||||
|
||||
# Translate a serial device name to a stable serial name in /dev/serial/by-path/
|
||||
def translate_serial_to_tty(device):
|
||||
ttyname = os.path.realpath(device)
|
||||
if not os.path.exists('/dev/serial/by-path/'):
|
||||
raise error("Unable to find serial 'by-path' folder")
|
||||
for fname in os.listdir('/dev/serial/by-path/'):
|
||||
fname = '/dev/serial/by-path/' + fname
|
||||
if os.path.realpath(fname) == ttyname:
|
||||
return ttyname, fname
|
||||
return ttyname, ttyname
|
||||
|
||||
# Translate a serial device name to a usb path (suitable for dfu-util)
|
||||
def translate_serial_to_usb_path(device):
|
||||
realdev = os.path.realpath(device)
|
||||
fname = os.path.basename(realdev)
|
||||
try:
|
||||
lname = os.readlink("/sys/class/tty/" + fname)
|
||||
except OSError as e:
|
||||
raise error("Unable to find tty device")
|
||||
ttypath_r = re.compile(r".*/usb\d+.*/(?P<path>\d+-[0-9.]+):\d+\.\d+/.*")
|
||||
m = ttypath_r.match(lname)
|
||||
if m is None:
|
||||
raise error("Unable to find tty usb device")
|
||||
devpath = os.path.realpath("/sys/class/tty/%s/device" % (fname,))
|
||||
return m.group("path"), devpath
|
||||
|
||||
# Wait for a given path to appear
|
||||
def wait_path(path, alt_path=None):
|
||||
time.sleep(.100)
|
||||
start_alt_path = None
|
||||
end_time = time.time() + 4.0
|
||||
while 1:
|
||||
time.sleep(0.100)
|
||||
cur_time = time.time()
|
||||
if os.path.exists(path):
|
||||
sys.stderr.write("Device reconnect on %s\n" % (path,))
|
||||
time.sleep(0.100)
|
||||
return path
|
||||
if alt_path is not None and os.path.exists(alt_path):
|
||||
if start_alt_path is None:
|
||||
start_alt_path = cur_time
|
||||
continue
|
||||
if cur_time >= start_alt_path + 0.300:
|
||||
sys.stderr.write("Device reconnect on alt path %s\n" % (
|
||||
alt_path,))
|
||||
return alt_path
|
||||
if cur_time > end_time:
|
||||
return path
|
||||
|
||||
CANBOOT_ID ="1d50:6177"
|
||||
|
||||
def detect_canboot(devpath):
|
||||
usbdir = os.path.dirname(devpath)
|
||||
try:
|
||||
with open(os.path.join(usbdir, "idVendor")) as f:
|
||||
vid = f.read().strip().lower()
|
||||
with open(os.path.join(usbdir, "idProduct")) as f:
|
||||
pid = f.read().strip().lower()
|
||||
except Exception:
|
||||
return False
|
||||
usbid = "%s:%s" % (vid, pid)
|
||||
return usbid == CANBOOT_ID
|
||||
|
||||
def call_flashcan(device, binfile):
|
||||
try:
|
||||
import serial
|
||||
except ModuleNotFoundError:
|
||||
sys.stderr.write(
|
||||
"Python's pyserial module is required to update. Install\n"
|
||||
"with the following command:\n"
|
||||
" %s -m pip install pyserial\n\n" % (sys.executable,)
|
||||
)
|
||||
sys.exit(-1)
|
||||
args = [sys.executable, "lib/canboot/flash_can.py", "-d",
|
||||
device, "-f", binfile]
|
||||
sys.stderr.write(" ".join(args) + '\n\n')
|
||||
res = subprocess.call(args)
|
||||
if res != 0:
|
||||
sys.stderr.write("Error running flash_can.py\n")
|
||||
sys.exit(-1)
|
||||
|
||||
def flash_canboot(options, binfile):
|
||||
ttyname, pathname = translate_serial_to_tty(options.device)
|
||||
call_flashcan(pathname, binfile)
|
||||
|
||||
# Flash via a call to bossac
|
||||
def flash_bossac(device, binfile, extra_flags=[]):
|
||||
ttyname, pathname = translate_serial_to_tty(device)
|
||||
enter_bootloader(pathname)
|
||||
pathname = wait_path(pathname, ttyname)
|
||||
baseargs = ["lib/bossac/bin/bossac", "-U", "-p", pathname]
|
||||
args = baseargs + extra_flags + ["-w", binfile, "-v"]
|
||||
sys.stderr.write(" ".join(args) + '\n\n')
|
||||
res = subprocess.call(args)
|
||||
if res != 0:
|
||||
raise error("Error running bossac")
|
||||
if "-R" not in extra_flags:
|
||||
args = baseargs + ["-b", "-R"]
|
||||
try:
|
||||
subprocess.check_output(args, stderr=subprocess.STDOUT)
|
||||
if "-b" not in extra_flags:
|
||||
wait_path(pathname)
|
||||
subprocess.check_output(args, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
pass
|
||||
|
||||
# Invoke the dfu-util program
|
||||
def call_dfuutil(flags, binfile, sudo):
|
||||
args = ["dfu-util"] + flags + ["-D", binfile]
|
||||
if sudo:
|
||||
args.insert(0, "sudo")
|
||||
sys.stderr.write(" ".join(args) + '\n\n')
|
||||
res = subprocess.call(args)
|
||||
if res != 0:
|
||||
raise error("Error running dfu-util")
|
||||
|
||||
# Flash via a call to dfu-util
|
||||
def flash_dfuutil(device, binfile, extra_flags=[], sudo=True):
|
||||
hexfmt_r = re.compile(r"^[a-fA-F0-9]{4}:[a-fA-F0-9]{4}$")
|
||||
if hexfmt_r.match(device.strip()):
|
||||
call_dfuutil(["-d", ","+device.strip()] + extra_flags, binfile, sudo)
|
||||
return
|
||||
ttyname, serbypath = translate_serial_to_tty(device)
|
||||
buspath, devpath = translate_serial_to_usb_path(device)
|
||||
enter_bootloader(device)
|
||||
pathname = wait_path(devpath)
|
||||
if detect_canboot(devpath):
|
||||
call_flashcan(serbypath, binfile)
|
||||
else:
|
||||
call_dfuutil(["-p", buspath] + extra_flags, binfile, sudo)
|
||||
|
||||
def call_hidflash(binfile, sudo):
|
||||
args = ["lib/hidflash/hid-flash", binfile]
|
||||
if sudo:
|
||||
args.insert(0, "sudo")
|
||||
sys.stderr.write(" ".join(args) + '\n\n')
|
||||
res = subprocess.call(args)
|
||||
if res != 0:
|
||||
raise error("Error running hid-flash")
|
||||
|
||||
# Flash via call to hid-flash
|
||||
def flash_hidflash(device, binfile, sudo=True):
|
||||
hexfmt_r = re.compile(r"^[a-fA-F0-9]{4}:[a-fA-F0-9]{4}$")
|
||||
if hexfmt_r.match(device.strip()):
|
||||
call_hidflash(binfile, sudo)
|
||||
return
|
||||
ttyname, serbypath = translate_serial_to_tty(device)
|
||||
buspath, devpath = translate_serial_to_usb_path(device)
|
||||
enter_bootloader(device)
|
||||
pathname = wait_path(devpath)
|
||||
if detect_canboot(devpath):
|
||||
call_flashcan(serbypath, binfile)
|
||||
else:
|
||||
call_hidflash(binfile, sudo)
|
||||
|
||||
# Call Klipper modified "picoboot"
|
||||
def call_picoboot(bus, addr, binfile, sudo):
|
||||
args = ["lib/rp2040_flash/rp2040_flash", binfile]
|
||||
if bus is not None:
|
||||
args.extend([bus, addr])
|
||||
if sudo:
|
||||
args.insert(0, "sudo")
|
||||
sys.stderr.write(" ".join(args) + '\n\n')
|
||||
res = subprocess.call(args)
|
||||
if res != 0:
|
||||
raise error("Error running rp2040_flash")
|
||||
|
||||
# Flash via Klipper modified "picoboot"
|
||||
def flash_picoboot(device, binfile, sudo):
|
||||
buspath, devpath = translate_serial_to_usb_path(device)
|
||||
# We need one level up to get access to busnum/devnum files
|
||||
usbdir = os.path.dirname(devpath)
|
||||
enter_bootloader(device)
|
||||
wait_path(usbdir)
|
||||
with open(usbdir + "/busnum") as f:
|
||||
bus = f.read().strip()
|
||||
with open(usbdir + "/devnum") as f:
|
||||
addr = f.read().strip()
|
||||
call_picoboot(bus, addr, binfile, sudo)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Device specific helpers
|
||||
######################################################################
|
||||
|
||||
def flash_atsam3(options, binfile):
|
||||
try:
|
||||
flash_bossac(options.device, binfile, ["-e", "-b"])
|
||||
except error as e:
|
||||
sys.stderr.write("Failed to flash to %s: %s\n" % (
|
||||
options.device, str(e)))
|
||||
sys.exit(-1)
|
||||
|
||||
def flash_atsam4(options, binfile):
|
||||
try:
|
||||
flash_bossac(options.device, binfile, ["-e"])
|
||||
except error as e:
|
||||
sys.stderr.write("Failed to flash to %s: %s\n" % (
|
||||
options.device, str(e)))
|
||||
sys.exit(-1)
|
||||
|
||||
def flash_atsamd(options, binfile):
|
||||
extra_flags = ["--offset=0x%x" % (options.start,), "-b", "-R"]
|
||||
try:
|
||||
flash_bossac(options.device, binfile, extra_flags)
|
||||
except error as e:
|
||||
sys.stderr.write("Failed to flash to %s: %s\n" % (
|
||||
options.device, str(e)))
|
||||
sys.exit(-1)
|
||||
|
||||
SMOOTHIE_HELP = """
|
||||
Failed to flash to %s: %s
|
||||
|
||||
If flashing Klipper to a Smoothieboard for the first time it may be
|
||||
necessary to manually place the board into "bootloader mode" - press
|
||||
and hold the "Play button" and then press and release the "Reset
|
||||
button".
|
||||
|
||||
When a Smoothieboard is in bootloader mode it can be flashed with the
|
||||
following command:
|
||||
make flash FLASH_DEVICE=1d50:6015
|
||||
|
||||
Alternatively, one can flash a Smoothieboard via SD card - copy the
|
||||
"out/klipper.bin" file to a file named "firmware.bin" on an SD card
|
||||
and then restart the Smoothieboard with that SD card.
|
||||
|
||||
"""
|
||||
|
||||
def flash_lpc176x(options, binfile):
|
||||
try:
|
||||
flash_dfuutil(options.device, binfile, [], options.sudo)
|
||||
except error as e:
|
||||
sys.stderr.write(SMOOTHIE_HELP % (options.device, str(e)))
|
||||
sys.exit(-1)
|
||||
|
||||
STM32F1_HELP = """
|
||||
Failed to flash to %s: %s
|
||||
|
||||
If the device is already in bootloader mode it can be flashed with the
|
||||
following command:
|
||||
make flash FLASH_DEVICE=1eaf:0003
|
||||
OR
|
||||
make flash FLASH_DEVICE=1209:beba
|
||||
|
||||
If attempting to flash via 3.3V serial, then use:
|
||||
make serialflash FLASH_DEVICE=%s
|
||||
|
||||
"""
|
||||
|
||||
def flash_stm32f1(options, binfile):
|
||||
try:
|
||||
if options.start == 0x8000800:
|
||||
flash_hidflash(options.device, binfile, options.sudo)
|
||||
else:
|
||||
flash_dfuutil(options.device, binfile, ["-R", "-a", "2"],
|
||||
options.sudo)
|
||||
except error as e:
|
||||
sys.stderr.write(STM32F1_HELP % (
|
||||
options.device, str(e), options.device))
|
||||
sys.exit(-1)
|
||||
|
||||
STM32F4_HELP = """
|
||||
Failed to flash to %s: %s
|
||||
|
||||
If the device is already in bootloader mode it can be flashed with the
|
||||
following command:
|
||||
make flash FLASH_DEVICE=0483:df11
|
||||
OR
|
||||
make flash FLASH_DEVICE=1209:beba
|
||||
|
||||
If attempting to flash via 3.3V serial, then use:
|
||||
make serialflash FLASH_DEVICE=%s
|
||||
|
||||
"""
|
||||
|
||||
def flash_stm32f4(options, binfile):
|
||||
start = "0x%x:leave" % (options.start,)
|
||||
try:
|
||||
if options.start == 0x8004000:
|
||||
flash_hidflash(options.device, binfile, options.sudo)
|
||||
else:
|
||||
flash_dfuutil(options.device, binfile,
|
||||
["-R", "-a", "0", "-s", start], options.sudo)
|
||||
except error as e:
|
||||
sys.stderr.write(STM32F4_HELP % (
|
||||
options.device, str(e), options.device))
|
||||
sys.exit(-1)
|
||||
|
||||
RP2040_HELP = """
|
||||
Failed to flash to %s: %s
|
||||
|
||||
If the device is already in bootloader mode it can be flashed with the
|
||||
following command:
|
||||
make flash FLASH_DEVICE=2e8a:0003
|
||||
|
||||
Alternatively, one can flash rp2040 boards like the Pico by manually
|
||||
entering bootloader mode(hold bootsel button during powerup), mount the
|
||||
device as a usb drive, and copy klipper.uf2 to the device.
|
||||
|
||||
"""
|
||||
|
||||
def flash_rp2040(options, binfile):
|
||||
try:
|
||||
if options.device.lower() == "2e8a:0003":
|
||||
call_picoboot(None, None, binfile, options.sudo)
|
||||
else:
|
||||
flash_picoboot(options.device, binfile, options.sudo)
|
||||
except error as e:
|
||||
sys.stderr.write(RP2040_HELP % (options.device, str(e)))
|
||||
sys.exit(-1)
|
||||
|
||||
MCUTYPES = {
|
||||
'sam3': flash_atsam3, 'sam4': flash_atsam4, 'samd': flash_atsamd,
|
||||
'same70': flash_atsam4, 'lpc176': flash_lpc176x, 'stm32f103': flash_stm32f1,
|
||||
'stm32f4': flash_stm32f4, 'stm32f042': flash_stm32f4,
|
||||
'stm32f072': flash_stm32f4, 'stm32g0b1': flash_stm32f4,
|
||||
'stm32h7': flash_stm32f4, 'rp2040': flash_rp2040
|
||||
}
|
||||
|
||||
|
||||
######################################################################
|
||||
# Startup
|
||||
######################################################################
|
||||
|
||||
def main():
|
||||
usage = "%prog [options] -t <type> -d <device> <klipper.bin>"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-t", "--type", type="string", dest="mcutype",
|
||||
help="micro-controller type")
|
||||
opts.add_option("-d", "--device", type="string", dest="device",
|
||||
help="serial port device")
|
||||
opts.add_option("-s", "--start", type="int", dest="start",
|
||||
help="start address in flash")
|
||||
opts.add_option("--no-sudo", action="store_false", dest="sudo",
|
||||
default=True, help="do not run sudo")
|
||||
options, args = opts.parse_args()
|
||||
if len(args) != 1:
|
||||
opts.error("Incorrect number of arguments")
|
||||
flash_func = None
|
||||
if options.mcutype:
|
||||
for prefix, func in MCUTYPES.items():
|
||||
if options.mcutype.startswith(prefix):
|
||||
flash_func = func
|
||||
break
|
||||
if flash_func is None:
|
||||
opts.error("USB flashing is not supported for MCU '%s'"
|
||||
% (options.mcutype,))
|
||||
if not options.device:
|
||||
sys.stderr.write("\nPlease specify FLASH_DEVICE\n\n")
|
||||
sys.exit(-1)
|
||||
flash_func(options, args[0])
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
259
scripts/graph_accelerometer.py
Normal file
259
scripts/graph_accelerometer.py
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
# Generate adxl345 accelerometer graphs
|
||||
#
|
||||
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
|
||||
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import importlib, optparse, os, sys
|
||||
from textwrap import wrap
|
||||
import numpy as np, matplotlib
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
'..', 'klippy'))
|
||||
shaper_calibrate = importlib.import_module('.shaper_calibrate', 'extras')
|
||||
|
||||
MAX_TITLE_LENGTH=65
|
||||
|
||||
def parse_log(logname, opts):
|
||||
with open(logname) as f:
|
||||
for header in f:
|
||||
if not header.startswith('#'):
|
||||
break
|
||||
if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
|
||||
# Raw accelerometer data
|
||||
return np.loadtxt(logname, comments='#', delimiter=',')
|
||||
# Power spectral density data or shaper calibration data
|
||||
opts.error("File %s does not contain raw accelerometer data and therefore "
|
||||
"is not supported by graph_accelerometer.py script. Please use "
|
||||
"calibrate_shaper.py script to process it instead." % (logname,))
|
||||
|
||||
######################################################################
|
||||
# Raw accelerometer graphing
|
||||
######################################################################
|
||||
|
||||
def plot_accel(data, logname):
|
||||
first_time = data[0, 0]
|
||||
times = data[:,0] - first_time
|
||||
fig, axes = matplotlib.pyplot.subplots(nrows=3, sharex=True)
|
||||
axes[0].set_title("\n".join(wrap("Accelerometer data (%s)" % (logname,),
|
||||
MAX_TITLE_LENGTH)))
|
||||
axis_names = ['x', 'y', 'z']
|
||||
for i in range(len(axis_names)):
|
||||
avg = data[:,i+1].mean()
|
||||
adata = data[:,i+1] - data[:,i+1].mean()
|
||||
ax = axes[i]
|
||||
ax.plot(times, adata, alpha=0.8)
|
||||
ax.grid(True)
|
||||
ax.set_ylabel('%s accel (%+.3f)\n(mm/s^2)' % (axis_names[i], -avg))
|
||||
axes[-1].set_xlabel('Time (%+.3f)\n(s)' % (-first_time,))
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
|
||||
######################################################################
|
||||
# Frequency graphing
|
||||
######################################################################
|
||||
|
||||
# Calculate estimated "power spectral density"
|
||||
def calc_freq_response(data, max_freq):
|
||||
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
||||
return helper.process_accelerometer_data(data)
|
||||
|
||||
def calc_specgram(data, axis):
|
||||
N = data.shape[0]
|
||||
Fs = N / (data[-1,0] - data[0,0])
|
||||
# Round up to a power of 2 for faster FFT
|
||||
M = 1 << int(.5 * Fs - 1).bit_length()
|
||||
window = np.kaiser(M, 6.)
|
||||
def _specgram(x):
|
||||
return matplotlib.mlab.specgram(
|
||||
x, Fs=Fs, NFFT=M, noverlap=M//2, window=window,
|
||||
mode='psd', detrend='mean', scale_by_freq=False)
|
||||
|
||||
d = {'x': data[:,1], 'y': data[:,2], 'z': data[:,3]}
|
||||
if axis != 'all':
|
||||
pdata, bins, t = _specgram(d[axis])
|
||||
else:
|
||||
pdata, bins, t = _specgram(d['x'])
|
||||
for ax in 'yz':
|
||||
pdata += _specgram(d[ax])[0]
|
||||
return pdata, bins, t
|
||||
|
||||
def plot_frequency(datas, lognames, max_freq):
|
||||
calibration_data = calc_freq_response(datas[0], max_freq)
|
||||
for data in datas[1:]:
|
||||
calibration_data.add_data(calc_freq_response(data, max_freq))
|
||||
freqs = calibration_data.freq_bins
|
||||
psd = calibration_data.psd_sum[freqs <= max_freq]
|
||||
px = calibration_data.psd_x[freqs <= max_freq]
|
||||
py = calibration_data.psd_y[freqs <= max_freq]
|
||||
pz = calibration_data.psd_z[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
|
||||
fig, ax = matplotlib.pyplot.subplots()
|
||||
ax.set_title("\n".join(wrap(
|
||||
"Frequency response (%s)" % (', '.join(lognames)), MAX_TITLE_LENGTH)))
|
||||
ax.set_xlabel('Frequency (Hz)')
|
||||
ax.set_ylabel('Power spectral density')
|
||||
|
||||
ax.plot(freqs, psd, label='X+Y+Z', alpha=0.6)
|
||||
ax.plot(freqs, px, label='X', alpha=0.6)
|
||||
ax.plot(freqs, py, label='Y', alpha=0.6)
|
||||
ax.plot(freqs, pz, label='Z', alpha=0.6)
|
||||
|
||||
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.grid(which='major', color='grey')
|
||||
ax.grid(which='minor', color='lightgrey')
|
||||
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0,0))
|
||||
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax.legend(loc='best', prop=fontP)
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
def plot_compare_frequency(datas, lognames, max_freq, axis):
|
||||
fig, ax = matplotlib.pyplot.subplots()
|
||||
ax.set_title('Frequency responses comparison')
|
||||
ax.set_xlabel('Frequency (Hz)')
|
||||
ax.set_ylabel('Power spectral density')
|
||||
|
||||
for data, logname in zip(datas, lognames):
|
||||
calibration_data = calc_freq_response(data, max_freq)
|
||||
freqs = calibration_data.freq_bins
|
||||
psd = calibration_data.get_psd(axis)[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
ax.plot(freqs, psd, label="\n".join(wrap(logname, 60)), alpha=0.6)
|
||||
|
||||
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.grid(which='major', color='grey')
|
||||
ax.grid(which='minor', color='lightgrey')
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax.legend(loc='best', prop=fontP)
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
# Plot data in a "spectrogram colormap"
|
||||
def plot_specgram(data, logname, max_freq, axis):
|
||||
pdata, bins, t = calc_specgram(data, axis)
|
||||
|
||||
fig, ax = matplotlib.pyplot.subplots()
|
||||
ax.set_title("\n".join(wrap("Spectrogram %s (%s)" % (axis, logname),
|
||||
MAX_TITLE_LENGTH)))
|
||||
ax.pcolormesh(t, bins, pdata, norm=matplotlib.colors.LogNorm())
|
||||
ax.set_ylim([0., max_freq])
|
||||
ax.set_ylabel('frequency (hz)')
|
||||
ax.set_xlabel('Time (s)')
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
######################################################################
|
||||
# CSV output
|
||||
######################################################################
|
||||
|
||||
def write_frequency_response(datas, output):
|
||||
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
||||
calibration_data = helper.process_accelerometer_data(datas[0])
|
||||
for data in datas[1:]:
|
||||
calibration_data.add_data(helper.process_accelerometer_data(data))
|
||||
helper.save_calibration_data(output, calibration_data)
|
||||
|
||||
def write_specgram(psd, freq_bins, time, output):
|
||||
M = freq_bins.shape[0]
|
||||
with open(output, "w") as csvfile:
|
||||
csvfile.write("freq\\t")
|
||||
for ts in time:
|
||||
csvfile.write(",%.6f" % (ts,))
|
||||
csvfile.write("\n")
|
||||
for i in range(M):
|
||||
csvfile.write("%.1f" % (freq_bins[i],))
|
||||
for value in psd[i,:]:
|
||||
csvfile.write(",%.6e" % (value,))
|
||||
csvfile.write("\n")
|
||||
|
||||
######################################################################
|
||||
# Startup
|
||||
######################################################################
|
||||
|
||||
def is_csv_output(output):
|
||||
return output and os.path.splitext(output)[1].lower() == '.csv'
|
||||
|
||||
def setup_matplotlib(output):
|
||||
global matplotlib
|
||||
if is_csv_output(output):
|
||||
# Only mlab may be necessary with CSV output
|
||||
import matplotlib.mlab
|
||||
return
|
||||
if output:
|
||||
matplotlib.rcParams.update({'figure.autolayout': True})
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||
import matplotlib.ticker
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
usage = "%prog [options] <raw logs>"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-o", "--output", type="string", dest="output",
|
||||
default=None, help="filename of output graph")
|
||||
opts.add_option("-f", "--max_freq", type="float", default=200.,
|
||||
help="maximum frequency to graph")
|
||||
opts.add_option("-r", "--raw", action="store_true",
|
||||
help="graph raw accelerometer data")
|
||||
opts.add_option("-c", "--compare", action="store_true",
|
||||
help="graph comparison of power spectral density "
|
||||
"between different accelerometer data files")
|
||||
opts.add_option("-s", "--specgram", action="store_true",
|
||||
help="graph spectrogram of accelerometer data")
|
||||
opts.add_option("-a", type="string", dest="axis", default="all",
|
||||
help="axis to graph (one of 'all', 'x', 'y', or 'z')")
|
||||
options, args = opts.parse_args()
|
||||
if len(args) < 1:
|
||||
opts.error("Incorrect number of arguments")
|
||||
|
||||
# Parse data
|
||||
datas = [parse_log(fn, opts) for fn in args]
|
||||
|
||||
setup_matplotlib(options.output)
|
||||
|
||||
if is_csv_output(options.output):
|
||||
if options.raw:
|
||||
opts.error("raw mode is not supported with csv output")
|
||||
if options.compare:
|
||||
opts.error("comparison mode is not supported with csv output")
|
||||
if options.specgram:
|
||||
if len(args) > 1:
|
||||
opts.error("Only 1 input is supported in specgram mode")
|
||||
pdata, bins, t = calc_specgram(datas[0], options.axis)
|
||||
write_specgram(pdata, bins, t, options.output)
|
||||
else:
|
||||
write_frequency_response(datas, options.output)
|
||||
return
|
||||
|
||||
# Draw graph
|
||||
if options.raw:
|
||||
if len(args) > 1:
|
||||
opts.error("Only 1 input is supported in raw mode")
|
||||
fig = plot_accel(datas[0], args[0])
|
||||
elif options.specgram:
|
||||
if len(args) > 1:
|
||||
opts.error("Only 1 input is supported in specgram mode")
|
||||
fig = plot_specgram(datas[0], args[0], options.max_freq, options.axis)
|
||||
elif options.compare:
|
||||
fig = plot_compare_frequency(datas, args, options.max_freq,
|
||||
options.axis)
|
||||
else:
|
||||
fig = plot_frequency(datas, args, options.max_freq)
|
||||
|
||||
# Show graph
|
||||
if options.output is None:
|
||||
matplotlib.pyplot.show()
|
||||
else:
|
||||
fig.set_size_inches(8, 6)
|
||||
fig.savefig(options.output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
193
scripts/graph_extruder.py
Normal file
193
scripts/graph_extruder.py
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python
|
||||
# Generate extruder pressure advance motion graphs
|
||||
#
|
||||
# Copyright (C) 2019-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import math, optparse, datetime
|
||||
import matplotlib
|
||||
|
||||
SEG_TIME = .000100
|
||||
INV_SEG_TIME = 1. / SEG_TIME
|
||||
|
||||
|
||||
######################################################################
|
||||
# Basic trapezoid motion
|
||||
######################################################################
|
||||
|
||||
# List of moves: [(start_v, end_v, move_t), ...]
|
||||
Moves = [
|
||||
(0., 0., .100),
|
||||
(0., 100., None), (100., 100., .200), (100., 60., None),
|
||||
(60., 100., None), (100., 100., .200), (100., 0., None),
|
||||
(0., 0., .300)
|
||||
]
|
||||
EXTRUDE_R = (.4 * .4 * .75) / (math.pi * (1.75 / 2.)**2)
|
||||
ACCEL = 3000. * EXTRUDE_R
|
||||
|
||||
def gen_positions():
|
||||
out = []
|
||||
start_d = start_t = t = 0.
|
||||
for start_v, end_v, move_t in Moves:
|
||||
start_v *= EXTRUDE_R
|
||||
end_v *= EXTRUDE_R
|
||||
if move_t is None:
|
||||
move_t = abs(end_v - start_v) / ACCEL
|
||||
half_accel = 0.
|
||||
if end_v > start_v:
|
||||
half_accel = .5 * ACCEL
|
||||
elif start_v > end_v:
|
||||
half_accel = -.5 * ACCEL
|
||||
end_t = start_t + move_t
|
||||
while t <= end_t:
|
||||
rel_t = t - start_t
|
||||
out.append(start_d + (start_v + half_accel * rel_t) * rel_t)
|
||||
t += SEG_TIME
|
||||
start_d += (start_v + half_accel * move_t) * move_t
|
||||
start_t = end_t
|
||||
return out
|
||||
|
||||
|
||||
######################################################################
|
||||
# List helper functions
|
||||
######################################################################
|
||||
|
||||
MARGIN_TIME = 0.050
|
||||
|
||||
def time_to_index(t):
|
||||
return int(t * INV_SEG_TIME + .5)
|
||||
|
||||
def indexes(positions):
|
||||
drop = time_to_index(MARGIN_TIME)
|
||||
return range(drop, len(positions)-drop)
|
||||
|
||||
def trim_lists(*lists):
|
||||
keep = len(lists[0]) - time_to_index(2. * MARGIN_TIME)
|
||||
for l in lists:
|
||||
del l[keep:]
|
||||
|
||||
|
||||
######################################################################
|
||||
# Common data filters
|
||||
######################################################################
|
||||
|
||||
# Generate estimated first order derivative
|
||||
def gen_deriv(data):
|
||||
return [0.] + [(data[i+1] - data[i]) * INV_SEG_TIME
|
||||
for i in range(len(data)-1)]
|
||||
|
||||
# Simple average between two points smooth_time away
|
||||
def calc_average(positions, smooth_time):
|
||||
offset = time_to_index(smooth_time * .5)
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
out[i] = .5 * (positions[i-offset] + positions[i+offset])
|
||||
return out
|
||||
|
||||
# Average (via integration) of smooth_time range
|
||||
def calc_smooth(positions, smooth_time):
|
||||
offset = time_to_index(smooth_time * .5)
|
||||
weight = 1. / (2*offset - 1)
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
out[i] = sum(positions[i-offset+1:i+offset]) * weight
|
||||
return out
|
||||
|
||||
# Time weighted average (via integration) of smooth_time range
|
||||
def calc_weighted(positions, smooth_time):
|
||||
offset = time_to_index(smooth_time * .5)
|
||||
weight = 1. / offset**2
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
weighted_data = [positions[j] * (offset - abs(j-i))
|
||||
for j in range(i-offset, i+offset)]
|
||||
out[i] = sum(weighted_data) * weight
|
||||
return out
|
||||
|
||||
|
||||
######################################################################
|
||||
# Pressure advance
|
||||
######################################################################
|
||||
|
||||
SMOOTH_TIME = .040
|
||||
PRESSURE_ADVANCE = .045
|
||||
|
||||
# Calculate raw pressure advance positions
|
||||
def calc_pa_raw(positions):
|
||||
pa = PRESSURE_ADVANCE * INV_SEG_TIME
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
out[i] = positions[i] + pa * (positions[i+1] - positions[i])
|
||||
return out
|
||||
|
||||
# Pressure advance after smoothing
|
||||
def calc_pa(positions):
|
||||
return calc_weighted(calc_pa_raw(positions), SMOOTH_TIME)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Plotting and startup
|
||||
######################################################################
|
||||
|
||||
def plot_motion():
|
||||
# Nominal motion
|
||||
positions = gen_positions()
|
||||
velocities = gen_deriv(positions)
|
||||
accels = gen_deriv(velocities)
|
||||
# Motion with pressure advance
|
||||
pa_positions = calc_pa_raw(positions)
|
||||
pa_velocities = gen_deriv(pa_positions)
|
||||
# Smoothed motion
|
||||
sm_positions = calc_pa(positions)
|
||||
sm_velocities = gen_deriv(sm_positions)
|
||||
# Build plot
|
||||
times = [SEG_TIME * i for i in range(len(positions))]
|
||||
trim_lists(times, velocities, accels,
|
||||
pa_positions, pa_velocities,
|
||||
sm_positions, sm_velocities)
|
||||
fig, ax1 = matplotlib.pyplot.subplots(nrows=1, sharex=True)
|
||||
ax1.set_title("Extruder Velocity")
|
||||
ax1.set_ylabel('Velocity (mm/s)')
|
||||
pa_plot, = ax1.plot(times, pa_velocities, 'r',
|
||||
label='Pressure Advance', alpha=0.3)
|
||||
nom_plot, = ax1.plot(times, velocities, 'black', label='Nominal')
|
||||
sm_plot, = ax1.plot(times, sm_velocities, 'g', label='Smooth PA', alpha=0.9)
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax1.legend(handles=[nom_plot, pa_plot, sm_plot], loc='best', prop=fontP)
|
||||
ax1.set_xlabel('Time (s)')
|
||||
ax1.grid(True)
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
def setup_matplotlib(output_to_file):
|
||||
global matplotlib
|
||||
if output_to_file:
|
||||
matplotlib.rcParams.update({'figure.autolayout': True})
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||
import matplotlib.ticker
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
usage = "%prog [options]"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-o", "--output", type="string", dest="output",
|
||||
default=None, help="filename of output graph")
|
||||
options, args = opts.parse_args()
|
||||
if len(args) != 0:
|
||||
opts.error("Incorrect number of arguments")
|
||||
|
||||
# Draw graph
|
||||
setup_matplotlib(options.output is not None)
|
||||
fig = plot_motion()
|
||||
|
||||
# Show graph
|
||||
if options.output is None:
|
||||
matplotlib.pyplot.show()
|
||||
else:
|
||||
fig.set_size_inches(6, 2.5)
|
||||
fig.savefig(options.output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
427
scripts/graph_motion.py
Normal file
427
scripts/graph_motion.py
Normal file
@@ -0,0 +1,427 @@
|
||||
#!/usr/bin/env python
|
||||
# Script to graph motion results
|
||||
#
|
||||
# Copyright (C) 2019-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import optparse, datetime, math
|
||||
import matplotlib
|
||||
|
||||
SEG_TIME = .000100
|
||||
INV_SEG_TIME = 1. / SEG_TIME
|
||||
|
||||
SPRING_FREQ=35.0
|
||||
DAMPING_RATIO=0.05
|
||||
|
||||
CONFIG_FREQ=40.0
|
||||
CONFIG_DAMPING_RATIO=0.1
|
||||
|
||||
######################################################################
|
||||
# Basic trapezoid motion
|
||||
######################################################################
|
||||
|
||||
# List of moves: [(start_v, end_v, move_t), ...]
|
||||
Moves = [
|
||||
(0., 0., .100),
|
||||
(6.869, 89.443, None), (89.443, 89.443, .120), (89.443, 17.361, None),
|
||||
(19.410, 120., None), (120., 120., .130), (120., 5., None),
|
||||
(0., 0., 0.01),
|
||||
(-5., -100., None), (-100., -100., .100), (-100., -.5, None),
|
||||
(0., 0., .200)
|
||||
]
|
||||
ACCEL = 3000.
|
||||
MAX_JERK = ACCEL * 0.6 * SPRING_FREQ
|
||||
|
||||
def get_accel(start_v, end_v):
|
||||
return ACCEL
|
||||
|
||||
def get_accel_jerk_limit(start_v, end_v):
|
||||
effective_accel = math.sqrt(MAX_JERK * abs(end_v - start_v) / 6.)
|
||||
return min(effective_accel, ACCEL)
|
||||
|
||||
# Standard constant acceleration generator
|
||||
def get_acc_pos_ao2(rel_t, start_v, accel, move_t):
|
||||
return (start_v + 0.5 * accel * rel_t) * rel_t
|
||||
|
||||
# Bezier curve "accel_order=4" generator
|
||||
def get_acc_pos_ao4(rel_t, start_v, accel, move_t):
|
||||
inv_accel_t = 1. / move_t
|
||||
accel_div_accel_t = accel * inv_accel_t
|
||||
accel_div_accel_t2 = accel_div_accel_t * inv_accel_t
|
||||
|
||||
c4 = -.5 * accel_div_accel_t2;
|
||||
c3 = accel_div_accel_t;
|
||||
c1 = start_v
|
||||
return ((c4 * rel_t + c3) * rel_t * rel_t + c1) * rel_t
|
||||
|
||||
# Bezier curve "accel_order=6" generator
|
||||
def get_acc_pos_ao6(rel_t, start_v, accel, move_t):
|
||||
inv_accel_t = 1. / move_t
|
||||
accel_div_accel_t = accel * inv_accel_t
|
||||
accel_div_accel_t2 = accel_div_accel_t * inv_accel_t
|
||||
accel_div_accel_t3 = accel_div_accel_t2 * inv_accel_t
|
||||
accel_div_accel_t4 = accel_div_accel_t3 * inv_accel_t
|
||||
|
||||
c6 = accel_div_accel_t4;
|
||||
c5 = -3. * accel_div_accel_t3;
|
||||
c4 = 2.5 * accel_div_accel_t2;
|
||||
c1 = start_v;
|
||||
return (((c6 * rel_t + c5) * rel_t + c4)
|
||||
* rel_t * rel_t * rel_t + c1) * rel_t
|
||||
|
||||
get_acc_pos = get_acc_pos_ao2
|
||||
get_acc = get_accel
|
||||
|
||||
# Calculate positions based on 'Moves' list
|
||||
def gen_positions():
|
||||
out = []
|
||||
start_d = start_t = t = 0.
|
||||
for start_v, end_v, move_t in Moves:
|
||||
if move_t is None:
|
||||
move_t = abs(end_v - start_v) / get_acc(start_v, end_v)
|
||||
accel = (end_v - start_v) / move_t
|
||||
end_t = start_t + move_t
|
||||
while t <= end_t:
|
||||
rel_t = t - start_t
|
||||
out.append(start_d + get_acc_pos(rel_t, start_v, accel, move_t))
|
||||
t += SEG_TIME
|
||||
start_d += get_acc_pos(move_t, start_v, accel, move_t)
|
||||
start_t = end_t
|
||||
return out
|
||||
|
||||
|
||||
######################################################################
|
||||
# Estimated motion with belt as spring
|
||||
######################################################################
|
||||
|
||||
def estimate_spring(positions):
|
||||
ang_freq2 = (SPRING_FREQ * 2. * math.pi)**2
|
||||
damping_factor = 4. * math.pi * DAMPING_RATIO * SPRING_FREQ
|
||||
head_pos = head_v = 0.
|
||||
out = []
|
||||
for stepper_pos in positions:
|
||||
head_pos += head_v * SEG_TIME
|
||||
head_a = (stepper_pos - head_pos) * ang_freq2
|
||||
head_v += head_a * SEG_TIME
|
||||
head_v -= head_v * damping_factor * SEG_TIME
|
||||
out.append(head_pos)
|
||||
return out
|
||||
|
||||
|
||||
######################################################################
|
||||
# List helper functions
|
||||
######################################################################
|
||||
|
||||
MARGIN_TIME = 0.050
|
||||
|
||||
def time_to_index(t):
|
||||
return int(t * INV_SEG_TIME + .5)
|
||||
|
||||
def indexes(positions):
|
||||
drop = time_to_index(MARGIN_TIME)
|
||||
return range(drop, len(positions)-drop)
|
||||
|
||||
def trim_lists(*lists):
|
||||
keep = len(lists[0]) - time_to_index(2. * MARGIN_TIME)
|
||||
for l in lists:
|
||||
del l[keep:]
|
||||
|
||||
|
||||
######################################################################
|
||||
# Common data filters
|
||||
######################################################################
|
||||
|
||||
# Generate estimated first order derivative
|
||||
def gen_deriv(data):
|
||||
return [0.] + [(data[i+1] - data[i]) * INV_SEG_TIME
|
||||
for i in range(len(data)-1)]
|
||||
|
||||
# Simple average between two points smooth_time away
|
||||
def calc_average(positions, smooth_time):
|
||||
offset = time_to_index(smooth_time * .5)
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
out[i] = .5 * (positions[i-offset] + positions[i+offset])
|
||||
return out
|
||||
|
||||
# Average (via integration) of smooth_time range
|
||||
def calc_smooth(positions, smooth_time):
|
||||
offset = time_to_index(smooth_time * .5)
|
||||
weight = 1. / (2*offset - 1)
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
out[i] = sum(positions[i-offset+1:i+offset]) * weight
|
||||
return out
|
||||
|
||||
# Time weighted average (via integration) of smooth_time range
|
||||
def calc_weighted(positions, smooth_time):
|
||||
offset = time_to_index(smooth_time * .5)
|
||||
weight = 1. / offset**2
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
weighted_data = [positions[j] * (offset - abs(j-i))
|
||||
for j in range(i-offset, i+offset)]
|
||||
out[i] = sum(weighted_data) * weight
|
||||
return out
|
||||
|
||||
# Weighted average (`h**2 - (t-T)**2`) of smooth_time range
|
||||
def calc_weighted2(positions, smooth_time):
|
||||
offset = time_to_index(smooth_time * .5)
|
||||
weight = .75 / offset**3
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
weighted_data = [positions[j] * (offset**2 - (j-i)**2)
|
||||
for j in range(i-offset, i+offset)]
|
||||
out[i] = sum(weighted_data) * weight
|
||||
return out
|
||||
|
||||
# Weighted average (`(h**2 - (t-T)**2)**2`) of smooth_time range
|
||||
def calc_weighted4(positions, smooth_time):
|
||||
offset = time_to_index(smooth_time * .5)
|
||||
weight = 15 / (16. * offset**5)
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
weighted_data = [positions[j] * ((offset**2 - (j-i)**2))**2
|
||||
for j in range(i-offset, i+offset)]
|
||||
out[i] = sum(weighted_data) * weight
|
||||
return out
|
||||
|
||||
# Weighted average (`(h - abs(t-T))**2 * (2 * abs(t-T) + h)`) of range
|
||||
def calc_weighted3(positions, smooth_time):
|
||||
offset = time_to_index(smooth_time * .5)
|
||||
weight = 1. / offset**4
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
weighted_data = [positions[j] * (offset - abs(j-i))**2
|
||||
* (2. * abs(j-i) + offset)
|
||||
for j in range(i-offset, i+offset)]
|
||||
out[i] = sum(weighted_data) * weight
|
||||
return out
|
||||
|
||||
|
||||
######################################################################
|
||||
# Spring motion estimation
|
||||
######################################################################
|
||||
|
||||
def calc_spring_raw(positions):
|
||||
sa = (INV_SEG_TIME / (CONFIG_FREQ * 2. * math.pi))**2
|
||||
ra = 2. * CONFIG_DAMPING_RATIO * math.sqrt(sa)
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
out[i] = (positions[i]
|
||||
+ sa * (positions[i-1] - 2.*positions[i] + positions[i+1])
|
||||
+ ra * (positions[i+1] - positions[i]))
|
||||
return out
|
||||
|
||||
def calc_spring_double_weighted(positions, smooth_time):
|
||||
offset = time_to_index(smooth_time * .25)
|
||||
sa = (INV_SEG_TIME / (offset * CONFIG_FREQ * 2. * math.pi))**2
|
||||
ra = 2. * CONFIG_DAMPING_RATIO * math.sqrt(sa)
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
out[i] = (positions[i]
|
||||
+ sa * (positions[i-offset] - 2.*positions[i]
|
||||
+ positions[i+offset])
|
||||
+ ra * (positions[i+1] - positions[i]))
|
||||
return calc_weighted(out, smooth_time=.5 * smooth_time)
|
||||
|
||||
######################################################################
|
||||
# Input shapers
|
||||
######################################################################
|
||||
|
||||
def get_zv_shaper():
|
||||
df = math.sqrt(1. - CONFIG_DAMPING_RATIO**2)
|
||||
K = math.exp(-CONFIG_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (CONFIG_FREQ * df)
|
||||
A = [1., K]
|
||||
T = [0., .5*t_d]
|
||||
return (A, T, "ZV")
|
||||
|
||||
def get_zvd_shaper():
|
||||
df = math.sqrt(1. - CONFIG_DAMPING_RATIO**2)
|
||||
K = math.exp(-CONFIG_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (CONFIG_FREQ * df)
|
||||
A = [1., 2.*K, K**2]
|
||||
T = [0., .5*t_d, t_d]
|
||||
return (A, T, "ZVD")
|
||||
|
||||
def get_mzv_shaper():
|
||||
df = math.sqrt(1. - CONFIG_DAMPING_RATIO**2)
|
||||
K = math.exp(-.75 * CONFIG_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (CONFIG_FREQ * df)
|
||||
|
||||
a1 = 1. - 1. / math.sqrt(2.)
|
||||
a2 = (math.sqrt(2.) - 1.) * K
|
||||
a3 = a1 * K * K
|
||||
|
||||
A = [a1, a2, a3]
|
||||
T = [0., .375*t_d, .75*t_d]
|
||||
return (A, T, "MZV")
|
||||
|
||||
def get_ei_shaper():
|
||||
v_tol = 0.05 # vibration tolerance
|
||||
df = math.sqrt(1. - CONFIG_DAMPING_RATIO**2)
|
||||
K = math.exp(-CONFIG_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (CONFIG_FREQ * df)
|
||||
|
||||
a1 = .25 * (1. + v_tol)
|
||||
a2 = .5 * (1. - v_tol) * K
|
||||
a3 = a1 * K * K
|
||||
|
||||
A = [a1, a2, a3]
|
||||
T = [0., .5*t_d, t_d]
|
||||
return (A, T, "EI")
|
||||
|
||||
def get_2hump_ei_shaper():
|
||||
v_tol = 0.05 # vibration tolerance
|
||||
df = math.sqrt(1. - CONFIG_DAMPING_RATIO**2)
|
||||
K = math.exp(-CONFIG_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (CONFIG_FREQ * df)
|
||||
|
||||
V2 = v_tol**2
|
||||
X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.)
|
||||
a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X)
|
||||
a2 = (.5 - a1) * K
|
||||
a3 = a2 * K
|
||||
a4 = a1 * K * K * K
|
||||
|
||||
A = [a1, a2, a3, a4]
|
||||
T = [0., .5*t_d, t_d, 1.5*t_d]
|
||||
return (A, T, "2-hump EI")
|
||||
|
||||
def get_3hump_ei_shaper():
|
||||
v_tol = 0.05 # vibration tolerance
|
||||
df = math.sqrt(1. - CONFIG_DAMPING_RATIO**2)
|
||||
K = math.exp(-CONFIG_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (CONFIG_FREQ * df)
|
||||
|
||||
K2 = K*K
|
||||
a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol))
|
||||
a2 = 0.25 * (1. - v_tol) * K
|
||||
a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2
|
||||
a4 = a2 * K2
|
||||
a5 = a1 * K2 * K2
|
||||
|
||||
A = [a1, a2, a3, a4, a5]
|
||||
T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d]
|
||||
return (A, T, "3-hump EI")
|
||||
|
||||
|
||||
def shift_pulses(shaper):
|
||||
A, T, name = shaper
|
||||
n = len(T)
|
||||
ts = (sum([A[i] * T[i] for i in range(n)])) / sum(A)
|
||||
for i in range(n):
|
||||
T[i] -= ts
|
||||
|
||||
def calc_shaper(shaper, positions):
|
||||
shift_pulses(shaper)
|
||||
A = shaper[0]
|
||||
inv_D = 1. / sum(A)
|
||||
n = len(A)
|
||||
T = [time_to_index(-shaper[1][j]) for j in range(n)]
|
||||
out = [0.] * len(positions)
|
||||
for i in indexes(positions):
|
||||
out[i] = sum([positions[i + T[j]] * A[j] for j in range(n)]) * inv_D
|
||||
return out
|
||||
|
||||
# Ideal values
|
||||
SMOOTH_TIME = (2./3.) / CONFIG_FREQ
|
||||
|
||||
def gen_updated_position(positions):
|
||||
#return calc_weighted(positions, 0.040)
|
||||
#return calc_spring_double_weighted(positions, SMOOTH_TIME)
|
||||
#return calc_weighted4(calc_spring_raw(positions), SMOOTH_TIME)
|
||||
return calc_shaper(get_ei_shaper(), positions)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Plotting and startup
|
||||
######################################################################
|
||||
|
||||
def plot_motion():
|
||||
# Nominal motion
|
||||
positions = gen_positions()
|
||||
velocities = gen_deriv(positions)
|
||||
accels = gen_deriv(velocities)
|
||||
# Updated motion
|
||||
upd_positions = gen_updated_position(positions)
|
||||
upd_velocities = gen_deriv(upd_positions)
|
||||
upd_accels = gen_deriv(upd_velocities)
|
||||
# Estimated position with model of belt as spring
|
||||
spring_orig = estimate_spring(positions)
|
||||
spring_upd = estimate_spring(upd_positions)
|
||||
spring_diff_orig = [n-o for n, o in zip(spring_orig, positions)]
|
||||
spring_diff_upd = [n-o for n, o in zip(spring_upd, positions)]
|
||||
head_velocities = gen_deriv(spring_orig)
|
||||
head_accels = gen_deriv(head_velocities)
|
||||
head_upd_velocities = gen_deriv(spring_upd)
|
||||
head_upd_accels = gen_deriv(head_upd_velocities)
|
||||
# Build plot
|
||||
times = [SEG_TIME * i for i in range(len(positions))]
|
||||
trim_lists(times, velocities, accels,
|
||||
upd_velocities, upd_velocities, upd_accels,
|
||||
spring_diff_orig, spring_diff_upd,
|
||||
head_velocities, head_upd_velocities,
|
||||
head_accels, head_upd_accels)
|
||||
fig, (ax1, ax2, ax3) = matplotlib.pyplot.subplots(nrows=3, sharex=True)
|
||||
ax1.set_title("Simulation: resonance freq=%.1f Hz, damping_ratio=%.3f,\n"
|
||||
"configured freq=%.1f Hz, damping_ratio = %.3f"
|
||||
% (SPRING_FREQ, DAMPING_RATIO, CONFIG_FREQ
|
||||
, CONFIG_DAMPING_RATIO))
|
||||
ax1.set_ylabel('Velocity (mm/s)')
|
||||
ax1.plot(times, upd_velocities, 'r', label='New Velocity', alpha=0.8)
|
||||
ax1.plot(times, velocities, 'g', label='Nominal Velocity', alpha=0.8)
|
||||
ax1.plot(times, head_velocities, label='Head Velocity', alpha=0.4)
|
||||
ax1.plot(times, head_upd_velocities, label='New Head Velocity', alpha=0.4)
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax1.legend(loc='best', prop=fontP)
|
||||
ax1.grid(True)
|
||||
ax2.set_ylabel('Acceleration (mm/s^2)')
|
||||
ax2.plot(times, upd_accels, 'r', label='New Accel', alpha=0.8)
|
||||
ax2.plot(times, accels, 'g', label='Nominal Accel', alpha=0.8)
|
||||
ax2.plot(times, head_accels, alpha=0.4)
|
||||
ax2.plot(times, head_upd_accels, alpha=0.4)
|
||||
ax2.set_ylim([-5. * ACCEL, 5. * ACCEL])
|
||||
ax2.legend(loc='best', prop=fontP)
|
||||
ax2.grid(True)
|
||||
ax3.set_ylabel('Deviation (mm)')
|
||||
ax3.plot(times, spring_diff_upd, 'r', label='New', alpha=0.8)
|
||||
ax3.plot(times, spring_diff_orig, 'g', label='Nominal', alpha=0.8)
|
||||
ax3.grid(True)
|
||||
ax3.legend(loc='best', prop=fontP)
|
||||
ax3.set_xlabel('Time (s)')
|
||||
return fig
|
||||
|
||||
def setup_matplotlib(output_to_file):
|
||||
global matplotlib
|
||||
if output_to_file:
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||
import matplotlib.ticker
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
usage = "%prog [options]"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-o", "--output", type="string", dest="output",
|
||||
default=None, help="filename of output graph")
|
||||
options, args = opts.parse_args()
|
||||
if len(args) != 0:
|
||||
opts.error("Incorrect number of arguments")
|
||||
|
||||
# Draw graph
|
||||
setup_matplotlib(options.output is not None)
|
||||
fig = plot_motion()
|
||||
|
||||
# Show graph
|
||||
if options.output is None:
|
||||
matplotlib.pyplot.show()
|
||||
else:
|
||||
fig.set_size_inches(8, 6)
|
||||
fig.savefig(options.output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
283
scripts/graph_shaper.py
Normal file
283
scripts/graph_shaper.py
Normal file
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python
|
||||
# Script to plot input shapers
|
||||
#
|
||||
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
|
||||
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import optparse, math
|
||||
import matplotlib
|
||||
|
||||
# A set of damping ratios to calculate shaper response for
|
||||
DAMPING_RATIOS=[0.05, 0.1, 0.2]
|
||||
|
||||
# Parameters of the input shaper
|
||||
SHAPER_FREQ=50.0
|
||||
SHAPER_DAMPING_RATIO=0.1
|
||||
|
||||
# Simulate input shaping of step function for these true resonance frequency
|
||||
# and damping ratio
|
||||
STEP_SIMULATION_RESONANCE_FREQ=60.
|
||||
STEP_SIMULATION_DAMPING_RATIO=0.15
|
||||
|
||||
# If set, defines which range of frequencies to plot shaper frequency responce
|
||||
PLOT_FREQ_RANGE = [] # If empty, will be automatically determined
|
||||
#PLOT_FREQ_RANGE = [10., 100.]
|
||||
|
||||
PLOT_FREQ_STEP = .01
|
||||
|
||||
######################################################################
|
||||
# Input shapers
|
||||
######################################################################
|
||||
|
||||
def get_zv_shaper():
|
||||
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
|
||||
K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (SHAPER_FREQ * df)
|
||||
A = [1., K]
|
||||
T = [0., .5*t_d]
|
||||
return (A, T, "ZV")
|
||||
|
||||
def get_zvd_shaper():
|
||||
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
|
||||
K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (SHAPER_FREQ * df)
|
||||
A = [1., 2.*K, K**2]
|
||||
T = [0., .5*t_d, t_d]
|
||||
return (A, T, "ZVD")
|
||||
|
||||
def get_mzv_shaper():
|
||||
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
|
||||
K = math.exp(-.75 * SHAPER_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (SHAPER_FREQ * df)
|
||||
|
||||
a1 = 1. - 1. / math.sqrt(2.)
|
||||
a2 = (math.sqrt(2.) - 1.) * K
|
||||
a3 = a1 * K * K
|
||||
|
||||
A = [a1, a2, a3]
|
||||
T = [0., .375*t_d, .75*t_d]
|
||||
return (A, T, "MZV")
|
||||
|
||||
def get_ei_shaper():
|
||||
v_tol = 0.05 # vibration tolerance
|
||||
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
|
||||
K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (SHAPER_FREQ * df)
|
||||
|
||||
a1 = .25 * (1. + v_tol)
|
||||
a2 = .5 * (1. - v_tol) * K
|
||||
a3 = a1 * K * K
|
||||
|
||||
A = [a1, a2, a3]
|
||||
T = [0., .5*t_d, t_d]
|
||||
return (A, T, "EI")
|
||||
|
||||
def get_2hump_ei_shaper():
|
||||
v_tol = 0.05 # vibration tolerance
|
||||
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
|
||||
K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (SHAPER_FREQ * df)
|
||||
|
||||
V2 = v_tol**2
|
||||
X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.)
|
||||
a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X)
|
||||
a2 = (.5 - a1) * K
|
||||
a3 = a2 * K
|
||||
a4 = a1 * K * K * K
|
||||
|
||||
A = [a1, a2, a3, a4]
|
||||
T = [0., .5*t_d, t_d, 1.5*t_d]
|
||||
return (A, T, "2-hump EI")
|
||||
|
||||
def get_3hump_ei_shaper():
|
||||
v_tol = 0.05 # vibration tolerance
|
||||
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
|
||||
K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df)
|
||||
t_d = 1. / (SHAPER_FREQ * df)
|
||||
|
||||
K2 = K*K
|
||||
a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol))
|
||||
a2 = 0.25 * (1. - v_tol) * K
|
||||
a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2
|
||||
a4 = a2 * K2
|
||||
a5 = a1 * K2 * K2
|
||||
|
||||
A = [a1, a2, a3, a4, a5]
|
||||
T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d]
|
||||
return (A, T, "3-hump EI")
|
||||
|
||||
|
||||
def estimate_shaper(shaper, freq, damping_ratio):
|
||||
A, T, _ = shaper
|
||||
n = len(T)
|
||||
inv_D = 1. / sum(A)
|
||||
omega = 2. * math.pi * freq
|
||||
damping = damping_ratio * omega
|
||||
omega_d = omega * math.sqrt(1. - damping_ratio**2)
|
||||
S = C = 0
|
||||
for i in range(n):
|
||||
W = A[i] * math.exp(-damping * (T[-1] - T[i]))
|
||||
S += W * math.sin(omega_d * T[i])
|
||||
C += W * math.cos(omega_d * T[i])
|
||||
return math.sqrt(S*S + C*C) * inv_D
|
||||
|
||||
def shift_pulses(shaper):
|
||||
A, T, name = shaper
|
||||
n = len(T)
|
||||
ts = sum([A[i] * T[i] for i in range(n)]) / sum(A)
|
||||
for i in range(n):
|
||||
T[i] -= ts
|
||||
|
||||
# Shaper selection
|
||||
get_shaper = get_ei_shaper
|
||||
|
||||
|
||||
######################################################################
|
||||
# Plotting and startup
|
||||
######################################################################
|
||||
|
||||
def bisect(func, left, right):
|
||||
lhs_sign = math.copysign(1., func(left))
|
||||
while right-left > 1e-8:
|
||||
mid = .5 * (left + right)
|
||||
val = func(mid)
|
||||
if math.copysign(1., val) == lhs_sign:
|
||||
left = mid
|
||||
else:
|
||||
right = mid
|
||||
return .5 * (left + right)
|
||||
|
||||
def find_shaper_plot_range(shaper, vib_tol):
|
||||
def eval_shaper(freq):
|
||||
return estimate_shaper(shaper, freq, DAMPING_RATIOS[0]) - vib_tol
|
||||
if not PLOT_FREQ_RANGE:
|
||||
left = bisect(eval_shaper, 0., SHAPER_FREQ)
|
||||
right = bisect(eval_shaper, SHAPER_FREQ, 2.4 * SHAPER_FREQ)
|
||||
else:
|
||||
left, right = PLOT_FREQ_RANGE
|
||||
return (left, right)
|
||||
|
||||
def gen_shaper_response(shaper):
|
||||
# Calculate shaper vibration responce on a range of requencies
|
||||
response = []
|
||||
freqs = []
|
||||
freq, freq_end = find_shaper_plot_range(shaper, vib_tol=0.25)
|
||||
while freq <= freq_end:
|
||||
vals = []
|
||||
for damping_ratio in DAMPING_RATIOS:
|
||||
vals.append(estimate_shaper(shaper, freq, damping_ratio))
|
||||
response.append(vals)
|
||||
freqs.append(freq)
|
||||
freq += PLOT_FREQ_STEP
|
||||
legend = ['damping ratio = %.3f' % d_r for d_r in DAMPING_RATIOS]
|
||||
return freqs, response, legend
|
||||
|
||||
def gen_shaped_step_function(shaper):
|
||||
# Calculate shaping of a step function
|
||||
A, T, _ = shaper
|
||||
inv_D = 1. / sum(A)
|
||||
n = len(T)
|
||||
|
||||
omega = 2. * math.pi * STEP_SIMULATION_RESONANCE_FREQ
|
||||
damping = STEP_SIMULATION_DAMPING_RATIO * omega
|
||||
omega_d = omega * math.sqrt(1. - STEP_SIMULATION_DAMPING_RATIO**2)
|
||||
phase = math.acos(STEP_SIMULATION_DAMPING_RATIO)
|
||||
|
||||
t_start = T[0] - .5 / SHAPER_FREQ
|
||||
t_end = T[-1] + 1.5 / STEP_SIMULATION_RESONANCE_FREQ
|
||||
result = []
|
||||
time = []
|
||||
t = t_start
|
||||
|
||||
def step_response(t):
|
||||
if t < 0.:
|
||||
return 0.
|
||||
return 1. - math.exp(-damping * t) * math.sin(omega_d * t
|
||||
+ phase) / math.sin(phase)
|
||||
|
||||
while t <= t_end:
|
||||
val = []
|
||||
val.append(1. if t >= 0. else 0.)
|
||||
#val.append(step_response(t))
|
||||
|
||||
commanded = 0.
|
||||
response = 0.
|
||||
S = C = 0
|
||||
for i in range(n):
|
||||
if t < T[i]:
|
||||
continue
|
||||
commanded += A[i]
|
||||
response += A[i] * step_response(t - T[i])
|
||||
val.append(commanded * inv_D)
|
||||
val.append(response * inv_D)
|
||||
|
||||
result.append(val)
|
||||
time.append(t)
|
||||
t += .01 / SHAPER_FREQ
|
||||
legend = ['step', 'shaper commanded', 'system response']
|
||||
return time, result, legend
|
||||
|
||||
|
||||
def plot_shaper(shaper):
|
||||
shift_pulses(shaper)
|
||||
freqs, response, response_legend = gen_shaper_response(shaper)
|
||||
time, step_vals, step_legend = gen_shaped_step_function(shaper)
|
||||
|
||||
fig, (ax1, ax2) = matplotlib.pyplot.subplots(nrows=2, figsize=(10,9))
|
||||
ax1.set_title("Vibration response simulation for shaper '%s',\n"
|
||||
"shaper_freq=%.1f Hz, damping_ratio=%.3f"
|
||||
% (shaper[-1], SHAPER_FREQ, SHAPER_DAMPING_RATIO))
|
||||
ax1.plot(freqs, response)
|
||||
ax1.set_ylim(bottom=0.)
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax1.legend(response_legend, loc='best', prop=fontP)
|
||||
ax1.set_xlabel('Resonance frequency, Hz')
|
||||
ax1.set_ylabel('Remaining vibrations, ratio')
|
||||
ax1.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax1.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax1.grid(which='major', color='grey')
|
||||
ax1.grid(which='minor', color='lightgrey')
|
||||
|
||||
ax2.set_title("Unit step input, resonance frequency=%.1f Hz, "
|
||||
"damping ratio=%.3f" % (STEP_SIMULATION_RESONANCE_FREQ,
|
||||
STEP_SIMULATION_DAMPING_RATIO))
|
||||
ax2.plot(time, step_vals)
|
||||
ax2.legend(step_legend, loc='best', prop=fontP)
|
||||
ax2.set_xlabel('Time, sec')
|
||||
ax2.set_ylabel('Amplitude')
|
||||
ax2.grid()
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
def setup_matplotlib(output_to_file):
|
||||
global matplotlib
|
||||
if output_to_file:
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||
import matplotlib.ticker
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
usage = "%prog [options]"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-o", "--output", type="string", dest="output",
|
||||
default=None, help="filename of output graph")
|
||||
options, args = opts.parse_args()
|
||||
if len(args) != 0:
|
||||
opts.error("Incorrect number of arguments")
|
||||
|
||||
# Draw graph
|
||||
setup_matplotlib(options.output is not None)
|
||||
fig = plot_shaper(get_shaper())
|
||||
|
||||
# Show graph
|
||||
if options.output is None:
|
||||
matplotlib.pyplot.show()
|
||||
else:
|
||||
fig.set_size_inches(8, 6)
|
||||
fig.savefig(options.output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
168
scripts/graph_temp_sensor.py
Normal file
168
scripts/graph_temp_sensor.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python2
|
||||
# Tool to graph temperature sensor ADC resolution
|
||||
#
|
||||
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import sys, os, optparse
|
||||
import matplotlib
|
||||
|
||||
|
||||
######################################################################
|
||||
# Dummy config / printer / etc. class emulation
|
||||
######################################################################
|
||||
|
||||
class DummyConfig:
|
||||
def __init__(self, config_settings):
|
||||
self.config_settings = config_settings
|
||||
self.sensor_factories = {}
|
||||
# Emulate config class
|
||||
def getfloat(self, option, default, **kw):
|
||||
return self.config_settings.get(option, default)
|
||||
def get(self, option, default=None):
|
||||
return default
|
||||
def get_printer(self):
|
||||
return self
|
||||
def get_name(self):
|
||||
return "dummy"
|
||||
# Emulate printer class
|
||||
def load_object(self, config, name):
|
||||
return self
|
||||
def lookup_object(self, name):
|
||||
return self
|
||||
# Emulate heaters class
|
||||
def add_sensor_factory(self, name, factory):
|
||||
self.sensor_factories[name] = factory
|
||||
def do_create_sensor(self, sensor_type):
|
||||
return self.sensor_factories[sensor_type](self).adc_convert
|
||||
# Emulate query_adc class
|
||||
def register_adc(self, name, klass):
|
||||
pass
|
||||
# Emulate pins class
|
||||
def setup_pin(self, pin_type, pin_name):
|
||||
return self
|
||||
# Emulate mcu_adc class
|
||||
def setup_adc_callback(self, time, callback):
|
||||
pass
|
||||
|
||||
|
||||
######################################################################
|
||||
# Plotting
|
||||
######################################################################
|
||||
|
||||
def plot_adc_resolution(config, sensors):
|
||||
# Temperature list
|
||||
all_temps = [float(i) for i in range(1, 351)]
|
||||
temps = all_temps[:-1]
|
||||
# Build plot
|
||||
fig, (ax1, ax2) = matplotlib.pyplot.subplots(nrows=2, sharex=True)
|
||||
pullup = config.getfloat('pullup_resistor', 0.)
|
||||
adc_voltage = config.getfloat('adc_voltage', 0.)
|
||||
ax1.set_title("Temperature Sensor (pullup=%.0f, adc_voltage=%.3f)"
|
||||
% (pullup, adc_voltage))
|
||||
ax1.set_ylabel('ADC')
|
||||
ax2.set_ylabel('ADC change per 1C')
|
||||
for sensor in sensors:
|
||||
sc = config.do_create_sensor(sensor)
|
||||
adcs = [sc.calc_adc(t) for t in all_temps]
|
||||
ax1.plot(temps, adcs[:-1], label=sensor, alpha=0.6)
|
||||
adc_deltas = [abs(adcs[i+1] - adcs[i]) for i in range(len(temps))]
|
||||
ax2.plot(temps, adc_deltas, alpha=0.6)
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax1.legend(loc='best', prop=fontP)
|
||||
ax2.set_xlabel('Temperature (C)')
|
||||
ax1.grid(True)
|
||||
ax2.grid(True)
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
def plot_resistance(config, sensors):
|
||||
# Temperature list
|
||||
all_temps = [float(i) for i in range(1, 351)]
|
||||
# Build plot
|
||||
fig, ax = matplotlib.pyplot.subplots()
|
||||
pullup = config.getfloat('pullup_resistor', 0.)
|
||||
ax.set_title("Temperature Sensor (pullup=%.0f)" % (pullup,))
|
||||
ax.set_ylabel('Resistance (Ohms)')
|
||||
for sensor in sensors:
|
||||
sc = config.do_create_sensor(sensor)
|
||||
adcs = [sc.calc_adc(t) for t in all_temps]
|
||||
rs = [pullup * adc / (1.0 - adc) for adc in adcs]
|
||||
ax.plot(all_temps, rs, label=sensor, alpha=0.6)
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax.legend(loc='best', prop=fontP)
|
||||
ax.set_xlabel('Temperature (C)')
|
||||
ax.grid(True)
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
|
||||
######################################################################
|
||||
# Startup
|
||||
######################################################################
|
||||
|
||||
def setup_matplotlib(output_to_file):
|
||||
global matplotlib
|
||||
if output_to_file:
|
||||
matplotlib.rcParams.update({'figure.autolayout': True})
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||
import matplotlib.ticker
|
||||
|
||||
def import_sensors(config):
|
||||
global extras
|
||||
# Load adc_temperature.py and thermistor.py modules
|
||||
kdir = os.path.join(os.path.dirname(__file__), '..', 'klippy')
|
||||
sys.path.append(kdir)
|
||||
import extras.adc_temperature, extras.thermistor
|
||||
extras.thermistor.load_config(config)
|
||||
extras.adc_temperature.load_config(config)
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
usage = "%prog [options]"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-o", "--output", type="string", dest="output",
|
||||
default=None, help="filename of output graph")
|
||||
opts.add_option("-p", "--pullup", type="float", dest="pullup",
|
||||
default=4700., help="pullup resistor")
|
||||
opts.add_option("-v", "--voltage", type="float", dest="voltage",
|
||||
default=5., help="pullup resistor")
|
||||
opts.add_option("-s", "--sensors", type="string", dest="sensors",
|
||||
default="", help="list of sensors (comma separated)")
|
||||
opts.add_option("-r", "--resistance", action="store_true",
|
||||
help="graph sensor resistance")
|
||||
options, args = opts.parse_args()
|
||||
if len(args) != 0:
|
||||
opts.error("Incorrect number of arguments")
|
||||
|
||||
# Import sensors
|
||||
config_settings = {'pullup_resistor': options.pullup,
|
||||
'adc_voltage': options.voltage}
|
||||
config = DummyConfig(config_settings)
|
||||
import_sensors(config)
|
||||
|
||||
# Determine sensors to graph
|
||||
if options.sensors:
|
||||
sensors = [s.strip() for s in options.sensors.split(',')]
|
||||
else:
|
||||
sensors = sorted(config.sensor_factories.keys())
|
||||
|
||||
# Draw graph
|
||||
setup_matplotlib(options.output is not None)
|
||||
if options.resistance:
|
||||
fig = plot_resistance(config, sensors)
|
||||
else:
|
||||
fig = plot_adc_resolution(config, sensors)
|
||||
|
||||
# Show graph
|
||||
if options.output is None:
|
||||
matplotlib.pyplot.show()
|
||||
else:
|
||||
fig.set_size_inches(8, 6)
|
||||
fig.savefig(options.output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
303
scripts/graphstats.py
Normal file
303
scripts/graphstats.py
Normal file
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python
|
||||
# Script to parse a logging file, extract the stats, and graph them
|
||||
#
|
||||
# Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import optparse, datetime
|
||||
import matplotlib
|
||||
|
||||
MAXBANDWIDTH=25000.
|
||||
MAXBUFFER=2.
|
||||
STATS_INTERVAL=5.
|
||||
TASK_MAX=0.0025
|
||||
|
||||
APPLY_PREFIX = [
|
||||
'mcu_awake', 'mcu_task_avg', 'mcu_task_stddev', 'bytes_write',
|
||||
'bytes_read', 'bytes_retransmit', 'freq', 'adj',
|
||||
'target', 'temp', 'pwm'
|
||||
]
|
||||
|
||||
def parse_log(logname, mcu):
|
||||
if mcu is None:
|
||||
mcu = "mcu"
|
||||
mcu_prefix = mcu + ":"
|
||||
apply_prefix = { p: 1 for p in APPLY_PREFIX }
|
||||
f = open(logname, 'r')
|
||||
out = []
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if not parts or parts[0] not in ('Stats', 'INFO:root:Stats'):
|
||||
#if parts and parts[0] == 'INFO:root:shutdown:':
|
||||
# break
|
||||
continue
|
||||
prefix = ""
|
||||
keyparts = {}
|
||||
for p in parts[2:]:
|
||||
if '=' not in p:
|
||||
prefix = p
|
||||
if prefix == mcu_prefix:
|
||||
prefix = ''
|
||||
continue
|
||||
name, val = p.split('=', 1)
|
||||
if name in apply_prefix:
|
||||
name = prefix + name
|
||||
keyparts[name] = val
|
||||
if 'print_time' not in keyparts:
|
||||
continue
|
||||
keyparts['#sampletime'] = float(parts[1][:-1])
|
||||
out.append(keyparts)
|
||||
f.close()
|
||||
return out
|
||||
|
||||
def setup_matplotlib(output_to_file):
|
||||
global matplotlib
|
||||
if output_to_file:
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||
import matplotlib.ticker
|
||||
|
||||
def find_print_restarts(data):
|
||||
runoff_samples = {}
|
||||
last_runoff_start = last_buffer_time = last_sampletime = 0.
|
||||
last_print_stall = 0
|
||||
for d in reversed(data):
|
||||
# Check for buffer runoff
|
||||
sampletime = d['#sampletime']
|
||||
buffer_time = float(d.get('buffer_time', 0.))
|
||||
if (last_runoff_start and last_sampletime - sampletime < 5
|
||||
and buffer_time > last_buffer_time):
|
||||
runoff_samples[last_runoff_start][1].append(sampletime)
|
||||
elif buffer_time < 1.:
|
||||
last_runoff_start = sampletime
|
||||
runoff_samples[last_runoff_start] = [False, [sampletime]]
|
||||
else:
|
||||
last_runoff_start = 0.
|
||||
last_buffer_time = buffer_time
|
||||
last_sampletime = sampletime
|
||||
# Check for print stall
|
||||
print_stall = int(d['print_stall'])
|
||||
if print_stall < last_print_stall:
|
||||
if last_runoff_start:
|
||||
runoff_samples[last_runoff_start][0] = True
|
||||
last_print_stall = print_stall
|
||||
sample_resets = {sampletime: 1 for stall, samples in runoff_samples.values()
|
||||
for sampletime in samples if not stall}
|
||||
return sample_resets
|
||||
|
||||
def plot_mcu(data, maxbw):
|
||||
# Generate data for plot
|
||||
basetime = lasttime = data[0]['#sampletime']
|
||||
lastbw = float(data[0]['bytes_write']) + float(data[0]['bytes_retransmit'])
|
||||
sample_resets = find_print_restarts(data)
|
||||
times = []
|
||||
bwdeltas = []
|
||||
loads = []
|
||||
awake = []
|
||||
hostbuffers = []
|
||||
for d in data:
|
||||
st = d['#sampletime']
|
||||
timedelta = st - lasttime
|
||||
if timedelta <= 0.:
|
||||
continue
|
||||
bw = float(d['bytes_write']) + float(d['bytes_retransmit'])
|
||||
if bw < lastbw:
|
||||
lastbw = bw
|
||||
continue
|
||||
load = float(d['mcu_task_avg']) + 3*float(d['mcu_task_stddev'])
|
||||
if st - basetime < 15.:
|
||||
load = 0.
|
||||
pt = float(d['print_time'])
|
||||
hb = float(d['buffer_time'])
|
||||
if hb >= MAXBUFFER or st in sample_resets:
|
||||
hb = 0.
|
||||
else:
|
||||
hb = 100. * (MAXBUFFER - hb) / MAXBUFFER
|
||||
hostbuffers.append(hb)
|
||||
times.append(datetime.datetime.utcfromtimestamp(st))
|
||||
bwdeltas.append(100. * (bw - lastbw) / (maxbw * timedelta))
|
||||
loads.append(100. * load / TASK_MAX)
|
||||
awake.append(100. * float(d.get('mcu_awake', 0.)) / STATS_INTERVAL)
|
||||
lasttime = st
|
||||
lastbw = bw
|
||||
|
||||
# Build plot
|
||||
fig, ax1 = matplotlib.pyplot.subplots()
|
||||
ax1.set_title("MCU bandwidth and load utilization")
|
||||
ax1.set_xlabel('Time')
|
||||
ax1.set_ylabel('Usage (%)')
|
||||
ax1.plot_date(times, bwdeltas, 'g', label='Bandwidth', alpha=0.8)
|
||||
ax1.plot_date(times, loads, 'r', label='MCU load', alpha=0.8)
|
||||
ax1.plot_date(times, hostbuffers, 'c', label='Host buffer', alpha=0.8)
|
||||
ax1.plot_date(times, awake, 'y', label='Awake time', alpha=0.6)
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax1.legend(loc='best', prop=fontP)
|
||||
ax1.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%H:%M'))
|
||||
ax1.grid(True)
|
||||
return fig
|
||||
|
||||
def plot_system(data):
|
||||
# Generate data for plot
|
||||
lasttime = data[0]['#sampletime']
|
||||
lastcputime = float(data[0]['cputime'])
|
||||
times = []
|
||||
sysloads = []
|
||||
cputimes = []
|
||||
memavails = []
|
||||
for d in data:
|
||||
st = d['#sampletime']
|
||||
timedelta = st - lasttime
|
||||
if timedelta <= 0.:
|
||||
continue
|
||||
lasttime = st
|
||||
times.append(datetime.datetime.utcfromtimestamp(st))
|
||||
cputime = float(d['cputime'])
|
||||
cpudelta = max(0., min(1.5, (cputime - lastcputime) / timedelta))
|
||||
lastcputime = cputime
|
||||
cputimes.append(cpudelta * 100.)
|
||||
sysloads.append(float(d['sysload']) * 100.)
|
||||
memavails.append(float(d['memavail']))
|
||||
|
||||
# Build plot
|
||||
fig, ax1 = matplotlib.pyplot.subplots()
|
||||
ax1.set_title("System load utilization")
|
||||
ax1.set_xlabel('Time')
|
||||
ax1.set_ylabel('Load (% of a core)')
|
||||
ax1.plot_date(times, sysloads, '-', label='system load',
|
||||
color='cyan', alpha=0.8)
|
||||
ax1.plot_date(times, cputimes, '-', label='process time',
|
||||
color='red', alpha=0.8)
|
||||
ax2 = ax1.twinx()
|
||||
ax2.set_ylabel('Available memory (KB)')
|
||||
ax2.plot_date(times, memavails, '-', label='system memory',
|
||||
color='yellow', alpha=0.3)
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax1li, ax1la = ax1.get_legend_handles_labels()
|
||||
ax2li, ax2la = ax2.get_legend_handles_labels()
|
||||
ax1.legend(ax1li + ax2li, ax1la + ax2la, loc='best', prop=fontP)
|
||||
ax1.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%H:%M'))
|
||||
ax1.grid(True)
|
||||
return fig
|
||||
|
||||
def plot_frequency(data, mcu):
|
||||
all_keys = {}
|
||||
for d in data:
|
||||
all_keys.update(d)
|
||||
one_mcu = mcu is not None
|
||||
graph_keys = { key: ([], []) for key in all_keys
|
||||
if (key in ("freq", "adj") or (not one_mcu and (
|
||||
key.endswith(":freq") or key.endswith(":adj")))) }
|
||||
for d in data:
|
||||
st = datetime.datetime.utcfromtimestamp(d['#sampletime'])
|
||||
for key, (times, values) in graph_keys.items():
|
||||
val = d.get(key)
|
||||
if val not in (None, '0', '1'):
|
||||
times.append(st)
|
||||
values.append(float(val))
|
||||
|
||||
# Build plot
|
||||
fig, ax1 = matplotlib.pyplot.subplots()
|
||||
if one_mcu:
|
||||
ax1.set_title("MCU '%s' frequency" % (mcu,))
|
||||
else:
|
||||
ax1.set_title("MCU frequency")
|
||||
ax1.set_xlabel('Time')
|
||||
ax1.set_ylabel('Frequency')
|
||||
for key in sorted(graph_keys):
|
||||
times, values = graph_keys[key]
|
||||
ax1.plot_date(times, values, '.', label=key)
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax1.legend(loc='best', prop=fontP)
|
||||
ax1.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%H:%M'))
|
||||
ax1.yaxis.set_major_formatter(matplotlib.ticker.FormatStrFormatter('%d'))
|
||||
ax1.grid(True)
|
||||
return fig
|
||||
|
||||
def plot_temperature(data, heaters):
|
||||
fig, ax1 = matplotlib.pyplot.subplots()
|
||||
ax2 = ax1.twinx()
|
||||
for heater in heaters.split(','):
|
||||
heater = heater.strip()
|
||||
temp_key = heater + ':' + 'temp'
|
||||
target_key = heater + ':' + 'target'
|
||||
pwm_key = heater + ':' + 'pwm'
|
||||
times = []
|
||||
temps = []
|
||||
targets = []
|
||||
pwm = []
|
||||
for d in data:
|
||||
temp = d.get(temp_key)
|
||||
if temp is None:
|
||||
continue
|
||||
times.append(datetime.datetime.utcfromtimestamp(d['#sampletime']))
|
||||
temps.append(float(temp))
|
||||
pwm.append(float(d.get(pwm_key, 0.)))
|
||||
targets.append(float(d.get(target_key, 0.)))
|
||||
ax1.plot_date(times, temps, '-', label='%s temp' % (heater,), alpha=0.8)
|
||||
if any(targets):
|
||||
label = '%s target' % (heater,)
|
||||
ax1.plot_date(times, targets, '-', label=label, alpha=0.3)
|
||||
if any(pwm):
|
||||
label = '%s pwm' % (heater,)
|
||||
ax2.plot_date(times, pwm, '-', label=label, alpha=0.2)
|
||||
# Build plot
|
||||
ax1.set_title("Temperature of %s" % (heaters,))
|
||||
ax1.set_xlabel('Time')
|
||||
ax1.set_ylabel('Temperature')
|
||||
ax2.set_ylabel('pwm')
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax1li, ax1la = ax1.get_legend_handles_labels()
|
||||
ax2li, ax2la = ax2.get_legend_handles_labels()
|
||||
ax1.legend(ax1li + ax2li, ax1la + ax2la, loc='best', prop=fontP)
|
||||
ax1.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%H:%M'))
|
||||
ax1.grid(True)
|
||||
return fig
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
usage = "%prog [options] <logfile>"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-f", "--frequency", action="store_true",
|
||||
help="graph mcu frequency")
|
||||
opts.add_option("-s", "--system", action="store_true",
|
||||
help="graph system load")
|
||||
opts.add_option("-o", "--output", type="string", dest="output",
|
||||
default=None, help="filename of output graph")
|
||||
opts.add_option("-t", "--temperature", type="string", dest="heater",
|
||||
default=None, help="graph heater temperature")
|
||||
opts.add_option("-m", "--mcu", type="string", dest="mcu", default=None,
|
||||
help="limit stats to the given mcu")
|
||||
options, args = opts.parse_args()
|
||||
if len(args) != 1:
|
||||
opts.error("Incorrect number of arguments")
|
||||
logname = args[0]
|
||||
|
||||
# Parse data
|
||||
data = parse_log(logname, options.mcu)
|
||||
if not data:
|
||||
return
|
||||
|
||||
# Draw graph
|
||||
setup_matplotlib(options.output is not None)
|
||||
if options.heater is not None:
|
||||
fig = plot_temperature(data, options.heater)
|
||||
elif options.frequency:
|
||||
fig = plot_frequency(data, options.mcu)
|
||||
elif options.system:
|
||||
fig = plot_system(data)
|
||||
else:
|
||||
fig = plot_mcu(data, MAXBANDWIDTH)
|
||||
|
||||
# Show graph
|
||||
if options.output is None:
|
||||
matplotlib.pyplot.show()
|
||||
else:
|
||||
fig.set_size_inches(8, 6)
|
||||
fig.savefig(options.output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
102
scripts/install-arch.sh
Normal file
102
scripts/install-arch.sh
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
# This script installs Klipper on an Arch Linux system
|
||||
|
||||
PYTHONDIR="${HOME}/klippy-env"
|
||||
SYSTEMDDIR="/etc/systemd/system"
|
||||
AURCLIENT="pamac"
|
||||
KLIPPER_USER=$USER
|
||||
KLIPPER_GROUP=$KLIPPER_USER
|
||||
|
||||
# Step 1: Install system packages
|
||||
install_packages()
|
||||
{
|
||||
# Packages for python cffi
|
||||
PKGLIST="python2-virtualenv libffi base-devel"
|
||||
# kconfig requirements
|
||||
PKGLIST="${PKGLIST} ncurses"
|
||||
# hub-ctrl
|
||||
PKGLIST="${PKGLIST} libusb"
|
||||
# AVR chip installation and building
|
||||
PKGLIST="${PKGLIST} avrdude avr-gcc avr-binutils avr-libc"
|
||||
# ARM chip installation and building
|
||||
AURLIST="stm32flash"
|
||||
PKGLIST="${PKGLIST} arm-none-eabi-newlib"
|
||||
PKGLIST="${PKGLIST} arm-none-eabi-gcc arm-none-eabi-binutils"
|
||||
|
||||
# Install desired packages
|
||||
report_status "Installing packages..."
|
||||
sudo pacman -S ${PKGLIST}
|
||||
$AURCLIENT build ${AURLIST}
|
||||
}
|
||||
|
||||
# Step 2: Create python virtual environment
|
||||
create_virtualenv()
|
||||
{
|
||||
report_status "Updating python virtual environment..."
|
||||
|
||||
# Create virtualenv if it doesn't already exist
|
||||
[ ! -d ${PYTHONDIR} ] && virtualenv2 ${PYTHONDIR}
|
||||
|
||||
# Install/update dependencies
|
||||
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt
|
||||
}
|
||||
|
||||
# Step 3: Install startup script
|
||||
install_script()
|
||||
{
|
||||
# Create systemd service file
|
||||
KLIPPER_LOG=/tmp/klippy.log
|
||||
report_status "Installing system start script..."
|
||||
sudo /bin/sh -c "cat > $SYSTEMDDIR/klipper.service" << EOF
|
||||
#Systemd service file for klipper
|
||||
[Unit]
|
||||
Description=Starts klipper on startup
|
||||
After=network.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$KLIPPER_USER
|
||||
RemainAfterExit=yes
|
||||
ExecStart=${PYTHONDIR}/bin/python ${SRCDIR}/klippy/klippy.py ${HOME}/printer.cfg -l ${KLIPPER_LOG}
|
||||
EOF
|
||||
# Use systemctl to enable the klipper systemd service script
|
||||
sudo systemctl enable klipper.service
|
||||
report_status "Make sure to add $KLIPPER_USER to the user group controlling your serial printer port"
|
||||
}
|
||||
|
||||
# Step 4: Start host software
|
||||
start_software()
|
||||
{
|
||||
report_status "Launching Klipper host software..."
|
||||
sudo systemctl start klipper
|
||||
}
|
||||
|
||||
# Helper functions
|
||||
report_status()
|
||||
{
|
||||
echo -e "\n\n###### $1"
|
||||
}
|
||||
|
||||
verify_ready()
|
||||
{
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "This script must not run as root"
|
||||
exit -1
|
||||
fi
|
||||
}
|
||||
|
||||
# Force script to exit if an error occurs
|
||||
set -e
|
||||
|
||||
# Find SRCDIR from the pathname of this script
|
||||
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
|
||||
# Run installation steps defined above
|
||||
verify_ready
|
||||
install_packages
|
||||
create_virtualenv
|
||||
install_script
|
||||
start_software
|
||||
73
scripts/install-beaglebone.sh
Normal file
73
scripts/install-beaglebone.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# This script installs Klipper on a Beaglebone running Debian Jessie
|
||||
# for use with its PRU micro-controller.
|
||||
|
||||
# Step 1: Do main install
|
||||
install_main()
|
||||
{
|
||||
# Run the debian script - should
|
||||
# work.
|
||||
${SRCDIR}/scripts/install-debian.sh
|
||||
}
|
||||
|
||||
# Step 2: Install additional system packages
|
||||
install_packages()
|
||||
{
|
||||
# Install desired packages
|
||||
PKGLIST="gcc-pru"
|
||||
|
||||
report_status "Installing beaglebone packages..."
|
||||
sudo apt-get install --yes ${PKGLIST}
|
||||
}
|
||||
|
||||
# Step 3: Install startup script
|
||||
install_script()
|
||||
{
|
||||
report_status "Installing pru start script..."
|
||||
sudo cp "${SRCDIR}/scripts/klipper-pru-start.sh" /etc/init.d/klipper_pru
|
||||
sudo update-rc.d klipper_pru defaults
|
||||
}
|
||||
|
||||
# Step 4: Install pru udev rule
|
||||
install_udev()
|
||||
{
|
||||
report_status "Installing pru udev rule..."
|
||||
sudo /bin/sh -c "cat > /etc/udev/rules.d/pru.rules" <<EOF
|
||||
KERNEL=="rpmsg_pru30", GROUP="tty", MODE="0660"
|
||||
EOF
|
||||
}
|
||||
|
||||
# Step 5: Add user to tty group
|
||||
install_groups()
|
||||
{
|
||||
report_status "Adding $USER to tty group..."
|
||||
sudo adduser $USER tty
|
||||
}
|
||||
|
||||
# Helper functions
|
||||
report_status()
|
||||
{
|
||||
echo -e "\n\n###### $1"
|
||||
}
|
||||
|
||||
verify_ready()
|
||||
{
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "This script must not run as root"
|
||||
exit -1
|
||||
fi
|
||||
}
|
||||
|
||||
# Force script to exit if an error occurs
|
||||
set -e
|
||||
|
||||
# Find SRCDIR from the pathname of this script
|
||||
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
|
||||
# Run installation steps defined above
|
||||
verify_ready
|
||||
install_main
|
||||
install_packages
|
||||
install_script
|
||||
install_udev
|
||||
install_groups
|
||||
101
scripts/install-centos.sh
Normal file
101
scripts/install-centos.sh
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
# This script installs Klipper on an x86_64 machine running the
|
||||
# CentOS 7 distribution.
|
||||
|
||||
PYTHONDIR="${HOME}/klippy-env"
|
||||
SYSTEMDDIR="/etc/systemd/system"
|
||||
|
||||
# Step 1: Install system packages
|
||||
install_packages()
|
||||
{
|
||||
# Packages for python cffi
|
||||
PKGLIST="python-virtualenv libffi-devel"
|
||||
# kconfig requirements
|
||||
PKGLIST="${PKGLIST} ncurses-devel"
|
||||
# hub-ctrl
|
||||
PKGLIST="${PKGLIST} libusb-devel"
|
||||
# AVR chip installation and building
|
||||
PKGLIST="${PKGLIST} avrdude gcc-avr32-linux-gnu binutils-avr32-linux-gnu avr-libc"
|
||||
# ARM chip installation and building
|
||||
# CentOS/Fedora do not appear to have these packages available at this time
|
||||
PKGLIST="${PKGLIST} arm-none-eabi-gcc-cs arm-none-eabi-newlib"
|
||||
|
||||
# Install desired packages
|
||||
report_status "Installing packages..."
|
||||
sudo yum install -y ${PKGLIST}
|
||||
}
|
||||
|
||||
# Step 2: Create python virtual environment
|
||||
create_virtualenv()
|
||||
{
|
||||
report_status "Updating python virtual environment..."
|
||||
|
||||
# Create virtualenv if it doesn't already exist
|
||||
[ ! -d ${PYTHONDIR} ] && virtualenv -p python2 ${PYTHONDIR}
|
||||
|
||||
# Install/update dependencies
|
||||
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt
|
||||
}
|
||||
|
||||
# Step 3: Install startup script
|
||||
install_script()
|
||||
{
|
||||
# Create systemd service file
|
||||
report_status "Installing system start script..."
|
||||
sudo /bin/sh -c "cat > $SYSTEMDDIR/klipper.service" << EOF
|
||||
#Systemd service file for klipper
|
||||
[Unit]
|
||||
Description=Starts klipper on startup
|
||||
After=network.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$USER
|
||||
RemainAfterExit=yes
|
||||
ExecStart=${PYTHONDIR}/bin/python ${SRCDIR}/klippy/klippy.py ${HOME}/printer.cfg -l /var/log/klippy.log
|
||||
EOF
|
||||
# Use systemctl to enable the klipper systemd service script
|
||||
sudo systemctl enable klipper.service
|
||||
}
|
||||
|
||||
# Configuration for systemctl klipper
|
||||
|
||||
KLIPPY_USER=$USER
|
||||
|
||||
|
||||
# Step 5: Start host software
|
||||
start_software()
|
||||
{
|
||||
report_status "Launching Klipper host software..."
|
||||
sudo systemctl restart klipper
|
||||
}
|
||||
|
||||
# Helper functions
|
||||
report_status()
|
||||
{
|
||||
echo -e "\n\n###### $1"
|
||||
}
|
||||
|
||||
verify_ready()
|
||||
{
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "This script must not run as root"
|
||||
exit -1
|
||||
fi
|
||||
}
|
||||
|
||||
# Force script to exit if an error occurs
|
||||
set -e
|
||||
|
||||
# Find SRCDIR from the pathname of this script
|
||||
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
|
||||
# Run installation steps defined above
|
||||
verify_ready
|
||||
install_packages
|
||||
create_virtualenv
|
||||
install_script
|
||||
start_software
|
||||
105
scripts/install-debian.sh
Normal file
105
scripts/install-debian.sh
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
# This script installs Klipper on an debian
|
||||
#
|
||||
|
||||
PYTHONDIR="${HOME}/klippy-env"
|
||||
SYSTEMDDIR="/etc/systemd/system"
|
||||
KLIPPER_USER=$USER
|
||||
KLIPPER_GROUP=$KLIPPER_USER
|
||||
|
||||
# Step 1: Install system packages
|
||||
install_packages()
|
||||
{
|
||||
# Packages for python cffi
|
||||
PKGLIST="virtualenv python-dev libffi-dev build-essential"
|
||||
# kconfig requirements
|
||||
PKGLIST="${PKGLIST} libncurses-dev"
|
||||
# hub-ctrl
|
||||
PKGLIST="${PKGLIST} libusb-dev"
|
||||
# AVR chip installation and building
|
||||
PKGLIST="${PKGLIST} avrdude gcc-avr binutils-avr avr-libc"
|
||||
# ARM chip installation and building
|
||||
PKGLIST="${PKGLIST} stm32flash libnewlib-arm-none-eabi"
|
||||
PKGLIST="${PKGLIST} gcc-arm-none-eabi binutils-arm-none-eabi libusb-1.0"
|
||||
|
||||
# Update system package info
|
||||
report_status "Running apt-get update..."
|
||||
sudo apt-get update
|
||||
|
||||
# Install desired packages
|
||||
report_status "Installing packages..."
|
||||
sudo apt-get install --yes ${PKGLIST}
|
||||
}
|
||||
|
||||
# Step 2: Create python virtual environment
|
||||
create_virtualenv()
|
||||
{
|
||||
report_status "Updating python virtual environment..."
|
||||
|
||||
# Create virtualenv if it doesn't already exist
|
||||
[ ! -d ${PYTHONDIR} ] && virtualenv -p python2 ${PYTHONDIR}
|
||||
|
||||
# Install/update dependencies
|
||||
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt
|
||||
}
|
||||
|
||||
# Step 3: Install startup script
|
||||
install_script()
|
||||
{
|
||||
# Create systemd service file
|
||||
KLIPPER_LOG=/tmp/klippy.log
|
||||
report_status "Installing system start script..."
|
||||
sudo /bin/sh -c "cat > $SYSTEMDDIR/klipper.service" << EOF
|
||||
#Systemd service file for klipper
|
||||
[Unit]
|
||||
Description=Starts klipper on startup
|
||||
After=network.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$KLIPPER_USER
|
||||
RemainAfterExit=yes
|
||||
ExecStart=${PYTHONDIR}/bin/python ${SRCDIR}/klippy/klippy.py ${HOME}/printer.cfg -l ${KLIPPER_LOG}
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
EOF
|
||||
# Use systemctl to enable the klipper systemd service script
|
||||
sudo systemctl enable klipper.service
|
||||
}
|
||||
|
||||
# Step 4: Start host software
|
||||
start_software()
|
||||
{
|
||||
report_status "Launching Klipper host software..."
|
||||
sudo systemctl start klipper
|
||||
}
|
||||
|
||||
# Helper functions
|
||||
report_status()
|
||||
{
|
||||
echo -e "\n\n###### $1"
|
||||
}
|
||||
|
||||
verify_ready()
|
||||
{
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "This script must not run as root"
|
||||
exit -1
|
||||
fi
|
||||
}
|
||||
|
||||
# Force script to exit if an error occurs
|
||||
set -e
|
||||
|
||||
# Find SRCDIR from the pathname of this script
|
||||
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
|
||||
# Run installation steps defined above
|
||||
verify_ready
|
||||
install_packages
|
||||
create_virtualenv
|
||||
install_script
|
||||
start_software
|
||||
103
scripts/install-octopi.sh
Normal file
103
scripts/install-octopi.sh
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# This script installs Klipper on a Raspberry Pi machine running the
|
||||
# OctoPi distribution.
|
||||
|
||||
PYTHONDIR="${HOME}/klippy-env"
|
||||
|
||||
# Step 1: Install system packages
|
||||
install_packages()
|
||||
{
|
||||
# Packages for python cffi
|
||||
PKGLIST="virtualenv python-dev libffi-dev build-essential"
|
||||
# kconfig requirements
|
||||
PKGLIST="${PKGLIST} libncurses-dev"
|
||||
# hub-ctrl
|
||||
PKGLIST="${PKGLIST} libusb-dev"
|
||||
# AVR chip installation and building
|
||||
PKGLIST="${PKGLIST} avrdude gcc-avr binutils-avr avr-libc"
|
||||
# ARM chip installation and building
|
||||
PKGLIST="${PKGLIST} stm32flash dfu-util libnewlib-arm-none-eabi"
|
||||
PKGLIST="${PKGLIST} gcc-arm-none-eabi binutils-arm-none-eabi libusb-1.0"
|
||||
|
||||
# Update system package info
|
||||
report_status "Running apt-get update..."
|
||||
sudo apt-get update
|
||||
|
||||
# Install desired packages
|
||||
report_status "Installing packages..."
|
||||
sudo apt-get install --yes ${PKGLIST}
|
||||
}
|
||||
|
||||
# Step 2: Create python virtual environment
|
||||
create_virtualenv()
|
||||
{
|
||||
report_status "Updating python virtual environment..."
|
||||
|
||||
# Create virtualenv if it doesn't already exist
|
||||
[ ! -d ${PYTHONDIR} ] && virtualenv -p python2 ${PYTHONDIR}
|
||||
|
||||
# Install/update dependencies
|
||||
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt
|
||||
}
|
||||
|
||||
# Step 3: Install startup script
|
||||
install_script()
|
||||
{
|
||||
report_status "Installing system start script..."
|
||||
sudo cp "${SRCDIR}/scripts/klipper-start.sh" /etc/init.d/klipper
|
||||
sudo update-rc.d klipper defaults
|
||||
}
|
||||
|
||||
# Step 4: Install startup script config
|
||||
install_config()
|
||||
{
|
||||
DEFAULTS_FILE=/etc/default/klipper
|
||||
[ -f $DEFAULTS_FILE ] && return
|
||||
|
||||
report_status "Installing system start configuration..."
|
||||
sudo /bin/sh -c "cat > $DEFAULTS_FILE" <<EOF
|
||||
# Configuration for /etc/init.d/klipper
|
||||
|
||||
KLIPPY_USER=$USER
|
||||
|
||||
KLIPPY_EXEC=${PYTHONDIR}/bin/python
|
||||
|
||||
KLIPPY_ARGS="${SRCDIR}/klippy/klippy.py ${HOME}/printer.cfg -l /tmp/klippy.log"
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Step 5: Start host software
|
||||
start_software()
|
||||
{
|
||||
report_status "Launching Klipper host software..."
|
||||
sudo /etc/init.d/klipper restart
|
||||
}
|
||||
|
||||
# Helper functions
|
||||
report_status()
|
||||
{
|
||||
echo -e "\n\n###### $1"
|
||||
}
|
||||
|
||||
verify_ready()
|
||||
{
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "This script must not run as root"
|
||||
exit -1
|
||||
fi
|
||||
}
|
||||
|
||||
# Force script to exit if an error occurs
|
||||
set -e
|
||||
|
||||
# Find SRCDIR from the pathname of this script
|
||||
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
|
||||
# Run installation steps defined above
|
||||
verify_ready
|
||||
install_packages
|
||||
create_virtualenv
|
||||
install_script
|
||||
install_config
|
||||
start_software
|
||||
102
scripts/install-ubuntu-18.04.sh
Normal file
102
scripts/install-ubuntu-18.04.sh
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
# This script installs Klipper on an Ubuntu 18.04 machine with Octoprint
|
||||
|
||||
PYTHONDIR="${HOME}/klippy-env"
|
||||
SYSTEMDDIR="/etc/systemd/system"
|
||||
KLIPPER_USER=$USER
|
||||
KLIPPER_GROUP=$KLIPPER_USER
|
||||
|
||||
# Step 1: Install system packages
|
||||
install_packages()
|
||||
{
|
||||
# Packages for python cffi
|
||||
PKGLIST="virtualenv python-dev libffi-dev build-essential"
|
||||
# kconfig requirements
|
||||
PKGLIST="${PKGLIST} libncurses-dev"
|
||||
# hub-ctrl
|
||||
PKGLIST="${PKGLIST} libusb-dev"
|
||||
# AVR chip installation and building
|
||||
PKGLIST="${PKGLIST} avrdude gcc-avr binutils-avr avr-libc"
|
||||
# ARM chip installation and building
|
||||
PKGLIST="${PKGLIST} stm32flash libnewlib-arm-none-eabi"
|
||||
PKGLIST="${PKGLIST} gcc-arm-none-eabi binutils-arm-none-eabi libusb-1.0"
|
||||
|
||||
# Update system package info
|
||||
report_status "Running apt-get update..."
|
||||
sudo apt-get update
|
||||
|
||||
# Install desired packages
|
||||
report_status "Installing packages..."
|
||||
sudo apt-get install --yes ${PKGLIST}
|
||||
}
|
||||
|
||||
# Step 2: Create python virtual environment
|
||||
create_virtualenv()
|
||||
{
|
||||
report_status "Updating python virtual environment..."
|
||||
|
||||
# Create virtualenv if it doesn't already exist
|
||||
[ ! -d ${PYTHONDIR} ] && virtualenv -p python2 ${PYTHONDIR}
|
||||
|
||||
# Install/update dependencies
|
||||
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt
|
||||
}
|
||||
|
||||
# Step 3: Install startup script
|
||||
install_script()
|
||||
{
|
||||
# Create systemd service file
|
||||
KLIPPER_LOG=/tmp/klippy.log
|
||||
report_status "Installing system start script..."
|
||||
sudo /bin/sh -c "cat > $SYSTEMDDIR/klipper.service" << EOF
|
||||
#Systemd service file for klipper
|
||||
[Unit]
|
||||
Description=Starts klipper on startup
|
||||
After=network.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$KLIPPER_USER
|
||||
RemainAfterExit=yes
|
||||
ExecStart=${PYTHONDIR}/bin/python ${SRCDIR}/klippy/klippy.py ${HOME}/printer.cfg -l ${KLIPPER_LOG}
|
||||
EOF
|
||||
# Use systemctl to enable the klipper systemd service script
|
||||
sudo systemctl enable klipper.service
|
||||
}
|
||||
|
||||
# Step 4: Start host software
|
||||
start_software()
|
||||
{
|
||||
report_status "Launching Klipper host software..."
|
||||
sudo systemctl start klipper
|
||||
}
|
||||
|
||||
# Helper functions
|
||||
report_status()
|
||||
{
|
||||
echo -e "\n\n###### $1"
|
||||
}
|
||||
|
||||
verify_ready()
|
||||
{
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "This script must not run as root"
|
||||
exit -1
|
||||
fi
|
||||
}
|
||||
|
||||
# Force script to exit if an error occurs
|
||||
set -e
|
||||
|
||||
# Find SRCDIR from the pathname of this script
|
||||
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
|
||||
# Run installation steps defined above
|
||||
verify_ready
|
||||
install_packages
|
||||
create_virtualenv
|
||||
install_script
|
||||
start_software
|
||||
78
scripts/klipper-mcu-start.sh
Normal file
78
scripts/klipper-mcu-start.sh
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
# System startup script to start the MCU Linux firmware
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: klipper_mcu
|
||||
# Required-Start: $local_fs
|
||||
# Required-Stop:
|
||||
# Default-Start: 3 4 5
|
||||
# Default-Stop: 0 1 2 6
|
||||
# Short-Description: Klipper_MCU daemon
|
||||
# Description: Starts the MCU for Klipper.
|
||||
### END INIT INFO
|
||||
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
DESC="klipper_mcu startup"
|
||||
NAME="klipper_mcu"
|
||||
KLIPPER_HOST_MCU=/usr/local/bin/klipper_mcu
|
||||
KLIPPER_HOST_ARGS="-r"
|
||||
PIDFILE=/var/run/klipper_mcu.pid
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
mcu_host_stop()
|
||||
{
|
||||
# Shutdown existing Klipper instance (if applicable). The goal is to
|
||||
# put the GPIO pins in a safe state.
|
||||
if [ -c /tmp/klipper_host_mcu ]; then
|
||||
log_daemon_msg "Attempting to shutdown host mcu..."
|
||||
set -e
|
||||
( echo "FORCE_SHUTDOWN" > /tmp/klipper_host_mcu ) 2> /dev/null || ( log_action_msg "Firmware busy! Please shutdown Klipper and then retry." && exit 1 )
|
||||
sleep 1
|
||||
( echo "FORCE_SHUTDOWN" > /tmp/klipper_host_mcu ) 2> /dev/null || ( log_action_msg "Firmware busy! Please shutdown Klipper and then retry." && exit 1 )
|
||||
sleep 1
|
||||
set +e
|
||||
fi
|
||||
|
||||
log_daemon_msg "Stopping klipper host mcu" $NAME
|
||||
killproc -p $PIDFILE $KLIPPER_HOST_MCU
|
||||
}
|
||||
|
||||
mcu_host_start()
|
||||
{
|
||||
[ -x $KLIPPER_HOST_MCU ] || return
|
||||
|
||||
if [ -c /tmp/klipper_host_mcu ]; then
|
||||
mcu_host_stop
|
||||
fi
|
||||
|
||||
log_daemon_msg "Starting klipper MCU" $NAME
|
||||
start-stop-daemon --start --quiet --exec $KLIPPER_HOST_MCU \
|
||||
--background --pidfile $PIDFILE --make-pidfile \
|
||||
-- $KLIPPER_HOST_ARGS
|
||||
log_end_msg $?
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
mcu_host_start
|
||||
;;
|
||||
stop)
|
||||
mcu_host_stop
|
||||
;;
|
||||
restart)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
reload|force-reload)
|
||||
log_daemon_msg "Reloading configuration not supported" $NAME
|
||||
log_end_msg 1
|
||||
;;
|
||||
status)
|
||||
status_of_proc -p $PIDFILE $KLIPPER_HOST_MCU $NAME && exit 0 || exit $?
|
||||
;;
|
||||
*) log_action_msg "Usage: /etc/init.d/klipper_mcu {start|stop|status|restart|reload|force-reload}"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
119
scripts/klipper-pru-start.sh
Normal file
119
scripts/klipper-pru-start.sh
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/bin/sh
|
||||
# System startup script to start the PRU firmware
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: klipper_pru
|
||||
# Required-Start: $local_fs
|
||||
# Required-Stop:
|
||||
# Default-Start: 3 4 5
|
||||
# Default-Stop: 0 1 2 6
|
||||
# Short-Description: Klipper_PRU daemon
|
||||
# Description: Starts the PRU for Klipper.
|
||||
### END INIT INFO
|
||||
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
DESC="klipper_pru startup"
|
||||
NAME="klipper_pru"
|
||||
KLIPPER_HOST_MCU=/usr/local/bin/klipper_mcu
|
||||
KLIPPER_HOST_ARGS="-w -r"
|
||||
PIDFILE=/var/run/klipper_mcu.pid
|
||||
RPROC0=/sys/class/remoteproc/remoteproc1
|
||||
RPROC1=/sys/class/remoteproc/remoteproc2
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
pru_stop()
|
||||
{
|
||||
# Shutdown existing Klipper instance (if applicable). The goal is to
|
||||
# put the GPIO pins in a safe state.
|
||||
if [ -c /dev/rpmsg_pru30 ]; then
|
||||
log_daemon_msg "Attempting to shutdown PRU..."
|
||||
set -e
|
||||
( echo "FORCE_SHUTDOWN" > /dev/rpmsg_pru30 ) 2> /dev/null || ( log_action_msg "Firmware busy! Please shutdown Klipper and then retry." && exit 1 )
|
||||
sleep 1
|
||||
( echo "FORCE_SHUTDOWN" > /dev/rpmsg_pru30 ) 2> /dev/null || ( log_action_msg "Firmware busy! Please shutdown Klipper and then retry." && exit 1 )
|
||||
sleep 1
|
||||
set +e
|
||||
fi
|
||||
|
||||
log_daemon_msg "Stopping pru"
|
||||
echo 'stop' > $RPROC0/state
|
||||
echo 'stop' > $RPROC1/state
|
||||
}
|
||||
|
||||
pru_start()
|
||||
{
|
||||
if [ -c /dev/rpmsg_pru30 ]; then
|
||||
pru_stop
|
||||
else
|
||||
echo 'stop' > $RPROC0/state
|
||||
echo 'stop' > $RPROC1/state
|
||||
fi
|
||||
sleep 1
|
||||
|
||||
log_daemon_msg "Starting pru"
|
||||
echo 'start' > $RPROC0/state
|
||||
echo 'start' > $RPROC1/state
|
||||
|
||||
# log_daemon_msg "Loading ADC module"
|
||||
# echo 'BB-ADC' > /sys/devices/platform/bone_capemgr/slots
|
||||
}
|
||||
|
||||
mcu_host_stop()
|
||||
{
|
||||
# Shutdown existing Klipper instance (if applicable). The goal is to
|
||||
# put the GPIO pins in a safe state.
|
||||
if [ -c /tmp/klipper_host_mcu ]; then
|
||||
log_daemon_msg "Attempting to shutdown host mcu..."
|
||||
set -e
|
||||
( echo "FORCE_SHUTDOWN" > /tmp/klipper_host_mcu ) 2> /dev/null || ( log_action_msg "Firmware busy! Please shutdown Klipper and then retry." && exit 1 )
|
||||
sleep 1
|
||||
( echo "FORCE_SHUTDOWN" > /tmp/klipper_host_mcu ) 2> /dev/null || ( log_action_msg "Firmware busy! Please shutdown Klipper and then retry." && exit 1 )
|
||||
sleep 1
|
||||
set +e
|
||||
fi
|
||||
|
||||
log_daemon_msg "Stopping klipper host mcu" $NAME
|
||||
killproc -p $PIDFILE $KLIPPER_HOST_MCU
|
||||
}
|
||||
|
||||
mcu_host_start()
|
||||
{
|
||||
[ -x $KLIPPER_HOST_MCU ] || return
|
||||
|
||||
if [ -c /tmp/klipper_host_mcu ]; then
|
||||
mcu_host_stop
|
||||
fi
|
||||
|
||||
log_daemon_msg "Starting klipper MCU" $NAME
|
||||
start-stop-daemon --start --quiet --exec $KLIPPER_HOST_MCU \
|
||||
--background --pidfile $PIDFILE --make-pidfile \
|
||||
-- $KLIPPER_HOST_ARGS
|
||||
log_end_msg $?
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
pru_start
|
||||
mcu_host_start
|
||||
;;
|
||||
stop)
|
||||
pru_stop
|
||||
mcu_host_stop
|
||||
;;
|
||||
restart)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
reload|force-reload)
|
||||
log_daemon_msg "Reloading configuration not supported" $NAME
|
||||
log_end_msg 1
|
||||
;;
|
||||
status)
|
||||
status_of_proc -p $PIDFILE $KLIPPER_HOST_MCU $NAME && exit 0 || exit $?
|
||||
;;
|
||||
*) log_action_msg "Usage: /etc/init.d/klipper {start|stop|status|restart|reload|force-reload}"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
54
scripts/klipper-start.sh
Normal file
54
scripts/klipper-start.sh
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
# System startup script for Klipper 3d-printer host code
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: klipper
|
||||
# Required-Start: $local_fs
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Klipper daemon
|
||||
# Description: Starts the Klipper daemon.
|
||||
### END INIT INFO
|
||||
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
DESC="klipper daemon"
|
||||
NAME="klipper"
|
||||
DEFAULTS_FILE=/etc/default/klipper
|
||||
PIDFILE=/var/run/klipper.pid
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
# Read defaults file
|
||||
[ -r $DEFAULTS_FILE ] && . $DEFAULTS_FILE
|
||||
|
||||
case "$1" in
|
||||
start) log_daemon_msg "Starting klipper" $NAME
|
||||
start-stop-daemon --start --quiet --exec $KLIPPY_EXEC \
|
||||
--background --pidfile $PIDFILE --make-pidfile \
|
||||
--chuid $KLIPPY_USER --user $KLIPPY_USER \
|
||||
-- $KLIPPY_ARGS
|
||||
log_end_msg $?
|
||||
;;
|
||||
stop) log_daemon_msg "Stopping klipper" $NAME
|
||||
killproc -p $PIDFILE $KLIPPY_EXEC
|
||||
RETVAL=$?
|
||||
[ $RETVAL -eq 0 ] && [ -e "$PIDFILE" ] && rm -f $PIDFILE
|
||||
log_end_msg $RETVAL
|
||||
;;
|
||||
restart) log_daemon_msg "Restarting klipper" $NAME
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
reload|force-reload)
|
||||
log_daemon_msg "Reloading configuration not supported" $NAME
|
||||
log_end_msg 1
|
||||
;;
|
||||
status)
|
||||
status_of_proc -p $PIDFILE $KLIPPY_EXEC $NAME && exit 0 || exit $?
|
||||
;;
|
||||
*) log_action_msg "Usage: /etc/init.d/klipper {start|stop|status|restart|reload|force-reload}"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
23
scripts/klipper-uninstall.sh
Normal file
23
scripts/klipper-uninstall.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# Uninstall script for raspbian/debian type installations
|
||||
|
||||
# Stop Klipper Service
|
||||
echo "#### Stopping Klipper Service.."
|
||||
sudo service klipper stop
|
||||
|
||||
# Remove Klipper from Startup
|
||||
echo
|
||||
echo "#### Removing Klipper from Startup.."
|
||||
sudo update-rc.d -f klipper remove
|
||||
|
||||
# Remove Klipper from Services
|
||||
echo
|
||||
echo "#### Removing Klipper Service.."
|
||||
sudo rm -f /etc/init.d/klipper /etc/default/klipper
|
||||
|
||||
# Notify user of method to remove Klipper source code
|
||||
echo
|
||||
echo "The Klipper system files have been removed."
|
||||
echo
|
||||
echo "The following command is typically used to remove local files:"
|
||||
echo " rm -rf ~/klippy-env ~/klipper"
|
||||
10
scripts/klippy-requirements.txt
Normal file
10
scripts/klippy-requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
# This file describes the Python virtualenv package requirements for
|
||||
# the Klipper host software (Klippy). These package requirements are
|
||||
# typically installed via the command:
|
||||
# pip install -r klippy-requirements.txt
|
||||
cffi==1.14.6
|
||||
pyserial==3.4
|
||||
greenlet==1.1.2
|
||||
Jinja2==2.11.3
|
||||
python-can==3.3.4
|
||||
markupsafe==1.1.1
|
||||
610
scripts/logextract.py
Normal file
610
scripts/logextract.py
Normal file
@@ -0,0 +1,610 @@
|
||||
#!/usr/bin/env python2
|
||||
# Script to extract config and shutdown information file a klippy.log file
|
||||
#
|
||||
# Copyright (C) 2017 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import sys, re, collections, ast
|
||||
|
||||
def format_comment(line_num, line):
|
||||
return "# %6d: %s" % (line_num, line)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Config file extraction
|
||||
######################################################################
|
||||
|
||||
class GatherConfig:
|
||||
def __init__(self, configs, line_num, recent_lines, logname):
|
||||
self.configs = configs
|
||||
self.line_num = line_num
|
||||
self.config_num = len(configs) + 1
|
||||
self.filename = "%s.config%04d.cfg" % (logname, self.config_num)
|
||||
self.config_lines = []
|
||||
self.comments = []
|
||||
def add_line(self, line_num, line):
|
||||
if line != '=======================':
|
||||
self.config_lines.append(line)
|
||||
return True
|
||||
self.finalize()
|
||||
return False
|
||||
def finalize(self):
|
||||
lines = tuple(self.config_lines)
|
||||
ch = self.configs.get(lines)
|
||||
if ch is None:
|
||||
self.configs[lines] = ch = self
|
||||
else:
|
||||
ch.comments.extend(self.comments)
|
||||
ch.comments.append(format_comment(self.line_num, "config file"))
|
||||
def add_comment(self, comment):
|
||||
if comment is not None:
|
||||
self.comments.append(comment)
|
||||
def write_file(self):
|
||||
f = open(self.filename, 'wb')
|
||||
f.write('\n'.join(self.comments + self.config_lines).strip() + '\n')
|
||||
f.close()
|
||||
|
||||
|
||||
######################################################################
|
||||
# TMC UART message parsing
|
||||
######################################################################
|
||||
|
||||
uart_r = re.compile(r"tmcuart_(?:send|response) oid=[0-9]+ (?:read|write)=")
|
||||
|
||||
class TMCUartHelper:
|
||||
def _calc_crc8(self, data):
|
||||
# Generate a CRC8-ATM value for a bytearray
|
||||
crc = 0
|
||||
for b in data:
|
||||
for i in range(8):
|
||||
if (crc >> 7) ^ (b & 0x01):
|
||||
crc = (crc << 1) ^ 0x07
|
||||
else:
|
||||
crc = (crc << 1)
|
||||
crc &= 0xff
|
||||
b >>= 1
|
||||
return crc
|
||||
def _add_serial_bits(self, data):
|
||||
# Add serial start and stop bits to a message in a bytearray
|
||||
out = 0
|
||||
pos = 0
|
||||
for d in data:
|
||||
b = (d << 1) | 0x200
|
||||
out |= (b << pos)
|
||||
pos += 10
|
||||
res = bytearray()
|
||||
for i in range((pos+7)//8):
|
||||
res.append((out >> (i*8)) & 0xff)
|
||||
return res
|
||||
def _encode_read(self, sync, addr, reg):
|
||||
# Generate a uart read register message
|
||||
msg = bytearray([sync, addr, reg])
|
||||
msg.append(self._calc_crc8(msg))
|
||||
return self._add_serial_bits(msg)
|
||||
def _encode_write(self, sync, addr, reg, val):
|
||||
# Generate a uart write register message
|
||||
msg = bytearray([sync, addr, reg, (val >> 24) & 0xff,
|
||||
(val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff])
|
||||
msg.append(self._calc_crc8(msg))
|
||||
return self._add_serial_bits(msg)
|
||||
def _decode_read(self, data):
|
||||
# Extract a uart read request message
|
||||
if len(data) != 5:
|
||||
return
|
||||
# Convert data into a long integer for easy manipulation
|
||||
mval = pos = 0
|
||||
for d in bytearray(data):
|
||||
mval |= d << pos
|
||||
pos += 8
|
||||
# Extract register value
|
||||
addr = (mval >> 11) & 0xff
|
||||
reg = (mval >> 21) & 0xff
|
||||
# Verify start/stop bits and crc
|
||||
encoded_data = self._encode_read(0xf5, addr, reg)
|
||||
if data != encoded_data:
|
||||
return "Invalid: %s" % (self.pretty_print(addr, reg),)
|
||||
return self.pretty_print(addr, reg)
|
||||
def _decode_reg(self, data):
|
||||
# Extract a uart read response message
|
||||
if len(data) != 10:
|
||||
return
|
||||
# Convert data into a long integer for easy manipulation
|
||||
mval = pos = 0
|
||||
for d in bytearray(data):
|
||||
mval |= d << pos
|
||||
pos += 8
|
||||
# Extract register value
|
||||
addr = (mval >> 11) & 0xff
|
||||
reg = (mval >> 21) & 0xff
|
||||
val = ((((mval >> 31) & 0xff) << 24) | (((mval >> 41) & 0xff) << 16)
|
||||
| (((mval >> 51) & 0xff) << 8) | ((mval >> 61) & 0xff))
|
||||
sync = 0xf5
|
||||
if addr == 0xff:
|
||||
sync = 0x05
|
||||
# Verify start/stop bits and crc
|
||||
encoded_data = self._encode_write(sync, addr, reg, val)
|
||||
if data != encoded_data:
|
||||
#print("Got %s vs %s" % (repr(data), repr(encoded_data)))
|
||||
return "Invalid:%s" % (self.pretty_print(addr, reg, val),)
|
||||
return self.pretty_print(addr, reg, val)
|
||||
def pretty_print(self, addr, reg, val=None):
|
||||
if val is None:
|
||||
return "(%x@%x)" % (reg, addr)
|
||||
if reg & 0x80:
|
||||
return "(%x@%x=%08x)" % (reg & ~0x80, addr, val)
|
||||
return "(%x@%x==%08x)" % (reg, addr, val)
|
||||
def parse_msg(self, msg):
|
||||
data = bytearray(msg)
|
||||
if len(data) == 10:
|
||||
return self._decode_reg(data)
|
||||
elif len(data) == 5:
|
||||
return self._decode_read(data)
|
||||
elif len(data) == 0:
|
||||
return ""
|
||||
return "(length?)"
|
||||
|
||||
|
||||
######################################################################
|
||||
# Shutdown extraction
|
||||
######################################################################
|
||||
|
||||
def add_high_bits(val, ref, mask):
|
||||
half = (mask + 1) // 2
|
||||
return ref + ((val - (ref & mask) + half) & mask) - half
|
||||
|
||||
count_s = r"(?P<count>[0-9]+)"
|
||||
time_s = r"(?P<time>[0-9]+[.][0-9]+)"
|
||||
esttime_s = r"(?P<esttime>[0-9]+[.][0-9]+)"
|
||||
shortseq_s = r"(?P<shortseq>[0-9a-f])"
|
||||
sent_r = re.compile(r"^Sent " + count_s + " " + esttime_s + " " + time_s
|
||||
+ " [0-9]+: seq: 1" + shortseq_s + ",")
|
||||
|
||||
# MCU "Sent" shutdown message parsing
|
||||
class MCUSentStream:
|
||||
def __init__(self, mcu, count):
|
||||
self.mcu = mcu
|
||||
self.sent_stream = []
|
||||
self.send_count = count
|
||||
def parse_line(self, line_num, line):
|
||||
m = sent_r.match(line)
|
||||
if m is not None:
|
||||
shortseq = int(m.group('shortseq'), 16)
|
||||
seq = (self.mcu.shutdown_seq + int(m.group('count'))
|
||||
- self.send_count)
|
||||
seq = add_high_bits(shortseq, seq, 0xf)
|
||||
ts = float(m.group('time'))
|
||||
esttime = float(m.group('esttime'))
|
||||
self.mcu.sent_time_to_seq[(esttime, seq & 0xf)] = seq
|
||||
self.mcu.sent_seq_to_time[seq] = ts
|
||||
line = self.mcu.annotate(line, seq, ts)
|
||||
self.sent_stream.append((ts, line_num, line))
|
||||
return True, None
|
||||
return self.mcu.parse_line(line_num, line)
|
||||
def get_lines(self):
|
||||
return self.sent_stream
|
||||
|
||||
receive_r = re.compile(r"^Receive: " + count_s + " " + time_s + " " + esttime_s
|
||||
+ " [0-9]+: seq: 1" + shortseq_s + ",")
|
||||
|
||||
# MCU "Receive" shutdown message parsing
|
||||
class MCUReceiveStream:
|
||||
def __init__(self, mcu):
|
||||
self.mcu = mcu
|
||||
self.receive_stream = []
|
||||
def parse_line(self, line_num, line):
|
||||
m = receive_r.match(line)
|
||||
if m is not None:
|
||||
shortseq = int(m.group('shortseq'), 16)
|
||||
ts = float(m.group('time'))
|
||||
esttime = float(m.group('esttime'))
|
||||
seq = self.mcu.sent_time_to_seq.get((esttime, (shortseq - 1) & 0xf))
|
||||
if seq is not None:
|
||||
self.mcu.receive_seq_to_time[seq + 1] = ts
|
||||
line = self.mcu.annotate(line, seq, ts)
|
||||
self.receive_stream.append((ts, line_num, line))
|
||||
return True, None
|
||||
return self.mcu.parse_line(line_num, line)
|
||||
def get_lines(self):
|
||||
return self.receive_stream
|
||||
|
||||
stats_seq_s = r" send_seq=(?P<sseq>[0-9]+) receive_seq=(?P<rseq>[0-9]+) "
|
||||
serial_dump_r = re.compile(r"^Dumping serial stats: .*" + stats_seq_s)
|
||||
send_dump_r = re.compile(r"^Dumping send queue " + count_s + " messages$")
|
||||
receive_dump_r = re.compile(r"^Dumping receive queue " + count_s + " messages$")
|
||||
clock_r = re.compile(r"^clocksync state: mcu_freq=(?P<freq>[0-9]+) .*"
|
||||
+ r" clock_est=\((?P<st>[^ ]+)"
|
||||
+ r" (?P<sc>[0-9]+) (?P<f>[^ ]+)\)")
|
||||
repl_seq_r = re.compile(r": seq: 1" + shortseq_s)
|
||||
clock_s = r"(?P<clock>[0-9]+)"
|
||||
repl_clock_r = re.compile(r"clock=" + clock_s + r"(?: |$)")
|
||||
repl_uart_r = re.compile(r"tmcuart_(?:response|send) oid=[0-9]+"
|
||||
+ r" (?:read|write)=(?P<msg>(?:'[^']*'"
|
||||
+ r'|"[^"]*"))(?: |$)')
|
||||
|
||||
# MCU shutdown message parsing
|
||||
class MCUStream:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.sent_time_to_seq = {}
|
||||
self.sent_seq_to_time = {}
|
||||
self.receive_seq_to_time = {}
|
||||
self.mcu_freq = 1
|
||||
self.clock_est = (0., 0., 1.)
|
||||
self.shutdown_seq = None
|
||||
def trans_clock(self, clock, ts):
|
||||
sample_time, sample_clock, freq = self.clock_est
|
||||
exp_clock = int(sample_clock + (ts - sample_time) * freq)
|
||||
ext_clock = add_high_bits(clock, exp_clock, 0xffffffff)
|
||||
return sample_time + (ext_clock - sample_clock) / freq
|
||||
def annotate(self, line, seq, ts):
|
||||
if seq is not None:
|
||||
line = repl_seq_r.sub(r"\g<0>(%d)" % (seq,), line)
|
||||
def clock_update(m):
|
||||
return m.group(0).rstrip() + "(%.6f) " % (
|
||||
self.trans_clock(int(m.group('clock')), ts),)
|
||||
line = repl_clock_r.sub(clock_update, line)
|
||||
def uart_update(m):
|
||||
msg = TMCUartHelper().parse_msg(ast.literal_eval(m.group('msg')))
|
||||
return m.group(0).rstrip() + "%s " % (msg,)
|
||||
line = repl_uart_r.sub(uart_update, line)
|
||||
if self.name != 'mcu':
|
||||
line = "mcu '%s': %s" % (self.name, line)
|
||||
return line
|
||||
def parse_line(self, line_num, line):
|
||||
m = clock_r.match(line)
|
||||
if m is not None:
|
||||
self.mcu_freq = int(m.group('freq'))
|
||||
st = float(m.group('st'))
|
||||
sc = int(m.group('sc'))
|
||||
f = float(m.group('f'))
|
||||
self.clock_est = (st, sc, f)
|
||||
m = serial_dump_r.match(line)
|
||||
if m is not None:
|
||||
self.shutdown_seq = int(m.group('rseq'))
|
||||
m = send_dump_r.match(line)
|
||||
if m is not None:
|
||||
return True, MCUSentStream(self, int(m.group('count')))
|
||||
m = receive_dump_r.match(line)
|
||||
if m is not None:
|
||||
return True, MCUReceiveStream(self)
|
||||
return False, None
|
||||
def get_lines(self):
|
||||
return []
|
||||
|
||||
stepper_move_r = re.compile(r"^queue_step " + count_s + r": t=" + clock_s
|
||||
+ r" ")
|
||||
|
||||
# Kinematic "trapq" shutdown message parsing
|
||||
class StepperStream:
|
||||
def __init__(self, name, mcu_name, mcus):
|
||||
self.name = name
|
||||
self.stepper_stream = []
|
||||
self.clock_est = (0., 0., 1.)
|
||||
mcu = mcus.get(mcu_name)
|
||||
if mcu is not None:
|
||||
self.clock_est = mcu.clock_est
|
||||
def parse_line(self, line_num, line):
|
||||
m = stepper_move_r.match(line)
|
||||
if m is not None:
|
||||
# Convert clock to systime
|
||||
clock = int(m.group('clock'))
|
||||
sample_time, sample_clock, freq = self.clock_est
|
||||
ts = sample_time + (clock - sample_clock) / freq
|
||||
# Add systime to log
|
||||
parts = line.split(' ', 4)
|
||||
parts[0] = "%s queue_step" % (self.name,)
|
||||
parts[2] += '(%.6f)' % (ts,)
|
||||
self.stepper_stream.append((ts, line_num, ' '.join(parts)))
|
||||
return True, None
|
||||
return False, None
|
||||
def get_lines(self):
|
||||
return self.stepper_stream
|
||||
|
||||
trapq_move_r = re.compile(r"^move " + count_s + r": pt=" + time_s)
|
||||
|
||||
# Kinematic "trapq" shutdown message parsing
|
||||
class TrapQStream:
|
||||
def __init__(self, name, mcus):
|
||||
self.name = name
|
||||
self.trapq_stream = []
|
||||
self.mcu_freq = 1
|
||||
self.clock_est = (0., 0., 1.)
|
||||
mcu = mcus.get("mcu")
|
||||
if mcu is not None:
|
||||
self.mcu_freq = mcu.mcu_freq
|
||||
self.clock_est = mcu.clock_est
|
||||
def parse_line(self, line_num, line):
|
||||
m = trapq_move_r.match(line)
|
||||
if m is not None:
|
||||
# Convert print_time to systime
|
||||
pt = float(m.group('time'))
|
||||
clock = pt * self.mcu_freq
|
||||
sample_time, sample_clock, freq = self.clock_est
|
||||
ts = sample_time + (clock - sample_clock) / freq
|
||||
# Add systime to log
|
||||
parts = line.split(' ', 4)
|
||||
parts[0] = "%s move" % (self.name,)
|
||||
parts[2] += '(%.6f)' % (ts,)
|
||||
self.trapq_stream.append((ts, line_num, ' '.join(parts)))
|
||||
return True, None
|
||||
return False, None
|
||||
def get_lines(self):
|
||||
return self.trapq_stream
|
||||
|
||||
gcode_cmd_r = re.compile(r"^Read " + time_s + r": (?P<gcode>['\"].*)$")
|
||||
varlist_split_r = re.compile(r"([^ ]+)=")
|
||||
|
||||
# G-Code shutdown message parsing
|
||||
class GCodeStream:
|
||||
def __init__(self, shutdown_line_num, logname):
|
||||
self.gcode_stream = []
|
||||
self.gcode_commands = []
|
||||
self.gcode_state = ''
|
||||
self.gcode_filename = "%s.gcode%05d" % (logname, shutdown_line_num)
|
||||
def extract_params(self, line):
|
||||
parts = varlist_split_r.split(line)
|
||||
try:
|
||||
return { parts[i]: ast.literal_eval(parts[i+1].strip())
|
||||
for i in range(1, len(parts), 2) }
|
||||
except:
|
||||
return {}
|
||||
def handle_gcode_state(self, line):
|
||||
kv = self.extract_params(line)
|
||||
out = ['; Start g-code state restore', 'G28']
|
||||
if not kv.get('absolute_coord', kv.get('absolutecoord')):
|
||||
out.append('G91')
|
||||
if not kv.get('absolute_extrude', kv.get('absoluteextrude')):
|
||||
out.append('M83')
|
||||
lp = kv['last_position']
|
||||
out.append('G1 X%f Y%f Z%f F%f' % (
|
||||
lp[0], lp[1], lp[2], kv['speed'] * 60.))
|
||||
bp = kv['base_position']
|
||||
if bp[:3] != [0., 0., 0.]:
|
||||
out.append('; Must manually set base position...')
|
||||
out.append('G92 E%f' % (lp[3] - bp[3],))
|
||||
hp = kv['homing_position']
|
||||
if hp != [0., 0., 0., 0.]:
|
||||
out.append('; Must manually set homing position...')
|
||||
if abs(kv['speed_factor'] - 1. / 60.) > .000001:
|
||||
out.append('M220 S%f' % (kv['speed_factor'] * 60. * 100.,))
|
||||
if kv['extrude_factor'] != 1.:
|
||||
out.append('M221 S%f' % (kv['extrude_factor'] * 100.,))
|
||||
out.extend(['; End of state restore', '', ''])
|
||||
self.gcode_state = '\n'.join(out)
|
||||
def parse_line(self, line_num, line):
|
||||
m = gcode_cmd_r.match(line)
|
||||
if m is not None:
|
||||
ts = float(m.group('time'))
|
||||
self.gcode_stream.append((ts, line_num, line))
|
||||
self.gcode_commands.append(m.group('gcode'))
|
||||
return True, None
|
||||
return False, None
|
||||
def get_lines(self):
|
||||
# Produce output gcode stream
|
||||
if self.gcode_stream:
|
||||
data = [ast.literal_eval(gc) for gc in self.gcode_commands]
|
||||
f = open(self.gcode_filename, 'wb')
|
||||
f.write(self.gcode_state + ''.join(data))
|
||||
f.close()
|
||||
return self.gcode_stream
|
||||
|
||||
api_cmd_r = re.compile(r"^Received " + time_s + r": \{.*\}$")
|
||||
|
||||
# API server shutdowm message parsing
|
||||
class APIStream:
|
||||
def __init__(self):
|
||||
self.api_stream = []
|
||||
def parse_line(self, line_num, line):
|
||||
m = api_cmd_r.match(line)
|
||||
if m is not None:
|
||||
ts = float(m.group('time'))
|
||||
self.api_stream.append((ts, line_num, line))
|
||||
return True, None
|
||||
return False, None
|
||||
def get_lines(self):
|
||||
return self.api_stream
|
||||
|
||||
stats_r = re.compile(r"^Stats " + time_s + ": ")
|
||||
mcu_r = re.compile(r"MCU '(?P<mcu>[^']+)' (is_)?shutdown: (?P<reason>.*)$")
|
||||
stepper_r = re.compile(r"^Dumping stepper '(?P<name>[^']*)' \((?P<mcu>[^)]+)\) "
|
||||
+ count_s + r" queue_step:$")
|
||||
trapq_r = re.compile(r"^Dumping trapq '(?P<name>[^']*)' " + count_s
|
||||
+ r" moves:$")
|
||||
gcode_r = re.compile(r"Dumping gcode input " + count_s + r" blocks$")
|
||||
gcode_state_r = re.compile(r"^gcode state: ")
|
||||
api_r = re.compile(r"Dumping " + count_s + r" requests for client "
|
||||
+ r"(?P<client>[0-9]+)" + r"$")
|
||||
|
||||
# Stats message parsing and high-level message dispatch
|
||||
class StatsStream:
|
||||
def __init__(self, shutdown_line_num, logname):
|
||||
self.shutdown_line_num = shutdown_line_num
|
||||
self.gcode_stream = GCodeStream(shutdown_line_num, logname)
|
||||
self.mcus = {}
|
||||
self.first_stat_time = self.last_stat_time = None
|
||||
self.stats_stream = []
|
||||
def reset_first_stat_time(self):
|
||||
self.first_stat_time = self.last_stat_time
|
||||
def get_stat_times(self):
|
||||
return self.first_stat_time, self.last_stat_time
|
||||
def check_stats_seq(self, ts, line):
|
||||
# Parse stats
|
||||
parts = line.split()
|
||||
mcu = ""
|
||||
keyparts = {}
|
||||
for p in parts[2:]:
|
||||
if '=' not in p:
|
||||
mcu = p
|
||||
continue
|
||||
name, val = p.split('=', 1)
|
||||
keyparts[mcu + name] = val
|
||||
min_ts = 0
|
||||
max_ts = 999999999999
|
||||
for mcu_name, mcu in self.mcus.items():
|
||||
sname = '%s:send_seq' % (mcu_name,)
|
||||
rname = '%s:receive_seq' % (mcu_name,)
|
||||
if sname not in keyparts:
|
||||
continue
|
||||
sseq = int(keyparts[sname])
|
||||
rseq = int(keyparts[rname])
|
||||
min_ts = max(min_ts, mcu.sent_seq_to_time.get(sseq-1, 0),
|
||||
mcu.receive_seq_to_time.get(rseq, 0))
|
||||
max_ts = min(max_ts, mcu.sent_seq_to_time.get(sseq, 999999999999),
|
||||
mcu.receive_seq_to_time.get(rseq+1, 999999999999))
|
||||
return min(max(ts, min_ts + 0.00000001), max_ts - 0.00000001)
|
||||
def parse_line(self, line_num, line):
|
||||
m = stats_r.match(line)
|
||||
if m is not None:
|
||||
ts = float(m.group('time'))
|
||||
self.last_stat_time = ts
|
||||
if self.first_stat_time is None:
|
||||
self.first_stat_time = ts
|
||||
self.stats_stream.append((ts, line_num, line))
|
||||
return True, None
|
||||
self.stats_stream.append((None, line_num, line))
|
||||
m = mcu_r.match(line)
|
||||
if m is not None:
|
||||
mcu_name = m.group('mcu')
|
||||
mcu_stream = MCUStream(mcu_name)
|
||||
self.mcus[mcu_name] = mcu_stream
|
||||
return True, mcu_stream
|
||||
m = stepper_r.match(line)
|
||||
if m is not None:
|
||||
return True, StepperStream(m.group('name'), m.group('mcu'),
|
||||
self.mcus)
|
||||
m = trapq_r.match(line)
|
||||
if m is not None:
|
||||
return True, TrapQStream(m.group('name'), self.mcus)
|
||||
m = gcode_r.match(line)
|
||||
if m is not None:
|
||||
return True, self.gcode_stream
|
||||
m = gcode_state_r.match(line)
|
||||
if m is not None:
|
||||
self.gcode_stream.handle_gcode_state(line)
|
||||
return True, None
|
||||
m = api_r.match(line)
|
||||
if m is not None:
|
||||
return True, APIStream()
|
||||
return False, None
|
||||
def get_lines(self):
|
||||
# Ignore old stats
|
||||
all_ts = []
|
||||
for mcu_name, mcu in self.mcus.items():
|
||||
all_ts.extend(mcu.sent_seq_to_time.values())
|
||||
all_ts.extend(mcu.receive_seq_to_time.values())
|
||||
if not all_ts:
|
||||
return []
|
||||
min_stream_ts = min(all_ts)
|
||||
max_stream_ts = max(all_ts)
|
||||
for i, info in enumerate(self.stats_stream):
|
||||
if info[0] is not None and info[0] >= min_stream_ts - 5.:
|
||||
del self.stats_stream[:i]
|
||||
break
|
||||
# Improve accuracy of stats timestamps
|
||||
last_ts = self.stats_stream[0][0]
|
||||
for i, (ts, line_num, line) in enumerate(self.stats_stream):
|
||||
if ts is not None:
|
||||
last_ts = self.check_stats_seq(ts, line)
|
||||
elif (line_num >= self.shutdown_line_num
|
||||
and last_ts <= max_stream_ts):
|
||||
last_ts = max_stream_ts + 0.00000001
|
||||
self.stats_stream[i] = (last_ts, line_num, line)
|
||||
return self.stats_stream
|
||||
|
||||
# Main handler for creating shutdown diagnostics file
|
||||
class GatherShutdown:
|
||||
def __init__(self, configs, line_num, recent_lines, logname):
|
||||
self.filename = "%s.shutdown%05d" % (logname, line_num)
|
||||
self.comments = []
|
||||
if configs:
|
||||
configs_by_id = {c.config_num: c for c in configs.values()}
|
||||
config = configs_by_id[max(configs_by_id.keys())]
|
||||
config.add_comment(format_comment(line_num, recent_lines[-1][1]))
|
||||
self.comments.append("# config %s" % (config.filename,))
|
||||
self.stats_stream = StatsStream(line_num, logname)
|
||||
self.active_streams = [self.stats_stream]
|
||||
self.all_streams = list(self.active_streams)
|
||||
for line_num, line in recent_lines:
|
||||
self.parse_line(line_num, line)
|
||||
self.stats_stream.reset_first_stat_time()
|
||||
def add_comment(self, comment):
|
||||
if comment is not None:
|
||||
self.comments.append(comment)
|
||||
def add_line(self, line_num, line):
|
||||
self.parse_line(line_num, line)
|
||||
first, last = self.stats_stream.get_stat_times()
|
||||
if first is not None and last > first + 5.:
|
||||
self.finalize()
|
||||
return False
|
||||
if (line.startswith('Git version')
|
||||
or line.startswith('Start printer at')
|
||||
or line == '===== Config file ====='):
|
||||
self.finalize()
|
||||
return False
|
||||
return True
|
||||
def parse_line(self, line_num, line):
|
||||
for s in self.active_streams:
|
||||
did_parse, new_stream = s.parse_line(line_num, line)
|
||||
if did_parse:
|
||||
if new_stream is not None:
|
||||
self.all_streams.append(new_stream)
|
||||
self.active_streams = [new_stream, self.stats_stream]
|
||||
break
|
||||
def finalize(self):
|
||||
# Make sure no timestamp goes backwards
|
||||
streams = [p.get_lines() for p in self.all_streams]
|
||||
for s in streams:
|
||||
for i in range(1, len(s)):
|
||||
if s[i-1][0] > s[i][0]:
|
||||
s[i] = (s[i-1][0], s[i][1], s[i][2])
|
||||
# Produce output sorted by timestamp
|
||||
out = [i for s in streams for i in s]
|
||||
out.sort()
|
||||
out = [i[2] for i in out]
|
||||
f = open(self.filename, 'wb')
|
||||
f.write('\n'.join(self.comments + out))
|
||||
f.close()
|
||||
|
||||
|
||||
######################################################################
|
||||
# Startup
|
||||
######################################################################
|
||||
|
||||
def main():
|
||||
logname = sys.argv[1]
|
||||
last_git = last_start = None
|
||||
configs = {}
|
||||
handler = None
|
||||
recent_lines = collections.deque([], 200)
|
||||
# Parse log file
|
||||
f = open(logname, 'rb')
|
||||
for line_num, line in enumerate(f):
|
||||
line = line.rstrip()
|
||||
line_num += 1
|
||||
recent_lines.append((line_num, line))
|
||||
if handler is not None:
|
||||
ret = handler.add_line(line_num, line)
|
||||
if ret:
|
||||
continue
|
||||
recent_lines.clear()
|
||||
handler = None
|
||||
if line.startswith('Git version'):
|
||||
last_git = format_comment(line_num, line)
|
||||
elif line.startswith('Start printer at'):
|
||||
last_start = format_comment(line_num, line)
|
||||
elif line == '===== Config file =====':
|
||||
handler = GatherConfig(configs, line_num, recent_lines, logname)
|
||||
handler.add_comment(last_git)
|
||||
handler.add_comment(last_start)
|
||||
elif 'shutdown: ' in line or line.startswith('Dumping '):
|
||||
handler = GatherShutdown(configs, line_num, recent_lines, logname)
|
||||
handler.add_comment(last_git)
|
||||
handler.add_comment(last_start)
|
||||
if handler is not None:
|
||||
handler.finalize()
|
||||
# Write found config files
|
||||
for cfg in configs.values():
|
||||
cfg.write_file()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
31
scripts/make_version.py
Normal file
31
scripts/make_version.py
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python2
|
||||
# Get the version number for klippy
|
||||
#
|
||||
# Copyright (C) 2018 Lucas Fink <software@lfcode.ca>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../klippy'))
|
||||
|
||||
import util
|
||||
|
||||
|
||||
def main(argv):
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument(
|
||||
'distroname',
|
||||
help='Name of distro this package is intended for'
|
||||
)
|
||||
args = p.parse_args()
|
||||
print(util.get_git_version(from_file=False),
|
||||
args.distroname.replace(' ', ''), sep='-')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
283
scripts/motan/analyzers.py
Normal file
283
scripts/motan/analyzers.py
Normal file
@@ -0,0 +1,283 @@
|
||||
# Log data analyzing functions
|
||||
#
|
||||
# Copyright (C) 2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import math, collections
|
||||
import readlog
|
||||
|
||||
|
||||
######################################################################
|
||||
# Analysis code
|
||||
######################################################################
|
||||
|
||||
# Analyzer handlers: {name: class, ...}
|
||||
AHandlers = {}
|
||||
|
||||
# Calculate a derivative (position to velocity, or velocity to accel)
|
||||
class GenDerivative:
|
||||
ParametersMin = ParametersMax = 1
|
||||
DataSets = [
|
||||
('derivative(<dataset>)', 'Derivative of the given dataset'),
|
||||
]
|
||||
def __init__(self, amanager, name_parts):
|
||||
self.amanager = amanager
|
||||
self.source = name_parts[1]
|
||||
amanager.setup_dataset(self.source)
|
||||
def get_label(self):
|
||||
label = self.amanager.get_label(self.source)
|
||||
lname = label['label']
|
||||
units = label['units']
|
||||
if '(mm)' in units:
|
||||
rep = [('Position', 'Velocity'), ('(mm)', '(mm/s)')]
|
||||
elif '(mm/s)' in units:
|
||||
rep = [('Velocity', 'Acceleration'), ('(mm/s)', '(mm/s^2)')]
|
||||
else:
|
||||
return {'label': 'Derivative', 'units': 'Unknown'}
|
||||
for old, new in rep:
|
||||
lname = lname.replace(old, new).replace(old.lower(), new.lower())
|
||||
units = units.replace(old, new).replace(old.lower(), new.lower())
|
||||
return {'label': lname, 'units': units}
|
||||
def generate_data(self):
|
||||
inv_seg_time = 1. / self.amanager.get_segment_time()
|
||||
data = self.amanager.get_datasets()[self.source]
|
||||
deriv = [(data[i+1] - data[i]) * inv_seg_time
|
||||
for i in range(len(data)-1)]
|
||||
return [deriv[0]] + deriv
|
||||
AHandlers["derivative"] = GenDerivative
|
||||
|
||||
# Calculate an integral (accel to velocity, or velocity to position)
|
||||
class GenIntegral:
|
||||
ParametersMin = 1
|
||||
ParametersMax = 3
|
||||
DataSets = [
|
||||
('integral(<dataset>)', 'Integral of the given dataset'),
|
||||
('integral(<dataset1>,<dataset2>)',
|
||||
'Integral with dataset2 as reference'),
|
||||
('integral(<dataset1>,<dataset2>,<half_life>)',
|
||||
'Integral with weighted half-life time'),
|
||||
]
|
||||
def __init__(self, amanager, name_parts):
|
||||
self.amanager = amanager
|
||||
self.source = name_parts[1]
|
||||
amanager.setup_dataset(self.source)
|
||||
self.ref = None
|
||||
self.half_life = 0.015
|
||||
if len(name_parts) >= 3:
|
||||
self.ref = name_parts[2]
|
||||
amanager.setup_dataset(self.ref)
|
||||
if len(name_parts) == 4:
|
||||
self.half_life = float(name_parts[3])
|
||||
def get_label(self):
|
||||
label = self.amanager.get_label(self.source)
|
||||
lname = label['label']
|
||||
units = label['units']
|
||||
if '(mm/s)' in units:
|
||||
rep = [('Velocity', 'Position'), ('(mm/s)', '(mm)')]
|
||||
elif '(mm/s^2)' in units:
|
||||
rep = [('Acceleration', 'Velocity'), ('(mm/s^2)', '(mm/s)')]
|
||||
else:
|
||||
return {'label': 'Integral', 'units': 'Unknown'}
|
||||
for old, new in rep:
|
||||
lname = lname.replace(old, new).replace(old.lower(), new.lower())
|
||||
units = units.replace(old, new).replace(old.lower(), new.lower())
|
||||
return {'label': lname, 'units': units}
|
||||
def generate_data(self):
|
||||
seg_time = self.amanager.get_segment_time()
|
||||
src = self.amanager.get_datasets()[self.source]
|
||||
offset = sum(src) / len(src)
|
||||
total = 0.
|
||||
ref = None
|
||||
if self.ref is not None:
|
||||
ref = self.amanager.get_datasets()[self.ref]
|
||||
offset -= (ref[-1] - ref[0]) / (len(src) * seg_time)
|
||||
total = ref[0]
|
||||
src_weight = 1.
|
||||
if self.half_life:
|
||||
src_weight = math.exp(math.log(.5) * seg_time / self.half_life)
|
||||
ref_weight = 1. - src_weight
|
||||
data = [0.] * len(src)
|
||||
for i, v in enumerate(src):
|
||||
total += (v - offset) * seg_time
|
||||
if ref is not None:
|
||||
total = src_weight * total + ref_weight * ref[i]
|
||||
data[i] = total
|
||||
return data
|
||||
AHandlers["integral"] = GenIntegral
|
||||
|
||||
# Calculate a kinematic stepper position from the toolhead requested position
|
||||
class GenKinematicPosition:
|
||||
ParametersMin = ParametersMax = 1
|
||||
DataSets = [
|
||||
('kin(<stepper>)', 'Stepper position derived from toolhead kinematics'),
|
||||
]
|
||||
def __init__(self, amanager, name_parts):
|
||||
self.amanager = amanager
|
||||
stepper = name_parts[1]
|
||||
status = self.amanager.get_initial_status()
|
||||
kin = status['configfile']['settings']['printer']['kinematics']
|
||||
if kin not in ['cartesian', 'corexy']:
|
||||
raise amanager.error("Unsupported kinematics '%s'" % (kin,))
|
||||
if stepper not in ['stepper_x', 'stepper_y', 'stepper_z']:
|
||||
raise amanager.error("Unknown stepper '%s'" % (stepper,))
|
||||
if kin == 'corexy' and stepper in ['stepper_x', 'stepper_y']:
|
||||
self.source1 = 'trapq(toolhead,x)'
|
||||
self.source2 = 'trapq(toolhead,y)'
|
||||
if stepper == 'stepper_x':
|
||||
self.generate_data = self.generate_data_corexy_plus
|
||||
else:
|
||||
self.generate_data = self.generate_data_corexy_minus
|
||||
amanager.setup_dataset(self.source1)
|
||||
amanager.setup_dataset(self.source2)
|
||||
else:
|
||||
self.source1 = 'trapq(toolhead,%s)' % (stepper[-1:],)
|
||||
self.source2 = None
|
||||
self.generate_data = self.generate_data_passthrough
|
||||
amanager.setup_dataset(self.source1)
|
||||
def get_label(self):
|
||||
return {'label': 'Position', 'units': 'Position\n(mm)'}
|
||||
def generate_data_corexy_plus(self):
|
||||
datasets = self.amanager.get_datasets()
|
||||
data1 = datasets[self.source1]
|
||||
data2 = datasets[self.source2]
|
||||
return [d1 + d2 for d1, d2 in zip(data1, data2)]
|
||||
def generate_data_corexy_minus(self):
|
||||
datasets = self.amanager.get_datasets()
|
||||
data1 = datasets[self.source1]
|
||||
data2 = datasets[self.source2]
|
||||
return [d1 - d2 for d1, d2 in zip(data1, data2)]
|
||||
def generate_data_passthrough(self):
|
||||
return self.amanager.get_datasets()[self.source1]
|
||||
AHandlers["kin"] = GenKinematicPosition
|
||||
|
||||
# Calculate a toolhead x/y position from corexy stepper positions
|
||||
class GenCorexyPosition:
|
||||
ParametersMin = ParametersMax = 3
|
||||
DataSets = [
|
||||
('corexy(x,<stepper>,<stepper>)', 'Toolhead x position from steppers'),
|
||||
('corexy(y,<stepper>,<stepper>)', 'Toolhead y position from steppers'),
|
||||
]
|
||||
def __init__(self, amanager, name_parts):
|
||||
self.amanager = amanager
|
||||
self.is_plus = name_parts[1] == 'x'
|
||||
self.source1, self.source2 = name_parts[2:]
|
||||
amanager.setup_dataset(self.source1)
|
||||
amanager.setup_dataset(self.source2)
|
||||
def get_label(self):
|
||||
axis = 'x'
|
||||
if not self.is_plus:
|
||||
axis = 'y'
|
||||
return {'label': 'Derived %s position' % (axis,),
|
||||
'units': 'Position\n(mm)'}
|
||||
def generate_data(self):
|
||||
datasets = self.amanager.get_datasets()
|
||||
data1 = datasets[self.source1]
|
||||
data2 = datasets[self.source2]
|
||||
if self.is_plus:
|
||||
return [.5 * (d1 + d2) for d1, d2 in zip(data1, data2)]
|
||||
return [.5 * (d1 - d2) for d1, d2 in zip(data1, data2)]
|
||||
AHandlers["corexy"] = GenCorexyPosition
|
||||
|
||||
# Calculate a position deviation
|
||||
class GenDeviation:
|
||||
ParametersMin = ParametersMax = 2
|
||||
DataSets = [
|
||||
('deviation(<dataset1>,<dataset2>)', 'Difference between datasets'),
|
||||
]
|
||||
def __init__(self, amanager, name_parts):
|
||||
self.amanager = amanager
|
||||
self.source1, self.source2 = name_parts[1:]
|
||||
amanager.setup_dataset(self.source1)
|
||||
amanager.setup_dataset(self.source2)
|
||||
def get_label(self):
|
||||
label1 = self.amanager.get_label(self.source1)
|
||||
label2 = self.amanager.get_label(self.source2)
|
||||
if label1['units'] != label2['units']:
|
||||
return {'label': 'Deviation', 'units': 'Unknown'}
|
||||
parts = label1['units'].split('\n')
|
||||
units = '\n'.join([parts[0]] + ['Deviation'] + parts[1:])
|
||||
return {'label': label1['label'] + ' deviation', 'units': units}
|
||||
def generate_data(self):
|
||||
datasets = self.amanager.get_datasets()
|
||||
data1 = datasets[self.source1]
|
||||
data2 = datasets[self.source2]
|
||||
return [d1 - d2 for d1, d2 in zip(data1, data2)]
|
||||
AHandlers["deviation"] = GenDeviation
|
||||
|
||||
|
||||
######################################################################
|
||||
# Analyzer management and data generation
|
||||
######################################################################
|
||||
|
||||
# Return a description of available analyzers
|
||||
def list_datasets():
|
||||
datasets = []
|
||||
for ah in sorted(AHandlers.keys()):
|
||||
datasets += AHandlers[ah].DataSets
|
||||
return datasets
|
||||
|
||||
# Manage raw and generated data samples
|
||||
class AnalyzerManager:
|
||||
error = None
|
||||
def __init__(self, lmanager, segment_time):
|
||||
self.lmanager = lmanager
|
||||
self.error = lmanager.error
|
||||
self.segment_time = segment_time
|
||||
self.raw_datasets = collections.OrderedDict()
|
||||
self.gen_datasets = collections.OrderedDict()
|
||||
self.datasets = {}
|
||||
self.dataset_times = []
|
||||
self.duration = 5.
|
||||
def set_duration(self, duration):
|
||||
self.duration = duration
|
||||
def get_segment_time(self):
|
||||
return self.segment_time
|
||||
def get_datasets(self):
|
||||
return self.datasets
|
||||
def get_dataset_times(self):
|
||||
return self.dataset_times
|
||||
def get_initial_status(self):
|
||||
return self.lmanager.get_initial_status()
|
||||
def setup_dataset(self, name):
|
||||
name = name.strip()
|
||||
if name in self.raw_datasets:
|
||||
return self.raw_datasets[name]
|
||||
if name in self.gen_datasets:
|
||||
return self.gen_datasets[name]
|
||||
name_parts = readlog.name_split(name)
|
||||
if name_parts[0] in self.lmanager.available_dataset_types():
|
||||
hdl = self.lmanager.setup_dataset(name)
|
||||
self.raw_datasets[name] = hdl
|
||||
else:
|
||||
cls = AHandlers.get(name_parts[0])
|
||||
if cls is None:
|
||||
raise self.error("Unknown dataset '%s'" % (name,))
|
||||
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.datasets[name] = []
|
||||
return hdl
|
||||
def get_label(self, dataset):
|
||||
hdl = self.raw_datasets.get(dataset)
|
||||
if hdl is None:
|
||||
hdl = self.gen_datasets.get(dataset)
|
||||
if hdl is None:
|
||||
raise self.error("Unknown dataset '%s'" % (dataset,))
|
||||
return hdl.get_label()
|
||||
def generate_datasets(self):
|
||||
# Generate raw data
|
||||
list_hdls = [(self.datasets[name], hdl)
|
||||
for name, hdl in self.raw_datasets.items()]
|
||||
initial_start_time = self.lmanager.get_initial_start_time()
|
||||
start_time = t = self.lmanager.get_start_time()
|
||||
end_time = start_time + self.duration
|
||||
while t < end_time:
|
||||
t += self.segment_time
|
||||
self.dataset_times.append(t - initial_start_time)
|
||||
for dl, hdl in list_hdls:
|
||||
dl.append(hdl.pull_data(t))
|
||||
# Generate analyzer data
|
||||
for name, hdl in self.gen_datasets.items():
|
||||
self.datasets[name] = hdl.generate_data()
|
||||
204
scripts/motan/data_logger.py
Normal file
204
scripts/motan/data_logger.py
Normal file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python
|
||||
# Tool to subscribe to motion data and log it to a disk file
|
||||
#
|
||||
# Copyright (C) 2020-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import sys, os, optparse, socket, select, json, errno, time, zlib
|
||||
|
||||
INDEX_UPDATE_TIME = 5.0
|
||||
ClientInfo = {'program': 'motan_data_logger', 'version': 'v0.1'}
|
||||
|
||||
def webhook_socket_create(uds_filename):
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.setblocking(0)
|
||||
sys.stderr.write("Waiting for connect to %s\n" % (uds_filename,))
|
||||
while 1:
|
||||
try:
|
||||
sock.connect(uds_filename)
|
||||
except socket.error as e:
|
||||
if e.errno == errno.ECONNREFUSED:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
sys.stderr.write("Unable to connect socket %s [%d,%s]\n"
|
||||
% (uds_filename, e.errno,
|
||||
errno.errorcode[e.errno]))
|
||||
sys.exit(-1)
|
||||
break
|
||||
sys.stderr.write("Connection.\n")
|
||||
return sock
|
||||
|
||||
class LogWriter:
|
||||
def __init__(self, filename):
|
||||
self.file = open(filename, "wb")
|
||||
self.comp = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
|
||||
zlib.DEFLATED, 31)
|
||||
self.raw_pos = self.file_pos = 0
|
||||
def add_data(self, data):
|
||||
d = self.comp.compress(data + b"\x03")
|
||||
self.file.write(d)
|
||||
self.file_pos += len(d)
|
||||
self.raw_pos += len(data) + 1
|
||||
def flush(self, flag=zlib.Z_FULL_FLUSH):
|
||||
if not self.raw_pos:
|
||||
return self.file_pos
|
||||
d = self.comp.flush(flag)
|
||||
self.file.write(d)
|
||||
self.file_pos += len(d)
|
||||
return self.file_pos
|
||||
def close(self):
|
||||
self.flush(zlib.Z_FINISH)
|
||||
self.file.close()
|
||||
self.file = None
|
||||
self.comp = None
|
||||
|
||||
class DataLogger:
|
||||
def __init__(self, uds_filename, log_prefix):
|
||||
# IO
|
||||
self.webhook_socket = webhook_socket_create(uds_filename)
|
||||
self.poll = select.poll()
|
||||
self.poll.register(self.webhook_socket, select.POLLIN | select.POLLHUP)
|
||||
self.socket_data = b""
|
||||
# Data log
|
||||
self.logger = LogWriter(log_prefix + ".json.gz")
|
||||
self.index = LogWriter(log_prefix + ".index.gz")
|
||||
# Handlers
|
||||
self.query_handlers = {}
|
||||
self.async_handlers = {}
|
||||
# get_status databasing
|
||||
self.db = {}
|
||||
self.next_index_time = 0.
|
||||
# Start login process
|
||||
self.send_query("info", "info", {"client_info": ClientInfo},
|
||||
self.handle_info)
|
||||
def error(self, msg):
|
||||
sys.stderr.write(msg + "\n")
|
||||
def finish(self, msg):
|
||||
self.error(msg)
|
||||
self.logger.close()
|
||||
self.index.close()
|
||||
sys.exit(0)
|
||||
# Unix Domain Socket IO
|
||||
def send_query(self, msg_id, method, params, cb):
|
||||
self.query_handlers[msg_id] = cb
|
||||
msg = {"id": msg_id, "method": method, "params": params}
|
||||
cm = json.dumps(msg, separators=(',', ':')).encode()
|
||||
self.webhook_socket.send(cm + b"\x03")
|
||||
def process_socket(self):
|
||||
data = self.webhook_socket.recv(4096)
|
||||
if not data:
|
||||
self.finish("Socket closed")
|
||||
parts = data.split(b"\x03")
|
||||
parts[0] = self.socket_data + parts[0]
|
||||
self.socket_data = parts.pop()
|
||||
for part in parts:
|
||||
try:
|
||||
msg = json.loads(part)
|
||||
except:
|
||||
self.error("ERROR: Unable to parse line")
|
||||
continue
|
||||
self.logger.add_data(part)
|
||||
msg_q = msg.get("q")
|
||||
if msg_q is not None:
|
||||
hdl = self.async_handlers.get(msg_q)
|
||||
if hdl is not None:
|
||||
hdl(msg, part)
|
||||
continue
|
||||
msg_id = msg.get("id")
|
||||
hdl = self.query_handlers.get(msg_id)
|
||||
if hdl is not None:
|
||||
del self.query_handlers[msg_id]
|
||||
hdl(msg, part)
|
||||
if not self.query_handlers:
|
||||
self.flush_index()
|
||||
continue
|
||||
self.error("ERROR: Message with unknown id")
|
||||
def run(self):
|
||||
try:
|
||||
while 1:
|
||||
res = self.poll.poll(1000.)
|
||||
for fd, event in res:
|
||||
if fd == self.webhook_socket.fileno():
|
||||
self.process_socket()
|
||||
except KeyboardInterrupt as e:
|
||||
self.finish("Keyboard Interrupt")
|
||||
# Query response handlers
|
||||
def send_subscribe(self, msg_id, method, params, cb=None, async_cb=None):
|
||||
if cb is None:
|
||||
cb = self.handle_dump
|
||||
if async_cb is not None:
|
||||
self.async_handlers[msg_id] = async_cb
|
||||
params["response_template"] = {"q": msg_id}
|
||||
self.send_query(msg_id, method, params, cb)
|
||||
def handle_info(self, msg, raw_msg):
|
||||
if msg["result"]["state"] != "ready":
|
||||
self.finish("Klipper not in ready state")
|
||||
self.send_query("list", "objects/list", {}, self.handle_list)
|
||||
def handle_list(self, msg, raw_msg):
|
||||
subreq = {o: None for o in msg["result"]["objects"]}
|
||||
self.send_subscribe("status", "objects/subscribe", {"objects": subreq},
|
||||
self.handle_subscribe, self.handle_async_db)
|
||||
def handle_subscribe(self, msg, raw_msg):
|
||||
result = msg["result"]
|
||||
self.next_index_time = result["eventtime"] + INDEX_UPDATE_TIME
|
||||
self.db["status"] = status = result["status"]
|
||||
# Subscribe to trapq and stepper queue updates
|
||||
motion_report = status.get("motion_report", {})
|
||||
for trapq in motion_report.get("trapq", []):
|
||||
self.send_subscribe("trapq:" + trapq, "motion_report/dump_trapq",
|
||||
{"name": trapq})
|
||||
for stepper in motion_report.get("steppers", []):
|
||||
self.send_subscribe("stepq:" + stepper,
|
||||
"motion_report/dump_stepper", {"name": stepper})
|
||||
# Subscribe to additional sensor data
|
||||
config = status["configfile"]["settings"]
|
||||
for cfgname in config.keys():
|
||||
if cfgname == "adxl345" or cfgname.startswith("adxl345 "):
|
||||
aname = cfgname.split()[-1]
|
||||
self.send_subscribe("adxl345:" + aname, "adxl345/dump_adxl345",
|
||||
{"sensor": aname})
|
||||
if cfgname.startswith("angle "):
|
||||
aname = cfgname.split()[1]
|
||||
self.send_subscribe("angle:" + aname, "angle/dump_angle",
|
||||
{"sensor": aname})
|
||||
def handle_dump(self, msg, raw_msg):
|
||||
msg_id = msg["id"]
|
||||
if "result" not in msg:
|
||||
self.error("Unable to subscribe to '%s': %s"
|
||||
% (msg_id, msg.get("error", {}).get("message", "")))
|
||||
return
|
||||
self.db.setdefault("subscriptions", {})[msg_id] = msg["result"]
|
||||
def flush_index(self):
|
||||
self.db['file_position'] = self.logger.flush()
|
||||
self.index.add_data(json.dumps(self.db, separators=(',', ':')).encode())
|
||||
self.db = {"status": {}}
|
||||
def handle_async_db(self, msg, raw_msg):
|
||||
params = msg["params"]
|
||||
db_status = self.db['status']
|
||||
for k, v in params.get("status", {}).items():
|
||||
db_status.setdefault(k, {}).update(v)
|
||||
eventtime = params['eventtime']
|
||||
if eventtime >= self.next_index_time:
|
||||
self.next_index_time = eventtime + INDEX_UPDATE_TIME
|
||||
self.flush_index()
|
||||
|
||||
def nice():
|
||||
try:
|
||||
# Try to re-nice writing process
|
||||
os.nice(10)
|
||||
except:
|
||||
pass
|
||||
|
||||
def main():
|
||||
usage = "%prog [options] <socket filename> <log name>"
|
||||
opts = optparse.OptionParser(usage)
|
||||
options, args = opts.parse_args()
|
||||
if len(args) != 2:
|
||||
opts.error("Incorrect number of arguments")
|
||||
|
||||
nice()
|
||||
dl = DataLogger(args[0], args[1])
|
||||
dl.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
149
scripts/motan/motan_graph.py
Normal file
149
scripts/motan/motan_graph.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python
|
||||
# Script to perform motion analysis and graphing
|
||||
#
|
||||
# Copyright (C) 2019-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import sys, optparse, ast
|
||||
import matplotlib
|
||||
import readlog, analyzers
|
||||
try:
|
||||
import urlparse
|
||||
except:
|
||||
import urllib.parse as urlparse
|
||||
|
||||
|
||||
######################################################################
|
||||
# Graphing
|
||||
######################################################################
|
||||
|
||||
def plot_motion(amanager, graphs, log_prefix):
|
||||
# Generate data
|
||||
for graph in graphs:
|
||||
for dataset, plot_params in graph:
|
||||
amanager.setup_dataset(dataset)
|
||||
amanager.generate_datasets()
|
||||
datasets = amanager.get_datasets()
|
||||
times = amanager.get_dataset_times()
|
||||
# Build plot
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
fig, rows = matplotlib.pyplot.subplots(nrows=len(graphs), sharex=True)
|
||||
if len(graphs) == 1:
|
||||
rows = [rows]
|
||||
rows[0].set_title("Motion Analysis (%s)" % (log_prefix,))
|
||||
for graph, graph_ax in zip(graphs, rows):
|
||||
graph_units = graph_twin_units = twin_ax = None
|
||||
for dataset, plot_params in graph:
|
||||
label = amanager.get_label(dataset)
|
||||
ax = graph_ax
|
||||
if graph_units is None:
|
||||
graph_units = label['units']
|
||||
ax.set_ylabel(graph_units)
|
||||
elif label['units'] != graph_units:
|
||||
if graph_twin_units is None:
|
||||
ax = twin_ax = graph_ax.twinx()
|
||||
graph_twin_units = label['units']
|
||||
ax.set_ylabel(graph_twin_units)
|
||||
elif label['units'] == graph_twin_units:
|
||||
ax = twin_ax
|
||||
else:
|
||||
graph_units = "Unknown"
|
||||
ax.set_ylabel(graph_units)
|
||||
pparams = {'label': label['label'], 'alpha': 0.8}
|
||||
pparams.update(plot_params)
|
||||
ax.plot(times, datasets[dataset], **pparams)
|
||||
if twin_ax is not None:
|
||||
li1, la1 = graph_ax.get_legend_handles_labels()
|
||||
li2, la2 = twin_ax.get_legend_handles_labels()
|
||||
twin_ax.legend(li1 + li2, la1 + la2, loc='best', prop=fontP)
|
||||
else:
|
||||
graph_ax.legend(loc='best', prop=fontP)
|
||||
graph_ax.grid(True)
|
||||
rows[-1].set_xlabel('Time (s)')
|
||||
return fig
|
||||
|
||||
|
||||
######################################################################
|
||||
# Startup
|
||||
######################################################################
|
||||
|
||||
def setup_matplotlib(output_to_file):
|
||||
global matplotlib
|
||||
if output_to_file:
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||
import matplotlib.ticker
|
||||
|
||||
def parse_graph_description(desc):
|
||||
if '?' not in desc:
|
||||
return (desc, {})
|
||||
dataset, params = desc.split('?', 1)
|
||||
params = {k: v for k, v in urlparse.parse_qsl(params)}
|
||||
for fkey in ['alpha']:
|
||||
if fkey in params:
|
||||
params[fkey] = float(params[fkey])
|
||||
return (dataset, params)
|
||||
|
||||
def list_datasets():
|
||||
datasets = readlog.list_datasets() + analyzers.list_datasets()
|
||||
out = ["\nAvailable datasets:\n"]
|
||||
for dataset, desc in datasets:
|
||||
out.append("%-24s: %s\n" % (dataset, desc))
|
||||
out.append("\n")
|
||||
sys.stdout.write("".join(out))
|
||||
sys.exit(0)
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
usage = "%prog [options] <logname>"
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-o", "--output", type="string", dest="output",
|
||||
default=None, help="filename of output graph")
|
||||
opts.add_option("-s", "--skip", type="float", default=0.,
|
||||
help="Set the start time to graph")
|
||||
opts.add_option("-d", "--duration", type="float", default=5.,
|
||||
help="Number of seconds to graph")
|
||||
opts.add_option("--segment-time", type="float", default=0.000100,
|
||||
help="Analysis segment time (default 0.000100 seconds)")
|
||||
opts.add_option("-g", "--graph", help="Graph to generate (python literal)")
|
||||
opts.add_option("-l", "--list-datasets", action="store_true",
|
||||
help="List available datasets")
|
||||
options, args = opts.parse_args()
|
||||
if options.list_datasets:
|
||||
list_datasets()
|
||||
if len(args) != 1:
|
||||
opts.error("Incorrect number of arguments")
|
||||
log_prefix = args[0]
|
||||
|
||||
# Open data files
|
||||
lmanager = readlog.LogManager(log_prefix)
|
||||
lmanager.setup_index()
|
||||
lmanager.seek_time(options.skip)
|
||||
amanager = analyzers.AnalyzerManager(lmanager, options.segment_time)
|
||||
amanager.set_duration(options.duration)
|
||||
|
||||
# Default graphs to draw
|
||||
graph_descs = [
|
||||
["trapq(toolhead,velocity)?color=green"],
|
||||
["trapq(toolhead,accel)?color=green"],
|
||||
["deviation(stepq(stepper_x),kin(stepper_x))?color=blue"],
|
||||
]
|
||||
if options.graph is not None:
|
||||
graph_descs = ast.literal_eval(options.graph)
|
||||
graphs = [[parse_graph_description(g) for g in graph_row]
|
||||
for graph_row in graph_descs]
|
||||
|
||||
# Draw graph
|
||||
setup_matplotlib(options.output is not None)
|
||||
fig = plot_motion(amanager, graphs, log_prefix)
|
||||
|
||||
# Show graph
|
||||
if options.output is None:
|
||||
matplotlib.pyplot.show()
|
||||
else:
|
||||
fig.set_size_inches(8, 6)
|
||||
fig.savefig(options.output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
629
scripts/motan/readlog.py
Normal file
629
scripts/motan/readlog.py
Normal file
@@ -0,0 +1,629 @@
|
||||
# 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']
|
||||
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']
|
||||
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
|
||||
|
||||
|
||||
######################################################################
|
||||
# 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
|
||||
4
test/configs/at90usb1286.config
Normal file
4
test/configs/at90usb1286.config
Normal file
@@ -0,0 +1,4 @@
|
||||
# Base config file for atmega at90usb1286
|
||||
CONFIG_MACH_AVR=y
|
||||
CONFIG_MACH_at90usb1286=y
|
||||
CONFIG_CLOCK_FREQ=16000000
|
||||
4
test/configs/atmega1280.config
Normal file
4
test/configs/atmega1280.config
Normal file
@@ -0,0 +1,4 @@
|
||||
# Base config file for atmega1280
|
||||
CONFIG_MACH_AVR=y
|
||||
CONFIG_MACH_atmega1280=y
|
||||
CONFIG_CLOCK_FREQ=16000000
|
||||
4
test/configs/atmega1284p.config
Normal file
4
test/configs/atmega1284p.config
Normal file
@@ -0,0 +1,4 @@
|
||||
# Base config file for atmega1284p
|
||||
CONFIG_MACH_AVR=y
|
||||
CONFIG_MACH_atmega1284p=y
|
||||
CONFIG_CLOCK_FREQ=16000000
|
||||
4
test/configs/atmega2560.config
Normal file
4
test/configs/atmega2560.config
Normal file
@@ -0,0 +1,4 @@
|
||||
# Base config file for atmega2560
|
||||
CONFIG_MACH_AVR=y
|
||||
CONFIG_MACH_atmega2560=y
|
||||
CONFIG_CLOCK_FREQ=16000000
|
||||
4
test/configs/atmega328.config
Normal file
4
test/configs/atmega328.config
Normal file
@@ -0,0 +1,4 @@
|
||||
# Base config file for atmega328
|
||||
CONFIG_MACH_AVR=y
|
||||
CONFIG_MACH_atmega328=y
|
||||
CONFIG_CLOCK_FREQ=16000000
|
||||
4
test/configs/atmega644p.config
Normal file
4
test/configs/atmega644p.config
Normal file
@@ -0,0 +1,4 @@
|
||||
# Base config file for atmega644p
|
||||
CONFIG_MACH_AVR=y
|
||||
CONFIG_MACH_atmega644p=y
|
||||
CONFIG_CLOCK_FREQ=16000000
|
||||
2
test/configs/hostsimulator.config
Normal file
2
test/configs/hostsimulator.config
Normal file
@@ -0,0 +1,2 @@
|
||||
# Base config file for host simulator
|
||||
CONFIG_MACH_SIMU=y
|
||||
2
test/configs/linuxprocess.config
Normal file
2
test/configs/linuxprocess.config
Normal file
@@ -0,0 +1,2 @@
|
||||
# Base config file for linux process
|
||||
CONFIG_MACH_LINUX=y
|
||||
2
test/configs/lpc176x.config
Normal file
2
test/configs/lpc176x.config
Normal file
@@ -0,0 +1,2 @@
|
||||
# Base config file for LPC176x boards
|
||||
CONFIG_MACH_LPC176X=y
|
||||
2
test/configs/pru.config
Normal file
2
test/configs/pru.config
Normal file
@@ -0,0 +1,2 @@
|
||||
# Base config file for the Beaglebone PRU
|
||||
CONFIG_MACH_PRU=y
|
||||
2
test/configs/rp2040.config
Normal file
2
test/configs/rp2040.config
Normal file
@@ -0,0 +1,2 @@
|
||||
# Base config file for rp2040 boards
|
||||
CONFIG_MACH_RP2040=y
|
||||
3
test/configs/sam3x8c.config
Normal file
3
test/configs/sam3x8c.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for Atmel SAM3x8e ARM processor
|
||||
CONFIG_MACH_ATSAM=y
|
||||
CONFIG_MACH_SAM3X8C=y
|
||||
3
test/configs/sam3x8e.config
Normal file
3
test/configs/sam3x8e.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for Atmel SAM3x8e ARM processor
|
||||
CONFIG_MACH_ATSAM=y
|
||||
CONFIG_MACH_SAM3X8E=y
|
||||
3
test/configs/sam4e8e.config
Normal file
3
test/configs/sam4e8e.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for Atmel SAM4E8E ARM processor
|
||||
CONFIG_MACH_ATSAM=y
|
||||
CONFIG_MACH_SAM4E8E=y
|
||||
3
test/configs/sam4s8c.config
Normal file
3
test/configs/sam4s8c.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for Atmel SAM4S8C ARM processor
|
||||
CONFIG_MACH_ATSAM=y
|
||||
CONFIG_MACH_SAM4S8C=y
|
||||
3
test/configs/samd21g18.config
Normal file
3
test/configs/samd21g18.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for SAMD21 boards
|
||||
CONFIG_MACH_ATSAMD=y
|
||||
CONFIG_MACH_SAMD21G18=y
|
||||
4
test/configs/samd51p20.config
Normal file
4
test/configs/samd51p20.config
Normal file
@@ -0,0 +1,4 @@
|
||||
# Base config file for Atmel SAMD51P20 ARM processor
|
||||
CONFIG_MACH_ATSAMD=y
|
||||
CONFIG_MACH_SAMD51P20=y
|
||||
CONFIG_CLOCK_REF_X25M=y
|
||||
3
test/configs/same70q20b.config
Normal file
3
test/configs/same70q20b.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for Atmel SAME70Q20B ARM processor
|
||||
CONFIG_MACH_ATSAM=y
|
||||
CONFIG_MACH_SAME70Q20B=y
|
||||
3
test/configs/stm32f031.config
Normal file
3
test/configs/stm32f031.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for STM32F031 boards
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32F031=y
|
||||
3
test/configs/stm32f070.config
Normal file
3
test/configs/stm32f070.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for STM32F070 ARM processor
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32F070=y
|
||||
4
test/configs/stm32f103-serial.config
Normal file
4
test/configs/stm32f103-serial.config
Normal file
@@ -0,0 +1,4 @@
|
||||
# Base config file for STM32F1 ARM processor using serial communication
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32F103=y
|
||||
CONFIG_STM32_SERIAL_USART1=y
|
||||
3
test/configs/stm32f103.config
Normal file
3
test/configs/stm32f103.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for STM32F1 ARM processor
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32F103=y
|
||||
3
test/configs/stm32f405.config
Normal file
3
test/configs/stm32f405.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for STM32F405 ARM processor
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32F405=y
|
||||
3
test/configs/stm32f407.config
Normal file
3
test/configs/stm32f407.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for STM32F407 ARM processor
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32F407=y
|
||||
3
test/configs/stm32f429.config
Normal file
3
test/configs/stm32f429.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for STM32F429 ARM processor
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32F429=y
|
||||
3
test/configs/stm32f446.config
Normal file
3
test/configs/stm32f446.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for STM32F446 ARM processor
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32F446=y
|
||||
3
test/configs/stm32g0b1.config
Normal file
3
test/configs/stm32g0b1.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for STM32G0B1 ARM processor
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32G0B1=y
|
||||
3
test/configs/stm32h743.config
Normal file
3
test/configs/stm32h743.config
Normal file
@@ -0,0 +1,3 @@
|
||||
# Base config file for STM32H743 ARM processor
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32H743=y
|
||||
52
test/klippy/bed_screws.cfg
Normal file
52
test/klippy/bed_screws.cfg
Normal file
@@ -0,0 +1,52 @@
|
||||
# Test config for bed screws tool
|
||||
[stepper_x]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
rotation_distance: 40
|
||||
microsteps: 16
|
||||
endstop_pin: ^PE5
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_y]
|
||||
step_pin: PF6
|
||||
dir_pin: !PF7
|
||||
enable_pin: !PF2
|
||||
rotation_distance: 40
|
||||
microsteps: 16
|
||||
endstop_pin: ^PJ1
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_z]
|
||||
step_pin: PL3
|
||||
dir_pin: PL1
|
||||
enable_pin: !PK0
|
||||
rotation_distance: 8
|
||||
microsteps: 16
|
||||
endstop_pin: ^PD3
|
||||
position_endstop: 0.5
|
||||
position_max: 200
|
||||
|
||||
[bed_screws]
|
||||
screw1: 100,50
|
||||
screw1_name: Front right
|
||||
screw1_fine_adjust: 200,50
|
||||
screw2: 75,75
|
||||
screw2_fine_adjust: 200,75
|
||||
screw3: 75,75
|
||||
screw3_name: Last
|
||||
screw3_fine_adjust: 75,90
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
max_z_velocity: 5
|
||||
max_z_accel: 100
|
||||
49
test/klippy/bed_screws.test
Normal file
49
test/klippy/bed_screws.test
Normal file
@@ -0,0 +1,49 @@
|
||||
# Test case for bed screws helper tool
|
||||
CONFIG bed_screws.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
# Start helper script and then abort it
|
||||
G28
|
||||
BED_SCREWS_ADJUST
|
||||
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
ABORT
|
||||
|
||||
# Start helper script and run until success
|
||||
BED_SCREWS_ADJUST
|
||||
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
|
||||
# Start helper script and run with two readjusts
|
||||
BED_SCREWS_ADJUST
|
||||
|
||||
ACCEPT
|
||||
ADJUSTED
|
||||
ACCEPT
|
||||
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
|
||||
ADJUSTED
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
ACCEPT
|
||||
|
||||
# Start helper script and run with two readjusts
|
||||
BED_SCREWS_ADJUST
|
||||
ABORT
|
||||
76
test/klippy/bltouch.cfg
Normal file
76
test/klippy/bltouch.cfg
Normal file
@@ -0,0 +1,76 @@
|
||||
# Test config for bltouch
|
||||
[stepper_x]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PE5
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_y]
|
||||
step_pin: PF6
|
||||
dir_pin: !PF7
|
||||
enable_pin: !PF2
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PJ1
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_z]
|
||||
step_pin: PL3
|
||||
dir_pin: PL1
|
||||
enable_pin: !PK0
|
||||
microsteps: 16
|
||||
rotation_distance: 8
|
||||
endstop_pin: probe:z_virtual_endstop
|
||||
position_max: 200
|
||||
|
||||
[extruder]
|
||||
step_pin: PA4
|
||||
dir_pin: PA6
|
||||
enable_pin: !PA2
|
||||
microsteps: 16
|
||||
rotation_distance: 33.5
|
||||
nozzle_diameter: 0.400
|
||||
filament_diameter: 1.750
|
||||
heater_pin: PB4
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK5
|
||||
control: pid
|
||||
pid_Kp: 22.2
|
||||
pid_Ki: 1.08
|
||||
pid_Kd: 114
|
||||
min_temp: 0
|
||||
max_temp: 250
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PH5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK6
|
||||
control: watermark
|
||||
min_temp: 0
|
||||
max_temp: 130
|
||||
|
||||
[bltouch]
|
||||
sensor_pin: PC7
|
||||
control_pin: PC5
|
||||
z_offset: 1.15
|
||||
|
||||
[bed_mesh]
|
||||
mesh_min: 10,10
|
||||
mesh_max: 180,180
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
max_z_velocity: 5
|
||||
max_z_accel: 100
|
||||
29
test/klippy/bltouch.test
Normal file
29
test/klippy/bltouch.test
Normal file
@@ -0,0 +1,29 @@
|
||||
# Test case for bltouch support
|
||||
CONFIG bltouch.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
# Start by homing the printer.
|
||||
G28
|
||||
G1 F6000
|
||||
|
||||
# Z / X / Y moves
|
||||
G1 Z1
|
||||
G1 X1
|
||||
G1 Y1
|
||||
|
||||
# Run BLTOUCH_DEBUG
|
||||
BLTOUCH_DEBUG
|
||||
BLTOUCH_DEBUG COMMAND=reset
|
||||
|
||||
# Run bed_mesh_calibrate
|
||||
BED_MESH_CALIBRATE
|
||||
|
||||
# Move again
|
||||
G1 Z5 X0 Y0
|
||||
|
||||
# Do regular probe
|
||||
PROBE
|
||||
QUERY_PROBE
|
||||
|
||||
# Move again
|
||||
G1 Z9
|
||||
46
test/klippy/commands.test
Normal file
46
test/klippy/commands.test
Normal file
@@ -0,0 +1,46 @@
|
||||
# Tests for miscellaneous g-code commands
|
||||
DICTIONARY atmega2560.dict
|
||||
CONFIG ../../config/example-cartesian.cfg
|
||||
|
||||
# Simple status commands
|
||||
GET_POSITION
|
||||
M114
|
||||
|
||||
STATUS
|
||||
|
||||
HELP
|
||||
|
||||
QUERY_ENDSTOPS
|
||||
|
||||
M115
|
||||
|
||||
M18
|
||||
|
||||
# G-code state commands
|
||||
G28
|
||||
SAVE_GCODE_STATE
|
||||
G92 Z-5
|
||||
G92 E5
|
||||
SAVE_GCODE_STATE NAME=test
|
||||
G1 Z-5
|
||||
G91
|
||||
G1 Z0
|
||||
RESTORE_GCODE_STATE NAME=test
|
||||
G1 Z-5
|
||||
RESTORE_GCODE_STATE
|
||||
G1 Z0 E0
|
||||
RESTORE_GCODE_STATE MOVE=1
|
||||
|
||||
# Update commands
|
||||
SET_GCODE_OFFSET Z=.1
|
||||
M206 Z-.2
|
||||
SET_GCODE_OFFSET Z_ADJUST=-.1
|
||||
|
||||
SET_VELOCITY_LIMIT ACCEL=100 VELOCITY=20 SQUARE_CORNER_VELOCITY=1 ACCEL_TO_DECEL=200
|
||||
M204 S500
|
||||
|
||||
SET_PRESSURE_ADVANCE EXTRUDER=extruder ADVANCE=.001
|
||||
SET_PRESSURE_ADVANCE ADVANCE=.002 ADVANCE_LOOKAHEAD_TIME=.001
|
||||
|
||||
# Restart command (must be last in test)
|
||||
RESTART
|
||||
47
test/klippy/delta.test
Normal file
47
test/klippy/delta.test
Normal file
@@ -0,0 +1,47 @@
|
||||
# Test case for basic movement on delta printers
|
||||
CONFIG ../../config/example-delta.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
# Start by homing the printer. Also tests Z moves.
|
||||
G28
|
||||
|
||||
# Perform an XY+Z move with infintesimal XY component
|
||||
G1 x0 y0 z15
|
||||
|
||||
# Perform an XY move along Y axis (aligned with rear tower)
|
||||
G1 x0 y5 z15
|
||||
|
||||
# Perform an XY+Z move along Y axis
|
||||
G1 x0 y-5 z10
|
||||
|
||||
# Perform a Z move
|
||||
G1 x0 y-5 z15
|
||||
|
||||
# Perform an XY move across all three towers
|
||||
G1 x2 y2 z10
|
||||
|
||||
# Perform an XY+Z move with tiny Z movement
|
||||
G1 x2 y-10 z10.1
|
||||
|
||||
# Move to far away position
|
||||
G1 x140 y0
|
||||
|
||||
# Move to extreme position
|
||||
G1 x145 y0
|
||||
|
||||
# Move to another extreme position
|
||||
G1 x145 y5
|
||||
|
||||
# Test delta_calibrate command
|
||||
DELTA_CALIBRATE
|
||||
|
||||
# Dummy move
|
||||
G1 Z5 X0 Y0
|
||||
|
||||
# Verify stepper_buzz
|
||||
STEPPER_BUZZ STEPPER=stepper_a
|
||||
STEPPER_BUZZ STEPPER=stepper_b
|
||||
STEPPER_BUZZ STEPPER=stepper_c
|
||||
|
||||
# Dummy move
|
||||
G1 Z3 X2 Y3
|
||||
76
test/klippy/delta_calibrate.cfg
Normal file
76
test/klippy/delta_calibrate.cfg
Normal file
@@ -0,0 +1,76 @@
|
||||
# Test config for the DELTA_CALIBRATE command (on linear delta robots)
|
||||
[stepper_a]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
microsteps: 16
|
||||
rotation_distance: 0.32
|
||||
endstop_pin: ^PE4
|
||||
homing_speed: 50
|
||||
#position_endstop: 297.05
|
||||
#arm_length: 333.0
|
||||
|
||||
[stepper_b]
|
||||
step_pin: PF6
|
||||
dir_pin: PF7
|
||||
enable_pin: !PF2
|
||||
microsteps: 16
|
||||
rotation_distance: 0.32
|
||||
endstop_pin: ^PJ0
|
||||
|
||||
[stepper_c]
|
||||
step_pin: PL3
|
||||
dir_pin: PL1
|
||||
enable_pin: !PK0
|
||||
microsteps: 16
|
||||
rotation_distance: 0.32
|
||||
endstop_pin: ^PD2
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: delta
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
#delta_radius: 174.75
|
||||
|
||||
[delta_calibrate]
|
||||
radius: 50
|
||||
|
||||
#*# <---------------------- SAVE_CONFIG ---------------------->
|
||||
#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated.
|
||||
#*#
|
||||
#*# [printer]
|
||||
#*# delta_radius = 174.750004
|
||||
#*#
|
||||
#*# [stepper_a]
|
||||
#*# angle = 210.000032
|
||||
#*# arm_length = 333.000032
|
||||
#*# position_endstop = 297.049970
|
||||
#*#
|
||||
#*# [stepper_b]
|
||||
#*# angle = 329.999997
|
||||
#*# arm_length = 332.999811
|
||||
#*# position_endstop = 297.050132
|
||||
#*#
|
||||
#*# [stepper_c]
|
||||
#*# angle = 90.000000
|
||||
#*# arm_length = 333.000181
|
||||
#*# position_endstop = 297.049900
|
||||
#*#
|
||||
#*# [delta_calibrate]
|
||||
#*# height0 = 0.0
|
||||
#*# height0_pos = 2970499.999,2970499.999,2970499.999
|
||||
#*# height1 = 0.0
|
||||
#*# height1_pos = 3163266.999,3163266.999,2727853.999
|
||||
#*# height2 = 0.0
|
||||
#*# height2_pos = 2869315.999,3303154.999,2869315.999
|
||||
#*# height3 = 0.0
|
||||
#*# height3_pos = 2749008.999,3138330.999,3138330.999
|
||||
#*# height4 = 0.0
|
||||
#*# height4_pos = 2885497.999,2885497.999,3218746.999
|
||||
#*# height5 = 0.0
|
||||
#*# height5_pos = 3114555.999,2771134.999,3114555.999
|
||||
#*# height6 = 0.0
|
||||
#*# height6_pos = 3260109.999,2876968.999,2876968.999
|
||||
31
test/klippy/delta_calibrate.test
Normal file
31
test/klippy/delta_calibrate.test
Normal file
@@ -0,0 +1,31 @@
|
||||
# Test case for basic movement on delta printers
|
||||
CONFIG delta_calibrate.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
# Start by homing the printer.
|
||||
G28
|
||||
|
||||
# Run basic delta calibration (in manual mode)
|
||||
DELTA_CALIBRATE METHOD=manual
|
||||
G1 Z0.1
|
||||
ACCEPT
|
||||
G1 Z0.1
|
||||
ACCEPT
|
||||
G1 Z0.1
|
||||
ACCEPT
|
||||
G1 Z0.1
|
||||
ACCEPT
|
||||
G1 Z0.1
|
||||
ACCEPT
|
||||
G1 Z0.1
|
||||
ACCEPT
|
||||
G1 Z0.1
|
||||
ACCEPT
|
||||
|
||||
# Run extended delta calibration
|
||||
DELTA_ANALYZE CENTER_DISTS=74,74,74,74,74,74
|
||||
DELTA_ANALYZE OUTER_DISTS=74,74,74,74,74,74
|
||||
DELTA_ANALYZE CENTER_PILLAR_WIDTHS=9,9,9
|
||||
DELTA_ANALYZE OUTER_PILLAR_WIDTHS=9,9,9,9,9,9
|
||||
DELTA_ANALYZE SCALE=1
|
||||
DELTA_ANALYZE CALIBRATE=extended
|
||||
125
test/klippy/dual_carriage.cfg
Normal file
125
test/klippy/dual_carriage.cfg
Normal file
@@ -0,0 +1,125 @@
|
||||
# Test config with dual carriage and multiple extruders
|
||||
[stepper_x]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PE5
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[dual_carriage]
|
||||
axis: x
|
||||
step_pin: PH1
|
||||
dir_pin: PH0
|
||||
enable_pin: !PA1
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PE4
|
||||
position_endstop: 200
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_y]
|
||||
step_pin: PF6
|
||||
dir_pin: !PF7
|
||||
enable_pin: !PF2
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PJ1
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_z]
|
||||
step_pin: PL3
|
||||
dir_pin: PL1
|
||||
enable_pin: !PK0
|
||||
microsteps: 16
|
||||
rotation_distance: 8
|
||||
endstop_pin: ^PD3
|
||||
position_endstop: 0.5
|
||||
position_max: 200
|
||||
|
||||
[extruder]
|
||||
step_pin: PA4
|
||||
dir_pin: PA6
|
||||
enable_pin: !PA2
|
||||
microsteps: 16
|
||||
rotation_distance: 33.5
|
||||
nozzle_diameter: 0.400
|
||||
filament_diameter: 1.750
|
||||
heater_pin: PB4
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK5
|
||||
control: pid
|
||||
pid_Kp: 22.2
|
||||
pid_Ki: 1.08
|
||||
pid_Kd: 114
|
||||
min_temp: 0
|
||||
max_temp: 250
|
||||
|
||||
[gcode_macro PARK_extruder0]
|
||||
gcode:
|
||||
G90
|
||||
G1 X0
|
||||
|
||||
[gcode_macro T0]
|
||||
gcode:
|
||||
PARK_{printer.toolhead.extruder}
|
||||
ACTIVATE_EXTRUDER EXTRUDER=extruder
|
||||
SET_DUAL_CARRIAGE CARRIAGE=0
|
||||
|
||||
[extruder1]
|
||||
step_pin: PC1
|
||||
dir_pin: PC3
|
||||
enable_pin: !PC7
|
||||
microsteps: 16
|
||||
rotation_distance: 33.5
|
||||
nozzle_diameter: 0.400
|
||||
filament_diameter: 1.750
|
||||
heater_pin: PB5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK7
|
||||
control: pid
|
||||
pid_Kp: 22.2
|
||||
pid_Ki: 1.08
|
||||
pid_Kd: 114
|
||||
min_temp: 0
|
||||
max_temp: 250
|
||||
|
||||
[gcode_macro PARK_extruder1]
|
||||
gcode:
|
||||
SET_SERVO SERVO=my_servo angle=100
|
||||
G90
|
||||
G1 X200
|
||||
|
||||
[gcode_macro T1]
|
||||
gcode:
|
||||
PARK_{printer.toolhead.extruder}
|
||||
SET_SERVO SERVO=my_servo angle=50
|
||||
ACTIVATE_EXTRUDER EXTRUDER=extruder1
|
||||
SET_DUAL_CARRIAGE CARRIAGE=1
|
||||
|
||||
[servo my_servo]
|
||||
pin: PH4
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PH5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK6
|
||||
control: watermark
|
||||
min_temp: 0
|
||||
max_temp: 130
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
max_z_velocity: 5
|
||||
max_z_accel: 100
|
||||
39
test/klippy/dual_carriage.test
Normal file
39
test/klippy/dual_carriage.test
Normal file
@@ -0,0 +1,39 @@
|
||||
# Test cases on printers with dual carriage and multiple extruders
|
||||
CONFIG dual_carriage.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
# First home the printer
|
||||
G90
|
||||
G28
|
||||
|
||||
# Perform a dummy move
|
||||
G1 X10 F6000
|
||||
|
||||
# Activate alternate carriage
|
||||
SET_DUAL_CARRIAGE CARRIAGE=1
|
||||
G1 X190 F6000
|
||||
|
||||
# Go back to main carriage
|
||||
SET_DUAL_CARRIAGE CARRIAGE=0
|
||||
G1 X20 F6000
|
||||
|
||||
# Test changing extruders
|
||||
G1 X5
|
||||
T1
|
||||
G91
|
||||
G1 X-10 E.2
|
||||
T0
|
||||
G91
|
||||
G1 X20 E.2
|
||||
G90
|
||||
|
||||
QUERY_ENDSTOPS
|
||||
|
||||
# Servo tests
|
||||
SET_SERVO servo=my_servo angle=160
|
||||
SET_SERVO servo=my_servo angle=130
|
||||
|
||||
# Verify STEPPER_BUZZ
|
||||
STEPPER_BUZZ STEPPER=dual_carriage
|
||||
STEPPER_BUZZ STEPPER=extruder
|
||||
STEPPER_BUZZ STEPPER=extruder1
|
||||
117
test/klippy/exclude_object.cfg
Normal file
117
test/klippy/exclude_object.cfg
Normal file
@@ -0,0 +1,117 @@
|
||||
[stepper_x]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PE5
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_y]
|
||||
step_pin: PF6
|
||||
dir_pin: !PF7
|
||||
enable_pin: !PF2
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PJ1
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_z]
|
||||
step_pin: PL3
|
||||
dir_pin: PL1
|
||||
enable_pin: !PK0
|
||||
microsteps: 16
|
||||
rotation_distance: 8
|
||||
endstop_pin: ^PD3
|
||||
position_endstop: 0.5
|
||||
position_max: 200
|
||||
|
||||
[extruder]
|
||||
step_pin: PA4
|
||||
dir_pin: PA6
|
||||
enable_pin: !PA2
|
||||
microsteps: 16
|
||||
rotation_distance: 33.5
|
||||
nozzle_diameter: 0.500
|
||||
filament_diameter: 3.500
|
||||
heater_pin: PB4
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK5
|
||||
control: pid
|
||||
pid_Kp: 22.2
|
||||
pid_Ki: 1.08
|
||||
pid_Kd: 114
|
||||
min_temp: 0
|
||||
max_temp: 210
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PH5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK6
|
||||
control: watermark
|
||||
min_temp: 0
|
||||
max_temp: 110
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
max_z_velocity: 5
|
||||
max_z_accel: 100
|
||||
|
||||
# Test config for exclude_object
|
||||
[exclude_object]
|
||||
|
||||
[gcode_macro M486]
|
||||
gcode:
|
||||
# Parameters known to M486 are as follows:
|
||||
# [C<flag>] Cancel the current object
|
||||
# [P<index>] Cancel the object with the given index
|
||||
# [S<index>] Set the index of the current object.
|
||||
# If the object with the given index has been canceled, this will cause
|
||||
# the firmware to skip to the next object. The value -1 is used to
|
||||
# indicate something that isn’t an object and shouldn’t be skipped.
|
||||
# [T<count>] Reset the state and set the number of objects
|
||||
# [U<index>] Un-cancel the object with the given index. This command will be
|
||||
# ignored if the object has already been skipped
|
||||
|
||||
{% if 'exclude_object' not in printer %}
|
||||
{action_raise_error("[exclude_object] is not enabled")}
|
||||
{% endif %}
|
||||
|
||||
{% if 'T' in params %}
|
||||
EXCLUDE_OBJECT RESET=1
|
||||
|
||||
{% for i in range(params.T | int) %}
|
||||
EXCLUDE_OBJECT_DEFINE NAME={i}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if 'C' in params %}
|
||||
EXCLUDE_OBJECT CURRENT=1
|
||||
{% endif %}
|
||||
|
||||
{% if 'P' in params %}
|
||||
EXCLUDE_OBJECT NAME={params.P}
|
||||
{% endif %}
|
||||
|
||||
{% if 'S' in params %}
|
||||
{% if params.S == '-1' %}
|
||||
{% if printer.exclude_object.current_object %}
|
||||
EXCLUDE_OBJECT_END NAME={printer.exclude_object.current_object}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
EXCLUDE_OBJECT_START NAME={params.S}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if 'U' in params %}
|
||||
EXCLUDE_OBJECT RESET=1 NAME={params.U}
|
||||
{% endif %}
|
||||
126
test/klippy/exclude_object.test
Normal file
126
test/klippy/exclude_object.test
Normal file
@@ -0,0 +1,126 @@
|
||||
DICTIONARY atmega2560.dict
|
||||
CONFIG exclude_object.cfg
|
||||
|
||||
|
||||
G28
|
||||
M83
|
||||
|
||||
M486 T3
|
||||
|
||||
M486 S0
|
||||
G0 X10
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
|
||||
M486 S1
|
||||
G0 X11
|
||||
M486 C
|
||||
|
||||
# "Prime" the transform
|
||||
G1 X140 E0.5
|
||||
G1 X160 E0.5
|
||||
G1 X140 E0.5
|
||||
G1 X160 E0.5
|
||||
G1 X140 E0.5
|
||||
G1 X160 E0.5
|
||||
G1 X140 E0.5
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
|
||||
M486 S2
|
||||
G0 X13
|
||||
|
||||
M486 S0
|
||||
G0 X10
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
|
||||
M486 S1
|
||||
G0 X-11
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
|
||||
M486 S2
|
||||
G0 X13
|
||||
|
||||
M486 P2
|
||||
EXCLUDE_OBJECT
|
||||
|
||||
M486 S0
|
||||
G0 X10
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
|
||||
M486 S1
|
||||
G0 X-11
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
|
||||
M486 S2
|
||||
G0 X-13
|
||||
|
||||
M486 U2
|
||||
EXCLUDE_OBJECT
|
||||
|
||||
M486 S0
|
||||
G0 X10
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
|
||||
M486 S1
|
||||
G0 X-11
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
|
||||
M486 S2
|
||||
G0 X13
|
||||
|
||||
M486 P0
|
||||
M486 P1
|
||||
M486 P2
|
||||
EXCLUDE_OBJECT
|
||||
|
||||
M486 S0
|
||||
G0 X-10
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
|
||||
M486 S1
|
||||
G0 X-11
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
|
||||
M486 S2
|
||||
G0 X-13
|
||||
|
||||
|
||||
M486 S66
|
||||
G0 X66
|
||||
|
||||
M486 S-1
|
||||
G0 X0
|
||||
M486 P66
|
||||
|
||||
M486 S66
|
||||
G0 X-66
|
||||
|
||||
M486 T3
|
||||
|
||||
M486 S0
|
||||
G0 X10
|
||||
|
||||
M486 S1
|
||||
G0 X11
|
||||
|
||||
M486 S2
|
||||
G0 X13
|
||||
68
test/klippy/extruders.cfg
Normal file
68
test/klippy/extruders.cfg
Normal file
@@ -0,0 +1,68 @@
|
||||
# Config for extruder testing
|
||||
[stepper_x]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PE5
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_y]
|
||||
step_pin: PF6
|
||||
dir_pin: !PF7
|
||||
enable_pin: !PF2
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PJ1
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_z]
|
||||
step_pin: PL3
|
||||
dir_pin: PL1
|
||||
enable_pin: !PK0
|
||||
microsteps: 16
|
||||
rotation_distance: 8
|
||||
endstop_pin: ^PD3
|
||||
position_endstop: 0.5
|
||||
position_max: 200
|
||||
|
||||
[extruder]
|
||||
step_pin: PA4
|
||||
dir_pin: PA6
|
||||
enable_pin: !PA2
|
||||
microsteps: 16
|
||||
rotation_distance: 33.5
|
||||
nozzle_diameter: 0.500
|
||||
filament_diameter: 3.500
|
||||
heater_pin: PB4
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK5
|
||||
control: pid
|
||||
pid_Kp: 22.2
|
||||
pid_Ki: 1.08
|
||||
pid_Kd: 114
|
||||
min_temp: 0
|
||||
max_temp: 210
|
||||
|
||||
[extruder_stepper my_extra_stepper]
|
||||
extruder: extruder
|
||||
step_pin: PH5
|
||||
dir_pin: PH6
|
||||
enable_pin: !PB5
|
||||
microsteps: 16
|
||||
rotation_distance: 28.2
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
max_z_velocity: 5
|
||||
max_z_accel: 100
|
||||
50
test/klippy/extruders.test
Normal file
50
test/klippy/extruders.test
Normal file
@@ -0,0 +1,50 @@
|
||||
# Extruder tests
|
||||
DICTIONARY atmega2560.dict
|
||||
CONFIG extruders.cfg
|
||||
|
||||
# Extrude only
|
||||
G1 E5
|
||||
G1 E-2
|
||||
G1 E7
|
||||
|
||||
# Home and extrusion moves
|
||||
G28
|
||||
G1 X20 Y20 Z1
|
||||
G1 X25 Y25 E7.5
|
||||
|
||||
# Update step_distance
|
||||
SET_EXTRUDER_ROTATION_DISTANCE EXTRUDER=extruder DISTANCE=33.2
|
||||
G1 X30 Y30 E8.0
|
||||
|
||||
# Reverse step_distance
|
||||
SET_EXTRUDER_ROTATION_DISTANCE EXTRUDER=extruder DISTANCE=-33.1
|
||||
G1 X30 Y30 E8.2
|
||||
|
||||
# Disable extruder stepper motor
|
||||
SYNC_EXTRUDER_MOTION EXTRUDER=extruder MOTION_QUEUE=
|
||||
G1 X35 Y35 E8.5
|
||||
|
||||
# Disable my_extra_stepper stepper motor
|
||||
SYNC_EXTRUDER_MOTION EXTRUDER=my_extra_stepper MOTION_QUEUE=
|
||||
G1 X40 Y40 E9.0
|
||||
|
||||
# Enable extruder stepper motor
|
||||
SYNC_EXTRUDER_MOTION EXTRUDER=extruder MOTION_QUEUE=extruder
|
||||
G1 X45 Y45 E9.5
|
||||
|
||||
# Switch to just my_extra_stepper stepper motor
|
||||
SYNC_EXTRUDER_MOTION EXTRUDER=extruder MOTION_QUEUE=
|
||||
SYNC_EXTRUDER_MOTION EXTRUDER=my_extra_stepper MOTION_QUEUE=extruder
|
||||
G1 X50 Y50 E10.0
|
||||
|
||||
# Test pressure advance move
|
||||
SET_PRESSURE_ADVANCE EXTRUDER=my_extra_stepper ADVANCE=0.020
|
||||
G1 X55 Y55 E0
|
||||
G1 X55 Y55 E0.5
|
||||
G1 X60 Y60 E1.1
|
||||
G1 X50 Y50
|
||||
SET_PRESSURE_ADVANCE EXTRUDER=extruder ADVANCE=0.025
|
||||
G1 X55 Y55 E1.5
|
||||
G1 X50 Y50
|
||||
G1 X55 Y55 E2.0
|
||||
G1 X50 Y50
|
||||
70
test/klippy/gcode_arcs.cfg
Normal file
70
test/klippy/gcode_arcs.cfg
Normal file
@@ -0,0 +1,70 @@
|
||||
# Test config for arcs
|
||||
[gcode_arcs]
|
||||
|
||||
[stepper_x]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PE5
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_y]
|
||||
step_pin: PF6
|
||||
dir_pin: !PF7
|
||||
enable_pin: !PF2
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PJ1
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_z]
|
||||
step_pin: PL3
|
||||
dir_pin: PL1
|
||||
enable_pin: !PK0
|
||||
microsteps: 16
|
||||
rotation_distance: 8
|
||||
endstop_pin: ^PD3
|
||||
position_endstop: 0.5
|
||||
position_max: 200
|
||||
|
||||
[extruder]
|
||||
step_pin: PA4
|
||||
dir_pin: PA6
|
||||
enable_pin: !PA2
|
||||
microsteps: 16
|
||||
rotation_distance: 33.5
|
||||
nozzle_diameter: 0.500
|
||||
filament_diameter: 3.500
|
||||
heater_pin: PB4
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK5
|
||||
control: pid
|
||||
pid_Kp: 22.2
|
||||
pid_Ki: 1.08
|
||||
pid_Kd: 114
|
||||
min_temp: 0
|
||||
max_temp: 210
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PH5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK6
|
||||
control: watermark
|
||||
min_temp: 0
|
||||
max_temp: 110
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
max_z_velocity: 5
|
||||
max_z_accel: 100
|
||||
11
test/klippy/gcode_arcs.test
Normal file
11
test/klippy/gcode_arcs.test
Normal file
@@ -0,0 +1,11 @@
|
||||
# Tests for g-code G2/G3 arc commands
|
||||
DICTIONARY atmega2560.dict
|
||||
CONFIG gcode_arcs.cfg
|
||||
|
||||
# Home and move in arcs
|
||||
G28
|
||||
G1 X20 Y20 Z20
|
||||
G2 X125 Y32 Z20 E1 I10.5 J10.5
|
||||
|
||||
# XY+Z arc move
|
||||
G2 X20 Y20 Z10 E1 I10.5 J10.5
|
||||
85
test/klippy/input_shaper.cfg
Normal file
85
test/klippy/input_shaper.cfg
Normal file
@@ -0,0 +1,85 @@
|
||||
# Test config for input_shaper
|
||||
[stepper_x]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PE5
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_y]
|
||||
step_pin: PF6
|
||||
dir_pin: !PF7
|
||||
enable_pin: !PF2
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PJ1
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_z]
|
||||
step_pin: PL3
|
||||
dir_pin: PL1
|
||||
enable_pin: !PK0
|
||||
microsteps: 16
|
||||
rotation_distance: 8
|
||||
endstop_pin: ^PD3
|
||||
position_endstop: 0.5
|
||||
position_max: 200
|
||||
|
||||
[extruder]
|
||||
step_pin: PA4
|
||||
dir_pin: PA6
|
||||
enable_pin: !PA2
|
||||
microsteps: 16
|
||||
rotation_distance: 33.5
|
||||
nozzle_diameter: 0.500
|
||||
filament_diameter: 3.500
|
||||
heater_pin: PB4
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK5
|
||||
control: pid
|
||||
pid_Kp: 22.2
|
||||
pid_Ki: 1.08
|
||||
pid_Kd: 114
|
||||
min_temp: 0
|
||||
max_temp: 210
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PH5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK6
|
||||
control: watermark
|
||||
min_temp: 0
|
||||
max_temp: 110
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
max_z_velocity: 5
|
||||
max_z_accel: 100
|
||||
|
||||
[input_shaper]
|
||||
shaper_type_x: mzv
|
||||
shaper_freq_x: 33.2
|
||||
shaper_type_x: ei
|
||||
shaper_freq_x: 39.3
|
||||
|
||||
[adxl345]
|
||||
cs_pin: PK7
|
||||
axes_map: -x,-y,z
|
||||
|
||||
[mpu9250 my_mpu]
|
||||
|
||||
[resonance_tester]
|
||||
probe_points: 20,20,20
|
||||
accel_chip_x: adxl345
|
||||
accel_chip_y: mpu9250 my_mpu
|
||||
7
test/klippy/input_shaper.test
Normal file
7
test/klippy/input_shaper.test
Normal file
@@ -0,0 +1,7 @@
|
||||
# Test case for input_stepper
|
||||
CONFIG input_shaper.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
# Simple command test
|
||||
SET_INPUT_SHAPER SHAPER_FREQ_X=22.2 DAMPING_RATIO_X=.1 SHAPER_TYPE_X=zv
|
||||
SET_INPUT_SHAPER SHAPER_FREQ_Y=33.3 DAMPING_RATIO_X=.11 SHAPER_TYPE_X=2hump_ei
|
||||
44
test/klippy/led.cfg
Normal file
44
test/klippy/led.cfg
Normal file
@@ -0,0 +1,44 @@
|
||||
# Test case for LED config sections
|
||||
|
||||
[led lled]
|
||||
red_pin: PA2
|
||||
initial_RED: 0.2
|
||||
|
||||
[neopixel nled]
|
||||
pin: PA3
|
||||
chain_count: 4
|
||||
initial_RED: 0.2
|
||||
initial_GREEN: 0.3
|
||||
initial_BLUE: 0.4
|
||||
|
||||
[dotstar dled]
|
||||
data_pin: PA4
|
||||
clock_pin: PA5
|
||||
chain_count: 2
|
||||
initial_RED: 0.2
|
||||
initial_GREEN: 0.3
|
||||
initial_BLUE: 0.4
|
||||
|
||||
[pca9533 p5led]
|
||||
initial_RED: 0.1
|
||||
initial_GREEN: 0.2
|
||||
initial_BLUE: 0.3
|
||||
|
||||
[pca9632 p6led]
|
||||
scl_pin: PB1
|
||||
sda_pin: PB2
|
||||
initial_RED: 0.4
|
||||
initial_GREEN: 0.5
|
||||
initial_BLUE: 0.6
|
||||
|
||||
[display_template dtest]
|
||||
param_myvar: 1.2
|
||||
text: { param_myvar }, { param_myvar / 2.0 }, 0.0, 2.0
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: none
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
58
test/klippy/led.test
Normal file
58
test/klippy/led.test
Normal file
@@ -0,0 +1,58 @@
|
||||
# Test case for LEDs
|
||||
CONFIG led.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
# SET_LED tests
|
||||
SET_LED LED=lled RED=0.2
|
||||
SET_LED LED=lled RED=0.3 TRANSMIT=0
|
||||
SET_LED LED=lled RED=0.4
|
||||
SET_LED LED=lled RED=0.5 SYNC=0
|
||||
|
||||
SET_LED LED=nled RED=0.2
|
||||
SET_LED LED=nled RED=0.3 TRANSMIT=0
|
||||
SET_LED LED=nled RED=0.4
|
||||
SET_LED LED=nled RED=0.5 SYNC=0
|
||||
|
||||
SET_LED LED=dled RED=0.2
|
||||
SET_LED LED=dled RED=0.3 TRANSMIT=0
|
||||
SET_LED LED=dled RED=0.4
|
||||
SET_LED LED=dled RED=0.5 SYNC=0
|
||||
|
||||
SET_LED LED=p5led RED=0.2
|
||||
SET_LED LED=p5led RED=0.3 TRANSMIT=0
|
||||
SET_LED LED=p5led RED=0.4
|
||||
SET_LED LED=p5led RED=0.5 SYNC=0
|
||||
|
||||
SET_LED LED=p6led RED=0.2
|
||||
SET_LED LED=p6led RED=0.3 TRANSMIT=0
|
||||
SET_LED LED=p6led RED=0.4
|
||||
SET_LED LED=p6led RED=0.5 SYNC=0
|
||||
|
||||
# SET_LED chain tests
|
||||
SET_LED LED=nled INDEX=2 RED=0.2
|
||||
SET_LED LED=nled INDEX=1 RED=0.3 TRANSMIT=0
|
||||
SET_LED LED=nled INDEX=2 RED=0.4
|
||||
SET_LED LED=nled INDEX=1 RED=0.5 SYNC=0
|
||||
|
||||
SET_LED LED=dled INDEX=2 RED=0.2
|
||||
SET_LED LED=dled INDEX=1 RED=0.3 TRANSMIT=0
|
||||
SET_LED LED=dled INDEX=2 RED=0.4
|
||||
SET_LED LED=dled INDEX=1 RED=0.5 SYNC=0
|
||||
|
||||
# SET_LED_TEMPLATE tests
|
||||
SET_LED_TEMPLATE LED=lled TEMPLATE=dtest
|
||||
SET_LED_TEMPLATE LED=lled TEMPLATE=
|
||||
|
||||
SET_LED_TEMPLATE LED=nled TEMPLATE=dtest
|
||||
SET_LED_TEMPLATE LED=nled TEMPLATE=
|
||||
SET_LED_TEMPLATE LED=nled INDEX=2 TEMPLATE=dtest
|
||||
SET_LED_TEMPLATE LED=nled TEMPLATE=
|
||||
|
||||
SET_LED_TEMPLATE LED=dled TEMPLATE=dtest
|
||||
SET_LED_TEMPLATE LED=dled TEMPLATE=
|
||||
SET_LED_TEMPLATE LED=dled INDEX=2 TEMPLATE=dtest
|
||||
SET_LED_TEMPLATE LED=dled TEMPLATE=
|
||||
|
||||
SET_LED_TEMPLATE LED=p5led TEMPLATE=dtest
|
||||
|
||||
SET_LED_TEMPLATE LED=p6led TEMPLATE=dtest
|
||||
15
test/klippy/linuxtest.cfg
Normal file
15
test/klippy/linuxtest.cfg
Normal file
@@ -0,0 +1,15 @@
|
||||
# Test config for linux process specific hardware
|
||||
[mcu]
|
||||
serial: /tmp/klipper_host_mcu
|
||||
|
||||
[printer]
|
||||
kinematics: none
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
|
||||
[temperature_sensor my_ds18b20]
|
||||
min_temp: 0
|
||||
max_temp: 100
|
||||
serial_no: 12345678
|
||||
sensor_mcu: mcu
|
||||
sensor_type: DS18B20
|
||||
5
test/klippy/linuxtest.test
Normal file
5
test/klippy/linuxtest.test
Normal file
@@ -0,0 +1,5 @@
|
||||
# Tests for various temperature sensors
|
||||
DICTIONARY linuxprocess.dict
|
||||
CONFIG linuxtest.cfg
|
||||
|
||||
G4 P1000
|
||||
132
test/klippy/macros.cfg
Normal file
132
test/klippy/macros.cfg
Normal file
@@ -0,0 +1,132 @@
|
||||
# Test config with macros
|
||||
[stepper_x]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PE5
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_y]
|
||||
step_pin: PF6
|
||||
dir_pin: !PF7
|
||||
enable_pin: !PF2
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PJ1
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_z]
|
||||
step_pin: PL3
|
||||
dir_pin: PL1
|
||||
enable_pin: !PK0
|
||||
microsteps: 16
|
||||
rotation_distance: 8
|
||||
endstop_pin: ^PD3
|
||||
position_endstop: 0.5
|
||||
position_max: 200
|
||||
|
||||
[extruder]
|
||||
step_pin: PA4
|
||||
dir_pin: PA6
|
||||
enable_pin: !PA2
|
||||
microsteps: 16
|
||||
rotation_distance: 33.5
|
||||
nozzle_diameter: 0.500
|
||||
filament_diameter: 3.500
|
||||
heater_pin: PB4
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK5
|
||||
control: pid
|
||||
pid_Kp: 22.2
|
||||
pid_Ki: 1.08
|
||||
pid_Kd: 114
|
||||
min_temp: 0
|
||||
max_temp: 210
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PH5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK6
|
||||
control: watermark
|
||||
min_temp: 0
|
||||
max_temp: 110
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
max_z_velocity: 5
|
||||
max_z_accel: 100
|
||||
|
||||
[gcode_macro TEST_SAVE_RESTORE]
|
||||
gcode:
|
||||
SAVE_GCODE_STATE NAME=TESTIT1
|
||||
G92 Z10
|
||||
RESTORE_GCODE_STATE NAME=TESTIT1
|
||||
G92 Z0
|
||||
|
||||
[gcode_macro TEST_expression]
|
||||
gcode:
|
||||
{% if printer.gcode_move.gcode_position.x != 0.0 %}
|
||||
M112
|
||||
{% else %}
|
||||
{ action_respond_info("TEST_expression") }
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro TEST_variable]
|
||||
variable_t: 12.0
|
||||
gcode:
|
||||
{ action_respond_info("TEST_variable") }
|
||||
{% if t - 12.0 != printer.toolhead.position.y %}
|
||||
M112
|
||||
{% endif %}
|
||||
{% if printer["gcode_macro TEST_variable"].t - 12.0 != 0.0 %}
|
||||
M112
|
||||
{% endif %}
|
||||
SET_GCODE_VARIABLE MACRO=TEST_variable VARIABLE=t VALUE=17
|
||||
TEST_variable_part2
|
||||
|
||||
[gcode_macro TEST_variable_part2]
|
||||
gcode:
|
||||
{ action_respond_info("TEST_variable_part2") }
|
||||
{% if printer["gcode_macro TEST_variable"].t != 17.0 %}
|
||||
M112
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro TEST_param]
|
||||
gcode:
|
||||
{ action_respond_info("TEST_param") }
|
||||
{% if params.T != "123" %}
|
||||
M112
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro TEST_in]
|
||||
gcode:
|
||||
{% if "abc" in printer or "toolhead" not in printer %}
|
||||
M112
|
||||
{% endif %}
|
||||
|
||||
# A utf8 test (with utf8 characters such as ° )
|
||||
[gcode_macro TEST_unicode] ; Also test end-of-line comments ( ° )
|
||||
variable_ABC: 25 # Another end-of-line comment test ( ° )
|
||||
description: A unicode test °
|
||||
gcode: G28
|
||||
|
||||
# Main test start point
|
||||
[gcode_macro TESTIT]
|
||||
gcode:
|
||||
TEST_SAVE_RESTORE
|
||||
TEST_expression
|
||||
TEST_variable
|
||||
TEST_param T=123
|
||||
TEST_unicode
|
||||
TEST_in
|
||||
6
test/klippy/macros.test
Normal file
6
test/klippy/macros.test
Normal file
@@ -0,0 +1,6 @@
|
||||
# Tests for miscellaneous g-code commands
|
||||
DICTIONARY atmega2560.dict
|
||||
CONFIG macros.cfg
|
||||
|
||||
# Run TESTIT macro
|
||||
TESTIT
|
||||
25
test/klippy/manual_stepper.cfg
Normal file
25
test/klippy/manual_stepper.cfg
Normal file
@@ -0,0 +1,25 @@
|
||||
# Test config for manual_stepper
|
||||
[manual_stepper basic_stepper]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
velocity: 7
|
||||
accel: 500
|
||||
|
||||
[manual_stepper homing_stepper]
|
||||
step_pin: PF6
|
||||
dir_pin: !PF7
|
||||
enable_pin: !PF2
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PJ1
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: none
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
24
test/klippy/manual_stepper.test
Normal file
24
test/klippy/manual_stepper.test
Normal file
@@ -0,0 +1,24 @@
|
||||
# Test case for manual_stepper
|
||||
CONFIG manual_stepper.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
# Test basic moves
|
||||
MANUAL_STEPPER STEPPER=basic_stepper ENABLE=1
|
||||
MANUAL_STEPPER STEPPER=basic_stepper SET_POSITION=0
|
||||
MANUAL_STEPPER STEPPER=basic_stepper MOVE=10 SPEED=10
|
||||
MANUAL_STEPPER STEPPER=basic_stepper MOVE=5
|
||||
MANUAL_STEPPER STEPPER=basic_stepper MOVE=12 SPEED=12 ACCEL=9000.2
|
||||
MANUAL_STEPPER STEPPER=basic_stepper ENABLE=0
|
||||
|
||||
# Test homing move
|
||||
MANUAL_STEPPER STEPPER=homing_stepper ENABLE=1
|
||||
MANUAL_STEPPER STEPPER=homing_stepper SET_POSITION=0
|
||||
MANUAL_STEPPER STEPPER=homing_stepper MOVE=10 SPEED=100 ACCEL=1
|
||||
MANUAL_STEPPER STEPPER=homing_stepper ENABLE=0
|
||||
|
||||
# Test motor off
|
||||
M84
|
||||
|
||||
# Verify stepper_buzz
|
||||
STEPPER_BUZZ STEPPER="manual_stepper basic_stepper"
|
||||
STEPPER_BUZZ STEPPER="manual_stepper homing_stepper"
|
||||
46
test/klippy/move.gcode
Normal file
46
test/klippy/move.gcode
Normal file
@@ -0,0 +1,46 @@
|
||||
; Simple movement tests
|
||||
|
||||
; Start by homing the printer.
|
||||
G28
|
||||
G90
|
||||
G1 F6000
|
||||
|
||||
; Z / X / Y moves
|
||||
G1 Z1
|
||||
G1 X1
|
||||
G1 Y1
|
||||
|
||||
; Delayed moves
|
||||
G1 Y2
|
||||
G4 P100
|
||||
G1 Y1.5
|
||||
M400
|
||||
G1 Y1
|
||||
|
||||
; diagonal moves
|
||||
G1 X0 Y0
|
||||
G1 X1 Z2
|
||||
G1 X0 Y1 Z1
|
||||
|
||||
; extrude only moves
|
||||
G1 E1
|
||||
G1 E0
|
||||
|
||||
; Verify GET_POSITION works
|
||||
GET_POSITION
|
||||
|
||||
; regular extrude move
|
||||
G1 X0 Y0 E.01
|
||||
|
||||
; fan speeds
|
||||
M106 S50
|
||||
M106
|
||||
M106 S90
|
||||
M106 S0
|
||||
M107
|
||||
|
||||
; coordinate manipulation
|
||||
G92 Y-3
|
||||
G1 Y-2
|
||||
G91
|
||||
G1 Y-1
|
||||
105
test/klippy/multi_z.cfg
Normal file
105
test/klippy/multi_z.cfg
Normal file
@@ -0,0 +1,105 @@
|
||||
# Test config with multiple z steppers
|
||||
[stepper_x]
|
||||
step_pin: PF0
|
||||
dir_pin: PF1
|
||||
enable_pin: !PD7
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PE5
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_y]
|
||||
step_pin: PF6
|
||||
dir_pin: !PF7
|
||||
enable_pin: !PF2
|
||||
microsteps: 16
|
||||
rotation_distance: 40
|
||||
endstop_pin: ^PJ1
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
|
||||
[stepper_z]
|
||||
step_pin: PL3
|
||||
dir_pin: PL1
|
||||
enable_pin: !PK0
|
||||
microsteps: 16
|
||||
rotation_distance: 8
|
||||
endstop_pin: ^PD3
|
||||
position_endstop: 0.5
|
||||
position_max: 200
|
||||
|
||||
[stepper_z1]
|
||||
step_pin: PC1
|
||||
dir_pin: PC3
|
||||
enable_pin: !PC7
|
||||
microsteps: 16
|
||||
rotation_distance: 8
|
||||
endstop_pin: ^PD2
|
||||
|
||||
[stepper_z2]
|
||||
step_pin: PH1
|
||||
dir_pin: PH0
|
||||
enable_pin: !PA1
|
||||
microsteps: 16
|
||||
rotation_distance: 8
|
||||
|
||||
[z_tilt]
|
||||
z_positions:
|
||||
-56,-17
|
||||
-56,322
|
||||
311,322
|
||||
points:
|
||||
50,50
|
||||
50,195
|
||||
195,195
|
||||
195,50
|
||||
|
||||
[bed_tilt]
|
||||
points:
|
||||
50,50
|
||||
50,195
|
||||
195,195
|
||||
195,50
|
||||
|
||||
[extruder]
|
||||
step_pin: PA4
|
||||
dir_pin: PA6
|
||||
enable_pin: !PA2
|
||||
microsteps: 16
|
||||
rotation_distance: 33.5
|
||||
nozzle_diameter: 0.400
|
||||
filament_diameter: 1.750
|
||||
heater_pin: PB4
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK5
|
||||
control: pid
|
||||
pid_Kp: 22.2
|
||||
pid_Ki: 1.08
|
||||
pid_Kd: 114
|
||||
min_temp: 0
|
||||
max_temp: 250
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PH5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PK6
|
||||
control: watermark
|
||||
min_temp: 0
|
||||
max_temp: 130
|
||||
|
||||
[probe]
|
||||
pin: PH6
|
||||
z_offset: 1.15
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
max_z_velocity: 5
|
||||
max_z_accel: 100
|
||||
60
test/klippy/multi_z.test
Normal file
60
test/klippy/multi_z.test
Normal file
@@ -0,0 +1,60 @@
|
||||
# Test case with multiple z stepper motors
|
||||
CONFIG multi_z.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
# Start by homing the printer.
|
||||
G28
|
||||
G1 F6000
|
||||
|
||||
# Z / X / Y moves
|
||||
G1 Z1
|
||||
G1 X1
|
||||
G1 Y1
|
||||
|
||||
# Run bed_tilt_calibrate
|
||||
BED_TILT_CALIBRATE
|
||||
|
||||
# Move again
|
||||
G1 Z5 X0 Y0
|
||||
|
||||
# Run Z_TILT_ADJUST
|
||||
Z_TILT_ADJUST
|
||||
|
||||
# Move again
|
||||
G1 Z2 X2 Y3
|
||||
|
||||
# Do regular probe
|
||||
PROBE
|
||||
QUERY_PROBE
|
||||
|
||||
# Test manual probe commands
|
||||
PROBE_CALIBRATE
|
||||
ABORT
|
||||
PROBE_CALIBRATE SPEED=7.3
|
||||
TESTZ Z=-.2
|
||||
TESTZ Z=-.3
|
||||
TESTZ Z=+.1
|
||||
TESTZ Z=++
|
||||
TESTZ Z=--
|
||||
TESTZ Z=+
|
||||
TESTZ Z=-
|
||||
ACCEPT
|
||||
Z_ENDSTOP_CALIBRATE
|
||||
TESTZ Z=-.1
|
||||
TESTZ Z=+.2
|
||||
ACCEPT
|
||||
MANUAL_PROBE
|
||||
TESTZ Z=--
|
||||
ABORT
|
||||
|
||||
# Do regular probe
|
||||
PROBE
|
||||
QUERY_PROBE
|
||||
|
||||
# Verify stepper_buzz
|
||||
STEPPER_BUZZ STEPPER=stepper_z
|
||||
STEPPER_BUZZ STEPPER=stepper_z1
|
||||
STEPPER_BUZZ STEPPER=stepper_z2
|
||||
|
||||
# Move again
|
||||
G1 Z9
|
||||
8
test/klippy/out_of_bounds.test
Normal file
8
test/klippy/out_of_bounds.test
Normal file
@@ -0,0 +1,8 @@
|
||||
# Test that basic bounds checks work
|
||||
CONFIG ../../config/example-cartesian.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
SHOULD_FAIL
|
||||
|
||||
# Home the printer, and then attempt to move to an obviously bad location
|
||||
G28
|
||||
G1 Y9999
|
||||
74
test/klippy/polar.test
Normal file
74
test/klippy/polar.test
Normal file
@@ -0,0 +1,74 @@
|
||||
# Test case for basic movement on polar printers
|
||||
CONFIG ../../config/example-polar.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
; Start by homing the printer.
|
||||
G28
|
||||
G90
|
||||
G1 F6000
|
||||
|
||||
; Z / X / Y moves
|
||||
G1 Z1
|
||||
G1 X1
|
||||
G1 Y1
|
||||
|
||||
; Delayed moves
|
||||
G1 Y2
|
||||
G4 P100
|
||||
G1 Y1.5
|
||||
M400
|
||||
G1 Y1
|
||||
|
||||
; diagonal moves
|
||||
G1 X10 Y0
|
||||
G1 X1 Z2
|
||||
G1 X0 Y1 Z1
|
||||
|
||||
; extrude only moves
|
||||
G1 E1
|
||||
G1 E0
|
||||
|
||||
; regular extrude move
|
||||
G1 X10 Y0 E.01
|
||||
|
||||
; Multiple rotations
|
||||
g1 X10 Y10
|
||||
g1 X-10 Y10
|
||||
g1 X-10 Y-10
|
||||
g1 X10 Y-10
|
||||
|
||||
g1 X10 Y15
|
||||
g1 X-10 Y15
|
||||
g1 X-10 Y-15
|
||||
g1 X10 Y-15
|
||||
|
||||
g1 X10 Y20
|
||||
g1 X-10 Y20
|
||||
g1 X-10 Y-20
|
||||
g1 X10 Y-20
|
||||
|
||||
g1 X10 Y25
|
||||
g1 X-10 Y25
|
||||
g1 X-10 Y-25
|
||||
g1 X10 Y-25
|
||||
|
||||
; Multiple rotations in reverse direction
|
||||
g1 X-15 Y-25
|
||||
g1 X-15 Y25
|
||||
g1 X15 Y25
|
||||
g1 X15 Y-25
|
||||
|
||||
g1 X-20 Y-25
|
||||
g1 X-20 Y25
|
||||
g1 X20 Y25
|
||||
g1 X20 Y-25
|
||||
|
||||
g1 X-25 Y-25
|
||||
g1 X-25 Y25
|
||||
g1 X25 Y25
|
||||
g1 X25 Y-25
|
||||
|
||||
g1 X-30 Y-25
|
||||
g1 X-30 Y25
|
||||
g1 X30 Y25
|
||||
g1 X30 Y-25
|
||||
232
test/klippy/printers.test
Normal file
232
test/klippy/printers.test
Normal file
@@ -0,0 +1,232 @@
|
||||
# Basic sanity checks on the example printer config files
|
||||
GCODE move.gcode
|
||||
|
||||
# Example kinematic files
|
||||
DICTIONARY atmega2560.dict
|
||||
CONFIG ../../config/example-cartesian.cfg
|
||||
CONFIG ../../config/example-corexy.cfg
|
||||
CONFIG ../../config/example-corexz.cfg
|
||||
CONFIG ../../config/example-hybrid-corexy.cfg
|
||||
CONFIG ../../config/example-hybrid-corexz.cfg
|
||||
CONFIG ../../config/example-delta.cfg
|
||||
CONFIG ../../config/example-rotary-delta.cfg
|
||||
CONFIG ../../config/example-winch.cfg
|
||||
|
||||
# Printers using the atmega2560
|
||||
DICTIONARY atmega2560.dict
|
||||
CONFIG ../../config/generic-einsy-rambo.cfg
|
||||
CONFIG ../../config/generic-fysetc-f6.cfg
|
||||
CONFIG ../../config/generic-gt2560.cfg
|
||||
CONFIG ../../config/generic-mini-rambo.cfg
|
||||
CONFIG ../../config/generic-rambo.cfg
|
||||
CONFIG ../../config/generic-ramps.cfg
|
||||
CONFIG ../../config/generic-rumba.cfg
|
||||
CONFIG ../../config/generic-ultimaker-ultimainboard-v2.cfg
|
||||
CONFIG ../../config/kit-zav3d-2019.cfg
|
||||
CONFIG ../../config/printer-adimlab-2018.cfg
|
||||
CONFIG ../../config/printer-anycubic-4max-2018.cfg
|
||||
CONFIG ../../config/printer-anycubic-4maxpro-2.0-2021.cfg
|
||||
CONFIG ../../config/printer-anycubic-i3-mega-2017.cfg
|
||||
CONFIG ../../config/printer-anycubic-kossel-2016.cfg
|
||||
CONFIG ../../config/printer-anycubic-kossel-plus-2017.cfg
|
||||
CONFIG ../../config/printer-creality-cr10-v3-2020.cfg
|
||||
CONFIG ../../config/printer-creality-cr10s-2017.cfg
|
||||
CONFIG ../../config/printer-creality-cr20-2018.cfg
|
||||
CONFIG ../../config/printer-creality-cr20-pro-2019.cfg
|
||||
CONFIG ../../config/printer-creality-ender5plus-2019.cfg
|
||||
CONFIG ../../config/printer-eryone-thinker-series-v2-2020.cfg
|
||||
CONFIG ../../config/printer-flashforge-creator-pro-2018.cfg
|
||||
CONFIG ../../config/printer-hiprecy-leo-2019.cfg
|
||||
CONFIG ../../config/printer-longer-lk4-pro-2019.cfg
|
||||
CONFIG ../../config/printer-lulzbot-mini1-2016.cfg
|
||||
CONFIG ../../config/printer-lulzbot-taz6-2017.cfg
|
||||
CONFIG ../../config/printer-lulzbot-taz6-dual-v3-2017.cfg
|
||||
CONFIG ../../config/printer-makergear-m2-2012.cfg
|
||||
CONFIG ../../config/printer-makergear-m2-2016.cfg
|
||||
CONFIG ../../config/printer-micromake-d1-2016.cfg
|
||||
CONFIG ../../config/printer-mtw-create-2015.cfg
|
||||
CONFIG ../../config/printer-robo3d-r2-2017.cfg
|
||||
CONFIG ../../config/printer-seemecnc-rostock-max-v2-2015.cfg
|
||||
CONFIG ../../config/printer-sovol-sv01-2020.cfg
|
||||
CONFIG ../../config/printer-sunlu-s8-2020.cfg
|
||||
CONFIG ../../config/printer-tevo-flash-2018.cfg
|
||||
CONFIG ../../config/printer-tevo-tarantula-pro-2020.cfg
|
||||
CONFIG ../../config/printer-velleman-k8200-2013.cfg
|
||||
CONFIG ../../config/printer-velleman-k8800-2017.cfg
|
||||
CONFIG ../../config/printer-wanhao-duplicator-i3-mini-2017.cfg
|
||||
CONFIG ../../config/printer-wanhao-duplicator-i3-plus-2017.cfg
|
||||
CONFIG ../../config/printer-wanhao-duplicator-i3-plus-mark2-2019.cfg
|
||||
CONFIG ../../config/printer-wanhao-duplicator-6-2016.cfg
|
||||
CONFIG ../../config/printer-wanhao-duplicator-9-2018.cfg
|
||||
|
||||
# Printers using the atmega1280
|
||||
DICTIONARY atmega1280.dict
|
||||
CONFIG ../../config/generic-mightyboard.cfg
|
||||
CONFIG ../../config/generic-minitronics1.cfg
|
||||
|
||||
# Printers using the atmega1284p
|
||||
DICTIONARY atmega1284p.dict
|
||||
CONFIG ../../config/generic-melzi.cfg
|
||||
CONFIG ../../config/printer-anet-a4-2018.cfg
|
||||
CONFIG ../../config/printer-anet-a8-2017.cfg
|
||||
CONFIG ../../config/printer-anet-e10-2018.cfg
|
||||
CONFIG ../../config/printer-anet-e16-2019.cfg
|
||||
CONFIG ../../config/printer-creality-cr10-2017.cfg
|
||||
CONFIG ../../config/printer-creality-cr10mini-2017.cfg
|
||||
CONFIG ../../config/printer-creality-ender2-2017.cfg
|
||||
CONFIG ../../config/printer-creality-ender3-2018.cfg
|
||||
CONFIG ../../config/printer-creality-ender5-2019.cfg
|
||||
CONFIG ../../config/printer-tronxy-p802e-2020.cfg
|
||||
CONFIG ../../config/printer-tronxy-p802m-2020.cfg
|
||||
CONFIG ../../config/printer-tronxy-x5s-2018.cfg
|
||||
CONFIG ../../config/printer-tronxy-x8-2018.cfg
|
||||
CONFIG ../../config/printer-wanhao-duplicator-i3-v2.1-2017.cfg
|
||||
|
||||
# Printers using the atmega644
|
||||
DICTIONARY atmega644p.dict
|
||||
CONFIG ../../config/generic-simulavr.cfg
|
||||
|
||||
# Printers using the at90usb1286
|
||||
DICTIONARY at90usb1286.dict
|
||||
CONFIG ../../config/generic-printrboard.cfg
|
||||
|
||||
# Printers using the sam3x8c
|
||||
DICTIONARY sam3x8c.dict
|
||||
CONFIG ../../config/generic-printrboard-g2.cfg
|
||||
|
||||
# Printers using the sam3x8e
|
||||
DICTIONARY sam3x8e.dict
|
||||
CONFIG ../../config/generic-alligator-r2.cfg
|
||||
CONFIG ../../config/generic-alligator-r3.cfg
|
||||
CONFIG ../../config/generic-archim2.cfg
|
||||
CONFIG ../../config/generic-radds.cfg
|
||||
CONFIG ../../config/generic-ruramps-v1.3.cfg
|
||||
|
||||
# Printers using the sam4s8c
|
||||
DICTIONARY sam4s8c.dict
|
||||
CONFIG ../../config/generic-duet2-maestro.cfg
|
||||
|
||||
# Printers using the sam4e8e
|
||||
DICTIONARY sam4e8e.dict
|
||||
CONFIG ../../config/generic-duet2.cfg
|
||||
CONFIG ../../config/generic-duet2-duex.cfg
|
||||
CONFIG ../../config/printer-modix-big60-2020.cfg
|
||||
|
||||
# Printers using the samd51
|
||||
DICTIONARY samd51p20.dict
|
||||
CONFIG ../../config/generic-duet3-mini.cfg
|
||||
|
||||
# Printers using the lpc176x
|
||||
DICTIONARY lpc176x.dict
|
||||
CONFIG ../../config/generic-azteeg-x5-mini-v3.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-e3-turbo.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-v1.1.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-v1.3.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-v1.4.cfg
|
||||
CONFIG ../../config/generic-mks-sgenl.cfg
|
||||
CONFIG ../../config/generic-re-arm.cfg
|
||||
CONFIG ../../config/generic-smoothieboard.cfg
|
||||
CONFIG ../../config/generic-th3d-ezboard-lite-v1.2.cfg
|
||||
|
||||
# Printers using the stm32f070
|
||||
DICTIONARY stm32f070.dict
|
||||
CONFIG ../../config/printer-monoprice-mini-delta-2017.cfg
|
||||
CONFIG ../../config/printer-monoprice-select-mini-v2-2018.cfg
|
||||
|
||||
# Printers using the stm32f103
|
||||
DICTIONARY stm32f103.dict
|
||||
CONFIG ../../config/generic-bigtreetech-skr-cr6-v1.0.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-e3-dip.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-mini.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-mini-e3-v1.0.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-mini-e3-v1.2.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-mini-e3-v2.0.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-mini-mz.cfg
|
||||
CONFIG ../../config/printer-anycubic-vyper-2021.cfg
|
||||
CONFIG ../../config/printer-monoprice-select-mini-v1-2016.cfg
|
||||
|
||||
# Printers using the stm32f103 via serial
|
||||
DICTIONARY stm32f103-serial.dict
|
||||
CONFIG ../../config/generic-creality-v4.2.7.cfg
|
||||
CONFIG ../../config/generic-creality-v4.2.10.cfg
|
||||
CONFIG ../../config/generic-fysetc-cheetah-v1.1.cfg
|
||||
CONFIG ../../config/generic-fysetc-cheetah-v1.2.cfg
|
||||
CONFIG ../../config/generic-mks-robin-e3.cfg
|
||||
CONFIG ../../config/generic-mks-robin-nano-v1.cfg
|
||||
CONFIG ../../config/generic-mks-robin-nano-v2.cfg
|
||||
CONFIG ../../config/printer-alfawise-u30-2018.cfg
|
||||
CONFIG ../../config/printer-creality-cr10-smart-pro-2022.cfg
|
||||
CONFIG ../../config/printer-creality-cr30-2021.cfg
|
||||
CONFIG ../../config/printer-creality-cr6se-2020.cfg
|
||||
CONFIG ../../config/printer-creality-cr6se-2021.cfg
|
||||
CONFIG ../../config/printer-creality-ender2pro-2021.cfg
|
||||
CONFIG ../../config/printer-creality-ender3-s1-2021.cfg
|
||||
CONFIG ../../config/printer-creality-ender3-v2-2020.cfg
|
||||
CONFIG ../../config/printer-creality-ender3max-2021.cfg
|
||||
CONFIG ../../config/printer-creality-ender3pro-2020.cfg
|
||||
CONFIG ../../config/printer-creality-ender5pro-2020.cfg
|
||||
CONFIG ../../config/printer-creality-ender6-2020.cfg
|
||||
CONFIG ../../config/printer-creality-sermoonD1-2021.cfg
|
||||
CONFIG ../../config/printer-creality-sermoonV1-2022.cfg
|
||||
CONFIG ../../config/printer-elegoo-neptune2-2021.cfg
|
||||
CONFIG ../../config/printer-eryone-er20-2021.cfg
|
||||
CONFIG ../../config/printer-flsun-q5-2020.cfg
|
||||
CONFIG ../../config/printer-flsun-qqs-2020.cfg
|
||||
CONFIG ../../config/printer-fokoos-odin5-f3-2021.cfg
|
||||
CONFIG ../../config/printer-tronxy-x5sa-v6-2019.cfg
|
||||
CONFIG ../../config/printer-tronxy-x5sa-pro-2020.cfg
|
||||
CONFIG ../../config/printer-tronxy-xy-2-Pro-2020.cfg
|
||||
CONFIG ../../config/printer-twotrees-sapphire-plus-sp-5-v1-2020.cfg
|
||||
CONFIG ../../config/printer-twotrees-sapphire-plus-sp-5-v1.1-2021.cfg
|
||||
CONFIG ../../config/printer-twotrees-sapphire-pro-sp-3-2020.cfg
|
||||
|
||||
# Printers using the stm32f405
|
||||
DICTIONARY stm32f405.dict
|
||||
CONFIG ../../config/generic-mellow-fly-gemini-v1.cfg
|
||||
CONFIG ../../config/generic-mellow-fly-gemini-v2.cfg
|
||||
|
||||
# Printers using the stm32f407
|
||||
DICTIONARY stm32f407.dict
|
||||
CONFIG ../../config/generic-bigtreetech-e3-rrf-v1.1.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-gtr.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-pro.cfg
|
||||
CONFIG ../../config/generic-bigtreetech-skr-2.cfg
|
||||
CONFIG ../../config/generic-flyboard.cfg
|
||||
CONFIG ../../config/generic-mellow-fly-cdy-v3.cfg
|
||||
CONFIG ../../config/generic-mellow-super-infinty-hv.cfg
|
||||
CONFIG ../../config/generic-mks-robin-nano-v3.cfg
|
||||
CONFIG ../../config/generic-prusa-buddy.cfg
|
||||
CONFIG ../../config/generic-th3d-ezboard-lite-v2.0.cfg
|
||||
CONFIG ../../config/printer-biqu-b1-se-plus-2022.cfg
|
||||
CONFIG ../../config/printer-prusa-mini-plus-2020.cfg
|
||||
|
||||
# Printers using the stm32f446
|
||||
DICTIONARY stm32f446.dict
|
||||
CONFIG ../../config/generic-bigtreetech-octopus.cfg
|
||||
CONFIG ../../config/generic-fysetc-s6.cfg
|
||||
CONFIG ../../config/generic-fysetc-s6-v2.cfg
|
||||
CONFIG ../../config/generic-fysetc-spider.cfg
|
||||
CONFIG ../../config/generic-mks-rumba32-v1.0.cfg
|
||||
|
||||
# Printers using the stm32h743
|
||||
DICTIONARY stm32h743.dict
|
||||
CONFIG ../../config/printer-biqu-bx-2021.cfg
|
||||
|
||||
# Printers using the stm32g0b1
|
||||
DICTIONARY stm32g0b1.dict
|
||||
CONFIG ../../config/generic-bigtreetech-skr-mini-e3-v3.0.cfg
|
||||
|
||||
# Printers using the rp2040
|
||||
DICTIONARY rp2040.dict
|
||||
CONFIG ../../config/generic-bigtreetech-skr-pico-v1.0.cfg
|
||||
|
||||
# Printers using the PRU
|
||||
DICTIONARY pru.dict host=linuxprocess.dict
|
||||
CONFIG ../../config/generic-cramps.cfg
|
||||
CONFIG ../../config/generic-replicape.cfg
|
||||
|
||||
# Tests with multiple mcus
|
||||
DICTIONARY atmega2560.dict zboard=atmega2560.dict auxboard=atmega2560.dict
|
||||
CONFIG ../../config/sample-multi-mcu.cfg
|
||||
DICTIONARY atmega2560.dict z=atmega2560.dict
|
||||
CONFIG ../../config/kit-voron2-250mm.cfg
|
||||
22
test/klippy/pwm.cfg
Normal file
22
test/klippy/pwm.cfg
Normal file
@@ -0,0 +1,22 @@
|
||||
[output_pin soft_pwm_pin]
|
||||
pin: PH5
|
||||
pwm: True
|
||||
value: 0
|
||||
shutdown_value: 0
|
||||
cycle_time: 0.01
|
||||
|
||||
[output_pin hard_pwm_pin]
|
||||
pin: PH6
|
||||
pwm: True
|
||||
hardware_pwm: True
|
||||
value: 0
|
||||
shutdown_value: 0
|
||||
cycle_time: 0.01
|
||||
|
||||
[mcu]
|
||||
serial: /dev/ttyACM0
|
||||
|
||||
[printer]
|
||||
kinematics: none
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
30
test/klippy/pwm.test
Normal file
30
test/klippy/pwm.test
Normal file
@@ -0,0 +1,30 @@
|
||||
# Test case for pwm
|
||||
CONFIG pwm.cfg
|
||||
DICTIONARY atmega2560.dict
|
||||
|
||||
# Hard PWM
|
||||
# Basic test
|
||||
SET_PIN PIN=hard_pwm_pin VALUE=0
|
||||
SET_PIN PIN=hard_pwm_pin VALUE=0.5
|
||||
SET_PIN PIN=hard_pwm_pin VALUE=0.5
|
||||
SET_PIN PIN=hard_pwm_pin VALUE=0.25
|
||||
SET_PIN PIN=hard_pwm_pin VALUE=1
|
||||
|
||||
# Soft PWM
|
||||
# Test basic on off
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=0
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=0.5
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=1
|
||||
|
||||
# Test cycle time
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=0 CYCLE_TIME=0.1
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=1 CYCLE_TIME=0.5
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=0.001
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=0.75 CYCLE_TIME=0.01
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=1
|
||||
|
||||
# Test duplicate values
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=0.5
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=0.5
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=0.75 CYCLE_TIME=0.5
|
||||
SET_PIN PIN=soft_pwm_pin VALUE=0.75 CYCLE_TIME=0.75
|
||||
Reference in New Issue
Block a user