def __init__(self, dongle_port):
        QMainWindow.__init__(self)
        self.setupUi(self)

        # setup the dongle
        self.dongle = Dongle()
        self.dongle.init(dongle_port)

        # disable maximize button
        flags = self.windowFlags()
        flags ^= QtCore.Qt.WindowMaximizeButtonHint
        self.setWindowFlags(flags | QtCore.Qt.CustomizeWindowHint)

        # hook up various Qt GUI slots etc
        self.devicelist_model = QtGui.QStandardItemModel(self.lstDevices)
        self.lstDevices.setModel(self.devicelist_model)
        self.lblPort.setText('Dongle serial port: {}'.format(dongle_port))
        self.btnRefresh.clicked.connect(self.refresh_devices)
        self.btnCancelScan.clicked.connect(self.cancel_scan)
        self.lstDevices.clicked.connect(self.device_selected)
        self.btnConnect.clicked.connect(self.connect_device)
        self.btnConnect.setEnabled(False)
        self.btnAcc.clicked.connect(self.accel_calibration)
        self.btnGyro.clicked.connect(self.gyro_calibration)
        self.btnMag.clicked.connect(self.mag_calibration)
        self.btnExit.clicked.connect(self.exit)
        self.spinIMU.valueChanged.connect(self.imu_changed)
        self.spinIMU.setEnabled(False)

        self.gyro_dialog = None
        self.acc_dialog = None
        self.mag_dialog = None
        self.sk8 = None
        self.current_imuid = None
        self.calibration_state = self.CAL_NONE
        self.scan_state = self.SCAN_STATE_IDLE

        # start a 30ms timer to update displayed sensor data
        self.data_timer = QtCore.QTimer(self)
        self.data_timer.setInterval(30)
        self.data_timer.timeout.connect(self.update_data)

        # start a 3s timer to update battery level
        self.battery_timer = QtCore.QTimer(self)
        self.battery_timer.setInterval(3000)
        self.battery_timer.timeout.connect(self.update_battery)

        # attempt to parse any existing calibration file
        self.calibration_data = ConfigParser()
        self.calibration_data.read(
            os.path.join(os.path.dirname(__file__),
                         self.DEFAULT_CALIB_FILENAME))

        # begin a scan on startup
        self.refresh_devices()
Ejemplo n.º 2
0
    def test_imu_streaming(self):
        with Dongle() as d:
            self.assertEqual(True, d.init(self.config['dongle_port']), 'Dongle init failed')
            (result, devicelist) = d.scan_and_connect([self.config['device_name']])
            self.assertEqual(result, True, 'Connection failed, result False')
            device = devicelist[0]
            self.assertEqual(self.config['device_name'], device.get_device_name(), 'Device name mismatch: {} vs expected {}'.format(device.get_device_name(), self.config['device_name']))

            device.set_imu_callback(self.imu_callback)
            result = device.enable_imu_streaming([0], SENSOR_ALL)
            self.assertEqual(result, True, 'Failed to enable IMU streaming')
            time.sleep(1.0)
            result = device.disable_imu_streaming()
            self.assertEqual(result, True, 'Failed to disable IMU streaming')

            # with internal IMU only, should get acc+gyro data but no mag data
            self.assertGreater(len(self.recv_data), 0, 'Failed to receive any data from the IMU!')

            expected = -1
            for d in self.recv_data:
                (acc, gyro, mag, index, seq, timestamp) = d
                self.assertNotEquals(max(acc), 0, 'Acc sample max value is zero: {}'.format(acc))
                self.assertNotEquals(max(gyro), 0, 'Gyro sample max value is zero: {}'.format(gyro))
                self.assertEqual(index, 0, 'Unexpected index value: {}'.format(index))
                if expected == -1:
                    expected = (seq + 1) % 256
                else:
                    self.assertEqual(expected, seq, 'Dropped packet(s) detected: expected {}, found {}'.format(expected, seq))
                    expected = (expected + 1) % 256
Ejemplo n.º 3
0
 def test_basic_connect_disconnect(self):
     with Dongle() as d:
         self.assertEqual(True, d.init(self.config['dongle_port']), "Dongle init failed")
         (result, device) = d.connect_direct(self.config['device_addr'])
         self.assertEqual(result, True, 'Connection failed, result False')
         self.assertIsInstance(device, SK8, 'Returned object is not SK8 instance')
         self.assertIsNotNone(device, 'Returned SK8 instance is None')
Ejemplo n.º 4
0
def hardware(port, device_name):
    with Dongle() as dongle: # this will disconnect any active connections when it goes out of scope

        if not dongle.init(port):
            sys.exit('Failed to initialise dongle on port {}'.format(port))

        # use the scan_and_connect convenience function to scan for a list of
        # one or more devices and then attempt to connect to them if they are
        # detected in the scan results
        (result, devices) = dongle.scan_and_connect([device_name])

        if not result:
            # scan failed to detect device(s), or connection to those device(s)
            # failed for some reason
            sys.exit('Failed to establish connections to all devices')

        print('Connections created to {} devices'.format(len(devices)))

        # Each entry in this list is an SK8 object representing the physical device
        sk8 = devices[0]

        while True:
            try:
                print('Has IMUs: {} | Has ExtAna: {}'.format(sk8.has_imus(False), sk8.has_extana(True)))
                time.sleep(1.0)
            except KeyboardInterrupt:
                print('Disconnecting...')
                break

        sk8.disconnect()
Ejemplo n.º 5
0
def set_name(port, device_name_old, device_name_new):
    with Dongle(
    ) as dongle:  # this will disconnect any active connections when it goes out of scope

        if not dongle.init(port):
            sys.exit('Failed to initialise dongle on port {}'.format(port))

        # use the scan_and_connect convenience function to scan for a list of
        # one or more devices and then attempt to connect to them if they are
        # detected in the scan results
        (result, devices) = dongle.scan_and_connect([device_name_old])

        if not result:
            # scan failed to detect device(s), or connection to those device(s)
            # failed for some reason
            sys.exit('Failed to establish connections to all devices')

        print('Connections created to {} devices'.format(len(devices)))

        # Each entry in this list is an SK8 object representing the physical device
        sk8 = dongle.get_devices()[0]

        print('Changing device name to "{}"...'.format(device_name_new))
        if not sk8.set_device_name(device_name_new):
            sys.exit('Failed to set device name')

        print('Reading back name (cached): {}'.format(
            sk8.get_device_name(cached=True)))
        print('Reading back name (uncached): {}'.format(
            sk8.get_device_name(cached=False)))
Ejemplo n.º 6
0
def scanner(port):
    with Dongle() as dongle:

        if not dongle.init(port):
            sys.exit('Failed to init dongle on port {}'.format(port))

        # begin a scan for BLE devices. this will run in the background
        # until end_scan() is called. The parameter is an optional callable
        # that will be called when a new device is discovered. You can use this
        # to stop the scan
        dongle.begin_scan(scan_callback)

        # wait until Ctrl-C is pressed to stop the scan
        print('Press Ctrl-C to stop scan')
        try:
            while True:
                time.sleep(1.0)
        except KeyboardInterrupt:
            pass

        # end the scan and retrieve a ScanResults object containing
        # all the discovered BLE devices (not that this list does NOT
        # only contain SK8 devices!)
        results = dongle.end_scan()
        print('Found {} results'.format(len(results)))
Ejemplo n.º 7
0
def extana(port, device_name):
    with Dongle(
    ) as dongle:  # this will disconnect any active connections when it goes out of scope

        if not dongle.init(port):
            sys.exit('Failed to initialise dongle on port {}'.format(port))

        # use the scan_and_connect convenience function to scan for a list of
        # one or more devices and then attempt to connect to them if they are
        # detected in the scan results
        (result, devices) = dongle.scan_and_connect([device_name])

        if not result:
            # scan failed to detect device(s), or connection to those device(s)
            # failed for some reason
            sys.exit('Failed to establish connections to all devices')

        print('Connections created to {} devices'.format(len(devices)))

        # Each entry in this list is an SK8 object representing the physical device
        sk8 = devices[0]

        # Enable ExtAna streaming, optionally with internal IMU active too
        enable_imu = True
        if not sk8.enable_extana_streaming(enable_imu):
            sys.exit('Failed to enable streaming!')

        # can optionally get packets through a callback
        def eana_callback(ch1, ch2, temp, seq, timestamp, data):
            print(ch1, ch2, seq)

            # LED is green if both channels above threshold, red for channel 1
            # only, blue for channel 2 only, otherwise off
            if ch1 > FSR_THRESHOLD and ch2 > FSR_THRESHOLD:
                sk8.set_extana_led(*GREEN)
            elif ch1 > FSR_THRESHOLD:
                sk8.set_extana_led(*RED)
            elif ch2 > FSR_THRESHOLD:
                sk8.set_extana_led(*BLUE)
            else:
                sk8.set_extana_led(*OFF)

        sk8.set_extana_callback(eana_callback)

        while True:
            try:
                # retrieve latest set of analog data
                data = sk8.get_extana()
                # if IMU enabled above, this should return valid data too
                imu_data = sk8.get_imu(0)

                time.sleep(0.01)

            except KeyboardInterrupt:
                print('Disconnecting...')
                break

        sk8.disable_extana_streaming()
        sk8.disconnect()
Ejemplo n.º 8
0
    def test_get_set_device_name(self):
        with Dongle() as d:
            self.assertEqual(True, d.init(self.config['dongle_port']), 'Dongle init failed')
            (result, device) = d.connect_direct(self.config['device_addr'])
            self.assertEqual(result, True, 'Connection failed, result False')
            self.assertIsInstance(device, SK8, 'Returned object is not SK8 instance')
            self.assertIsNotNone(device, 'Returned SK8 instance is None')
            self.assertEqual(self.config['device_name'], device.get_device_name(), 'Device name mismatch: {} vs expected {}'.format(device.get_device_name(), self.config['device_name']))

            temp_name = 'temp_name'
            self.assertEqual(True, device.set_device_name(temp_name), 'Failed to set device name')
            self.assertEqual(temp_name, device.get_device_name(), 'Device name mismatch: {} vs expected {}'.format(device.get_device_name(), temp_name))
            
            self.assertEqual(True, device.set_device_name(self.config['device_name']), 'Failed to set device name')
            self.assertEqual(self.config['device_name'], device.get_device_name(), 'Device name mismatch: {} vs expected {}'.format(device.get_device_name(), self.config['device_name']))
Ejemplo n.º 9
0
 def test_dongle_init(self):
     with Dongle() as d:
         self.assertEqual(True, d.init(self.config['dongle_port'], hard_reset=False), "Dongle init failed")
         self.assertGreater(d.get_supported_connections(), 0, 'Supported connections should be at least 1, found {}'.format(d.get_supported_connections()))
Ejemplo n.º 10
0
 def test_find_port(self):
     port = Dongle.find_dongle_port()
     self.assertIsNotNone(port, 'find_dongle_port is None')
     self.assertEqual(port, self.config['dongle_port'], 'discovered port {} != defined port {}'.format(port, self.config['dongle_port']))
Ejemplo n.º 11
0
def samplerate(port, device_names):
    with Dongle(
    ) as dongle:  # this will disconnect any active connections when it goes out of scope

        if not dongle.init(port):
            sys.exit('Failed to initialise dongle on port {}'.format(port))

        # use the scan_and_connect convenience function to scan for a list of
        # one or more devices and then attempt to connect to them if they are
        # detected in the scan results
        (result, devices) = dongle.scan_and_connect(device_names, 5.0)

        if not result:
            # scan failed to detect device(s), or connection to those device(s)
            # failed for some reason
            sys.exit('Failed to establish connections to all devices')

        print('Connections created to {} devices'.format(len(devices)))

        # Enable IMU data streaming from the SK8 only (no external IMUs)
        for sk8 in devices:
            sk8.enable_imu_streaming([0, 1, 2, 3, 4],
                                     enabled_sensors=SENSOR_ACC | SENSOR_GYRO)
        # start_time = time.time()
        last_time = time.time()
        show_data = False

        logfile = open('log.txt', 'w')

        def imu_callback(acc, gyro, mag, index, seq, timestamp, data):
            if show_data:
                print('{}: [{}] acc={}, mag={}, gyro={}, seq={}'.format(
                    data, index, acc, mag, gyro, seq))
            logfile.write('{},{},{},{},{}\n'.format(data, index, seq,
                                                    timestamp, acc))

        for sk8 in devices:
            sk8.set_imu_callback(imu_callback, sk8.name)

        while True:
            try:
                if time.time() - last_time > 1.0:

                    # display the rate at which data packets are being received (from each IMU)
                    for sk8 in devices:
                        rates = []
                        drops = []
                        for i in sk8.get_enabled_imus():
                            imu = sk8.get_imu(i)
                            rates.append(imu.get_sample_rate())
                            drops.append(imu.get_packets_lost())

                        if -1 not in rates:
                            print('------------')
                            fmt_f = '{:.1f}|' * len(rates)
                            fmt_d = '{:02d}|' * len(rates)
                            print('Rates: ' + fmt_f.format(*rates))
                            print('Drops: ' + fmt_d.format(*drops))
                            print(devices[0].get_polling_override())
                        else:
                            print('Waiting...')

                    last_time = time.time()

                time.sleep(0.01)

                if kbhit():
                    ch = getch()
                    if ch == b's':
                        show_data = not show_data
                    if ch == b'1':
                        print(devices[0].set_polling_override(50))
                    if ch == b'0':
                        print(devices[0].set_polling_override(10))

            except KeyboardInterrupt:
                print('Disconnecting...')
                break

        for sk8 in devices:
            sk8.disable_imu_streaming()
            sk8.disconnect()
        logfile.close()
Ejemplo n.º 12
0
class SK8Calibration(QMainWindow, Ui_MainWindow):

    # scanning states
    SCAN_STATE_IDLE = 0
    SCAN_STATE_ACTIVE = 1
    SCAN_STATE_STOP = 2

    # calibration modes
    CAL_ACC = 0
    CAL_GYRO = 1
    CAL_MAG = 2
    CAL_NONE = 3

    # default filename for storing calibration data
    DEFAULT_CALIB_FILENAME = 'sk8calib.ini'

    # strings identifying the various calibration parameters
    ACCX_OFFSET = 'accx_offset'
    ACCY_OFFSET = 'accy_offset'
    ACCZ_OFFSET = 'accz_offset'
    ACCX_SCALE = 'accx_scale'
    ACCY_SCALE = 'accy_scale'
    ACCZ_SCALE = 'accz_scale'
    ACC_TIMESTAMP = 'acc_timestamp'

    GYROX_OFFSET = 'gyrox_offset'
    GYROY_OFFSET = 'gyroy_offset'
    GYROZ_OFFSET = 'gyroz_offset'
    GYRO_TIMESTAMP = 'gyro_timestamp'

    MAGX_OFFSET = 'magx_offset'
    MAGY_OFFSET = 'magy_offset'
    MAGZ_OFFSET = 'magz_offset'
    MAGX_SCALE = 'magx_scale'
    MAGY_SCALE = 'magy_scale'
    MAGZ_SCALE = 'magz_scale'
    MAG_TIMESTAMP = 'mag_timestamp'

    def __init__(self, dongle_port):
        QMainWindow.__init__(self)
        self.setupUi(self)

        # setup the dongle
        self.dongle = Dongle()
        self.dongle.init(dongle_port)

        # disable maximize button
        flags = self.windowFlags()
        flags ^= QtCore.Qt.WindowMaximizeButtonHint
        self.setWindowFlags(flags | QtCore.Qt.CustomizeWindowHint)

        # hook up various Qt GUI slots etc
        self.devicelist_model = QtGui.QStandardItemModel(self.lstDevices)
        self.lstDevices.setModel(self.devicelist_model)
        self.lblPort.setText('Dongle serial port: {}'.format(dongle_port))
        self.btnRefresh.clicked.connect(self.refresh_devices)
        self.btnCancelScan.clicked.connect(self.cancel_scan)
        self.lstDevices.clicked.connect(self.device_selected)
        self.btnConnect.clicked.connect(self.connect_device)
        self.btnConnect.setEnabled(False)
        self.btnAcc.clicked.connect(self.accel_calibration)
        self.btnGyro.clicked.connect(self.gyro_calibration)
        self.btnMag.clicked.connect(self.mag_calibration)
        self.btnExit.clicked.connect(self.exit)
        self.spinIMU.valueChanged.connect(self.imu_changed)
        self.spinIMU.setEnabled(False)

        self.gyro_dialog = None
        self.acc_dialog = None
        self.mag_dialog = None
        self.sk8 = None
        self.current_imuid = None
        self.calibration_state = self.CAL_NONE
        self.scan_state = self.SCAN_STATE_IDLE

        # start a 30ms timer to update displayed sensor data
        self.data_timer = QtCore.QTimer(self)
        self.data_timer.setInterval(30)
        self.data_timer.timeout.connect(self.update_data)

        # start a 3s timer to update battery level
        self.battery_timer = QtCore.QTimer(self)
        self.battery_timer.setInterval(3000)
        self.battery_timer.timeout.connect(self.update_battery)

        # attempt to parse any existing calibration file
        self.calibration_data = ConfigParser()
        self.calibration_data.read(
            os.path.join(os.path.dirname(__file__),
                         self.DEFAULT_CALIB_FILENAME))

        # begin a scan on startup
        self.refresh_devices()

    def get_current_data(self):
        """Return the calibration data for the current IMU, if any."""
        if self.current_imuid in self.calibration_data:
            return self.calibration_data[self.current_imuid]

        return {}

    def update_battery(self):
        """Updates the battery level in the UI for the connected SK8, if any"""
        if self.sk8 is None:
            return
        battery = self.sk8.get_battery_level()
        self.lblBattery.setText('Battery: {}%'.format(battery))

    def imu_changed(self, val):
        """Handle clicks on the IMU index spinner."""
        self.current_imuid = '{}_IMU{}'.format(self.sk8.get_device_name(), val)
        self.update_data_display(self.get_current_data())

    def accel_calibration(self):
        """Perform accelerometer calibration for current IMU."""
        self.calibration_state = self.CAL_ACC
        self.acc_dialog = SK8AccDialog(self.sk8.get_imu(self.spinIMU.value()),
                                       self)
        if self.acc_dialog.exec_() == QDialog.Rejected:
            return

        self.calculate_acc_calibration(self.acc_dialog.samples)

    def gyro_calibration(self):
        """Perform gyroscope calibration for current IMU."""
        QtWidgets.QMessageBox.information(
            self, 'Gyro calibration',
            'Ensure the selected IMU is in a stable, unmoving position, then click OK. Don\'t move the the IMU for a few seconds'
        )
        self.calibration_state = self.CAL_GYRO
        self.gyro_dialog = SK8GyroDialog(
            self.sk8.get_imu(self.spinIMU.value()), self)
        if self.gyro_dialog.exec_() == QDialog.Rejected:
            return

        self.calculate_gyro_calibration(self.gyro_dialog.samples)

    def mag_calibration(self):
        """Perform magnetometer calibration for current IMU."""
        self.calibration_state = self.CAL_MAG
        self.mag_dialog = SK8MagDialog(self.sk8.get_imu(self.spinIMU.value()),
                                       self)
        if self.mag_dialog.exec_() == QDialog.Rejected:
            return

        self.calculate_mag_calibration(self.mag_dialog.samples)

    def update_data(self):
        """Updates the displayed data in the GUI for the current IMU, applying
        current calibration if it is available. """
        if self.sk8 is not None:
            imu = self.spinIMU.value()
            data = self.sk8.get_imu(imu)
            if self.current_imuid in self.calibration_data:
                calib_data = self.calibration_data[self.current_imuid]
            else:
                calib_data = {}
                self.calibration_data[self.current_imuid] = calib_data

            if calib_data is not None and self.ACC_TIMESTAMP in calib_data:
                self.dataAccX.setText(
                    str(
                        int((data.acc[0] *
                             float(calib_data[self.ACCX_SCALE])) -
                            float(calib_data[self.ACCX_OFFSET]))))
                self.dataAccY.setText(
                    str(
                        int((data.acc[1] *
                             float(calib_data[self.ACCY_SCALE])) -
                            float(calib_data[self.ACCY_OFFSET]))))
                self.dataAccZ.setText(
                    str(
                        int((data.acc[2] *
                             float(calib_data[self.ACCZ_SCALE])) -
                            float(calib_data[self.ACCZ_OFFSET]))))
            else:
                self.dataAccX.setText(str(data.acc[0]))
                self.dataAccY.setText(str(data.acc[1]))
                self.dataAccZ.setText(str(data.acc[2]))

            if calib_data is not None and self.GYRO_TIMESTAMP in calib_data:
                self.dataGyroX.setText(
                    str(data.gyro[0] - int(calib_data[self.GYROX_OFFSET])))
                self.dataGyroY.setText(
                    str(data.gyro[1] - int(calib_data[self.GYROY_OFFSET])))
                self.dataGyroZ.setText(
                    str(data.gyro[2] - int(calib_data[self.GYROZ_OFFSET])))
            else:
                self.dataGyroX.setText(str(data.gyro[0]))
                self.dataGyroY.setText(str(data.gyro[1]))
                self.dataGyroZ.setText(str(data.gyro[2]))

            if calib_data is not None and self.MAG_TIMESTAMP in calib_data:
                self.dataAccX.setText(
                    str(
                        int((data.mag[0] *
                             float(calib_data[self.MAGX_SCALE])) -
                            float(calib_data[self.MAGX_OFFSET]))))
                self.dataAccY.setText(
                    str(
                        int((data.mag[1] *
                             float(calib_data[self.MAGY_SCALE])) -
                            float(calib_data[self.MAGY_OFFSET]))))
                self.dataAccZ.setText(
                    str(
                        int((data.mag[2] *
                             float(calib_data[self.MAGZ_SCALE])) -
                            float(calib_data[self.MAGZ_OFFSET]))))
            else:
                self.dataMagX.setText(str(data.mag[0]))
                self.dataMagY.setText(str(data.mag[1]))
                self.dataMagZ.setText(str(data.mag[2]))

    def calculate_gyro_calibration(self, gyro_samples):
        """Performs a basic gyroscope bias calculation. 

        Takes a list of (x, y, z) samples and averages over each axis to calculate
        the bias values, and stores them in the calibration data structure for the 
        currently connected SK8"""

        totals = [0, 0, 0]
        for gs in gyro_samples:
            totals[0] += gs[0]
            totals[1] += gs[1]
            totals[2] += gs[2]

        for i in range(3):
            totals[i] = int(float(totals[i]) / len(gyro_samples))

        print('Saving gyro offsets for {}'.format(self.current_imuid))
        self.calibration_data[self.current_imuid][self.GYROX_OFFSET] = str(
            totals[0])
        self.calibration_data[self.current_imuid][self.GYROY_OFFSET] = str(
            totals[1])
        self.calibration_data[self.current_imuid][self.GYROZ_OFFSET] = str(
            totals[2])
        self.calibration_data[self.current_imuid][
            self.GYRO_TIMESTAMP] = datetime.now().isoformat()
        self.write_calibration_data()
        self.update_data_display(self.calibration_data[self.current_imuid])
        self.calibration_state = self.CAL_NONE

    def calculate_acc_calibration(self, acc_samples):
        """Performs accelerometer calibration. Assumes acc_samples contains samples
        in order [+x, -x, +y, -y, +z, -z]. Calculates per-axis scale/offset values"""

        # assumes 2g range
        data = self.calibration_data[self.current_imuid]
        accx_pos = sum([x[0]
                        for x in acc_samples[0]]) / float(len(acc_samples[0]))
        accx_neg = sum([x[0]
                        for x in acc_samples[1]]) / float(len(acc_samples[1]))
        accy_pos = sum([y[1]
                        for y in acc_samples[2]]) / float(len(acc_samples[2]))
        accy_neg = sum([y[1]
                        for y in acc_samples[3]]) / float(len(acc_samples[3]))
        accz_pos = sum([z[2]
                        for z in acc_samples[4]]) / float(len(acc_samples[4]))
        accz_neg = sum([z[2]
                        for z in acc_samples[5]]) / float(len(acc_samples[5]))
        data[self.ACCX_SCALE] = str(2000.0 / (accx_pos - accx_neg))
        data[self.ACCY_SCALE] = str(2000.0 / (accy_pos - accy_neg))
        data[self.ACCZ_SCALE] = str(2000.0 / (accz_pos - accz_neg))

        data[self.ACCX_OFFSET] = str(int((accx_pos + accx_neg) / 2.0))
        data[self.ACCY_OFFSET] = str(int((accy_pos + accy_neg) / 2.0))
        data[self.ACCZ_OFFSET] = str(int((accz_pos + accz_neg) / 2.0))

        data[self.ACC_TIMESTAMP] = datetime.now().isoformat()

        self.write_calibration_data()
        self.update_data_display(self.calibration_data[self.current_imuid])
        self.calibration_state = self.CAL_NONE

    def calculate_mag_calibration(self, mag_samples):
        """Performs magnetometer calibration. Assumes mag_samples contains samples
        in order [+x, -x, +y, -y, +z, -z]. Calculates per-axis scale/offset values"""

        max_vals = [mag_samples[0][0], mag_samples[2][1], mag_samples[3][2]]
        min_vals = [mag_samples[1][0], mag_samples[3][1], mag_samples[5][2]]

        magbiases = [int((max_vals[i] + min_vals[i]) / 2.0) for i in range(3)]
        magscalings = [(max_vals[i] - min_vals[i]) / 2.0 for i in range(3)]
        avg_rads = sum(magscalings) / 3.0
        magscalings = [avg_rads / magscalings[i] for i in range(3)]

        data = self.calibration_data[self.current_imuid]

        data[self.MAGX_OFFSET] = str(int(magbiases[0]))
        data[self.MAGY_OFFSET] = str(int(magbiases[1]))
        data[self.MAGZ_OFFSET] = str(int(magbiases[2]))

        data[self.MAGX_SCALE] = str(magscalings[0])
        data[self.MAGY_SCALE] = str(magscalings[1])
        data[self.MAGZ_SCALE] = str(magscalings[2])

        data[self.MAG_TIMESTAMP] = datetime.now().isoformat()

        self.write_calibration_data()
        self.update_data_display(self.calibration_data[self.current_imuid])
        self.calibration_state = self.CAL_NONE

    def exit(self):
        # if a device is connected, disconnect it
        if self.sk8 is not None:
            self.sk8.disconnect()

        # if currently scanning, stop the scan
        if self.scan_state == self.SCAN_STATE_ACTIVE:
            self.dongle.end_scan()
        self.dongle.close()

        print('Writing calibration data')
        self.write_calibration_data()
        QtCore.QCoreApplication.exit()

    def write_calibration_data(self):
        with open(
                os.path.join(os.path.dirname(__file__),
                             self.DEFAULT_CALIB_FILENAME), 'w') as f:
            self.calibration_data.write(f)

    def scan_result_found(self, result):
        """Callback triggered when a BLE device is found by the scanning process"""
        self.devicelist_model.appendRow(ScanDeviceItem(result))

    def cancel_scan(self):
        if self.scan_state == self.SCAN_STATE_ACTIVE:
            self.scan_state = self.SCAN_STATE_IDLE
            print('Scanning stopped')
            self.dongle.end_scan()
            self.scan_state = self.SCAN_STATE_IDLE
            self.btnRefresh.setEnabled(True)
            self.btnCancelScan.setEnabled(False)

    def refresh_devices(self):
        if self.scan_state == self.SCAN_STATE_IDLE:
            self.devicelist_model.clear()
            self.scan_state = self.SCAN_STATE_ACTIVE
            self.dongle.begin_scan(self.scan_result_found)
            self.btnRefresh.setEnabled(False)
            self.btnCancelScan.setEnabled(True)

    def device_selected(self, index):
        """Handler for selecting a device from the list in the UI"""
        device = self.devicelist_model.itemFromIndex(index)
        print(device.device.addr)
        self.btnConnect.setEnabled(True)

    def update_data_display(self, data):
        """Triggered when the selected device/IMU is changed. Updates the background
        colours of the text widgets in the UI to indicate which sensors have calibration
        data available (green) and which do not (red)"""
        acc_cal = self.ACC_TIMESTAMP in data
        mag_cal = self.MAG_TIMESTAMP in data
        gyro_cal = self.GYRO_TIMESTAMP in data

        uncal = 'QLineEdit {background-color: #cc3333;};'
        cal = 'QLineEdit {background-color: #33cc33;};'

        if acc_cal:
            self.dataAccX.setStyleSheet(cal)
            self.dataAccY.setStyleSheet(cal)
            self.dataAccZ.setStyleSheet(cal)
        else:
            self.dataAccX.setStyleSheet(uncal)
            self.dataAccY.setStyleSheet(uncal)
            self.dataAccZ.setStyleSheet(uncal)

        if gyro_cal:
            self.dataGyroX.setStyleSheet(cal)
            self.dataGyroY.setStyleSheet(cal)
            self.dataGyroZ.setStyleSheet(cal)
        else:
            self.dataGyroX.setStyleSheet(uncal)
            self.dataGyroY.setStyleSheet(uncal)
            self.dataGyroZ.setStyleSheet(uncal)

        if mag_cal:
            self.dataMagX.setStyleSheet(cal)
            self.dataMagY.setStyleSheet(cal)
            self.dataMagZ.setStyleSheet(cal)
        else:
            self.dataMagX.setStyleSheet(uncal)
            self.dataMagY.setStyleSheet(uncal)
            self.dataMagZ.setStyleSheet(uncal)

    def connect_device(self):
        if self.sk8 is not None:
            # disconnect
            self.data_timer.stop()
            self.battery_timer.stop()
            self.sk8.disconnect()
            self.sk8 = None
            print('Disconnected')
            self.btnConnect.setText('Connect device')
            self.statusbar.showMessage('Disconnected')
            self.btnRefresh.setEnabled(True)
            self.spinIMU.setEnabled(False)
            self.btnAcc.setEnabled(False)
            self.btnGyro.setEnabled(False)
            self.btnMag.setEnabled(False)
            return

        # stop any scan still in progress
        self.cancel_scan()

        # retrieve the selected device object and try to connect
        selected = self.lstDevices.selectedIndexes()
        if len(selected) == 0:
            print('No device selected')
            return

        dev = self.devicelist_model.itemFromIndex(selected[0])
        if not self.dongle.connect([dev.device]):
            print('Failed to connect to device')
            return

        # configure the UI state for the new device
        self.sk8 = self.dongle.get_device(dev.device.addr)
        self.btnConnect.setText('Disconnect {}'.format(dev.device.name))
        self.statusbar.showMessage('Connected to {}'.format(dev.device.name))
        self.sk8.enable_imu_streaming([0, 1, 2, 3, 4])
        self.data_timer.start()
        self.update_battery()
        self.battery_timer.start()
        self.btnRefresh.setEnabled(False)
        self.spinIMU.setEnabled(True)
        self.btnAcc.setEnabled(True)
        self.btnGyro.setEnabled(True)
        self.btnMag.setEnabled(True)

        # load existing calibration, if any
        devname = self.sk8.get_device_name()
        imu = self.spinIMU.value()
        self.current_imuid = '{}_IMU{}'.format(devname, imu)
        if self.current_imuid in self.calibration_data:
            print('Found existing calibration data for {}'.format(
                self.current_imuid))
        else:
            print('No calibration data for {}'.format(devname))
            for i in range(5):
                data = {}
                self.calibration_data[self.current_imuid] = data

        self.update_data_display(self.calibration_data[self.current_imuid])