# V005.py (VERSION: QT-ORDER-FIX-1) print("=== V005 VERSION: QT-ORDER-FIX-1 ===") import sys # ✅ MUST create QApplication BEFORE any QWidget exists from PyQt5 import QtWidgets, QtCore app = QtWidgets.QApplication.instance() if app is None: app = QtWidgets.QApplication(sys.argv) # Now it is safe to import pyqtgraph / OpenGL stuff import asyncio import math import time import threading import numpy as np import pyqtgraph.opengl as gl from bleak import BleakClient # ====== HARD-CODED DEVICE ====== ADDRESS = "10:51:DB:1B:E7:1E" CHAR_UUID = "7e2a1002-1111-2222-3333-444455556666" latest = {"ax": 0.0, "ay": 0.0, "az": 1.0, "ts": 0.0, "count": 0, "raw": ""} def parse_csv_triplet(s: str): s = s.strip().strip('"') parts = s.split(",") if len(parts) != 3: return None try: return float(parts[0]), float(parts[1]), float(parts[2]) except ValueError: return None def notify_handler(_sender: int, data: bytearray): s = data.decode(errors="ignore").strip() latest["raw"] = s v = parse_csv_triplet(s) if v is None: return latest["ax"], latest["ay"], latest["az"] = v latest["ts"] = time.time() latest["count"] += 1 def roll_pitch_from_accel(ax, ay, az): roll = math.degrees(math.atan2(ay, az)) pitch = math.degrees(math.atan2(-ax, math.sqrt(ay * ay + az * az))) return roll, pitch async def ble_loop(): while True: try: print(f"Connecting to {ADDRESS} ...") async with BleakClient(ADDRESS, timeout=15.0) as client: if not client.is_connected: raise RuntimeError("Connect failed.") print("Connected. Enabling notifications...") await client.start_notify(CHAR_UUID, notify_handler) print("Notify enabled. Streaming...\n") while True: await asyncio.sleep(1.0) except Exception as e: print(f"[BLE] Error/disconnect: {e}") print("Reconnecting in 2 seconds...\n") await asyncio.sleep(2.0) def start_ble_thread(): t = threading.Thread(target=lambda: asyncio.run(ble_loop()), daemon=True) t.start() def make_cuboid(size=(2.0, 1.2, 0.1)): L, W, T = size x, y, z = L/2, W/2, T/2 verts = np.array([ [-x,-y,-z], [ x,-y,-z], [ x, y,-z], [-x, y,-z], [-x,-y, z], [ x,-y, z], [ x, y, z], [-x, y, z], ], dtype=float) faces = np.array([ [0,1,2],[0,2,3], [4,6,5],[4,7,6], [0,4,5],[0,5,1], [1,5,6],[1,6,2], [2,6,7],[2,7,3], [3,7,4],[3,4,0], ], dtype=int) return verts, faces class Viewer(QtWidgets.QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("ADXL345 – 3D PCB Tilt (BLE)") self.view = gl.GLViewWidget() self.view.setCameraPosition(distance=6, elevation=25, azimuth=40) self.setCentralWidget(self.view) grid = gl.GLGridItem() grid.setSize(10, 10) grid.setSpacing(1, 1) self.view.addItem(grid) axis = gl.GLAxisItem() axis.setSize(2, 2, 2) self.view.addItem(axis) verts, faces = make_cuboid() colors = np.ones((faces.shape[0], 4), dtype=float) colors[:,0]=0.25; colors[:,1]=0.7; colors[:,2]=0.35; colors[:,3]=0.9 meshdata = gl.MeshData(vertexes=verts, faces=faces) self.mesh = gl.GLMeshItem(meshdata=meshdata, faceColors=colors, drawEdges=True, edgeColor=(0,0,0,1)) self.view.addItem(self.mesh) self.vec = gl.GLLinePlotItem(pos=np.array([[0,0,0],[0,0,1]]), width=3, antialias=True) self.view.addItem(self.vec) self.label = QtWidgets.QLabel(self) self.label.setStyleSheet("color:white; background:rgba(0,0,0,140); padding:6px;") self.label.move(10, 10) self.label.resize(440, 90) self.roll = 0.0 self.pitch = 0.0 self.timer = QtCore.QTimer() self.timer.timeout.connect(self.update_scene) self.timer.start(20) def update_scene(self): axg, ayg, azg = latest["ax"], latest["ay"], latest["az"] roll, pitch = roll_pitch_from_accel(axg, ayg, azg) alpha = 0.15 self.roll = (1-alpha)*self.roll + alpha*roll self.pitch = (1-alpha)*self.pitch + alpha*pitch self.mesh.resetTransform() self.mesh.rotate(self.pitch, 0, 1, 0) self.mesh.rotate(self.roll, 1, 0, 0) self.vec.setData(pos=np.array([[0,0,0],[axg, ayg, azg]], dtype=float)) age = (time.time()-latest["ts"])*1000 if latest["ts"] else 0 self.label.setText( f"Packets: {latest['count']} Age: {age:.0f} ms\n" f"ax={axg:+.3f}g ay={ayg:+.3f}g az={azg:+.3f}g\n" f"roll={self.roll:+.1f}° pitch={self.pitch:+.1f}°\n" f"raw: {latest['raw']}" ) def main(): print("IMPORTANT: Disconnect nRF Connect (or any other BLE client) first.") start_ble_thread() win = Viewer() win.resize(900, 650) win.show() sys.exit(app.exec_()) if __name__ == "__main__": main()