How do you do the Jetson Orin Nano <--> Cube Orange+ thing?

I’m sorry to bug you guys with something so trivial but I can not figure it out. This has got to be baby’s first steps when it comes to what people do when they first get a Jetson.

This kind of works.
The Cube can see the Here4. The Jetson can see the cube. I can run “candump can0” on the Jetson and get a stream of data. Here’s some:

can0 184E270A [8] A1 5E 00 02 64 19 10 99
can0 184E270A [8] 00 00 00 00 00 00 00 79
can0 184E270A [8] A1 5E 00 02 64 19 10 9A
can0 184E270A [8] 00 00 00 00 00 00 00 7A
can0 184E270A [8] A1 5E 00 02 64 19 10 9B
can0 184E270A [8] 00 00 00 00 00 00 00 7B
can0 184E270A [8] A1 5E 00 02 64 19 10 9C
can0 184E270A [8] 00 00 00 00 00 00 00 7C
can0 184E270A [8] A1 5E 00 02 64 19 10 9D
can0 184E270A [8] 00 00 00 00 00 00 00 7D
can0 184E270A [8] A1 5E 00 02 64 19 10 9E
can0 184E270A [8] 00 00 00 00 00 00 00 7E
can0 1804390A [4] 00 63 20 D0
can0 184E270A [8] A1 5E 00 02 64 19 10 9F
can0 184E270A [8] 00 00 00 00 00 00 00 7F
can0 18044C0A [2] 00 D5
can0 184E200A [2] 00 D5
can0 184E270A [8] A1 5E 00 02 64 19 10 80
can0 184E270A [8] 00 00 00 00 00 00 00 60
can0 184E270A [8] A1 5E 00 02 64 19 10 81
can0 184E270A [8] 00 00 00 00 00 00 00 61
can0 184E270A [8] A1 5E 00 02 64 19 10 82
can0 184E270A [8] 00 00 00 00 00 00 00 62
can0 1804390A [4] 00 00 00 D1
can0 184E270A [8] A1 5E 00 02 64 19 10 83
can0 184E270A [8] 00 00 00 00 00 00 00 63
can0 184E270A [8] A1 5E 00 02 64 19 10 84
can0 184E270A [8] 00 00 00 00 00 00 00 64
can0 184E270A [8] A1 5E 00 02 64 19 10 85
can0 184E270A [8] 00 00 00 00 00 00 00 65
can0 184E270A [8] A1 5E 00 02 64 19 10 86
can0 184E270A [8] 00 00 00 00 00 00 00 66
can0 1804390A [4] 00 63 20 D2
can0 184E270A [8] A1 5E 00 02 64 19 10 87
can0 184E270A [8] 00 00 00 00 00 00 00 67
can0 184E270A [8] A1 5E 00 02 64 19 10 88
can0 184E270A [8] 00 00 00 00 00 00 00 68
can0 1F01550A [8] 80 01 00 00 00 00 00 DF
can0 104E2D0A [2] 00 DB
can0 184E270A [8] A1 5E 00 02 64 19 10 89
can0 184E270A [8] 00 00 00 00 00 00 00 69
can0 18044C0A [2] 00 D6
can0 184E200A [2] 00 D6
can0 184E270A [8] A1 5E 00 02 64 19 10 8A
can0 184E270A [8] 00 00 00 00 00 00 00 6A
can0 1804390A [4] 00 00 00 D3
can0 184E270A [8] A1 5E 00 02 64 19 10 8B
can0 184E270A [8] 00 00 00 00 00 00 00 6B
can0 184E270A [8] A1 5E 00 02 64 19 10 8C
can0 184E270A [8] 00 00 00 00 00 00 00 6C
can0 184E270A [8] A1 5E 00 02 64 19 10 8D
can0 184E270A [8] 00 00 00 00 00 00 00 6D
can0 184E270A [8] A1 5E 00 02 64 19 10 8E
can0 184E270A [8] 00 00 00 00 00 00 00 6E
can0 184E270A [8] A1 5E 00 02 64 19 10 8F
can0 184E270A [8] 00 00 00 00 00 00 00 6F
can0 184E270A [8] A1 5E 00 02 64 19 10 90
can0 184E270A [8] 00 00 00 00 00 00 00 70
can0 184E270A [8] A1 5E 00 02 64 19 10 91
can0 184E270A [8] 00 00 00 00 00 00 00 71
can0 1804390A [4] 00 63 20 D4
can0 184E270A [8] A1 5E 00 02 64 19 10 92
can0 184E270A [8] 00 00 00 00 00 00 00 72
can0 184E270A [8] A1 5E 00 02 64 19 10 93
can0 184E270A [8] 00 00 00 00 00 00 00 73
can0 18044C0A [2] 00 D7
can0 184E200A [2] 00 D7
can0 184E270A [8] A1 5E 00 02 64 19 10 94
can0 184E270A [8] 00 00 00 00 00 00 00 74
can0 184E270A [8] A1 5E 00 02 64 19 10 95
can0 184E270A [8] 00 00 00 00 00 00 00 75
can0 1804390A [4] 00 00 00 D5
can0 184E270A [8] A1 5E 00 02 64 19 10 96
can0 184E270A [8] 00 00 00 00 00 00 00 76

The goal here is to generate orthomosaics using WebODM and photos taken from a (not shown) camera rig attached to the Jetson. The CAN connection is supposed to stream GNSS data from the GPS to the Jetson via the Cube.

So far I have:
Verified that setting the parameter CAN_D1_UC_OPTION to any value other than 32 disables the CAN data stream.
Set ARMING_SKIPCHK to -1 in case the Cube needs to be armed to stream GNSS data.
Set EK3_SRC1_YAW to 0 in case not having a good magnetometer signal could cause it to not stream data.

The GPS is sitting in my window right now. It can see 18 satellites and has a 3D dgps fix. If I were only interested in the “data” tab in mission planner, I would think that the Cube is satisfied with its position lock.

Also, I only know how to “Vibe” code. I’m sorry. I’m trying to learn but the python script I’m using is AI generated. It’s possible (or even likely) that this is where the problem actually lies. But the LLM that wrote the program doesn’t see a problem and I do not know where to turn from there but here. I know, “If you couldn’t be bothered to write it, I can’t be bothered to read it.” And that’s perfectly valid criticism. But in case this is where my problem is, here’s the script I’m using:

import socket
import struct
import sys
import math

# DroneCAN v0 Message Type IDs
GNSS_FIX_MESSAGE_TYPE = 20007
AHRS_SOL_MESSAGE_TYPE = 1000

try:
sock = socket.socket(socket.AF_CAN, socket.SOCK_RAW, socket.CAN_RAW)
sock.bind(("can0",))
print("=========================================================")
print(" 🛰️ MAPPING TELEMETRY LOGGER (WEBODM READY) ")
print("=========================================================")
print("Assembling spatial maps from active CAN buses...\n")
except Exception as e:
print(f"Failed to bind raw SocketCAN socket: {e}")
sys.exit(1)

# Global vehicle state dictionaries
gps_state = {"lat": 0.0, "lon": 0.0, "alt": 0.0, "sats": 0, "fix": "NO FIX"}
imu_state = {"yaw": 0.0, "pitch": 0.0, "roll": 0.0}

def quaternion_to_euler(x, y, z, w):
"""
Converts standard DroneCAN xyzw normalized quaternions into
Aerospace Euler angles (Roll, Pitch, Yaw) in degrees.
"""
# Roll calculation (x-axis rotation)
sinr_cosp = 2 * (w * x + y * z)
cosr_cosp = 1 - 2 * (x * x + y * y)
roll = math.atan2(sinr_cosp, cosr_cosp)

# Pitch calculation (y-axis rotation)
sinp = 2 * (w * y - z * x)
if abs(sinp) >= 1:
pitch = math.copysign(math.pi / 2, sinp) # Use 90 degrees if out of bounds
else:
pitch = math.asin(sinp)

# Yaw calculation (z-axis rotation)
siny_cosp = 2 * (w * z + x * y)
cosy_cosp = 1 - 2 * (y * y + z * z)
yaw = math.atan2(siny_cosp, cosy_cosp)

# Convert radians to degrees
return math.degrees(roll), math.degrees(pitch), math.degrees(yaw)

try:
while True:
can_bytes = sock.recv(16)
if len(can_bytes) < 16:
continue

can_id, dlc, raw_payload = struct.unpack("<IB3x8s", can_bytes)
clean_id = can_id & socket.CAN_EFF_MASK

# Deconstruct DroneCAN Bitmask to extract fields
message_type_id = (clean_id >> 8) & 0xFFFF

# --------------------------------------------------------------------
# HOOK 1: PARSE GNSS DATA (Message 20007)
# --------------------------------------------------------------------
if message_type_id == GNSS_FIX_MESSAGE_TYPE:
# For simplicity, extract non-fragmented positions directly
# or map assembled arrays from your multi-frame buffer logic
if dlc >= 7:
# (Assuming coordinate buffer extraction loop updates state)
pass

# --------------------------------------------------------------------
# HOOK 2: PARSE IMU ATTITUDE DATA (Message 1000)
# --------------------------------------------------------------------
elif message_type_id == AHRS_SOL_MESSAGE_TYPE:
if len(raw_payload) >= 8:
try:
import numpy as np

# Unpack the 4 raw uint16 quaternion channels
q_raw = struct.unpack("<HHHH", raw_payload[:8])

# Map each unique index channel directly into its float16 view
qx = np.array([q_raw[0]], dtype=np.uint16).view(np.float16)[0]
qy = np.array([q_raw[1]], dtype=np.uint16).view(np.float16)[0]
qz = np.array([q_raw[2]], dtype=np.uint16).view(np.float16)[0]
qw = np.array([q_raw[3]], dtype=np.uint16).view(np.float16)[0]

# Run the mathematical conversion to Euler degrees
roll, pitch, yaw = quaternion_to_euler(qx, qy, qz, qw)

# Update active vehicle states dynamically
imu_state["roll"] = roll
imu_state["pitch"] = pitch
imu_state["yaw"] = yaw

except Exception as e:
pass

# --------------------------------------------------------------------
# APPLICATION LEVEL: STREAM UNIFIED METRICS TO SCREEN
# --------------------------------------------------------------------
# In practice, you trigger this print line inside your actual camera shutter callback routine.
# This mirrors the exact formatting blueprint WebODM expects inside geo.txt.
sys.stdout.write(
f"\r📝 [GEO_READY] Lon: {gps_state['lon']:.7f}° | Lat: {gps_state['lat']:.7f}° | "
f"Alt: {gps_state['alt']:.2f}m | Yaw: {imu_state['yaw']:.1f}° | "
f"Pitch: {imu_state['pitch']:.1f}° | Roll: {imu_state['roll']:.1f}°"
)
sys.stdout.flush()

except KeyboardInterrupt:
print("\nExiting Telemetry Logging Module.")
sock.close()

But all it does is sit there saying:
[GEO_READY] Lon: 0.0000000° | Lat: 0.0000000° | Alt: 0.00m | Yaw: 0.0° | Pitch: 0.0° | Roll: 0.0°

TL;DR: CAN is streaming something, but it’s not what I want. How do I make it send GNSS data?

EDIT: Disregard the rest of this post. I’m keeping it for reference for anyone else venturing down this rabbit hole.

It has to be serial.

The connection only started working a little when I moved the CAN connection from the Cube Orange+'s CAN2 slot to the CAN1 slot on a multiplexer. But the GPS is a Here4. Nobody buys a Here4 for GPS. They buy it for RTK. And you can’t get RTK data off the GPS. It’s fusion - done inside the Flight Controller. It requires information from the Here4 as well as from the ground station via a telemetry link. The GPS does not and can not broadcast RTK. Only the FC can, and it simply does not want to broadcast on CAN.

But from minimOSD to HDzero and everything in between, everyone knows that every serial port can display everything from battery voltage to lat and long. And it’s got orientation information. All the cool kids who run with artificial horizon know it.

I have to redesign my layout to move the Jetson to a serial link. It’s the only way. Please don’t waste your time like I did trying to make it work via CAN. It does not work.

Holy Moly that is a heck of a first post!

First of all, thank you. I’m not able to get CAN working in spite of your help. But unfortunately my configuration isn’t compatible with serial. The serial interface is already in use to connect the Jetson to a Raspberry Pi. (The Jetson is taking downward facing photos, while the Pi handles a forward facing camera responsible for obstacle avoidance).

One other point the LLM mentioned is that the Cube Orange+ might not be broadcasting GNSS data from CAN1 onto CAN2, and that I should change parameters to make it do that. But then it told me to change parameters that don’t actually exist. Does that sound to you like a useless hallucination or is it maybe onto something?

Thank you again for your help. I’ve reverted ARMING_SKIPCHK back to a safe configuration and re-enabled the magnetometer on EK3_SRC1.

EDIT: I moved the GPS to a CAN multiplexer and a script using your insight. It works now.
fake edit: mostly. I’ve got Lat + Long but no Yaw/Pitch/Roll or Altitude. Could I ask for your assistance one more time?

EDIT2: I forgot to post this. It outputs GPS but not orientation.

import time
import math
import dronecan
import can

# Prevent newer python-can versions from crashing on standard Linux socketcan
can.BusABC.flush_tx_buffer = lambda self: None

def quaternion_to_euler(w, x, y, z):
"""Converts a standard normalized quaternion to Aerospace Euler degrees."""
sinr_cosp = 2 * (w * x + y * z)
cosr_cosp = 1 - 2 * (x * x + y * y)
roll = math.atan2(sinr_cosp, cosr_cosp)

sinp = 2 * (w * y - z * x)
pitch = math.copysign(math.pi / 2, sinp) if abs(sinp) >= 1 else math.asin(sinp)

siny_cosp = 2 * (w * z + x * y)
cosy_cosp = 1 - 2 * (y * y + z * z)
yaw = math.atan2(siny_cosp, cosy_cosp)

return math.degrees(roll), math.degrees(pitch), math.degrees(yaw)

# 1. Initialize the DroneCAN node on the Jetson Orin Nano
# Node ID 100 avoids conflict with the Cube Orange+ (Node 10)
node = dronecan.make_node('can0', node_id=100, bitrate=1000000)

# Global runtime data structures
gps_state = {"lat": 0.0, "lon": 0.0, "alt": 0.0}
imu_state = {"roll": 0.0, "pitch": 0.0, "yaw": 0.0}

def on_gnss_fix(e):
""" Handles raw hardware GPS data from DroneCAN """
msg = e.message

# Try dynamic variants of DroneCAN v0/v1 coordinate names
lat = getattr(msg, 'latitude_deg_1e8', None) or getattr(msg, 'latitude_deg_1e7', None) or getattr(msg, 'latitude', None)
lon = getattr(msg, 'longitude_deg_1e8', None) or getattr(msg, 'longitude_deg_1e7', None) or getattr(msg, 'longitude', None)

# FIX: Correct property maps for ArduPilot's compiled altitude names
alt = getattr(msg, 'height_msl', None) or getattr(msg, 'height_amsl_mm', None) or getattr(msg, 'height', None)

if lat is not None and lon is not None:
# Resolve positional precision scaling logic
gps_state["lat"] = lat / 1e8 if abs(lat) > 180 else lat
gps_state["lon"] = lon / 1e8 if abs(lon) > 180 else lon

if alt is not None:
# Downscale millimeters to meters if numerical bounds require it
gps_state["alt"] = alt / 1000.0 if abs(alt) > 50000 else alt

def on_ahrs_solution(e):
""" Parses attitude orientation matrices passing through the EKF payload """
msg = e.message

if hasattr(msg, 'orientation') and msg.orientation is not None:
# FIX: Secure explicit index elements from the DroneCAN array struct
q = msg.orientation
if len(q) >= 4:
w = q[0]
x = q[1]
y = q[2]
z = q[3]

# Guard against uninitialized math parameters causing domain failures
if not (w == 0.0 and x == 0.0 and y == 0.0 and z == 0.0):
r, p, y_deg = quaternion_to_euler(w, x, y, z)
imu_state["roll"] = r
imu_state["pitch"] = p
# Handle standardizing yaw boundaries (0° to 360°)
imu_state["yaw"] = y_deg if y_deg >= 0 else (y_deg + 360.0)

# 2. Register wide handlers for ALL variations of GPS and IMU Attitude structures
node.add_handler(dronecan.uavcan.equipment.gnss.Fix2, on_gnss_fix)
node.add_handler(dronecan.uavcan.equipment.gnss.Fix, on_gnss_fix)
node.add_handler(dronecan.uavcan.equipment.ahrs.Solution, on_ahrs_solution)

print("=========================================================")
print(" 🛰️ SYNCHRONIZED GPS + IMU MONITOR ")
print("=========================================================")
print("Listening on can0... Ready to stream data for geo.txt\n")

last_status_broadcast = 0
last_terminal_print = 0
start_time = time.time()

try:
while True:
# Spin non-blocking: allows DroneCAN to continuously ingest backlogged buffers
node.spin(0.005)

current_time = time.time()

# Broadcast NodeStatus once per second to maintain bus authorization
if current_time - last_status_broadcast >= 1.0:
node.broadcast(dronecan.uavcan.protocol.NodeStatus(
uptime_sec=int(current_time - start_time),
health=dronecan.uavcan.protocol.NodeStatus().HEALTH_OK,
mode=dronecan.uavcan.protocol.NodeStatus().MODE_OPERATIONAL,
sub_mode=0,
vendor_specific_status_code=0
))
last_status_broadcast = current_time

# Smoothly stream data updates to screen every 200ms
if current_time - last_terminal_print >= 0.2:
last_terminal_print = current_time
print(
f"\r🛰️ Lat: {gps_state['lat']:.8f}° | "
f"Lon: {gps_state['lon']:.8f}° | "
f"Alt: {gps_state['alt']:.2f}m | "
f"Yaw (Heading): {imu_state['yaw']:.1f}° | "
f"Pitch: {imu_state['pitch']:.1f}° | "
f"Roll: {imu_state['roll']:.1f}°",
end="",
flush=True
)

except KeyboardInterrupt:
print("\nStopping data stream node.")

It’s AI slop. I am going to remove it.

Fair play. Most of my post was too.