init workflow
This commit is contained in:
10
README.md
10
README.md
@@ -2,5 +2,15 @@
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
|
||||
@@ -7,7 +6,7 @@ import asyncio
|
||||
|
||||
from .filter import *
|
||||
from .gui.device import Device
|
||||
from .gui.slider import Slider
|
||||
from .gui.toolbar import RecordForm, FilterForm, DataStats
|
||||
|
||||
class FilterDevApp(tk.Tk):
|
||||
def __init__(self, loop: asyncio.EventLoop):
|
||||
@@ -15,9 +14,7 @@ class FilterDevApp(tk.Tk):
|
||||
self.loop = loop
|
||||
self.protocol("WM_DELETE_WINDOW", self.close)
|
||||
self.tasks = []
|
||||
self.tasks.append(loop.create_task(self.updater(1./2)))
|
||||
self.tasks.append(loop.create_task(self.update_plot(1./20)))
|
||||
self.tasks.append(loop.create_task(self.read_values(1./100)))
|
||||
self.tasks.append(loop.create_task(self.updater(1./100)))
|
||||
|
||||
self.filter = None
|
||||
|
||||
@@ -29,6 +26,7 @@ class FilterDevApp(tk.Tk):
|
||||
|
||||
# Create a figure for plotting
|
||||
self.fig, self.ax = plt.subplots()
|
||||
self.ax2 = self.ax.twinx()
|
||||
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame)
|
||||
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)
|
||||
|
||||
# Device Settings
|
||||
self.connect_button = ttk.Button(self.toolbar, text="Connect", command=self.connect_disconnect)
|
||||
self.connect_button.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.record_form = RecordForm(self.toolbar, self.record_data)
|
||||
self.record_form.pack(pady=10)
|
||||
|
||||
self.device = Device(self.device_name)
|
||||
self.device = Device(self.record_form.device_name)
|
||||
|
||||
# Filter Settings
|
||||
self.filter_type_label = ttk.Label(self.toolbar, text="Filter:")
|
||||
self.filter_type_label.pack(pady=10)
|
||||
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)
|
||||
self.filter_form = FilterForm(self.toolbar, self.update_filter)
|
||||
self.filter = MovAvg(self.device, self.toolbar, self.update_plot)
|
||||
|
||||
# Objects
|
||||
self.filter = MovAvg(self.device, self.toolbar, lambda: None)
|
||||
self.filter.pack()
|
||||
self.data_stats = DataStats(self.toolbar, self.reset)
|
||||
|
||||
async def update_plot(self, interval):
|
||||
while await asyncio.sleep(interval, True):
|
||||
if self.filter is None:
|
||||
continue
|
||||
def update_plot(self):
|
||||
if self.filter is None:
|
||||
return
|
||||
|
||||
# Clear the current plot
|
||||
self.ax.clear()
|
||||
# Clear the current plot
|
||||
self.ax.clear()
|
||||
self.ax2.clear()
|
||||
|
||||
# Get current values from sliders
|
||||
df = self.filter()
|
||||
# Get current values from sliders
|
||||
df = self.filter()
|
||||
self.data_stats.update_stats(df)
|
||||
|
||||
# Generate data
|
||||
x = df['timestamps']
|
||||
y1 = df['weights']
|
||||
y2 = df['filtered']
|
||||
# Generate data
|
||||
x = df['timestamps']
|
||||
y1 = df['weights']
|
||||
# y1_g = df['calib_weights']
|
||||
# y2 = df['filtered']
|
||||
y2_g = df['filtered_calib']
|
||||
|
||||
# Plot the data
|
||||
self.ax.plot(x, y1)
|
||||
self.ax.plot(x, y2)
|
||||
self.ax.set_xlabel("Time in ms")
|
||||
self.ax.set_ylabel("Weight")
|
||||
self.ax.grid()
|
||||
# Plot the data
|
||||
self.ax.plot(x, y1)
|
||||
# self.ax.plot(x, y2)
|
||||
self.ax.set_xlabel("Time in ms")
|
||||
self.ax.set_ylabel("Raw Weight")
|
||||
self.ax.grid()
|
||||
|
||||
# Draw the updated plot
|
||||
self.canvas.draw()
|
||||
# self.ax2.plot(x, y1_g)
|
||||
self.ax2.plot(x, y2_g, color="orange")
|
||||
|
||||
# Draw the updated plot
|
||||
self.canvas.draw()
|
||||
|
||||
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):
|
||||
self.filter = MovAvg(self.device, self.toolbar, self.update_plot)
|
||||
|
||||
self.update_plot()
|
||||
|
||||
def connect_disconnect(self):
|
||||
def record_data(self):
|
||||
if self.device.is_connected:
|
||||
self.device_label.pack(pady=10)
|
||||
self.device_name.pack(pady=10)
|
||||
self.connect_button.config(text="Connect")
|
||||
self.connect_button.pack(pady=10)
|
||||
self.record_form.pack(pady=10)
|
||||
|
||||
self.device.disconnect()
|
||||
|
||||
else:
|
||||
task = self.loop.create_task(self.device.connect())
|
||||
task.add_done_callback(self.connected)
|
||||
record_duration = self.record_form.record_time.get_value()
|
||||
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:
|
||||
self.device_label.pack_forget()
|
||||
self.device_name.pack_forget()
|
||||
self.connect_button.config(text="Disconnect")
|
||||
self.record_form.pack_forget()
|
||||
|
||||
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):
|
||||
await self.device.read_values(interval)
|
||||
self.update_filter()
|
||||
|
||||
async def updater(self, interval):
|
||||
while await asyncio.sleep(interval, True):
|
||||
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):
|
||||
for task in self.tasks:
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
SERVICE_UUID = "9f0dfdb2-e978-494c-8f15-68dbe8d28672"
|
||||
MILLIS_UUID = "abb92561-a809-453c-8c7c-71d3fff5b86e"
|
||||
WEIGHT_UUID = "123e4567-e89b-12d3-a456-426614174000"
|
||||
|
||||
DEFAULT_CALIB = 104167.17
|
||||
@@ -3,8 +3,8 @@ from typing import Dict
|
||||
import pandas as pd
|
||||
from tkinter.ttk import Frame
|
||||
|
||||
from ..gui.device import Device
|
||||
from ..gui.slider import Slider
|
||||
from ..gui import Device, Slider, Entry
|
||||
from ..config import DEFAULT_CALIB
|
||||
|
||||
class Filter:
|
||||
|
||||
@@ -15,6 +15,8 @@ class Filter:
|
||||
self.toolbar = toolbar
|
||||
self.callback = callback
|
||||
|
||||
self.calib_entry = Entry(toolbar, "Calibration Factor", DEFAULT_CALIB)
|
||||
|
||||
self.init_params(toolbar)
|
||||
|
||||
def init_params(self, toolbar):
|
||||
@@ -28,17 +30,21 @@ class Filter:
|
||||
return params
|
||||
|
||||
def __call__(self) -> pd.DataFrame:
|
||||
calib_factor = 100. / float(self.calib_entry.get())
|
||||
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
|
||||
|
||||
def filter(self, df: pd.DataFrame) -> pd.Series:
|
||||
def filter(self, df: pd.DataFrame, calib_factor: float) -> pd.Series:
|
||||
raise NotImplementedError()
|
||||
|
||||
def pack(self):
|
||||
def pack(self, **kwargs):
|
||||
self.calib_entry.pack(**kwargs)
|
||||
for k, v in self.param_map.items():
|
||||
v.pack()
|
||||
v.pack(**kwargs)
|
||||
|
||||
def pack_forget(self):
|
||||
self.calib_entry.pack_forget()
|
||||
for k, v in self.param_map.items():
|
||||
v.pack_forget()
|
||||
@@ -1,18 +1,21 @@
|
||||
from tkinter import ttk
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from .base import Filter
|
||||
from ..gui.slider import Slider
|
||||
from ..gui import Slider
|
||||
|
||||
class MovAvg(Filter):
|
||||
|
||||
def init_params(self, toolbar):
|
||||
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),
|
||||
# "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()
|
||||
return df['weights'].rolling(window=int(params['window_size'])).mean()\
|
||||
.round(int(params['decimals']))
|
||||
mov_avg = df['weights'].rolling(window=int(params['window_size'])).mean()
|
||||
mov_avg_calib = (mov_avg * calib_factor).round(int(params['decimals']))
|
||||
return mov_avg, mov_avg_calib
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .device import Device
|
||||
|
||||
from .slider import Slider
|
||||
from .entry import Entry
|
||||
@@ -1,7 +1,9 @@
|
||||
from bleak import BleakClient, BleakScanner
|
||||
import pandas as pd
|
||||
from tkinter.ttk import Entry
|
||||
from tkinter.messagebox import showerror, showinfo
|
||||
import asyncio
|
||||
from time import time
|
||||
|
||||
from ..config import MILLIS_UUID, WEIGHT_UUID
|
||||
|
||||
@@ -32,21 +34,26 @@ class Device:
|
||||
def disconnect(self):
|
||||
self.device = None
|
||||
|
||||
async def read_values(self, interval):
|
||||
while await asyncio.sleep(interval, True):
|
||||
if self.is_connected:
|
||||
async with BleakClient(self.device.address) as client:
|
||||
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
|
||||
async def read_values(self, duration):
|
||||
if not await self.connect():
|
||||
showerror("Record Data", f"Device {self.device_name.get()} not found!")
|
||||
return
|
||||
|
||||
self.timestamps.append(millis)
|
||||
self.weights.append(weight)
|
||||
self.clear_data()
|
||||
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:
|
||||
break
|
||||
self.timestamps.append(millis)
|
||||
self.weights.append(weight)
|
||||
|
||||
time_passed = time() - time_start
|
||||
|
||||
|
||||
def clear_data(self):
|
||||
|
||||
19
filter_dev/gui/entry.py
Normal file
19
filter_dev/gui/entry.py
Normal 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()
|
||||
3
filter_dev/gui/toolbar/__init__.py
Normal file
3
filter_dev/gui/toolbar/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .record_form import RecordForm
|
||||
from .filter_form import FilterForm
|
||||
from .data_stats import DataStats
|
||||
26
filter_dev/gui/toolbar/data_stats.py
Normal file
26
filter_dev/gui/toolbar/data_stats.py
Normal 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}")
|
||||
15
filter_dev/gui/toolbar/filter_form.py
Normal file
15
filter_dev/gui/toolbar/filter_form.py
Normal 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()
|
||||
19
filter_dev/gui/toolbar/record_form.py
Normal file
19
filter_dev/gui/toolbar/record_form.py
Normal 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)
|
||||
Reference in New Issue
Block a user