#!/usr/bin/env python3 import argparse import re import struct TM_INTV = 270 # internal temperature regulation interval (ms) FAN_MAX_LAG = 2500 # maximum fan lag for reporting (ms) def parse_therm_dump(path): # header yield ['sample', 'ms', 'int', 'pwm', 't_nozzle', 't_ambient', 'fan'] cnt = 0 fan = float('NAN') fan_lag = 0 for line in open(path): # opportunistically parse M155 fan values m = re.search(r'\bE0:\d+ RPM PRN1:\d+ RPM E0@:\d+ \bPRN1@:(\d+)$', line) if m is not None: fan = int(m.group(1)) fan_lag = 0 elif fan_lag > int(FAN_MAX_LAG/TM_INTV): fan = float('NAN') # search for the D70 TML output signature m = re.search(r'\bTML (\d+) (\d+) ([0-9a-f]+) ([0-9a-f]+) ([0-9a-f]+)$', line) if m is None: continue # decode fields skip = int(m.group(1)) intv = int(m.group(2)) + TM_INTV pwm = int(m.group(3), 16) t = struct.unpack('f', int(m.group(4), 16).to_bytes(4, 'little'))[0] a = struct.unpack('f', int(m.group(5), 16).to_bytes(4, 'little'))[0] # output values ms = cnt * TM_INTV yield [cnt, ms, intv, pwm, t, a, fan] smp = skip + 1 cnt += smp fan_lag += smp def plot_therm_dump(data, output, title): import matplotlib.pyplot as plt import numpy as np plt.gcf().set_size_inches(20, 5) ts = np.array(data['ms']) / 1000 colors = iter([x['color'] for x in plt.rcParams['axes.prop_cycle']]) ax1 = plt.gca() ax1.plot(ts, data['t_nozzle'], c=next(colors), label='Nozzle (C)') ax1.axhline(data['t_ambient'][0], ls='-.', c='k', label='Ambient baseline (C)') ax1.plot(ts, data['t_ambient'], c=next(colors), label='Ambient (C)') ax1.set_ylabel('Temperature (C)') ax1.set_xlabel('Time (s)') ax1.legend(loc='upper left') ax2 = plt.twinx() ax2.plot(ts, np.array(data['fan']) / 255 * 100, c=next(colors), label='Fan (%)') ax2.plot(ts, np.array(data['pwm']) / 127 * 100, c=next(colors), label='Heater (%)') ax2.set_ylim(-1, 101) ax2.set_ylabel('Power (%)') ax2.legend(loc='upper right') plt.title(title) plt.tight_layout() plt.savefig(output, dpi=300) def main(): ap = argparse.ArgumentParser(description='Decode (or plot) the TML trace contained in the serial LOG', epilog=""" The TML trace needs to be enabled by issuing "M155 S1 C3" and "D70 S1" to the printer. Output the decoded trace to standard output by default. If --plot is provided, produce a graph into IMG directly instead. """) ap.add_argument('log', metavar='LOG', help='Serial log file containing D70 debugging traces') ap.add_argument('--plot', '-p', metavar='IMG', help='Plot trace into IMG (eg: output.png)') args = ap.parse_args() if args.plot is None: # just output the streaming trace for line in parse_therm_dump(args.log): print('\t'.join(map(str, line))) else: # convert to dict and plot it = parse_therm_dump(args.log) data = dict(zip(next(it), zip(*it))) plot_therm_dump(data, args.plot, args.log) if __name__ == '__main__': exit(main())