Skip to content

Commit 39aeb92

Browse files
marknolanclaude
andcommitted
DEV-793 Add MLX90632 skin-temperature decoder (data-block id 9)
Second Phase-C gen-2 sensor. Data block = fixed 16 samples x 4 bytes (object int16 then ambient int16, little-endian centi-degrees C - the firmware applies the chip EEPROM calibration before quantising, and only emits full blocks, partials discarded on stop per hal_slowSensorSampler.c), so it rides the generic fixed-size block path like the ambient light. New SensorMLX90632 mirrors the web SDK's SensorMLX90632.ts: UNCAL = centi-degC counts, CAL = /100 degrees Celsius. Config from payload header byte 32 (bit 0 measType medical/extended, bits 3:1 refresh code 0..7 = 0.5..64 Hz); enable from GEN_CFG_3 bit 4; output rate = refresh / sub-measurements (medical 2, extended 3). The light-rate refinement is generalised to refineSlowSensorSamplingRateFromBlockTicks(sensorId) and applied to both LIGHT and SKIN_TEMP blocks - the header-derived rates are estimates, inter-block tick spacing is truth. Metadata guard now accepts ids <=7 plus 9; ALGO_HUB (8) and FLICKER (10) still fail loudly (deferred to the new prototypes per DEV-793). CSV config line: 'MLX90632 {Sampling Rate [Configured/Calculated]; Refresh = X Hz; Mode = Medical|Extended; Resolution = 16-bit}'. Compile-clean, full ASM_PC_00005 suite green (all existing references bit-identical). NOT yet exercised on real data - no skin-temp recording exists; a ~2 min recording with skin temp enabled on any SR61-5/SR68-9 is wanted for the reference test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0165c3f commit 39aeb92

6 files changed

Lines changed: 304 additions & 16 deletions

File tree

ShimmerDriver/src/main/java/com/shimmerresearch/driver/Configuration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2649,6 +2649,8 @@ public class SensorBitmap {
26492649
public static final int LSM6DSV_MAG = 1 << (2 + (8*0));
26502650
/** VD6283TX45 ambient light (second-generation HW); enable is GEN_CFG_3 bit 3. */
26512651
public static final int VD6283 = 1 << (1 + (8*0));
2652+
/** MLX90632 skin temperature (second-generation HW); enable is GEN_CFG_3 bit 4. */
2653+
public static final int MLX90632 = 1 << (0 + (8*0));
26522654
}
26532655

26542656
public class DerivedSensorsBitMask {
@@ -2685,6 +2687,7 @@ public class SENSOR_ID {
26852687
public static final int LSM6DSV_GYRO = 2016;
26862688
public static final int LSM6DSV_MAG = 2017;
26872689
public static final int VD6283 = 2018;
2690+
public static final int MLX90632 = 2019;
26882691
}
26892692

26902693
public enum LABEL_SENSOR_TILE{

ShimmerDriver/src/main/java/com/shimmerresearch/sensors/AbstractSensor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public enum SENSORS{
6969
LIS2MDL("LIS2MDL"),
7070
LSM6DSV("LSM6DSV"),
7171
VD6283("VD6283"),
72+
MLX90632("MLX90632"),
7273
BMP390("BMP390");
7374

7475
private final String text;

ShimmerDriver/src/main/java/com/shimmerresearch/verisense/VerisenseDevice.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import com.shimmerresearch.verisense.sensors.SensorLIS2DW12;
6767
import com.shimmerresearch.verisense.sensors.SensorLSM6DS3;
6868
import com.shimmerresearch.verisense.sensors.SensorLSM6DSV;
69+
import com.shimmerresearch.verisense.sensors.SensorMLX90632;
6970
import com.shimmerresearch.verisense.sensors.SensorVD6283;
7071
import com.shimmerresearch.verisense.sensors.SensorMAX86150;
7172
import com.shimmerresearch.verisense.sensors.SensorMAX86916;
@@ -589,6 +590,7 @@ public void configBytesParse(byte[] configBytes, COMMUNICATION_TYPE commType) {
589590
int gen2GenCfg3 = configBytes[PAYLOAD_CONFIG_BYTE_INDEX.GEN_CFG_3] & 0xFF;
590591
if((gen2GenCfg3 & (1<<2))!=0) { enabledSensors |= Configuration.Verisense.SensorBitmap.LSM6DSV_MAG; }
591592
if((gen2GenCfg3 & (1<<3))!=0) { enabledSensors |= Configuration.Verisense.SensorBitmap.VD6283; }
593+
if((gen2GenCfg3 & (1<<4))!=0) { enabledSensors |= Configuration.Verisense.SensorBitmap.MLX90632; }
592594
}
593595
setEnabledAndDerivedSensorsAndUpdateMaps(enabledSensors, mDerivedSensors, commType);
594596

@@ -1084,6 +1086,23 @@ && isSensorEnabled(Configuration.Verisense.SENSOR_ID.VD6283)) {
10841086
sb.append(SENSOR_CONFIG_STRINGS.RESOLUTION);
10851087
sb.append("24-bit");
10861088
sb.append("}");
1089+
} else if(sensorClassKey==AbstractSensor.SENSORS.MLX90632
1090+
&& isSensorEnabled(Configuration.Verisense.SENSOR_ID.MLX90632)) {
1091+
// Second-generation skin temperature.
1092+
SensorMLX90632 sensorMlx90632 = getSensorMLX90632();
1093+
1094+
sb.append(generateCalcSamplingRateConfigStr(sensorClassKey, sensorMlx90632.getRateFreq(), calculatedSamplingRate));
1095+
1096+
sb.append("Refresh = ");
1097+
sb.append(UtilVerisenseDriver.formatDoubleToNdecimalPlaces(sensorMlx90632.getRefreshHz(), 1));
1098+
sb.append(" ");
1099+
sb.append(CHANNEL_UNITS.FREQUENCY);
1100+
sb.append("; Mode = ");
1101+
sb.append(sensorMlx90632.getMeasTypeString());
1102+
sb.append("; ");
1103+
sb.append(SENSOR_CONFIG_STRINGS.RESOLUTION);
1104+
sb.append("16-bit");
1105+
sb.append("}");
10871106
} else if(LIST_OF_PPG_SENSORS.contains(sensorClassKey) && isHwPpgAndAnyMaxChEnabled()) {
10881107
AbstractSensor abstractSensor = getAbstractSensorPpg();
10891108
if(abstractSensor!=null) {
@@ -1388,8 +1407,9 @@ public void sensorAndConfigMapsCreate() {
13881407
if(isPayloadDesignV13orAbove()) {
13891408
// Second-generation IMU (LSM6DSV accel/gyro + LIS2MDL mag via sensor hub)
13901409
addSensorClass(SENSORS.LSM6DSV, new SensorLSM6DSV(this));
1391-
// Second-generation ambient light
1410+
// Second-generation ambient light + skin temperature
13921411
addSensorClass(SENSORS.VD6283, new SensorVD6283(this));
1412+
addSensorClass(SENSORS.MLX90632, new SensorMLX90632(this));
13931413
} else if(doesHwSupportLsm6ds3()) {
13941414
addSensorClass(SENSORS.LSM6DS3, new SensorLSM6DS3(this));
13951415
}
@@ -1606,6 +1626,9 @@ public List<SENSORS> getSensorKeysForDatablockId(DATABLOCK_SENSOR_ID datablockSe
16061626
case LIGHT:
16071627
listOfSensorIds.add(SENSORS.VD6283);
16081628
break;
1629+
case SKIN_TEMP:
1630+
listOfSensorIds.add(SENSORS.MLX90632);
1631+
break;
16091632
case PPG:
16101633
SENSORS sensorClassKey = getPpgSensorClassKey();
16111634
if(sensorClassKey!=null) {
@@ -1632,6 +1655,10 @@ public int calculateFifoBlockSize(SENSORS sensorClassKey) {
16321655
// NUM_LIGHT_SAMPLES_PER_BLOCK samples (partials are discarded on stop).
16331656
dataBlockSize = SensorVD6283.BLOCK_DATA_BYTES;
16341657
break;
1658+
case MLX90632:
1659+
// Fixed-size block: full blocks of NUM_TEMP_SAMPLES_PER_BLOCK samples only.
1660+
dataBlockSize = SensorMLX90632.BLOCK_DATA_BYTES;
1661+
break;
16351662
case LSM6DS3:
16361663
AbstractSensor abstractSensor = getSensorClass(SENSORS.LSM6DS3);
16371664
if(abstractSensor!=null) {
@@ -1924,7 +1951,9 @@ public DataBlockDetails parseDataBlockMetaData(byte[] byteBuffer, int dataBlockS
19241951
// without a getSensorKeysForDatablockId()/calculateFifoBlockSize() handler
19251952
// would corrupt the parse, so they are rejected with the informative
19261953
// exception below until their decoders are implemented.
1927-
if(sensorId>0 && sensorId<=DATABLOCK_SENSOR_ID.LIGHT.ordinal()) {
1954+
// Parse-supported block ids: everything up to LIGHT (7), plus SKIN_TEMP (9).
1955+
// ALGO_HUB (8) and FLICKER (10) still fail loudly below until their decoders land.
1956+
if(sensorId>0 && (sensorId<=DATABLOCK_SENSOR_ID.LIGHT.ordinal() || sensorId==DATABLOCK_SENSOR_ID.SKIN_TEMP.ordinal())) {
19281957
DATABLOCK_SENSOR_ID datablockSensorId = payloadSensorIdValues[sensorId];
19291958

19301959
List<SENSORS> listOfSensorClassKeys = getOrCreateListOfSensorClassKeysForDataBlockId(datablockSensorId);
@@ -2374,6 +2403,14 @@ public SensorVD6283 getSensorVD6283() {
23742403
return null;
23752404
}
23762405

2406+
public SensorMLX90632 getSensorMLX90632() {
2407+
AbstractSensor abstractSensor = getSensorClass(SENSORS.MLX90632);
2408+
if(abstractSensor!=null && abstractSensor instanceof SensorMLX90632) {
2409+
return (SensorMLX90632) abstractSensor;
2410+
}
2411+
return null;
2412+
}
2413+
23772414
/**
23782415
* @return Null if sensor not supported by current hardware
23792416
* @see SensorGSRVerisense

ShimmerDriver/src/main/java/com/shimmerresearch/verisense/payloaddesign/AsmBinaryFileConstants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ public class PAYLOAD_CONFIG_BYTE_INDEX {
4747
public static final int LIGHT_GAIN_AND_DARK = 26;
4848
/** Second-generation only: ambient-light exposure index (abs byte 31). */
4949
public static final int LIGHT_EXPOSURE = 27;
50+
/** Second-generation only: skin-temp config (abs byte 32) - bit 0 measType
51+
* (0=medical/1=extended), bits 3:1 MLX90632 refresh-rate code. */
52+
public static final int SKIN_TEMP_CONFIG = 28;
5053
}
5154

5255
public class BYTE_COUNT {

ShimmerDriver/src/main/java/com/shimmerresearch/verisense/payloaddesign/PayloadContentsDetailsV8orAbove.java

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,13 @@ public void parsePayloadContentsMetaData(int binFileByteIndex) throws IOExceptio
9393

9494
// --------- End of parsing ------------------
9595

96-
// The ambient-light sample rate isn't stored in the payload header (only
97-
// gain/exposure are) and the achieved rate differs from both the configured
98-
// rate and 1/exposure (per-measurement dead time), so refine it from the
99-
// data itself before the block timings are back-filled below.
100-
refineLightSamplingRateFromBlockTicks();
96+
// The slow sensors' achieved sample rates differ from what the payload
97+
// header can tell us (the light rate isn't stored at all and the chip adds
98+
// per-measurement dead time; the skin-temp output cadence is refresh-code
99+
// derived but similarly approximate), so refine them from the data itself
100+
// before the block timings are back-filled below.
101+
refineSlowSensorSamplingRateFromBlockTicks(DATABLOCK_SENSOR_ID.LIGHT);
102+
refineSlowSensorSamplingRateFromBlockTicks(DATABLOCK_SENSOR_ID.SKIN_TEMP);
101103

102104
// Up to, and including, payload design v10, the real-world clock time that was
103105
// stored in the payload footer was the real-world time at the end of the
@@ -314,19 +316,20 @@ private boolean isParserAtEndOfBuffer(int bufferLength, int currentByteIndex) {
314316
}
315317

316318
/**
317-
* Derive the achieved ambient-light sample period from the spacing of
318-
* consecutive light-block end ticks and apply it to the light blocks' sampling
319-
* rate before their timings are back-filled. The light sample rate cannot be
320-
* read from the stored payload header (see SensorVD6283.getRateFreq), and each
321-
* light block holds a fixed number of samples, so
319+
* Derive a slow sensor's (ambient light / skin temp) achieved sample period
320+
* from the spacing of consecutive same-sensor block end ticks and apply it to
321+
* those blocks' sampling rate before their timings are back-filled. The
322+
* header-derived rates are only estimates (the light rate isn't stored at all
323+
* - see SensorVD6283.getRateFreq - and the skin-temp cadence is refresh-code
324+
* derived), and each block holds a fixed number of samples, so
322325
* {@code inter-block ticks / samples-per-block} is the exact per-sample period.
323-
* With fewer than two light blocks in the payload the exposure-limited
324-
* estimate the blocks were created with is left in place.
326+
* With fewer than two blocks in the payload the header-derived estimate the
327+
* blocks were created with is left in place.
325328
*/
326-
private void refineLightSamplingRateFromBlockTicks() {
329+
private void refineSlowSensorSamplingRateFromBlockTicks(DATABLOCK_SENSOR_ID slowSensorId) {
327330
List<DataBlockDetails> lightBlocks = new ArrayList<DataBlockDetails>();
328331
for(DataBlockDetails dataBlockDetails:listOfDataBlocksInOrder) {
329-
if(dataBlockDetails.datablockSensorId==DATABLOCK_SENSOR_ID.LIGHT) {
332+
if(dataBlockDetails.datablockSensorId==slowSensorId) {
330333
lightBlocks.add(dataBlockDetails);
331334
}
332335
}

0 commit comments

Comments
 (0)