From 9abfb936a32a6bbabf314e02f59d8a1db6df1b8f Mon Sep 17 00:00:00 2001 From: Jannes Date: Sun, 13 Apr 2025 22:55:55 +0200 Subject: [PATCH] init --- .gitignore | 1 + README.md | 6 ++ esp32_readout/display.cpp | 37 ++++++++++ esp32_readout/display.h | 25 +++++++ esp32_readout/env.h | 67 ++++++++++++++++++ esp32_readout/readout_test.ino | 121 +++++++++++++++++++++++++++++++++ filter_dev/app.py | 91 +++++++++++++++++++++++++ filter_dev/ble.py | 47 +++++++++++++ filter_dev/config.py | 4 ++ filter_dev/filter/__init__.py | 1 + filter_dev/filter/base.py | 36 ++++++++++ filter_dev/filter/mov_avg.py | 18 +++++ filter_dev/gui/__init__.py | 0 filter_dev/gui/device.py | 40 +++++++++++ filter_dev/gui/slider.py | 28 ++++++++ requirements.txt | 5 ++ 16 files changed, 527 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 esp32_readout/display.cpp create mode 100644 esp32_readout/display.h create mode 100644 esp32_readout/env.h create mode 100644 esp32_readout/readout_test.ino create mode 100644 filter_dev/app.py create mode 100644 filter_dev/ble.py create mode 100644 filter_dev/config.py create mode 100644 filter_dev/filter/__init__.py create mode 100644 filter_dev/filter/base.py create mode 100644 filter_dev/filter/mov_avg.py create mode 100644 filter_dev/gui/__init__.py create mode 100644 filter_dev/gui/device.py create mode 100644 filter_dev/gui/slider.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..115d4c6 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Filter Dev App + +App and small arduino code to develop an optimal filter algorithm. + +## Algorithm + diff --git a/esp32_readout/display.cpp b/esp32_readout/display.cpp new file mode 100644 index 0000000..c799e4c --- /dev/null +++ b/esp32_readout/display.cpp @@ -0,0 +1,37 @@ +#include "display.h" + +#include + +Display::Display() { + this->_display = new Adafruit_SharpMem(SHARP_SCK, SHARP_MOSI, SHARP_SS, DISPLAY_HEIGHT, DISPLAY_WIDTH); + + this->mid_x = DISPLAY_WIDTH / 2; + this->mid_y = DISPLAY_HEIGHT / 2; +} + +void Display::setup() { + // start & clear the display + this->_display->begin(); + this->_display->clearDisplay(); + this->_display->setRotation(2); + + this->_display->setTextColor(BLACK, WHITE); + this->_display->setTextSize(FONT_SIZE); + this->_display->setFont(FONT); + + this->print("Smaage"); +} + +void Display::print(const char* text) { + this->_display->clearDisplay(); + this->_display->setCursor(12, this->mid_x); + this->_display->println(text); + this->_display->refresh(); +} + +void Display::print_weight(double val) { + this->_display->clearDisplay(); + this->_display->setCursor(12, this->mid_x); + this->_display->printf("%.2f g", val); + this->_display->refresh(); +} \ No newline at end of file diff --git a/esp32_readout/display.h b/esp32_readout/display.h new file mode 100644 index 0000000..cc89794 --- /dev/null +++ b/esp32_readout/display.h @@ -0,0 +1,25 @@ + +#ifndef DISPLAY_DEF + #define DISPLAY_DEF + + #include + #include + #include + + #include "env.h" + + class Display { + private: + Adafruit_SharpMem* _display; + + int mid_x; + int mid_y; + + public: + Display(); + void setup(); + void print(const char* text); + void print_weight(double weight); + }; + +#endif \ No newline at end of file diff --git a/esp32_readout/env.h b/esp32_readout/env.h new file mode 100644 index 0000000..df1ff75 --- /dev/null +++ b/esp32_readout/env.h @@ -0,0 +1,67 @@ +#ifndef ENV + #define ENV + #include + #include + #include + #include + + #include + #include + #include + + /********************* NAU7802 ****************************/ + + #define LDO NAU7802_3V3 + #define GAIN NAU7802_GAIN_128 + #define SPS NAU7802_RATE_20SPS + + /****************************** DISPLAY *******************/ + + // any pins can be used + #define SHARP_SCK 7 + #define SHARP_MOSI 9 + #define SHARP_SS 44 + #define DISPLAY_HEIGHT 144 + #define DISPLAY_WIDTH 168 + + #define BLACK 0 + #define WHITE 1 + + #define FONT_SIZE 2 + #define FONT &FreeSans9pt7b + + /********************* BLE ********************************/ + + #define BLE true + #if BLE + #define SERVICE_UUID "9f0dfdb2-e978-494c-8f15-68dbe8d28672" + #define MILLIS_UUID "abb92561-a809-453c-8c7c-71d3fff5b86e" + #define WEIGHT_UUID "123e4567-e89b-12d3-a456-426614174000" + #endif + + #if GAIN == NAU7802_GAIN_1 + #define CALIBRATION_FACTOR 100.0 / 424.47 + + #elif GAIN == NAU7802_GAIN_2 + #define CALIBRATION_FACTOR 100.0 / 2457.96 + + #elif GAIN == NAU7802_GAIN_4 + #define CALIBRATION_FACTOR 100.0 / 3622.82 + + #elif GAIN == NAU7802_GAIN_8 + #define CALIBRATION_FACTOR 100.0 / 6630.74 + + #elif GAIN == NAU7802_GAIN_16 + #define CALIBRATION_FACTOR 100.0 / 13179.24 + + #elif GAIN == NAU7802_GAIN_32 + #define CALIBRATION_FACTOR 100.0 / 25955.84 + + #elif GAIN == NAU7802_GAIN_64 + #define CALIBRATION_FACTOR 100.0 / 52865.63 + + #elif GAIN == NAU7802_GAIN_128 + #define CALIBRATION_FACTOR 100.0 / 104167.17 + #endif + +#endif \ No newline at end of file diff --git a/esp32_readout/readout_test.ino b/esp32_readout/readout_test.ino new file mode 100644 index 0000000..bd19af1 --- /dev/null +++ b/esp32_readout/readout_test.ino @@ -0,0 +1,121 @@ +#include "env.h" +#include "display.h" + +Display display; +Adafruit_NAU7802 nau; +unsigned long _millis = 0; +int32_t val = 0; +#if BLE + BLEServer *pServer = NULL; + BLECharacteristic *millisCharacteristic = NULL; + BLECharacteristic *weightCharacteristic = NULL; + BLEAdvertising *pAdvertising = NULL; + bool deviceConnected = false; + bool oldDeviceConnected = false; + + // Callback class to handle connection events + class MyServerCallbacks : public BLEServerCallbacks { + void onConnect(BLEServer* pServer) { + deviceConnected = true; + BLEDevice::startAdvertising(); + } + + void onDisconnect(BLEServer* pServer) { + deviceConnected = false; + } + }; +#endif + +void setup() { + Serial.begin(115200); + display.setup(); + + if (! nau.begin()) { + Serial.println("Failed to find NAU7802"); + while (1) delay(10); // Don't proceed. + } + + nau.setLDO(LDO); + nau.setGain(GAIN); + nau.setRate(SPS); + + // Take 10 readings to flush out readings + for (uint8_t i=0; i<10; i++) { + while (! nau.available()) delay(1); + nau.read(); + } + + while (! nau.calibrate(NAU7802_CALMOD_INTERNAL)) { + Serial.println("Failed to calibrate internal offset, retrying!"); + delay(1000); + } + + while (! nau.calibrate(NAU7802_CALMOD_OFFSET)) { + Serial.println("Failed to calibrate system offset, retrying!"); + delay(1000); + } + + #if BLE + // initialize the Bluetooth® Low Energy hardware + BLEDevice::init("Smaage"); + pServer = BLEDevice::createServer(); + pServer->setCallbacks(new MyServerCallbacks()); + + BLEService *pService = pServer->createService(SERVICE_UUID); + + millisCharacteristic = pService->createCharacteristic( + MILLIS_UUID, + BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY + ); + weightCharacteristic = pService->createCharacteristic( + WEIGHT_UUID, + BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY + ); + millisCharacteristic->setValue((uint8_t *)&_millis, sizeof _millis); + weightCharacteristic->setValue((uint8_t *)&val, sizeof val); + pService->start(); + + pAdvertising = BLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(SERVICE_UUID); + pAdvertising->setScanResponse(true); + BLEDevice::startAdvertising(); + + Serial.println("BLE device is now advertising..."); + Serial.print("BLE Address: "); + Serial.println(BLEDevice::getAddress().toString().c_str()); + #endif + + display.print("Smaage is ready!"); +} + +void loop() { + while (!nau.available()) { + delay(1); + } + val = nau.read(); + + #if BLE + if (deviceConnected) { + // Send the sensor reading + _millis = millis(); + millisCharacteristic->setValue((uint8_t *)&_millis, sizeof _millis); + millisCharacteristic->notify(); + weightCharacteristic->setValue((uint8_t *)&val, sizeof val); + weightCharacteristic->notify(); + } + // disconnecting + if (!deviceConnected && oldDeviceConnected) { + delay(500); // give the bluetooth stack the chance to get things ready + pServer->startAdvertising(); // restart advertising + Serial.println("start advertising"); + oldDeviceConnected = deviceConnected; + } + // connecting + if (deviceConnected && !oldDeviceConnected) { + // do stuff here on connecting + oldDeviceConnected = deviceConnected; + } + #else + Serial.print(millis()); Serial.print(","); Serial.println(val); + #endif +} diff --git a/filter_dev/app.py b/filter_dev/app.py new file mode 100644 index 0000000..0b68050 --- /dev/null +++ b/filter_dev/app.py @@ -0,0 +1,91 @@ +import tkinter as tk +from tkinter import ttk +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg + +from .filter import * +from .gui.device import Device + +class FilterDevApp: + def __init__(self, root): + self.filter = None + + self.root = root + self.root.title("JannTers Filter Evaluation Tool") + + # Create a frame for the plot and sliders + self.frame = tk.Frame(self.root) + self.frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # Create a figure for plotting + self.fig, self.ax = plt.subplots() + self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame) + self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + # Create a frame for sliders + self.toolbar = tk.Frame(self.root) + self.toolbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Device Settings + 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.connect_button = ttk.Button(self.toolbar, text="Connect Device", command=self.device.connect) + self.connect_button.pack(pady=10) + + # 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) + + # Objects + self.filter = MovAvg(self.device, self.toolbar, self.update_plot) + + # Initial plot + self.update_plot() + + def update_plot(self, *args): + if self.filter is None: + return + + # Clear the current plot + self.ax.clear() + + # Get current values from sliders + df = self.filter() + + # Generate data + x = df['timestamps'] + y1 = df['weights'] + y2 = df['filtered'] + + # 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() + + # Draw the updated plot + self.canvas.draw() + + def update_filter(self): + option = self.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() + +if __name__ == "__main__": + root = tk.Tk() + app = FilterDevApp(root) + root.mainloop() diff --git a/filter_dev/ble.py b/filter_dev/ble.py new file mode 100644 index 0000000..91c813a --- /dev/null +++ b/filter_dev/ble.py @@ -0,0 +1,47 @@ +import matplotlib.pyplot as plt +from time import time +import asyncio +from bleak import BleakClient, BleakScanner +import pandas as pd +from argparse import ArgumentParser +from tqdm.auto import tqdm + +async def read_ble(reading_time=READING_TIME): + device = await BleakScanner.find_device_by_name("Smaage") + timestamps = [] + raw_weights = [] + + async with BleakClient(device.address) as client: + print(f"Connected to {device}") + + # Read the characteristic value + pbar = tqdm(desc="reading data", total=reading_time) + time_start = time() + time_passed = 0 + last_time_passed = 0 + while time_passed < reading_time: + try: + # Read the sensor data + 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 + + timestamps.append(millis) + raw_weights.append(weight) + + except Exception as e: + print(f"Error reading data: {e}") + break + + time_passed = time() - time_start + time_delta = (time_passed - last_time_passed) + last_time_passed = time_passed + pbar.update(time_delta) + + pbar.close() + + df = pd.DataFrame({"timestamps": timestamps, "weights": raw_weights}) + print(df['weights'].describe()) + plt.plot(timestamps, raw_weights) + plt.show() diff --git a/filter_dev/config.py b/filter_dev/config.py new file mode 100644 index 0000000..736f7b9 --- /dev/null +++ b/filter_dev/config.py @@ -0,0 +1,4 @@ +# Replace with your Arduino's service and characteristic UUIDs +SERVICE_UUID = "9f0dfdb2-e978-494c-8f15-68dbe8d28672" +MILLIS_UUID = "abb92561-a809-453c-8c7c-71d3fff5b86e" +WEIGHT_UUID = "123e4567-e89b-12d3-a456-426614174000" \ No newline at end of file diff --git a/filter_dev/filter/__init__.py b/filter_dev/filter/__init__.py new file mode 100644 index 0000000..7a0ea17 --- /dev/null +++ b/filter_dev/filter/__init__.py @@ -0,0 +1 @@ +from .mov_avg import MovAvg \ No newline at end of file diff --git a/filter_dev/filter/base.py b/filter_dev/filter/base.py new file mode 100644 index 0000000..a5a0222 --- /dev/null +++ b/filter_dev/filter/base.py @@ -0,0 +1,36 @@ +from typing import Dict + +import pandas as pd +from tkinter.ttk import Frame + +from ..gui.device import Device +from ..gui.slider import Slider + +class Filter: + + param_map: Dict[str, Slider] = {} + + def __init__(self, device: Device, toolbar: Frame, callback: callable): + self.device = device + self.toolbar = toolbar + self.callback = callback + + self.init_params(toolbar) + + def init_params(self, toolbar): + raise NotImplementedError() + + + def _get_params(self): + params = {} + for k, v in self.param_map.items(): + params[k] = v.get_value() + return params + + def __call__(self) -> pd.DataFrame: + df = self.device.data + df['filtered'] = self.filter(df) + return df + + def filter(self, df: pd.DataFrame) -> pd.Series: + raise NotImplementedError() \ No newline at end of file diff --git a/filter_dev/filter/mov_avg.py b/filter_dev/filter/mov_avg.py new file mode 100644 index 0000000..4d1ee05 --- /dev/null +++ b/filter_dev/filter/mov_avg.py @@ -0,0 +1,18 @@ +import pandas as pd + +from .base import Filter +from ..gui.slider import Slider + +class MovAvg(Filter): + + def init_params(self, toolbar): + self.param_map = { + "window_size": Slider(toolbar, "Window Size", 1, 500, 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: + params = self._get_params() + return df['weights'].rolling(window=int(params['window_size'])).mean()\ + .round(int(params['decimals'])) diff --git a/filter_dev/gui/__init__.py b/filter_dev/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/filter_dev/gui/device.py b/filter_dev/gui/device.py new file mode 100644 index 0000000..56ba385 --- /dev/null +++ b/filter_dev/gui/device.py @@ -0,0 +1,40 @@ +from bleak import BleakClient, BleakScanner +import pandas as pd +from tkinter.ttk import Entry + +from ..config import MILLIS_UUID, WEIGHT_UUID + +class Device: + + @property + def is_connected(self): + return self.device is not None + + @property + def data(self): + return pd.DataFrame({ + "timestamps": self.timestamps, + "weights": self.weights + }) + + def __init__(self, device_name: Entry): + self.device = None + self.device_name = device_name + self.timestamps = [] + self.weights = [] + + async def connect(self): + self.device = await BleakScanner.find_device_by_name(self.device_name.get()) + + assert self.device is not None, "No Device found!" + + async def read_values(self): + assert self.is_connected, "Not connected" + async with BleakClient(self.device.address) as client: + 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.weights.append(weight) \ No newline at end of file diff --git a/filter_dev/gui/slider.py b/filter_dev/gui/slider.py new file mode 100644 index 0000000..6693c65 --- /dev/null +++ b/filter_dev/gui/slider.py @@ -0,0 +1,28 @@ +from tkinter import ttk + +class Slider: + def __init__(self, + parent, + label_text, + from_, to, + initial_value, + command): + self.command = command + + self.frame = ttk.Frame(parent) + self.frame.pack(pady=10) + + self.label = ttk.Label(self.frame, text=f"{label_text}: {int(initial_value)}") + self.label.pack() + + self.slider = ttk.Scale(self.frame, from_=from_, to=to, orient='horizontal', command=self.update) + self.slider.set(initial_value) + self.slider.pack() + + def update(self, event=None): + value = self.slider.get() + self.label.config(text=f"{self.label.cget('text').split(':')[0]}: {int(value)}") + self.command() + + def get_value(self): + return self.slider.get() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5f823fa --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +matplotlib +bleak +pandas +tqdm +numpy \ No newline at end of file