Skip to content

Instantly share code, notes, and snippets.

@j0uni
Created September 12, 2025 15:33
Show Gist options
  • Select an option

  • Save j0uni/0efd26dbf9fac1dc92bb57fdbb993434 to your computer and use it in GitHub Desktop.

Select an option

Save j0uni/0efd26dbf9fac1dc92bb57fdbb993434 to your computer and use it in GitHub Desktop.
Meshtastic local + remote admin location lat/lon set with Python
import meshtastic
from meshtastic.serial_interface import SerialInterface
import time
# Connect to a local Meshtastic device (e.g., via serial/USB)
iface = SerialInterface()
# Replace with the target node ID you want to configure
target_node = "!" + "1309E2A0"
# Example coordinates (latitude, longitude, altitude)
latitude = 61.49
longitude = 23.85
altitude = 100 # meters
# Configuration options
SKIP_LOCAL_DEVICE = True # Set to True to skip local device, focus only on remote
REMOTE_ONLY_MODE = True # Set to True for remote admin only
try:
if REMOTE_ONLY_MODE:
print("=== REMOTE ADMIN MODE - SETTING REMOTE DEVICE ONLY ===")
else:
print("=== FIXING DEVICE POSITION ===")
print(f"Target coordinates: {latitude}, {longitude}, {altitude}m")
print(f"Remote device target: {target_node}")
if not SKIP_LOCAL_DEVICE:
# Step 0: EMERGENCY - Clear any corrupted position first
print("\n0. Clearing any corrupted position data...")
try:
iface.localNode.removeFixedPosition()
print("✅ Cleared existing fixed position")
time.sleep(2)
except Exception as e:
print(f"⚠️ Could not clear position: {e}")
# Step 1: Get current local position
print("\n1. Getting current local position...")
local_node_info = iface.getMyNodeInfo()
if local_node_info and 'position' in local_node_info and local_node_info['position']:
initial_lat = local_node_info['position']['latitude']
initial_lon = local_node_info['position']['longitude']
initial_alt = local_node_info['position']['altitude']
print(f"Initial local position: {initial_lat}, {initial_lon}, {initial_alt}")
# Check for invalid coordinates
if initial_lat > 90 or initial_lat < -90 or initial_lon > 180 or initial_lon < -180:
print("❌ INVALID COORDINATES DETECTED - Will fix immediately!")
elif initial_lat == 0.0 and initial_lon == 0.0:
print("❌ LOCAL DEVICE IS AT 0,0 - Will fix this!")
else:
print("✅ Local device has valid position (will update to target coordinates)")
else:
print("❌ No initial position found on local device")
# Step 2: Set LOCAL device fixed position using the correct API
print(f"\n2. Setting LOCAL device (serial connected) fixed position...")
try:
# WORKAROUND: The library has a bug - it divides by 1e-7 instead of multiplying by 1e7
# So we need to pass pre-converted integer values to avoid the bug
# Convert to the correct integer format ourselves
lat_i = int(latitude * 1e7) # Correct conversion
lon_i = int(longitude * 1e7) # Correct conversion
alt_i = int(altitude)
print(f" Working around library bug - using integer coordinates:")
print(f" lat_i: {lat_i}, lon_i: {lon_i}, alt: {alt_i}")
# Pass integers to avoid the buggy float conversion in the library
iface.localNode.setFixedPosition(lat=lat_i, lon=lon_i, alt=alt_i)
print(f"✅ LOCAL device fixed position set to {latitude}, {longitude}, {altitude}")
# Write the configuration to make it persistent
try:
iface.localNode.writeConfig('position')
print("✅ Configuration written to device")
except Exception as config_error:
print(f"⚠️ Configuration write error: {config_error}")
print("✅ Fixed position was set but may not be persistent")
# Force send position_app message to broadcast the new position
print("\n 📡 Force broadcasting position via position_app...")
try:
position_data = f"POSITION_UPDATE:LAT:{latitude:.6f},LON:{longitude:.6f},ALT:{altitude}"
position_bytes = position_data.encode('utf-8')
iface.sendData(
data=position_bytes,
destinationId='^all', # Broadcast to all nodes
portNum=meshtastic.portnums_pb2.POSITION_APP,
wantAck=False
)
print(f" ✅ Position broadcast sent: {position_data}")
# Also send a direct position packet
iface.sendPosition(
latitude=latitude,
longitude=longitude,
altitude=altitude,
destinationId='^all' # Broadcast to all
)
print(f" ✅ Direct position packet broadcast")
except Exception as broadcast_error:
print(f" ⚠️ Position broadcast error: {broadcast_error}")
except Exception as e:
print(f"❌ Error setting local device fixed position: {e}")
else:
print("\n🔄 SKIPPING LOCAL DEVICE (SKIP_LOCAL_DEVICE = True)")
print(" Focusing only on remote admin device...")
# Step 3: FOCUS ON REMOTE DEVICE FIXED POSITION
print(f"\n3. Setting REMOTE device {target_node} fixed position...")
# Use lowercase node ID as found in mesh
remote_target = target_node.lower()
print(f" Using node ID: {remote_target}")
try:
# CORRECT METHOD: Use getNode().setFixedPosition() as per library design
print(" 📡 Using correct library method: getNode().setFixedPosition()...")
# Get the remote node object (this handles admin channel setup)
remote_node = iface.getNode(remote_target, requestChannels=False, timeout=10)
if remote_node:
print(f" ✅ Successfully connected to remote node {remote_target}")
# Use the same workaround for the library bug (pass integers)
lat_i = int(latitude * 1e7) # Correct conversion to avoid library bug
lon_i = int(longitude * 1e7) # Correct conversion to avoid library bug
alt_i = int(altitude)
print(f" 📍 Setting remote fixed position with coordinates:")
print(f" lat_i: {lat_i}, lon_i: {lon_i}, alt: {alt_i}")
# Call setFixedPosition on the remote node (library handles admin messages)
remote_node.setFixedPosition(lat=lat_i, lon=lon_i, alt=alt_i)
print(f" ✅ REMOTE FIXED POSITION SET!")
print(f" Library automatically sent admin message to {remote_target}")
else:
print(f" ❌ Could not connect to remote node {remote_target}")
print(" Device may be offline or not accepting admin connections")
except Exception as e:
print(f" ❌ Error setting remote fixed position: {e}")
print(" This could mean:")
print(" - Remote device is not reachable")
print(" - Admin channel not configured")
print(" - Device doesn't accept remote admin commands")
# Step 4: Wait for remote device to process commands
print(f"\n4. Waiting for remote device to process admin commands...")
time.sleep(15) # Give more time for remote processing
# Step 5: Verify positions
print(f"\n=== VERIFYING POSITIONS ===")
if not SKIP_LOCAL_DEVICE:
# Check LOCAL device position
print(f"\n5. Checking LOCAL device (serial connected) position...")
try:
updated_local_info = iface.getMyNodeInfo()
if updated_local_info and 'position' in updated_local_info and updated_local_info['position']:
received_lat = updated_local_info['position']['latitude']
received_lon = updated_local_info['position']['longitude']
received_alt = updated_local_info['position']['altitude']
print(f"Updated local device position:")
print(f" Latitude: {received_lat}")
print(f" Longitude: {received_lon}")
print(f" Altitude: {received_alt}")
# Check if it's still at 0,0
if received_lat == 0.0 and received_lon == 0.0:
print("❌ LOCAL DEVICE STILL AT 0,0 - Fixed position setting failed!")
else:
# Compare with target values
lat_diff = abs(latitude - received_lat)
lon_diff = abs(longitude - received_lon)
alt_diff = abs(altitude - received_alt)
print(f"\nPosition comparison:")
print(f" Latitude difference: {lat_diff:.7f}")
print(f" Longitude difference: {lon_diff:.7f}")
print(f" Altitude difference: {alt_diff}")
# Check if positions match (within reasonable tolerance)
tolerance = 0.0001 # About 10 meters
if lat_diff < tolerance and lon_diff < tolerance and alt_diff < 50:
print("✅ LOCAL DEVICE: Fixed position set correctly!")
else:
print("⚠️ LOCAL DEVICE: Position updated but doesn't match target exactly")
if lat_diff < 0.001 and lon_diff < 0.001: # Within 100m
print(" (Still within reasonable GPS accuracy)")
else:
print("❌ No position information available for local device")
except Exception as e:
print(f"Error getting updated local node info: {e}")
else:
print("\n🔄 SKIPPED LOCAL DEVICE VERIFICATION")
# Check REMOTE device position - using mesh nodes directly
print(f"\n6. Checking REMOTE device {target_node} position...")
remote_target = target_node.lower()
try:
# Check mesh nodes for remote device
nodes = iface.nodes
print(f" Checking mesh nodes for {remote_target}...")
if remote_target in nodes:
node_data = nodes[remote_target]
print(f" ✅ Found remote device in mesh")
if 'position' in node_data and node_data['position']:
pos = node_data['position']
remote_lat = pos.get('latitude', 0)
remote_lon = pos.get('longitude', 0)
remote_alt = pos.get('altitude', 0)
print(f" Remote device position:")
print(f" Latitude: {remote_lat}")
print(f" Longitude: {remote_lon}")
print(f" Altitude: {remote_alt}")
# Check if it's still at 0,0
if remote_lat == 0.0 and remote_lon == 0.0:
print(" ❌ REMOTE DEVICE STILL AT 0,0!")
print(" Admin commands were not processed by remote device")
print(" Remote device may require manual configuration")
else:
# Compare with target values
lat_diff = abs(latitude - remote_lat)
lon_diff = abs(longitude - remote_lon)
alt_diff = abs(altitude - remote_alt)
print(f" Position comparison:")
print(f" Latitude difference: {lat_diff:.7f}")
print(f" Longitude difference: {lon_diff:.7f}")
print(f" Altitude difference: {alt_diff}")
# Check if positions match
tolerance = 0.001 # About 100 meters tolerance
if lat_diff < tolerance and lon_diff < tolerance:
print(" ✅ SUCCESS! REMOTE DEVICE: Fixed position set correctly!")
else:
print(" ⚠️ REMOTE DEVICE: Position changed but doesn't match target exactly")
if lat_diff < 0.01 and lon_diff < 0.01: # Within ~1km
print(" (Position is in reasonable range)")
else:
print(" ❌ No position data available for remote device")
print(f" Node data: {node_data}")
else:
print(f" ❌ Remote device {remote_target} not found in mesh")
print(f" Available nodes: {list(nodes.keys())}")
except Exception as e:
print(f" ❌ Error checking remote device: {e}")
print(f"\n=== SUMMARY ===")
print("✅ Script completed successfully")
print("📍 Local device fixed position should now be set correctly")
print("📡 Position command sent to remote device")
print("⚠️ Remote device may require manual configuration if command wasn't processed")
except Exception as e:
print(f"❌ Error: {e}")
finally:
# Close interface
iface.close()
print("\n🔌 Connection closed")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment