Trying to resolve heart rate issue
This commit is contained in:
@@ -133,6 +133,20 @@ public class SdData implements Parcelable {
|
|||||||
public boolean mAverageHrAlarmStanding = false;
|
public boolean mAverageHrAlarmStanding = false;
|
||||||
public double mHR = 0;
|
public double mHR = 0;
|
||||||
|
|
||||||
|
// Heart-rate diagnostics.
|
||||||
|
// These are used to distinguish fresh BLE HR measurements from stale values.
|
||||||
|
public long mHrLastUpdateMillis = 0;
|
||||||
|
public int mHrNotificationCount = 0;
|
||||||
|
public int mHrRawValue = -1;
|
||||||
|
|
||||||
|
// BLE Heart Rate Measurement sensor contact status.
|
||||||
|
// -1 = unknown / not provided
|
||||||
|
// 0 = contact feature not supported or not detected depending on flags
|
||||||
|
// 1 = contact not detected
|
||||||
|
// 2 = contact detected
|
||||||
|
// 3 = reserved/unknown
|
||||||
|
public int mHrSensorContactStatus = -1;
|
||||||
|
|
||||||
public boolean mO2SatAlarmStanding = false;
|
public boolean mO2SatAlarmStanding = false;
|
||||||
public boolean mO2SatFaultStanding = false;
|
public boolean mO2SatFaultStanding = false;
|
||||||
public double mO2Sat = 0;
|
public double mO2Sat = 0;
|
||||||
|
|||||||
@@ -363,16 +363,38 @@ public class SdDataSourceBLE2 extends SdDataSource {
|
|||||||
Log.v(TAG,"onCharacteristicUpdate() - Characteristic "+charUuidStr+" updated");
|
Log.v(TAG,"onCharacteristicUpdate() - Characteristic "+charUuidStr+" updated");
|
||||||
|
|
||||||
if (charUuidStr.equals(CHAR_HEART_RATE_MEASUREMENT)) {
|
if (charUuidStr.equals(CHAR_HEART_RATE_MEASUREMENT)) {
|
||||||
Log.v(TAG, String.format("%s", "HR Measurement"));
|
Log.v(TAG, "HR Measurement");
|
||||||
// Parse the flags
|
|
||||||
int flags = parser.getUInt8();
|
int flags = parser.getUInt8();
|
||||||
final int unit = flags & 0x01;
|
|
||||||
final int sensorContactStatus = (flags & 0x06) >> 1;
|
// Bit 0: 0 = UINT8 HR, 1 = UINT16 HR
|
||||||
final boolean energyExpenditurePresent = (flags & 0x08) > 0;
|
final int unit = flags & 0x01;
|
||||||
final boolean rrIntervalPresent = (flags & 0x10) > 0;
|
|
||||||
// Parse heart rate
|
// Bits 1-2: sensor contact status, if provided by the device
|
||||||
mSdData.mHR = (unit == 0) ? parser.getUInt8() : parser.getUInt16();
|
final int sensorContactStatus = (flags & 0x06) >> 1;
|
||||||
Log.d(TAG,"Received HR="+mSdData.mHR);
|
|
||||||
|
final boolean energyExpenditurePresent = (flags & 0x08) > 0;
|
||||||
|
final boolean rrIntervalPresent = (flags & 0x10) > 0;
|
||||||
|
|
||||||
|
int heartRate = (unit == 0) ? parser.getUInt8() : parser.getUInt16();
|
||||||
|
|
||||||
|
// Timestamp every actual HR BLE notification, even if the value is invalid.
|
||||||
|
mSdData.mHrLastUpdateMillis = System.currentTimeMillis();
|
||||||
|
mSdData.mHrNotificationCount++;
|
||||||
|
mSdData.mHrRawValue = heartRate;
|
||||||
|
mSdData.mHrSensorContactStatus = sensorContactStatus;
|
||||||
|
|
||||||
|
// Treat 0 and 255 as invalid/fault values, matching the older BLE datasource behavior.
|
||||||
|
if (heartRate == 0 || heartRate == 255) {
|
||||||
|
mSdData.mHR = -1;
|
||||||
|
} else {
|
||||||
|
mSdData.mHR = (double) heartRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Received HR raw=" + heartRate
|
||||||
|
+ ", stored=" + mSdData.mHR
|
||||||
|
+ ", contactStatus=" + sensorContactStatus
|
||||||
|
+ ", notificationCount=" + mSdData.mHrNotificationCount);
|
||||||
|
|
||||||
} else if (charUuidStr.equals(CHAR_OSD_ACC_DATA)
|
} else if (charUuidStr.equals(CHAR_OSD_ACC_DATA)
|
||||||
|| charUuidStr.equals(CHAR_INFINITIME_ACC_DATA)) {
|
|| charUuidStr.equals(CHAR_INFINITIME_ACC_DATA)) {
|
||||||
|
|||||||
@@ -88,8 +88,18 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
private double currentHrSum = 0.0;
|
private double currentHrSum = 0.0;
|
||||||
private int currentHrValidCount = 0;
|
private int currentHrValidCount = 0;
|
||||||
private int currentHrReadCount = 0;
|
private int currentHrReadCount = 0;
|
||||||
private final ArrayList<HeartRatePoint> hrPoints = new ArrayList<>();
|
|
||||||
|
|
||||||
|
// Counts actual fresh BLE HR notifications per one-second bucket.
|
||||||
|
private int currentHrFreshNotificationCount = 0;
|
||||||
|
private long currentHrLastSeenNotificationCount = -1;
|
||||||
|
private long currentHrLastUpdateMillisInSecond = 0;
|
||||||
|
private int currentHrRawValue = -1;
|
||||||
|
private int currentHrSensorContactStatus = -1;
|
||||||
|
|
||||||
|
private long hrStartNotificationCount = 0;
|
||||||
|
private long hrLastSeenNotificationCount = -1;
|
||||||
|
|
||||||
|
private final ArrayList<HeartRatePoint> hrPoints = new ArrayList<>();
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -120,13 +130,32 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||||
android.R.layout.simple_spinner_item,
|
android.R.layout.simple_spinner_item,
|
||||||
new String[]{"30 seconds", "60 seconds"});
|
new String[]{"30 seconds", "60 seconds", "2 minutes", "3 minutes", "5 minutes"});
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||||
durationSpinner.setAdapter(adapter);
|
durationSpinner.setAdapter(adapter);
|
||||||
durationSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
durationSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||||
targetSeconds = position == 0 ? 30 : 60;
|
switch (position) {
|
||||||
|
case 0:
|
||||||
|
targetSeconds = 30;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
targetSeconds = 60;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
targetSeconds = 120;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
targetSeconds = 180;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
targetSeconds = 300;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
targetSeconds = 30;
|
||||||
|
break;
|
||||||
|
}
|
||||||
updateProgressText();
|
updateProgressText();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,10 +500,20 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
hrPoints.clear();
|
hrPoints.clear();
|
||||||
currentHrSecond = -1;
|
currentHrSecond = -1;
|
||||||
currentHrSum = 0.0;
|
currentHrSum = 0.0;
|
||||||
currentHrValidCount = 0;
|
currentHrValidCount = 0;
|
||||||
currentHrReadCount = 0;
|
currentHrReadCount = 0;
|
||||||
hrStartMillis = System.currentTimeMillis();
|
currentHrFreshNotificationCount = 0;
|
||||||
collectingHr = true;
|
currentHrLastUpdateMillisInSecond = 0;
|
||||||
|
currentHrRawValue = -1;
|
||||||
|
currentHrSensorContactStatus = -1;
|
||||||
|
|
||||||
|
SdData sdData = mConnection.mSdServer.mSdData;
|
||||||
|
hrStartNotificationCount = sdData != null ? sdData.mHrNotificationCount : 0;
|
||||||
|
hrLastSeenNotificationCount = hrStartNotificationCount;
|
||||||
|
currentHrLastSeenNotificationCount = hrStartNotificationCount;
|
||||||
|
|
||||||
|
hrStartMillis = System.currentTimeMillis();
|
||||||
|
collectingHr = true;
|
||||||
|
|
||||||
hrResultTv.setText("Collecting heart-rate data...");
|
hrResultTv.setText("Collecting heart-rate data...");
|
||||||
updateHeartRateProgressText();
|
updateHeartRateProgressText();
|
||||||
@@ -492,56 +531,87 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void pollHeartRateSample() {
|
private void pollHeartRateSample() {
|
||||||
if (!collectingHr) return;
|
if (!collectingHr) return;
|
||||||
if (!mConnection.mBound || mConnection.mSdServer == null || mConnection.mSdServer.mSdData == null) {
|
if (!mConnection.mBound || mConnection.mSdServer == null || mConnection.mSdServer.mSdData == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
double elapsedSeconds = (now - hrStartMillis) / 1000.0;
|
double elapsedSeconds = (now - hrStartMillis) / 1000.0;
|
||||||
int secondIndex = (int) Math.floor(elapsedSeconds);
|
int secondIndex = (int) Math.floor(elapsedSeconds);
|
||||||
|
|
||||||
if (secondIndex >= targetSeconds) {
|
if (secondIndex >= targetSeconds) {
|
||||||
stopHeartRateTest(null);
|
stopHeartRateTest(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentHrSecond != secondIndex) {
|
if (currentHrSecond != secondIndex) {
|
||||||
flushCurrentHeartRateSecond();
|
flushCurrentHeartRateSecond();
|
||||||
currentHrSecond = secondIndex;
|
currentHrSecond = secondIndex;
|
||||||
currentHrSum = 0.0;
|
currentHrSum = 0.0;
|
||||||
currentHrValidCount = 0;
|
currentHrValidCount = 0;
|
||||||
currentHrReadCount = 0;
|
currentHrReadCount = 0;
|
||||||
}
|
currentHrFreshNotificationCount = 0;
|
||||||
|
currentHrLastUpdateMillisInSecond = 0;
|
||||||
|
currentHrRawValue = -1;
|
||||||
|
currentHrSensorContactStatus = -1;
|
||||||
|
currentHrLastSeenNotificationCount = hrLastSeenNotificationCount;
|
||||||
|
}
|
||||||
|
|
||||||
double hr = mConnection.mSdServer.mSdData.mHR;
|
SdData sdData = mConnection.mSdServer.mSdData;
|
||||||
currentHrReadCount++;
|
|
||||||
if (hr >= 0.0) {
|
|
||||||
currentHrSum += hr;
|
|
||||||
currentHrValidCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateHeartRateProgressText();
|
currentHrReadCount++;
|
||||||
}
|
|
||||||
|
boolean freshNotification = sdData.mHrNotificationCount != hrLastSeenNotificationCount;
|
||||||
|
if (freshNotification) {
|
||||||
|
int newNotifications = (int) (sdData.mHrNotificationCount - hrLastSeenNotificationCount);
|
||||||
|
if (newNotifications < 0) newNotifications = 0;
|
||||||
|
|
||||||
|
currentHrFreshNotificationCount += newNotifications;
|
||||||
|
hrLastSeenNotificationCount = sdData.mHrNotificationCount;
|
||||||
|
|
||||||
|
currentHrLastUpdateMillisInSecond = sdData.mHrLastUpdateMillis;
|
||||||
|
currentHrRawValue = sdData.mHrRawValue;
|
||||||
|
currentHrSensorContactStatus = sdData.mHrSensorContactStatus;
|
||||||
|
|
||||||
|
double hr = sdData.mHR;
|
||||||
|
if (hr >= 0.0) {
|
||||||
|
currentHrSum += hr;
|
||||||
|
currentHrValidCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHeartRateProgressText();
|
||||||
|
}
|
||||||
|
|
||||||
private void flushCurrentHeartRateSecond() {
|
private void flushCurrentHeartRateSecond() {
|
||||||
if (currentHrSecond < 0) return;
|
if (currentHrSecond < 0) return;
|
||||||
|
|
||||||
double averageHr = currentHrValidCount > 0 ? currentHrSum / currentHrValidCount : -1.0;
|
double averageHr = currentHrValidCount > 0 ? currentHrSum / currentHrValidCount : -1.0;
|
||||||
long timestampMillis = hrStartMillis + currentHrSecond * 1000L;
|
long timestampMillis = hrStartMillis + currentHrSecond * 1000L;
|
||||||
String timestampIso = timestampIsoLocal(timestampMillis);
|
String timestampIso = timestampIsoLocal(timestampMillis);
|
||||||
String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.UK).format(new Date(timestampMillis));
|
String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.UK).format(new Date(timestampMillis));
|
||||||
|
|
||||||
hrPoints.add(new HeartRatePoint(
|
long lastHrAgeMs = -1;
|
||||||
hrPoints.size(),
|
if (currentHrLastUpdateMillisInSecond > 0) {
|
||||||
timestampIso,
|
lastHrAgeMs = timestampMillis - currentHrLastUpdateMillisInSecond;
|
||||||
timestampMillis,
|
if (lastHrAgeMs < 0) lastHrAgeMs = 0;
|
||||||
timestamp,
|
}
|
||||||
currentHrSecond,
|
|
||||||
averageHr,
|
hrPoints.add(new HeartRatePoint(
|
||||||
currentHrValidCount,
|
hrPoints.size(),
|
||||||
currentHrReadCount));
|
timestampIso,
|
||||||
}
|
timestampMillis,
|
||||||
|
timestamp,
|
||||||
|
currentHrSecond,
|
||||||
|
averageHr,
|
||||||
|
currentHrValidCount,
|
||||||
|
currentHrReadCount,
|
||||||
|
currentHrFreshNotificationCount,
|
||||||
|
lastHrAgeMs,
|
||||||
|
currentHrRawValue,
|
||||||
|
currentHrSensorContactStatus));
|
||||||
|
}
|
||||||
|
|
||||||
private void updateHeartRateProgressText() {
|
private void updateHeartRateProgressText() {
|
||||||
double elapsedSeconds = 0.0;
|
double elapsedSeconds = 0.0;
|
||||||
@@ -568,12 +638,18 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
double min = Double.MAX_VALUE;
|
double min = Double.MAX_VALUE;
|
||||||
double max = -Double.MAX_VALUE;
|
double max = -Double.MAX_VALUE;
|
||||||
int validSeconds = 0;
|
int validSeconds = 0;
|
||||||
int totalReadings = 0;
|
int totalReadings = 0;
|
||||||
int validReadings = 0;
|
int validReadings = 0;
|
||||||
|
int freshNotificationSeconds = 0;
|
||||||
|
int totalFreshNotifications = 0;
|
||||||
|
|
||||||
for (HeartRatePoint point : hrPoints) {
|
for (HeartRatePoint point : hrPoints) {
|
||||||
totalReadings += point.readingsInSecond;
|
totalReadings += point.readingsInSecond;
|
||||||
validReadings += point.validReadingsInSecond;
|
validReadings += point.validReadingsInSecond;
|
||||||
|
totalFreshNotifications += point.freshNotificationsInSecond;
|
||||||
|
if (point.freshNotificationsInSecond > 0) {
|
||||||
|
freshNotificationSeconds++;
|
||||||
|
}
|
||||||
if (point.valid) {
|
if (point.valid) {
|
||||||
sum += point.averageHr;
|
sum += point.averageHr;
|
||||||
min = Math.min(min, point.averageHr);
|
min = Math.min(min, point.averageHr);
|
||||||
@@ -593,6 +669,8 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
sb.append("Seconds with valid HR: ").append(validSeconds).append("\n");
|
sb.append("Seconds with valid HR: ").append(validSeconds).append("\n");
|
||||||
sb.append("Total reads: ").append(totalReadings).append("\n");
|
sb.append("Total reads: ").append(totalReadings).append("\n");
|
||||||
sb.append("Valid reads: ").append(validReadings).append("\n");
|
sb.append("Valid reads: ").append(validReadings).append("\n");
|
||||||
|
sb.append("Seconds with fresh HR notifications: ").append(freshNotificationSeconds).append("\n");
|
||||||
|
sb.append("Fresh HR notifications: ").append(totalFreshNotifications).append("\n");
|
||||||
if (validSeconds > 0) {
|
if (validSeconds > 0) {
|
||||||
sb.append("Average HR: ").append(df2.format(sum / validSeconds)).append(" bpm\n");
|
sb.append("Average HR: ").append(df2.format(sum / validSeconds)).append(" bpm\n");
|
||||||
sb.append("Min HR: ").append(df2.format(min)).append(" bpm\n");
|
sb.append("Min HR: ").append(df2.format(min)).append(" bpm\n");
|
||||||
@@ -608,17 +686,21 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
File file = new File(getOutputDir(), "validation_hr_" + timestampForFilename() + ".csv");
|
File file = new File(getOutputDir(), "validation_hr_" + timestampForFilename() + ".csv");
|
||||||
try {
|
try {
|
||||||
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file));
|
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file));
|
||||||
writer.write("point_index,timestamp_iso,epoch_ms,time_hhmmss,elapsed_seconds,avg_hr_bpm,valid,valid_readings_in_second,total_readings_in_second\n");
|
writer.write("point_index,timestamp_iso,epoch_ms,time_hhmmss,elapsed_seconds,avg_hr_bpm,valid,valid_readings_in_second,total_reads_in_second,fresh_notifications_in_second,last_hr_age_ms,raw_hr_value,sensor_contact_status\n");
|
||||||
for (HeartRatePoint point : hrPoints) {
|
for (HeartRatePoint point : hrPoints) {
|
||||||
writer.write(point.index + ","
|
writer.write(point.index + ","
|
||||||
+ point.timestampIso + ","
|
+ point.timestampIso + ","
|
||||||
+ point.epochMillis + ","
|
+ point.epochMillis + ","
|
||||||
+ point.timestampHhMmSs + ","
|
+ point.timestampHhMmSs + ","
|
||||||
+ point.elapsedSecond + ","
|
+ point.elapsedSecond + ","
|
||||||
+ (point.valid ? String.format(Locale.UK, "%.3f", point.averageHr) : "") + ","
|
+ (point.valid ? String.format(Locale.UK, "%.3f", point.averageHr) : "") + ","
|
||||||
+ point.valid + ","
|
+ point.valid + ","
|
||||||
+ point.validReadingsInSecond + ","
|
+ point.validReadingsInSecond + ","
|
||||||
+ point.readingsInSecond + "\n");
|
+ point.readingsInSecond + ","
|
||||||
|
+ point.freshNotificationsInSecond + ","
|
||||||
|
+ point.lastHrAgeMs + ","
|
||||||
|
+ point.rawHrValue + ","
|
||||||
|
+ point.sensorContactStatus + "\n");
|
||||||
}
|
}
|
||||||
writer.close();
|
writer.close();
|
||||||
mUtil.showToast("Saved HR CSV: " + file.getAbsolutePath());
|
mUtil.showToast("Saved HR CSV: " + file.getAbsolutePath());
|
||||||
@@ -629,35 +711,48 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class HeartRatePoint {
|
private static class HeartRatePoint {
|
||||||
final int index;
|
final int index;
|
||||||
final String timestampIso;
|
final String timestampIso;
|
||||||
final long epochMillis;
|
final long epochMillis;
|
||||||
final String timestampHhMmSs;
|
final String timestampHhMmSs;
|
||||||
final int elapsedSecond;
|
final int elapsedSecond;
|
||||||
final double averageHr;
|
final double averageHr;
|
||||||
final boolean valid;
|
final boolean valid;
|
||||||
final int validReadingsInSecond;
|
final int validReadingsInSecond;
|
||||||
final int readingsInSecond;
|
final int readingsInSecond;
|
||||||
|
|
||||||
HeartRatePoint(int index,
|
final int freshNotificationsInSecond;
|
||||||
String timestampIso,
|
final long lastHrAgeMs;
|
||||||
long epochMillis,
|
final int rawHrValue;
|
||||||
String timestampHhMmSs,
|
final int sensorContactStatus;
|
||||||
int elapsedSecond,
|
|
||||||
double averageHr,
|
HeartRatePoint(int index,
|
||||||
int validReadingsInSecond,
|
String timestampIso,
|
||||||
int readingsInSecond) {
|
long epochMillis,
|
||||||
this.index = index;
|
String timestampHhMmSs,
|
||||||
this.timestampIso = timestampIso;
|
int elapsedSecond,
|
||||||
this.epochMillis = epochMillis;
|
double averageHr,
|
||||||
this.timestampHhMmSs = timestampHhMmSs;
|
int validReadingsInSecond,
|
||||||
this.elapsedSecond = elapsedSecond;
|
int readingsInSecond,
|
||||||
this.averageHr = averageHr;
|
int freshNotificationsInSecond,
|
||||||
this.valid = validReadingsInSecond > 0;
|
long lastHrAgeMs,
|
||||||
this.validReadingsInSecond = validReadingsInSecond;
|
int rawHrValue,
|
||||||
this.readingsInSecond = readingsInSecond;
|
int sensorContactStatus) {
|
||||||
}
|
this.index = index;
|
||||||
}
|
this.timestampIso = timestampIso;
|
||||||
|
this.epochMillis = epochMillis;
|
||||||
|
this.timestampHhMmSs = timestampHhMmSs;
|
||||||
|
this.elapsedSecond = elapsedSecond;
|
||||||
|
this.averageHr = averageHr;
|
||||||
|
this.valid = validReadingsInSecond > 0;
|
||||||
|
this.validReadingsInSecond = validReadingsInSecond;
|
||||||
|
this.readingsInSecond = readingsInSecond;
|
||||||
|
this.freshNotificationsInSecond = freshNotificationsInSecond;
|
||||||
|
this.lastHrAgeMs = lastHrAgeMs;
|
||||||
|
this.rawHrValue = rawHrValue;
|
||||||
|
this.sensorContactStatus = sensorContactStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private File getOutputDir() {
|
private File getOutputDir() {
|
||||||
File dir = new File(getExternalFilesDir(null), "sensor_validation");
|
File dir = new File(getExternalFilesDir(null), "sensor_validation");
|
||||||
|
|||||||
Reference in New Issue
Block a user