import asyncio import math import time import threading import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation from bleak import BleakClient # ====== HARD-CODED DEVICE ====== ADDRESS = "10:51:DB:1B:E7:1E" # ====== ADXL345 CHARACTERISTIC UUID ====== CHAR_UUID = "7e2a1002-1111-2222-3333-444455556666" # Plot refresh interval (ms) PLOT_INTERVAL_MS = 50 latest = { "ax": 0.0, "ay": 0.0, "az": 1.0, "ts": 0.0, "raw": "", "count": 0, } 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 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 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 ax, ay, az = v latest["ax"], latest["ay"], latest["az"] = ax, ay, az latest["ts"] = time.time() latest["count"] += 1 # Debug print every 5 packets (avoid flooding) if latest["count"] % 5 == 0: print(f"RX[{latest['count']}]: {ax:+.4f}, {ay:+.4f}, {az:+.4f}") 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 (not connected).") print("Connected.") print("Subscribing to 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 main(): print("IMPORTANT: Disconnect nRF Connect (or any other BLE client) first.") start_ble_thread() # --- Matplotlib setup --- fig = plt.figure() ax3 = fig.add_subplot(111, projection="3d") ax3.set_title("ADXL345 live acceleration vector (g)") lim = 2.0 ax3.set_xlim(-lim, lim) ax3.set_ylim(-lim, lim) ax3.set_zlim(-lim, lim) ax3.set_xlabel("X (g)") ax3.set_ylabel("Y (g)") ax3.set_zlabel("Z (g)") # Axes lines ax3.plot([-lim, lim], [0, 0], [0, 0]) ax3.plot([0, 0], [-lim, lim], [0, 0]) ax3.plot([0, 0], [0, 0], [-lim, lim]) # Initial arrow + overlay q = ax3.quiver(0, 0, 0, 0, 0, 1, length=1.0, normalize=False) overlay = ax3.text2D(0.02, 0.95, "Waiting for data...", transform=ax3.transAxes) def update(_frame): nonlocal q # Remove old arrow try: q.remove() except Exception: pass vx, vy, vz = latest["ax"], latest["ay"], latest["az"] roll, pitch = roll_pitch_from_accel(vx, vy, vz) age_ms = (time.time() - latest["ts"]) * 1000.0 if latest["ts"] else float("inf") # Draw new arrow q = ax3.quiver(0, 0, 0, vx, vy, vz, length=1.0, normalize=False) overlay.set_text( f"packets: {latest['count']}\n" f"ax={vx:+.4f} ay={vy:+.4f} az={vz:+.4f} (g)\n" f"roll={roll:+.1f}° pitch={pitch:+.1f}° age={age_ms:.0f} ms\n" f"raw: {latest['raw']}" ) return q, overlay # KEY FIX: keep this object alive ani = FuncAnimation(fig, update, interval=PLOT_INTERVAL_MS, blit=False) plt.show() if __name__ == "__main__": main()