com.diozero.devices.sandpit.Max30102 Maven / Gradle / Ivy
The newest version!
/*-
* #%L
* Organisation: diozero
* Project: diozero - Core
* Filename: Max30102.java
*
* This file is part of the diozero project. More information about this project
* can be found at https://www.diozero.com/.
* %%
* Copyright (C) 2016 - 2024 diozero
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* #L%
*/
package com.diozero.devices.sandpit;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.tinylog.Logger;
import com.diozero.api.DeviceInterface;
import com.diozero.api.I2CConstants;
import com.diozero.api.I2CDevice;
import com.diozero.api.I2CDeviceInterface;
import com.diozero.api.RuntimeIOException;
import com.diozero.util.DiozeroScheduler;
import com.diozero.util.Hex;
import com.diozero.util.RangeUtil;
import com.diozero.util.SleepUtil;
/**
* maxim integrated High-sensitivity Pulse Oximeter and Heart-rate Sensor.
* Datasheet
*
*
* INT IRD RD GND
* GND SCL SDA VIN
*
* INT: Interrupt pin
* IRD: The IR LED ground of the MAX30102 chip, generally not connected
* RD: RED LED ground terminal of MAX30102 chip, generally not connected
* GND: Ground
* SCL: I2C Clock
* SDA: I2C Data
* VIN: The main power input terminal 1.8-5V
*
* 3-bit pad: Select the pull-up level of the bus, depending on the pin master voltage,
* select 1.8v or 3_3v (this terminal contains 3.3V and above)
*
*
* The active-low interrupt pin pulls low when an interrupt is triggered. The
* pin is open-drain, meaning it requires a pull-up resistor (min 4.7kOhm).
*
* Credit to:
* https://makersportal.com/blog/2019/6/24/arduino-heart-rate-monitor-using-max30102-and-pulse-oximetry
* https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library
* https://github.com/doug-burrell/max30102/blob/master/max30102.py
*/
public class Max30102 implements DeviceInterface {
public static final int FIFO_ALMOST_FULL_MAX = 15;
public static final float LED_MAX_CURRENT_MA = 51f;
// Default device address
private static int DEVICE_ADDRESS = 0x57;
// Registers
private static final int REG_INTR_STATUS_1 = 0x00;
private static final int REG_INTR_STATUS_2 = 0x01;
private static final int REG_INTR_ENABLE_1 = 0x02;
private static final int REG_INTR_ENABLE_2 = 0x03;
private static final int REG_FIFO_WRITE_PTR = 0x04;
private static final int REG_OVERFLOW_CTR = 0x05;
private static final int REG_FIFO_READ_PTR = 0x06;
private static final int REG_FIFO_DATA = 0x07;
private static final int REG_FIFO_CONFIG = 0x08;
private static final int REG_MODE_CONFIG = 0x09;
private static final int REG_SPO2_CONFIG = 0x0a;
private static final int REG_LED1_PULSE_AMPL = 0x0c;
private static final int REG_LED2_PULSE_AMPL = 0x0d;
private static final int REG_PILOT_PA = 0x10;
private static final int REG_MULTI_LED_CTRL1 = 0x11;
private static final int REG_MULTI_LED_CTRL2 = 0x12;
private static final int REG_DIE_TEMP_INT = 0x1f;
private static final int REG_DIE_TEMP_FRC = 0x20;
private static final int REG_DIE_TEMP_CONFIG = 0x21;
private static final int REG_REVISION_ID = 0xfe;
private static final int REG_PART_ID = 0xff;
private static final byte PART_ID = 0x15;
// Interrupt Status / Enable #1 (0x00 / 0x02)
private static final int INT1_FIFO_ALMOST_FULL_BIT = 7;
/**
* FIFO Almost Full Flag bit mask.
*
* In SpO2 and HR modes, this interrupt triggers when the FIFO write pointer has
* a certain number of free spaces remaining. The trigger can be set by the
* FIFO_A_FULL[3:0] register. The interrupt is cleared by reading the Interrupt
* Status 1 register (0x00).
*/
private static final int INT1_FIFO_ALMOST_FULL_MASK = 1 << INT1_FIFO_ALMOST_FULL_BIT;
private static final int INT1_PPG_RDY_BIT = 6;
/**
* New FIFO Data Ready bit mask.
*
* In SpO2 and HR modes, this interrupt triggers when there is a new sample in
* the data FIFO. The interrupt is cleared by reading the Interrupt Status 1
* register (0x00), or by reading the FIFO_DATA register.
*/
private static final byte INT1_PPG_RDY_DATA_MASK = 1 << INT1_PPG_RDY_BIT;
private static final int INT1_ALC_OVF_BIT = 5;
/**
* Ambient Light Cancellation Overflow bit mask.
*
* Ambient Light Cancellation Overflow This interrupt triggers when the ambient
* light cancellation function of the SpO2/HR photodiode has reached its maximum
* limit, and therefore, ambient light is affecting the output of the ADC. The
* interrupt is cleared by reading the Interrupt Status 1 register (0x00).
*/
private static final byte INT1_ALC_OVF_MASK = 1 << INT1_ALC_OVF_BIT;
private static final int INT1_PWR_RDY_BIT = 0;
/**
* Power Ready Flag bit mask.
*
* On power-up a power-ready interrupt is triggered to signal that the module is
* powered-up and ready to collect data.
*
* Note can only be read from the Interrupt Status 1 register, cannot be written
* to the Interrupt Enable 1 register.
*/
private static final byte INT1_PWR_RDY_MASK = 1 << INT1_PWR_RDY_BIT;
// Interrupt Status #2 (0x01 / 0x03)
private static final int INT2_DIE_TEMP_RDY_BIT = 1;
/**
* Internal Temperature Ready Flag bit mask.
*
* When an internal die temperature conversion is finished, this interrupt is
* triggered so the processor can read the temperature data registers. The
* interrupt is cleared by reading either the Interrupt Status 2 register (0x01)
* or the TFRAC register (0x20).
*/
private static final byte INT2_DIE_TEMP_RDY_MASK = 1 << INT2_DIE_TEMP_RDY_BIT;
// FIFO Config (0x08)
// Mode Configuration (0x09)
private static final int MODE_CONFIG_SHUTDOWN_BIT = 7;
private static final int MODE_CONFIG_SHUTDOWN_MASK = 1 << MODE_CONFIG_SHUTDOWN_BIT;
private static final int MODE_CONFIG_RESET_BIT = 6;
private static final byte MODE_CONFIG_RESET_MASK = 1 << MODE_CONFIG_RESET_BIT;
// Die Temperate Config (0x21)
private static final int DIE_TEMP_EN_BIT = 0;
private static final int DIE_TEMP_EN_MASK = 1 << DIE_TEMP_EN_BIT;
public enum SampleAveraging {
_1(0b000), _2(0b001), _4(0b010), _8(0b011), _16(0b100), _32(0b101);
private static final int BIT_SHIFT = 5;
private byte mask;
SampleAveraging(int val) {
this.mask = (byte) (val << BIT_SHIFT);
}
byte getMask() {
return mask;
}
}
/**
* Controls the behaviour of the FIFO when it becomes completely filled with
* data
*/
public enum FifoRolloverOnFull {
/**
* The FIFO address rolls over to zero and the FIFO continues to fill with new
* data
*/
ENABLED(1),
/**
* The FIFO is not updated until FIFO_DATA is read or the WRITE/READ positions
* are changed
*/
DISABLED(0);
private static final int BIT_SHIFT = 4;
private byte mask;
FifoRolloverOnFull(int val) {
this.mask = (byte) (val << BIT_SHIFT);
}
byte getMask() {
return mask;
}
}
// Mode Configuration (0x09)
public enum Mode {
HEART_RATE(0b010), SPO2(0b011), MULTI_LED(0b111);
private static final int BIT_SHIFT = 0;
private byte mask;
Mode(int val) {
this.mask = (byte) (val << BIT_SHIFT);
}
byte getMask() {
return mask;
}
}
// SpO2 Configuration (0x0A)
public enum SpO2AdcRange {
_2048(0b00, 7.81f, 2048), _4096(0b01, 15.63f, 4096), _8192(0b10, 31.25f, 8192), _16384(0b11, 62.5f, 16384);
private static final int BIT_SHIFT = 5;
private byte mask;
private float lsbSize;
private int fullScale;
SpO2AdcRange(int val, float lsbSize, int fullScale) {
this.mask = (byte) (val << BIT_SHIFT);
this.lsbSize = lsbSize;
this.fullScale = fullScale;
}
byte getMask() {
return mask;
}
public float getLsbSize() {
return lsbSize;
}
public int getFullScale() {
return fullScale;
}
}
public enum SpO2SampleRate {
_50(0b000, 50), _100(0b001, 100), _200(0b010, 200), _400(0b011, 400), _800(0b100, 800), _1000(0b101, 1000),
_1600(0b110, 1600), _3200(0b111, 3200);
private static final int BIT_SHIFT = 2;
private byte mask;
private int sampleRate;
SpO2SampleRate(int val, int sampleRate) {
this.mask = (byte) (val << BIT_SHIFT);
this.sampleRate = sampleRate;
}
byte getMask() {
return mask;
}
public int getSampleRate() {
return sampleRate;
}
}
public enum LedPulseWidth {
_69(0b00, 68.95f, 15), _118(0b01, 117.78f, 16), _215(0b10, 215.44f, 17), _411(0b11, 410.75f, 18);
private static final int BIT_SHIFT = 0;
private byte mask;
private float pulseWidthUs;
private int adcResolution;
LedPulseWidth(int val, float pulseWidthUs, int adcResolution) {
this.mask = (byte) (val << BIT_SHIFT);
this.pulseWidthUs = pulseWidthUs;
this.adcResolution = adcResolution;
}
byte getMask() {
return mask;
}
public float getPulseWidthUs() {
return pulseWidthUs;
}
public int getAdcResolution() {
return adcResolution;
}
}
private I2CDevice device;
private int revisionId;
private Mode mode;
private SampleAveraging sampleAveraging;
private SpO2AdcRange spo2AdcRange;
private SpO2SampleRate spo2SampleRate;
private LedPulseWidth ledPulseWidth;
private BlockingQueue sampleQueue;
private AtomicBoolean running;
private Future> future;
public Max30102() {
this(I2CConstants.CONTROLLER_1);
}
public Max30102(int controller) {
device = I2CDevice.builder(DEVICE_ADDRESS).setController(controller).build();
sampleQueue = new LinkedBlockingQueue<>();
running = new AtomicBoolean();
// Validate the part id
byte part_id = getPartId();
Logger.trace("Got part id {}", Byte.valueOf(part_id));
if (part_id != PART_ID) {
throw new RuntimeIOException("Unexpected part id: " + part_id + ", expected: " + PART_ID);
}
// Read the revision id
readRevisionId();
Logger.trace("Got revision id {}", Integer.valueOf(revisionId));
// Note that resetting the device does not trigger a PWR_RDY interrupt event
reset();
}
@Override
public void close() {
if (running.get()) {
stop();
}
shutdown();
device.close();
}
public void reset() {
// Send the reset bit only
device.writeByteData(REG_MODE_CONFIG, MODE_CONFIG_RESET_MASK);
// Poll for the reset bit to clear, timeout after 100ms
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 100) {
if ((device.readByteData(REG_MODE_CONFIG) & MODE_CONFIG_RESET_MASK) == 0) {
Logger.debug("Reset bit set");
break;
}
SleepUtil.sleepMillis(1);
}
// Reading the interrupt registers clears the interrupt status
device.readByteData(REG_INTR_STATUS_1);
device.readByteData(REG_INTR_STATUS_2);
}
public void shutdown() {
// Send the shutdown bit only
device.writeByteData(REG_MODE_CONFIG, MODE_CONFIG_SHUTDOWN_MASK);
}
public void wakeup() {
device.writeByteData(REG_MODE_CONFIG, 0x00);
}
private void readRevisionId() {
revisionId = device.readByteData(REG_REVISION_ID) & 0xff;
}
public int getRevisionId() {
return revisionId;
}
private byte getPartId() {
return device.readByteData(REG_PART_ID);
}
/**
* Setup the device.
*
* * Note actual measured current can vary widely due to trimming methodology
*
* @param sampleAveraging number of samples averaged per FIFO sample
* @param fifoRolloverOnFull whether or not the FIFO rolls over when full
* @param fifoAlmostFullValue set the number of data samples remaining in the
* FIFO when the interrupt is issued (range 0..15).
* E.g. if set to 0, the interrupt is issued when
* there are no data samples remaining in the FIFO
* (all 32 FIFO words have unread data, i.e. the FIFO
* is full), if set to 15, the interrupt is issued
* when there are 15 data samples remaining in the
* FIFO (17 unread)
* @param mode Operating mode
* @param spo2AdcRange SpO2 ADC range
* @param spo2SampleRate SpO2 sample rate
* @param ledPulseWidth LED pulse width (us)
* @param led1CurrentMA LED-1 pulse amplitude (range 0..51 mA) *
* @param led2CurrentMA LED-2 pulse amplitude (range 0..51 mA) *
*/
@SuppressWarnings("incomplete-switch")
public void setup(SampleAveraging sampleAveraging, boolean fifoRolloverOnFull, int fifoAlmostFullValue, Mode mode,
SpO2AdcRange spo2AdcRange, SpO2SampleRate spo2SampleRate, LedPulseWidth ledPulseWidth, float led1CurrentMA,
float led2CurrentMA) {
// fifoAlmostFullValue must be 0..15
if (fifoAlmostFullValue < 0 || fifoAlmostFullValue > FIFO_ALMOST_FULL_MAX) {
throw new IllegalArgumentException("Invalid fifoAlmostFullValue value (" + fifoAlmostFullValue
+ "), must be 0.." + FIFO_ALMOST_FULL_MAX);
}
// led1CurrentMA must be 0..51
if (led1CurrentMA < 0 || led1CurrentMA > LED_MAX_CURRENT_MA) {
throw new IllegalArgumentException(
"Invalid led1CurrentMA value (" + led1CurrentMA + "), must be 0.." + LED_MAX_CURRENT_MA);
}
// led2CurrentMA must be 0..51
if (led2CurrentMA < 0 || led2CurrentMA > LED_MAX_CURRENT_MA) {
throw new IllegalArgumentException(
"Invalid led2CurrentMA value (" + led2CurrentMA + "), must be 0.." + LED_MAX_CURRENT_MA);
}
// P23 of the datasheet
switch (mode) {
case SPO2:
// Validate LED Pulse Width us and SPS
switch (ledPulseWidth) {
case _69:
if (spo2SampleRate.getSampleRate() > SpO2SampleRate._1600.getSampleRate()) {
throw new IllegalArgumentException(
"In SpO2 mode sample rate must be <= " + SpO2SampleRate._1600.getSampleRate()
+ " for led pulse width " + ledPulseWidth.getPulseWidthUs());
}
break;
case _118:
if (spo2SampleRate.getSampleRate() > SpO2SampleRate._1000.getSampleRate()) {
throw new IllegalArgumentException(
"In SpO2 mode sample rate must be <= " + SpO2SampleRate._1000.getSampleRate()
+ " for led pulse width " + ledPulseWidth.getPulseWidthUs());
}
break;
case _215:
if (spo2SampleRate.getSampleRate() > SpO2SampleRate._800.getSampleRate()) {
throw new IllegalArgumentException(
"In SpO2 mode sample rate must be <= " + SpO2SampleRate._800.getSampleRate()
+ " for led pulse width " + ledPulseWidth.getPulseWidthUs());
}
break;
case _411:
if (spo2SampleRate.getSampleRate() > SpO2SampleRate._400.getSampleRate()) {
throw new IllegalArgumentException(
"In SpO2 mode sample rate must be <= " + SpO2SampleRate._400.getSampleRate()
+ " for led pulse width " + ledPulseWidth.getPulseWidthUs());
}
break;
}
break;
case HEART_RATE:
// Validate LED Pulse Width us and SPS
switch (ledPulseWidth) {
case _118:
case _215:
if (spo2SampleRate.getSampleRate() > SpO2SampleRate._1600.getSampleRate()) {
throw new IllegalArgumentException(
"In heart rate mode sample rate must be <= " + SpO2SampleRate._1600.getSampleRate()
+ " for led pulse width " + ledPulseWidth.getPulseWidthUs());
}
break;
case _411:
if (spo2SampleRate.getSampleRate() > SpO2SampleRate._1000.getSampleRate()) {
throw new IllegalArgumentException(
"In heart rate mode sample rate must be <= " + SpO2SampleRate._1000.getSampleRate()
+ " for led pulse width " + ledPulseWidth.getPulseWidthUs());
}
break;
}
break;
// TODO Support for multi-led mode - what needs to be validated?
}
// Enable all interrupts
writeByteData(REG_INTR_ENABLE_1, INT1_FIFO_ALMOST_FULL_MASK | INT1_PPG_RDY_DATA_MASK | INT1_ALC_OVF_MASK);
// device.writeByteData(REG_INTR_ENABLE_2, INT2_DIE_TEMP_RDY_MASK);
writeByteData(REG_INTR_ENABLE_2, 0);
writeByteData(REG_FIFO_CONFIG, sampleAveraging.getMask()
| (fifoRolloverOnFull ? FifoRolloverOnFull.ENABLED.getMask() : FifoRolloverOnFull.DISABLED.getMask())
| fifoAlmostFullValue);
// Sample avg = 4, FIFO rollover = false, FIFO almost full = 15 (17 unread)
// device.writeByteData(REG_FIFO_CONFIG, 0b0100_1111);
this.sampleAveraging = sampleAveraging;
writeByteData(REG_SPO2_CONFIG, spo2AdcRange.getMask() | spo2SampleRate.getMask() | ledPulseWidth.getMask());
// SPO2_ADC range = 4096nA, SPO2 sample rate = 100Hz, LED pulse-width = 411uS
// device.writeByteData(REG_SPO2_CONFIG, 0b0010_0111);
this.spo2AdcRange = spo2AdcRange;
this.spo2SampleRate = spo2SampleRate;
this.ledPulseWidth = ledPulseWidth;
writeByteData(REG_LED1_PULSE_AMPL, RangeUtil.map(led1CurrentMA, 0f, LED_MAX_CURRENT_MA, 0, 255));
writeByteData(REG_LED2_PULSE_AMPL, RangeUtil.map(led2CurrentMA, 0f, LED_MAX_CURRENT_MA, 0, 255));
writeByteData(REG_PILOT_PA, 0x7f);
// Choose value for ~7.1mA for LED1
// device.writeByteData(REG_LED1_PULSE_AMPL, 0x24);
// Choose value for ~7.1mA for LED2
// device.writeByteData(REG_LED2_PULSE_AMPL, 0x24);
// Reset the FIFO write pointer, overflow counter and FIFO read pointer
clearFifo();
// TODO Multi-LED mode (red and IR) (see Multi-LED Mode Control Registers)
writeByteData(REG_MODE_CONFIG, mode.getMask());
this.mode = mode;
}
private void writeByteData(int register, int mask) {
Logger.debug("setting register 0x{} to 0x{}", Integer.toHexString(register), Integer.toHexString(mask));
device.writeByteData(register, mask);
}
public int getDataPresent() {
int read_ptr = device.readByteData(REG_FIFO_READ_PTR) & 0xff;
int write_ptr = device.readByteData(REG_FIFO_WRITE_PTR) & 0xff;
int num_samples = write_ptr - read_ptr;
if (num_samples < 0) {
num_samples += 32;
}
return num_samples;
}
public void clearFifo() {
device.writeByteData(REG_FIFO_WRITE_PTR, 0);
device.writeByteData(REG_OVERFLOW_CTR, 0);
device.writeByteData(REG_FIFO_READ_PTR, 0);
}
public int getFifoWritePointer() {
return device.readByteData(REG_FIFO_WRITE_PTR) & 0xff;
}
public int getFifoReadPointer() {
return device.readByteData(REG_FIFO_READ_PTR) & 0xff;
}
public float readTemperatureCelsius() {
// DIE_TEMP_RDY interrupt must be enabled
// Step 1: Config die temperature register to take 1 temperature sample
device.writeByteData(REG_DIE_TEMP_CONFIG, DIE_TEMP_EN_MASK);
// Poll for bit to clear, reading is then complete
// Timeout after 100ms
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 100) {
if ((device.readByteData(REG_INTR_STATUS_2) & INT2_DIE_TEMP_RDY_MASK) != 0) {
break;
}
SleepUtil.sleepMillis(1);
}
// Step 2: Read die temperature register (integer)
int temp_int = device.readByteData(REG_DIE_TEMP_INT);
int temp_frac = device.readByteData(REG_DIE_TEMP_FRC) & 0xff;
// Causes the clearing of the DIE_TEMP_RDY interrupt
// Step 3: Calculate temperature (datasheet pg. 23)
return temp_int + (temp_frac * 0.0625f);
}
/**
* Poll the sensor for new data - call regularly. If new data is available, it
* adds data to the queues
*
* @return number of new samples obtained
*/
public int pollForData() {
// FIXME Limit this loop?
while (true) {
if ((device.readByteData(REG_INTR_STATUS_1) & INT1_PPG_RDY_DATA_MASK) != 0) {
break;
}
SleepUtil.sleepMillis(1);
}
/*
* The write pointer increments every time a new sample is added to the FIFO.
* The read pointer is incremented every time a sample is read from the FIFO.
*/
int fifo_read_ptr = getFifoReadPointer();
int fifo_write_ptr = getFifoWritePointer();
// Calculate the number of readings we need to get from sensor
int num_available_samples = fifo_write_ptr - fifo_read_ptr;
Logger.trace("fifo_read_ptr: {}, fifo_write_ptr: {}", Integer.valueOf(fifo_read_ptr),
Integer.valueOf(fifo_write_ptr));
// Is new data available?
if (num_available_samples != 0) {
// Take into account pointer wrap around
if (num_available_samples < 0) {
num_available_samples += 32;
}
/*
* The data FIFO consists of a 32-sample memory bank that can store IR and Red
* ADC data. Since each sample consists of two channels of data, there are 6
* bytes of data for each sample, and therefore 192 total bytes of data can be
* stored in the FIFO.
*/
// Heart rate mode activates the Red LED only, SpO2 and Multi-mode activate Red
// and IR
int num_channels = (mode == Mode.HEART_RATE) ? 1 : 2;
int bytes_to_read = num_channels * 3 * num_available_samples;
Logger.trace("num_available_samples: {}, bytes_to_read: {}", Integer.valueOf(num_available_samples),
Integer.valueOf(bytes_to_read));
byte[] data = new byte[bytes_to_read];
/*-
// Note that this code would need to be adjusted to work with the 32-byte SMBus
// limit
int bytes_read = device.readI2CBlockData(REG_FIFO_DATA, data);
Logger.debug("Read {} bytes from the FIFO, should have read {}", Integer.valueOf(bytes_read),
Integer.valueOf(bytes_to_read));
*/
/*-
// Use raw I2C readWrite and not the SMBus interface to avoid the 32-byte limit
// FIXME Note this code will not work on Arduino
I2CMessage[] messages = { //
new I2CMessage(I2CMessage.I2C_M_WR, 1), // Write the REG_FIFO_DATA register address
new I2CMessage(I2CMessage.I2C_M_RD, bytes_to_read) // Read FIFO data
};
byte[] buffer = new byte[1 + bytes_to_read];
buffer[0] = REG_FIFO_DATA;
device.readWrite(messages, buffer);
System.arraycopy(buffer, 1, data, 0, bytes_to_read);
*/
// Note this will not perform as well as readWrite(I2CMessage[], byte[])
device.readWrite(new I2CDeviceInterface.ReadCommand(REG_FIFO_DATA, data));
if (Logger.isTraceEnabled()) {
Hex.dumpByteArray(data);
}
// Now unpack the data
int pos = 0;
for (int sample = 0; sample < num_available_samples; sample++) {
int value = readUnsignedInt(data, pos++);
Logger.debug("Read HR {} for sample {}, pos {}", Integer.valueOf(value), Integer.valueOf(sample),
Integer.valueOf(pos));
Sample s = new Sample(value);
if (num_channels > 1) {
value = readUnsignedInt(data, pos++);
Logger.debug("Read SpO2 {} for sample {}, pos {}", Integer.valueOf(value), Integer.valueOf(sample),
Integer.valueOf(pos));
s.setSpo2(value);
}
sampleQueue.offer(s);
}
}
return num_available_samples;
}
public BlockingQueue getSampleQueue() {
return sampleQueue;
}
private static int readUnsignedInt(byte[] data, int pos) {
return (((data[pos * 3] & 0xff) << 16) | ((data[pos * 3 + 1] & 0xff) << 8) | (data[pos * 3 + 2] & 0xff))
& 0x03FFFF;
}
public SampleAveraging getSampleAveraging() {
return sampleAveraging;
}
public SpO2AdcRange getSpo2AdcRange() {
return spo2AdcRange;
}
public SpO2SampleRate getSpo2SampleRate() {
return spo2SampleRate;
}
public LedPulseWidth getLedPulseWidth() {
return ledPulseWidth;
}
public void start() {
if (running.get()) {
Logger.error("Already running");
return;
}
running.set(true);
future = DiozeroScheduler.getDefaultInstance().submit(() -> {
final AtomicBoolean local_running = running;
int sleep_ms = 1_000 / spo2SampleRate.getSampleRate();
while (local_running.get()) {
pollForData();
try {
Thread.sleep(sleep_ms);
} catch (InterruptedException e) {
local_running.set(false);
Logger.info(e, "Interrupted: {}", e);
}
}
});
}
public void stop() {
if (future == null || !running.get()) {
Logger.info("Not running");
return;
}
running.set(false);
future.cancel(true);
}
public static final class Sample {
private int heartRate;
private int spo2 = -1;
public Sample(int heartRate) {
this.heartRate = heartRate;
}
public int getHeartRate() {
return heartRate;
}
public int getSpo2() {
return spo2;
}
void setSpo2(int spo2) {
this.spo2 = spo2;
}
}
}