record lux_fast and lux _slow

This commit is contained in:
2024-12-11 17:00:53 +08:00
parent 6d89725d65
commit 2a4e21fa25
9 changed files with 444 additions and 202 deletions

View File

@@ -9,6 +9,9 @@ import android.hardware.SensorEventListener;
import android.hardware.SensorManager; import android.hardware.SensorManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Toast; import android.widget.Toast;
@@ -18,6 +21,7 @@ import androidx.annotation.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import io.flutter.Log;
import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.EventChannel;
@@ -28,11 +32,23 @@ public class MainActivity extends FlutterActivity implements EventChannel.Stream
private Sensor mLightSensor; private Sensor mLightSensor;
private SensorManager mSensorManager; private SensorManager mSensorManager;
private EventChannel.EventSink mEventSink; private EventChannel.EventSink mEventSink;
private AmbientLightRingBuffer mAmbientLightRingBuffer;
private long mAmbientLightHorizonLong = 4000;
private int mRecentLightSamples = 0;
private float mLastObservedLux;
private long mLastObservedLuxTime;
private Handler mHandler;
private long mAmbientLightHorizonShort = 2000;
private float mSlowAmbientLux;
private float mFastAmbientLux;
private long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
mHandler = new AutomaticBrightnessHandler();
} }
@Override @Override
@@ -49,6 +65,7 @@ public class MainActivity extends FlutterActivity implements EventChannel.Stream
super.onResume(); super.onResume();
mSensorManager.registerListener(this, mLightSensor, SensorManager.SENSOR_DELAY_FASTEST); mSensorManager.registerListener(this, mLightSensor, SensorManager.SENSOR_DELAY_FASTEST);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
mAmbientLightRingBuffer = new AmbientLightRingBuffer(250, 4000, new RealClock(false));
} }
@Override @Override
@@ -69,12 +86,14 @@ public class MainActivity extends FlutterActivity implements EventChannel.Stream
@Override @Override
public void onSensorChanged(SensorEvent sensorEvent) { public void onSensorChanged(SensorEvent sensorEvent) {
if (mEventSink != null) { final long time = SystemClock.uptimeMillis();
final Map data = new HashMap(); final float lux = sensorEvent.values[0];
data.put("lux", sensorEvent.values[0]); Message msg = mHandler.obtainMessage();
data.put("timestamp", sensorEvent.timestamp); Bundle data = new Bundle();
mEventSink.success(data); data.putFloat("lux", lux);
} data.putLong("time", time);
msg.setData(data);
msg.sendToTarget();
} }
@Override @Override
@@ -99,4 +118,297 @@ public class MainActivity extends FlutterActivity implements EventChannel.Stream
break; break;
} }
} }
/**
* A ring buffer of ambient light measurements sorted by time.
*
* Each entry consists of a timestamp and a lux measurement, and the overall buffer is sorted
* from oldest to newest.
*/
private static final class AmbientLightRingBuffer {
// Proportional extra capacity of the buffer beyond the expected number of light samples
// in the horizon
private static final float BUFFER_SLACK = 1.5f;
private float[] mRingLux;
private long[] mRingTime;
private int mCapacity;
// The first valid element and the next open slot.
// Note that if mCount is zero then there are no valid elements.
private int mStart;
private int mEnd;
private int mCount;
Clock mClock;
public AmbientLightRingBuffer(long lightSensorRate, int ambientLightHorizon, Clock clock) {
if (lightSensorRate <= 0) {
throw new IllegalArgumentException("lightSensorRate must be above 0");
}
mCapacity = (int) Math.ceil(ambientLightHorizon * BUFFER_SLACK / lightSensorRate);
mRingLux = new float[mCapacity];
mRingTime = new long[mCapacity];
mClock = clock;
}
public float getLux(int index) {
return mRingLux[offsetOf(index)];
}
public float[] getAllLuxValues() {
float[] values = new float[mCount];
if (mCount == 0) {
return values;
}
if (mStart < mEnd) {
System.arraycopy(mRingLux, mStart, values, 0, mCount);
} else {
System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart);
System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd);
}
return values;
}
public long getTime(int index) {
return mRingTime[offsetOf(index)];
}
public long[] getAllTimestamps() {
long[] values = new long[mCount];
if (mCount == 0) {
return values;
}
if (mStart < mEnd) {
System.arraycopy(mRingTime, mStart, values, 0, mCount);
} else {
System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart);
System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd);
}
return values;
}
public void push(long time, float lux) {
int next = mEnd;
if (mCount == mCapacity) {
int newSize = mCapacity * 2;
float[] newRingLux = new float[newSize];
long[] newRingTime = new long[newSize];
int length = mCapacity - mStart;
System.arraycopy(mRingLux, mStart, newRingLux, 0, length);
System.arraycopy(mRingTime, mStart, newRingTime, 0, length);
if (mStart != 0) {
System.arraycopy(mRingLux, 0, newRingLux, length, mStart);
System.arraycopy(mRingTime, 0, newRingTime, length, mStart);
}
mRingLux = newRingLux;
mRingTime = newRingTime;
next = mCapacity;
mCapacity = newSize;
mStart = 0;
}
mRingTime[next] = time;
mRingLux[next] = lux;
mEnd = next + 1;
if (mEnd == mCapacity) {
mEnd = 0;
}
mCount++;
}
public void prune(long horizon) {
if (mCount == 0) {
return;
}
while (mCount > 1) {
int next = mStart + 1;
if (next >= mCapacity) {
next -= mCapacity;
}
if (mRingTime[next] > horizon) {
// Some light sensors only produce data upon a change in the ambient light
// levels, so we need to consider the previous measurement as the ambient light
// level for all points in time up until we receive a new measurement. Thus, we
// always want to keep the youngest element that would be removed from the
// buffer and just set its measurement time to the horizon time since at that
// point it is the ambient light level, and to remove it would be to drop a
// valid data point within our horizon.
break;
}
mStart = next;
mCount -= 1;
}
if (mRingTime[mStart] < horizon) {
mRingTime[mStart] = horizon;
}
}
public int size() {
return mCount;
}
public void clear() {
mStart = 0;
mEnd = 0;
mCount = 0;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append('[');
for (int i = 0; i < mCount; i++) {
final long next = i + 1 < mCount ? getTime(i + 1)
: mClock.getSensorEventScaleTime();
if (i != 0) {
buf.append(", ");
}
buf.append(getLux(i));
buf.append(" / ");
buf.append(next - getTime(i));
buf.append("ms");
}
buf.append(']');
return buf.toString();
}
private int offsetOf(int index) {
if (index >= mCount || index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
index += mStart;
if (index >= mCapacity) {
index -= mCapacity;
}
return index;
}
}
private static class RealClock implements Clock {
private final boolean mOffloadControlsDozeBrightness;
RealClock(boolean offloadControlsDozeBrightness) {
mOffloadControlsDozeBrightness = offloadControlsDozeBrightness;
}
@Override
public long uptimeMillis() {
return SystemClock.uptimeMillis();
}
public long getSensorEventScaleTime() {
return (mOffloadControlsDozeBrightness)
? SystemClock.elapsedRealtime() : uptimeMillis();
}
}
interface Clock {
/**
* Returns current time in milliseconds since boot, not counting time spent in deep sleep.
*/
long uptimeMillis();
/**
* Gets the time on either the elapsedTime or the uptime scale, depending on how we
* processing the events from the sensor
*/
long getSensorEventScaleTime();
}
private class AutomaticBrightnessHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
default:
final long time = msg.getData().getLong("time");
final float lux = msg.getData().getFloat("lux");
handleLightSensorEvent(time, lux);
break;
}
}
}
private void handleLightSensorEvent(long time, float lux) {
mRecentLightSamples++;
mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
mAmbientLightRingBuffer.push(time, lux);
// Remember this sample value.
mLastObservedLux = lux;
mLastObservedLuxTime = time;
//updateAmbientLux(time);
mFastAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonShort);
mSlowAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonLong);
// Log.i("moon", "mFastAmbientLux: " + mFastAmbientLux);
// Log.i("moon", "mSlowAmbientLux: " + mSlowAmbientLux);
if (mEventSink != null) {
final Map data = new HashMap();
data.put("lux", lux);
data.put("lux_fast", mFastAmbientLux);
data.put("lux_slow", mSlowAmbientLux);
data.put("timestamp", time);
mEventSink.success(data);
}
}
private float calculateAmbientLux(long now, long horizon) {
final int N = mAmbientLightRingBuffer.size();
if (N == 0) {
// Slog.e(TAG, "calculateAmbientLux: No ambient light readings available");
return -1;
}
// Find the first measurement that is just outside of the horizon.
int endIndex = 0;
final long horizonStartTime = now - horizon;
for (int i = 0; i < N-1; i++) {
if (mAmbientLightRingBuffer.getTime(i + 1) <= horizonStartTime) {
endIndex++;
} else {
break;
}
}
// if (mLoggingEnabled) {
// Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=(" +
// mAmbientLightRingBuffer.getTime(endIndex) + ", " +
// mAmbientLightRingBuffer.getLux(endIndex) + ")");
// }
float sum = 0;
float totalWeight = 0;
long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
for (int i = N - 1; i >= endIndex; i--) {
long eventTime = mAmbientLightRingBuffer.getTime(i);
if (i == endIndex && eventTime < horizonStartTime) {
// If we're at the final value, make sure we only consider the part of the sample
// within our desired horizon.
eventTime = horizonStartTime;
}
final long startTime = eventTime - now;
float weight = calculateWeight(startTime, endTime);
float lux = mAmbientLightRingBuffer.getLux(i);
// if (mLoggingEnabled) {
// Log.i("moon", "calculateAmbientLux: [" + startTime + ", " + endTime + "]: " +
// "lux=" + lux + ", " +
// "weight=" + weight);
// }
totalWeight += weight;
sum += lux * weight;
endTime = startTime;
}
// if (mLoggingEnabled) {
// Log.i("moon", "calculateAmbientLux: " +
// "totalWeight=" + totalWeight + ", " +
// "newAmbientLux=" + (sum / totalWeight));
// }
return sum / totalWeight;
}
private float calculateWeight(long startDelta, long endDelta) {
return weightIntegral(endDelta) - weightIntegral(startDelta);
}
private float weightIntegral(long x) {
return x * (x * 0.5f + 4000);
}
} }

View File

@@ -3,9 +3,13 @@ import 'dart:ffi';
class LightSensorEvent { class LightSensorEvent {
final double lux; final double lux;
final int timestamp; final int timestamp;
LightSensorEvent(this.lux, this.timestamp); final double lux_fast;
final double lux_slow;
LightSensorEvent(this.lux, this.timestamp, this.lux_fast, this.lux_slow);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'lux': lux,
'timestamp': timestamp, 'timestamp': timestamp,
'lux': lux,
'lux_fast': lux_fast,
'lux_slow': lux_slow,
}; };
} }

View File

@@ -174,8 +174,8 @@ class _MyHomePageState extends State<MyHomePage> {
Stream<LightSensorEvent> streamSensorEventFromNative() { Stream<LightSensorEvent> streamSensorEventFromNative() {
const eventChannel = EventChannel('tw.moonjuice.light_sensor.stream'); const eventChannel = EventChannel('tw.moonjuice.light_sensor.stream');
return eventChannel return eventChannel.receiveBroadcastStream().map((event) =>
.receiveBroadcastStream() LightSensorEvent(event['lux'], event['timestamp'], event['lux_fast'],
.map((event) => LightSensorEvent(event['lux'], event['timestamp'])); event['lux_slow']));
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

View File

@@ -5,21 +5,15 @@
<title>D3.js Multiple Line Chart</title> <title>D3.js Multiple Line Chart</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<style> <style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .line {
.line { fill: none; stroke-width: 2px; } fill: none;
.axis { font-size: 12px; } stroke-width: 2px;
.legend { font-size: 12px; } }
.tooltip { .axis-label {
position: absolute; font-size: 16px;
text-align: center; }
width: 120px; .legend {
height: 40px; font-size: 16px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
} }
</style> </style>
</head> </head>
@@ -28,207 +22,140 @@
<div id="chart"></div> <div id="chart"></div>
<script> <script>
// Sample multiple dataset // Data
const data = [ const sample_data = [
{"lux":10.0,"timestamp":442470841810000}, {"lux":1263.0,"timestamp":18188401,"lux_fast":1000.0,"lux_slow":900.0},
{"lux":20.0,"timestamp":442471010509000}, {"lux":1000.0,"timestamp":18188571,"lux_fast":1263.0,"lux_slow":1000.0},
{"lux":30.0,"timestamp":442471182576000}, {"lux":900.0,"timestamp":18188741,"lux_fast":1262.0,"lux_slow":1263.0}
{"lux":40.0,"timestamp":442471360117000},
{"lux":100.0,"timestamp":442471522264000}
]; ];
const colorMap = new Map();
colorMap.set('original', 'steelblue');
colorMap.set('fastLuxData', 'green');
colorMap.set('slowLuxData', 'red');
function weightIntegral(x) { function createChart(data) {
return x * (x * 0.5 + 4000000000); d3.select("#chart").select("svg").remove();
} // Set the dimensions and margins of the graph
const margin = {top: 20, right: 80, bottom: 50, left: 60};
function calculateLux(datas, now) { const width = 600 - margin.left - margin.right;
var sum = 0, totalWeight = 0, endDelta = 100000000;
for (var i = datas.length - 1; i >= 0; i--) {
var eventTime = datas[i].timestamp;
var startDelta = eventTime - now;
var weight = weightIntegral(endDelta) - weightIntegral(startDelta);
var lux = datas[i].lux;
totalWeight += weight;
sum += lux * weight;
endDelta = startDelta;
}
return sum / totalWeight;
}
// Render multiple line chart
function renderMultiLineChart(data) {
var timestampNow;
var fastLuxData = [];
var slowLuxData = [];
var datasets = [];
for (timestampNow=data[0].timestamp; timestampNow<=data[data.length-1].timestamp; timestampNow+=200000000) {
var fastRingBuffer = data.filter((value) => value.timestamp >= timestampNow-2000000000 && value.timestamp <= timestampNow);
var slowRingBuffer = data.filter((value) => value.timestamp >= timestampNow-4000000000 && value.timestamp <= timestampNow);
if (fastRingBuffer.length <= 1) {
fastLuxData.push(data[0]);
} else {
fastLuxData.push({timestamp:timestampNow, lux:calculateLux(fastRingBuffer, timestampNow)});
}
if (slowRingBuffer.length <= 1) {
slowLuxData.push(data[0]);
} else {
slowLuxData.push({timestamp:timestampNow, lux:calculateLux(slowRingBuffer, timestampNow)});
}
}
var dataObj = new Object();
dataObj.name = "original";
dataObj.values = data;
datasets.push(dataObj);
var fastLuxDataObj = new Object();
fastLuxDataObj.name = "fastLuxData";
fastLuxDataObj.values = fastLuxData;
datasets.push(fastLuxDataObj);
var slowLuxDataObj = new Object();
slowLuxDataObj.name = "slowLuxData";
slowLuxDataObj.values = slowLuxData;
datasets.push(slowLuxDataObj);
console.log(datasets.length);
console.log(datasets[0]);
// Clear previous chart
d3.select("#chart").html("");
// Chart dimensions
const margin = {top: 50, right: 50, bottom: 50, left: 50};
const width = 800 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom; const height = 400 - margin.top - margin.bottom;
// Tooltip // Append the svg object to the body of the page
const tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Create SVG
const svg = d3.select("#chart") const svg = d3.select("#chart")
.append("svg") .append("svg")
.attr("width", width + margin.left + margin.right) .attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom) .attr("height", height + margin.top + margin.bottom)
.append("g") .append("g")
.attr("transform", `translate(${margin.left},${margin.top})`); .attr("transform", `translate(${margin.left},${margin.top})`);
// Parse dates // X scale
const parseTime = d3.timeParse("%Y-%m-%d");
const formatTime = d3.timeFormat("%Y-%m-%d");
/*datasets.forEach(series => {
series.values.forEach(d => {
d.timestamp = parseTime(d.timestamp);
});
});*/
// Combine all values for scale
const allValues = datasets.flatMap(series => series.values.map(d => d.lux));
// Scales
const x = d3.scaleLinear() const x = d3.scaleLinear()
.domain(d3.extent(datasets[0].values, d => d.timestamp)) .domain(d3.extent(data, d => d.timestamp))
.range([0, width]); .range([0, width]);
// Y scale
const y = d3.scaleLinear() const y = d3.scaleLinear()
.domain([ .domain([d3.min(data, d => Math.min(d.lux, d.lux_fast, d.lux_slow)) - 0.5,
d3.min(allValues) * 0.9, d3.max(data, d => Math.max(d.lux, d.lux_fast, d.lux_slow)) + 0.5])
d3.max(allValues) * 1.1
])
.range([height, 0]); .range([height, 0]);
// Add X axis
svg.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x));
// Add Y axis
svg.append("g")
.call(d3.axisLeft(y));
// X axis label
svg.append("text")
.attr("class", "axis-label")
.attr("text-anchor", "middle")
.attr("x", width / 2)
.attr("y", height + margin.bottom - 10)
.text("Timestamp");
// Y axis label
svg.append("text")
.attr("class", "axis-label")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.attr("y", -margin.left + 20)
.attr("x", -height / 2)
.text("Lux Value");
// Line generator // Line generator
const line = d3.line() const line = d3.line()
.x(d => x(d.timestamp)) .x(d => x(d.timestamp))
.y(d => y(d.lux)); .y(d => y(d.lux));
// X-axis // Add the lux line
svg.append("g") svg.append("path")
.attr("class", "axis") .datum(data)
.attr("transform", `translate(0,${height})`) .attr("class", "line")
.call(d3.axisBottom(x)); .attr("d", line)
.attr("stroke", "blue");
// Y-axis // Add the lux_fast
svg.append("g") svg.append("path")
.attr("class", "axis") .datum(data)
.call(d3.axisLeft(y)); .attr("class", "line")
.attr("d", d3.line()
.x(d => x(d.timestamp))
.y(d => y(d.lux_fast)))
.attr("stroke", "red");
// Lines and data points for each dataset // Add the lux_slow line
datasets.forEach(series => { svg.append("path")
// Line .datum(data)
svg.append("path") .attr("class", "line")
.datum(series.values) .attr("d", d3.line()
.attr("class", "line") .x(d => x(d.timestamp))
.attr("fill", "none") .y(d => y(d.lux_slow)))
.attr("stroke", colorMap.get(series.name)) .attr("stroke", "green");
.attr("stroke-width", 2)
.attr("d", line);
// Data points // Add legend
svg.selectAll(`.dot-${series.name}`) const legend = svg.append("g")
.data(series.values) .attr("class", "legend")
.enter().append("circle") .attr("transform", `translate(${width + 10}, 0)`);
.attr("class", `dot dot-${series.name}`)
.attr("cx", d => x(d.timestamp)) const legendItems = [
.attr("cy", d => y(d.lux)) { label: "Lux", color: "blue" },
.attr("r", 5) { label: "Lux Fast", color: "red" },
.attr("fill", colorMap.get(series.name)) { label: "Lux Slow", color: "green" }
.on("mouseover", function(event, d) { ];
tooltip.transition()
.duration(200) legendItems.forEach((item, index) => {
.style("opacity", .9); const legendItem = legend.append("g")
tooltip.html(`${series.name}<br/>timestamp: ${d.timestamp}<br/>lux: ${d.lux}`) .attr("transform", `translate(0, ${index * 20})`);
.style("left", (event.pageX + 5) + "px")
.style("top", (event.pageY - 28) + "px"); legendItem.append("rect")
}) .attr("width", 10)
.on("mouseout", function(d) { .attr("height", 10)
tooltip.transition() .attr("fill", item.color);
.duration(500)
.style("opacity", 0); legendItem.append("text")
}); .attr("x", 15)
.attr("y", 10)
.text(item.label);
}); });
// Legend
const legend = svg.selectAll(".legend")
.data(datasets)
.enter().append("g")
.attr("class", "legend")
.attr("transform", (d, i) => `translate(${width + 10},${i * 20})`);
legend.append("rect")
.attr("x", 0)
.attr("width", 10)
.attr("height", 10)
.style("fill", d => colorMap.get(d.name));
legend.append("text")
.attr("x", 20)
.attr("y", 9)
.text(d => d.name)
.style("text-anchor", "start");
} }
// File input event listener
// Render chart
renderMultiLineChart(data);
// File input handler
document.getElementById('fileInput').addEventListener('change', function(e) { document.getElementById('fileInput').addEventListener('change', function(e) {
const file = e.target.files[0]; const file = e.target.files[0];
const reader = new FileReader(); if (file) {
const reader = new FileReader();
reader.onload = function(event) { reader.onload = function(e) {
try { const contents = e.target.result;
const jsonData = JSON.parse(event.target.result); try {
renderMultiLineChart(jsonData); const data = JSON.parse(contents);
} catch (error) { createChart(data);
alert('Error parsing JSON file: ' + error); } catch (error) {
} console.error("Error parsing JSON:", error);
}; alert("Error parsing JSON file. Please make sure it's a valid JSON.");
}
reader.readAsText(file); };
reader.readAsText(file);
}
}); });
createChart(sample_data);
</script> </script>
</body> </body>
</html> </html>