# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/sensors.ipynb.

# %% auto 0
__all__ = ['packet_labels', 'decode_packet', 'GPIOInterface', 'MPInterface', 'SensorStream', 'set_pps_cb', 'clear_pps_cb',
           'collect_sim', 'interp2camera_times', 'SensorDashboard']

# %% ../nbs/api/sensors.ipynb 4
from fastcore.foundation import patch
from fastcore.meta import delegates
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from scipy.interpolate import interp1d
from tqdm import tqdm
import warnings
from multiprocessing import Process

import os
from typing import Iterable, Union, Callable, List, TypeVar, Generic, Tuple, Optional
import json
import pickle
from pathlib import Path
import time
import serial
import datetime

# %% ../nbs/api/sensors.ipynb 6
packet_labels = ["rpi_time","rtc_status","imu_status","air_status","gps_status",
                "rtc_time",
                "air_offset","temp","pressure","humidity",
                "imu_offset","imu_cal","quat_w","quat_x","quat_y","quat_z",
                "gps_offset","latitude","longitude","altitude","num_sats","pDOP","footer"]

# %% ../nbs/api/sensors.ipynb 8
def decode_packet(buff:"byte string"=None) -> list:
    """Decode `buff` into a list of decoded variables"""
    if buff is None or len(buff) < 73: return []
    
    contents = [None]*23
    np_buff = np.frombuffer(buff,dtype='uint8').astype(np.uint8)
    
    # sensor status
    contents[ 0] = datetime.datetime.now()   # time of recording this data packet
    #contents[ 0] = np_buff[0].view(np.uint8) # '*' is 42 decimal ASCII
    contents[ 1] = np_buff[1].view(np.uint8) # rtc status
    contents[ 2] = np_buff[2].view(np.uint8) # imu status
    contents[ 3] = np_buff[3].view(np.uint8) # air status
    contents[ 4] = np_buff[4].view(np.uint8) # gps status
    
    # Real Time Clock
    year   = np_buff[ 6: 8].view(np.uint16)[0] # rtc year
    month  = np_buff[ 8].view(np.uint8)        # rtc month
    day    = np_buff[ 9].view(np.uint8)        # rtc day
    hour   = np_buff[10].view(np.uint8)        # rtc hour
    minute = np_buff[11].view(np.uint8)        # rtc minute
    second = np_buff[12].view(np.uint8)        # rtc second
    ms     = np_buff[14:16].view(np.uint16)[0] # rtc ms
    try: contents[ 5] = datetime.datetime(year,month,day,hour,minute,second,ms*1000)
    except ValueError: return [] # packets corrupted
    
    # Humidity, Pressure sensor
    contents[ 6] = np_buff[16:18].view(np.int16)[0]   # humidity timestamp offset [ms]
    contents[ 7] = np_buff[20:24].view(np.float32)[0] # temperature [deg C]
    contents[ 8] = np_buff[24:28].view(np.float32)[0] # pressure [hPa]
    contents[ 9] = np_buff[28:32].view(np.float32)[0] # humidity [relative humidity %]
    
    # Inertial Measurement Unit
    contents[10] = np_buff[32:34].view(np.int16)[0]   # imu timestamp offset [ms]
    contents[11] = np_buff[34].view(np.uint8)         # calibration status
    contents[12] = np_buff[36:40].view(np.float32)[0] # quaternion w
    contents[13] = np_buff[40:44].view(np.float32)[0] # quaternion x
    contents[14] = np_buff[44:48].view(np.float32)[0] # quaternion y
    contents[15] = np_buff[48:52].view(np.float32)[0] # quaternion z
    
    # Global Positioning System
    contents[16] = np_buff[52:54].view(np.int16)[0]  # gps timestamp offset [ms]
    contents[17] = float(np_buff[56:60].view(np.int32)[0])/1e7  # latitude [deg]
    contents[18] = float(np_buff[60:64].view(np.int32)[0])/1e7  # longitude [deg]
    contents[19] = float(np_buff[64:68].view(np.int32)[0])  # altitude [mm above ellipsoid]
    contents[20] = np_buff[68].view(np.uint8)        # number of satellites in view and used in compute
    contents[21] = np_buff[70:72].view(np.uint16)[0] # position DOP [*0.01]
    contents[22] = np_buff[72].view(np.uint8)        # '\n' is 10 decimal ASCII
    
    return contents

# %% ../nbs/api/sensors.ipynb 10
class GPIOInterface():
    def __init__(self, start_pin):
        self.start_pin = start_pin
        
        try: 
            import RPi.GPIO as GPIO
        except ModuleNotFoundError:
            try: 
                import Jetson.GPIO as GPIO
            except ModuleNotFoundError:
                print("No module named RPi or Jetson. Did you try `pip install RPi.GPIO` or `pip install Jetson.GPIO`?")
        
        self.GPIO=GPIO
        
        try:
            #GPIO.setwarnings(False)
            self.GPIO.setmode(GPIO.BCM) # BCM pin-numbering scheme from Raspberry Pi
            self.GPIO.setup(self.start_pin, GPIO.IN)
        except NameError:
            warnings.warn(f"GPIO requires Jetson or Raspberry Pi.",stacklevel=2)
         
    def status(self):
        return self.GPIO.input(self.start_pin)
    

class MPInterface():
    def __init__(self, mp_bool):
        self._mp_bool = mp_bool
         
    @property
    def status(self):
        return self._mp_bool.value
    
    @status.setter
    def status(self, value):
        self._mp_bool.value = value
    

# %% ../nbs/api/sensors.ipynb 11
class SensorStream():
    """Parses ancillary sensor data for saving"""
    def __init__(self, 
                 baudrate:int=921_600,    # baudrate of sensor board
                 port:str="/dev/serial0", # serial port of sensor board
                 toggle_interface=None,   # interface to control loop
                 ssd_dir:str=f"/media/pi/fastssd/", # save dir, absoulte path
                 cam_class=None                     # OpenHSI camera class
                ):
        """Opens a serial port. When the button `start_pin` is pressed, start recording.
        Packets are saved at `ssd_dir`, if None, then mounting/unmountng will not be attempted."""
        
        self.ssd_dir  = ssd_dir
        self.cam_class = cam_class
        self.packets = []
        
        self.toggle_interface = toggle_interface
        
        try:
            self.ser = serial.Serial(port=port,
                                    baudrate=baudrate,
                                    bytesize=serial.EIGHTBITS,
                                    parity=serial.PARITY_NONE,
                                    stopbits=serial.STOPBITS_ONE)
            self.ser.flushInput()
        except Exception as e:
            warnings.warn(f"{e}: could not open port {port}.",stacklevel=2)
        
    def read_packet(self,header:chr = b"*", num_bytes:int = 76, timeout:float = 2.) -> "byte string":
        """Reads at least `num_bytes` of a data packet starting with `header` 
        and times out after `timeout` seconds if packet is invalid."""
        buff = header

        # Check for packet start of frame
        start_time = time.time()
        while True:
            if self.ser.in_waiting > 0:
                b = self.ser.read()
                if b ==  header: break # packet received

            if time.time() > start_time + timeout: 
                print("No data packets.")
                return None

        # read the rest of data packet
        start_time = time.time()
        for i in range(num_bytes):
            if time.time() > start_time + timeout:
                print("Received Incomplete Packet")
                break

            if self.ser.in_waiting > 0:
                buff += self.ser.read()

        return buff
    
    def save(self):
        """Save the data packets. Will save some plots of the data as well."""
        self.directory = f"{self.ssd_dir}/{datetime.datetime.now(datetime.timezone.utc).strftime('%Y_%m_%d')}/"
        Path(self.directory).mkdir(parents=False, exist_ok=True)
        
        df = pd.DataFrame(self.packets,columns=packet_labels)
        self.new_df = self.clean_df(df)
        
        # find the time offset between the board and the system time (including the small delay between loops)
        offset_ms = np.nanmin(df.rpi_time.to_numpy() - df.rtc_time.to_numpy() - np.timedelta64(1, "ms"))
        
        offset_ms /= np.timedelta64(1,"ms") # convert to ms
        
        fname = self.directory+f"{self.new_df.rpi_time[0].strftime('%Y_%m_%d-%H_%M_%S')}_ancillary_{offset_ms:.0f}.pkl"
        with open(fname,"wb") as f:
            pickle.dump(self.new_df,f,protocol=4)
                    
    def master_loop(self,
                    n_lines:int        = 128,  # how many along-track pixels
                    processing_lvl:int = 0,    # desired processing done in real time
                    json_path:str      = None, # path to settings file
                    pkl_path:str       = None, # path to calibration file
                    preconfig_meta:str = None, # path to metadata file
                    ssd_dir:str        = None, # path to SSD
                   ):
        """Continuous run saving packets during start button pressed. If you want to capture camera as well, 
        input all the optional parameters."""
        self.is_mounted = False
        self.packets = []

        while True:
            if self.toggle_interface.status == True:

                if not self.is_mounted and self.ssd_dir: 
                    # os.system("mount /dev/sda1");
                    self.is_mounted = True
                    self.ser.write(b'y') # let sensor board know everything is set up
                    if self.cam_class:
                        from openhsi.cameras import switched_camera
                        self.p = Process(target=switched_camera, args=(self.cam_class, n_lines, processing_lvl, json_path,
                                                                           pkl_path, preconfig_meta, ssd_dir, self.toggle_interface))
                        self.p.start()

                while self.ser.in_waiting > 0:
                    self.packets.append( decode_packet(self.read_packet()) )
                
                time.sleep(0.001)

            elif self.toggle_interface.status == False: # button off, stop collection and save packets to file

                if self.ssd_dir and (len(self.packets)>0):
                    if self.cam_class:
                        self.ser.write(b'n') # let sensor board to stop sending packets
                        self.save()
                        #os.system("umount /dev/sda1"); self.is_mounted = False # keeps causing problems...
                        self.is_mounted = False # not actually unmounted but lets you keep collecting with button pressses
                        self.packets = []

                time.sleep(0.001)

    def clean_df(self, df:pd.DataFrame) -> pd.DataFrame:
        """Converts time offsets in `df` into datetime and splits sensor readings that update 
        at different rates. Also saves the plots as a picture."""
        rtc_df = df[df.columns][(df["rtc_status"]==1)]
        air_df = df[df.columns][(df["air_status"]==1)]
        imu_df = df[df.columns][(df["imu_status"]==1)]
        gps_df = df[df.columns][(df["gps_status"]==1)]

        air_df.air_offset = air_df.rtc_time + pd.to_timedelta(air_df.air_offset, unit="ms")
        imu_df.imu_offset = imu_df.rtc_time + pd.to_timedelta(imu_df.imu_offset, unit="ms")
        gps_df.gps_offset = gps_df.rtc_time + pd.to_timedelta(gps_df.gps_offset, unit="ms")

        air_df.rename({'air_offset': 'board_time'}, axis=1, inplace=True)
        imu_df.rename({'imu_offset': 'board_time'}, axis=1, inplace=True)
        gps_df.rename({'gps_offset': 'board_time'}, axis=1, inplace=True)

        air_df.drop(["rtc_time","rtc_status","imu_status","air_status","gps_status","footer","imu_offset","gps_offset"],axis=1,inplace=True)
        imu_df.drop(["rtc_time","rtc_status","imu_status","air_status","gps_status","footer","air_offset","gps_offset"],axis=1,inplace=True)
        gps_df.drop(["rtc_time","rtc_status","imu_status","air_status","gps_status","footer","air_offset","imu_offset"],axis=1,inplace=True)

        air_df[["imu_cal","quat_w","quat_x","quat_y","quat_z","latitude","longitude","altitude","num_sats","pDOP"]] = np.nan
        imu_df[["temp","pressure","humidity","latitude","longitude","altitude","num_sats","pDOP"]] = np.nan
        gps_df[["temp","pressure","humidity","imu_cal","quat_w","quat_x","quat_y","quat_z"]] = np.nan

        new_df = pd.concat([air_df,imu_df,gps_df])
        new_df.set_index("board_time",inplace=True)
        new_df.sort_index(inplace=True)

        fig, axes = plt.subplots(nrows=2, ncols=3,figsize=(16,12))
        gps_df.plot(ax=axes[0,0],x="longitude",y="latitude",ylabel="latitude",label="path")
        gps_df.plot(ax=axes[0,1],x="rpi_time",y="num_sats")
        imu_df.plot(ax=axes[0,2],x="rpi_time",y=["quat_w","quat_x","quat_y","quat_z"])
        air_df.plot(ax=axes[1,0],x="rpi_time",y="temp")
        air_df.plot(ax=axes[1,1],x="rpi_time",y="pressure")
        air_df.plot(ax=axes[1,2],x="rpi_time",y="humidity")
        
        fig.savefig(self.directory+f"{new_df.rpi_time[0].strftime('%Y_%m_%d-%H_%M_%S')}_ancillary.png",bbox_inches='tight',transparent=False, pad_inches=0)
        return new_df
    

# %% ../nbs/api/sensors.ipynb 14
def set_pps_cb(
    gps_pin:int=19, # GPS pulse per second pin
    times_list:List[datetime.datetime]=[], # Any list to append system time when callback is called
    bouncetime_ms:float=10, # Debouncing time for the GPS PPS signal
):
    """Setup a callback that appends the system time to `times_list` each time 
    a GPS pulse per second is detected on `gps_pin`."""
    GPIO.setup(gps_pin,GPIO.IN,pull_up_down=GPIO.PUD_DOWN)
    def pps_cb(channel):
        times_list.append(datetime.datetime.now()-datetime.timedelta(milliseconds=bouncetime_ms))
    GPIO.add_event_detect(gps_pin,GPIO.RISING,callback=pps_cb,bouncetime=bouncetime_ms)

def clear_pps_cb(
    gps_pin:int=19, # GPS pulse per second pin
):
    """Clear the GPS pulse per second callback on `gps_pin`."""
    GPIO.remove_event_detect(gps_pin)

# %% ../nbs/api/sensors.ipynb 16
def collect_sim(rtc_offset_ms:float=0) -> list:
    """Generate fake sensor packets for testing."""
    
    contents = [None]*23
    
    # sensor status
    contents[ 0] = datetime.datetime.now()# time of recording this data packet
    contents[ 1] = np.random.randint(9,20)//10 # rtc status
    contents[ 2] = np.random.randint(0,2) # imu status
    contents[ 3] = np.random.randint(0,2) # air status
    contents[ 4] = np.random.randint(0,2) # gps status
    
    contents[ 5] = datetime.datetime.now() + datetime.timedelta(milliseconds=rtc_offset_ms)
    
    # Humidity, Pressure sensor
    contents[ 6] = np.random.randint(0,100)     # humidity timestamp offset [ms]
    contents[ 7] = 25 + 5*np.random.rand()      # temperature [deg C]
    contents[ 8] = 1013.25 + 1*np.random.rand() # pressure [hPa]
    contents[ 9] = 50 + 20*np.random.rand()     # humidity [relative humidity %]
    
    # Inertial Measurement Unit
    contents[10] = np.random.randint(0,100)   # imu timestamp offset [ms]
    contents[11] = np.random.randint(0,256)   # calibration status
    contents[12] = -1+2*np.random.rand() # quaternion w
    contents[13] = -1+2*np.random.rand() # quaternion x
    contents[14] = -1+2*np.random.rand() # quaternion y
    contents[15] = -1+2*np.random.rand() # quaternion z
    
    # Global Positioning System
    contents[16] = np.random.randint(0,100)        # gps timestamp offset [ms]
    contents[17] = np.int32((-33+0.1*np.random.rand())*1e7) /1e7  # latitude [deg *10^-7]
    contents[18] = np.int32((141+0.1*np.random.rand())*1e7) /1e7  # longitude [deg *10^-7]
    contents[19] = np.int32((100+20*np.random.rand())*1e3)  /1e7   # altitude [mm above ellipsoid]
    contents[20] = np.random.randint(0,20)         # number of satellites in view and used in compute
    contents[21] = np.random.randint(0,999)        # position DOP [*0.01]
    contents[22] = 10                              # '\n' is 10 decimal ASCII
    
    return contents

# %% ../nbs/api/sensors.ipynb 24
def interp2camera_times(df:pd.DataFrame, ts:np.array) -> pd.DataFrame:
    """Interpolate the ancillary sensor data to the timestamps for when 
    each frame was taken with the camera."""
    df_add = pd.DataFrame({"cam_now":ts,"type":"camera"}).set_index("cam_now",inplace=False)
    df.insert(0,"type","sensors")
    df_with_cam = pd.concat([df,df_add]).sort_index()
    
    df_with_cam.interpolate(method="cubicspline",axis="index",limit_direction="both",inplace=True)
    df_with_cam = df_with_cam[df_with_cam.type.str.match("camera")]
    df_with_cam.drop(labels="type", axis=1,inplace=True)

    return df_with_cam
    

# %% ../nbs/api/sensors.ipynb 26
#| output: false
import param
import panel as pn
import hvplot.pandas
import hvplot.streamz
import holoviews as hv
from holoviews.element.tiles import EsriImagery
from holoviews.selection import link_selections
from datashader.utils import lnglat_to_meters
from streamz.dataframe import PeriodicDataFrame
hv.extension('bokeh',logo=False)
from holoviews.streams import Pipe, Buffer

# %% ../nbs/api/sensors.ipynb 27
class SensorDashboard():
    """A dashboard for viewing ancillary sensor status."""
    def __init__(self, baudrate=115_200, port="/dev/cu.usbserial-DN05TVTD",buff_len:int = 100):
        """Create a dashboard to view ancillary sensor diagnostics."""
        self.ser = serial.Serial(port=port,
                                baudrate=baudrate,
                                bytesize=serial.EIGHTBITS,
                                parity=serial.PARITY_NONE,
                                stopbits=serial.STOPBITS_ONE,)
        
        # Instantiate for storing data
        self.data = []
        self.data_df = pd.DataFrame(None, columns=["lat","lon","sats","temp","pressure","humidity","sys_cal","gyro_cal","accel_cal","mag_cal"])
        
        self.loc_stream = Buffer( pd.DataFrame({"lon":[],"lat":[]}), length=buff_len, index=False)
        #temp = pd.DataFrame({"lon":[151.1,151.2],"lat":[-33.8,-34]})
        #self.loc_stream.send( pd.concat(lnglat_to_meters(temp["lon"],temp["lat"]),axis=1))
        self.loc_dmap = hv.DynamicMap(hv.Scatter,streams=[self.loc_stream]).opts(xlabel="latitude",ylabel="longitude")
        self.map_tiles  = EsriImagery().opts(alpha=0.5,bgcolor='white')
        
        self.sat_stream = Buffer( pd.DataFrame({"sats":[]}), length=buff_len)
        self.sat_dmap   = hv.DynamicMap(hv.Curve,streams=[self.sat_stream]).opts(xlabel="index",ylabel="number of satellites in view")
        
        self.temp_stream = Buffer( pd.DataFrame({"temp":[]}), length=buff_len)
        self.temp_dmap = hv.DynamicMap(hv.Curve,streams=[self.temp_stream]).opts(xlabel="index",ylabel="temperature (deg C)",toolbar=None)
        
        self.pressure_stream = Buffer( pd.DataFrame({"pressure":[]}), length=buff_len)
        self.pressure_dmap   = hv.DynamicMap(hv.Curve,streams=[self.pressure_stream]).opts(xlabel="index",ylabel="pressure (hPa)",toolbar=None)
        
        self.humidity_stream = Buffer( pd.DataFrame({"humidity":[]}), length=buff_len)
        self.humidity_dmap   = hv.DynamicMap(hv.Curve,streams=[self.humidity_stream]).opts(xlabel="index",ylabel="relative humidity %",toolbar=None)
        
        self.sys_stream   = Buffer( pd.DataFrame({"sys_cal":[]}),length=buff_len)
        self.sys_dmap     = hv.DynamicMap(hv.Curve,streams=[self.sys_stream],label="system")
        self.gyro_stream  = Buffer( pd.DataFrame({"gyro_cal":[]}),length=buff_len)
        self.gyro_dmap    = hv.DynamicMap(hv.Curve,streams=[self.gyro_stream],label="gyro")
        self.accel_stream = Buffer( pd.DataFrame({"accel_cal":[]}),length=buff_len)
        self.accel_dmap   = hv.DynamicMap(hv.Curve,streams=[self.accel_stream],label="accel")
        self.mag_stream   = Buffer( pd.DataFrame({"mag_cal":[]}),length=buff_len)
        self.mag_dmap     = hv.DynamicMap(hv.Curve,streams=[self.mag_stream],label="mag")
        
        self.clear_btn = pn.widgets.Button(name='Clear', button_type='primary')
        self.msg = pn.widgets.StaticText(name="")
        self.clear_btn.on_click(self.clear_all)
        self.rpi_ready_btn = pn.widgets.Button(name="Camera Off",button_type="danger")
        self.rpi_ready = False
        self.counter = 0
        
    def run(self):
        """Infinite loop to check sensor status every second."""
        try:
            while True:
                time.sleep(1)
                self.read()
                self.update()
                self.counter += 1
                self.msg.value = f"Running. Count = {self.counter}"
                if self.rpi_ready: self.rpi_ready_btn.button_type == "success"; self.rpi_ready_btn.name == "Camera ON"
                else: self.rpi_ready_btn.button_type == "danger"; self.rpi_ready_btn.name == "Camera OFF"
        except KeyboardInterrupt:
            self.ser.close()
    
    def __call__(self):
        """Create layout of panels"""
        self.imu_stats = (self.sys_dmap*self.gyro_dmap*self.accel_dmap*self.mag_dmap).opts(ylabel="IMU calibration status",legend_position="left")
        return pn.Column(pn.Row(self.loc_dmap.opts(hv.opts.Scatter(size=10))*self.map_tiles,self.sat_dmap,self.imu_stats),
                         pn.Row(self.temp_dmap,self.pressure_dmap,self.humidity_dmap),
                         pn.Row(pn.Row(self.clear_btn,self.rpi_ready_btn),self.msg)).servable()
    def close(self):
        self.ser.close()
    
    def clear_all(self,event):
        """Clears all the sensor streams"""
        self.loc_stream.clear()
        self.sat_stream.clear()
        self.temp_stream.clear()
        self.pressure_stream.clear()
        self.humidity_stream.clear()
        self.sys_stream.clear()
        self.gyro_stream.clear()
        self.accel_stream.clear()
        self.mag_stream.clear()
        self.msg.value = f"Cleared {self.clear_btn.clicks} time(s)"
        
        
    def update(self):
        """Push new sensor data to streams"""
        self.loc_stream.send( pd.concat(lnglat_to_meters(self.data_df['lon'], self.data_df['lat']),axis=1))
        self.sat_stream.send( self.data_df["sats"].to_frame() )
        self.temp_stream.send( self.data_df["temp"].to_frame() )
        self.pressure_stream.send( self.data_df["pressure"].to_frame() )
        self.humidity_stream.send( self.data_df["humidity"].to_frame() )
        self.sys_stream.send( self.data_df["sys_cal"].to_frame() )
        self.gyro_stream.send( self.data_df["gyro_cal"].to_frame() )
        self.accel_stream.send( self.data_df["accel_cal"].to_frame() )
        self.mag_stream.send( self.data_df["mag_cal"].to_frame() )
        
    def read(self,timeout:float=2):
        """Parse XBee data packets and timeout if none received."""
        start_time = time.time()

        # Check if line is ready
        while self.ser.inWaiting() > 0:
            
            # Read line from serial
            self.line_data = self.ser.readline()
            if len(self.line_data) < 22: continue # packet not complete, skip
            
            if time.time()-start_time > timeout:
                print("timeout")
                self.ser.flushInput()
                break
            
            contents = []
            np_buff = np.frombuffer(self.line_data,dtype='int8').astype(np.uint8) 
            
            contents.append( float(np_buff[0:4].view(np.int32))*1e-7 ) # latitude [deg]
            contents.append( float(np_buff[4:8].view(np.int32))*1e-7 ) # longitude [deg]
            contents.append( np_buff[20].view(np.uint8)    ) # number of satellites in view and used in compute
            
            # remove before export
            #contents[0] += 2*np.random.rand() - 1; contents[1] += 2*np.random.rand() - 1 # obfuscate my position
            
            contents.append( np_buff[ 8:12].view(np.float32)[0] ) # temperature [deg C]
            contents.append( np_buff[12:16].view(np.float32)[0] ) # pressure [hPa]
            contents.append( np_buff[16:20].view(np.float32)[0] ) # humidity [relative humidity %]
            
            self.rpi_ready = True if np_buff[21].view(np.uint8) > 0 else False
            
            cal_char = np_buff[22].view(np.uint8)
            contents.append( (cal_char & 0b1100_0000) >> 6) # system calibration
            contents.append( (cal_char & 0b0011_0000) >> 4) # gyro calibration
            contents.append( (cal_char & 0b0000_1100) >> 2) # accel calibration
            contents.append(  cal_char & 0b0000_0011      ) # mag calibration
            self.data.append(contents)
        self.data_df = pd.DataFrame(self.data, columns=["lat","lon","sats","temp","pressure","humidity","sys_cal","gyro_cal","accel_cal","mag_cal"])
        
    
