-
-
Save TimMcMahon/4598ea7ae9c38e604e40bd9c13d26e6c to your computer and use it in GitHub Desktop.
Generate graphs from files generated by ble_monitor.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# old fields Time,Lux,[relative time],Duration,Lumens,Temperature (C) | |
# new fields timestamp batt A (DC volts) batt V (DC volts) | |
# new fields Time,Current,Voltage | |
# Based on https://github.com/bmengineer-gear/RuTiTe/blob/master/rutite.py | |
from time import strftime, gmtime, sleep | |
from datetime import datetime, timedelta | |
from string import Template | |
import time | |
import math | |
import os.path | |
from os import path | |
import numpy | |
import pandas as pd | |
import argparse | |
import sys | |
import matplotlib.pyplot as plt | |
import copy | |
from matplotlib.ticker import (MultipleLocator, FormatStrFormatter, | |
AutoMinorLocator, FuncFormatter, NullFormatter) | |
import matplotlib.font_manager | |
plt.rcParams["font.family"] = 'sans-serif' | |
PX = 1/plt.rcParams['figure.dpi'] | |
TEMP_STEP = 2 | |
SMALL_SIZE = 8 | |
MEDIUM_SIZE = 9 | |
BIGGER_SIZE = 12 | |
TITLE_SIZE = 14 | |
SUBTITLE_SIZE = 10 | |
# https://xkcd.com/color/rgb/ | |
COLOUR_VOLTAGE = 'xkcd:bright red' | |
COLOUR_CURRENT = 'xkcd:kelly green' | |
COLOUR_TEMP = 'xkcd:black' | |
class DeltaTemplate(Template): | |
delimiter = "%" | |
def strfdelta(tdelta, fmt): | |
d = {"D": tdelta.days} | |
hours, rem = divmod(int(tdelta.total_seconds()), 3600) | |
minutes, seconds = divmod(rem, 60) | |
d["H"] = '{:02d}'.format(hours) | |
d["M"] = '{:02d}'.format(minutes) | |
d["S"] = '{:02d}'.format(seconds) | |
t = DeltaTemplate(fmt) | |
return t.substitute(**d) | |
def build_parser(): | |
parser = argparse.ArgumentParser() | |
parser.add_argument('-in','--inputfile', dest='filename', | |
help = 'filename for the csv input') | |
parser.add_argument('-tf','--temperature-file', dest='temperature_filename', | |
help = 'filename for the temperature csv input') | |
parser.add_argument('-g', '--graph-title', dest='graph_title', | |
help = 'graph title') | |
parser.add_argument('-gs', '--graph-subtitle', dest='graph_subtitle', | |
help = 'graph subtitle') | |
parser.add_argument('-yl', '--y-label', dest='y_label', | |
default = 'Voltage', | |
help = 'y label (e.g. Voltage)') | |
parser.add_argument('-sr', '--shunt-resistance', dest='shunt_resistance', type=float, | |
default = 0.0095686, | |
help = 'shunt resistance') | |
parser.add_argument('-gvmin', '--graph-voltage-min', dest='graph_voltage_min', type=float, | |
default = 2.9, | |
help = 'graph voltage min') | |
parser.add_argument('-gvmax', '--graph-voltage-max', dest='graph_voltage_max', type=float, | |
default = 4.3, | |
help = 'graph voltage max') | |
parser.add_argument('-vs', '--voltage-step', dest='voltage_step', type=float, | |
default = 0.05, | |
help = 'voltage step') | |
parser.add_argument('-cs', '--current-step', dest='current_step', type=float, | |
default = 100, | |
help = 'current step') | |
parser.add_argument('-ts', '--temperature-step', dest='temperature_step', type=float, | |
default = 5, | |
help = 'temperature step') | |
parser.add_argument('-gcmin', '--graph-current-min', dest='graph_current_min', type=int, | |
default = 0, | |
help = 'graph current min') | |
parser.add_argument('-gcmax', '--graph-current-max', dest='graph_current_max', type=int, | |
default = 2200, | |
help = 'graph current max') | |
parser.add_argument('-gtmin', '--graph-temp-min', dest='graph_temp_min', type=int, | |
default = 20, | |
help = 'graph temperature min') | |
parser.add_argument('-gtmax', '--graph-temp-max', dest='graph_temp_max', type=int, | |
default = 80, | |
help = 'graph temperature max') | |
parser.add_argument('-dmax', '--duration-max', dest='duration_max', type=int, | |
help = 'max duration in seconds') | |
parser.add_argument('-dminor', '--duration-minor', dest='duration_minor', type=int, | |
default = 300, | |
help = 'minor gridlines for duration in seconds') | |
parser.add_argument('-dmajor', '--duration-major', dest='duration_major', type=int, | |
default = 600, | |
help = 'major gridlines for duration in seconds') | |
parser.add_argument('-wi', '--width', dest='width', type=int, | |
default = 900, | |
help = 'width') | |
parser.add_argument('-hi', '--height', dest='height', type=int, | |
default = 900, | |
help = 'height') | |
parser.add_argument('-wa', '--watermark', dest='watermark', | |
default = '', | |
help = 'watermark') | |
parser.add_argument('-wx', '--watermark-x', dest='watermark_x', type=float, | |
default = 0.08, | |
help = 'x location of watermark') | |
parser.add_argument('-wy', '--watermark-y', dest='watermark_y', type=float, | |
default = 0.3, | |
help = 'y location of watermark') | |
return parser | |
def load_options(): | |
parser = build_parser() | |
options = parser.parse_args() | |
return options | |
def runtimeplot(options): | |
print('Creating plot...') | |
data = pd.read_csv(options.filename, sep='\t') | |
data = data.rename(columns={ | |
"timestamp": "Time", | |
"batt A (DC volts)": "Current", | |
"batt V (DC volts)":"Voltage" | |
}, errors="raise") | |
# 2024-02-24 07:45:42.809613 string to timestamp | |
data.Time = pd.to_datetime(data.Time, format="%Y-%m-%d %H:%M:%S.%f") | |
data.set_index('Time', drop=False) | |
temperature_data = pd.read_csv(options.temperature_filename, sep='\t') | |
temperature_data = temperature_data.rename(columns={ | |
"timestamp": "Time", | |
"temperature": "Temperature" | |
}, errors="raise") | |
temperature_data.Time = pd.to_datetime(temperature_data.Time, format="%Y-%m-%d %H:%M:%S.%f") | |
temperature_data.set_index('Time', drop=False) | |
data = data.join(temperature_data[['Temperature']]) | |
plt.rc('font', size=SMALL_SIZE) # controls default text sizes | |
plt.rc('axes', titlesize=SMALL_SIZE) # fontsize of the axes title | |
plt.rc('axes', labelsize=MEDIUM_SIZE) # fontsize of the x and y labels | |
plt.rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels | |
plt.rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels | |
plt.rc('legend', fontsize=SMALL_SIZE) # legend fontsize | |
plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title | |
fig, ax = plt.subplots(figsize=(options.width*PX, options.height*PX)) | |
plt.suptitle(options.graph_title + '\n', fontsize=TITLE_SIZE, x=0.01, ha='left') | |
plt.rcParams['axes.titlepad'] = TITLE_SIZE | |
#fig.text(0.01, 0.92, options.graph_subtitle, fontsize=SUBTITLE_SIZE, ha='left', alpha=0.8) | |
fig.text(0.011, 0.918, options.graph_subtitle, fontsize=SUBTITLE_SIZE, ha='left', alpha=0.8) | |
plt.grid(True, which='both') | |
ax.minorticks_on() | |
twin1 = ax.twinx() | |
twin2 = ax.twinx() | |
twin3 = ax.twinx() | |
twin3.spines.right.set_position(("axes", 1.1)) | |
# Make Duration start at zero (use Time to calculate duration). | |
# FIXME is this working? Chopped start off? | |
#time_start = datetime.fromtimestamp(data.Time.to_timestamp().min()) | |
#time_finish = datetime.fromtimestamp(data.Time.to_timestamp().max()) | |
time_start = data.Time.min() | |
time_finish = data.Time.max() | |
duration = int((time_finish - time_start).ceil('s').total_seconds()) | |
if options.duration_max: | |
duration = options.duration_max | |
print("Duration: %s seconds" % duration) | |
data.Time = data.Time.map(lambda x: (x - time_start).total_seconds()) | |
# Ohm's law: V=IR. I=V/R. Convert 0.00099 V (00.99mV displayed). 0.00099 / 0.0095686 = 0.10346 A | |
data.Current = (data.Current / options.shunt_resistance) * 1000 | |
ax.plot(data.Time, data.Voltage, color=COLOUR_VOLTAGE, label='Voltage') | |
ax.set_xlabel('Duration hh:mm:ss') | |
ax.set_ylabel('Voltage') | |
ax.set_ylim((options.graph_voltage_min, options.graph_voltage_max)) | |
ax.set_yticks(numpy.arange(options.graph_voltage_min, options.graph_voltage_max + options.voltage_step, options.voltage_step)) | |
ax.yaxis.set_minor_locator(MultipleLocator(options.voltage_step)) | |
ax.yaxis.set_minor_formatter(NullFormatter()) | |
ax.yaxis.set_major_formatter(FormatStrFormatter('%.2f')) | |
ax.yaxis.tick_left() | |
ax.yaxis.label.set_color(COLOUR_VOLTAGE) | |
ax.tick_params(axis='y', colors=COLOUR_VOLTAGE, which='both') | |
formatter = FuncFormatter(lambda s, x: strfdelta(((time_start + pd.to_timedelta(s, unit='s')) - time_start), '%H:%M:%S')) | |
ax.xaxis.set_major_formatter(formatter) | |
ax.xaxis.set_minor_locator(MultipleLocator(options.duration_minor)) | |
ax.set_xticks([x for x in range(0, (duration * 60) + 1, options.duration_major)]) | |
x_ticks = ax.xaxis.get_major_ticks() | |
x_ticks[0].label1.set_visible(False) | |
x_ticks[-1].label1.set_visible(False) | |
twin1.plot(data.Time, data['Voltage'], color=COLOUR_VOLTAGE, label='Voltage') | |
twin1.set_ylim((options.graph_voltage_min, options.graph_voltage_max)) | |
twin1.set_yticks(numpy.arange(options.graph_voltage_min, options.graph_voltage_max + options.voltage_step, options.voltage_step)) | |
twin1.yaxis.set_minor_locator(MultipleLocator(options.voltage_step)) | |
twin1.yaxis.set_minor_formatter(NullFormatter()) | |
twin1.yaxis.set_major_formatter(FormatStrFormatter('%.2f')) | |
twin1.yaxis.tick_left() | |
twin1.yaxis.label.set_color(COLOUR_VOLTAGE) | |
twin1.tick_params(axis='y', colors=COLOUR_VOLTAGE, which='both') | |
twin2.plot(data.Time, data['Current'], color=COLOUR_CURRENT, label='Current (mA)') | |
twin2.set_ylabel('Current (mA)') | |
twin2.set_ylim((options.graph_current_min, options.graph_current_max)) | |
twin2.set_yticks(numpy.arange(options.graph_current_min, options.graph_current_max + options.current_step, options.current_step)) | |
twin2.yaxis.set_minor_locator(MultipleLocator(options.current_step)) | |
twin2.yaxis.set_minor_formatter(NullFormatter()) | |
twin2.yaxis.set_major_formatter(FormatStrFormatter('%d')) | |
twin2.yaxis.label.set_color(COLOUR_CURRENT) | |
twin2.tick_params(axis='y', colors=COLOUR_CURRENT, which='both') | |
twin3.plot(data.Time, data['Temperature'], color=COLOUR_TEMP, label='Temperature (C)') | |
twin3.set_ylabel('Temperature (C)') | |
twin3.set_ylim((options.graph_temp_min, options.graph_temp_max)) | |
twin3.set_yticks(numpy.arange(options.graph_temp_min, options.graph_temp_max + options.temperature_step, options.temperature_step)) | |
twin3.yaxis.set_minor_locator(MultipleLocator(options.temperature_step)) | |
twin3.yaxis.set_minor_formatter(NullFormatter()) | |
twin3.yaxis.set_major_formatter(FormatStrFormatter('%d')) | |
ax.set_zorder(1) | |
ax.set_frame_on(False) | |
twin1.set_zorder(3) | |
twin2.set_zorder(2) | |
twin3.set_zorder(4) | |
ax.xaxis.grid(color='gray', linestyle='dashed', which='both') | |
ax.yaxis.grid(color='gray', linestyle='dashed', which='both') | |
fig.text(options.watermark_x, options.watermark_y, options.watermark, alpha=0.5, fontsize=TITLE_SIZE, ha='left') | |
plt.xlim(left=0) | |
plt.xlim(right=duration) | |
# Hide borders | |
# ax.spines['left'].set_visible(False) | |
# ax.spines['right'].set_visible(False) | |
# ax.spines[['top','right']].set_visible(True) | |
#twin2.spines['left'].set_visible(False) | |
#twin2.spines['left'].set_visible(False) | |
#twin3.spines['top'].set_visible(False) | |
#twin3.spines['top'].set_visible(False) | |
# Hide left ticks lines | |
# y_ticks = ax.yaxis.get_major_ticks() | |
# [t.tick1line.set_visible(False) for t in y_ticks] | |
# y_ticks = twin2.yaxis.get_major_ticks() | |
# [t.tick1line.set_visible(False) for t in y_ticks] | |
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes] | |
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)] | |
lines = lines[1:] | |
labels = labels[1:] | |
# legend line thickness | |
handles = [copy.copy(ha) for ha in lines] | |
[ha.set_linewidth(2) for ha in handles] | |
fig.legend(handles, labels, | |
ncol=3, | |
loc='upper center', | |
frameon=False, | |
bbox_to_anchor=(0.5, 0.908), | |
borderaxespad=0, | |
bbox_transform=fig.transFigure | |
) | |
plt.tight_layout(rect=[0, 0, 1, 0.99]) | |
plt.savefig(options.graph_title.replace(' ', '-').lower()+'.png') | |
print('plot saved') | |
def main(): | |
options = load_options() | |
runtimeplot(options) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example usage:
Example sr_config:
Troubleshooting installing ble_monitor.py on Ubuntu 22.04 on an rpi4:
http://parametrek.com/synthetic-runtime/code.zip
This error happens with ubuntu 22.04 and rpi4 and bluez 5.64:
Workaround: