Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.json.JSONObject;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
Expand Down Expand Up @@ -63,6 +64,8 @@ public class BesLogManager {
private final StringBuilder mLogBuffer = new StringBuilder();
private boolean mIsReceiving = false;
private final AtomicBoolean mFinished = new AtomicBoolean(false);
private final CountDownLatch mCollectionFinished = new CountDownLatch(1);
private String mCollectedLog;

private final Runnable mFirstPacketTimeout;
private final Runnable mOverallTimeout;
Expand Down Expand Up @@ -156,13 +159,37 @@ public boolean isFinished() {
return mFinished.get();
}

/**
* Wait for capture settlement only; the independent HTTP upload can still be running.
* A caller deadline or interruption finalizes this capture through the same partial-log path
* as its watchdog, so another incident cannot replace a still-active session.
*/
public boolean awaitCollection(long timeout, TimeUnit unit) throws InterruptedException {
try {
boolean completed = mCollectionFinished.await(timeout, unit);
if (!completed) finish("incident_capture_wait_expired");
return completed;
} catch (InterruptedException e) {
finish("incident_capture_interrupted");
throw e;
}
}

/** Return the frozen firmware payload after collection settlement, without waiting for callbacks. */
public String getCollectedFirmwareJson() {
if (mCollectionFinished.getCount() != 0) {
throw new IllegalStateException("BES collection has not settled");
}
return buildFirmwareUploadJson(mCollectedLog);
}

/**
* Process one incoming {@code sr_log} packet.
*
* @param cur packet sequence number; 255 signals end of stream
* @param body log text chunk, or {@code "end"} for the terminator packet
*/
public void onLogPacketReceived(int cur, String body) {
public synchronized void onLogPacketReceived(int cur, String body) {
if (mFinished.get()) return;

if (!mIsReceiving) {
Expand Down Expand Up @@ -191,12 +218,14 @@ public void onLogPacketReceived(int cur, String body) {
*
* @param timeoutReason null on normal completion, otherwise a reason string
*/
private void finish(String timeoutReason) {
private synchronized void finish(String timeoutReason) {
if (!mFinished.compareAndSet(false, true)) return;
mHandler.removeCallbacks(mFirstPacketTimeout);
mHandler.removeCallbacks(mOverallTimeout);

String fullLog = mLogBuffer.toString();
mCollectedLog = fullLog;
mCollectionFinished.countDown();

if (timeoutReason != null) {
Log.w(TAG, "⚠️ BES log collection ended due to: " + timeoutReason
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,9 @@ public void requestBesLogs(
* ServerConfigUtil (only used when {@code relayFirmwareJson} is null)
* @param relayFirmwareJson if non-null, assembled BES logs are passed to this consumer as
* glasses_firmware JSON instead of HTTP upload
* @return the exact started capture session, or null if no capture started
*/
public void requestBesLogs(
public BesLogManager requestBesLogs(
String incidentId,
Context context,
IConfigurationManager configManager,
Expand All @@ -177,22 +178,23 @@ public void requestBesLogs(
if (relayFirmwareJson != null) {
relayFirmwareJson.accept(BesLogManager.buildFirmwareUploadJson(""));
}
return;
return null;
}

if (!serviceManager.getBluetoothManager().isConnected()) {
Log.w(TAG, "⚠️ UART not connected — cannot request BES logs");
if (relayFirmwareJson != null) {
relayFirmwareJson.accept(BesLogManager.buildFirmwareUploadJson(""));
}
return;
return null;
}

// Replace any stale session from a previous request
mBesLogSession =
BesLogManager session =
(relayFirmwareJson != null)
? new BesLogManager(incidentId, context, configManager, relayFirmwareJson)
: new BesLogManager(incidentId, context, configManager, apiBaseUrl);
mBesLogSession = session;

try {
JSONObject k900Command = new JSONObject();
Expand All @@ -211,7 +213,8 @@ public void requestBesLogs(

if (sent) {
Log.i(TAG, "✅ mh_logs sent — starting BES log collection timeouts");
mBesLogSession.startTimeouts();
session.startTimeouts();
return session;
} else {
Log.e(TAG, "❌ Failed to send mh_logs");
mBesLogSession = null;
Expand All @@ -226,6 +229,7 @@ public void requestBesLogs(
relayFirmwareJson.accept(BesLogManager.buildFirmwareUploadJson(""));
}
}
return null;
}

public boolean requestBesLogsForTrace(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
* Handles the "upload_incident_logs" BLE command from the phone.
Expand Down Expand Up @@ -200,13 +198,11 @@ public void onResponse(Call call, Response response) {
+ " ("
+ logs.length()
+ " entries)");
if (mK900CommandHandler != null) {
mK900CommandHandler.requestBesLogs(
incidentId,
mContext,
mConfigurationManager,
apiBaseUrl);
}
new Thread(
() ->
captureBesLogsOverHttp(
incidentId, apiBaseUrl))
.start();
} else {
Log.e(
TAG,
Expand All @@ -232,7 +228,29 @@ private void triggerBleFallback(String incidentId, String reason) {
new Thread(() -> relayLogsViaBle(incidentId)).start();
}

void relayLogsViaBle(String incidentId) {
synchronized void captureBesLogsOverHttp(String incidentId, String apiBaseUrl) {
if (mK900CommandHandler == null) return;
try {
BesLogManager session =
mK900CommandHandler.requestBesLogs(
incidentId, mContext, mConfigurationManager, apiBaseUrl, null);
awaitBesCapture(session, incidentId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.e(TAG, "HTTP incident BES capture interrupted for " + incidentId, e);
}
}

private void awaitBesCapture(BesLogManager session, String incidentId)
throws InterruptedException {
if (session != null && !session.awaitCollection(BES_WAIT_SEC, TimeUnit.SECONDS)) {
Log.w(TAG, "Incident BES capture wait expired after partial settlement: " + incidentId);
}
}

// Both reports share one BES capture session and one BLE file-transfer slot.
// Hold this handler's monitor for the complete B/L transaction, not just its idle check.
synchronized void relayLogsViaBle(String incidentId) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Serialize HTTP-triggered BES captures with BLE relays

The monitor guards only relayLogsViaBle; the successful callback in uploadLogsOverHttp still calls requestBesLogs directly. If report A enters BLE fallback while report B's logcat HTTP request succeeds, B replaces the single K900CommandHandler.mBesLogSession while A is awaiting its latch, so A can time out and send an empty firmware payload while subsequent BES packets are attributed to B. Serialize every incident BES capture through the same lock rather than only the BLE relay path.

Useful? React with 👍 / 👎.

IBluetoothManager bt =
mServiceManager != null ? mServiceManager.getBluetoothManager() : null;
if (bt == null) {
Expand All @@ -251,33 +269,17 @@ void relayLogsViaBle(String incidentId) {
return;
}

CountDownLatch besDone = new CountDownLatch(1);
AtomicReference<String> firmwareJson = new AtomicReference<>(null);

BesLogManager session = null;
if (mK900CommandHandler != null) {
mK900CommandHandler.requestBesLogs(
incidentId,
mContext,
mConfigurationManager,
json -> {
firmwareJson.set(json);
besDone.countDown();
});
} else {
besDone.countDown();
}

boolean besFinished = besDone.await(BES_WAIT_SEC, TimeUnit.SECONDS);
if (!besFinished) {
Log.w(
TAG,
"Timed out waiting for BES log collection — sending empty firmware"
+ " payload");
}
String fwJson = firmwareJson.get();
if (fwJson == null) {
fwJson = BesLogManager.buildFirmwareUploadJson("");
// Select BLE delivery; the exact session supplies its frozen JSON after settlement,
// independently from callback scheduling or the handler's ownership monitor.
session = mK900CommandHandler.requestBesLogs(
incidentId, mContext, mConfigurationManager, "", json -> {});
}
awaitBesCapture(session, incidentId);
String fwJson = session != null
? session.getCollectedFirmwareJson()
: BesLogManager.buildFirmwareUploadJson("");

relayFileViaBle(bt, incidentId, "glasses_firmware", 'B', fwJson);

Expand Down
Loading
Loading