Unitree Qmini
Build Qmini Robot with OAK-D-Pro W IMU
Author
Tony Wang
Date Published
1pip install depthai2echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="03e7", MODE="0666"' | sudo tee /etc/udev/rules.d/80-movidius.rules3sudo udevadm control --reload-rules && sudo udevadm trigger4lsusb | grep -i movidius
Should return like 'Bus 001 Device 005: ID 03e7:2485 Intel Movidius MyriadX'
Replace the /user/IMU/imu_receiver.py file with the following code:
1import argparse2import sys3import depthai as dai4import json5import math6import time78# Constants9PI = 3.14159265358979310DEG_TO_RAD = 0.01745329251994329511isrun = True1213# Global variables for persistent connection14device = None15queue = None16madgwick = None1718def parse_opt(known=False):19 parser = argparse.ArgumentParser()20 parser.add_argument('--frequency', type=int, default=100, help='IMU sampling frequency; default: 100Hz')21 parser.add_argument('--timeout', type=int, default=20, help='set the timeout; default: 20')2223 receive_params = parser.parse_known_args()[0] if known else parser.parse_args()24 return receive_params2526class MadgwickFilter:27 """Madgwick AHRS filter for quaternion estimation from accelerometer and gyroscope"""28 def __init__(self, beta=0.1, sample_freq=100):29 self.beta = beta30 self.sample_freq = sample_freq31 self.q = [1.0, 0.0, 0.0, 0.0] # quaternion [w, x, y, z]3233 def update(self, gx, gy, gz, ax, ay, az):34 """Update quaternion with gyroscope and accelerometer data"""35 q1, q2, q3, q4 = self.q3637 # Normalise accelerometer measurement38 norm = math.sqrt(ax * ax + ay * ay + az * az)39 if norm == 0:40 return self.q41 ax /= norm42 ay /= norm43 az /= norm4445 # Auxiliary variables to avoid repeated arithmetic46 _2q1 = 2 * q147 _2q2 = 2 * q248 _2q3 = 2 * q349 _2q4 = 2 * q450 _4q1 = 4 * q151 _4q2 = 4 * q252 _4q3 = 4 * q353 _8q2 = 8 * q254 _8q3 = 8 * q355 q1q1 = q1 * q156 q2q2 = q2 * q257 q3q3 = q3 * q358 q4q4 = q4 * q45960 # Gradient decent algorithm corrective step61 s1 = _4q1 * q3q3 + _2q3 * ax + _4q1 * q2q2 - _2q2 * ay62 s2 = _4q2 * q4q4 - _2q4 * ax + 4 * q1q1 * q2 - _2q1 * ay - _4q2 + _8q2 * q2q2 + _8q2 * q3q3 + _4q2 * az63 s3 = 4 * q1q1 * q3 + _2q1 * ax + _4q3 * q4q4 - _2q4 * ay - _4q3 + _8q3 * q2q2 + _8q3 * q3q3 + _4q3 * az64 s4 = 4 * q2q2 * q4 - _2q2 * ax + 4 * q3q3 * q4 - _2q3 * ay6566 # Normalise step magnitude67 norm = math.sqrt(s1 * s1 + s2 * s2 + s3 * s3 + s4 * s4)68 if norm != 0:69 s1 /= norm70 s2 /= norm71 s3 /= norm72 s4 /= norm7374 # Apply feedback step75 qDot1 = 0.5 * (-q2 * gx - q3 * gy - q4 * gz) - self.beta * s176 qDot2 = 0.5 * (q1 * gx + q3 * gz - q4 * gy) - self.beta * s277 qDot3 = 0.5 * (q1 * gy - q2 * gz + q4 * gx) - self.beta * s378 qDot4 = 0.5 * (q1 * gz + q2 * gy - q3 * gx) - self.beta * s47980 # Integrate rate of change of quaternion81 q1 += qDot1 * (1.0 / self.sample_freq)82 q2 += qDot2 * (1.0 / self.sample_freq)83 q3 += qDot3 * (1.0 / self.sample_freq)84 q4 += qDot4 * (1.0 / self.sample_freq)8586 # Normalise quaternion87 norm = math.sqrt(q1 * q1 + q2 * q2 + q3 * q3 + q4 * q4)88 if norm != 0:89 self.q = [q1/norm, q2/norm, q3/norm, q4/norm]9091 return self.q9293def quaternion_to_euler(qw, qx, qy, qz):94 """Convert quaternion to Euler angles (roll, pitch, yaw)"""95 # Roll (x-axis rotation)96 sinr_cosp = 2 * (qw * qx + qy * qz)97 cosr_cosp = 1 - 2 * (qx * qx + qy * qy)98 roll = math.atan2(sinr_cosp, cosr_cosp)99100 # Pitch (y-axis rotation)101 sinp = 2 * (qw * qy - qz * qx)102 if abs(sinp) >= 1:103 pitch = math.copysign(PI / 2, sinp) # use 90 degrees if out of range104 else:105 pitch = math.asin(sinp)106107 # Yaw (z-axis rotation)108 siny_cosp = 2 * (qw * qz + qx * qy)109 cosy_cosp = 1 - 2 * (qy * qy + qz * qz)110 yaw = math.atan2(siny_cosp, cosy_cosp)111112 return roll, pitch, yaw113114def initialize_imu_connection(frequency=100):115 """Initialize a persistent connection to the OAK-D-Pro W device"""116 global device, queue, madgwick117118 try:119 # Create pipeline120 pipeline = dai.Pipeline()121122 # Create IMU node - BMI270 only supports ACCELEROMETER_RAW and GYROSCOPE_RAW123 imu = pipeline.create(dai.node.IMU)124 imu.enableIMUSensor(dai.IMUSensor.ACCELEROMETER_RAW, frequency)125 imu.enableIMUSensor(dai.IMUSensor.GYROSCOPE_RAW, frequency)126 imu.setBatchReportThreshold(1)127 imu.setMaxBatchReports(10)128129 # Create output130 imuOut = pipeline.create(dai.node.XLinkOut)131 imuOut.setStreamName("imu")132 imu.out.link(imuOut.input)133134 # Initialize Madgwick filter for quaternion estimation135 madgwick = MadgwickFilter(beta=0.1, sample_freq=frequency)136137 # Connect to device and start pipeline138 device = dai.Device(pipeline)139 queue = device.getOutputQueue(name="imu", maxSize=50, blocking=False)140 print(f"Connected to OAK-D-Pro W device with BMI270 IMU sensor")141 return True142 except Exception as e:143 print(f"Error initializing OAK-D-Pro W IMU connection: {e}")144 return False145146def read_imu_data(frequency=100, timeout=1):147 """Read IMU data from OAK-D-Pro W device (BMI270 sensor)"""148 global device, queue, madgwick149150 # Initialize connection if not already done151 if device is None or queue is None or madgwick is None:152 if not initialize_imu_connection(frequency):153 return {154 "Accelerometer_X": 0,155 "Accelerometer_Y": 0,156 "Accelerometer_Z": 0,157 "RollSpeed": 0,158 "PitchSpeed": 0,159 "HeadingSpeed": 0,160 "Roll": 0,161 "Pitch": 0,162 "Heading": 0,163 "qw": 0,164 "qx": 0,165 "qy": 0,166 "qz": 0,167 }168169 try:170 # Get data from the queue171 inIMU = queue.get()172 if inIMU is None:173 return None174175 imuData = inIMU.packets[-1]176177 # Get raw accelerometer and gyroscope data178 ax = imuData.acceleroMeter.x179 ay = imuData.acceleroMeter.y180 az = imuData.acceleroMeter.z181182 gx = imuData.gyroscope.x183 gy = imuData.gyroscope.y184 gz = imuData.gyroscope.z185186 # Update Madgwick filter to get quaternion187 quaternion = madgwick.update(gx, gy, gz, ax, ay, az)188 qw, qx, qy, qz = quaternion189190 # Calculate Euler angles from quaternion191 roll, pitch, yaw = quaternion_to_euler(qw, qx, qy, qz)192193 # Apply the same coordinate transformations as the original code194 result = {195 "Accelerometer_X": ax,196 "Accelerometer_Y": ay,197 "Accelerometer_Z": az,198 "RollSpeed": gy, # Swapped as in original199 "PitchSpeed": gx * -1, # Inverted as in original200 "HeadingSpeed": gz,201 "Roll": pitch, # Swapped as in original202 "Pitch": roll * -1, # Inverted as in original203 "Heading": yaw,204 "qw": qw,205 "qx": qx,206 "qy": qy,207 "qz": qz,208 }209 return result210211 except Exception as e:212 print(f"Error reading OAK-D-Pro W IMU data: {e}")213 # Try to reinitialize the connection214 if initialize_imu_connection(frequency):215 return read_imu_data(frequency, timeout) # Try again216 return {217 "Accelerometer_X": 0,218 "Accelerometer_Y": 0,219 "Accelerometer_Z": 0,220 "RollSpeed": 0,221 "PitchSpeed": 0,222 "HeadingSpeed": 0,223 "Roll": 0,224 "Pitch": 0,225 "Heading": 0,226 "qw": 0,227 "qx": 0,228 "qy": 0,229 "qz": 0,230 }231232def cleanup_imu_connection():233 """Clean up the IMU connection when done"""234 global device235 if device is not None:236 device.close()237 device = None238239if __name__ == "__main__":240 args = parse_opt()241 try:242 initialize_imu_connection(frequency=args.frequency)243 while True:244 imu_data = read_imu_data(frequency=args.frequency, timeout=args.timeout)245 print(f"IMU Data: {imu_data}")246 time.sleep(0.01) # Small delay to prevent overwhelming output247 except KeyboardInterrupt:248 print("Exiting...")249 finally:250 cleanup_imu_connection()
IMU Data Dump
Create a new file imu_oak_dump.py in the bin folder:
1import argparse2import sys3import time4import json5import depthai as dai6import math7import numpy as np89# Constants10PI = 3.14159265358979311DEG_TO_RAD = 0.0174532925199432951213def parse_opt(known=False):14 parser = argparse.ArgumentParser(description='OAK-D-Pro W IMU Data Dumper')15 parser.add_argument('--output', type=str, default='oak_imu_data.txt', help='output file to dump IMU data')16 parser.add_argument('--duration', type=int, default=60, help='duration to collect data in seconds; default: 60')17 parser.add_argument('--frequency', type=int, default=100, help='IMU sampling frequency; default: 100Hz')18 parser.add_argument('--format', type=str, choices=['json', 'csv'], default='json', help='output format: json or csv')19 parser.add_argument('--verbose', action='store_true', help='print data to console as well')2021 receive_params = parser.parse_known_args()[0] if known else parser.parse_args()22 return receive_params2324class MadgwickFilter:25 """Madgwick AHRS filter for quaternion estimation from accelerometer and gyroscope"""26 def __init__(self, beta=0.1, sample_freq=100):27 self.beta = beta28 self.sample_freq = sample_freq29 self.q = [1.0, 0.0, 0.0, 0.0] # quaternion [w, x, y, z]3031 def update(self, gx, gy, gz, ax, ay, az):32 """Update quaternion with gyroscope and accelerometer data"""33 q1, q2, q3, q4 = self.q3435 # Normalise accelerometer measurement36 norm = math.sqrt(ax * ax + ay * ay + az * az)37 if norm == 0:38 return39 ax /= norm40 ay /= norm41 az /= norm4243 # Auxiliary variables to avoid repeated arithmetic44 _2q1 = 2 * q145 _2q2 = 2 * q246 _2q3 = 2 * q347 _2q4 = 2 * q448 _4q1 = 4 * q149 _4q2 = 4 * q250 _4q3 = 4 * q351 _8q2 = 8 * q252 _8q3 = 8 * q353 q1q1 = q1 * q154 q2q2 = q2 * q255 q3q3 = q3 * q356 q4q4 = q4 * q45758 # Gradient decent algorithm corrective step59 s1 = _4q1 * q3q3 + _2q3 * ax + _4q1 * q2q2 - _2q2 * ay60 s2 = _4q2 * q4q4 - _2q4 * ax + 4 * q1q1 * q2 - _2q1 * ay - _4q2 + _8q2 * q2q2 + _8q2 * q3q3 + _4q2 * az61 s3 = 4 * q1q1 * q3 + _2q1 * ax + _4q3 * q4q4 - _2q4 * ay - _4q3 + _8q3 * q2q2 + _8q3 * q3q3 + _4q3 * az62 s4 = 4 * q2q2 * q4 - _2q2 * ax + 4 * q3q3 * q4 - _2q3 * ay6364 # Normalise step magnitude65 norm = math.sqrt(s1 * s1 + s2 * s2 + s3 * s3 + s4 * s4)66 if norm != 0:67 s1 /= norm68 s2 /= norm69 s3 /= norm70 s4 /= norm7172 # Apply feedback step73 qDot1 = 0.5 * (-q2 * gx - q3 * gy - q4 * gz) - self.beta * s174 qDot2 = 0.5 * (q1 * gx + q3 * gz - q4 * gy) - self.beta * s275 qDot3 = 0.5 * (q1 * gy - q2 * gz + q4 * gx) - self.beta * s376 qDot4 = 0.5 * (q1 * gz + q2 * gy - q3 * gx) - self.beta * s47778 # Integrate rate of change of quaternion79 q1 += qDot1 * (1.0 / self.sample_freq)80 q2 += qDot2 * (1.0 / self.sample_freq)81 q3 += qDot3 * (1.0 / self.sample_freq)82 q4 += qDot4 * (1.0 / self.sample_freq)8384 # Normalise quaternion85 norm = math.sqrt(q1 * q1 + q2 * q2 + q3 * q3 + q4 * q4)86 if norm != 0:87 self.q = [q1/norm, q2/norm, q3/norm, q4/norm]8889 return self.q9091def quaternion_to_euler(qw, qx, qy, qz):92 """Convert quaternion to Euler angles (roll, pitch, yaw)"""93 # Roll (x-axis rotation)94 sinr_cosp = 2 * (qw * qx + qy * qz)95 cosr_cosp = 1 - 2 * (qx * qx + qy * qy)96 roll = math.atan2(sinr_cosp, cosr_cosp)9798 # Pitch (y-axis rotation)99 sinp = 2 * (qw * qy - qz * qx)100 if abs(sinp) >= 1:101 pitch = math.copysign(PI / 2, sinp) # use 90 degrees if out of range102 else:103 pitch = math.asin(sinp)104105 # Yaw (z-axis rotation)106 siny_cosp = 2 * (qw * qz + qx * qy)107 cosy_cosp = 1 - 2 * (qy * qy + qz * qz)108 yaw = math.atan2(siny_cosp, cosy_cosp)109110 return roll, pitch, yaw111112def read_oak_imu_data(frequency=100):113 """Read IMU data from OAK-D-Pro W device (BMI270 sensor)"""114 try:115 # Create pipeline116 pipeline = dai.Pipeline()117118 # Create IMU node - BMI270 only supports ACCELEROMETER_RAW and GYROSCOPE_RAW119 imu = pipeline.create(dai.node.IMU)120 imu.enableIMUSensor(dai.IMUSensor.ACCELEROMETER_RAW, frequency)121 imu.enableIMUSensor(dai.IMUSensor.GYROSCOPE_RAW, frequency)122 imu.setBatchReportThreshold(1)123 imu.setMaxBatchReports(10)124125 # Create output126 imuOut = pipeline.create(dai.node.XLinkOut)127 imuOut.setStreamName("imu")128 imu.out.link(imuOut.input)129130 # Initialize Madgwick filter for quaternion estimation131 madgwick = MadgwickFilter(beta=0.1, sample_freq=frequency)132133 # Connect to device and start pipeline134 with dai.Device(pipeline) as device:135 q = device.getOutputQueue(name="imu", maxSize=50, blocking=False)136137 print(f"Connected to OAK-D-Pro W device with BMI270 IMU sensor")138139 while True:140 inIMU = q.get()141 if inIMU is None:142 continue143144 imuData = inIMU.packets[-1]145146 # Get raw accelerometer and gyroscope data147 ax = imuData.acceleroMeter.x148 ay = imuData.acceleroMeter.y149 az = imuData.acceleroMeter.z150151 gx = imuData.gyroscope.x152 gy = imuData.gyroscope.y153 gz = imuData.gyroscope.z154155 # Update Madgwick filter to get quaternion156 quaternion = madgwick.update(gx, gy, gz, ax, ay, az)157 qw, qx, qy, qz = quaternion158159 # Calculate Euler angles from quaternion160 roll, pitch, yaw = quaternion_to_euler(qw, qx, qy, qz)161162 result = {163 "timestamp": time.time(),164 "Accelerometer_X": ax,165 "Accelerometer_Y": ay,166 "Accelerometer_Z": az,167 "RollSpeed": gx,168 "PitchSpeed": gy,169 "HeadingSpeed": gz,170 "Roll": roll,171 "Pitch": pitch,172 "Heading": yaw,173 "qw": qw,174 "qx": qx,175 "qy": qy,176 "qz": qz,177 }178179 yield result180181 except Exception as e:182 print(f"Error reading OAK-D-Pro W IMU data: {e}")183 return None184185def dump_imu_data():186 """Main function to dump IMU data"""187 args = parse_opt()188189 print(f"Starting OAK-D-Pro W IMU data collection...")190 print(f"Duration: {args.duration} seconds")191 print(f"Frequency: {args.frequency} Hz")192 print(f"Output file: {args.output}")193 print(f"Format: {args.format}")194 print("Note: Using BMI270 sensor (ACCELEROMETER_RAW + GYROSCOPE_RAW only)")195 print("Quaternions calculated using Madgwick AHRS filter")196 print("Press Ctrl+C to stop early\n")197198 start_time = time.time()199 data_count = 0200201 try:202 with open(args.output, 'w') as f:203 # Write CSV header if needed204 if args.format == 'csv':205 header = "timestamp,Accelerometer_X,Accelerometer_Y,Accelerometer_Z,RollSpeed,PitchSpeed,HeadingSpeed,Roll,Pitch,Heading,qw,qx,qy,qz\n"206 f.write(header)207208 for imu_data in read_oak_imu_data(args.frequency):209 if imu_data is None:210 break211212 # Check if duration exceeded213 if time.time() - start_time > args.duration:214 break215216 # Write data to file217 if args.format == 'json':218 f.write(json.dumps(imu_data) + '\n')219 elif args.format == 'csv':220 csv_line = f"{imu_data['timestamp']},{imu_data['Accelerometer_X']},{imu_data['Accelerometer_Y']},{imu_data['Accelerometer_Z']},{imu_data['RollSpeed']},{imu_data['PitchSpeed']},{imu_data['HeadingSpeed']},{imu_data['Roll']},{imu_data['Pitch']},{imu_data['Heading']},{imu_data['qw']},{imu_data['qx']},{imu_data['qy']},{imu_data['qz']}\n"221 f.write(csv_line)222223 f.flush() # Ensure data is written immediately224225 # Print to console if verbose226 if args.verbose:227 print(f"Sample {data_count + 1}: {json.dumps(imu_data, indent=2)}")228229 data_count += 1230231 # Small delay to prevent overwhelming the system232 time.sleep(0.001)233234 except KeyboardInterrupt:235 print("\nData collection stopped by user.")236 except Exception as e:237 print(f"Error during data collection: {e}")238239 elapsed_time = time.time() - start_time240 print(f"\nData collection completed!")241 print(f"Total samples collected: {data_count}")242 print(f"Elapsed time: {elapsed_time:.2f} seconds")243 print(f"Average sampling rate: {data_count/elapsed_time:.2f} Hz")244 print(f"Data saved to: {args.output}")245246if __name__ == "__main__":247 dump_imu_data()
Run the imu_oak_dump.py
1$ cd ~/RoboTamerSdk4Qmini/bin2# Basic usage - collect for 60 seconds at 100Hz, save as JSON3$ python3 imu_oak_dump.py4# Quick 10-second test with console output and save data as a txt file5$ python3 imu_oak_dump.py --duration 10 --verbose6# Collect for 30 seconds at 200Hz, save as CSV with verbose output7$ python3 imu_oak_dump.py --duration 30 --frequency 200 --format csv --verbose --output my_imu_data.csv

Unitree Qmini