import sys import json import time import math import board import busio from adafruit_bno08x.i2c import BNO08X_I2C from adafruit_bno08x import BNO_REPORT_ROTATION_VECTOR POLL_INTERVAL = 0.1 # seconds between output lines REPORT_INTERVAL_US = 50000 # sensor report rate (20 Hz) STUCK_TIMEOUT = 10.0 # re-init if quaternion is bit-identical this long MAX_CONSECUTIVE_ERRORS = 10 def quaternion_to_euler(x, y, z, w): """Adafruit BNO08X quaternion (x, y, z, w) -> yaw, pitch, roll in degrees.""" sinr_cosp = 2.0 * (w * x + y * z) cosr_cosp = 1.0 - 2.0 * (x * x + y * y) roll = math.atan2(sinr_cosp, cosr_cosp) sinp = max(-1.0, min(1.0, 2.0 * (w * y - z * x))) pitch = math.asin(sinp) siny_cosp = 2.0 * (w * z + x * y) cosy_cosp = 1.0 - 2.0 * (y * y + z * z) yaw = math.atan2(siny_cosp, cosy_cosp) return math.degrees(yaw), math.degrees(pitch), math.degrees(roll) def valid_quaternion(quat): """Return a normalized (x, y, z, w) tuple, or None if the data is garbage.""" if not quat or len(quat) != 4: return None if any(v is None or math.isnan(v) or math.isinf(v) for v in quat): return None norm = math.sqrt(sum(v * v for v in quat)) # A healthy unit quaternion has norm ~1. Corrupt I2C reads usually don't. if norm < 0.9 or norm > 1.1: return None return tuple(v / norm for v in quat) def init_sensor(i2c): bno = BNO08X_I2C(i2c, address=0x4B) try: bno.enable_feature(BNO_REPORT_ROTATION_VECTOR, REPORT_INTERVAL_US) except TypeError: # Older library versions don't accept a report interval bno.enable_feature(BNO_REPORT_ROTATION_VECTOR) time.sleep(0.5) return bno def main(): try: i2c = busio.I2C(board.SCL, board.SDA) bno = init_sensor(i2c) last_quat = None last_change = time.time() errors = 0 while True: try: raw = bno.quaternion # (x, y, z, w) from rotation vector quat = valid_quaternion(raw) if quat is None: raise ValueError("invalid quaternion") errors = 0 # Stuck-sensor detection if raw != last_quat: last_quat = raw last_change = time.time() elif time.time() - last_change > STUCK_TIMEOUT: raise RuntimeError("sensor output frozen") yaw, pitch, roll = quaternion_to_euler(*quat) if yaw < 0: yaw += 360.0 print(json.dumps({ "yaw": round(yaw, 2), "pitch": round(pitch, 2), "roll": round(roll, 2), })) sys.stdout.flush() except (ValueError, TypeError, IndexError, KeyError, OSError, RuntimeError) as e: errors += 1 frozen = isinstance(e, RuntimeError) if frozen or errors >= MAX_CONSECUTIVE_ERRORS: print(json.dumps({"warning": f"re-initializing sensor ({e})"}), file=sys.stderr) sys.stderr.flush() try: bno = init_sensor(i2c) except Exception as init_err: print(json.dumps({"warning": f"re-init failed: {init_err}"}), file=sys.stderr) time.sleep(1.0) errors = 0 last_quat = None last_change = time.time() time.sleep(POLL_INTERVAL) except Exception as e: print(json.dumps({"error": str(e)}), file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()