Added GPS Telemetry packet support. Added JSON logging to UBlox lib.

pull/1/head
Mark Jessop 2016-12-23 18:02:45 +10:30
rodzic 928a2fe8e4
commit e6f4a7bff0
6 zmienionych plików z 242 dodań i 31 usunięć

Wyświetl plik

@ -4,6 +4,7 @@
# #
import struct import struct
import traceback import traceback
import datetime
WENET_IMAGE_UDP_PORT = 7890 WENET_IMAGE_UDP_PORT = 7890
WENET_TELEMETRY_UDP_PORT = 7891 WENET_TELEMETRY_UDP_PORT = 7891
@ -12,12 +13,15 @@ WENET_TELEMETRY_UDP_PORT = 7891
class WENET_PACKET_TYPES: class WENET_PACKET_TYPES:
TEXT_MESSAGE = 0x00 TEXT_MESSAGE = 0x00
TELEMETRY = 0x01 GPS_TELEMETRY = 0x01
IMU_TELEMETRY = 0x02
# Your packet types here! # Your packet types here!
IMAGE_TELEMETRY = 0x54
SSDV = 0x55 SSDV = 0x55
IDLE = 0x56 IDLE = 0x56
class WENET_PACKET_LENGTHS:
GPS_TELEMETRY = 35
def decode_packet_type(packet): def decode_packet_type(packet):
# Convert packet to a list of integers before parsing. # Convert packet to a list of integers before parsing.
@ -30,8 +34,12 @@ def packet_to_string(packet):
if packet_type == WENET_PACKET_TYPES.TEXT_MESSAGE: if packet_type == WENET_PACKET_TYPES.TEXT_MESSAGE:
return text_message_string(packet) return text_message_string(packet)
elif packet_type == WENET_PACKET_TYPES.TELEMETRY: elif packet_type == WENET_PACKET_TYPES.GPS_TELEMETRY:
return telemetry_string(packet) return gps_telemetry_string(packet)
elif packet_type == WENET_PACKET_TYPES.IMU_TELEMETRY:
return imu_telemetry_string(packet)
elif packet_type == WENET_PACKET_TYPES.IMAGE_TELEMETRY:
return image_telemetry_string(packet)
elif packet_type == WENET_PACKET_TYPES.SSDV: elif packet_type == WENET_PACKET_TYPES.SSDV:
return ssdv_packet_string(packet) return ssdv_packet_string(packet)
else: else:
@ -116,7 +124,136 @@ def text_message_string(packet):
return "Text Message #%d: \t%s" % (message['id'],message['text']) return "Text Message #%d: \t%s" % (message['id'],message['text'])
# #
# GPS/IMU Telemetry # GPS Telemetry Decoder
# #
def telemetry_string(packet): # Refer Telemetry format documented in:
# https://docs.google.com/document/d/12230J1X3r2-IcLVLkeaVmIXqFeo3uheurFakElIaPVo/edit?usp=sharing
#
# The above
def gps_weeksecondstoutc(gpsweek, gpsseconds, leapseconds):
""" Convert time in GPS time (GPS Week, seconds-of-week) to a UTC timestamp """
epoch = datetime.datetime.strptime("1980-01-06 00:00:00","%Y-%m-%d %H:%M:%S")
elapsed = datetime.timedelta(days=(gpsweek*7),seconds=(gpsseconds+leapseconds))
timestamp = epoch + elapsed
return timestamp.isoformat()
def gps_telemetry_decoder(packet):
""" Extract GPS telemetry data from a packet, and return it as a python dictionary. """
# We need the packet as a string, convert to a string in case we were passed a list of bytes.
packet = str(bytearray(packet))
gps_data = {}
# Some basic sanity checking of the packet before we attempt to decode.
if len(packet) < WENET_PACKET_LENGTHS.GPS_TELEMETRY:
return {'error': 'GPS Telemetry Packet has invalid length.'}
elif len(packet) > WENET_PACKET_LENGTHS.GPS_TELEMETRY:
# If the packet is too big (which it will be, as it's padded with 0x55's), clip it.
packet = packet[:WENET_PACKET_LENGTHS.GPS_TELEMETRY]
else:
pass
# Wrap the next bit in exception handling.
try:
# Unpack the packet into a list.
data = struct.unpack('>BHIBffffffBBB', packet)
gps_data['week'] = data[1]
gps_data['iTOW'] = data[2]/1000.0 # iTOW provided as milliseconds, convert to seconds.
gps_data['leapS'] = data[3]
gps_data['latitude'] = data[4]
gps_data['longitude'] = data[5]
gps_data['altitude'] = data[6]
gps_data['ground_speed'] = data[7]
gps_data['heading'] = data[8]
gps_data['ascent_rate'] = data[9]
gps_data['numSV'] = data[10]
gps_data['gpsFix'] = data[11]
gps_data['dynamic_model'] = data[12]
# Perform some post-processing on the data, to make some of the fields easier to read.
# Produce a human-readable timestamp, in UTC time.
gps_data['timestamp'] = gps_weeksecondstoutc(gps_data['week'], gps_data['iTOW'], gps_data['leapS'])
# Produce a human-readable indication of GPS Fix state.
if gps_data['gpsFix'] == 0:
gps_data['gpsFix_str'] = 'No Fix'
elif gps_data['gpsFix'] == 2:
gps_data['gpsFix_str'] = '2D Fix'
elif gps_data['gpsFix'] == 3:
gps_data['gpsFix_str'] = '3D Fix'
elif gps_data['gpsFix'] == 5:
gps_data['gpsFix_str'] = 'Time Only'
else:
gps_data['gpsFix_str'] = 'Unknown (%d)' % gps_data['gpsFix']
# Produce a human-readable indication of the current dynamic model.
if gps_data['dynamic_model'] == 0:
gps_data['dynamic_model_str'] = 'Portable'
elif gps_data['dynamic_model'] == 1:
gps_data['dynamic_model_str'] = 'Not Used'
elif gps_data['dynamic_model'] == 2:
gps_data['dynamic_model_str'] = 'Stationary'
elif gps_data['dynamic_model'] == 3:
gps_data['dynamic_model_str'] = 'Pedestrian'
elif gps_data['dynamic_model'] == 4:
gps_data['dynamic_model_str'] = 'Automotive'
elif gps_data['dynamic_model'] == 5:
gps_data['dynamic_model_str'] = 'Sea'
elif gps_data['dynamic_model'] == 6:
gps_data['dynamic_model_str'] = 'Airborne 1G'
elif gps_data['dynamic_model'] == 7:
gps_data['dynamic_model_str'] = 'Airborne 2G'
elif gps_data['dynamic_model'] == 8:
gps_data['dynamic_model_str'] = 'Airborne 4G'
else:
gps_data['dynamic_model_str'] = 'Unknown'
gps_data['error'] = 'None'
return gps_data
except:
traceback.print_exc()
print(packet)
return {'error': 'Could not decode GPS telemetry packet.'}
def gps_telemetry_string(packet):
gps_data = gps_telemetry_decoder(packet)
if gps_data['error'] != 'None':
return "GPS: ERROR Could not decode."
else:
gps_data_string = "GPS: %s Lat/Lon: %.5f,%.5f Alt: %dm, Speed: H %dkph V %.1fm/s, Heading: %d deg, Fix: %s, SVs: %d, Model: %s " % (
gps_data['timestamp'],
gps_data['latitude'],
gps_data['longitude'],
int(gps_data['altitude']),
int(gps_data['ground_speed']),
gps_data['ascent_rate'],
int(gps_data['heading']),
gps_data['gpsFix_str'],
gps_data['numSV'],
gps_data['dynamic_model_str']
)
return gps_data_string
#
# IMU Telemetry Decoder
#
def imu_telemetry_decoder(packet):
return "Not Implemented."
def imu_telemetry_string(packet):
return "Not Implemented."
#
# Image (Combined GPS/IMU) Telemetry Decoder
#
def image_telemetry_decoder(packet):
return "Not Implemented."
def image_telemetry_string(packet):
return "Not Implemented." return "Not Implemented."

Wyświetl plik

@ -92,7 +92,7 @@ while True:
print("Text Message #%d: \t%s" % (message['id'],message['text'])) print("Text Message #%d: \t%s" % (message['id'],message['text']))
current_text_message = message['id'] current_text_message = message['id']
elif packet_type == WENET_PACKET_TYPES.TELEMETRY: elif packet_type == WENET_PACKET_TYPES.GPS_TELEMETRY:
broadcast_telemetry_packet(data) broadcast_telemetry_packet(data)
print("Telemetry Packet: Not Implemented yet.") print("Telemetry Packet: Not Implemented yet.")

Wyświetl plik

@ -8,6 +8,7 @@
import json import json
import socket import socket
import datetime import datetime
import traceback
from WenetPackets import * from WenetPackets import *
def process_udp(udp_packet): def process_udp(udp_packet):
@ -48,6 +49,7 @@ def udp_rx_thread():
try: try:
process_udp(m[0]) process_udp(m[0])
except: except:
traceback.print_exc()
pass pass
print("Closing UDP Listener") print("Closing UDP Listener")

Wyświetl plik

@ -20,6 +20,7 @@ import sys
import os import os
import crcmod import crcmod
import struct import struct
import traceback
from time import sleep from time import sleep
from threading import Thread from threading import Thread
import numpy as np import numpy as np
@ -165,10 +166,8 @@ class PacketTX(object):
def queue_image_file(self, filename): def queue_image_file(self, filename):
""" Read in <filename> and transmit it, 256 bytes at a time. """ Read in <filename> and transmit it, 256 bytes at a time.
Intended for transmitting SSDV packets. Intended for transmitting SSDV images.
""" """
file_size = os.path.getsize(filename) file_size = os.path.getsize(filename)
try: try:
f = open(filename,'rb') f = open(filename,'rb')
@ -180,7 +179,6 @@ class PacketTX(object):
except: except:
return False return False
def image_queue_empty(self): def image_queue_empty(self):
return self.ssdv_queue.qsize() == 0 return self.ssdv_queue.qsize() == 0
@ -191,7 +189,22 @@ class PacketTX(object):
def telemetry_queue_empty(self): def telemetry_queue_empty(self):
return self.telemetry_queue.qsize() == 0 return self.telemetry_queue.qsize() == 0
#
# Various Telemetry Packet Generation functions
#
def transmit_text_message(self,message, repeats = 1): def transmit_text_message(self,message, repeats = 1):
""" Generate and Transmit a Text Message Packet
Keyword Arguments:
message: A string, up to 252 characters long, to transmit.
repeats: An optional field, defining the number of time to
transmit the packet. Can be used to increase chances
of receiving the packet, at the expense of higher
channel usage.
"""
# Increment text message counter. # Increment text message counter.
self.text_message_count = (self.text_message_count+1)%65536 self.text_message_count = (self.text_message_count+1)%65536
# Clip message if required. # Clip message if required.
@ -203,6 +216,42 @@ class PacketTX(object):
self.queue_telemetry_packet(packet, repeats=repeats) self.queue_telemetry_packet(packet, repeats=repeats)
print("TXing Text Message #%d: %s" % (self.text_message_count,message)) print("TXing Text Message #%d: %s" % (self.text_message_count,message))
def transmit_gps_telemetry(self, gps_data):
""" Generate and Transmit a GPS Telemetry Packet
Keyword Arguments:
gps_data: A dictionary, as produced by the UBloxGPS class. It must have the following fields:
latitude, longitude, altitude, ground_speed, ascent_rate, heading, gpsFix, numSV,
week, iTOW, leapS, dynamic_model.
The generated packet format is in accordance with the specification in:
https://docs.google.com/document/d/12230J1X3r2-IcLVLkeaVmIXqFeo3uheurFakElIaPVo/edit?usp=sharing
The corresponding decoder for this packet format is within rx/WenetPackets.py, in the function
gps_telemetry_decoder
"""
try:
gps_packet = struct.pack(">BHIBffffffBBB",
1, # Packet ID for the GPS Telemetry Packet.
gps_data['week'],
int(gps_data['iTOW']*1000), # Convert the GPS week value to milliseconds, and cast to an int.
gps_data['leapS'],
gps_data['latitude'],
gps_data['longitude'],
gps_data['altitude'],
gps_data['ground_speed'],
gps_data['heading'],
gps_data['ascent_rate'],
gps_data['numSV'],
gps_data['gpsFix'],
gps_data['dynamic_model']
)
self.queue_telemetry_packet(gps_packet)
except:
traceback.print_exc()
class BinaryDebug(object): class BinaryDebug(object):

Wyświetl plik

@ -39,27 +39,30 @@ time.sleep(1)
# Initialise a couple of global variables. # Initialise a couple of global variables.
max_altitude = 0 max_altitude = 0
def handle_gps_data(gps_data):
""" Handle GPS data passed to us from the UBloxGPS instance """
global max_altitude, tx
# Immediately transmit a GPS packet.
tx.transmit_gps_telemetry(gps_data)
if gps_data['altitude'] > max_altitude:
max_altitude = gps_data['altitude']
# Try and start up the GPS rx thread. # Try and start up the GPS rx thread.
try: try:
gps = ublox.UBloxGPS(port=args.gps, gps = ublox.UBloxGPS(port=args.gps,
dynamic_model = ublox.DYNAMIC_MODEL_AIRBORNE1G, dynamic_model = ublox.DYNAMIC_MODEL_AIRBORNE1G,
update_rate_ms = 500, update_rate_ms = 1000,
debug_ptr = tx.transmit_text_message) debug_ptr = tx.transmit_text_message,
callback = handle_gps_data,
log_file = 'gps_data.log'
)
except Exception as e: except Exception as e:
tx.transmit_text_message("ERROR: Could not Open GPS - %s" % str(e), repeats=5) tx.transmit_text_message("ERROR: Could not Open GPS - %s" % str(e), repeats=5)
gps = None gps = None
def poll_gps():
global gps, max_altitude
if gps != None:
gps_state = gps.read_state()
if gps_state['altitude'] > max_altitude:
max_altitude = gps_state['altitude']
# Define our post-processing callback function, which gets called by WenetPiCam # Define our post-processing callback function, which gets called by WenetPiCam
# after an image has been captured. # after an image has been captured.
def post_process_image(filename): def post_process_image(filename):
@ -115,7 +118,6 @@ picam.run(destination_directory="./tx_images/",
# Main 'loop'. # Main 'loop'.
try: try:
while True: while True:
poll_gps() # Keep polling the GPS so we keep track of our maximum altitude.
time.sleep(1) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
print("Closing") print("Closing")

Wyświetl plik

@ -12,7 +12,7 @@ Added UBloxGPS abstraction layer class for use with Wenet TX system.
import struct import struct
import datetime import datetime
from threading import Thread from threading import Thread
import time, os import time, os, json
# protocol constants # protocol constants
PREAMBLE1 = 0xb5 PREAMBLE1 = 0xb5
@ -917,6 +917,7 @@ class UBloxGPS(object):
'timestamp': " ", # ISO-8601 Compliant Date-code (generate by Python's datetime.isoformat() function) 'timestamp': " ", # ISO-8601 Compliant Date-code (generate by Python's datetime.isoformat() function)
'dynamic_model': 0 # Current dynamic model in use. 'dynamic_model': 0 # Current dynamic model in use.
} }
# Lock files for writing and reading to the internal state dictionary.
state_writelock = False state_writelock = False
state_readlock = False state_readlock = False
@ -924,7 +925,8 @@ class UBloxGPS(object):
callback=None, callback=None,
update_rate_ms=500, update_rate_ms=500,
dynamic_model=DYNAMIC_MODEL_AIRBORNE1G, dynamic_model=DYNAMIC_MODEL_AIRBORNE1G,
debug_ptr = None): debug_ptr = None,
log_file = None):
""" Initialise a UBloxGPS Abstraction layer object. """ Initialise a UBloxGPS Abstraction layer object.
@ -944,6 +946,9 @@ class UBloxGPS(object):
In the wenet payload, we use this to link this object to the PacketTX object to be able to In the wenet payload, we use this to link this object to the PacketTX object to be able to
transit debug messages to the ground. transit debug messages to the ground.
log_file: An optional filename in which to log GPS state data. Data will be stored as lines of JSON data.
Data is written whenever the gps_callback function is called.
""" """
# Copy supplied values. # Copy supplied values.
@ -955,6 +960,13 @@ class UBloxGPS(object):
self.debug_ptr = debug_ptr self.debug_ptr = debug_ptr
self.callback = callback self.callback = callback
# Open log file, if one has been given.
if log_file != None:
self.log_file = open(log_file,'a')
else:
self.log_file = None
# Attempt to inialise. # Attempt to inialise.
self.gps = UBlox(self.port, self.baudrate, self.timeout) self.gps = UBlox(self.port, self.baudrate, self.timeout)
self.setup_ublox() self.setup_ublox()
@ -991,8 +1003,7 @@ class UBloxGPS(object):
""" Write a debug message. """ Write a debug message.
If debug_ptr was set to a function during init, this will If debug_ptr was set to a function during init, this will
pass the message to that function, else it will just print it. pass the message to that function, else it will just print it.
This is used mainly to get updates on image capture into the Wenet downlink. This is used mainly to get error and other state updates into the Wenet downlink.
""" """
message = "GPS Debug: " + message message = "GPS Debug: " + message
if self.debug_ptr != None: if self.debug_ptr != None:
@ -1000,6 +1011,7 @@ class UBloxGPS(object):
else: else:
print(message) print(message)
# Thread-safe read/write access into the internal state dictionary
def write_state(self, value, parameter): def write_state(self, value, parameter):
""" (Hopefully) thread-safe state dictionary write access """ """ (Hopefully) thread-safe state dictionary write access """
while self.state_readlock: while self.state_readlock:
@ -1022,16 +1034,23 @@ class UBloxGPS(object):
return state_copy return state_copy
# Function called whenever we have a new GPS fix.
def gps_callback(self): def gps_callback(self):
""" Pass the latest GPS state to an external callback function """ """ Pass the latest GPS state to an external callback function """
# Grab latest state. # Grab latest state.
latest_state = self.read_state() latest_state = self.read_state()
# Write into the log file, if we are using one.
if self.log_file != None:
self.log_file.write(json.dumps(latest_state) + '\n')
# If we don't have a callback function to use, return immediately.
if self.callback == None: if self.callback == None:
return return
else:
self.callback(latest_state)
self.callback(latest_state) # Utility function to convert GPS time to UTC time.
def weeksecondstoutc(self, gpsweek, gpsseconds, leapseconds): def weeksecondstoutc(self, gpsweek, gpsseconds, leapseconds):
""" Convert time in GPS time (GPS Week, seconds-of-week) to a UTC timestamp """ """ Convert time in GPS time (GPS Week, seconds-of-week) to a UTC timestamp """
epoch = datetime.datetime.strptime("1980-01-06 00:00:00","%Y-%m-%d %H:%M:%S") epoch = datetime.datetime.strptime("1980-01-06 00:00:00","%Y-%m-%d %H:%M:%S")
@ -1111,6 +1130,7 @@ class UBloxGPS(object):
# A message with only 0x00 in the payload field is a poll. # A message with only 0x00 in the payload field is a poll.
self.gps.send_message(CLASS_CFG, MSG_CFG_NAV5,'\x00') self.gps.send_message(CLASS_CFG, MSG_CFG_NAV5,'\x00')
# Send data to the callback function.
callback_thread = Thread(target=self.gps_callback) callback_thread = Thread(target=self.gps_callback)
callback_thread.start() callback_thread.start()
@ -1129,6 +1149,7 @@ class UBloxGPS(object):
self.rx_running = False self.rx_running = False
time.sleep(0.5) time.sleep(0.5)
self.gps.close() self.gps.close()
self.log_file.close()
if __name__ == "__main__": if __name__ == "__main__":
""" Basic test script for the above UBloxGPS class. """ Basic test script for the above UBloxGPS class.