diff --git a/experimental/devices/tlv493d/doc.go b/experimental/devices/tlv493d/doc.go new file mode 100644 index 0000000..b2bb21e --- /dev/null +++ b/experimental/devices/tlv493d/doc.go @@ -0,0 +1,23 @@ +// Copyright 2020 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +// Package tlv493d implements interfacing code to the Infineon TLV493D haff effect sensor. +// +// Features of the device: +// 3-dimensional hall effect sensor, measures up to +/-130 mT magnetic flux. +// temperature sensor +// i2c interface +// 12-bit resolution +// low power consumption +// +// Features of the driver: +// Implemented all options of the device +// Power modes described in the documentation are defined as constants +// 2 precisions: high precision (12 bits), where all registers are read or low precision, which saves 50% of I2C bandwidth, but without temperature and only 8-bit resolution +// Continuous reading mode +// +// Datasheet and application notes: +// https://www.infineon.com/cms/en/product/sensor/magnetic-sensors/magnetic-position-sensors/3d-magnetics/tlv493d-a1b6/ +// +package tlv493d diff --git a/experimental/devices/tlv493d/example_test.go b/experimental/devices/tlv493d/example_test.go new file mode 100644 index 0000000..9b6ec50 --- /dev/null +++ b/experimental/devices/tlv493d/example_test.go @@ -0,0 +1,58 @@ +// Copyright 2020 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package tlv493d_test + +import ( + "fmt" + "log" + + "periph.io/x/periph/conn/i2c/i2creg" + "periph.io/x/periph/conn/physic" + "periph.io/x/periph/experimental/devices/tlv493d" + "periph.io/x/periph/host" +) + +func Example() { + // Make sure periph is initialized. + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + // Open default I²C bus. + bus, err := i2creg.Open("") + if err != nil { + log.Fatalf("failed to open I²C: %v", err) + } + defer bus.Close() + + // Create a new TLV493D hall effect sensor. + tlv, err := tlv493d.New(bus, &tlv493d.DefaultOpts) + if err != nil { + log.Fatalln(err) + } + defer tlv.Halt() + + // Read a single value. + tlv.SetMode(tlv493d.LowPowerMode) + fmt.Println("Single reading") + reading, err := tlv.Read(tlv493d.HighPrecisionWithTemperature) + + if err != nil { + log.Fatalln(err) + } + + fmt.Println(reading) + + // Read values continuously from the sensor. + fmt.Println("Continuous reading") + c, err := tlv.ReadContinuous(100*physic.Hertz, tlv493d.LowPrecision) + if err != nil { + log.Fatalln(err) + } + + for reading := range c { + fmt.Println(reading) + } +} diff --git a/experimental/devices/tlv493d/tlv493d.go b/experimental/devices/tlv493d/tlv493d.go new file mode 100644 index 0000000..7f3c292 --- /dev/null +++ b/experimental/devices/tlv493d/tlv493d.go @@ -0,0 +1,473 @@ +// Copyright 2020 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package tlv493d + +import ( + "errors" + "fmt" + "sync" + "time" + + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/physic" +) + +// I2CAddr is the default I2C address for the TLV493D component. +const I2CAddr uint16 = 0x5e + +// I2CAddr1 is an alternative I2C address for TLV493D components. +const I2CAddr1 uint16 = 0x1f + +// Precision represents a request for a compromise between I2C bandwidth +// versus measurement precision. +type Precision int + +const ( + // HighPrecisionWithTemperature reads the full 12-bit value for each axis + // and the temperature + HighPrecisionWithTemperature Precision = 0 + // LowPrecision reads only 8-bits for each axis. Temperature is not read. + LowPrecision Precision = 1 +) + +const ( + numberOfReadRegisters = 10 + numberOfMeasurementRegisters = 7 + numberOfFastMeasurementRegisters = 3 + startupDelay = 40 * time.Millisecond + commandRecovery = 0xff + + registerBx = 0 + registerBy = 1 + registerBz = 2 + registerTemp = 3 + registerBx2 = 4 + registerBz2 = 5 + registerTemp2 = 6 + registerFactSet1 = 7 + registerFactSet2 = 8 + registerFactSet3 = 9 + + bitParity = 7 + bitFastMode = 1 + bitLowPowerMode = 0 + bitLowPowerPeriod = 6 + bitInterruptPad = 2 + bitTemperatureMeasurement = 7 + bitParityTest = 5 + + magneticFluxScaling = 98 * physic.MicroTesla + temperatureScaling = 1100 * physic.MilliCelsius + temperatureScalingDiv = 10 + referenceTemperature = 25*physic.Celsius + physic.ZeroCelsius +) + +// Mode reprents the various power modes described in the documentation +type Mode struct { + fastMode bool + lowPowerMode bool + lowPowerPeriod bool + timeToMeasure time.Duration + measurementFrequency physic.Frequency +} + +// PowerDownMode shuts down the sensor. It can still reply to I2C commands. +var PowerDownMode = Mode{ + fastMode: false, + lowPowerMode: false, + lowPowerPeriod: false, + timeToMeasure: 10 * time.Millisecond, + measurementFrequency: 1 * physic.Hertz, +} + +// FastMode is the mode using the most energy +var FastMode = Mode{ + fastMode: true, + lowPowerMode: false, + lowPowerPeriod: false, + timeToMeasure: 0, + measurementFrequency: 3300 * physic.Hertz, +} + +// LowPowerMode uses less energy than FastMode, with lower measurement rate +var LowPowerMode = Mode{ + fastMode: false, + lowPowerMode: true, + lowPowerPeriod: true, + timeToMeasure: 0, + measurementFrequency: 100 * physic.Hertz, +} + +// UltraLowPowerMode saves the most energy but with very low measurement rate +var UltraLowPowerMode = Mode{ + fastMode: false, + lowPowerMode: true, + lowPowerPeriod: false, + timeToMeasure: 0, + measurementFrequency: 10 * physic.Hertz, +} + +// MasterControlledMode refer to TLV493D documentation on how to use this mode +var MasterControlledMode = Mode{ + fastMode: true, + lowPowerMode: true, + lowPowerPeriod: true, + timeToMeasure: 0, + measurementFrequency: 3300 * physic.Hertz, +} + +// Opts holds the configuration options. +type Opts struct { + I2cAddress uint16 + Reset bool + Mode Mode + InterruptPadEnabled bool // If enabled, this can cause I2C failures. See documentation. + EnableTemperatureMeasurement bool // Disable to save power. + ParityTestEnabled bool + TemperatureOffsetCompensation int +} + +// DefaultOpts are the recommended default options. +var DefaultOpts = Opts{ + I2cAddress: I2CAddr, + Reset: true, + Mode: PowerDownMode, + EnableTemperatureMeasurement: true, + InterruptPadEnabled: false, + ParityTestEnabled: true, + TemperatureOffsetCompensation: 340, // As per the documentation, can be calibrated for better precision +} + +// Sample contains the metrics measured by the sensor +type Sample struct { + Bx physic.MagneticFluxDensity + By physic.MagneticFluxDensity + Bz physic.MagneticFluxDensity + Temperature physic.Temperature +} + +// Dev is an handle to a TLV493D hall effect sensor. +type Dev struct { + mu sync.Mutex + i2c i2c.Dev + stop chan struct{} + continuousReadWG sync.WaitGroup + + registersBuffer []byte + + mode Mode + enableTemperatureMeasurement bool + interruptPadEnabled bool + parityTestEnabled bool + + temperatureOffsetCompensation int +} + +// New creates a new TLV493D driver for a 3D hall effect sensors +func New(i i2c.Bus, opts *Opts) (*Dev, error) { + switch opts.I2cAddress { + case I2CAddr, I2CAddr1: + default: + return nil, errors.New("TLV493D: given address not supported by device") + } + + d := &Dev{ + i2c: i2c.Dev{Bus: i, Addr: opts.I2cAddress}, + mode: opts.Mode, + enableTemperatureMeasurement: opts.EnableTemperatureMeasurement, + interruptPadEnabled: opts.InterruptPadEnabled, + parityTestEnabled: opts.ParityTestEnabled, + temperatureOffsetCompensation: opts.TemperatureOffsetCompensation, + registersBuffer: make([]byte, numberOfReadRegisters), + } + if err := d.initialize(opts); err != nil { + return nil, err + } + return d, nil +} + +// String implements conn.Resource. +func (d *Dev) String() string { + return "TLV493D" +} + +// Halt implements conn.Resource. +func (d *Dev) Halt() error { + // Stop any continuous read + d.StopContinousRead() + + return d.SetMode(PowerDownMode) +} + +func (d *Dev) initialize(opts *Opts) error { + d.mu.Lock() + defer d.mu.Unlock() + + time.Sleep(startupDelay) + + // Send recovery + if err := d.i2c.Tx([]byte{commandRecovery}, nil); err != nil { + return err + } + + if opts.Reset { + // Reset I2C address + var resetAddress byte = 0x00 + if d.i2c.Addr == I2CAddr1 { + resetAddress = 0xff + } + if err := d.i2c.Tx([]byte{resetAddress}, nil); err != nil { + return err + } + } + + // Read all 10 registers and store it as the initial data + if err := d.i2c.Tx([]byte{registerBx}, d.registersBuffer); err != nil { + return err + } + + // Configure sensor + return d.configure() +} + +func (d *Dev) configure() error { + + // Configure sensor + config1 := d.registersBuffer[registerFactSet1] + config2 := d.registersBuffer[registerFactSet3] + + // Unset parity bit first + config1 = setBit(config1, bitParity, false) + + // Mode + config1 = setBit(config1, bitFastMode, d.mode.fastMode) + config1 = setBit(config1, bitLowPowerMode, d.mode.lowPowerMode) + config2 = setBit(config2, bitLowPowerPeriod, d.mode.lowPowerPeriod) + + // Temperature: set to 0 to enable it + config2 = setBit(config2, bitTemperatureMeasurement, !d.enableTemperatureMeasurement) + + // Other configuration bits + config1 = setBit(config1, bitInterruptPad, d.interruptPadEnabled) + config2 = setBit(config2, bitParityTest, d.parityTestEnabled) + + configBuffer := []byte{ + 0x00, + config1, + d.registersBuffer[registerFactSet2], + config2, + } + + // Parity: the number of bits set must be odd + // As we unset the parity bit first, if the number of bits is currently even, we have to set it + // to make the number of bits set odd + configBuffer[1] = setBit(config1, 7, isNumberOfBitsEven(configBuffer)) + + if err := d.i2c.Tx(configBuffer, nil); err != nil { + return fmt.Errorf("unable to read configuration: %#v", err) + } + + return nil +} + +// SetMode sets the power mode of the sensor +func (d *Dev) SetMode(mode Mode) error { + d.mu.Lock() + defer d.mu.Unlock() + + d.mode = mode + return d.configure() +} + +// EnableTemperatureMeasurement controls the temperature sensor activation +func (d *Dev) EnableTemperatureMeasurement(enable bool) error { + d.mu.Lock() + defer d.mu.Unlock() + + d.enableTemperatureMeasurement = enable + return d.configure() +} + +// EnableInterruptions controls if the sensor should send interruption of new measurement +func (d *Dev) EnableInterruptions(enable bool) error { + d.mu.Lock() + defer d.mu.Unlock() + + d.interruptPadEnabled = enable + return d.configure() +} + +// EnableParityTest controls if the sensor should control the parity of the data transmitted +func (d *Dev) EnableParityTest(enable bool) error { + d.mu.Lock() + defer d.mu.Unlock() + + d.parityTestEnabled = enable + return d.configure() +} + +// Read returns a sample from the last measurement of the sensor +func (d *Dev) Read(precision Precision) (Sample, error) { + d.mu.Lock() + defer d.mu.Unlock() + + if precision == LowPrecision { + return d.readLowPrecision() + } + + return d.readHighPrecision() +} + +func (d *Dev) readLowPrecision() (Sample, error) { + // The information we need is in the first 3 registers + if err := d.i2c.Tx([]byte{registerBx}, d.registersBuffer[:numberOfFastMeasurementRegisters]); err != nil { + return Sample{}, err + } + buf := d.registersBuffer + + // The values are signed: + // convert uint8 to int8, then convert to int to preserve the sign + rawBx := int(int8(buf[registerBx])) << 4 + rawBy := int(int8(buf[registerBy])) << 4 + rawBz := int(int8(buf[registerBz])) << 4 + + return Sample{ + Bx: magneticFluxScaling * physic.MagneticFluxDensity(rawBx), + By: magneticFluxScaling * physic.MagneticFluxDensity(rawBy), + Bz: magneticFluxScaling * physic.MagneticFluxDensity(rawBz), + }, nil +} + +func (d *Dev) readHighPrecision() (Sample, error) { + // The information we need is in the first 7 registers + if err := d.i2c.Tx([]byte{registerBx}, d.registersBuffer[:numberOfMeasurementRegisters]); err != nil { + return Sample{}, err + } + buf := d.registersBuffer + + // The values are signed: + // convert uint8 to int8, then convert to int to preserve the sign + rawBx := (int(int8(buf[registerBx])) << 4) | (int(buf[registerBx2]&0xf0) >> 4) + rawBy := (int(int8(buf[registerBy])) << 4) | int(buf[registerBx2]&0x0f) + rawBz := (int(int8(buf[registerBz])) << 4) | int(buf[registerBz2]&0x0f) + rawTemp := (int(int8(buf[registerTemp]&0xf0)) << 4) | int(buf[registerTemp2]) + + // Compute measurement based upon reference documentation + temp := physic.Temperature(rawTemp-d.temperatureOffsetCompensation)*temperatureScaling + referenceTemperature + + return Sample{ + Bx: magneticFluxScaling * physic.MagneticFluxDensity(rawBx), + By: magneticFluxScaling * physic.MagneticFluxDensity(rawBy), + Bz: magneticFluxScaling * physic.MagneticFluxDensity(rawBz), + Temperature: temp, + }, nil +} + +// ReadContinuous returns a channel which will receive readings at regular intervals +func (d *Dev) ReadContinuous(frequency physic.Frequency, precision Precision) (<-chan Sample, error) { + // First release the current continuous reading if there is one + d.StopContinousRead() + reading := make(chan Sample, 16) + d.stop = make(chan struct{}) + + // Choose the best operating mode for the sensor + newMode, err := bestModeForFrequency(frequency) + if err != nil { + return nil, err + } + + previousMode := d.mode + d.SetMode(newMode) + + t := time.NewTicker(frequency.Period()) + + d.continuousReadWG.Add(1) + + go func(s <-chan struct{}) { + defer d.SetMode(previousMode) + defer t.Stop() + defer close(reading) + defer d.continuousReadWG.Done() + + for { + select { + case <-s: + return + case <-t.C: + value, err := d.Read(precision) + if err != nil { + // In continuous mode, we'll ignore errors silently. + continue + } + reading <- value + } + } + }(d.stop) + + return reading, nil +} + +// StopContinousRead stops a currently running continuous read +func (d *Dev) StopContinousRead() { + if d.stop == nil { + return + } + + d.stop <- struct{}{} + d.stop = nil + d.continuousReadWG.Wait() +} + +func bestModeForFrequency(frequency physic.Frequency) (Mode, error) { + + allowed := []*Mode{&FastMode, &LowPowerMode, &UltraLowPowerMode} + var minAbove *Mode = nil + + for _, m := range allowed { + if m.measurementFrequency >= frequency { + if minAbove == nil || minAbove.measurementFrequency > m.measurementFrequency { + minAbove = m + } + } + } + + if minAbove == nil { + return PowerDownMode, fmt.Errorf("frequency too high, no mode available for %s", frequency) + } + + return *minAbove, nil +} + +// Sets the bit at pos in the integer n. +func setBit(n byte, pos uint, isSet bool) byte { + var bit byte = (1 << pos) + if isSet { + return n | bit + } + return n &^ bit +} + +func isNumberOfBitsEven(array []byte) bool { + var accumulator byte = 0 + + // The keys is to use XOR + // 1 ^ 1 -> 0 (even) + // 0 ^ 1 -> 1 (odd) + // 1 ^ 0 -> 1 (odd) + // 0 ^ 0 -> 0 (even) + + // Combine all bytes + for _, b := range array { + accumulator ^= b + } + + // Combine adjacent bits + accumulator ^= (accumulator >> 1) + accumulator ^= (accumulator >> 2) + accumulator ^= (accumulator >> 4) + + // Parity is in the LSB + return !((accumulator & 0x01) == 1) +} diff --git a/experimental/devices/tlv493d/tlv493d_test.go b/experimental/devices/tlv493d/tlv493d_test.go new file mode 100644 index 0000000..089b39f --- /dev/null +++ b/experimental/devices/tlv493d/tlv493d_test.go @@ -0,0 +1,345 @@ +// Copyright 2020 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package tlv493d + +import ( + "testing" + + "periph.io/x/periph/conn/i2c/i2ctest" + "periph.io/x/periph/conn/physic" +) + +func TestDev_String(t *testing.T) { + b := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Recovery + { + Addr: 0x5e, + W: []byte{0xff}, + R: []byte{}, + }, + // Reset + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{}, + }, + // Read configuration + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{0xfd, 0x2d, 0x79, 0x14, 0xab, 0x22, 0x51, 0x81, 0x4, 0x60}, + }, + // Configure + { + Addr: 0x5e, + W: []byte{0x0, 0x80, 0x4, 0x20}, + R: []byte{}, + }, + // Halt: power down + { + Addr: 0x5e, + W: []byte{0x0, 0x80, 0x4, 0x20}, + R: []byte{}, + }, + }, + } + defer b.Close() + + d, err := New(&b, &DefaultOpts) + if err != nil { + t.Fatal(err) + } + if s := d.String(); s != "TLV493D" { + t.Fatal(s) + } + if err := d.Halt(); err != nil { + t.Fatal(err) + } +} + +func TestTLV493D_Read(t *testing.T) { + b := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Recovery + { + Addr: 0x5e, + W: []byte{0xff}, + R: []byte{}, + }, + // Reset + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{}, + }, + // Read configuration + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{0xfd, 0x2d, 0x79, 0x14, 0xab, 0x22, 0x51, 0x81, 0x4, 0x60}, + }, + // Configure + { + Addr: 0x5e, + W: []byte{0x0, 0x81, 0x4, 0x60}, + R: []byte{}, + }, + // Read measurements + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{0xfd, 0x2d, 0x79, 0x18, 0xbb, 0x31, 0x51}, + }, + // Halt: power down + { + Addr: 0x5e, + W: []byte{0x0, 0x80, 0x4, 0x20}, + R: []byte{}, + }, + }, + } + defer b.Close() + + opts := DefaultOpts + opts.Mode = LowPowerMode + + d, err := New(&b, &opts) + if err != nil { + t.Fatal(err) + } + + // Read values from ADC. + reading, err := d.Read(HighPrecisionWithTemperature) + if err != nil { + t.Fatal(err) + } + + assertSample(t, Sample{ + Bx: -3626 * physic.MicroTesla, + By: 71638 * physic.MicroTesla, + Bz: 189826 * physic.MicroTesla, + Temperature: 294850 * physic.MilliKelvin, + }, reading) + + if err := d.Halt(); err != nil { + t.Fatal(err) + } +} + +func TestTLV493D_ReadContinous(t *testing.T) { + b := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Recovery + { + Addr: 0x5e, + W: []byte{0xff}, + R: []byte{}, + }, + // Reset + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{}, + }, + // Read configuration + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{0xfd, 0x2d, 0x79, 0x14, 0xab, 0x22, 0x51, 0x81, 0x4, 0x60}, + }, + // Configure + { + Addr: 0x5e, + W: []byte{0x0, 0x80, 0x4, 0x20}, + R: []byte{}, + }, + // Configure for continuous mode + { + Addr: 0x5e, + W: []byte{0x0, 0x81, 0x4, 0x60}, + R: []byte{}, + }, + // Read measurements + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{0xfd, 0x2d, 0x79}, + }, + // Read measurements + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{0xf3, 0xd5, 0xa}, + }, + // End of continuous reading, restore previous mode + { + Addr: 0x5e, + W: []byte{0x0, 0x80, 0x4, 0x20}, + R: []byte{}, + }, + // Halt: power down + { + Addr: 0x5e, + W: []byte{0x0, 0x80, 0x4, 0x20}, + R: []byte{}, + }, + }, + } + defer b.Close() + + samples := []Sample{ + { + Bx: -4704 * physic.MicroTesla, + By: 70560 * physic.MicroTesla, + Bz: 189728 * physic.MicroTesla, + }, + { + Bx: -20384 * physic.MicroTesla, + By: -67424 * physic.MicroTesla, + Bz: 15680 * physic.MicroTesla, + }, + } + + d, err := New(&b, &DefaultOpts) + if err != nil { + t.Fatal(err) + } + + // Read values from sensor. + c, err := d.ReadContinuous(100*physic.Hertz, LowPrecision) + if err != nil { + t.Fatal(err) + } + + var i = 0 + for reading := range c { + assertSample(t, samples[i], reading) + + i++ + if i >= len(samples) { + break + } + } + + d.StopContinousRead() + + if err := d.Halt(); err != nil { + t.Fatal(err) + } +} + +func TestTLV493D_Configuration(t *testing.T) { + b := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Recovery + { + Addr: 0x5e, + W: []byte{0xff}, + R: []byte{}, + }, + // Reset + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{}, + }, + // Read configuration + { + Addr: 0x5e, + W: []byte{0x0}, + R: []byte{0xfd, 0x2d, 0x79, 0x14, 0xab, 0x22, 0x51, 0x81, 0x4, 0x60}, + }, + // Configure + { + Addr: 0x5e, + W: []byte{0x0, 0x80, 0x4, 0x20}, + R: []byte{}, + }, + // Configure for UltraLowPowerMode + { + Addr: 0x5e, + W: []byte{0x0, 0x1, 0x4, 0x20}, + R: []byte{}, + }, + // Disable temperature measurement + { + Addr: 0x5e, + W: []byte{0x0, 0x81, 0x4, 0xa0}, + R: []byte{}, + }, + // Disable parity test + { + Addr: 0x5e, + W: []byte{0x0, 0x1, 0x4, 0x80}, + R: []byte{}, + }, + // Disable interruptions + { + Addr: 0x5e, + W: []byte{0x0, 0x85, 0x4, 0x80}, + R: []byte{}, + }, + // Halt: power down + { + Addr: 0x5e, + W: []byte{0x0, 0x4, 0x4, 0x80}, + R: []byte{}, + }, + }, + } + defer b.Close() + + opts := DefaultOpts + + d, err := New(&b, &opts) + if err != nil { + t.Fatal(err) + } + + // Change configuration items + err = d.SetMode(UltraLowPowerMode) + if err != nil { + t.Fatal(err) + } + + err = d.EnableTemperatureMeasurement(false) + if err != nil { + t.Fatal(err) + } + + err = d.EnableParityTest(false) + if err != nil { + t.Fatal(err) + } + + err = d.EnableInterruptions(true) + if err != nil { + t.Fatal(err) + } + + if err := d.Halt(); err != nil { + t.Fatal(err) + } +} + +func assertSample(t *testing.T, expected Sample, actual Sample) { + if actual.Bx != expected.Bx { + t.Fatalf("Bx: Found %d, expected %d", actual.Bx, expected.Bx) + } + + if actual.By != expected.By { + t.Fatalf("By: Found %d, expected %d", actual.By, expected.By) + } + + if actual.Bz != expected.Bz { + t.Fatalf("Bz: Found %d, expected %d", actual.Bz, expected.Bz) + } + + if actual.Temperature != expected.Temperature { + t.Fatalf("Temperature: Found %d, expected %d", actual.Temperature, expected.Temperature) + } + +}