diff --git a/.gitignore b/.gitignore index 7932308..4ab9be9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,14 @@ */__pycache__ +__pycache__ +.vscode/* +.vscode */.idea +.idea *.pyc +<<<<<<< HEAD /logs +======= +*/logs +logs +>>>>>>> feature_add_data_vis diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 3691827..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "python.linting.pylintEnabled": false, - "python.linting.pep8Enabled": true, - "python.linting.enabled": true, - "python.linting.pep8Path": "pycodestyle" -} \ No newline at end of file diff --git a/GenerateHeaders.py b/GenerateHeaders.py deleted file mode 100644 index 4f03546..0000000 --- a/GenerateHeaders.py +++ /dev/null @@ -1,24 +0,0 @@ -import pandas as pd - -# Name of the column with "C-friendly" names -name_column = "name" - -sensors_df = pd.read_csv("SensorDict.csv", sep=",", header=0, dtype="str") -savefile = open("CANID.h", "w+") - -lastID = "" -for row in range(0, len(sensors_df)): - sensor_name_and_type = sensors_df.loc[row, name_column] - last_underscore = sensor_name_and_type[::-1].find("_") - assert last_underscore != -1, "Names not formatted correctly" - sensor_name = sensor_name_and_type[0:len(sensor_name_and_type) - 1 - - last_underscore] # i.e. "ACCEL" - ID = sensors_df.loc[row, "id"] - if lastID != ID: - savefile.write("\n#define " + sensor_name + "_CAN_ID " + ID + "\n") - savefile.write("#define " + sensor_name_and_type + "_START_BYTE " + - sensors_df.loc[row, "MSB"] + "\n") - savefile.write("#define " + sensor_name_and_type + "_END_BYTE " + - sensors_df.loc[row, "LSB"] + "\n") - lastID = ID -savefile.close() diff --git a/Main_WT_Server_DAQ.py b/Main_WT_Server_DAQ.py deleted file mode 100644 index aa2a3b6..0000000 --- a/Main_WT_Server_DAQ.py +++ /dev/null @@ -1,130 +0,0 @@ -import WirelessTelemServer as server -import asyncio -import random -import serial_asyncio -import time -from datetime import datetime -import logging -import os.path - -# This will run on startup on the RPI. - -# Using different threads, this file will setup a -# websocket server @ 192.168.10.1:5000 and broadcast -# every CAN message from the car. -# Information is available on the wiki page here: -# http://purdueelectricracing.com/wiki/index.php/Wireless_Telemetry -ip = '192.168.10.1' -port = 5000 - -start_date_time = datetime.now().strftime("%Y_%m_%d_%H_%M_%S") - -start_path = os.path.abspath(os.path.dirname(__file__)) -log_path = os.path.join(start_path, 'logs', - start_date_time + '.txt') -# configure logger to not format messages -logging.basicConfig(level=logging.DEBUG, filename=log_path, - filemode="a+", format='%(message)s') - -data_logger = logging.getLogger(__name__) -logging.getLogger("asyncio").setLevel(logging.WARNING) - -data_logger.info(start_date_time) - -# Max resolution (ms) for data frames to be logged -max_time_diff_ms = 100 -start_time = int(round(time.time() * 1000)) -last_time = start_time -multi_frame_message = '' - - -# Logs a set of CAN frames in a time interval to a log file -def log_CAN_data(timestamp, can_id, length, message): - current_time = int(round(time.time() * 1000)) - - # Wait for serial connection to stabilize, avoid garbage data - if current_time - start_time < 1500: - return - - global last_time, multi_frame_message - - delta_time = current_time - last_time - single_frame = ',' + can_id + ',' + length + ',' + message - - if(delta_time >= max_time_diff_ms): - data_logger.info(multi_frame_message) - last_time = current_time - multi_frame_message = str(current_time - start_time) + single_frame - else: - multi_frame_message += single_frame - - -# Used to setup and open CANdapter, only runs once. -async def write_serial_data(serial_connection): - serial_connection.write(b'A1\r') # Enable Timestamps - serial_connection.write(b'S6\r') # CAN Baudrate set to 500k - serial_connection.write(b'O\r') # Open CANdapter - - -# Read serial data from the CANdapter -async def rec_serial_data(serial_connection): - current_time = time.time() - last_time = current_time - # buffer_time = 1/500 - buffer_time = 0 - - buffer = {} - while True: - # Message EOF is a \r character - message_raw = await serial_connection.readuntil(b'\r') - - # Message format - # tIIILDDDDDDDD - # III = CAN ID - # L = Message Length - # D = Message data - - # Remove trailing \r and leadting t character - message = message_raw.replace(b'\r', b'')[1:].decode("utf-8", "ignore") - - m_id = message[0:3] - m_len = message[3:4] - m_message = message[4:-4] - m_timestamp = str(datetime.now()) - - log_CAN_data(m_timestamp, m_id, m_len, m_message) - - data = { - "id": m_id, - "length": m_len, - "message": m_message, - "timestamp": m_timestamp - } - - buffer[m_id] = data - - current_time = time.time() - if current_time - last_time >= buffer_time: - buffer_len = len(buffer) - if buffer_len > 0: - last_time = current_time - await server.send_data(buffer) - buffer = {} - - -async def main(loop): - serial_reader, serial_writer = await serial_asyncio.open_serial_connection( - url='/dev/ttyUSB0', baudrate=2500000) - - read_task = asyncio.ensure_future(rec_serial_data(serial_reader)) - write_task = asyncio.ensure_future(write_serial_data(serial_writer)) - - server_task = asyncio.ensure_future(server.get_server(ip, port)) - - done, pending = await asyncio.wait( - [read_task, write_task, server_task], - return_when=asyncio.ALL_COMPLETED - ) - -loop = asyncio.get_event_loop() -loop.run_until_complete(main(loop)) diff --git a/SavedGoogleDrive.csv b/SavedGoogleDrive.csv deleted file mode 100644 index d37cf33..0000000 --- a/SavedGoogleDrive.csv +++ /dev/null @@ -1,43 +0,0 @@ -vvvvv PLEASE READ BEFORE USING vvvvv,,,,,,,,,,,, -Values in square brackts [x:y] indicate the bits in the CAN frame byte.,,,,,,,,,,,, -Values in the parentheses (a:b) indicate the bits in the data field.,,,,,,,,,,,, -Fields are separated by commas.,,,,,,,,,,,, -These are all of the CAN IDs for the DCAN,,,,,,,,,,,, -TRANSMITTING MODULE,DESCRIPTION / SENSOR,ID,AddMethd,BYTE0,BYTE1,BYTE2,BYTE3,BYTE4,BYTE5,BYTE6,BYTE7,Comments -Example Module,,0x001,,,"Status bits (7:6), Ambient Temperature bits (5:0)",,,,,,, -Pedal Module,Pedalbox1,0x500,1,Raw Throttle Value 1 (11:8) [3:0],Throttle Value (7:0) [7:0],Brake Value (11:8) [3:0],Brake Value (7:0) [7:0],,,,, -DAQ2 Front,WHEEL_SPEED_FRONT,0x700,,WHEEL_SPEED_FRONT_L (15:8),WHEEL_SPEED_FRONT_L (7:0),WHEEL_SPEED_FRONT_R (15:8),WHEEL_SPEED_FRONT_R (7:0),,,,, -DAQ2 Rear,WHEEL_SPEED_REAR,0x701,,WHEEL_SPEED_REAR_L (15:8),WHEEL_SPEED_REAR_L (7:0),WHEEL_SPEED_REAR_R (15:8),WHEEL_SPEED_REAR_R (7:0),,,,, -DAQ2 Front,TIRE_TEMP_FRONT,0x710,,TIRE_TEMP_FRONT_R (15:8),TIRE_TEMP_FRONT_R (7:0),TIRE_TEMP_FRONT_L (15:8),TIRE_TEMP_FRONT_L (7:0),,,,, -DAQ2 Rear,TIRE_TEMP_REAR,0x711,,TIRE_TEMP_REAR_R (15:8),TIRE_TEMP_REAR_R (7:0),TIRE_TEMP_REAR_L (15:8),TIRE_TEMP_REAR_L (7:0),,,,, -DAQ2 Rear,COOLANT,0x720,,COOLANT_MOTOR (15:8),COOLANT_MOTOR (7:0),COOLANT_FLOW (15:8),COOLANT_FLOW (7:0),COOLANT_RAD (15:8),COOLANT_RAD (7:0),COOLANT_MC (15:8),COOLANT_MC (7:0), -DAQ2 Front,SHOCK_POT_FRONT,0x730,,SHOCK_POT_FRONT_L (15:8),SHOCK_POT_FRONT_L (7:0),SHOCK_POT_FRONT_R (15:8),SHOCK_POT_FRONT_R (7:0),,,,, -DAQ2 Rear,SHOCK_POT_REAR,0x731,,SHOCK_POT_REAR_R (15:8),SHOCK_POT_REAR_R (7:0),SHOCK_POT_REAR_L (15:8),SHOCK_POT_REAR_L (7:0),,,,, -DAQ2 Front,DROP_LINK,0x740,,DROP_LINK_LEFT (15:8),DROP_LINK_LEFT (7:0),DROP_LINK_RIGHT (15:8),DROP_LINK_RIGHT (7:0),,,,, -DAQ2 Front,STEER_ROD_FRONT,0x750,,STEER_ROD_FRONT_L (15:8),STEER_ROD_FRONT_L (7:0),PUSHROD_FRONT_R (15:8),PUSHROD_FRONT_R (7:0),PUSHROD_FRONT_L (15:8),PUSHROD_FRONT_L (7:0),STEER_ROD_FRONT_R (15:8),STEER_ROD_FRONT_R (7:0), -DAQ2 Rear,PUSHROD_REAR,0x760,,PUSHROD_REAR_R (15:8),PUSHROD_REAR_R (7:0),TIEROD_REAR_L (15:8),TIEROD_REAR_L (7:0),TIEROD_REAR_R (15:8),TIEROD_REAR_R (7:0),PUSHROD_REAR_L (15:8),PUSHROD_REAR_L (7:0), -DAQ2 Front,LCA_FRONT,0x770,,LCA_FRONT_LB (15:8),LCA_FRONT_LB (7:0),LCA_FRONT_RF (15:8),LCA_FRONT_RF (7:0),LCA_FRONT_LF (15:8),LCA_FRONT_LF (7:0),LCA_FRONT_RB (15:8),LCA_FRONT_RB (7:0), -DAQ2 Rear,LCA_REAR,0x780,,LCA_REAR_LF (15:8),LCA_REAR_LF (7:0),LCA_REAR_RF (15:8),LCA_REAR_RF (7:0),LCA_REAR_LB (15:8),LCA_REAR_LB (7:0),LCA_REAR_RB (15:8),LCA_REAR_RB (7:0), -DAQ2 Front,UCA_FRONT,0x790,,UCA_FRONT_RB (15:8),UCA_FRONT_RB (7:0),UCA_FRONT_LB (15:8),UCA_FRONT_LB (7:0),UCA_FRONT_RF (15:8),UCA_FRONT_RF (7:0),UCA_FRONT_LF (15:8),UCA_FRONT_LF (7:0), -DAQ2 Rear,UCA_REAR,0x7a0,,UCA_REAR_LF (15:8),UCA_REAR_LF (7:0),UCA_REAR_RB (15:8),UCA_REAR_RB (7:0),UCA_REAR_LB (15:8),UCA_REAR_LB (7:0),UCA_REAR_RF (15:8),UCA_REAR_RF (7:0), -DAQ2 Front,FRONT_TORSIONAL,0x7b0,,FRONT_TORSIONAL_ARB (15:8),FRONT_TORSIONAL_ARB (7:0),,,,,,, -DAQ2 Rear,REAR_TORSIONAL,0x7b1,,REAR_TORSIONAL_ARB (15:8),REAR_TORSIONAL_ARB (7:0),,,,,,, -Dashboard (VCAN),STEERING,0x7c0,,STEERING_TORSION (15:8),STEERING_TORSION (7:0),STEERING_ANGLE (15:8),STEERING_ANGLE (7:0),,,,, -DAQ2 Front,IMU,0x7d0,,IMU_TYPE (7:0),IMU_DATA (15:8),IMU_DATA (7:0),IMU_TIME (23:16),IMU_TIME (15:8),IMU_TIME (7:0,,, -,,,,,,,,,,,, -,,,,,,,,,,,, -,,,,,,,,,,,, -Main(?),Sensor enable/disable,0x7E0,,"Wheel speed (7) -Tire temp (6) -Coolant temp (5) -Coolant flow (4) -Shock Pots (3) -ARB/Droplinks (2) -Push rods (1) -Tie rods (0)","LCA (7) -UCA (6) -ARB_Torsional (5) -Steering angle (4) -Steering torsional (3) -Accelerometer (2) -Gyroscope (1)",,,,,,, \ No newline at end of file diff --git a/WT_Server/CAN_Logger.py b/WT_Server/CAN_Logger.py new file mode 100644 index 0000000..4455d49 --- /dev/null +++ b/WT_Server/CAN_Logger.py @@ -0,0 +1,69 @@ +import os.path +from datetime import datetime +import time +import sys +import csv + +log_path = '' + +# Max resolution (ms) for data frames to be logged +max_time_diff_ms = 10 +start_time = int(round(time.time() * 1000)) +last_time = start_time +multi_frame_message = [] + +def log_string(message): + global log_path + with open(log_path, 'a+') as logfile: + logfile.write(message) + +#Same concept as SensorDict's save_data_to_csv( +def log_csv(input_list): + global log_path + with open(log_path, 'a+') as fid: + logfile = csv.DictWriter(fid, + fieldnames=["timestamp", "id", "data"], + extrasaction="ignore") + for dictionary in input_list: + logfile.writerow(dictionary) + +def create_logfile(): + print("[INFO] Creating logfile...") + global log_path + + start_date_time = datetime.now().strftime("%Y_%m_%d_%H_%M_%S") + start_path = os.path.abspath(os.path.dirname(__file__)) + + log_path = os.path.join(start_path, 'logs', + start_date_time + '.csv') + + #log_string(start_date_time) + log_csv([{"id": "id", "data": "data", "timestamp": "timestamp"}]) + + print("[INFO] Logfile created: {}.csv".format(start_date_time)) + + +# Logs a set of CAN frames in a time interval to a log file +def log_CAN_data(can_id, message): + + current_time = int(round(time.time() * 1000)) + + global start_time + + # Wait for serial connection to stabilize, avoid garbage data + if current_time - start_time < 1500: + return + + global last_time, multi_frame_message, log_path + + delta_time = current_time - last_time + single_frame = {"id": str(can_id), "data": str(message), "timestamp": str(current_time-start_time)} + + multi_frame_message.append(single_frame) + + if(delta_time >= max_time_diff_ms): + log_csv(multi_frame_message) + last_time = current_time + print(str(multi_frame_message)) + multi_frame_message.clear() + diff --git a/DAQ_Simulation.py b/WT_Server/DAQ_Simulation.py similarity index 81% rename from DAQ_Simulation.py rename to WT_Server/DAQ_Simulation.py index e20a680..d75d89f 100644 --- a/DAQ_Simulation.py +++ b/WT_Server/DAQ_Simulation.py @@ -1,6 +1,7 @@ import WirelessTelemServer as server import asyncio import random +import time # This file simulates the job of the DAQ code. # A server is setup and broadcast on ip:port @@ -9,6 +10,14 @@ ip = '127.0.0.1' port = 5000 +possible_data = [ + { + 'i': '501', + 'm': '0F900EBF02A0029F', + 'ts': None + }, +] + possible_data = [ # { @@ -47,12 +56,13 @@ async def send_data(): await asyncio.sleep(0.01) index = int(random.random() * len(possible_data)) data = possible_data[index] - await server.send_data(data) + data['ts'] = int(time.time()*1000) + await server.send_data([data]) async def run_server(ip, port): wait_task = asyncio.ensure_future(send_data()) - server_task = asyncio.ensure_future(server.get_server('127.0.0.1', 5000)) + server_task = asyncio.ensure_future(server.get_server(ip, port)) done, pending = await asyncio.wait( [wait_task, server_task], diff --git a/WT_Server/Main_WT_Server_DAQ.py b/WT_Server/Main_WT_Server_DAQ.py new file mode 100644 index 0000000..ca50471 --- /dev/null +++ b/WT_Server/Main_WT_Server_DAQ.py @@ -0,0 +1,189 @@ +import WirelessTelemServer as server +import asyncio +import serial_asyncio +import serial +import time +from datetime import datetime +import os.path +import CAN_Logger +import sys + +# This will run on startup on the RPI. + +# Using different threads, this file will setup a +# websocket server @ 192.168.10.1:5000 and broadcast +# every CAN message from the car. +# Information is available on the wiki page here: +# http://purdueelectricracing.com/wiki/index.php/Wireless_Telemetry + +ip = '192.168.10.1' +port = 5000 +SER_PORT = '/dev/ttyUSB0' +SER_RATE = 5000000 +ENABLE_WEBSOCKETS = False + +ID_FILTER = [ + '0A0', # MC Temp 1 + '0A1', # MC Temp 2 + '0A2', # MC Temp 3 + '0A6', # MC Current + '0A7', # MC Voltage + '0A8', # MC Flux + '0AC', # MC Torque + '700', # Wheel Speed + '701', # Wheel Speed + '421', # Accel + '720', # Coolant + '721', # Coolant Flow + '6B1' # SOC Data +] + +if(len(sys.argv)) > 1: + ip = sys.argv[1] + +# Max resolution (ms) for data frames to be logged +start_time = int(round(time.time() * 1000)) +last_time = start_time +multi_frame_message = '' + + +def close_CANDAPTER(serial_connection): + close_message = "\rC\r".encode() + print(serial_connection.write(close_message)) + serial_connection.flush() + serial_connection.reset_output_buffer() + serial_connection.close() + print("Done cleaning CANDapter...") + + +def initalize_CANDAPTER(serial_connection): + baudrate = 'S6\r'.encode() # CAN Baudrate set to 500k + timestamp = 'A0\r'.encode() # Enable timestamps + open_message = 'O\r'.encode() # Open connection to CANDAPTER + clear_message = '\r\r\r'.encode() + + serial_connection.write(baudrate) + serial_connection.write(timestamp) + serial_connection.write(clear_message) + serial_connection.write(open_message) + + print("Done init CANDapter...") + + +# Read serial data from the CANdapter +async def rec_serial_data(ser_read, ser_write): + current_time = time.time() + last_time = current_time + buffer_time = 1/100 + + parsed_buffer = {} + message_buffer = bytes() + + initalize_CANDAPTER(ser_write) + time.sleep(1) + + START_LOGGING = True + + while True: + + # EXPERIMENTAL + # if not server.can_to_send == "": + # print("Message to send over CAN: " + server.can_to_send.encode) + # ser_write.write(server.can_to_send.encode() + b"\r") + # server.can_to_send = "" + # await ser_write.drain() + + in_byte = await ser_read.read(1) + if b"\x06" in in_byte: + # CANDapter has recieved our command. + print("CANDapter acknowledged command.") + elif b"\x07" in in_byte: + # CANDapter error + print("CANDApter Error") + elif b"\r" in in_byte: + # End of CAN frame + raw_message = message_buffer.decode().strip().replace("\r", "") + message_buffer = bytes() + if "t" not in raw_message: + # Reset message buffer if invalid message was recieved + continue + + # Strip initial 't' off of the message + t_index = raw_message.index("t") + + can_message = raw_message[t_index + 1:] + + m_id = can_message[0:3] + if not START_LOGGING: + if '350' in m_id: + START_LOGGING = True + print("Start button pressed! Logging data...\n") + CAN_Logger.create_logfile() + else: + if m_id in ID_FILTER: + stripped_message = can_message[4:] + CAN_Logger.log_CAN_data(m_id, stripped_message) + + + + + # m_timestamp = int(round(time.time() * 1000)) + + # data = { + # "i": m_id, + # "m": stripped_message, + # "ts": m_timestamp + # } + # parsed_buffer[num_id] = data + + # Log all data to the logfile. Can be done periodically + # by placing this line in the below FOR loop. + # ENABLE_WEBSOCKETS must be enabled to do so. + + + # current_time = time.time() + # if ENABLE_WEBSOCKETS and current_time - last_time >= buffer_time: + # buffer_len = len(parsed_buffer) + # if buffer_len > 0: + # last_time = current_time + # await server.send_data(parsed_buffer) + # parsed_buffer = {} + + # Clear message buffer once we are done with it. + + + else: + # Part of CAN frame, append to message being recieved + message_buffer += in_byte + + +async def main(loop): + serial_reader, serial_writer = await serial_asyncio.open_serial_connection( + url=SER_PORT, baudrate=SER_RATE + ) + + ser_task = asyncio.ensure_future( + rec_serial_data(serial_reader, serial_writer) + ) + + if ENABLE_WEBSOCKETS: + server_task = asyncio.ensure_future( + server.get_server(ip, port) + ) + done, pending = await asyncio.wait( + [ser_task, server_task], + return_when=asyncio.ALL_COMPLETED + ) + else: + done, pending = await asyncio.wait( + [ser_task], + return_when=asyncio.ALL_COMPLETED + ) + + +ser = serial.Serial(SER_PORT, SER_RATE) +# Close candapter at start to avoid any loose messages in the queue. +close_CANDAPTER(ser) + +loop = asyncio.get_event_loop() +loop.run_until_complete(main(loop)) diff --git a/WT_Server/Offline_DAQ.py b/WT_Server/Offline_DAQ.py new file mode 100644 index 0000000..6b49086 --- /dev/null +++ b/WT_Server/Offline_DAQ.py @@ -0,0 +1,118 @@ +import serial +import time +from datetime import datetime +import CAN_Logger +import sys +import pdb + +# This will run on startup on the RPI. + +# Using different threads, this file will setup a +# websocket server @ 192.168.10.1:5000 and broadcast +# every CAN message from the car. +# Information is available on the wiki page here: +# http://purdueelectricracing.com/wiki/index.php/Wireless_Telemetry + +SER_PORT = '/dev/ttyUSB0' +SER_RATE = 115200 + +ID_FILTER = [ + '0A0', # MC Temp 1 10100000 + '0A1', # MC Temp 2 + '0A2', # MC Temp 3 + '0A6', # MC Current + '0A7', # MC Voltage + '0A8', # MC Flux + '0AC', # MC Torque + '700', # Wheel Speed + '701', # Wheel Speed + '421', # Accel + '720', # Coolant + '721', # Coolant Flow + '6B1', # SOC Data + '0C0' # Main Torque command +] + + +def close_CANDAPTER(serial_connection): + close_message = "\rC\r".encode() + serial_connection.write(close_message) + serial_connection.flush() + serial_connection.reset_output_buffer() + serial_connection.close() + print("[INFO] Done cleaning CANDapter...") + + +def initalize_CANDAPTER(serial_connection): + baudrate = 'S6\r'.encode() # CAN Baudrate set to 500k + timestamp = 'A0\r'.encode() # Disable timestamps + open_message = 'O\r'.encode() # Open connection to CANDAPTER + clear_message = '\r\r\r'.encode() + + serial_connection.write(baudrate) + serial_connection.write(timestamp) + serial_connection.write(clear_message) + serial_connection.write(open_message) + + print("[INFO] Done init CANDapter...") + + +# Read serial data from the CANdapter +def start_data_collection(ser_port): + message_buffer = "" + + initalize_CANDAPTER(ser_port) + time.sleep(1) + + START_LOGGING = False + print("[INFO] Begin CAN bus listening...") + + while True: + # Read all data waiting in the buffer + message_buffer += ser_port.read().decode() + + # Messages are deliniaed with a \r char. The last message might not be complete so it + # must be carrried over to the next iteration + + finished_messages = [] + if '\r' in message_buffer: + temp = message_buffer.split("\r") + message_buffer = temp[-1] + finished_messages = temp[:-1] + + # Do not iterate over the last message in the buffer, it might be incomplete. + for clean_message in finished_messages: + + # All valid can messages begin with a 't' character + if not clean_message.startswith('t'): + # Reset message buffer if invalid message was recieved + print("[WARN] Invalid message recieved, t: (" + clean_message+")") + continue + + # Strip initial 't' off of the message + can_message = clean_message[1:] + + if not len(can_message) > 4: + # print("[WARN] Invalid message length recieved, length: (" + clean_message + ")") + continue + + m_id = can_message[0:3] + + if not START_LOGGING: + # if '350' in m_id: + START_LOGGING = True + print("[INFO] Creating logfile...") + CAN_Logger.create_logfile() + print("[INFO] Now logging data...") + + else: + if m_id in ID_FILTER: + stripped_message = can_message[4:] + CAN_Logger.log_CAN_data(m_id, stripped_message) + + +# Close candapter at start to avoid any loose messages in the queue. +close_CANDAPTER(serial.Serial(SER_PORT, SER_RATE)) + +start_data_collection(serial.Serial(SER_PORT, SER_RATE)) + diff --git a/WirelessTelemServer.py b/WT_Server/WirelessTelemServer.py similarity index 81% rename from WirelessTelemServer.py rename to WT_Server/WirelessTelemServer.py index f17942f..41208b2 100644 --- a/WirelessTelemServer.py +++ b/WT_Server/WirelessTelemServer.py @@ -2,11 +2,14 @@ import websockets from datetime import datetime import datetime +import time import json # Contains list of all connected users USERS = set() +can_to_send = "" + def get_server(ip, port): print('Serving websocket server on ' + ip + ':' + str(port) + ' ...') @@ -21,8 +24,8 @@ async def on_client_connect(websocket, path): print('Connected to client at ' + connection_addr + ' on ' + str(datetime.datetime.now())) - await websocket.send(json.dumps({'type': 'connected', 'payload': 'true', - 'timestamp': str(datetime.datetime.now()) + await websocket.send(json.dumps({'t': 'c', 'p': 't', + 'ts': int(round(time.time() * 1000)) })) global USERS try: @@ -40,9 +43,12 @@ async def on_client_connect(websocket, path): async def send_data(data): global USERS if USERS: - payload = json.dumps({'type': 'data', 'payload': data, - 'timestamp': str(datetime.datetime.now()}, - default=str)) + message = { + 't': 'd', + 'p': data, + 'ts': int(round(time.time() * 1000)) + } + payload = json.dumps(message, default=str) await asyncio.wait([user.send(payload) for user in USERS]) @@ -52,6 +58,10 @@ async def recieve_data_loop(websocket): while websocket.open: try: message = await websocket.recv() + + global can_to_send + can_to_send = message + print('Message from client: ' + str(message)) except Exception: print('Client disconnected!') diff --git a/CANID.h b/can_mgmt/CANID.h similarity index 93% rename from CANID.h rename to can_mgmt/CANID.h index 30c0e92..b358a7a 100644 --- a/CANID.h +++ b/can_mgmt/CANID.h @@ -1,4 +1,14 @@ - +/* Generated on 2019-02-13 19:08:58.489687 +* This file contains all of the constants that are +* being used on the CAN bus. +* +* Each message has an ID with specified sensors that +* lie on each message from bytes *_START_BYTE to +* *_END_BYTE. +*/ + +#ifndef CANID_H +#define CANID_H #define WHEEL_SPEED_FRONT_CAN_ID 0x700 #define WHEEL_SPEED_FRONT_L_START_BYTE 0 #define WHEEL_SPEED_FRONT_L_END_BYTE 1 @@ -132,3 +142,4 @@ #define IMU_DATA_END_BYTE 2 #define IMU_TIME_START_BYTE 3 #define IMU_TIME_END_BYTE 5 +#endif /* CANID_H */ \ No newline at end of file diff --git a/FormatSensorInfo.py b/can_mgmt/FormatSensorInfo.py similarity index 63% rename from FormatSensorInfo.py rename to can_mgmt/FormatSensorInfo.py index 749aaa7..42f4ddc 100644 --- a/FormatSensorInfo.py +++ b/can_mgmt/FormatSensorInfo.py @@ -3,12 +3,13 @@ parser = CANParser() - -# When run as main, output current sensor CSV data to a format -# for pasting into the Wiki +# Display formatted text for either pasting into google sheets +# or on the wiki for reference. if __name__ == '__main__': if len(sys.argv) >= 2: + # Output for google sheets with the following output: + # Name, ID, , SensorName (ByteN), SensorName (ByteN-1), ... if sys.argv[1] == 'sheets': outputList = {} @@ -37,25 +38,26 @@ for canID in sorted(outputList): print(outputList[canID]) + # Outputs all data in the wiki markdown format for a table + # ID, Name, [MSB:LSB] elif sys.argv[1] == 'wiki': print("{| class=\"wikitable\"") print("|-") print("!ID") print("!Name") print("![MSB:LSB]") - print("!Comments") print("|-") - for row in sensor_file: - if row: - - row['id'] = (row['id'].strip()) - row['LSB'] = int(row['LSB'].strip()) - row['MSB'] = int(row['MSB'].strip()) - row['name'] = row['name'].strip() - - print("|", row['id']) - print("|", row['name']) - print("|[" + str(row['MSB']) + ":" + str(row['LSB'])+"]") - print("|", row['comments']) + for canIDDec in parser.sensorLib: + idEntry = parser.sensorLib[canIDDec] + canID = hex(canIDDec) + + for sensorName in idEntry: + sensorData = idEntry[sensorName] + msb = int(sensorData[0]) + lsb = int(sensorData[1]) + + print("|", canID) + print("|", sensorName) + print("|[" + str(msb) + ":" + str(lsb)+"]") print("|-") print("|}") diff --git a/can_mgmt/GenerateHeaders.py b/can_mgmt/GenerateHeaders.py new file mode 100644 index 0000000..ee13cc2 --- /dev/null +++ b/can_mgmt/GenerateHeaders.py @@ -0,0 +1,37 @@ +import pandas as pd +import datetime as dt + +sensors_df = pd.read_csv("SensorDict.csv", sep=",", header=0, dtype="str") +savefile = open("CANID.h", "w+") + +header = """/* Generated on {} +* This file contains all of the constants that are +* being used on the CAN bus. +* +* Each message has an ID with specified sensors that +* lie on each message from bytes *_START_BYTE to +* *_END_BYTE. +*/ + +#ifndef CANID_H +#define CANID_H""".format(str(dt.datetime.now())) + +savefile.write(header) + +last_ID = "" +for index, row in sensors_df.iterrows(): + sensor_name_and_type = row["name"] + last_underscore = sensor_name_and_type.rindex("_") + assert last_underscore != -1, "Names not formatted correctly" + sensor_name = sensor_name_and_type[0:last_underscore] + ID = row["id"] + if last_ID != ID: + savefile.write("\n#define {}_CAN_ID {}\n".format(sensor_name, ID)) + savefile.write("#define {}_START_BYTE {}\n".format(sensor_name_and_type, + row["MSB"])) + savefile.write("#define {}_END_BYTE {}\n".format(sensor_name_and_type, + row["LSB"])) + last_ID = ID + +savefile.write("#endif /* CANID_H */") +savefile.close() diff --git a/can_mgmt/SavedGoogleDrive.csv b/can_mgmt/SavedGoogleDrive.csv new file mode 100644 index 0000000..7b588f3 --- /dev/null +++ b/can_mgmt/SavedGoogleDrive.csv @@ -0,0 +1,55 @@ +id,MSB,LSB,name +0x500,0,0,Raw Throttle Value 1 +0x500,1,1,Throttle Value +0x500,2,3,Brake Value +0x700,0,1,WHEEL_SPEED_FRONT_L +0x700,2,3,WHEEL_SPEED_FRONT_R +0x701,0,1,WHEEL_SPEED_REAR_L +0x701,2,3,WHEEL_SPEED_REAR_R +0x710,0,1,TIRE_TEMP_FRONT_R +0x710,2,3,TIRE_TEMP_FRONT_L +0x711,0,1,TIRE_TEMP_REAR_R +0x711,2,3,TIRE_TEMP_REAR_L +0x720,0,1,COOLANT_MOTOR +0x720,2,3,COOLANT_FLOW +0x720,4,5,COOLANT_RAD +0x720,6,7,COOLANT_MC +0x730,0,1,SHOCK_POT_FRONT_L +0x730,2,3,SHOCK_POT_FRONT_R +0x731,0,1,SHOCK_POT_REAR_R +0x731,2,3,SHOCK_POT_REAR_L +0x740,0,1,DROP_LINK_LEFT +0x740,2,3,DROP_LINK_RIGHT +0x750,0,1,STEER_ROD_FRONT_L +0x750,2,3,PUSHROD_FRONT_R +0x750,4,5,PUSHROD_FRONT_L +0x750,6,7,STEER_ROD_FRONT_R +0x760,0,1,PUSHROD_REAR_R +0x760,2,3,TIEROD_REAR_L +0x760,4,5,TIEROD_REAR_R +0x760,6,7,PUSHROD_REAR_L +0x770,0,1,LCA_FRONT_LB +0x770,2,3,LCA_FRONT_RF +0x770,4,5,LCA_FRONT_LF +0x770,6,7,LCA_FRONT_RB +0x780,0,1,LCA_REAR_LF +0x780,2,3,LCA_REAR_RF +0x780,4,5,LCA_REAR_LB +0x780,6,7,LCA_REAR_RB +0x790,0,1,UCA_FRONT_RB +0x790,2,3,UCA_FRONT_LB +0x790,4,5,UCA_FRONT_RF +0x790,6,7,UCA_FRONT_LF +0x7a0,0,1,UCA_REAR_LF +0x7a0,2,3,UCA_REAR_RB +0x7a0,4,5,UCA_REAR_LB +0x7a0,6,7,UCA_REAR_RF +0x7b0,0,1,FRONT_TORSIONAL_ARB +0x7b1,0,1,REAR_TORSIONAL_ARB +0x7c0,0,1,STEERING_TORSION +0x7c0,2,3,STEERING_ANGLE +0x7d0,0,0,IMU_TYPE +0x7d0,1,2,IMU_DATA +0x7d0,3,5,IMU_TIME +0x7E0,0,0,Wheel speed +0x7E0,1,1,LCA diff --git a/SensorData.csv b/can_mgmt/SensorData.csv similarity index 100% rename from SensorData.csv rename to can_mgmt/SensorData.csv diff --git a/SensorDict.csv b/can_mgmt/SensorDict.csv similarity index 100% rename from SensorDict.csv rename to can_mgmt/SensorDict.csv diff --git a/SensorDict.py b/can_mgmt/SensorDict.py similarity index 100% rename from SensorDict.py rename to can_mgmt/SensorDict.py diff --git a/update-SensorDict.py b/can_mgmt/update-SensorDict.py similarity index 70% rename from update-SensorDict.py rename to can_mgmt/update-SensorDict.py index fef4d18..cfe6f30 100644 --- a/update-SensorDict.py +++ b/can_mgmt/update-SensorDict.py @@ -1,5 +1,6 @@ import requests import pandas as pd +import math # Maybe you can do this with the csv library # But Pandas is way easier to use @@ -33,39 +34,35 @@ # Drop example row: content_df = content_df.drop(0) -content_df = content_df.set_index("ID", drop=False) +content_df = content_df.set_index("ID", drop=True) # New dataframe for output -output_df = pd.DataFrame(None, columns=["id", "MSB", "LSB", - "name", "comments"]) +output_df = pd.DataFrame(None, columns=["id", "MSB", "LSB", "name"]) -# Now iterate through each cell -for row in content_df["ID"]: - lastcell = None +for index, row in content_df.iterrows(): + last_cell = None + if row.empty: + print("Unable to parse " + str(row)) + continue for column in bytes_list: - item = str(content_df.loc[row, column]) - # Ignore bytes with no assigned name + item = str(row[column]) if(item == "nan"): continue - # Find location of parenthesis paren_start = item.find("(") # If there is an opening parenthesis, remove all text after it if(paren_start != -1): item = item[0:paren_start-1] # When two adjacent bytes are the same, increment least sig. byte - if(item == lastcell): + if(item == last_cell): new_lsb = int(output_df.loc[output_df.index[-1], "LSB"]) + 1 output_df.at[output_df.index[-1], "LSB"] = str(new_lsb) # Otherwise create new row in the csv with the byte number else: byte_num = column[4] - comment = str(content_df.loc[row, "Comments"]) - # The first byte of a sensor gets any comments in the spreadsheet - comment = None if byte_num != "0" or comment == "nan" else comment - output_df = output_df.append({"id": row, "MSB": byte_num, - "LSB": byte_num, "name": item, - "comments": comment}, + output_df = output_df.append({"id": index, "MSB": byte_num, + "LSB": byte_num, "name": item}, ignore_index=True) - lastcell = item + last_cell = item + # Save new output file. output_df.to_csv(output_file, index=None, header=True) diff --git a/clear_logs.sh b/clear_logs.sh new file mode 100644 index 0000000..54205ac --- /dev/null +++ b/clear_logs.sh @@ -0,0 +1 @@ +ssh pi@192.168.4.1 rm -rf /home/pi/WT_Server/logs/*.txt \ No newline at end of file diff --git a/download.sh b/download.sh new file mode 100644 index 0000000..dfb4dba --- /dev/null +++ b/download.sh @@ -0,0 +1 @@ +pscp -pw "PER2018!" -r pi@192.168.4.1:/home/pi/WT_Server/logs ./ \ No newline at end of file diff --git a/pyqt_dashboard/CANWebsocketClient.py b/pyqt_dashboard/CANWebsocketClient.py new file mode 100644 index 0000000..36a4935 --- /dev/null +++ b/pyqt_dashboard/CANWebsocketClient.py @@ -0,0 +1,158 @@ +import websocket +import json +import datetime as dt +import logging +from datetime import datetime +import os +import random + + +def mc_parse_A5(can_id, data, time_pi): + return { + 'id': can_id, + 'angle': int(data[:4], 16), + 'speed': int(data[4:8], 16), + 'e_out_freq': int(data[8:10], 16), + 'delta': int(data[10:12], 16), + 'parsed': True, + 'timestamp': time_pi + } + + +def mc_parse_A8(can_id, data, time_pi): + return { + 'id': can_id, + 'flux_command': int(data[:4], 16), + 'est_flux': int(data[4:8], 16), + 'id_fdbk': int(data[8:10], 16), + 'iq_fdbk': int(data[10:12], 16), + 'parsed': True, + 'timestamp': time_pi + } + + +def mc_parse_AC(can_id, data, time_pi): + fdbk = int(data[4:8], 16) + + # Feedback torque is sometimes negative + if fdbk > 65535 * 0.9: + fdbk -= 65535 + + return { + 'id': can_id, + 'cmd_torque': int(data[:4], 16), + 'fdbk_torque': fdbk, + 'parsed': True, + 'timestamp': time_pi + } + + +def mc_parse_AD(can_id, data, time_pi): + return { + 'id': can_id, + 'mod_indx': int(data[:4], 16), + 'flx_weak': int(data[4:8], 16), + 'id_cmd': int(data[8:10], 16), + 'iq_cmd': int(data[10:12], 16), + 'parsed': True, + 'timestamp': time_pi + } + + +def pedalbox2_parse(can_id, data, time_pi): + return { + 'id': can_id, + 'throttle_1': int(data[:4], 16)* random.random(), + 'throttle_2': int(data[4:8], 16), + 'brake_1': int(data[8:12], 16) * random.random(), + 'brake_2': int(data[12:16], 16), + 'fancy': int(data[12:16], 16) * random.random(), + 'parsed': True, + 'timestamp': time_pi + } + + +def pedalbox1_parse(can_id, data, time_pi): + return { + 'id': can_id, + 'throttle_value': int(data[:4], 16), + 'brake_value': int(data[4:8], 16), + 'parsed': True, + 'timestamp': time_pi + } + + +def no_parse_function_found(can_id, data, time_pi): + # print("No parse function found for " + str(can_id) + " with data: " + + # str(data)) + return {'id': can_id, 'data': data, 'parsed': False} + + +def parse_CAN_frame(can_id, data, time): + id_parse_functions = { + '500': pedalbox1_parse, + '501': pedalbox2_parse, + '0A5': mc_parse_A5, + '0A8': mc_parse_A8, + '0AC': mc_parse_AC, + '0AD': mc_parse_AD, + } + + parse_function = id_parse_functions.get( + can_id, + no_parse_function_found) + return parse_function(can_id, data, time) + + +class CANWebsocketClient(): + def __init__(self, callback, on_close, debug=False): + self.callback = callback + self.on_close = on_close + self.is_debug = debug + self.ws = None + + def on_message(self, message): + data = json.loads(message) + + # When running from localhost, messages are sent in strings, + # not JSON objects. They need to be parsed yet again... + if self.is_debug and isinstance(data, str): + data = json.loads(data) + if 't' not in data: + return + + # If type is data + if data['t'] == 'd': + payload = data['p'] + self.onRecvData(payload) + + def sendCANMEssage(self, m_id, message): + message_len = str(len(message) / 2) + message_fmt = 't' + m_id + message_len + message + print("Sending message:", message_fmt) + + # self.ws.send(message) + + def onRecvData(self, payload): + self.callback(payload) + + def on_error(self, ws, error): + print(error) + + def start(self, address): + if self.ws is not None: + self.ws.close() + self.ws = None + + self.ws = websocket.WebSocketApp(address, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close) + self.ws.run_forever() + + def parseRawMessage(self, message, time): + can_frame_data = parse_CAN_frame( + message['i'], + message['m'], time) + + return can_frame_data diff --git a/pyqt_dashboard/CAN_Logger.py b/pyqt_dashboard/CAN_Logger.py new file mode 100644 index 0000000..44d2b22 --- /dev/null +++ b/pyqt_dashboard/CAN_Logger.py @@ -0,0 +1,50 @@ +import os.path +from datetime import datetime +import time +import sys + +# start_date_time = datetime.now().strftime("%Y_%m_%d_%H_%M_%S") + +# start_path = os.path.abspath(os.path.dirname(__file__)) +# log_path = os.path.join(start_path, 'logs', +# start_date_time + '.txt') + +# with open(log_path, 'a+') as logfile: +# logfile.write(start_date_time) + +# # Max resolution (ms) for data frames to be logged +# max_time_diff_ms = 1000 +# start_time = int(round(time.time() * 1000)) +# last_time = start_time +# multi_frame_message = '' + +# total_size = 0 + + +# Logs a set of CAN frames in a time interval to a log file +def log_CAN_data(can_id, message): + return + current_time = int(round(time.time() * 1000)) + + global start_time + + # Wait for serial connection to stabilize, avoid garbage data + if current_time - start_time < 1500: + return + + global last_time, multi_frame_message, log_path, total_size + + delta_time = current_time - last_time + single_frame = ';' + str(hex(can_id)) + ',' + str(message) + + if(delta_time >= max_time_diff_ms): + with open(log_path, 'a+') as logfile: + logfile.write("\n" + multi_frame_message) + last_time = current_time + + total_size += sys.getsizeof(multi_frame_message) + print(total_size) + + multi_frame_message = str(current_time - start_time) + single_frame + else: + multi_frame_message += single_frame diff --git a/pyqt_dashboard/DataReadouts.py b/pyqt_dashboard/DataReadouts.py new file mode 100644 index 0000000..71e4e68 --- /dev/null +++ b/pyqt_dashboard/DataReadouts.py @@ -0,0 +1,308 @@ +import pyqtgraph as pg +from PyQt5.QtWidgets import QTableWidgetItem, QTableWidget, QLabel, \ + QVBoxLayout, QHBoxLayout, QWidget, QPushButton,\ + QGridLayout, QHeaderView +from PyQt5.QtCore import QObject, pyqtSignal, Qt, QTimer +from PyQt5.QtGui import QStandardItemModel, QStandardItem, QPainter, QColor, \ + QPalette, QGraphicsView +import time +from CAN_Logger import * + + +class DataSpec(): + def __init__(self, m_id, xVal_key, yVal_key, + line_color='r', rawData=False): + self.id = m_id + self.xVal_key = xVal_key + self.yVal_key = yVal_key + + self.xVals = [] + self.yVals = [] + + self.maxPoints = 150 + + self.pen = pg.mkPen(width=4.5, color=line_color) + self.rawData = rawData + + def addXYPoint(self, point): + x, y = point + self.xVals.append(x) + self.yVals.append(y) + + self.xVals = self.xVals[-self.maxPoints:] + self.yVals = self.yVals[-self.maxPoints:] + + def isValidData(self, data): + return (data['i'] == self.id and self.rawData) or \ + (data['i'] == self.id and + self.xVal_key in data and + self.yVal_key in data) + + def isSemiValidData(self, data): + return (data['i'] == self.id and self.rawData) + + +class DataModule(QObject): + def __init__(self, dataSpecs): + super().__init__() + self.title = "N/A" + self.dataSpecs = dataSpecs + + def addDataPoint(self, *args): + raise NotImplementedError('subclasses must override addDataPoint()!') + + def getDataSpecs(self): + return self.dataSpecs + + +def timestamp(): + return time.time() + + +class TimeAxisItem(pg.AxisItem): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setLabel(text='Time', units=None) + self.enableAutoSIPrefix(False) + + def tickStrings(self, values, scale, spacing): + try: + vals = [datetime.datetime.fromtimestamp(value).strftime("%H:%M:%S") + for value in values] + return vals + except Exception: + return[] + + +class TimeGraph(DataModule): + def __init__(self, title, lineSpecs, data_range): + super().__init__(lineSpecs) + self.plotItem = pg.PlotWidget(title=title, axisItems={'bottom': + TimeAxisItem(orientation='bottom')}) + + self.maxPoints = 100 + self.updateRate = 1/20 + self.lastUpdate = time.time() + self.title = title + + self.plotItem.setYRange(*data_range) + + self.lines = {} + self.plotItem.addLegend() + for spec in lineSpecs: + self.lines[spec] = self.plotItem.plot([], + pen=spec.pen, + name=spec.yVal_key, + autoDownsample=True) + + self.plotItem.setMouseEnabled(x=False, y=False) + self.plotItem.showGrid(x=False, y=True, alpha=0.5) + + self.isPaused = False + self.plotItem.scene().sigMouseClicked.connect(self.togglePaused) + + def togglePaused(self): + self.isPaused = not self.isPaused + + def updateGraph(self): + if not self.isPaused: + for spec, line in self.lines.items(): + line.setData(spec.xVals, spec.yVals) + + def addDataPoint(self, data): + needsUpdate = False + + for spec in self.lines.keys(): + x = data[spec.xVal_key] + y = data[spec.yVal_key] + spec.addXYPoint((x, y)) + needsUpdate = True + + if needsUpdate and self.lastUpdate + self.updateRate < time.time(): + self.lastUpdate = time.time() + self.updateGraph() + + def getWidget(self): + return self.plotItem + + +class RawText(DataModule): + def __init__(self, title, spec): + super().__init__([spec]) + self.title = title + self.text = QLabel() + self.text.setText(title) + self.spec = spec + + def addDataPoint(self, data): + value = "N/A" + if self.spec.rawData: + value = data['data'] + else: + value = data[self.spec.xVal_key] + + self.text.setText(self.title + str(value)) + + def getWidget(self): + return self.text + + +class CANTable(DataModule): + update_table = pyqtSignal(object) + + def __init__(self): + super().__init__([]) + self.table = QTableWidget(0, 0) + self.table.verticalHeader().setVisible(False) + + col_headers = ['ID'] + for i in range(0, 8): + col_headers.append('Byte ' + str(i)) + col_headers.append('Count') + + self.table.setColumnCount(len(col_headers)) + self.table.setHorizontalHeaderLabels(col_headers) + + header = self.table.horizontalHeader() + header.setSectionResizeMode(1) + + self.rawDataList = {} + self.rawDataCount = {} + self.updateRate = 1/20 + self.nextUpdate = time.time() + self.updateRate + + def addDataPoint(self, raw_data): + m_id = int(raw_data['i'], 16) + + log_CAN_data(m_id, raw_data['m']) + + if m_id not in self.rawDataCount: + self.rawDataCount[m_id] = 0 + + self.rawDataCount[m_id] = self.rawDataCount[m_id] + 1 + + self.rawDataList[m_id] = raw_data + sortedDataList = sorted(self.rawDataList) + + self.table.setRowCount(len(sortedDataList)) + + all_rows = [] + + for row_index, data_key in enumerate(sortedDataList): + + raw_data_point = self.rawDataList[data_key] + message = raw_data_point['m'] + byte_length = len(message) + + row_cells = [] + + QTableWidgetItem('0x' + raw_data_point['i']) + row_cells.insert(0, QTableWidgetItem('0x' + raw_data_point['i'])) + + for i in range(0, 8): + cell_item = QTableWidgetItem(message[i*2: i*2 + 2]) + if i >= byte_length: + cell_item = QTableWidgetItem("-") + row_cells.insert(i+1, cell_item) + + cur_id = int(raw_data_point['i'], 16) + count = self.rawDataCount[cur_id] + row_cells.insert(9, QTableWidgetItem(str(count))) + + all_rows.append(row_cells) + if time.time() > self.nextUpdate: + self.nextUpdate = time.time() + self.updateRate + self.update_table.emit(all_rows) + + def getWidget(self): + return self.table + + +class LightToggle(QPushButton): + def __init__(self, text, parent=None): + QWidget.__init__(self, parent=None) + self.isEnabled = -1 + + self.maxRate = 100/1000 + self.lastTime = time.time() + self.setEnabled(1) + + def setEnabled(self, enabled): + if enabled == self.isEnabled: + return + + if self.lastTime + self.maxRate > time.time(): + self.lastTime = time.time() + + self.isEnabled = enabled + + if self.isEnabled: + self.setStyleSheet("background-color: green") + else: + self.setStyleSheet("background-color: red") + + +class LightArray(DataModule): + def __init__(self, dataSpecs): + super().__init__(dataSpecs) + self.vBox = QVBoxLayout() + self.grid = QGridLayout() + self.buttons = {} + + for i, spec in enumerate(dataSpecs): + btn = LightToggle(spec.xVal_key) + label = QLabel(spec.xVal_key) + + btn.setFixedWidth(50) + btn.setFixedHeight(50) + label.setFixedWidth(60) + label.setFixedHeight(50) + + self.grid.addWidget(label, i, 1) + self.grid.addWidget(btn, i, 2) + self.buttons[spec] = btn + + self.vBox.addLayout(self.grid) + self.vBox.addStretch() + + def addDataPoint(self, data): + for spec, btn in self.buttons.items(): + if (data['id'] == spec.id): + btn.setEnabled(data[spec.yVal_key] > 0) + + def getWidget(self): + return self.vBox + + +class DataModuleManager(): + def __init__(self): + + self.rawModules = [] + self.subscribedIDs = {} + + self.rawTypes = (CANTable, RawText) + self.parsedTypes = (TimeGraph, RawText, LightArray) + + def manageModule(self, newModule): + + if isinstance(newModule, self.rawTypes): + self.rawModules.append(newModule) + + for spec in newModule.getDataSpecs(): + if spec.id not in self.subscribedIDs: + self.subscribedIDs[spec.id] = [newModule] + else: + self.subscribedIDs[spec.id].append(newModule) + + # Called whenever new data is to be pushed + # any graph which is being displayed + def onRawDataCallback(self, data): + for module in self.rawModules: + module.addDataPoint(data) + + # Called whenever new data is to be pushed + # any graph which is being displayed + def onParsedDataCallback(self, target_id, data): + if target_id in self.subscribedIDs: + for module in self.subscribedIDs[target_id]: + module.addDataPoint(data) diff --git a/pyqt_dashboard/Layouts.py b/pyqt_dashboard/Layouts.py new file mode 100644 index 0000000..c55c232 --- /dev/null +++ b/pyqt_dashboard/Layouts.py @@ -0,0 +1,149 @@ +from PyQt5.QtGui import QWidget, QGridLayout, QLineEdit + +from PyQt5.QtGui import QHBoxLayout, QPushButton +from DataReadouts import * +import pyqtgraph as pg + + +# Master class for all readout layouts +# Contains methods for easily adding data readouts +# in multiple contexts +class ReadoutLayout(QWidget): + def __init__(self, moduleManager, parent=None): + super().__init__(parent=parent) + self.masterLayout = QGridLayout() + self.setLayout(self.masterLayout) + self.moduleManager = moduleManager + + # Create and place new graph in a specific location + def placeNewGraph(self, title, dataSpec, position, data_range=(0, 5000)): + newGraph = TimeGraph(title, dataSpec, data_range) + self.moduleManager.manageModule(newGraph) + self.masterLayout.addWidget(newGraph.getWidget(), *position) + return newGraph + + def placeText(self, title, spec, position): + newText = RawText(title, spec) + self.moduleManager.manageModule(newText) + self.masterLayout.addWidget(newText.getWidget(), *position) + return newText + + def placeCANTable(self): + table = CANTable() + self.moduleManager.manageModule(table) + self.masterLayout.addWidget(table.getWidget(), 2, 1) + return table + + def placeCANSender(self): + id_field = QLineEdit() + id_field.setFixedWidth(100) + id_field.setPlaceholderText("CAN ID") + + message_field = QLineEdit() + message_field.setFixedWidth(300) + message_field.setPlaceholderText("Message") + + send_button = QPushButton("Send") + send_button.setFixedWidth(80) + + def sendMessage(): + print(id_field.text()) + # TODO Send message to server to broadcast on CAN + pass + + send_button.clicked.connect(sendMessage) + + layout = QHBoxLayout() + layout.addWidget(id_field) + layout.addWidget(message_field) + layout.addWidget(send_button) + layout.addStretch() + + self.masterLayout.addLayout(layout, 1, 1) + + def placeLightArray(self, specs, position): + lights = LightArray(specs) + self.moduleManager.manageModule(lights) + self.masterLayout.addLayout(lights.getWidget(), *position) + + +class CriticalLayout(ReadoutLayout): + def __init__(self, moduleManager, parent=None): + super().__init__(moduleManager, parent=parent) + + SDC = [ + DataSpec('501', 'IMD', 'throttle_1'), + DataSpec('501', 'BMS', 'brake_1'), + DataSpec('501', 'BSPD', 'brake_1'), + DataSpec('501', 'INRT', 'brake_1'), + DataSpec('501', 'Front', 'brake_1'), + DataSpec('501', 'Main', 'brake_1'), + DataSpec('501', 'Right', 'brake_1'), + DataSpec('501', 'Left', 'brake_1'), + DataSpec('501', 'HVD', 'brake_1'), + DataSpec('501', 'TSMS', 'brake_1') + ] + self.placeLightArray(SDC, (1, 1)) + + throttleLine = [ + DataSpec('501', 'timestamp', 'throttle_1'), + DataSpec('501', 'timestamp', 'throttle_2') + ] + self.placeNewGraph("Throttle 1", throttleLine, (1, 2)) + + +class TuningLayout(ReadoutLayout): + def __init__(self, moduleManager, parent=None): + super().__init__(moduleManager, parent=parent) + + torque = [ + DataSpec('0AC', 'timestamp', 'cmd_torque'), + DataSpec('0AC', 'timestamp', 'fdbk_torque', line_color='w'), + DataSpec('0A5', 'timestamp', 'speed', line_color='g') + ] + self.placeNewGraph("Torque Command", torque, (1, 1, 1, 2), + data_range=(0, 150000 / 2)) + + iDGraph = [ + DataSpec('0AD', 'timestamp', 'id_cmd'), + DataSpec('0A8', 'timestamp', 'id_fdbk', line_color='w') + ] + self.placeNewGraph("ID Command", iDGraph, (2, 1), data_range=(0, 30)) + + iQGraph = [ + DataSpec('0AD', 'timestamp', 'iq_cmd'), + DataSpec('0A8', 'timestamp', 'iq_fdbk', line_color='w') + ] + self.placeNewGraph("IQ Command", iQGraph, (2, 2), data_range=(0, 30)) + + +class DynamicsLayout(ReadoutLayout): + def __init__(self, moduleManager, parent=None): + super().__init__(moduleManager, parent=parent) + + brakeLine = [ + DataSpec('501', 'timestamp', 'brake_1'), + DataSpec('501', 'timestamp', 'brake_2', line_color='w') + ] + self.placeNewGraph("Brake", brakeLine, (1, 1)) + + throttleLine = [ + DataSpec('501', 'timestamp', 'throttle_1'), + DataSpec('501', 'timestamp', 'throttle_2', line_color='w') + ] + self.placeNewGraph("Throttle", throttleLine, (2, 1)) + + +class CANTableLayout(ReadoutLayout): + def __init__(self, moduleManager, parent=None): + super().__init__(moduleManager, parent=parent) + + self.placeCANSender() + + self.table = self.placeCANTable() + self.table.update_table.connect(self.on_data_ready) + + def on_data_ready(self, data): + for r, row in enumerate(data): + for c, cell in enumerate(row): + self.table.table.setItem(r, c, cell) diff --git a/pyqt_dashboard/WebsocketProcess.py b/pyqt_dashboard/WebsocketProcess.py new file mode 100644 index 0000000..3fdd27d --- /dev/null +++ b/pyqt_dashboard/WebsocketProcess.py @@ -0,0 +1,45 @@ +import time +from PyQt5.QtCore import QThread +from PyQt5.QtWidgets import QInputDialog + +from CANWebsocketClient import * + + +class WebsocketProcess(QThread): + def __init__(self, dataManager, ip, isDebug=False): + QThread.__init__(self) + self.dataManager = dataManager + self.isDebug = isDebug + self.ip = ip + self.client = CANWebsocketClient( + self.appendToBuffer, + self.on_close, + self.isDebug) + self.buffer = [] + + def __del__(self): + self.wait() + + def appendToBuffer(self, data): + + # if not isinstance(data, list): + # data = [data] + for m_id in data: + message = data[m_id] + time = float(message['ts']) + + self.dataManager.onRawDataCallback(message) + + parsed = self.client.parseRawMessage(message, time) + + if parsed['parsed']: + m_id = parsed['id'] + self.dataManager.onParsedDataCallback(m_id, parsed) + + def run(self): + self.client.start(self.ip) + + def on_close(self): + self.connected = False + print("Closed!") + pass diff --git a/pyqt_dashboard/main.py b/pyqt_dashboard/main.py new file mode 100644 index 0000000..1b55cd8 --- /dev/null +++ b/pyqt_dashboard/main.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +from PyQt5.QtWidgets import QTabWidget, QVBoxLayout, QWidget, QInputDialog, \ + QApplication + +import pyqtgraph as pg +from DataReadouts import TimeGraph, DataModuleManager +from Layouts import CriticalLayout, DynamicsLayout, TuningLayout, \ + CANTableLayout + +from WebsocketProcess import * +import sys + + +# Graph manager needs to be able to recieve data +# from seperate threads to update all graphs + +DEFAULT_WS_ADDRESS = "ws://192.168.4.1:5000" +# DEFAULT_WS_ADDRESS = "ws://127.0.0.1:5000" + + +def main(): + app = QApplication([]) + + sys._excepthook = sys.excepthook + + def exception_hook(exctype, value, traceback): + print(exctype, value, traceback) + sys._excepthook(exctype, value, traceback) + sys.exit(1) + sys.excepthook = exception_hook + + dmm = DataModuleManager() + # Initalize main window widget + window = QWidget() + + pg.setConfigOptions(antialias=False, useOpenGL=True) + + # Initalize main layout and + tabs = QTabWidget() + layout = QVBoxLayout() + layout.addWidget(tabs) + window.setLayout(layout) + + # Add layouts to window + cl = CriticalLayout(dmm) + tabs.addTab(cl, "Critical") + + dl = DynamicsLayout(dmm) + tabs.addTab(dl, "Dynamics") + + tl = TuningLayout(dmm) + tabs.addTab(tl, "Tuning Layout") + + ct = CANTableLayout(dmm) + tabs.addTab(ct, "CAN Table") + + # Display window + window.show() + window.resize(1600, 600) + window.raise_() + + result, address = getWebsocketAddress(window) + + if result: + wsp = WebsocketProcess(dmm, address, isDebug=False) + wsp.start() + else: + sys.exit() + + app.exec_() + + +def getWebsocketAddress(window): + result = False + address = DEFAULT_WS_ADDRESS + + while not result: + address, result = QInputDialog.getText(window, 'Disconnected', + 'Enter WS address', + text=DEFAULT_WS_ADDRESS) + return (result, address) + + +if __name__ == "__main__": + main() diff --git a/serial-read.py b/serial-read.py deleted file mode 100644 index 7b72f8c..0000000 --- a/serial-read.py +++ /dev/null @@ -1,31 +0,0 @@ -import serial -import time -import datetime - -ser = serial.Serial('/dev/ttyUSB0', 2500000, timeout=None) - -print(ser.write('S6\r')) # CAN Baudrate set to 500k -print(ser.write('O\r')) # Open CANdapter - -while True: - in_wait = ser.in_waiting - if(in_wait > 0): - try: - # Message format - # tIIILDDDDDDDDTTTT - # III = CAN ID - # L = Message Length - # D = Message data - # T = Timestamp - - message = ser.read_until('\r').replace('\r', '')[1:] - - m_id = message[0:3] - m_len = message[3:4] - m_message = message[4:-4] - m_time_stamp = str(datetime.datetime.now()) - print("id: " + m_id + "\t\tlen: " + m_len + - "\t\ttime_stamp: " m_time_stamp + "\tmsg: " + m_message) - - except (serial.serialutil.SerialException): - print(str(in_wait) + " !!!!") diff --git a/startup.sh b/startup.sh new file mode 100644 index 0000000..227d154 --- /dev/null +++ b/startup.sh @@ -0,0 +1,5 @@ +ssh pi@192.168.4.1 pkill screen +ssh pi@192.168.4.1 screen -ls +ssh pi@192.168.4.1 screen -S DAQ -d -m python3 "~/WT_Server/Offline_DAQ.py" +ssh pi@192.168.4.1 ps -u $USER +ssh pi@192.168.4.1 screen -ls \ No newline at end of file diff --git a/stop_daq.sh b/stop_daq.sh new file mode 100644 index 0000000..45771bb --- /dev/null +++ b/stop_daq.sh @@ -0,0 +1 @@ +ssh pi@192.168.4.1 pkill screen \ No newline at end of file diff --git a/upload.sh b/upload.sh new file mode 100644 index 0000000..5eea483 --- /dev/null +++ b/upload.sh @@ -0,0 +1 @@ +pscp -pw "PER2018!" -r WT_Server pi@192.168.4.1:/home/pi \ No newline at end of file