Skip to content

Commit 0165c3f

Browse files
marknolanclaude
andcommitted
DEV-793 Add VD6283 ambient-light decoder (data-block id 7)
First Phase-C gen-2 sensor: the VD6283TX45 ambient light. Data block = fixed 10 samples x 18 bytes (6 channels x 24-bit LE, order RED/VISIBLE/BLUE/GREEN/IR/CLEAR - the firmware only emits full blocks, partials are discarded on stop), so it rides the generic fixed-size block path. New SensorVD6283 ports the firmware App_vd6283tx.c conversions (and matches the web SDK's SensorVD6283.ts): UNCAL = raw counts, CAL = gain/exposure-normalised counts plus derived Lux (XYZ Y) and CCT (McCamy). Gain/exposure/dark-channel parsed from payload header bytes 30/31; enable from GEN_CFG_3 bit 3. The light sample rate is NOT stored in the payload header and the achieved rate differs from both the configured rate and 1/exposure (per-measurement dead time, e.g. ~110 ms at the 100 ms exposure), so PayloadContentsDetailsV8orAbove derives it per payload from the median inter-light-block tick spacing before block timings are back-filled; the exposure-limited estimate seeds blocks when a payload has fewer than two light blocks. Metadata guard widened to accept id 7 (ALGO_HUB/SKIN_TEMP/FLICKER still fail loudly). Slot-1 visible-vs-dark is recorded in the CSV sensor-config line ('Slot1 = Visible|Dark'); the column name stays Light_Visible until a dark-enabled recording exists. Hand-verified against the spec formulas on the first real light recording (DEV-793 2026-07-27, SR61-5 unit AF6A): normalized RED 278.586 and Lux 207.586 match manual computation exactly; 290 parsed rows = 29 blocks x 10 samples; derived rate 9.209 Hz. Full ASM_PC_00005 suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6da56fc commit 0165c3f

6 files changed

Lines changed: 472 additions & 1 deletion

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
@@ -2647,6 +2647,8 @@ public class SensorBitmap {
26472647
public static final int LSM6DSV_ACCEL = 1 << (4 + (8*0));
26482648
public static final int LSM6DSV_GYRO = 1 << (3 + (8*0));
26492649
public static final int LSM6DSV_MAG = 1 << (2 + (8*0));
2650+
/** VD6283TX45 ambient light (second-generation HW); enable is GEN_CFG_3 bit 3. */
2651+
public static final int VD6283 = 1 << (1 + (8*0));
26502652
}
26512653

26522654
public class DerivedSensorsBitMask {
@@ -2682,6 +2684,7 @@ public class SENSOR_ID {
26822684
public static final int LSM6DSV_ACCEL = 2015;
26832685
public static final int LSM6DSV_GYRO = 2016;
26842686
public static final int LSM6DSV_MAG = 2017;
2687+
public static final int VD6283 = 2018;
26852688
}
26862689

26872690
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
@@ -68,6 +68,7 @@ public enum SENSORS{
6868
LIS3MDL("LIS3MDL"), //to be changed
6969
LIS2MDL("LIS2MDL"),
7070
LSM6DSV("LSM6DSV"),
71+
VD6283("VD6283"),
7172
BMP390("BMP390");
7273

7374
private final String text;

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

Lines changed: 47 additions & 1 deletion
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.SensorVD6283;
6970
import com.shimmerresearch.verisense.sensors.SensorMAX86150;
7071
import com.shimmerresearch.verisense.sensors.SensorMAX86916;
7172
import com.shimmerresearch.verisense.sensors.SensorMAX86XXX;
@@ -587,6 +588,7 @@ public void configBytesParse(byte[] configBytes, COMMUNICATION_TYPE commType) {
587588
if((gen2Cfg0 & (1<<5))!=0) { enabledSensors |= Configuration.Verisense.SensorBitmap.LSM6DSV_GYRO; }
588589
int gen2GenCfg3 = configBytes[PAYLOAD_CONFIG_BYTE_INDEX.GEN_CFG_3] & 0xFF;
589590
if((gen2GenCfg3 & (1<<2))!=0) { enabledSensors |= Configuration.Verisense.SensorBitmap.LSM6DSV_MAG; }
591+
if((gen2GenCfg3 & (1<<3))!=0) { enabledSensors |= Configuration.Verisense.SensorBitmap.VD6283; }
590592
}
591593
setEnabledAndDerivedSensorsAndUpdateMaps(enabledSensors, mDerivedSensors, commType);
592594

@@ -1056,6 +1058,32 @@ && isAnyLsm6dsvChannelEnabled()) {
10561058
sb.append(SENSOR_CONFIG_STRINGS.RESOLUTION);
10571059
sb.append("16-bit");
10581060
sb.append("}");
1061+
} else if(sensorClassKey==AbstractSensor.SENSORS.VD6283
1062+
&& isSensorEnabled(Configuration.Verisense.SENSOR_ID.VD6283)) {
1063+
// Second-generation ambient light. The configured sample rate isn't stored
1064+
// in the payload header, so only the calculated (achieved) rate is reported.
1065+
SensorVD6283 sensorVd6283 = getSensorVD6283();
1066+
1067+
sb.append(sensorClassKey.toString());
1068+
sb.append(" {Sampling Rate [");
1069+
sb.append(SENSOR_CONFIG_STRINGS.SAMPLING_RATE_CALCULATED);
1070+
if(!Double.isNaN(calculatedSamplingRate)) {
1071+
sb.append(UtilVerisenseDriver.formatDoubleToNdecimalPlaces(calculatedSamplingRate, 3));
1072+
sb.append(" ");
1073+
sb.append(CHANNEL_UNITS.FREQUENCY);
1074+
} else {
1075+
sb.append(UtilVerisenseDriver.UNAVAILABLE);
1076+
}
1077+
sb.append("]; Gain = ");
1078+
sb.append(UtilVerisenseDriver.formatDoubleToNdecimalPlaces(sensorVd6283.getGain(), 2));
1079+
sb.append("x; Exposure = ");
1080+
sb.append(UtilVerisenseDriver.formatDoubleToNdecimalPlaces(sensorVd6283.getExposureUs()/1000.0, 1));
1081+
sb.append(" ms; Slot1 = ");
1082+
sb.append(sensorVd6283.isDarkChannelEnabled() ? "Dark" : "Visible");
1083+
sb.append("; ");
1084+
sb.append(SENSOR_CONFIG_STRINGS.RESOLUTION);
1085+
sb.append("24-bit");
1086+
sb.append("}");
10591087
} else if(LIST_OF_PPG_SENSORS.contains(sensorClassKey) && isHwPpgAndAnyMaxChEnabled()) {
10601088
AbstractSensor abstractSensor = getAbstractSensorPpg();
10611089
if(abstractSensor!=null) {
@@ -1360,6 +1388,8 @@ public void sensorAndConfigMapsCreate() {
13601388
if(isPayloadDesignV13orAbove()) {
13611389
// Second-generation IMU (LSM6DSV accel/gyro + LIS2MDL mag via sensor hub)
13621390
addSensorClass(SENSORS.LSM6DSV, new SensorLSM6DSV(this));
1391+
// Second-generation ambient light
1392+
addSensorClass(SENSORS.VD6283, new SensorVD6283(this));
13631393
} else if(doesHwSupportLsm6ds3()) {
13641394
addSensorClass(SENSORS.LSM6DS3, new SensorLSM6DS3(this));
13651395
}
@@ -1573,6 +1603,9 @@ public List<SENSORS> getSensorKeysForDatablockId(DATABLOCK_SENSOR_ID datablockSe
15731603
case LSM6DSV:
15741604
listOfSensorIds.add(SENSORS.LSM6DSV);
15751605
break;
1606+
case LIGHT:
1607+
listOfSensorIds.add(SENSORS.VD6283);
1608+
break;
15761609
case PPG:
15771610
SENSORS sensorClassKey = getPpgSensorClassKey();
15781611
if(sensorClassKey!=null) {
@@ -1594,6 +1627,11 @@ public int calculateFifoBlockSize(SENSORS sensorClassKey) {
15941627
case LIS2DW12:
15951628
dataBlockSize = SensorLIS2DW12.FIFO_SIZE_IN_CHIP;
15961629
break;
1630+
case VD6283:
1631+
// Fixed-size block: the firmware only emits FULL blocks of
1632+
// NUM_LIGHT_SAMPLES_PER_BLOCK samples (partials are discarded on stop).
1633+
dataBlockSize = SensorVD6283.BLOCK_DATA_BYTES;
1634+
break;
15971635
case LSM6DS3:
15981636
AbstractSensor abstractSensor = getSensorClass(SENSORS.LSM6DS3);
15991637
if(abstractSensor!=null) {
@@ -1886,7 +1924,7 @@ public DataBlockDetails parseDataBlockMetaData(byte[] byteBuffer, int dataBlockS
18861924
// without a getSensorKeysForDatablockId()/calculateFifoBlockSize() handler
18871925
// would corrupt the parse, so they are rejected with the informative
18881926
// exception below until their decoders are implemented.
1889-
if(sensorId>0 && sensorId<=DATABLOCK_SENSOR_ID.LSM6DSV.ordinal()) {
1927+
if(sensorId>0 && sensorId<=DATABLOCK_SENSOR_ID.LIGHT.ordinal()) {
18901928
DATABLOCK_SENSOR_ID datablockSensorId = payloadSensorIdValues[sensorId];
18911929

18921930
List<SENSORS> listOfSensorClassKeys = getOrCreateListOfSensorClassKeysForDataBlockId(datablockSensorId);
@@ -2328,6 +2366,14 @@ public SensorLSM6DSV getSensorLSM6DSV() {
23282366
return null;
23292367
}
23302368

2369+
public SensorVD6283 getSensorVD6283() {
2370+
AbstractSensor abstractSensor = getSensorClass(SENSORS.VD6283);
2371+
if(abstractSensor!=null && abstractSensor instanceof SensorVD6283) {
2372+
return (SensorVD6283) abstractSensor;
2373+
}
2374+
return null;
2375+
}
2376+
23312377
/**
23322378
* @return Null if sensor not supported by current hardware
23332379
* @see SensorGSRVerisense

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ public class PAYLOAD_CONFIG_BYTE_INDEX {
4242
// Second-generation (payload design v13) only: GEN_CFG_3 at abs byte 29 (rel 25)
4343
// carries the mag/light/skin-temp/algo-hub enables + LED mode.
4444
public static final int GEN_CFG_3 = 25;
45+
/** Second-generation only: ambient-light gain index in bits 2:0 and the
46+
* dark-channel enable in bit 7 (abs byte 30). */
47+
public static final int LIGHT_GAIN_AND_DARK = 26;
48+
/** Second-generation only: ambient-light exposure index (abs byte 31). */
49+
public static final int LIGHT_EXPOSURE = 27;
4550
}
4651

4752
public class BYTE_COUNT {

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.io.IOException;
44
import java.util.ArrayList;
5+
import java.util.Collections;
56
import java.util.List;
67
import java.util.ListIterator;
78
import java.util.Map.Entry;
@@ -92,6 +93,12 @@ public void parsePayloadContentsMetaData(int binFileByteIndex) throws IOExceptio
9293

9394
// --------- End of parsing ------------------
9495

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();
101+
95102
// Up to, and including, payload design v10, the real-world clock time that was
96103
// stored in the payload footer was the real-world time at the end of the
97104
// payload. From payload design v11 onwards, the real-world clock offset is
@@ -306,6 +313,59 @@ private boolean isParserAtEndOfBuffer(int bufferLength, int currentByteIndex) {
306313
return (bufferLength-currentByteIndex)<(footerLength+BYTE_COUNT.PAYLOAD_CRC);
307314
}
308315

316+
/**
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
322+
* {@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.
325+
*/
326+
private void refineLightSamplingRateFromBlockTicks() {
327+
List<DataBlockDetails> lightBlocks = new ArrayList<DataBlockDetails>();
328+
for(DataBlockDetails dataBlockDetails:listOfDataBlocksInOrder) {
329+
if(dataBlockDetails.datablockSensorId==DATABLOCK_SENSOR_ID.LIGHT) {
330+
lightBlocks.add(dataBlockDetails);
331+
}
332+
}
333+
if(lightBlocks.size()<2) {
334+
return;
335+
}
336+
337+
// v11+ payloads store microcontroller-clock ticks per block, earlier designs
338+
// store real-world-clock ticks; either works as only deltas are used.
339+
boolean useUcClockTicks = verisenseDevice.isPayloadDesignV11orAbove();
340+
List<Double> perSamplePeriodsS = new ArrayList<Double>();
341+
for(int i=1;i<lightBlocks.size();i++) {
342+
VerisenseTimeDetails prev = useUcClockTicks? lightBlocks.get(i-1).getTimeDetailsUcClock():lightBlocks.get(i-1).getTimeDetailsRwc();
343+
VerisenseTimeDetails curr = useUcClockTicks? lightBlocks.get(i).getTimeDetailsUcClock():lightBlocks.get(i).getTimeDetailsRwc();
344+
// 3-byte tick counter at 32768 Hz; handle wrap (every ~512 s, far above the
345+
// ~1 s light-block cadence).
346+
long deltaTicks = (curr.getEndTimeTicks() - prev.getEndTimeTicks()) & 0xFFFFFF;
347+
int sampleCount = lightBlocks.get(i).getSampleCount();
348+
if(deltaTicks>0 && sampleCount>0) {
349+
perSamplePeriodsS.add((deltaTicks/32768.0)/sampleCount);
350+
}
351+
}
352+
if(perSamplePeriodsS.isEmpty()) {
353+
return;
354+
}
355+
// Median so a dropped block (a 2x gap) can't skew the period.
356+
Collections.sort(perSamplePeriodsS);
357+
double medianPeriodS = perSamplePeriodsS.get(perSamplePeriodsS.size()/2);
358+
if(!(medianPeriodS>0)) {
359+
return;
360+
}
361+
362+
double achievedRateHz = 1.0/medianPeriodS;
363+
for(DataBlockDetails dataBlockDetails:lightBlocks) {
364+
dataBlockDetails.setSamplingRate(achievedRateHz);
365+
dataBlockDetails.calculateTimestampDiffInS();
366+
}
367+
}
368+
309369
private void backfillDataBlockRwcTimestamps() {
310370
backfillDataBlockUcClockOrRwcTimestamps(false);
311371
}

0 commit comments

Comments
 (0)