diff --git a/software/firmware/src/peripherals/include/imu.h b/software/firmware/src/peripherals/include/imu.h index 1926a38c..3c7b04d8 100644 --- a/software/firmware/src/peripherals/include/imu.h +++ b/software/firmware/src/peripherals/include/imu.h @@ -13,7 +13,7 @@ // For burst data transfer #define BURST_READ_BASE_ADDR BNO055_GYRO_DATA_X_LSB_ADDR -#define BURST_READ_LEN 38 +#define BURST_READ_LEN 40 #define GYRO_DATA_LEN 6 #define ACC_DATA_LEN 6 diff --git a/software/firmware/src/tasks/app_task_ranging.c b/software/firmware/src/tasks/app_task_ranging.c index e3457077..fc576862 100644 --- a/software/firmware/src/tasks/app_task_ranging.c +++ b/software/firmware/src/tasks/app_task_ranging.c @@ -231,6 +231,7 @@ static void imu_burst_data_handler(uint8_t *burst_data_buffer) const bno055_data_type_t data_types[] = {STAT_DATA,LACC_DATA,GYRO_DATA}; uint8_t index = 0; uint8_t len = 0; + print("got imu data!\n"); #ifndef _TEST_NO_STORAGE for (uint8_t i = 0; i < sizeof(data_types)/sizeof(data_types[0]); i+=1) @@ -330,6 +331,7 @@ void app_allow_downloads(bool allow) // Enable data downloading from ranging mode if (allow) { + print("allowing downloads...\n"); // Disable writing to storage storage_disable(true); storage_enter_maintenance_mode(); @@ -385,6 +387,7 @@ void AppTaskRanging(void *uid) //imu_register_motion_change_callback(motion_change_handler); imu_register_data_ready_callback(imu_burst_data_handler); imu_set_power_mode(POWER_MODE_NORMAL); + //imu_set_power_mode(POWER_MODE_LOWPOWER); imu_set_fusion_mode(OPERATION_MODE_NDOF); #endif diff --git a/software/firmware/tests/Makefile b/software/firmware/tests/Makefile index ee6b39eb..c568b347 100644 --- a/software/firmware/tests/Makefile +++ b/software/firmware/tests/Makefile @@ -310,7 +310,7 @@ full_segger: $(CONFIG) $(CONFIG)/main.o $(CONFIG)/SEGGER_RTT.o $(CONFIG)/$$(TARG full_exp: TARGET = TestFullExp full_exp: SRC += main.c SEGGER_RTT.c -full_exp: CFLAGS += -D__USE_FREERTOS__ -D__USE_SEGGER__ -DNONBLOCKING=1 -D_TEST_IMU_DATA -D_REMOTE_MODE_SWITCH_ENABLED -D_TEST_NO_EXP_DETAILS -D_USE_DEFAULT_EXP_DETAILS +full_exp: CFLAGS += -D__USE_FREERTOS__ -D__USE_SEGGER__ -DNONBLOCKING=1 -D_TEST_IMU_DATA -D_REMOTE_MODE_SWITCH_ENABLED -D_TEST_NO_EXP_DETAILS -D_USE_DEFAULT_EXP_DETAILS -D_LIVE_IMU_DATA full_exp: $(CONFIG) $(CONFIG)/main.o $(CONFIG)/SEGGER_RTT.o $(CONFIG)/$$(TARGET).bin program imu: TARGET = TestIMU diff --git a/software/management/dashboard/experimental_tottag.py b/software/management/dashboard/experimental_tottag.py new file mode 100755 index 00000000..22e015a7 --- /dev/null +++ b/software/management/dashboard/experimental_tottag.py @@ -0,0 +1,956 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# PYTHON INCLUSIONS --------------------------------------------------------------------------------------------------- + +from functools import partial +from bleak import BleakClient, BleakScanner +from tkinter import ttk, filedialog +from collections import defaultdict +import struct, queue, datetime, tzlocal +import os, pickle, pytz, time +import traceback +import tkinter as tk +import tkcalendar +import threading +import asyncio +import argparse + + +# CONSTANTS AND DEFINITIONS ------------------------------------------------------------------------------------------- + +DEVICE_ID_UUID = '00002a23-0000-1000-8000-00805f9b34fb' +LOCATION_SERVICE_UUID = 'd68c3156-a23f-ee90-0c45-5231395e5d2e' +FIND_MY_TOTTAG_SERVICE_UUID = 'd68c3155-a23f-ee90-0c45-5231395e5d2e' +MODE_SWITCH_UUID = 'd68c3164-a23f-ee90-0c45-5231395e5d2e' +TIMESTAMP_SERVICE_UUID = 'd68c3154-a23f-ee90-0c45-5231395e5d2e' +VOLTAGE_SERVICE_UUID = 'd68c3153-a23f-ee90-0c45-5231395e5d2e' +EXPERIMENT_SERVICE_UUID = 'd68c3161-a23f-ee90-0c45-5231395e5d2e' +MAINTENANCE_COMMAND_SERVICE_UUID = 'd68c3162-a23f-ee90-0c45-5231395e5d2e' +MAINTENANCE_DATA_SERVICE_UUID = 'd68c3163-a23f-ee90-0c45-5231395e5d2e' + +MAINTENANCE_NEW_EXPERIMENT = 0x01 +MAINTENANCE_DELETE_EXPERIMENT = 0x02 +MAINTENANCE_DOWNLOAD_LOG = 0x03 +MAINTENANCE_SET_LOG_DOWNLOAD_DATES = 0x04 +MAINTENANCE_DOWNLOAD_COMPLETE = 0xFF + +FIND_MY_TOTTAG_ACTIVATION_SECONDS = 10 +MAX_RANGING_DISTANCE_MM = 16000 +MAX_LABEL_LENGTH = 16 +MAX_NUM_DEVICES = 10 + +STORAGE_TYPE_VOLTAGE = 1 +STORAGE_TYPE_CHARGING_EVENT = 2 +STORAGE_TYPE_MOTION = 3 +STORAGE_TYPE_RANGES = 4 +STORAGE_TYPE_IMU = 5 + +IMU_DATA_LEN = 13 + +BATTERY_CODES = defaultdict(lambda: 'Unknown Battery Event') +BATTERY_CODES[1] = 'Plugged' +BATTERY_CODES[2] = 'Unplugged' +BATTERY_CODES[3] = 'Charging' +BATTERY_CODES[4] = 'Not Charging' + + +# HELPER FUNCTIONS ---------------------------------------------------------------------------------------------------- + +async def ble_command_sender(message_queue, command): + await message_queue.put(command) + +def ble_issue_command(event_loop, message_queue, command): + asyncio.run_coroutine_threadsafe(ble_command_sender(message_queue, command), event_loop) + +def get_download_directory(): + if os.name == 'nt': + import winreg + sub_key = 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders' + downloads_guid = '{374DE290-123F-4565-9164-39C4925E467B}' + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key: + location = winreg.QueryValueEx(key, downloads_guid)[0] + return location + else: + return os.path.join(os.path.expanduser('~'), 'Downloads') + +def validate_time(new_val): + return (len(new_val) == 0) or \ + (len(new_val) == 1 and new_val.isnumeric()) or \ + (len(new_val) == 2 and (new_val[-1] == ':' or (new_val[-1].isnumeric() and int(new_val) < 24))) or \ + (len(new_val) == 3 and ((new_val[-2] != ':' and new_val[-1] == ':') or (new_val[-2] == ':' and new_val[-1].isnumeric() and int(new_val[-1]) <= 5))) or \ + (len(new_val) == 4 and new_val[-1].isnumeric() and (new_val[-2] != ':' or int(new_val[-1]) <= 5)) or \ + (len(new_val) == 5 and new_val[-1].isnumeric() and new_val[-3] == ':') + +def pack_datetime(time_zone, date_string, time_string, daily): + if not daily: + utc_datetime = pytz.timezone(time_zone).localize(datetime.datetime.strptime(date_string + ' ' + time_string, '%m/%d/%Y %H:%M')).astimezone(pytz.utc) + timestamp = int(utc_datetime.timestamp()) + else: + offset = datetime.datetime.strptime(date_string, '%m/%d/%Y').astimezone(pytz.timezone(time_zone)).utcoffset().total_seconds() + timestamp = int((datetime.datetime.strptime(time_string, '%H:%M') - datetime.datetime.strptime('00:00', '%H:%M')).total_seconds() - offset) + return timestamp + +def unpack_datetime(time_zone, start_timestamp, timestamp): + date_string = None + if start_timestamp is None: + local_datetime = pytz.utc.localize(datetime.datetime.utcfromtimestamp(timestamp)).astimezone(pytz.timezone(time_zone)) + date_string = local_datetime.strftime('%m/%d/%Y') + time_string = local_datetime.strftime('%H:%M') + seconds_string = local_datetime.strftime('%S') + else: + offset = pytz.utc.localize(datetime.datetime.utcfromtimestamp(start_timestamp)).astimezone(pytz.timezone(time_zone)).utcoffset().total_seconds() + hours = int((timestamp + offset) / 3600) + time_string = '%02d:%02d'%(hours, int((timestamp + offset - (hours*3600)) / 60)) + seconds_string = '%02d'%(timestamp % 60) + return date_string, time_string, seconds_string + +def pack_experiment_details(data): + experiment_struct = struct.pack(' int(time.time()) or data[i] < 1 or data[i] > 5: + i += 1 + print(i) + elif timestamp_raw % 500 == 0: + #most_recent_aligned_timestamp = timestamp + if data[i] == STORAGE_TYPE_VOLTAGE: + datum = struct.unpack(' 0 and datum < 4500: + print(timestamp) + log_data[timestamp]['v'] = datum + i += 9 + saved_data_len+=9 + else: + i += 1 + print(i) + elif data[i] == STORAGE_TYPE_CHARGING_EVENT: + if data[i+5] > 0 and data[i+5] < 5: + print(timestamp) + log_data[timestamp]['c'] = BATTERY_CODES[data[i+5]] + i += 6 + saved_data_len+=6 + else: + i += 1 + print(i) + elif data[i] == STORAGE_TYPE_MOTION: + if data[i+5] == 0 or data[i+5] == 1: + print(timestamp) + log_data[timestamp]['m'] = data[i+5] > 0 + i += 6 + saved_data_len+=6 + else: + i += 1 + print(i) + elif data[i] == STORAGE_TYPE_RANGES: + log_data[timestamp]['r'] = {} + if data[i+5] < MAX_NUM_DEVICES: + for j in range(data[i+5]): + uid = data[i+6+(j*3)] + datum = struct.unpack('> 2) & 0x03) < 4) and (((reg_val >> 4) & 0x03) < 4) and (((reg_val >> 6) & 0x03) < 4): + print("imu data found", timestamp) + imu_data = data[i+5:i+5+IMU_DATA_LEN] + log_data.setdefault(most_recent_aligned_timestamp, {}).setdefault('i', []).append((timestamp, imu_data)) + i += 5 + IMU_DATA_LEN + saved_data_len+=5 + IMU_DATA_LEN + else: + i+=1 + print(i) + else: + i+=1 + print(i) + elif data[i] == STORAGE_TYPE_IMU: + reg_val = data[i+5] + if ( (reg_val & 0x03) < 4) and (((reg_val >> 2) & 0x03) < 4) and (((reg_val >> 4) & 0x03) < 4) and (((reg_val >> 6) & 0x03) < 4): + print("imu data found", timestamp) + imu_data = data[i+5:i+5+IMU_DATA_LEN] + if (timestamp>=most_recent_aligned_timestamp) and (timestamp-most_recent_aligned_timestamp<0.5): + log_data.setdefault(most_recent_aligned_timestamp, {}).setdefault('i', []).append((timestamp, imu_data)) + saved_data_len+=5 + IMU_DATA_LEN + i += 5 + IMU_DATA_LEN + else: + i+=1 + print(i) + else: + i+=1 + print(i) + except Exception: + traceback.print_exc() + print(f"len data: {len(data)}, saved: {saved_data_len}") + log_data = [dict({'t': ts}, **datum) for ts, datum in log_data.items()] + with open(os.path.join(storage_directory, uid_to_labels[from_uid] + '.pkl'), 'wb') as file: + pickle.dump(log_data, file, protocol=pickle.HIGHEST_PROTOCOL) + + +# BLUETOOTH LE COMMUNICATIONS ----------------------------------------------------------------------------------------- + +class TotTagBLE(threading.Thread): + + def __init__(self, command_queue, result_queue, event_loop): + super().__init__() + self.operations = { 'SCAN': self.scan_for_tottags, + 'CONNECT': self.connect_to_tottag, + 'DISCONNECT': self.disconnect_from_tottag, + 'SUBSCRIBE_RANGES': self.subscribe_to_ranges, + 'FIND_TOTTAG': self.find_my_tottag, + 'TIMESTAMP': self.retrieve_timestamp, + 'VOLTAGE': self.retrieve_voltage, + 'NEW_EXPERIMENT_FULL': self.create_new_experiment, + 'NEW_EXPERIMENT_SINGLE': self.update_new_experiment, + 'GET_EXPERIMENT': self.retrieve_experiment, + 'DELETE_EXPERIMENT': self.delete_experiment, + 'DOWNLOAD': self.download_logs, + 'DOWNLOAD_DONE': self.download_logs_done, + 'ENABLE_STORAGE_MAINTENANCE': self.enable_storage_maintenance, + } + self.storage_directory = get_download_directory() + self.subscribed_to_notifications = False + self.downloading_log_file = False + self.download_raw_logs = False + self.command_queue = command_queue + self.result_queue = result_queue + self.discovered_devices = {} + self.connected_device = None + self.event_loop = event_loop + self.data_details = None + self.data_length = 0 + self.data_index = 0 + self.data = None + + def run(self): + self.event_loop.run_until_complete(self.await_command()) + + async def await_command(self): + command = 'START' + while command != 'QUIT': + command = await self.command_queue.get() + await self.download_logs_done() + if self.subscribed_to_notifications: + await self.unsubscribe_from_ranges() + if command in self.operations: + await self.operations[command]() + else: + await self.disconnect_from_tottag() + self.command_queue.task_done() + + def disconnected_callback(self, _device): + self.result_queue.put_nowait(('DISCONNECTED', True)) + self.connected_device = None + + def ranges_callback(self, _sender_uuid, data): + self.result_queue.put_nowait(('RANGES', data)) + + def data_callback(self, _sender_uuid, data): + if self.data_length == 0: + if len(data) >= 4: + self.data_index = 0 + self.data_length = struct.unpack(' 1)) + await self.connected_device.stop_notify(MAINTENANCE_DATA_SERVICE_UUID) + process_tottag_data(int(self.connected_device.address.split(':')[-1], 16), self.storage_directory, self.data_details, self.data[:self.data_index], self.download_raw_logs) + except Exception as e: + print('Log file processing error:', e); + self.result_queue.put_nowait(('ERROR', ('TotTag Error', 'Unable to write log file to ' + self.storage_directory))) + + +# GUI DESIGN ---------------------------------------------------------------------------------------------------------- + +class TotTagGUI(tk.Frame): + + def __init__(self, mode_switch_visibility=False): + + # Set up the root application window + super().__init__(None) + self.master.title('TotTag Dashboard') + try: + self.master.iconphoto(True, tk.PhotoImage(file='dashboard/tottag_dashboard.png')) + except Exception: + self.master.iconphoto(True, tk.PhotoImage(file=os.path.dirname(os.path.realpath(__file__)) + '/tottag_dashboard.png')) + self.master.protocol('WM_DELETE_WINDOW', self._exit) + self.master.geometry("900x700+" + str((self.winfo_screenwidth()-900)//2) + "+" + str((self.winfo_screenheight()-700)//2)) + self.pack(fill=tk.BOTH, expand=True) + + # Create an asynchronous event loop + self.event_loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.event_loop) + + # Create all necessary shared variables + self.device_list = [] + self.failed_devices = [] + self.use_daily_times = tk.IntVar() + self.download_raw_data = tk.IntVar() + self.ble_command_queue = asyncio.Queue() + self.ble_result_queue = queue.Queue() + self.tottag_selection = tk.StringVar(self.master, 'Press "Scan for TotTags" to begin...') + self.tottag_timezone = tk.StringVar(self.master, tzlocal.get_localzone()) + self.save_directory = tk.StringVar(self.master, get_download_directory()) + self.daily_start_time = tk.StringVar(self.master, "07:00") + self.daily_end_time = tk.StringVar(self.master, "22:00") + self.start_time = tk.StringVar(self.master, "07:00") + self.end_time = tk.StringVar(self.master, "22:00") + self.start_date = tk.StringVar() + self.end_date = tk.StringVar() + self.data_length = 0 + + # Create the control bar + control_bar = tk.Frame(self) + control_bar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5, expand=False) + control_bar.columnconfigure(1, weight=1) + self.scan_button = ttk.Button(control_bar, text="Scan for TotTags", command=partial(ble_issue_command, self.event_loop, self.ble_command_queue, 'SCAN'), width=20) + self.scan_button.grid(column=0, row=0, padx=(10,0)) + self.tottag_selector = ttk.Combobox(control_bar, textvariable=self.tottag_selection, state=['readonly']) + self.tottag_selector.grid(column=1, row=0, padx=10, sticky=tk.W+tk.E) + self.connect_button = ttk.Button(control_bar, text="Connect", command=self._connect, state=['disabled']) + self.connect_button.grid(column=2, row=0) + ttk.Button(control_bar, text="Quit", command=self._exit).grid(column=3, row=0) + + # Create the operations bar + self.operations_bar = tk.Frame(self) + self.operations_bar.pack(side=tk.LEFT, fill=tk.Y, padx=5, expand=False) + ttk.Label(self.operations_bar, text="TotTag Actions", padding=6).grid(row=0) + ttk.Button(self.operations_bar, text="Subscribe to Live Ranging Data", command=self._subscribe_to_live_ranges, state=['disabled']).grid(row=1, sticky=tk.W+tk.E) + ttk.Button(self.operations_bar, text="Activate Find my TotTag", command=partial(ble_issue_command, self.event_loop, self.ble_command_queue, 'FIND_TOTTAG'), state=['disabled']).grid(row=2, sticky=tk.W+tk.E) + ttk.Button(self.operations_bar, text="Retrieve Current Timestamp", command=partial(ble_issue_command, self.event_loop, self.ble_command_queue, 'TIMESTAMP'), state=['disabled']).grid(row=3, sticky=tk.W+tk.E) + ttk.Button(self.operations_bar, text="Retrieve Battery Voltage", command=partial(ble_issue_command, self.event_loop, self.ble_command_queue, 'VOLTAGE'), state=['disabled']).grid(row=4, sticky=tk.W+tk.E) + self.schedule_button = ttk.Button(self.operations_bar, text="Schedule New Pilot Deployment", command=self._create_new_experiment, state=['disabled']) + self.schedule_button.grid(row=5, sticky=tk.W+tk.E) + ttk.Button(self.operations_bar, text="Get Scheduled Deployment Details", command=partial(ble_issue_command, self.event_loop, self.ble_command_queue, 'GET_EXPERIMENT'), state=['disabled']).grid(row=6, sticky=tk.W+tk.E) + self.cancel_button = ttk.Button(self.operations_bar, text="Cancel Scheduled Pilot Deployment", command=self._delete_experiment, state=['disabled']) + self.cancel_button.grid(row=7, sticky=tk.W+tk.E) + ttk.Button(self.operations_bar, text="Download Deployment Logs", command=self._download_logs, state=['disabled']).grid(row=8, sticky=tk.W+tk.E) + if mode_switch_visibility: + self.switch_button = ttk.Button(self.operations_bar, text="Mode Switch", command=partial(ble_issue_command, self.event_loop, self.ble_command_queue, 'ENABLE_STORAGE_MAINTENANCE'), state=['disabled']) + self.switch_button.grid(row=9) + + # Create the workspace canvas + self.canvas = tk.Frame(self) + self.canvas.pack(fill=tk.BOTH, padx=(0, 5), pady=(0, 5), expand=True) + tk.Label(self.canvas, text="Scan for TotTag devices to continue...").pack(fill=tk.BOTH, expand=True) + + # Start the BLE communications thread + self.ble_comms = TotTagBLE(self.ble_command_queue, self.ble_result_queue, self.event_loop) + self.ble_comms.daemon = True + self.ble_comms.start() + + # Start a timer to refresh UI data every 100ms + self.master.after(100, self._refresh_data) + + def _exit(self): + self._clear_canvas() + tk.Label(self.canvas, text="Shutting down...").pack(fill=tk.BOTH, expand=True) + ble_issue_command(self.event_loop, self.ble_command_queue, 'QUIT') + self.ble_comms.join() + self.master.destroy() + + def _connect(self): + ble_issue_command(self.event_loop, self.ble_command_queue, 'CONNECT') + ble_issue_command(self.event_loop, self.ble_command_queue, self.tottag_selection.get()) + + def _delete_experiment(self): + self._clear_canvas() + prompt_area = tk.Frame(self.canvas) + prompt_area.place(relx=0.5, rely=0.5, anchor=tk.CENTER) + tk.Label(prompt_area, text="Are you sure you want to cancel the currently scheduled deployment?").grid(column=0, row=0, columnspan=4, sticky=tk.W+tk.E+tk.N+tk.S) + ttk.Button(prompt_area, text="Yes", command=partial(ble_issue_command, self.event_loop, self.ble_command_queue, 'DELETE_EXPERIMENT')).grid(column=1, row=1) + ttk.Button(prompt_area, text="No", command=partial(self._clear_canvas_with_prompt)).grid(column=2, row=1) + + def _download_logs(self): + self._clear_canvas() + self.download_raw_data.set(0) + prompt_area = tk.Frame(self.canvas) + prompt_area.place(relx=0.5, rely=0.5, anchor=tk.CENTER) + tk.Label(prompt_area, text="Download Deployment Log Files").grid(column=0, row=0, columnspan=4, sticky=tk.W+tk.E+tk.N+tk.S) + ttk.Label(prompt_area, text=" ").grid(column=0, row=1) + self.progress_label = ttk.Label(prompt_area, text="Current Progress: 0%") + self.progress_label.grid(column=0, row=2, columnspan=2, sticky=tk.W) + self.progress_bar = ttk.Progressbar(prompt_area, mode='determinate', orient=tk.HORIZONTAL, length=400) + self.progress_bar.grid(column=0, row=3, columnspan=4) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '10')).grid(column=0, row=4) + save_controls = tk.Frame(prompt_area) + save_controls.grid(column=0, row=5, columnspan=4, sticky=tk.W+tk.E+tk.N+tk.S) + ttk.Label(save_controls, text="Saving to: ").pack(side=tk.LEFT) + ttk.Button(save_controls, text="Change", command=self._change_save_directory).pack(side=tk.RIGHT) + ttk.Entry(save_controls, textvariable=self.save_directory).pack(fill=tk.X) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '4')).grid(column=0, row=6) + start_time_controls = tk.Frame(prompt_area) + start_time_controls.grid(column=0, row=7, columnspan=2, sticky=tk.W+tk.E+tk.N+tk.S) + end_time_controls = tk.Frame(prompt_area) + end_time_controls.grid(column=2, row=7, columnspan=2, sticky=tk.W+tk.E+tk.N+tk.S) + ttk.Label(start_time_controls, text="Start Date: ").pack(side=tk.LEFT) + tkcalendar.DateEntry(start_time_controls, textvariable=self.start_date, selectmode='day', firstweekday='sunday', showweeknumbers=False, date_pattern='mm/dd/yyyy').pack(side=tk.LEFT) + tkcalendar.DateEntry(end_time_controls, textvariable=self.end_date, selectmode='day', firstweekday='sunday', showweeknumbers=False, date_pattern='mm/dd/yyyy').pack(side=tk.RIGHT) + ttk.Label(end_time_controls, text="End Date: ").pack(side=tk.RIGHT) + ttk.Checkbutton(prompt_area, text="Download Raw Unprocessed Data", variable=self.download_raw_data).grid(column=0, columnspan=4, row=8, pady=5, sticky=tk.W+tk.N) + def begin_download(self): + self.data_length = 0 + ble_issue_command(self.event_loop, self.ble_command_queue, 'DOWNLOAD') + ble_issue_command(self.event_loop, self.ble_command_queue, { + 'dir': self.save_directory.get(), + 'raw': self.download_raw_data.get(), + 'start': pack_datetime(str(tzlocal.get_localzone()), self.start_date.get(), "00:00", False), + 'end': pack_datetime(str(tzlocal.get_localzone()), self.end_date.get(), "00:00", False) + 86400 + }) + ttk.Button(prompt_area, text="Begin", command=partial(begin_download, self)).grid(column=1, row=9) + ttk.Button(prompt_area, text="Cancel", command=partial(self._clear_canvas_with_prompt)).grid(column=2, row=9) + + def _create_new_experiment(self): + self._clear_canvas() + self.tottag_rows = [] + prompt_area = tk.Frame(self.canvas) + prompt_area.place(relx=0.5, rely=0, anchor=tk.N) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '4')).grid(column=0, row=0) + tk.Label(prompt_area, text="Schedule New Pilot Deployment").grid(column=0, row=1, columnspan=5, sticky=tk.W+tk.E+tk.N+tk.S) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '4')).grid(column=0, row=2) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '2')).grid(column=0, row=4) + ttk.Label(prompt_area, text="Deployment Timezone:").grid(column=0, row=5, columnspan=2, sticky=tk.W) + ttk.Combobox(prompt_area, textvariable=self.tottag_timezone, values=pytz.all_timezones, state=['readonly']).grid(column=2, row=5, columnspan=3, sticky=tk.W+tk.E) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '2')).grid(column=0, row=6) + tk.Label(prompt_area, text="Start Date").grid(column=0, row=7, sticky=tk.W) + tk.Label(prompt_area, text="Start Time").grid(column=1, row=7, sticky=tk.W) + tk.Label(prompt_area, text=" ").grid(column=2, row=7) + tk.Label(prompt_area, text="End Date").grid(column=3, row=7, sticky=tk.W) + tk.Label(prompt_area, text="End Time").grid(column=4, row=7, sticky=tk.W) + tkcalendar.DateEntry(prompt_area, textvariable=self.start_date, selectmode='day', firstweekday='sunday', showweeknumbers=False, date_pattern='mm/dd/yyyy').grid(column=0, row=8, sticky=tk.W) + ttk.Entry(prompt_area, textvariable=self.start_time, width=10, validate='all', validatecommand=(prompt_area.register(validate_time), '%P')).grid(column=1, row=8, sticky=tk.W) + tkcalendar.DateEntry(prompt_area, textvariable=self.end_date, selectmode='day', firstweekday='sunday', showweeknumbers=False, date_pattern='mm/dd/yyyy').grid(column=3, row=8, sticky=tk.W) + ttk.Entry(prompt_area, textvariable=self.end_time, width=10, validate='all', validatecommand=(prompt_area.register(validate_time), '%P')).grid(column=4, row=8, sticky=tk.W) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '4')).grid(column=0, row=9) + daily_label_start = tk.Label(prompt_area, text="Daily Start Time", state=['normal' if self.use_daily_times.get() else 'disabled']) + daily_label_start.grid(column=0, row=10, columnspan=2, sticky=tk.W) + daily_label_end = tk.Label(prompt_area, text="Daily End Time", state=['normal' if self.use_daily_times.get() else 'disabled']) + daily_label_end.grid(column=3, row=10, columnspan=2, sticky=tk.W) + daily_entry_start = ttk.Entry(prompt_area, textvariable=self.daily_start_time, width=10, validate='all', validatecommand=(prompt_area.register(validate_time), '%P'), state=['normal' if self.use_daily_times.get() else 'disabled']) + daily_entry_start.grid(column=0, row=11, sticky=tk.W) + daily_entry_end = ttk.Entry(prompt_area, textvariable=self.daily_end_time, width=10, validate='all', validatecommand=(prompt_area.register(validate_time), '%P'), state=['normal' if self.use_daily_times.get() else 'disabled']) + daily_entry_end.grid(column=3, row=11, sticky=tk.W) + def change_daily_entries_state(self): + daily_label_start['state'] = ['normal' if self.use_daily_times.get() else 'disabled'] + daily_label_end['state'] = ['normal' if self.use_daily_times.get() else 'disabled'] + daily_entry_start['state'] = ['normal' if self.use_daily_times.get() else 'disabled'] + daily_entry_end['state'] = ['normal' if self.use_daily_times.get() else 'disabled'] + ttk.Checkbutton(prompt_area, text="Include daily start/end times", variable=self.use_daily_times, command=partial(change_daily_entries_state, self)).grid(column=0, row=3, columnspan=5, sticky=tk.W) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '12')).grid(column=0, row=12) + ttk.Separator(prompt_area, orient='horizontal').grid(column=0, row=13, columnspan=5, sticky=tk.W+tk.E) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '8')).grid(column=0, row=14) + ttk.Label(prompt_area, text="TotTags in Deployment:").grid(column=0, row=15, columnspan=3, sticky=tk.W) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '4')).grid(column=0, row=16) + def remove_tottag(self, row): + row.destroy() + self.tottag_rows.remove(row) + for idx in range(len(self.tottag_rows)): + self.tottag_rows[idx].grid(row=17+idx, column=0, columnspan=5, sticky=tk.W+tk.E) + def add_tottag(self): + if len(self.tottag_rows) < MAX_NUM_DEVICES: + row = tk.Frame(prompt_area) + tottag_label = tk.StringVar(row) + row.grid(row=17+len(self.tottag_rows), column=0, columnspan=5, sticky=tk.W+tk.E) + tottag_selector = ttk.Combobox(row, width=18, values=self.device_list, state=['readonly' if self.connect_button['text'] != 'Disconnect' else '']) + tottag_selector.pack(side=tk.LEFT, expand=False) + tottag_selector.set(self.device_list[0]) + ttk.Label(row, text=" using label ").pack(side=tk.LEFT, expand=False) + ttk.Entry(row, textvariable=tottag_label, validate='all', validatecommand=(row.register(lambda new_val: len(new_val) <= MAX_LABEL_LENGTH), '%P')).pack(side=tk.LEFT, fill=tk.X, expand=True) + ttk.Label(row, text=" ").pack(side=tk.LEFT, expand=False) + ttk.Button(row, text="Remove", command=partial(remove_tottag, self, row)).pack(side=tk.LEFT, expand=False) + self.tottag_rows.append(row) + def construct_details(self): + devices = [] + labels = [b''] * MAX_NUM_DEVICES + uids = [[0 for _ in range(6)] for _ in range(MAX_NUM_DEVICES)] + for i, row in enumerate(self.tottag_rows): + for child in row.winfo_children(): + if isinstance(child, ttk.Combobox): + devices.append(child.get()) + for j, grouping in enumerate(child.get().split(':')[::-1]): + uids[i][j] = int(grouping, 16) + elif isinstance(child, ttk.Entry): + labels[i] = bytes(child.get(), 'utf-8') + chosen_tags, chosen_labels, errors = [], [], False + for i in range(len(self.tottag_rows)): + if errors: + break + if uids[i] in chosen_tags: + errors = True + tk.messagebox.showerror('TotTag Error', 'ERROR: You have chosen more than one of the same TotTag!') + else: + chosen_tags.append(uids[i]) + if not errors and len(labels[i]) > 0: + if labels[i] in chosen_labels: + errors = True + tk.messagebox.showerror('TotTag Error', 'ERROR: You have chosen the same label for multiple TotTags!') + else: + chosen_labels.append(labels[i]) + if not errors: + details = { + 'start_time': pack_datetime(self.tottag_timezone.get(), self.start_date.get(), self.start_time.get(), False), + 'end_time': pack_datetime(self.tottag_timezone.get(), self.end_date.get(), self.end_time.get(), False), + 'daily_start_time': pack_datetime(self.tottag_timezone.get(), self.start_date.get(), self.daily_start_time.get(), True) if self.use_daily_times.get() else 0, + 'daily_end_time': pack_datetime(self.tottag_timezone.get(), self.start_date.get(), self.daily_end_time.get(), True) if self.use_daily_times.get() else 0, + 'use_daily_times': 1 if self.use_daily_times.get() else 0, + 'num_devices': len(self.tottag_rows), + 'uids': uids, + 'labels': labels, + 'devices': devices + } + ble_issue_command(self.event_loop, self.ble_command_queue, 'NEW_EXPERIMENT_FULL' if self.connect_button['text'] != 'Disconnect' else 'NEW_EXPERIMENT_SINGLE') + ble_issue_command(self.event_loop, self.ble_command_queue, details) + ttk.Button(prompt_area, text="Add", command=partial(add_tottag, self)).grid(column=4, row=15, sticky=tk.E) + ttk.Label(prompt_area, text=" ", font=('Helvetica', '4')).grid(column=0, row=99) + ttk.Button(prompt_area, text="Schedule", command=partial(construct_details, self)).grid(column=1, row=100) + ttk.Button(prompt_area, text="Cancel", command=partial(self._clear_canvas_with_prompt)).grid(column=3, row=100) + add_tottag(self) + + def _show_experiment(self, data): + self._clear_canvas() + uids, labels = [], [] + start_date_deployment, start_time_deployment, _ = unpack_datetime(self.tottag_timezone.get(), None, data['start_time']) + end_date_deployment, end_time_deployment, _ = unpack_datetime(self.tottag_timezone.get(), None, data['end_time']) + start_date_local, start_time_local, _ = unpack_datetime(tzlocal.get_localzone_name(), None, data['start_time']) + end_date_local, end_time_local, _ = unpack_datetime(tzlocal.get_localzone_name(), None, data['end_time']) + start_date_utc, start_time_utc, _ = unpack_datetime('UTC', None, data['start_time']) + end_date_utc, end_time_utc, _ = unpack_datetime('UTC', None, data['end_time']) + daily_start_time = unpack_datetime(self.tottag_timezone.get(), data['start_time'], data['daily_start_time'])[1] if data['use_daily_times'] > 0 else None + daily_end_time = unpack_datetime(self.tottag_timezone.get(), data['start_time'], data['daily_end_time'])[1] if data['use_daily_times'] > 0 else None + for i in range(data['num_devices']): + uids.append('%02X:%02X:%02X:%02X:%02X:%02X'%tuple(data['uids'][i][::-1])) + labels.append(data['labels'][i].decode().rstrip('\x00')) + area = tk.Frame(self.canvas) + self.start_datetime_deployment = tk.StringVar(area, start_date_deployment + ' ' + start_time_deployment) + self.end_datetime_deployment = tk.StringVar(area, end_date_deployment + ' ' + end_time_deployment) + area.place(relx=0.5, rely=0, anchor=tk.N) + ttk.Label(area, text=" ", font=('Helvetica', '4')).grid(column=0, row=0) + tk.Label(area, text="Pilot Deployment Details").grid(column=0, row=1, columnspan=5, sticky=tk.W+tk.E+tk.N+tk.S) + ttk.Label(area, text=" ", font=('Helvetica', '6')).grid(column=0, row=2) + ttk.Label(area, text="Select Deployment Timezone:").grid(column=0, row=3, columnspan=2, sticky=tk.E) + def change_deployment_timezone(self, _event, start_time, end_time): + start_date_deployment, start_time_deployment, _ = unpack_datetime(self.tottag_timezone.get(), None, start_time) + end_date_deployment, end_time_deployment, _ = unpack_datetime(self.tottag_timezone.get(), None, end_time) + self.start_datetime_deployment.set(start_date_deployment + ' ' + start_time_deployment) + self.end_datetime_deployment.set(end_date_deployment + ' ' + end_time_deployment) + tz_selection = ttk.Combobox(area, textvariable=self.tottag_timezone, values=pytz.all_timezones, state=['readonly']) + tz_selection.grid(column=2, row=3, columnspan=3, sticky=tk.W+tk.E) + tz_selection.bind("<>", lambda event, st=data['start_time'], et=data['end_time']: change_deployment_timezone(self, event, st, et)) + ttk.Label(area, text=" ", font=('Helvetica', '6')).grid(column=0, row=4) + tk.Label(area, text="Start Date and Time (UTC)").grid(column=0, row=5, columnspan=2, sticky=tk.W) + tk.Label(area, text=" ").grid(column=2, row=5) + tk.Label(area, text="End Date and Time (UTC)").grid(column=3, row=5, columnspan=2, sticky=tk.W) + tk.Label(area, text=start_date_utc + ' ' + start_time_utc).grid(column=0, row=6, columnspan=2, sticky=tk.W) + tk.Label(area, text=" ").grid(column=2, row=6) + tk.Label(area, text=end_date_utc + ' ' + end_time_utc).grid(column=3, row=6, columnspan=2, sticky=tk.W) + ttk.Label(area, text=" ", font=('Helvetica', '4')).grid(column=0, row=7) + tk.Label(area, text="Start Date and Time (Local)").grid(column=0, row=8, columnspan=2, sticky=tk.W) + tk.Label(area, text=" ").grid(column=2, row=8) + tk.Label(area, text="End Date and Time (Local)").grid(column=3, row=8, columnspan=2, sticky=tk.W) + tk.Label(area, text=start_date_local + ' ' + start_time_local).grid(column=0, row=9, columnspan=2, sticky=tk.W) + tk.Label(area, text=" ").grid(column=2, row=9) + tk.Label(area, text=end_date_local + ' ' + end_time_local).grid(column=3, row=9, columnspan=2, sticky=tk.W) + ttk.Label(area, text=" ", font=('Helvetica', '4')).grid(column=0, row=10) + tk.Label(area, text="Start Date and Time (Deployment)").grid(column=0, row=11, columnspan=2, sticky=tk.W) + tk.Label(area, text=" ").grid(column=2, row=11) + tk.Label(area, text="End Date and Time (Deployment)").grid(column=3, row=11, columnspan=2, sticky=tk.W) + tk.Label(area, textvariable=self.start_datetime_deployment).grid(column=0, row=12, columnspan=2, sticky=tk.W) + tk.Label(area, text=" ").grid(column=2, row=12) + tk.Label(area, textvariable=self.end_datetime_deployment).grid(column=3, row=12, columnspan=2, sticky=tk.W) + if daily_start_time is not None and daily_end_time is not None: + ttk.Label(area, text=" ", font=('Helvetica', '4')).grid(column=0, row=13) + tk.Label(area, text="Daily Start Time").grid(column=0, row=14, columnspan=2, sticky=tk.W) + tk.Label(area, text=" ").grid(column=2, row=14) + tk.Label(area, text="Daily End Time").grid(column=3, row=14, columnspan=2, sticky=tk.W) + tk.Label(area, text=daily_start_time).grid(column=0, row=15, columnspan=2, sticky=tk.W) + tk.Label(area, text=" ").grid(column=2, row=15) + tk.Label(area, text=daily_end_time).grid(column=3, row=15, columnspan=2, sticky=tk.W) + ttk.Label(area, text=" ", font=('Helvetica', '12')).grid(column=0, row=16) + ttk.Separator(area, orient='horizontal').grid(column=0, row=17, columnspan=5, sticky=tk.W+tk.E) + ttk.Label(area, text=" ", font=('Helvetica', '8')).grid(column=0, row=18) + ttk.Label(area, text="TotTags in Deployment:").grid(column=0, row=19, columnspan=3, sticky=tk.W) + ttk.Label(area, text=" ", font=('Helvetica', '4')).grid(column=0, row=20) + for i in range(len(uids)): + ttk.Label(area, text=' ' + uids[i] + ': ' + (labels[i] if labels[i] else '')).grid(row=21+i, column=0, columnspan=5, sticky=tk.W+tk.E) + + def _subscribe_to_live_ranges(self): + self._clear_canvas() + scroll_area = tk.Frame(self.canvas) + scroll_area.pack(fill=tk.BOTH, expand=True) + scroll_area.rowconfigure(0, weight=1) + scroll_area.columnconfigure(0, weight=1) + self.txt_area = tk.Text(scroll_area, highlightthickness=0, takefocus=0, undo=False, state=tk.DISABLED) + self.txt_area.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W) + scrollbar = ttk.Scrollbar(scroll_area, command=self.txt_area.yview) + scrollbar.grid(row=0, column=1, sticky=tk.N+tk.S+tk.E+tk.W) + self.txt_area['yscrollcommand'] = scrollbar.set + ble_issue_command(self.event_loop, self.ble_command_queue, 'SUBSCRIBE_RANGES') + + def _range_received(self, data): + self.txt_area['state'] = tk.NORMAL + txt_string = 'Ranges to %d devices:\n'%data[0] + for i in range(data[0]): + txt_string += ' 0x%02X: %d mm\n'%(data[(3*i)+1], struct.unpack('> 2) & 0x03 + calib_gyro = (reg_value >> 4) & 0x03 + calib_sys = (reg_value >> 6) & 0x03 + return stat(calib_mag,calib_accel,calib_gyro,calib_sys) + +def dataclass_to_list(d): + return list(asdict(d).values()) + +def unpack_imu_data(data, data_type_seq): + """ + unpack multi-byte multi-type imu data according to seq defined in data_type_seq + returns an array with all elements in the order of data_type_seq + the available types are defined in data_type_len dict + """ + all_elements = [] + current_index = 0 + for t in data_type_seq: + unpacked = unpack_imu_data_single_type(data[current_index:current_index+data_type_len[t]],t) + current_index+=data_type_len[t] + all_elements = all_elements + dataclass_to_list(unpacked) + return all_elements + +#a = load_data("~/Downloads/Unknown.pkl") +a = load_data("./Unknown.pkl") +data_types = [STAT_DATA,LACC_DATA,GYRO_DATA] +for segment in a.loc["i"]: + for ts, data in segment: + print(ts, unpack_imu_data(data,data_types)) \ No newline at end of file diff --git a/software/management/dashboard/segger_download.py b/software/management/dashboard/segger_download.py index dbdc39ef..797bc6f4 100755 --- a/software/management/dashboard/segger_download.py +++ b/software/management/dashboard/segger_download.py @@ -4,6 +4,7 @@ # PYTHON INCLUSIONS --------------------------------------------------------------------------------------------------- import tottag +from experimental_tottag import * import os, signal, sys, tempfile, time import subprocess, multiprocessing @@ -31,8 +32,8 @@ def handle_incoming_data(fifo_file_name, storage_directory, pipe, is_running): with open(fifo_file_name, 'rb') as rtt_file: # Wait until all experiment details have been received - data = rtt_file.read(4 + 4 * 4 + 2 + 6 * tottag.MAX_NUM_DEVICES + tottag.MAX_NUM_DEVICES * tottag.MAX_LABEL_LENGTH) - details = tottag.unpack_experiment_details(data[4:]) + data = rtt_file.read(4 + 4 * 4 + 2 + 6 * MAX_NUM_DEVICES + MAX_NUM_DEVICES * MAX_LABEL_LENGTH) + details = unpack_experiment_details(data[4:]) pipe.send(details) # Create a thread to monitor the data reading activity @@ -96,7 +97,7 @@ def main(): # Convert the downloaded log data to a PKL file print('\nDeserializing log data into a PKL file... ', end='') with open(os.path.join(storage_directory, 'data.ttg'), 'rb') as ttg_file: - tottag.process_tottag_data(int(device_id, 16), storage_directory, details, ttg_file.read(), True) + process_tottag_data(int(device_id, 16), storage_directory, details, ttg_file.read(), True) print('Done') # Clean up all temporary files and directories