Skip to content

Instantly share code, notes, and snippets.

@TimMcMahon
Last active March 15, 2024 11:29
Show Gist options
  • Save TimMcMahon/4598ea7ae9c38e604e40bd9c13d26e6c to your computer and use it in GitHub Desktop.
Save TimMcMahon/4598ea7ae9c38e604e40bd9c13d26e6c to your computer and use it in GitHub Desktop.
Generate graphs from files generated by ble_monitor.py
#!/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()
@TimMcMahon
Copy link
Author

TimMcMahon commented Feb 24, 2024

Example usage:

python3.10 charger_plot.py -in simple-gyrfalcon-s8000-n-charge-ch1-li-ion-1-2a-molicel-p45b.log -tf gyrfalcon-s8000-n-charge-ch1-li-ion-1-2a-molicel-p45b-temp.log -wx 0.09 -wy 0.095 -g 'Gyrfalcon S8000 N Charge CH1 Li-ion 2A' -gs 'Mode: Normal | Charge | Channel: 1 | Current: 2A | Finish: 4.2V | Battery: Molicel P45B 21700 Li-ion 4500mAh' -vs 0.1 -cs 100 -dmajor 1800 -dminor 600 -wa "timmcmahon.com.au" -wi 900 -hi 600
python3.10 charger_plot.py -in simple-gyrfalcon-s8000-p-storage-ch1-li-ion-1-molicel-p45b.log -tf gyrfalcon-s8000-p-storage-ch1-li-ion-1-molicel-p45b-temp.log -wx 0.09 -wy 0.095 -g 'Gyrfalcon S8000 P Storage CH1 Li-ion 21700' -gs 'Mode: Professional | Storage | Channel: 1 | Current: 1A | Finish: 3.6V | Battery: Molicel P45B 21700 Li-ion 4500mAh' -vs 0.1 -gcmax 1100 -cs 100 -dmajor 1800 -dminor 600 -wa "timmcmahon.com.au" -wi 900 -hi 600

Example sr_config:

[DEFAULT]
Mode = summary
Duration = 0.2
Timestamp = %Y-%m-%dZ%H:%M:%S.%f
Columns = timestamp duration name model unit mean samples deviation minimum quartile1 median quartile3 maximum
Dialect = excel-tab

[batt A]
Type = ble:dmm
Address = FC:58:FA:XX:XX:XX
Model = AN9002
Unit = DC volts

[batt V]
Type = ble:dmm
Address = FC:58:FA:XX:XX:XX
Model = AN9002
Unit = DC volts

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:

[org.bluez.Error.Failed] le-connection-abort-by-local

Workaround:

  1. Remove bluez 5.64
sudo apt remove bluez
  1. Download, compile and install bluez 5.66
wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.66.tar.xz
tar -xvf bluez-5.66.tar.xz
cd bluez-5.66
./configure --prefix=/usr --mandir=/usr/share/man --sysconfdir=/etc --localstatedir=/var --enable-experimental --enable-deprecated
make
sudo make install
  1. Configure the bluetooth.service and restart it
sudo vim /lib/systemd/system/bluetooth.service
ExecStart=/usr/libexec/bluetooth/bluetoothd --experimental
sudo systemctl daemon-reload
sudo systemctl unmask bluetooth.service
sudo systemctl restart bluetooth 
  1. Install pi-bluetooth
apt-get download pi-bluetooth

sudo dpkg --ignore-depends=bluez -i pi-bluetooth_0.1.18ubuntu4_arm64.deb

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment