diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..951390d --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 130 +exclude = .git,__pycache__,.venv diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b1862c1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# this is the list of code owners that are automatically set as code +# reviewers for a new PR +* @akmaily @MaxClerkwell \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..1a1f870 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,58 @@ +name: "🐛 Bug report" +description: "Something isn’t working as expected" +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! + Please fill out **all** required fields. + - type: input + id: summary + attributes: + label: "Short summary" + placeholder: "Crash when clicking Connect" + validations: + required: true + + - type: textarea + id: steps + attributes: + label: "Steps to reproduce" + placeholder: | + 1. Go to … + 2. Click … + 3. Observe … + validations: + required: true + + - type: textarea + id: expected + attributes: + label: "Expected behaviour" + validations: + required: true + + - type: textarea + id: actual + attributes: + label: "Actual behaviour" + validations: + required: true + + - type: textarea + id: environment + attributes: + label: "Environment" + placeholder: | + - OS: + - Browser: + - Angular app version: + - Electron app verison: + - type: checkboxes + id: self_assign + attributes: + label: "Will you implement this yourself?" + options: + - label: "Yes – assign this issue to me" + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/contribution_proposal.yml b/.github/ISSUE_TEMPLATE/contribution_proposal.yml new file mode 100644 index 0000000..a57b2a0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/contribution_proposal.yml @@ -0,0 +1,48 @@ +name: "✨ Contribution proposal" +description: "Suggest a feature / improvement and (optionally) volunteer to implement it" +labels: ["enhancement", "contribution"] +body: + - type: markdown + attributes: + value: | + Tell us what you’d like to add or improve. + Check the box below if **you** intend to implement it. + - type: input + id: title + attributes: + label: "Brief title" + placeholder: "Add dark-mode toggle" + validations: + required: true + + - type: textarea + id: problem + attributes: + label: "What problem does this solve?" + validations: + required: true + + - type: textarea + id: approach + attributes: + label: "Proposed solution / approach" + placeholder: "High-level plan, API changes, UI mock-ups …" + + - type: checkboxes + id: self_assign + attributes: + label: "Will you implement this yourself?" + options: + - label: "Yes – assign this issue to me" + required: false + + - type: textarea + id: definition_of_done + attributes: + label: "Definition of Done" + placeholder: "Acceptance criteria, tests, docs…" + + - type: textarea + id: extras + attributes: + label: "Additional context / references" \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d3a8b0e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,45 @@ +## Summary + + +*Add description here* + +Issue number *Add issue number here* + +## 🛠 Type of change + + +| Type | Included | +|-------------------|----------| +| Bug fix | ❌ | +| New feature | ✅ | +| Refactor | ❌ | +| Docs / examples | ❌ | +| CI / tooling | ❌ | + +## 📝 Design Decisions + + +*Add description here* + +## How to test + +### 1. Expected behavior +*Add description here* +### 2. Steps to reproduce +*Add description here* +### 3. Is there still unexpected behaviour which needs to be addressed in the future? +*Add description here* + +## Checklist + +Make sure you + +- [ ] have read the [contribution guidelines](../CONTRIBUTION.md), +- [ ] given this PR a concise and imperative title, +- [ ] have added necessary unit/e2e tests if necessary, +- [ ] have added documentation if necessary, +- [ ] have documented the changes in the [CHANGELOG.md](../CHANGELOG.md). \ No newline at end of file diff --git a/.github/workflows/lint-and-format.yml b/.github/workflows/lint-and-format.yml new file mode 100644 index 0000000..6076462 --- /dev/null +++ b/.github/workflows/lint-and-format.yml @@ -0,0 +1,27 @@ +name: Lint and Format + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + pip install black flake8 isort + + - name: Run Black (formatter) + run: black --check src/ + + - name: Run isort (import sorting) + run: isort --check-only src/ + + - name: Run Flake8 (linter) + run: flake8 src/ diff --git a/.github/workflows/ping-codeowners-on-issue.yml b/.github/workflows/ping-codeowners-on-issue.yml new file mode 100644 index 0000000..ab364ac --- /dev/null +++ b/.github/workflows/ping-codeowners-on-issue.yml @@ -0,0 +1,34 @@ +name: Notify Selected Code Owners on Issue + +on: + issues: + types: [opened] + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Notify selected owners + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issueTitle = context.payload.issue.title; + const issueUrl = context.payload.issue.html_url; + const issueAuthor = context.payload.issue.user.login; + + let mentions = []; + mentions.push("@akmaily"); + + const message = + "Issue created by @" + issueAuthor + "\n\n" + + "Automatically pinging maintainers:\n" + + mentions.join(" ") + "\n\n" + + "Please triage or assign this issue as needed."; + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }); diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..12f2219 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,64 @@ +name: Build Executable for Linux and Windows + +on: + pull_request: + +jobs: + build-linux: + name: Build Linux Executable + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pyinstaller + + - name: Build executable (Linux) + run: | + pyinstaller --onefile ./src/main.py --name OmnAIView-Python-Linux + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: OmnAIView-Linux + path: dist/OmnAIView-Python-Linux + + build-windows: + name: Build Windows Executable + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pyinstaller + + - name: Build executable (Windows) + run: | + pyinstaller --onefile .\src\main.py --name OmnAIView-Python-Windows + shell: powershell + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: OmnAIView-Windows + path: dist\OmnAIView-Python-Windows \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fb18e02 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +### Changed +### Removed \ No newline at end of file diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md new file mode 100644 index 0000000..decd595 --- /dev/null +++ b/CONTRIBUTION.md @@ -0,0 +1,157 @@ +# Contribution Guide + +Thank you for your interest in contributing to **OmnAIView**! +This guide outlines the workflow for submitting contributions. + +## 1. Search existing issues + +Before you begin, look through the issue tracker for the feature or fix you have in mind. + +## 1. Matching issue + +Join the conversation and help decide who will take ownership. + +## 1. No matching issue + +Use our issue template to describe the change you propose. + + >For bugs use the issue template. + >For features etc. use the other template. + >If you plan to implement it yourself, tick the “I’ll do it” box. + +## 1. Discussion about the issue + +At least one maintainer (and probably other contributors) will review your issue. Together we will: + +1. give feedback + +2. refine the approach + +3. agree on a Definition of Done + +The discussion is completed when a *Definition of Done* is approved by a maintainer of the project + +## 1. Assign responsibility + +Within 24 hours after setting the Definition of Done, the issue will be assigned by a maintainer to + +1. you, if you volunteered, or + +2. another contributor with necessary skills. + +If you are assigned, follow the workflow outlined below. + +## 1. Fork the Repository (Skip if you already have a fork) + +1. Navigate to the [OmnAIView-python repository](https://github.com/omnai-project/OmnAIView-python). +1. Click the **Fork** button (top right) to create your own copy of the repository. +1. Make sure that you have added the upstream to your git repo + +Check with +``` +git remote -v +``` +Add upstream with +``` +git remote add upstream git@github.com:omnai-project/OmnAIView-python.git +``` + +## 1. Clone Your Fork + +Clone your fork to your local machine: + +```sh +git clone git@github.com:omnai-project/OmnAIView-python.git +cd OmnAIView-python +``` + +## 1. Create a Feature Branch + +Before making changes, create a new branch: + +```sh +git checkout -b feature/your-feature-name +``` + +Follow the naming convention: +- `feature/your-feature-name` for new features +- `fix/your-fix-description` for bug fixes +- `docs/update-readme` for documentation updates + +## 1. Implement Your Changes + +- Follow the project's [coding guidelines](https://angular.dev/style-guide). +- Ensure your code is properly formatted and linted by running ```npm run style```. +- Write or update tests if applicable +> this currently includes CI builds and ng test +> there are no e2e tests yet, if you want to implement one for your feature it is highly appreciated + +## 1. Lint and format your code + +Lint and format the code with the following commands: + +``` +black src/ +isort src/ +flake8 src/ +``` + +## 1. Commit Your Changes + +Write meaningful commit messages: + +```sh +git add . +git commit +``` + +It is expected that commits don't only have a header but also +1. Why did you add/change something? +2. What did you add/change in the commit? +3. Possible important things to know about the commit. + +## 1. Document your changes in the changelog + +To keep track of changes between different versions a changelog according to the (keepAChangelog)[https://keepachangelog.com/en/1.1.0/] +is used. + +It is expected that new changes are documented in this changelog. + +## 1. Push to Your Fork + +Push your branch to your fork: + +```sh +git push origin \ +``` + +## 1. Open a Pull Request + +1. Go to the original repository on GitHub. +2. Click **New Pull Request**. +3. Select your fork and branch. +4. Provide a **clear description** of your changes. Please follow our [pull request template](.github/PULL_REQUEST_TEMPLATE.md). +5. Submit the pull request. + +## 1. Review & Approval + +- Reviewers are automatically added to a PR +- PRs will be reviewed by at least **one maintainer** +- After review address requested changes by updating your branch and pushing updates. +- Keep your branch updated with the current master +- The PR needs to be approved by one maintainer before merging +- Once approved, the PR will be merged by one of the maintainer. + +## 1. Keep Your Fork Updated + +To stay up to date with the latest changes: + +```sh +git checkout master +git fetch upstream +git merge upstream/master +git push origin master +``` + +**Happy coding!** If you have any questions, feel free to ask in [Discussions](https://github.com/omnai-project/OmnAIView-python/discussions). + diff --git a/README.md b/README.md index 02bebba..a10c67b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ The example shows how to receive data from a [DevDataServer](https://github.com/ This example is part of the [OmnAIView](https://github.com/AI-Gruppe/OmnAIView) project an OpenSource project for an omnipotent Datavisualization and Dataanalyzing tool and shows an implementation in python instead of Angular. +If you want to contribute to the project please read the [CONTRIBUTION.md](./CONTRIBUTION.md). + --- ## DataServers This project works with all OmnAI compatible Data-Sources. diff --git a/requirements.txt b/requirements.txt index d41a400..41e03a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,29 @@ +black==25.1.0 certifi==2025.4.26 charset-normalizer==3.4.2 +click==8.2.1 +colorama==0.4.6 contourpy==1.3.2 cycler==0.12.1 +flake8==7.3.0 fonttools==4.58.0 idna==3.10 +isort==6.0.1 kiwisolver==1.4.8 matplotlib==3.10.3 +mccabe==0.7.0 +mypy_extensions==1.1.0 numpy==2.2.6 packaging==25.0 +pathspec==0.12.1 pillow==11.2.1 +platformdirs==4.3.8 +pycodestyle==2.14.0 +pyflakes==3.4.0 pyparsing==3.2.3 python-dateutil==2.9.0.post0 requests==2.32.3 +ruff==0.12.1 six==1.17.0 urllib3==2.4.0 -websockets==15.0.1 \ No newline at end of file +websockets==15.0.1 diff --git a/src/datasources.py b/src/datasources.py index 2ee41ae..d8667c1 100644 --- a/src/datasources.py +++ b/src/datasources.py @@ -9,21 +9,22 @@ from __future__ import annotations +import json +import random from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Dict, List, Tuple -import json -import random import requests + # ---------------------------------------------------------------------- # Domain object # ---------------------------------------------------------------------- @dataclass class Device: uuid: str - color: str # hex "#rrggbb" + color: str # hex "#rrggbb" # ---------------------------------------------------------------------- @@ -32,33 +33,27 @@ class Device: class DataSourceStrategy(ABC): """Common interface every data source must fulfil.""" - name: str # appears in the GUI drop-down - formats: List[str] # allowed data formats (json/csv …) + name: str # appears in the GUI drop-down + formats: List[str] # allowed data formats (json/csv …) # --- REST ---------------------------------------------------------- @abstractmethod - def fetch_devices(self, host_port: str) -> List[Device]: - ... + def fetch_devices(self, host_port: str) -> List[Device]: ... # --- WebSocket handshake ------------------------------------------ @abstractmethod - def build_ws_uri(self, host_port: str) -> str: - ... + def build_ws_uri(self, host_port: str) -> str: ... @abstractmethod - def build_subscribe_cmd( - self, uuids: List[str], rate: int, fmt: str - ) -> str | bytes: + def build_subscribe_cmd(self, uuids: List[str], rate: int, fmt: str) -> str | bytes: """Return the exact payload that has to be sent after the WS is open. May be text (str) or binary (bytes).""" # --- WebSocket frame parsing -------------------------------------- @abstractmethod - def parse_ws_msg( - self, raw: str | bytes - ) -> Tuple[float, Dict[str, float]]: + def parse_ws_msg(self, raw: str | bytes) -> Tuple[float, Dict[str, float]]: """Convert a raw frame into - (timestamp, {uuid: value, …}).""" + (timestamp, {uuid: value, …}).""" ... # helper for concrete strategies @@ -66,12 +61,12 @@ def _to_hex(self, maybe_rgb): if isinstance(maybe_rgb, dict): return f"#{maybe_rgb['r']:02x}{maybe_rgb['g']:02x}{maybe_rgb['b']:02x}" return maybe_rgb - + def server_sends_initial_msg(self) -> bool: """Return True if the server always sends *one* informational frame immediately after the WebSocket is opened and the client must wait (and optionally inspect it) before sending its subscribe command.""" - return False # default: talk first + return False # default: talk first def handle_initial_msg(self, raw: str | bytes) -> None: """Called with that very first frame when @@ -79,6 +74,7 @@ def handle_initial_msg(self, raw: str | bytes) -> None: Default behaviour: just ignore it.""" return + # ---------------------------------------------------------------------- # Strategy registry (simple plugin container) # ---------------------------------------------------------------------- @@ -99,11 +95,11 @@ def get_strategy(name: str) -> DataSourceStrategy: return _registry[name]() - # ====================================================================== # Concrete Strategies # ====================================================================== + # ---------------------------------------------------------------------- # 1) DevDataServer (existing backend) # ---------------------------------------------------------------------- @@ -146,13 +142,15 @@ def parse_ws_msg(self, raw): if raw.startswith("{"): obj = json.loads(raw) ts_UNIX = obj["timestamp"] - values = obj["data"][0] # list in *subscription* order + values = obj["data"][0] # list in *subscription* order else: - # ---------- CSV ---------- + # ---------- CSV ---------- parts = raw.split(",") ts = float(parts[0]) values = list(map(float, parts[1:])) - ts = int(ts_UNIX * 1000) # transform UNIX timestamps into correct integer values + ts = int( + ts_UNIX * 1000 + ) # transform UNIX timestamps into correct integer values return ts, dict(zip(self._uuids, values)) @@ -160,6 +158,7 @@ def parse_ws_msg(self, raw): # 2) OmnAIScope DataServer (new backend) # ---------------------------------------------------------------------- + @register class OmnAIScopeStrategy(DataSourceStrategy): """ @@ -188,10 +187,12 @@ def fetch_devices(self, host_port: str) -> List[Device]: devices: List[Device] = [] for idx, ds in enumerate(ds_list): uid = ds.get("UUID") or ds.get("uuid") - # Get colors + # Get colors if idx < len(color_list): col = color_list[idx].get("color", {}) - color_hex = f"#{col.get('r', 0):02x}{col.get('g', 0):02x}{col.get('b', 0):02x}" + color_hex = ( + f"#{col.get('r', 0):02x}{col.get('g', 0):02x}{col.get('b', 0):02x}" + ) else: color_hex = f"#{random.randint(0, 0xFFFFFF):06x}" devices.append(Device(uuid=uid, color=color_hex)) @@ -234,13 +235,13 @@ def parse_ws_msg(self, raw: str | bytes): # → Binary / unknown (skip) raise ValueError("Unsupported or malformed frame received from OmnAIScope.") - + # ---------- WS “server-talks-first” ---------- def server_sends_initial_msg(self) -> bool: - return True # <- OmnAIScope does send one frame first + return True # <- OmnAIScope does send one frame first def handle_initial_msg(self, raw): # We don’t care what it is for now – but you could log or parse it. if isinstance(raw, bytes): raw = raw.decode(errors="ignore") - print(f"[OmnAIScope] initial server frame: {raw!r}") \ No newline at end of file + print(f"[OmnAIScope] initial server frame: {raw!r}") diff --git a/src/main.py b/src/main.py index 4198aed..2fe1188 100644 --- a/src/main.py +++ b/src/main.py @@ -5,38 +5,37 @@ Start: python main.py """ +import argparse import asyncio +import datetime import json +import os import queue import threading +import tkinter as tk from functools import partial +from tkinter import messagebox, ttk from typing import Dict, List, Tuple -import os -import datetime import matplotlib import matplotlib.animation as animation import matplotlib.pyplot as plt -import tkinter as tk -from tkinter import messagebox, ttk +import requests from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from websockets import connect -import requests -import argparse -from datasources import ( - Device, - available_sources, - get_strategy, -) +from datasources import Device, available_sources, get_strategy -#------------------------------------------------------------ +# ------------------------------------------------------------ # Parse command-line arguments -#------------------------------------------------------------ +# ------------------------------------------------------------ parser = argparse.ArgumentParser() -parser.add_argument('--logging', action='store_true', help='Enable terminal logging of stream values') +parser.add_argument( + "--logging", action="store_true", help="Enable terminal logging of stream values" +) args = parser.parse_args() -ENABLE_LOG = args.logging if 'args' in globals() else False +ENABLE_LOG = args.logging if "args" in globals() else False + # ------------------------------------------------------------ # GUI-Class @@ -50,15 +49,18 @@ def __init__(self): # ­­­– Toolbar –­­­ bar = ttk.Frame(self) bar.pack(side="top", fill="x") - ttk.Button(bar, text="Connect to Websocket", - command=self._connect_dialog).pack(side="left", padx=4, pady=4) - # Record data : Data is saved in RAM until flushed - self.rec_btn = ttk.Button(bar, text="● Record", - command=self._toggle_recording, state="disabled") + ttk.Button(bar, text="Connect to Websocket", command=self._connect_dialog).pack( + side="left", padx=4, pady=4 + ) + # Record data : Data is saved in RAM until flushed + self.rec_btn = ttk.Button( + bar, text="● Record", command=self._toggle_recording, state="disabled" + ) self.rec_btn.pack(side="left", padx=4, pady=4) - # Analyse data : This is currently fixed to one analysis on a specific server - self.analyse_btn = ttk.Button(bar, text="Analysis", - command=self._run_analysis, state="disabled") + # Analyse data : This is currently fixed to one analysis on a specific server + self.analyse_btn = ttk.Button( + bar, text="Analysis", command=self._run_analysis, state="disabled" + ) self.analyse_btn.pack(side="left", padx=4, pady=4) # ­­­– Plot-Window –­­­ @@ -79,12 +81,12 @@ def __init__(self): self.strategy = None self.host_port = "" self.active_uuids: List[str] = [] - - # State Management of Toolbar - self.recording = False - self.record_data = [] # List[ List[float] ] - self.record_fh = None - self.last_record_file = None + + # State Management of Toolbar + self.recording = False + self.record_data = [] # List[ List[float] ] + self.record_fh = None + self.last_record_file = None # Animation of the data self.ani = animation.FuncAnimation( @@ -102,30 +104,35 @@ def _connect_dialog(self): dlg = tk.Toplevel(self) dlg.title("Connect") - ttk.Label(dlg, text="WebSocket-Server (ip:port):") \ - .grid(row=0, column=0, padx=6, pady=4, sticky="e") + ttk.Label(dlg, text="WebSocket-Server (ip:port):").grid( + row=0, column=0, padx=6, pady=4, sticky="e" + ) host_var = tk.StringVar(value="localhost:8080") - ttk.Entry(dlg, textvariable=host_var, width=20) \ - .grid(row=0, column=1, pady=4) + ttk.Entry(dlg, textvariable=host_var, width=20).grid(row=0, column=1, pady=4) - ttk.Label(dlg, text="Data source:") \ - .grid(row=1, column=0, padx=6, pady=4, sticky="e") + ttk.Label(dlg, text="Data source:").grid( + row=1, column=0, padx=6, pady=4, sticky="e" + ) src_var = tk.StringVar(value=available_sources()[0]) ttk.Combobox( - dlg, textvariable=src_var, + dlg, + textvariable=src_var, values=available_sources(), - state="readonly", width=18 + state="readonly", + width=18, ).grid(row=1, column=1, pady=4) ttk.Button( - dlg, text="Connect", + dlg, + text="Connect", command=lambda: self._on_connect_submit( - dlg, host_var.get().strip(), src_var.get()) + dlg, host_var.get().strip(), src_var.get() + ), ).grid(row=2, column=0, columnspan=2, pady=8) def _on_connect_submit(self, dlg, host_port, source_name): dlg.destroy() - self.strategy = get_strategy(source_name) # ← concrete Strategy + self.strategy = get_strategy(source_name) # ← concrete Strategy self.host_port = host_port try: devices = self.strategy.fetch_devices(host_port) @@ -145,31 +152,34 @@ def _device_dialog(self, devices: List[Device]): vars_: Dict[str, tk.BooleanVar] = {} for dev in devices: v = tk.BooleanVar(value=False) - ttk.Checkbutton(dlg, text=dev.uuid, variable=v) \ - .pack(anchor="w", padx=20) + ttk.Checkbutton(dlg, text=dev.uuid, variable=v).pack(anchor="w", padx=20) vars_[dev.uuid] = v frm = ttk.Frame(dlg) frm.pack(fill="x", pady=6) - ttk.Label(frm, text="Samplerate (Hz):") \ - .grid(row=0, column=0, sticky="e") + ttk.Label(frm, text="Samplerate (Hz):").grid(row=0, column=0, sticky="e") rate_var = tk.IntVar(value=60) - ttk.Entry(frm, textvariable=rate_var, width=8) \ - .grid(row=0, column=1, sticky="w", padx=4) + ttk.Entry(frm, textvariable=rate_var, width=8).grid( + row=0, column=1, sticky="w", padx=4 + ) - ttk.Label(frm, text="Format:") \ - .grid(row=1, column=0, sticky="e") + ttk.Label(frm, text="Format:").grid(row=1, column=0, sticky="e") fmt_var = tk.StringVar(value=self.strategy.formats[0]) - ttk.Combobox(frm, textvariable=fmt_var, - values=self.strategy.formats, - state="readonly", width=7) \ - .grid(row=1, column=1, sticky="w", padx=4) + ttk.Combobox( + frm, + textvariable=fmt_var, + values=self.strategy.formats, + state="readonly", + width=7, + ).grid(row=1, column=1, sticky="w", padx=4) - ttk.Button(dlg, text="Start measurement", - command=partial( - self._start_measurement, - dlg, vars_, devices, rate_var, fmt_var) - ).pack(pady=8) + ttk.Button( + dlg, + text="Start measurement", + command=partial( + self._start_measurement, dlg, vars_, devices, rate_var, fmt_var + ), + ).pack(pady=8) # -------------------------------------------------------- # Step 3 – WebSocket thread start @@ -203,11 +213,10 @@ def _start_measurement(self, dlg, vars_, devices, rate_var, fmt_var): target=self._ws_worker, args=( self.strategy.build_ws_uri(self.host_port), - self.strategy.build_subscribe_cmd( - uuids, rate_var.get(), fmt_var.get() - ), + self.strategy.build_subscribe_cmd(uuids, rate_var.get(), fmt_var.get()), ), - daemon=True) + daemon=True, + ) self.ws_thread.start() # change toolbar states self.rec_btn.config(state="normal") @@ -215,14 +224,14 @@ def _start_measurement(self, dlg, vars_, devices, rate_var, fmt_var): # -------------------------------------------------------- # Background Thread – WebSocket client (asyncio) - # connects to websocket and receives the data + # connects to websocket and receives the data # -------------------------------------------------------- def _ws_worker(self, uri: str, subscribe_cmd: str | bytes): async def _runner(): async with connect(uri) as ws: # ---- optional “server-talks-first” handshake ------------------- if self.strategy.server_sends_initial_msg(): - first_frame = await ws.recv() # wait & store + first_frame = await ws.recv() # wait & store self.strategy.handle_initial_msg(first_frame) # may ignore # ---- now send the normal subscribe command -------------------- await ws.send(subscribe_cmd) @@ -231,10 +240,11 @@ async def _runner(): try: raw = await asyncio.wait_for(ws.recv(), timeout=0.5) except asyncio.TimeoutError: - continue # regelmäßig Stop-Flag prüfen + continue # regelmäßig Stop-Flag prüfen ts, val_dict = self.strategy.parse_ws_msg(raw) - values = [val_dict.get(uid, float("nan")) - for uid in self.active_uuids] + values = [ + val_dict.get(uid, float("nan")) for uid in self.active_uuids + ] # Conditional terminal logging if ENABLE_LOG: print(f"Timestamp: {ts}, Values: {values}") @@ -246,11 +256,11 @@ async def _runner(): asyncio.run(_runner()) except Exception as e: self.queue.put(("__error__", e)) - + # -------------------------------------------------------- # Record-Handling (record a measurement) - # -------------------------------------------------------- - + # -------------------------------------------------------- + def _toggle_recording(self): if self.recording: self._stop_recording() @@ -273,14 +283,14 @@ def _stop_recording(self): # dump JSON and close json.dump({"signal": self.record_data}, self.record_fh, indent=2) self.record_fh.close() - # set name of last file recorded / this is dummy code seriously not a pretty solution for this - self.last_record_file = self.record_fh.name + # set name of last file recorded / this is dummy code seriously not a pretty solution for this + self.last_record_file = self.record_fh.name self.analyse_btn.config(state="normal") - # recording state + # recording state self.recording = False self.rec_btn.config(text="● Record") print(f"[Recorder] stopped: {self.record_fh.name}") - + # -------------------------------------------------------- # send last recording as pure JSON to /mean and show result # -------------------------------------------------------- @@ -292,12 +302,12 @@ def _run_analysis(self): # 1) load JSON file try: with open(self.last_record_file, "r", encoding="utf-8") as fh: - payload = json.load(fh) + payload = json.load(fh) except Exception as exc: messagebox.showerror("Analysis", f"Cannot read file:\n{exc}") return - # 2) POST as application/json + # 2) POST as application/json url = "http://127.0.0.1:8000/mean" try: resp = requests.post(url, json=payload, timeout=10) @@ -306,10 +316,8 @@ def _run_analysis(self): except Exception as exc: messagebox.showerror("Analysis", f"Request failed:\n{exc}") return - - messagebox.showinfo("Analysis", f"Mittelwert : {mean_val}") - + messagebox.showinfo("Analysis", f"Mittelwert : {mean_val}") # -------------------------------------------------------- # Plot-Update (every 100 ms) @@ -324,7 +332,7 @@ def _update_plot(self, _): ts, values = item # Record data until flushed if self.recording: - self.record_data.append([ts, *values]) + self.record_data.append([ts, *values]) for uid, val in zip(self.active_uuids, values): self.data_buffer[uid].append((ts, val)) # keep only last 1000 datapoints @@ -344,8 +352,8 @@ def _update_plot(self, _): # -------------------------------------------------------- def _on_close(self): self.stop_event.set() - # ensure data is flushed when app is closed - if self.recording: + # ensure data is flushed when app is closed + if self.recording: self._stop_recording() self.destroy()