init workflow

This commit is contained in:
2025-04-20 23:23:41 +02:00
parent 05abcca6f5
commit ed7c1bf62e
12 changed files with 201 additions and 88 deletions

View File

@@ -2,5 +2,15 @@
App and small arduino code to develop an optimal filter algorithm. App and small arduino code to develop an optimal filter algorithm.
## Install dependencies
```
pip install -r requirements.txt
```
## Run App
```
python -m filter_dev_app.app
```
## Algorithm ## Algorithm

View File

@@ -1,5 +1,4 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
@@ -7,7 +6,7 @@ import asyncio
from .filter import * from .filter import *
from .gui.device import Device from .gui.device import Device
from .gui.slider import Slider from .gui.toolbar import RecordForm, FilterForm, DataStats
class FilterDevApp(tk.Tk): class FilterDevApp(tk.Tk):
def __init__(self, loop: asyncio.EventLoop): def __init__(self, loop: asyncio.EventLoop):
@@ -15,9 +14,7 @@ class FilterDevApp(tk.Tk):
self.loop = loop self.loop = loop
self.protocol("WM_DELETE_WINDOW", self.close) self.protocol("WM_DELETE_WINDOW", self.close)
self.tasks = [] self.tasks = []
self.tasks.append(loop.create_task(self.updater(1./2))) self.tasks.append(loop.create_task(self.updater(1./100)))
self.tasks.append(loop.create_task(self.update_plot(1./20)))
self.tasks.append(loop.create_task(self.read_values(1./100)))
self.filter = None self.filter = None
@@ -29,6 +26,7 @@ class FilterDevApp(tk.Tk):
# Create a figure for plotting # Create a figure for plotting
self.fig, self.ax = plt.subplots() self.fig, self.ax = plt.subplots()
self.ax2 = self.ax.twinx()
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame) self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame)
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH) self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH)
@@ -37,91 +35,92 @@ class FilterDevApp(tk.Tk):
self.toolbar.pack(side=tk.RIGHT, fill=tk.Y) self.toolbar.pack(side=tk.RIGHT, fill=tk.Y)
# Device Settings # Device Settings
self.connect_button = ttk.Button(self.toolbar, text="Connect", command=self.connect_disconnect) self.record_form = RecordForm(self.toolbar, self.record_data)
self.connect_button.pack(pady=10) self.record_form.pack(pady=10)
self.device_label = ttk.Label(self.toolbar, text="Device Name:")
self.device_label.pack(pady=10)
self.device_name = ttk.Entry(self.toolbar)
self.device_name.insert(0, "Smaage") # Set default value
self.device_name.pack(pady=10)
self.device = Device(self.device_name) self.device = Device(self.record_form.device_name)
# Filter Settings # Filter Settings
self.filter_type_label = ttk.Label(self.toolbar, text="Filter:") self.filter_form = FilterForm(self.toolbar, self.update_filter)
self.filter_type_label.pack(pady=10) self.filter = MovAvg(self.device, self.toolbar, self.update_plot)
self.filter_type_combobox = ttk.Combobox(self.toolbar, values=["MovAvg"])
self.filter_type_combobox.set("MovAvg") # Set default value
self.filter_type_combobox.pack(pady=10)
self.change_filter = ttk.Button(self.toolbar, text="Change Filter", command=self.update_filter)
self.change_filter.pack(pady=10)
# Objects self.data_stats = DataStats(self.toolbar, self.reset)
self.filter = MovAvg(self.device, self.toolbar, lambda: None)
self.filter.pack()
async def update_plot(self, interval): def update_plot(self):
while await asyncio.sleep(interval, True): if self.filter is None:
if self.filter is None: return
continue
# Clear the current plot # Clear the current plot
self.ax.clear() self.ax.clear()
self.ax2.clear()
# Get current values from sliders # Get current values from sliders
df = self.filter() df = self.filter()
self.data_stats.update_stats(df)
# Generate data # Generate data
x = df['timestamps'] x = df['timestamps']
y1 = df['weights'] y1 = df['weights']
y2 = df['filtered'] # y1_g = df['calib_weights']
# y2 = df['filtered']
y2_g = df['filtered_calib']
# Plot the data # Plot the data
self.ax.plot(x, y1) self.ax.plot(x, y1)
self.ax.plot(x, y2) # self.ax.plot(x, y2)
self.ax.set_xlabel("Time in ms") self.ax.set_xlabel("Time in ms")
self.ax.set_ylabel("Weight") self.ax.set_ylabel("Raw Weight")
self.ax.grid() self.ax.grid()
# Draw the updated plot # self.ax2.plot(x, y1_g)
self.canvas.draw() self.ax2.plot(x, y2_g, color="orange")
# Draw the updated plot
self.canvas.draw()
def update_filter(self): def update_filter(self):
option = self.filter_type_combobox.get() option = self.filter_form.filter_type_combobox.get()
if option == 'MovAvg' and not isinstance(self.filter, MovAvg): if option == 'MovAvg' and not isinstance(self.filter, MovAvg):
self.filter = MovAvg(self.device, self.toolbar, self.update_plot) self.filter = MovAvg(self.device, self.toolbar, self.update_plot)
self.update_plot() self.update_plot()
def connect_disconnect(self): def record_data(self):
if self.device.is_connected: if self.device.is_connected:
self.device_label.pack(pady=10) self.record_form.pack(pady=10)
self.device_name.pack(pady=10)
self.connect_button.config(text="Connect")
self.connect_button.pack(pady=10)
self.device.disconnect() self.device.disconnect()
else: else:
task = self.loop.create_task(self.device.connect()) record_duration = self.record_form.record_time.get_value()
task.add_done_callback(self.connected) task = self.loop.create_task(self.device.read_values(record_duration))
task.add_done_callback(self.data_recorded)
def connected(self, *args): def data_recorded(self, *args):
if self.device.is_connected: if self.device.is_connected:
self.device_label.pack_forget() self.record_form.pack_forget()
self.device_name.pack_forget()
self.connect_button.config(text="Disconnect")
self.filter.pack() self.filter_form.pack(pady=10)
self.filter.pack(pady=10)
self.data_stats.pack(pady=10)
async def read_values(self, interval): self.update_filter()
await self.device.read_values(interval)
async def updater(self, interval): async def updater(self, interval):
while await asyncio.sleep(interval, True): while await asyncio.sleep(interval, True):
self.update() self.update()
def reset(self):
self.device.disconnect()
self.device.clear_data()
self.filter_form.pack_forget()
self.filter.pack_forget()
self.data_stats.pack_forget()
self.record_form.pack()
self.update_plot()
def close(self): def close(self):
for task in self.tasks: for task in self.tasks:

View File

@@ -2,3 +2,5 @@
SERVICE_UUID = "9f0dfdb2-e978-494c-8f15-68dbe8d28672" SERVICE_UUID = "9f0dfdb2-e978-494c-8f15-68dbe8d28672"
MILLIS_UUID = "abb92561-a809-453c-8c7c-71d3fff5b86e" MILLIS_UUID = "abb92561-a809-453c-8c7c-71d3fff5b86e"
WEIGHT_UUID = "123e4567-e89b-12d3-a456-426614174000" WEIGHT_UUID = "123e4567-e89b-12d3-a456-426614174000"
DEFAULT_CALIB = 104167.17

View File

@@ -3,8 +3,8 @@ from typing import Dict
import pandas as pd import pandas as pd
from tkinter.ttk import Frame from tkinter.ttk import Frame
from ..gui.device import Device from ..gui import Device, Slider, Entry
from ..gui.slider import Slider from ..config import DEFAULT_CALIB
class Filter: class Filter:
@@ -15,6 +15,8 @@ class Filter:
self.toolbar = toolbar self.toolbar = toolbar
self.callback = callback self.callback = callback
self.calib_entry = Entry(toolbar, "Calibration Factor", DEFAULT_CALIB)
self.init_params(toolbar) self.init_params(toolbar)
def init_params(self, toolbar): def init_params(self, toolbar):
@@ -28,17 +30,21 @@ class Filter:
return params return params
def __call__(self) -> pd.DataFrame: def __call__(self) -> pd.DataFrame:
calib_factor = 100. / float(self.calib_entry.get())
df = self.device.data df = self.device.data
df['filtered'] = self.filter(df) df['filtered'], df['filtered_calib'] = self.filter(df, calib_factor)
df['calib_weights'] = df['weights'] * calib_factor
return df return df
def filter(self, df: pd.DataFrame) -> pd.Series: def filter(self, df: pd.DataFrame, calib_factor: float) -> pd.Series:
raise NotImplementedError() raise NotImplementedError()
def pack(self): def pack(self, **kwargs):
self.calib_entry.pack(**kwargs)
for k, v in self.param_map.items(): for k, v in self.param_map.items():
v.pack() v.pack(**kwargs)
def pack_forget(self): def pack_forget(self):
self.calib_entry.pack_forget()
for k, v in self.param_map.items(): for k, v in self.param_map.items():
v.pack_forget() v.pack_forget()

View File

@@ -1,18 +1,21 @@
from tkinter import ttk
import pandas as pd import pandas as pd
from .base import Filter from .base import Filter
from ..gui.slider import Slider from ..gui import Slider
class MovAvg(Filter): class MovAvg(Filter):
def init_params(self, toolbar): def init_params(self, toolbar):
self.param_map = { self.param_map = {
"window_size": Slider(toolbar, "Window Size", 1, 500, 10, self.callback), "window_size": Slider(toolbar, "Window Size", 1, 100, 10, self.callback),
"decimals": Slider(toolbar, "Decimals", 1, 5, 1, self.callback), "decimals": Slider(toolbar, "Decimals", 1, 5, 1, self.callback),
# "reset_threshold": Slider(self.toolbar, "Reset Threshold", 0.001, 0.1, 0.1, self.update), # "reset_threshold": Slider(self.toolbar, "Reset Threshold", 0.001, 0.1, 0.1, self.update),
} }
def filter(self, df: pd.DataFrame) -> pd.Series: def filter(self, df: pd.DataFrame, calib_factor: float) -> pd.Series:
params = self._get_params() params = self._get_params()
return df['weights'].rolling(window=int(params['window_size'])).mean()\ mov_avg = df['weights'].rolling(window=int(params['window_size'])).mean()
.round(int(params['decimals'])) mov_avg_calib = (mov_avg * calib_factor).round(int(params['decimals']))
return mov_avg, mov_avg_calib

View File

@@ -0,0 +1,4 @@
from .device import Device
from .slider import Slider
from .entry import Entry

View File

@@ -1,7 +1,9 @@
from bleak import BleakClient, BleakScanner from bleak import BleakClient, BleakScanner
import pandas as pd import pandas as pd
from tkinter.ttk import Entry from tkinter.ttk import Entry
from tkinter.messagebox import showerror, showinfo
import asyncio import asyncio
from time import time
from ..config import MILLIS_UUID, WEIGHT_UUID from ..config import MILLIS_UUID, WEIGHT_UUID
@@ -32,21 +34,26 @@ class Device:
def disconnect(self): def disconnect(self):
self.device = None self.device = None
async def read_values(self, interval): async def read_values(self, duration):
while await asyncio.sleep(interval, True): if not await self.connect():
if self.is_connected: showerror("Record Data", f"Device {self.device_name.get()} not found!")
async with BleakClient(self.device.address) as client: return
while await asyncio.sleep(interval, True):
millis = await client.read_gatt_char(MILLIS_UUID)
millis = int.from_bytes(millis, byteorder='little') # Adjust based on your data format
weight = await client.read_gatt_char(WEIGHT_UUID)
weight = int.from_bytes(weight, byteorder='little') # Adjust based on your data format
self.timestamps.append(millis) self.clear_data()
self.weights.append(weight) async with BleakClient(self.device.address) as client:
showinfo("Recording Data", f"Recording data for {duration} seconds.")
time_start = time()
time_passed = 0
while time_passed < duration:
millis = await client.read_gatt_char(MILLIS_UUID)
millis = int.from_bytes(millis, byteorder='little') # Adjust based on your data format
weight = await client.read_gatt_char(WEIGHT_UUID)
weight = int.from_bytes(weight, byteorder='little') # Adjust based on your data format
if not self.is_connected: self.timestamps.append(millis)
break self.weights.append(weight)
time_passed = time() - time_start
def clear_data(self): def clear_data(self):

19
filter_dev/gui/entry.py Normal file
View File

@@ -0,0 +1,19 @@
from tkinter import ttk
class Entry:
def __init__(self, master, title, default_value):
self.label = ttk.Label(master, text=title)
self.entry = ttk.Entry(master)
self.entry.insert(0, default_value) # Set default value
def get(self):
return self.entry.get()
def pack(self, **kwargs):
self.label.pack(**kwargs)
self.entry.pack(**kwargs)
def pack_forget(self):
self.label.pack_forget()
self.entry.pack_forget()

View File

@@ -0,0 +1,3 @@
from .record_form import RecordForm
from .filter_form import FilterForm
from .data_stats import DataStats

View File

@@ -0,0 +1,26 @@
import tkinter as tk
from tkinter import ttk
import pandas as pd
class DataStats(tk.Frame):
def __init__(self, master, command, **kwargs):
super().__init__(master, **kwargs)
self.items = ttk.Label(self, text="Recorded Items:")
self.items.pack()
self.duration = ttk.Label(self, text="Time Delta:")
self.duration.pack()
self.std = ttk.Label(self, text="Raw Weights:")
self.std.pack()
self.reset_button = ttk.Button(self, text="Reset", command=command)
self.reset_button.pack()
def update_stats(self, df: pd.DataFrame):
self.items.config(text=f"Recorded Items: {df.size:01d}")
delta = df['timestamps'].diff().shift(-1)
self.duration.config(text=f"Time Delta: {delta.mean():.2f} ± {delta.std():.2f}")
self.std.config(text=f"Raw Weights: {df['weights'].mean():.2f} ± {df['weights'].std():.2f}")

View File

@@ -0,0 +1,15 @@
import tkinter as tk
from tkinter import ttk
class FilterForm(tk.Frame):
def __init__(self, master, command, **kwargs):
super().__init__(master, **kwargs)
self.filter_type_label = ttk.Label(self, text="Filter:")
self.filter_type_label.pack()
self.filter_type_combobox = ttk.Combobox(self, values=["MovAvg"])
self.filter_type_combobox.pack()
self.filter_type_combobox.set("MovAvg") # Set default value
self.change_filter = ttk.Button(self, text="Change Filter", command=command)
self.change_filter.pack()

View File

@@ -0,0 +1,19 @@
import tkinter as tk
from tkinter import ttk
from ..slider import Slider
class RecordForm(tk.Frame):
def __init__(self, master, record_command, **kwargs):
super().__init__(master, **kwargs)
self.device_label = ttk.Label(self, text="Device Name:")
self.device_label.pack(pady=10)
self.device_name = ttk.Entry(self)
self.device_name.insert(0, "Smaage") # Set default value
self.device_name.pack()
self.record_time = Slider(self, "Record Time:", 10, 30, 10, lambda: None)
self.record_time.pack(pady=10)
self.record_button = ttk.Button(self, text="Record Data", command=record_command)
self.record_button.pack(pady=10)