Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User/evan012/at command support multiple message parse #88

Closed
31 changes: 31 additions & 0 deletions docs/MESSAGE_FORMATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,35 @@ On the parser side, we randomize the IMU message as follows:

**EXAMPLE RAW IMU STRING**: "AÙw«â»K@AYD>#"

## Local AT Command

This is the xbee generated response that is returned after we send a local AT Command from Sunlink. The minimum length is 9 bytes, which is when the command returns no data. The length will increase depending on the length of the return data. The length is comprised of:

- 7E start delimiter ( 1 byte)
- 2 Byte Length
- 1 by indicating frame type (0x88 in this case)
- 1 Byte indicating Frame ID
- 2 Bytes indicating the corresponding AT Command
- 1 Byte indicating the command Status
- 0-256 bytes indicateing the command data
- 1 byte indicating the checksum

A detailed look into the specific AT command returns we expect to get on sunlink can be found in the API Frames BOM.


## Remote AT Command
This is the xbee response returned after we send a remote AT Command from Sunlink (Command that affects Xbee module connected to Telemetry board.) The minimum length is 19 bytes, which occurs when the command returns no data. The length will increase depending on the length of the return data. The length is comprised of

- 1 Byte start delimiter
- 2 bytes length
- 1 Byte Frame Type (0x97)
- 1 Byte Frame ID
- 8 Byte 64 bit address (of remote radio returning message)
- 2 Byte 16 bit Address (of remote device)
- 2 Byte AT Command
- 1 Byte command status
- 0-256 Byte Command Data
- 1 Byte Checksum


## NEW MESSAGE TYPE HERE
106 changes: 82 additions & 24 deletions link_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import os
import glob
import struct
import re
import threading

from datetime import datetime
from toml.decoder import TomlDecodeError
Expand Down Expand Up @@ -72,6 +74,19 @@
HEALTH_ENDPOINT = f"{PARSER_URL}/api/v1/health"

EXPECTED_CAN_MSG_LENGTH = 30
CAN_MSG_LENGTH = 24
IMU_MSG_LENGTH = 17
GPS_MSG_LENGTH = 200

CAN_BYTE = 0x00
IMU_BYTE = 0x01
GPS_BYTE = 0x02


LOCAL_AT_BYTE = 0x03
REMOTE_AT_BYTE = 0x04

UNKNOWN_BYTE = 0x05

# ANSI sequences
ANSI_ESCAPE = "\033[0m"
Expand Down Expand Up @@ -405,41 +420,79 @@ def upload_logs(args, live_filters, log_filters, endpoint):
# Call the memorator log uploader function
memorator_upload_script(sendToParser, live_filters, log_filters, args, endpoint)


"""
Purpose: Processes the message by splitting it into parts and returning the parts and the buffer
Parameters:
message - The total chunk read from the serial stream
buffer - the buffer to be added to the start of the message
Returns (tuple):
parts - the fully complete messages of the total chunk read
buffer - leftover chunk that is not a message
"""
def process_message(message: str, buffer: str = "") -> list:
# Remove 00 0a from the start if present
if message.startswith("000a"):
message = message[4:]
elif message.startswith("0a"):
message = message[2:]


# Add buffer to the start of the message
message = buffer + message

# Split the message by 0d 0a
parts = message.split("0d0a")
# Split the message by 7e and one of 88, 97, 10 (types of radio messages we get)
pattern = '(?=7E....88|7E....97|7E....10)'
parts = re.split(pattern, message)

if len(parts) > 1:
buffer = parts.pop()

if len(parts[-1]) != 30 or len(parts[-1]) != 396 or len(parts[-1]) != 44:
buffer = parts.pop()
return [bytes.fromhex(part).decode('latin-1') for part in parts]


return [bytes.fromhex(part).decode('latin-1') for part in parts] , buffer
"""""
smaller_parts = []
for part in parts:
if part[3] == '10':
if part[17] == CAN_BYTE:
smaller_parts.extend(split_api_packet(part, CAN_MSG_LENGTH, CAN_BYTE))
elif part[17] == IMU_BYTE:
smaller_parts.extend(split_api_packet(part, IMU_MSG_LENGTH, IMU_BYTE))
elif part[17] == GPS_BYTE:
smaller_parts.extend(split_api_packet(part, GPS_MSG_LENGTH, GPS_BYTE))
else:
smaller_parts.extend(UNKNOWN_BYTE + part)
elif part[3] == '88':
smaller_parts.extend(LOCAL_AT_BYTE + part)
elif part[3] == '97':
smaller_parts.extend(REMOTE_AT_BYTE + part)
else:
smaller_parts.extend(UNKNOWN_BYTE + part)

return [bytes.fromhex(part).decode('latin-1') for part in smaller_parts] , buffer

def split_api_packet(message, message_size, message_byte):
return [message_byte + message[i: i + message_size] for i in range(19, len(message), message_size)]

"""
"""
Purpose: Sends data and filters to parser and registers a callback to process the response
Parameters:
message - raw byte data to be parsed on parser side
live_filters - filters for which messages to live stream to Grafana
args - the arguments passed to ./link_telemetry.py
parser_endpoint - the endpoint to send the data to
Returns: None
"""
def sendToParser(message: str, live_filters: list, args: list, parser_endpoint: str):
payload = {
"message" : message,
"live_filters" : live_filters
}

# submit to thread pool
future = executor.submit(parser_request, payload, parser_endpoint)

# register done callback with future (lambda function to pass in arguments)
future.add_done_callback(lambda future: process_response(future, args))

def atDiagnosticCommand(command_list):
lock.acquire()
while True:
for command in command_list:
serial.write(command)
time.sleep(1)
lock.release()
def main():
"""
Main telemetry link entrypoint.
"""

lock = threading.Lock()
# <----- Argument parsing ----->

parser = argparse.ArgumentParser(
Expand Down Expand Up @@ -574,13 +627,14 @@ def main():
elif not args.log:
log_filters = ["NONE"]


# <----- Configuration confirmation ----->
if not args.log_upload:
print_config_table(args, live_filters)
choice = input("Are you sure you want to continue with this configuration? (y/N) > ")
if choice.lower() != "y":
return

# Define
# <----- Create the thread pool ----->

global executor
Expand Down Expand Up @@ -610,6 +664,9 @@ def main():
if args.log_upload:
upload_logs(args, live_filters, log_filters, LOG_WRITE_ENDPOINT)
return 0
##ADD New Thread Here!
future = executor.submit(atDiagnosticCommand, command_list)


while True:
message: bytes
Expand Down Expand Up @@ -652,11 +709,12 @@ def main():
ser.open()

while True:
lock.acquire()
# read in bytes from COM port
chunk = ser.read(CHUNK_SIZE)
chunk = chunk.hex()
parts, buffer = process_message(chunk, buffer)

lock.release()
for part in parts:
sendToParser(part, live_filters, log_filters, args, PARSER_ENDPOINT)

Expand Down
58 changes: 44 additions & 14 deletions parser/create_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
from parser.data_classes.CAN_Msg import CAN # CAN message
from parser.data_classes.IMU_Msg import IMU # IMU message
from parser.data_classes.GPS_Msg import GPS # GPS message
from parser.data_classes.Local_AT_Command_Return import AT as AT_LOCAL
from parser.data_classes.Remote_AT_Command_Return import AT as AT_REMOTE
from parser.parameters import * # For mins and maxes of messages



"""
Factory method for creating a Message object based on the message type
To add new message types simply add a new elif statement:
Expand All @@ -21,24 +24,51 @@
Returns:
a message object (CAN, GPS, IMU, etc.)
"""
# def split_api_packet(message, message_size, message_byte):
# return [message_byte + message[i: i + message_size] for i in range(19, len(message), message_size)]
# try:
# individual_messsages = []
# if message[3] == '10':
# if message[17] == CAN_BYTE:
# individual_messsages.extend(split_api_packet(message, CAN_MSG_LENGTH, CAN_BYTE))
# elif message[17] == IMU_BYTE:
# individual_messsages.extend(split_api_packet(message, IMU_MSG_LENGTH, IMU_BYTE))
# elif message[17] == GPS_BYTE:
# individual_messsages.extend(split_api_packet(message, GPS_MSG_LENGTH, GPS_BYTE))

# elif message[3] == '88':
# individual_messsages.extend(LOCAL_AT_BYTE + message)
# elif message[3] == '97':
# individual_messsages.extend(REMOTE_AT_BYTE + message)

# ## [bytes.fromhex(part).decode('latin-1') for part in smaller_parts]

# for part in individual_messsages:
# part = bytes.fromhex(part).decode('latin-1')
def create_message(message: str):
try:
if CAN_LENGTH_MIN <= len(message) <= CAN_LENGTH_MAX:
return CAN(message)
elif GPS_LENGTH_MIN <= len(message) <= GPS_LENGTH_MAX:
return GPS(message)
elif IMU_LENGTH_MIN <= len(message) <= IMU_LENGTH_MAX:
return IMU(message)
else:
raise Exception(
f"Message length of {len(message)} is not a valid length for any message type\n"
if message[0] == "7E" and (message[4] == ("88" or "97")):
parse_api_packet(message)
else:
try:
if part[0] == CAN_BYTE:
return CAN(part[1:])
elif part[0] == GPS_BYTE:
return GPS(part[1:])
elif message[0] == IMU_BYTE:
return IMU(part[1:])
elif message[0] == LOCAL_AT_BYTE:
return AT_LOCAL(part[1:])
elif message[0] == REMOTE_AT_BYTE:
return AT_REMOTE(part[1:])
else:
raise Exception(
f"Message byte of {message[0]} is not a valid byte for any message type\n"
f" Message: {message}\n"
f" Hex Message: {message.encode('latin-1').hex()}"
)

raise Exception(f"Message length of {len(message)} is not a valid length for any message type")
except Exception as e:
raise Exception(

except Exception as e:
raise Exception(
f"{ANSI_BOLD}Failed in create_message{ANSI_ESCAPE}:\n"
f" {e}"
)
Expand Down
30 changes: 30 additions & 0 deletions parser/data_classes/API_Frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Parameter imports. ADD AND REMOVE AS NEEDED
from parser.parameters import *
import struct
from time import strftime, localtime
from datetime import datetime

FRAME_DATA_POSITION = 19

def split_api_packet(message, message_size, message_byte):
return [message_byte + message[i: i + message_size] for i in range(FRAME_DATA_POSITION, len(message), message_size)]

def parse_api_packet(message):
individual_messsages = []
if message[3] == '10':
if message[17] == CAN_BYTE:
individual_messsages.extend(split_api_packet(message, CAN_MSG_LENGTH, CAN_BYTE))
elif message[17] == IMU_BYTE:
individual_messsages.extend(split_api_packet(message, IMU_MSG_LENGTH, IMU_BYTE))
elif message[17] == GPS_BYTE:
individual_messsages.extend(split_api_packet(message, GPS_MSG_LENGTH, GPS_BYTE))

elif message[3] == '88':
individual_messsages.extend(LOCAL_AT_BYTE + message)
elif message[3] == '97':
individual_messsages.extend(REMOTE_AT_BYTE + message)

for message in individual_messsages:
message = bytes.fromhex(message).decode('latin-1')
create_message(message)

Loading
Loading