From dc6ed8843abe017da2bc4b3e4c78d18d47b82de1 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:26 +0000 Subject: [PATCH 01/47] Add hardware-agnostic RgbLedConstants with LED indexes and default brightness --- .../hardware/interfaces/RgbLedConstants.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 asg_client/app/src/main/java/com/mentra/asg_client/io/hardware/interfaces/RgbLedConstants.java diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/hardware/interfaces/RgbLedConstants.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/hardware/interfaces/RgbLedConstants.java new file mode 100644 index 0000000000..33cd772add --- /dev/null +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/hardware/interfaces/RgbLedConstants.java @@ -0,0 +1,21 @@ +package com.mentra.asg_client.io.hardware.interfaces; + +/** + * Hardware-agnostic RGB LED index and brightness constants. + * + *

These values define the wire contract between the phone and the glasses for RGB LED commands + * (LED index 0-4 plus a default brightness). Vendor-specific controllers (e.g. {@code + * K900RgbLedController}) must keep their own constants in sync with these values. + */ +public final class RgbLedConstants { + private RgbLedConstants() {} + + public static final int LED_RED = 0; + public static final int LED_GREEN = 1; + public static final int LED_BLUE = 2; + public static final int LED_ORANGE = 3; + public static final int LED_WHITE = 4; + + /** Default LED brightness (0-255 scale). */ + public static final int DEFAULT_BRIGHTNESS = 100; +} From 653ef68a7a72411b5efdd12fcf40b8f093e67f46 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:26 +0000 Subject: [PATCH 02/47] Add IBesOtaController interface for BES OTA subsystem operations --- .../io/ota/interfaces/IBesOtaController.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 asg_client/app/src/main/java/com/mentra/asg_client/io/ota/interfaces/IBesOtaController.java diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/interfaces/IBesOtaController.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/interfaces/IBesOtaController.java new file mode 100644 index 0000000000..24ab941178 --- /dev/null +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/interfaces/IBesOtaController.java @@ -0,0 +1,55 @@ +package com.mentra.asg_client.io.ota.interfaces; + +import androidx.annotation.Nullable; + +/** + * Operations on the BES coprocessor OTA subsystem. Implemented by the vendor-specific OTA manager + * (e.g. {@code BesOtaManager} on Mentra Live); core code interacts with BES firmware updates only + * through this interface. + */ +public interface IBesOtaController { + + /** + * @return true if a BES OTA update is currently in progress + */ + boolean isBesOtaInProgress(); + + /** + * Get the current firmware version reported by the BES device. + * + * @return version bytes [major, minor, patch, build], or null if not yet known + */ + @Nullable + byte[] getCurrentFirmwareVersion(); + + /** + * Parse a server-supplied version code into the byte format used for comparison. + * + * @param versionCode long version code from the server (XYYYZZZ format) + * @return byte array [major, minor, patch, build] + */ + byte[] parseServerVersionCode(long versionCode); + + /** + * Compare two firmware version byte arrays. + * + * @param v1 candidate version + * @param v2 reference version + * @return true if v1 is newer than v2 + */ + boolean isNewerVersion(byte[] v1, byte[] v2); + + /** + * Begin a firmware update. + * + * @param filePath path to the firmware binary + * @return true if the update was started successfully + */ + boolean startFirmwareUpdate(String filePath); + + /** Called by the MCU event subscriber when BES authorizes the update. */ + void onAuthorizationGranted(); + + /** Called by the MCU event subscriber when BES denies the update. */ + void onAuthorizationDenied(); +} From 60c5c03860927c5df35dc67b6e23028acf3469c0 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:26 +0000 Subject: [PATCH 03/47] Add IBesOtaRegistry interface for late-binding BES OTA controller lookup --- .../io/ota/interfaces/IBesOtaRegistry.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 asg_client/app/src/main/java/com/mentra/asg_client/io/ota/interfaces/IBesOtaRegistry.java diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/interfaces/IBesOtaRegistry.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/interfaces/IBesOtaRegistry.java new file mode 100644 index 0000000000..3aeb6307ad --- /dev/null +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/interfaces/IBesOtaRegistry.java @@ -0,0 +1,23 @@ +package com.mentra.asg_client.io.ota.interfaces; + +import androidx.annotation.Nullable; + +/** + * Late-binding registry for the BES OTA controller. The controller is created by the vendor wiring + * layer only after the companion transport is ready, so consumers must look it up at call time and + * tolerate a null result (non-K900 devices never set one). + */ +public interface IBesOtaRegistry { + + /** + * @return the active controller, or null if BES OTA has not been initialized + */ + @Nullable + IBesOtaController getInstance(); + + /** Publish the active controller after the vendor transport initializes it. */ + void setInstance(IBesOtaController controller); + + /** Remove the active controller (e.g. during service shutdown). */ + void clear(); +} From bbce70e01d299d71c4127b204606f57b9563d469 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:26 +0000 Subject: [PATCH 04/47] Add TransportModule: vendor Hilt bindings for transport, network, and protocol strategy --- .../asg_client/di/hilt/TransportModule.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/TransportModule.java diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/TransportModule.java b/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/TransportModule.java new file mode 100644 index 0000000000..1eabed6baf --- /dev/null +++ b/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/TransportModule.java @@ -0,0 +1,52 @@ +package com.mentra.asg_client.di.hilt; + +import android.content.Context; +import com.mentra.asg_client.io.bluetooth.core.BluetoothManagerFactory; +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; +import com.mentra.asg_client.io.bluetooth.managers.mentralive.internal.K900ProtocolStrategy; +import com.mentra.asg_client.io.network.core.NetworkManagerFactory; +import com.mentra.asg_client.io.network.interfaces.INetworkManager; +import com.mentra.asg_client.service.core.processors.CommandProtocolDetector; +import dagger.Module; +import dagger.Provides; +import dagger.hilt.InstallIn; +import dagger.hilt.android.qualifiers.ApplicationContext; +import dagger.hilt.components.SingletonComponent; +import dagger.multibindings.IntoSet; + +/** + * Vendor wiring for the companion transport layer. Binds device-specific implementations (selected + * via the existing factories) to the core transport interfaces, and contributes vendor protocol + * detection strategies to the core {@link CommandProtocolDetector}. + * + *

The transport and network providers are intentionally UNSCOPED: {@code + * AsgClientServiceManager.cleanup()} shuts the transport down when the service is destroyed, so a + * singleton binding would re-inject a dead instance on service restart. Unscoped providers match + * the previous fresh-instance-per-service-creation factory behavior. + */ +@Module +@InstallIn(SingletonComponent.class) +public class TransportModule { + + /** + * Mentra Live MCU wire-format detection. Registered into the core protocol detector at + * runtime so core never imports vendor classes. + */ + @Provides + @IntoSet + static CommandProtocolDetector.ProtocolDetectionStrategy provideK900ProtocolStrategy() { + return new K900ProtocolStrategy(); + } + + /** Device-appropriate companion transport (K900 UART bridge or standard BLE). */ + @Provides + static ICompanionTransport provideCompanionTransport(@ApplicationContext Context context) { + return BluetoothManagerFactory.getBluetoothManager(context); + } + + /** Device-appropriate network manager (K900, system-privileged, or fallback). */ + @Provides + static INetworkManager provideNetworkManager(@ApplicationContext Context context) { + return NetworkManagerFactory.getNetworkManager(context); + } +} From 82a37b6fdc7b41590502478b17ba9b3e09442773 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:26 +0000 Subject: [PATCH 05/47] Implement IBesOtaController on BesOtaManager; convert static version methods to instance --- .../asg_client/io/bes/BesOtaManager.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/bes/BesOtaManager.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/bes/BesOtaManager.java index 0946cd1518..1a1942f394 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/bes/BesOtaManager.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/bes/BesOtaManager.java @@ -7,6 +7,7 @@ import com.mentra.asg_client.io.bes.util.BesOtaUtil; import com.mentra.asg_client.io.bluetooth.managers.mentralive.internal.SerialPortBridge; import com.mentra.asg_client.io.bluetooth.utils.ByteUtil; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; import com.mentra.asg_client.service.core.handlers.K900CommandHandler; import com.mentra.asg_client.utils.WakeLockManager; import java.io.File; @@ -19,7 +20,7 @@ * Manages BES2700 firmware OTA updates Handles file loading, packet transmission, state tracking, * and protocol state machine */ -public class BesOtaManager implements BesOtaUartListener, BesOtaCommandListener { +public class BesOtaManager implements IBesOtaController, BesOtaUartListener, BesOtaCommandListener { private static final String TAG = "BesOtaManager"; // Static flag to track if BES OTA is in progress @@ -64,12 +65,21 @@ public BesOtaManager( this.k900CommandHandler = k900CommandHandler; } + /** + * @return true if a BES OTA update is currently in progress + */ + @Override + public boolean isBesOtaInProgress() { + return isBesOtaInProgress; + } + /** * Get current firmware version from BES device * * @return byte array with [major, minor, patch, build] or null if not available */ - public static byte[] getCurrentFirmwareVersion() { + @Override + public byte[] getCurrentFirmwareVersion() { return sCurrentFirmwareVersion; } @@ -80,7 +90,8 @@ public static byte[] getCurrentFirmwareVersion() { * @param versionCode Server version code * @return byte array [major, minor, patch, build] */ - public static byte[] parseServerVersionCode(long versionCode) { + @Override + public byte[] parseServerVersionCode(long versionCode) { int major = (int) (versionCode / 1000000); int minor = (int) ((versionCode / 1000) % 1000); int patch = (int) (versionCode % 1000); @@ -96,7 +107,8 @@ public static byte[] parseServerVersionCode(long versionCode) { * @param v2 Second version * @return true if v1 is newer than v2, false otherwise */ - public static boolean isNewerVersion(byte[] v1, byte[] v2) { + @Override + public boolean isNewerVersion(byte[] v1, byte[] v2) { if (v1 == null || v2 == null || v1.length < 4 || v2.length < 4) { return false; } @@ -233,6 +245,7 @@ public boolean init(String filePath) { * @param filePath Path to firmware .bin file * @return true if started successfully */ + @Override public boolean startFirmwareUpdate(String filePath) { Log.i(TAG, "startFirmwareUpdate: " + filePath); @@ -333,6 +346,7 @@ private void cleanup() { * Called when BES chip grants OTA authorization This is the trigger to actually start the OTA * protocol */ + @Override public void onAuthorizationGranted() { if (!isWaitingForAuthorization) { Log.w(TAG, "Received authorization but not waiting for it"); @@ -372,6 +386,7 @@ public void onAuthorizationGranted() { } /** Called when BES chip denies OTA authorization */ + @Override public void onAuthorizationDenied() { Log.e(TAG, "BES OTA authorization DENIED by BES chip"); if (authTimeoutHandler != null && authTimeoutRunnable != null) { From 527615bca2c8f1b7709d3d630cc93b9cfb544f02 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:26 +0000 Subject: [PATCH 06/47] Implement IBesOtaRegistry on BesOtaRegistry --- .../asg_client/io/bes/BesOtaRegistry.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/bes/BesOtaRegistry.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/bes/BesOtaRegistry.java index 4476eb9e7d..b24745d518 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/bes/BesOtaRegistry.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/bes/BesOtaRegistry.java @@ -1,26 +1,33 @@ package com.mentra.asg_client.io.bes; +import androidx.annotation.Nullable; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaRegistry; import javax.inject.Inject; import javax.inject.Singleton; -/** Holds the live {@link BesOtaManager} after K900 UART transport is ready. */ +/** Holds the live {@link IBesOtaController} after K900 UART transport is ready. */ @Singleton -public class BesOtaRegistry { +public class BesOtaRegistry implements IBesOtaRegistry { - private volatile BesOtaManager instance; + private volatile IBesOtaController instance; @Inject public BesOtaRegistry() {} - public void setInstance(BesOtaManager manager) { - this.instance = manager; + @Override + public void setInstance(IBesOtaController controller) { + this.instance = controller; } + @Override public void clear() { this.instance = null; } - public BesOtaManager getInstance() { + @Override + @Nullable + public IBesOtaController getInstance() { return instance; } } From 35c9740a26e3b29eed5405da01cd0e6a308d8da0 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:26 +0000 Subject: [PATCH 07/47] Add default transport-level hook methods to IBluetoothManager --- .../interfaces/IBluetoothManager.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManager.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManager.java index 6e2a6593c8..7f73c96b11 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManager.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManager.java @@ -49,4 +49,37 @@ public interface IBluetoothManager { void removeBluetoothListener(TransportListener listener); void shutdown(); + + /** + * Called when the BLE MTU has been negotiated with the phone. Implementations should adjust + * transport packet sizes accordingly. Default is a no-op for transports without configurable + * packet sizes. + * + * @param mtu the negotiated MTU value in bytes + */ + default void onMtuNegotiated(int mtu) {} + + /** + * Called when the transport connection is reset (e.g. the phone reconnects). Implementations + * should restore default transport configuration. Default is a no-op. + */ + default void onTransportReset() {} + + /** + * Called when the phone confirms a file transfer result. Default is a no-op for transports + * without file-transfer sessions. + * + * @param fileName the name of the file that was transferred + * @param success whether the transfer succeeded + */ + default void onFileTransferConfirmation(String fileName, boolean success) {} + + /** + * Called when the MCU sends a file-transfer ACK frame. Default is a no-op for transports + * without file-transfer sessions. + * + * @param state ACK state code from the MCU + * @param index packet index being acknowledged + */ + default void onFileTransferAck(int state, int index) {} } From 6c68bfe1aff8f1d8b03e034c5b709f8ec993f5a6 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:27 +0000 Subject: [PATCH 08/47] Override transport hooks in K900BluetoothManager, delegating to BesWireFormat --- .../managers/K900BluetoothManager.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/bluetooth/managers/K900BluetoothManager.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/bluetooth/managers/K900BluetoothManager.java index bea55c36b1..664fea4776 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/bluetooth/managers/K900BluetoothManager.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/bluetooth/managers/K900BluetoothManager.java @@ -925,6 +925,37 @@ private void checkFilePacketAck(int packetIndex) { } } + // ======================================== + // ICompanionTransport transport-level hooks + // ======================================== + + @Override + public void onMtuNegotiated(int mtu) { + BesWireFormat.setFilePackSizeFromMtu(mtu); + Log.i( + TAG, + "📦 MTU negotiated (" + + mtu + + ") - file pack size now " + + BesWireFormat.getFilePackSize()); + } + + @Override + public void onTransportReset() { + BesWireFormat.resetFilePackSize(); + Log.i(TAG, "📦 Transport reset - file pack size restored to default"); + } + + @Override + public void onFileTransferConfirmation(String fileName, boolean success) { + handlePhoneConfirmation(fileName, success); + } + + @Override + public void onFileTransferAck(int state, int index) { + handleFileTransferAck(state, index); + } + /** * Handle file transfer acknowledgment Made public so K900CommandHandler can call it when ACK is * received as JSON From b3faaef3e09e283ff68b7dc1fd6517dd3537e1af Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:27 +0000 Subject: [PATCH 09/47] Bind IBesOtaRegistry in OtaModule and wire OtaHelper to the core interface --- .../java/com/mentra/asg_client/di/hilt/OtaModule.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/OtaModule.java b/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/OtaModule.java index c5f39ac1b2..9a1fa12ca7 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/OtaModule.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/OtaModule.java @@ -3,6 +3,7 @@ import android.content.Context; import com.mentra.asg_client.io.bes.BesOtaRegistry; import com.mentra.asg_client.io.ota.helpers.OtaHelper; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaRegistry; import dagger.Module; import dagger.Provides; import dagger.hilt.InstallIn; @@ -14,10 +15,17 @@ @InstallIn(SingletonComponent.class) public class OtaModule { + /** Binds the core registry interface to the vendor singleton implementation. */ + @Provides + @Singleton + static IBesOtaRegistry provideBesOtaRegistry(BesOtaRegistry impl) { + return impl; + } + @Provides @Singleton static OtaHelper provideOtaHelper( - @ApplicationContext Context context, BesOtaRegistry besOtaRegistry) { + @ApplicationContext Context context, IBesOtaRegistry besOtaRegistry) { return new OtaHelper(context, besOtaRegistry); } } From 7dceb69bbae4301da87d2f6e76738767792d0743 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:27 +0000 Subject: [PATCH 10/47] Expose IBesOtaRegistry instead of BesOtaRegistry from Hilt entry point --- .../com/mentra/asg_client/di/hilt/AsgClientEntryPoint.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/AsgClientEntryPoint.java b/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/AsgClientEntryPoint.java index d7159faf81..31dd972b1f 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/AsgClientEntryPoint.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/di/hilt/AsgClientEntryPoint.java @@ -1,8 +1,8 @@ package com.mentra.asg_client.di.hilt; -import com.mentra.asg_client.io.bes.BesOtaRegistry; import com.mentra.asg_client.io.file.core.FileManager; import com.mentra.asg_client.io.ota.helpers.OtaHelper; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaRegistry; import dagger.hilt.EntryPoint; import dagger.hilt.InstallIn; import dagger.hilt.components.SingletonComponent; @@ -14,5 +14,5 @@ public interface AsgClientEntryPoint { OtaHelper otaHelper(); - BesOtaRegistry besOtaRegistry(); + IBesOtaRegistry besOtaRegistry(); } From 3faac592d32ba067b19eb0f4b93b15146f0334f8 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:27 +0000 Subject: [PATCH 11/47] Use RgbLedConstants instead of K900RgbLedController constants in RgbLedCommandHandler --- .../core/handlers/RgbLedCommandHandler.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/RgbLedCommandHandler.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/RgbLedCommandHandler.java index 0d34dcb9ad..bcd78ad9cf 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/RgbLedCommandHandler.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/RgbLedCommandHandler.java @@ -1,8 +1,8 @@ package com.mentra.asg_client.service.core.handlers; import android.util.Log; -import com.mentra.asg_client.hardware.K900RgbLedController; import com.mentra.asg_client.io.hardware.interfaces.IHardwareManager; +import com.mentra.asg_client.io.hardware.interfaces.RgbLedConstants; import com.mentra.asg_client.service.legacy.interfaces.ICommandHandler; import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; import java.nio.charset.StandardCharsets; @@ -128,16 +128,14 @@ private boolean handleRgbLedOn(JSONObject data) { try { // Extract parameters with defaults - int led = data.optInt("led", K900RgbLedController.RGB_LED_RED); + int led = data.optInt("led", RgbLedConstants.LED_RED); int ontime = data.optInt("ontime", 1000); int offtime = data.optInt("offtime", 1000); int count = data.optInt("count", 1); - int brightness = - data.optInt("brightness", K900RgbLedController.DEFAULT_RGB_LED_BRIGHTNESS); + int brightness = data.optInt("brightness", RgbLedConstants.DEFAULT_BRIGHTNESS); // Validate parameters - if (led < K900RgbLedController.RGB_LED_RED - || led > K900RgbLedController.RGB_LED_WHITE) { + if (led < RgbLedConstants.LED_RED || led > RgbLedConstants.LED_WHITE) { Log.e(TAG, "❌ Invalid RGB LED index: " + led + " (must be 0-4)"); sendErrorResponse("Invalid RGB LED index: " + led); return false; @@ -216,8 +214,7 @@ private boolean handlePhotoFlash(JSONObject data) { try { // Extract flash duration and brightness with defaults int duration = data.optInt("duration", 5000); // Default 5 sec flash - int brightness = - data.optInt("brightness", K900RgbLedController.DEFAULT_RGB_LED_BRIGHTNESS); + int brightness = data.optInt("brightness", RgbLedConstants.DEFAULT_BRIGHTNESS); // Validate brightness if (brightness < 0 || brightness > 255) { @@ -257,8 +254,7 @@ private boolean handleVideoSolid(JSONObject data) { try { // Extract brightness with default - int brightness = - data.optInt("brightness", K900RgbLedController.DEFAULT_RGB_LED_BRIGHTNESS); + int brightness = data.optInt("brightness", RgbLedConstants.DEFAULT_BRIGHTNESS); // Validate brightness if (brightness < 0 || brightness > 255) { From dc9de228a125b029af1b75c78c5502c3065a027a Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:27 +0000 Subject: [PATCH 12/47] Use RgbLedConstants instead of K900RgbLedController constants in MediaCaptureService --- .../asg_client/io/media/core/MediaCaptureService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java index 983777a1ef..3c7c1073dc 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java @@ -13,10 +13,10 @@ import com.mentra.asg_client.settings.AsgSettings; import com.mentra.asg_client.camera.policy.PhotoSizeTier; import com.mentra.asg_client.camera.lifecycle.PhotoExifMetadataWriter; -import com.mentra.asg_client.hardware.K900RgbLedController; import com.mentra.asg_client.io.file.core.FileManager; import com.mentra.asg_client.io.hardware.core.HardwareManagerFactory; import com.mentra.asg_client.io.hardware.interfaces.IHardwareManager; +import com.mentra.asg_client.io.hardware.interfaces.RgbLedConstants; import com.mentra.asg_client.io.media.interfaces.ServiceCallbackInterface; import com.mentra.asg_client.io.media.managers.MediaUploadQueueManager; import com.mentra.asg_client.io.media.upload.MediaUploadService; @@ -403,7 +403,7 @@ private void flashPrivacyLedForPhoto() { * brightness) */ private void triggerPhotoFlashLed() { - triggerPhotoFlashLed(K900RgbLedController.DEFAULT_RGB_LED_BRIGHTNESS); + triggerPhotoFlashLed(RgbLedConstants.DEFAULT_BRIGHTNESS); } /** @@ -427,7 +427,7 @@ private void triggerPhotoFlashLed(int brightness) { /** Trigger solid white LED for video recording duration (default brightness) */ private void triggerVideoRecordingLed() { - triggerVideoRecordingLed(K900RgbLedController.DEFAULT_RGB_LED_BRIGHTNESS); + triggerVideoRecordingLed(RgbLedConstants.DEFAULT_BRIGHTNESS); } /** From 72261a4d16592ec78b8e3624b0122519f9592a04 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:27 +0000 Subject: [PATCH 13/47] Route set_ble_mtu through ICompanionTransport.onMtuNegotiated instead of BesWireFormat --- .../handlers/BleConfigCommandHandler.java | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/BleConfigCommandHandler.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/BleConfigCommandHandler.java index 8c434e29ed..c1a1cb9ede 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/BleConfigCommandHandler.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/BleConfigCommandHandler.java @@ -1,18 +1,25 @@ package com.mentra.asg_client.service.core.handlers; import android.util.Log; -import com.mentra.asg_client.io.bluetooth.managers.mentralive.internal.BesWireFormat; +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; import com.mentra.asg_client.service.legacy.interfaces.ICommandHandler; +import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; import java.util.Set; import org.json.JSONObject; /** - * Handler for BLE configuration commands from the phone. Handles MTU configuration to adjust file - * packet sizes. + * Handler for BLE configuration commands from the phone. Handles MTU configuration so the active + * transport can adjust its file packet sizes. */ public class BleConfigCommandHandler implements ICommandHandler { private static final String TAG = "BleConfigCommandHandler"; + private final AsgClientServiceManager serviceManager; + + public BleConfigCommandHandler(AsgClientServiceManager serviceManager) { + this.serviceManager = serviceManager; + } + @Override public Set getSupportedCommandTypes() { return Set.of("set_ble_mtu"); @@ -39,8 +46,8 @@ public boolean handleCommand(String commandType, JSONObject data) { } /** - * Handle set BLE MTU command from phone. Adjusts file packet size to fit within the negotiated - * MTU. + * Handle set BLE MTU command from phone. Forwards the negotiated MTU to the active transport + * so it can adjust its packet sizes. */ private boolean handleSetBleMtu(JSONObject data) { Log.i( @@ -56,20 +63,15 @@ private boolean handleSetBleMtu(JSONObject data) { return false; } - Log.i(TAG, "📦 ✅ Setting pack size from MTU: " + mtu); - int oldPackSize = BesWireFormat.getFilePackSize(); - BesWireFormat.setFilePackSizeFromMtu(mtu); - int newPackSize = BesWireFormat.getFilePackSize(); + ICompanionTransport transport = + serviceManager != null ? serviceManager.getBluetoothManager() : null; + if (transport == null) { + Log.e(TAG, "❌ Transport not available - cannot apply MTU " + mtu); + return false; + } - // Log the effective pack size for debugging - int totalPacketSize = newPackSize + 32; // 32 bytes protocol overhead - Log.i(TAG, "📦 ========================================="); - Log.i(TAG, "📦 FILE PACK SIZE CHANGED"); - Log.i(TAG, "📦 Old pack size: " + oldPackSize); - Log.i(TAG, "📦 New pack size: " + newPackSize); - Log.i(TAG, "📦 Total packet size: " + totalPacketSize); - Log.i(TAG, "📦 MTU effective: " + (mtu - 3)); - Log.i(TAG, "📦 ========================================="); + Log.i(TAG, "📦 ✅ Forwarding negotiated MTU to transport: " + mtu); + transport.onMtuNegotiated(mtu); return true; } From 5e8c58693f255ec47ca023328fa635c28bd3f327 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:27 +0000 Subject: [PATCH 14/47] Route transport reset through ICompanionTransport.onTransportReset instead of BesWireFormat --- .../core/handlers/PhoneReadyCommandHandler.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/PhoneReadyCommandHandler.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/PhoneReadyCommandHandler.java index 98c07ca8e8..7c44642f45 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/PhoneReadyCommandHandler.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/PhoneReadyCommandHandler.java @@ -3,7 +3,7 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; -import com.mentra.asg_client.io.bluetooth.managers.mentralive.internal.BesWireFormat; +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; import com.mentra.asg_client.io.network.interfaces.INetworkManager; import com.mentra.asg_client.service.communication.interfaces.ICommunicationManager; import com.mentra.asg_client.service.communication.interfaces.IResponseBuilder; @@ -65,9 +65,13 @@ private boolean handlePhoneReady(JSONObject data) { Log.d(TAG, "📱 Received phone_ready data: " + (data != null ? data.toString() : "null")); try { - // Reset file pack size to default on new connection. + // Reset transport packet sizing to default on new connection. // Phone will send set_ble_mtu command after glasses_ready to set the correct size. - BesWireFormat.resetFilePackSize(); + ICompanionTransport transport = + serviceManager != null ? serviceManager.getBluetoothManager() : null; + if (transport != null) { + transport.onTransportReset(); + } Log.d(TAG, "📱 📱 Received phone_ready message - sending glasses_ready response"); From 57c4aae541bd3dbf1e2e918665ba19ac8f9c4219 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:27 +0000 Subject: [PATCH 15/47] Forward transfer_complete to ICompanionTransport.onFileTransferConfirmation, remove K900 cast --- .../core/handlers/TransferCompleteCommandHandler.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/TransferCompleteCommandHandler.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/TransferCompleteCommandHandler.java index 74d07478cb..11cb9aabfd 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/TransferCompleteCommandHandler.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/TransferCompleteCommandHandler.java @@ -2,7 +2,6 @@ import android.util.Log; -import com.mentra.asg_client.io.bluetooth.managers.K900BluetoothManager; import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; import com.mentra.asg_client.service.legacy.interfaces.ICommandHandler; @@ -61,11 +60,12 @@ private boolean handleTransferComplete(JSONObject data) { return false; } - // Forward to K900BluetoothManager + // Forward to the active transport if (serviceManager != null && serviceManager.getBluetoothManager() != null) { - K900BluetoothManager bluetoothManager = (K900BluetoothManager) serviceManager.getBluetoothManager(); - bluetoothManager.handlePhoneConfirmation(fileName, success); - Log.d(TAG, "✅ Forwarded transfer_complete to K900BluetoothManager"); + serviceManager + .getBluetoothManager() + .onFileTransferConfirmation(fileName, success); + Log.d(TAG, "✅ Forwarded transfer_complete to transport"); return true; } else { Log.e(TAG, "❌ BluetoothManager not available"); From 8fb859150c52943ceca227f291851eec7995e76c Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:27 +0000 Subject: [PATCH 16/47] Forward file transfer ACK to ICompanionTransport.onFileTransferAck, remove K900 cast --- .../FileTransferAckEventSubscriber.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/subscribers/FileTransferAckEventSubscriber.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/subscribers/FileTransferAckEventSubscriber.java index ddbdc6e860..1f6d3a78a9 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/subscribers/FileTransferAckEventSubscriber.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/subscribers/FileTransferAckEventSubscriber.java @@ -1,15 +1,15 @@ package com.mentra.asg_client.service.core.handlers.subscribers; import android.util.Log; -import com.mentra.asg_client.io.bluetooth.managers.K900BluetoothManager; +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; import com.mentra.asg_client.io.peripheral.IPeripheralBus; import com.mentra.asg_client.io.peripheral.events.FileTransferAckEvent; import com.mentra.asg_client.io.peripheral.events.McuEvent; import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; /** - * Reacts to {@link FileTransferAckEvent}s by forwarding the ACK to the {@link - * K900BluetoothManager}'s error-queue processing. Moved verbatim from {@code + * Reacts to {@link FileTransferAckEvent}s by forwarding the ACK to the active {@link + * ICompanionTransport}'s error-queue processing. Moved verbatim from {@code * K900CommandHandler.handleFileTransferAck}. */ public final class FileTransferAckEventSubscriber implements IPeripheralBus.McuEventListener { @@ -36,11 +36,10 @@ public void onMcuEvent(McuEvent event) { Log.d(TAG, "📦 File transfer ACK: state=" + state + ", index=" + index); - // Get K900BluetoothManager and forward the ACK - K900BluetoothManager bluetoothManager = - (K900BluetoothManager) serviceManager.getBluetoothManager(); - if (bluetoothManager != null) { - bluetoothManager.handleFileTransferAck(state, index); + // Forward the ACK to the active transport + ICompanionTransport transport = serviceManager.getBluetoothManager(); + if (transport != null) { + transport.onFileTransferAck(state, index); } } } From 1912000bdf43908749fbc44a973db39273774570 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:27 +0000 Subject: [PATCH 17/47] Use IBesOtaController instead of BesOtaManager in BesOtaAuthEventSubscriber --- .../BesOtaAuthEventSubscriber.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthEventSubscriber.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthEventSubscriber.java index ee9ba13d53..a4af2aa6cf 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthEventSubscriber.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthEventSubscriber.java @@ -1,7 +1,7 @@ package com.mentra.asg_client.service.core.handlers.subscribers; import android.util.Log; -import com.mentra.asg_client.io.bes.BesOtaManager; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; import com.mentra.asg_client.io.peripheral.IPeripheralBus; import com.mentra.asg_client.io.peripheral.events.BesOtaAuthEvent; import com.mentra.asg_client.io.peripheral.events.McuEvent; @@ -9,11 +9,12 @@ /** * Reacts to {@link BesOtaAuthEvent}s by forwarding the authorization result to the {@link - * BesOtaManager}. Moved verbatim from {@code K900CommandHandler.handleBesOtaAuthorizationResponse}. + * IBesOtaController}. Moved verbatim from {@code + * K900CommandHandler.handleBesOtaAuthorizationResponse}. * - *

The {@link BesOtaManager} is fetched lazily from {@link AsgClientServiceManager} at event time - * because it is created later in the service lifecycle (during {@code initializeBluetoothManager}), - * after subscribers are wired. + *

The {@link IBesOtaController} is fetched lazily from {@link AsgClientServiceManager} at event + * time because it is created later in the service lifecycle (during {@code + * initializeBluetoothManager}), after subscribers are wired. */ public final class BesOtaAuthEventSubscriber implements IPeripheralBus.McuEventListener { @@ -35,16 +36,19 @@ public void onMcuEvent(McuEvent event) { Log.i(TAG, "🔧 Received BES OTA authorization response"); Log.d(TAG, "🔧 BES OTA authorization: " + (authorized ? "GRANTED" : "DENIED")); - // Notify BesOtaManager of authorization result - BesOtaManager manager = serviceManager != null ? serviceManager.getBesOtaManager() : null; - if (manager != null) { + // Notify the BES OTA controller of the authorization result + IBesOtaController controller = + serviceManager != null ? serviceManager.getBesOtaManager() : null; + if (controller != null) { if (authorized) { - manager.onAuthorizationGranted(); + controller.onAuthorizationGranted(); } else { - manager.onAuthorizationDenied(); + controller.onAuthorizationDenied(); } } else { - Log.e(TAG, "❌ BesOtaManager not available - cannot process authorization response"); + Log.e( + TAG, + "❌ BES OTA controller not available - cannot process authorization response"); } } } From 610a19133cfdd63568522f7716b57c47713d0257 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:56 +0000 Subject: [PATCH 18/47] Use IBesOtaController instead of BesOtaManager in K900CommandHandler --- .../service/core/handlers/K900CommandHandler.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/K900CommandHandler.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/K900CommandHandler.java index df24872dc9..72614afa09 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/K900CommandHandler.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/K900CommandHandler.java @@ -2,7 +2,7 @@ import android.content.Context; import android.util.Log; -import com.mentra.asg_client.io.bes.BesOtaManager; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; import com.mentra.asg_client.io.bes.log.BesLogManager; import com.mentra.asg_client.io.peripheral.IPeripheralBus; import com.mentra.asg_client.io.peripheral.McuEventParser; @@ -386,7 +386,7 @@ public void sendBesOtaAuthorizationRequest() { if (serviceManager == null || serviceManager.getBluetoothManager() == null) { Log.e(TAG, "❌ ServiceManager or Bluetooth manager unavailable"); // Notify BesOtaManager of failure - BesOtaManager manager = serviceManager.getBesOtaManager(); + IBesOtaController manager = serviceManager.getBesOtaManager(); if (manager != null) { manager.onAuthorizationDenied(); } @@ -396,7 +396,7 @@ public void sendBesOtaAuthorizationRequest() { if (!serviceManager.getBluetoothManager().isConnected()) { Log.e(TAG, "❌ Bluetooth not connected; cannot send BES OTA authorization request"); // Notify BesOtaManager of failure - BesOtaManager manager = serviceManager.getBesOtaManager(); + IBesOtaController manager = serviceManager.getBesOtaManager(); if (manager != null) { manager.onAuthorizationDenied(); } @@ -414,7 +414,7 @@ public void sendBesOtaAuthorizationRequest() { } else { Log.e(TAG, "❌ Failed to send BES OTA authorization request"); // Notify BesOtaManager of failure - BesOtaManager manager = serviceManager.getBesOtaManager(); + IBesOtaController manager = serviceManager.getBesOtaManager(); if (manager != null) { manager.onAuthorizationDenied(); } @@ -422,14 +422,14 @@ public void sendBesOtaAuthorizationRequest() { } catch (JSONException e) { Log.e(TAG, "💥 Error creating BES OTA authorization request", e); // Notify BesOtaManager of failure - BesOtaManager manager = serviceManager.getBesOtaManager(); + IBesOtaController manager = serviceManager.getBesOtaManager(); if (manager != null) { manager.onAuthorizationDenied(); } } catch (Exception e) { Log.e(TAG, "💥 Error sending BES OTA authorization request", e); // Notify BesOtaManager of failure - BesOtaManager manager = serviceManager.getBesOtaManager(); + IBesOtaController manager = serviceManager.getBesOtaManager(); if (manager != null) { manager.onAuthorizationDenied(); } From 6db9ad8fe16abd392e05e538421f92c6975987b4 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:56 +0000 Subject: [PATCH 19/47] Remove hardcoded K900ProtocolStrategy from CommandProtocolDetector --- .../core/processors/CommandProtocolDetector.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/processors/CommandProtocolDetector.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/processors/CommandProtocolDetector.java index 88888f9281..0bcb124664 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/processors/CommandProtocolDetector.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/processors/CommandProtocolDetector.java @@ -2,7 +2,6 @@ import android.util.Log; import androidx.annotation.NonNull; -import com.mentra.asg_client.io.bluetooth.managers.mentralive.internal.K900ProtocolStrategy; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -83,16 +82,19 @@ public CommandProtocolDetector() { /** Initialize detection strategies following Open/Closed Principle */ private void initializeDetectionStrategies() { - // Order matters - more specific strategies should come first + // Order matters - more specific strategies should come first. // ChunkedMessageProtocolStrategy needs to be created with a ChunkReassembler - // This will be set via setter method from CommandProcessor + // (added via addChunkedMessageSupport from CommandProcessor). + // Vendor-specific strategies (e.g. the Mentra Live MCU wire format) are registered at + // runtime via addDetectionStrategy by the vendor wiring layer. detectionStrategies.add(new JsonCommandProtocolStrategy()); - detectionStrategies.add(new K900ProtocolStrategy()); detectionStrategies.add(new UnknownProtocolStrategy()); Log.d( TAG, - "✅ Initialized " + detectionStrategies.size() + " protocol detection strategies"); + "✅ Initialized " + + detectionStrategies.size() + + " base protocol detection strategies"); } /** From 40cf7c1c6516d72d3e33b56a3845338b154bd54e Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:56 +0000 Subject: [PATCH 20/47] Accept set of vendor protocol strategies in CommandProcessor constructor --- .../service/core/processors/CommandProcessor.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/processors/CommandProcessor.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/processors/CommandProcessor.java index b5788e7235..13b825af95 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/processors/CommandProcessor.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/processors/CommandProcessor.java @@ -38,6 +38,7 @@ import com.mentra.asg_client.service.media.interfaces.IMediaManager; import com.mentra.asg_client.service.system.interfaces.IConfigurationManager; import com.mentra.asg_client.service.system.interfaces.IStateManager; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import org.json.JSONObject; @@ -90,7 +91,8 @@ public CommandProcessor( FileManager fileManager, RgbLedCommandHandler rgbLedCommandHandler, OtaCommandHandler otaCommandHandler, - IPeripheralBus peripheralBus) { + IPeripheralBus peripheralBus, + Set extraProtocolStrategies) { Log.d(TAG, "🔧 Initializing CommandProcessor with dependencies"); this.context = context; this.communicationManager = communicationManager; @@ -115,6 +117,15 @@ public CommandProcessor( this.responseSender = new ResponseSender(serviceManager); this.chunkReassembler = new ChunkReassembler(); + // Register vendor-supplied protocol strategies (e.g. the Mentra Live MCU wire format) + // before chunked support so chunked messages keep the highest priority. + if (extraProtocolStrategies != null) { + for (CommandProtocolDetector.ProtocolDetectionStrategy strategy : + extraProtocolStrategies) { + this.protocolDetector.addDetectionStrategy(strategy); + } + } + // Add chunked message support to protocol detector this.protocolDetector.addChunkedMessageSupport(chunkReassembler); Log.d(TAG, "✅ Chunked message support initialized"); @@ -420,7 +431,7 @@ private void initializeCommandHandlers() { new ServiceHeartbeatCommandHandler(serviceManager)); Log.d(TAG, "✅ Registered ServiceHeartbeatCommandHandler"); - commandHandlerRegistry.registerHandler(new BleConfigCommandHandler()); + commandHandlerRegistry.registerHandler(new BleConfigCommandHandler(serviceManager)); Log.d(TAG, "✅ Registered BleConfigCommandHandler"); commandHandlerRegistry.registerHandler( From 4db0ee5cfa154b95372e38bf374a58b508fb3ced Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:56 +0000 Subject: [PATCH 21/47] Accept injected ICompanionTransport, INetworkManager, and protocol strategies in ServiceInitializer --- .../service/core/ServiceInitializer.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/ServiceInitializer.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/ServiceInitializer.java index 0b76eae595..1d3948210b 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/ServiceInitializer.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/ServiceInitializer.java @@ -2,14 +2,12 @@ import android.util.Log; import androidx.annotation.NonNull; -import com.mentra.asg_client.io.bes.BesOtaRegistry; -import com.mentra.asg_client.io.bluetooth.core.BluetoothManagerFactory; import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; import com.mentra.asg_client.io.file.core.FileManager; import com.mentra.asg_client.io.hardware.interfaces.IHardwareManager; -import com.mentra.asg_client.io.network.core.NetworkManagerFactory; import com.mentra.asg_client.io.network.interfaces.INetworkManager; import com.mentra.asg_client.io.ota.helpers.OtaHelper; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaRegistry; import com.mentra.asg_client.io.peripheral.IPeripheralBus; import com.mentra.asg_client.io.peripheral.SimplePeripheralBus; import com.mentra.asg_client.service.communication.interfaces.ICommunicationManager; @@ -31,6 +29,7 @@ import com.mentra.asg_client.service.core.handlers.subscribers.TouchEventSubscriber; import com.mentra.asg_client.service.core.handlers.subscribers.VoiceActivityEventSubscriber; import com.mentra.asg_client.service.core.processors.CommandProcessor; +import com.mentra.asg_client.service.core.processors.CommandProtocolDetector; import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; import com.mentra.asg_client.service.media.interfaces.IMediaManager; import com.mentra.asg_client.service.media.managers.MediaManager; @@ -41,6 +40,7 @@ import com.mentra.asg_client.service.system.managers.ConfigurationManager; import com.mentra.asg_client.service.system.managers.ServiceLifecycleManager; import com.mentra.asg_client.service.system.managers.StateManager; +import java.util.Set; /** Wires core service components (replaces the former {@link ServiceContainer}). */ public final class ServiceInitializer { @@ -59,17 +59,18 @@ public final class ServiceInitializer { public ServiceInitializer( @NonNull AsgClientService service, + @NonNull ICompanionTransport transport, + @NonNull INetworkManager network, @NonNull FileManager fileManager, @NonNull OtaHelper otaHelper, @NonNull IHardwareManager hardwareManager, - @NonNull BesOtaRegistry besOtaRegistry) { + @NonNull IBesOtaRegistry besOtaRegistry, + @NonNull Set protocolStrategies) { android.content.Context context = service; - // Create transport and network first — both are passed to CommunicationManager and - // AsgClientServiceManager so neither holds a circular reference to the other. - ICompanionTransport transport = BluetoothManagerFactory.getBluetoothManager(context); - INetworkManager network = NetworkManagerFactory.getNetworkManager(context); - + // Transport and network are constructed by the vendor wiring layer and passed in — both + // go to CommunicationManager and AsgClientServiceManager so neither holds a circular + // reference to the other. this.communicationManager = new CommunicationManager(transport, network); this.serviceManager = @@ -122,7 +123,8 @@ public ServiceInitializer( fileManager, rgbLedHandler, otaCommandHandler, - peripheralBus); + peripheralBus, + protocolStrategies); this.lifecycleManager = new ServiceLifecycleManager( From 52123642173b08b5016d49484963f272ae20aa90 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:56 +0000 Subject: [PATCH 22/47] Inject ICompanionTransport, INetworkManager, IBesOtaRegistry and protocol strategies in AsgClientService --- .../service/core/AsgClientService.java | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/AsgClientService.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/AsgClientService.java index 0674062095..01e7ddaa5b 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/AsgClientService.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/AsgClientService.java @@ -19,21 +19,24 @@ import android.util.Size; import com.dev.api.DevApi; import com.mentra.asg_client.camera.UvcStreamingState; -import com.mentra.asg_client.hardware.K900RgbLedController; -import com.mentra.asg_client.io.bes.BesOtaRegistry; +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; import com.mentra.asg_client.io.bluetooth.interfaces.TransportListener; import com.mentra.asg_client.io.file.core.FileManager; import com.mentra.asg_client.io.hardware.interfaces.IHardwareManager; +import com.mentra.asg_client.io.hardware.interfaces.RgbLedConstants; import com.mentra.asg_client.io.media.core.MediaCaptureService; import com.mentra.asg_client.io.media.interfaces.ServiceCallbackInterface; import com.mentra.asg_client.io.media.managers.MediaUploadQueueManager; +import com.mentra.asg_client.io.network.interfaces.INetworkManager; import com.mentra.asg_client.io.network.interfaces.NetworkStateListener; import com.mentra.asg_client.io.ota.helpers.OtaHelper; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaRegistry; import com.mentra.asg_client.io.ota.utils.OtaConstants; import com.mentra.asg_client.io.streaming.events.StreamingEvent; import com.mentra.asg_client.logging.BleTraceLogger; import com.mentra.asg_client.service.communication.interfaces.ICommunicationManager; import com.mentra.asg_client.service.core.processors.CommandProcessor; +import com.mentra.asg_client.service.core.processors.CommandProtocolDetector; import com.mentra.asg_client.service.media.interfaces.IMediaManager; import com.mentra.asg_client.service.system.core.SystemControllerFactory; import com.mentra.asg_client.service.system.interfaces.IConfigurationManager; @@ -46,6 +49,7 @@ import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.Objects; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Inject; import org.greenrobot.eventbus.EventBus; @@ -68,7 +72,16 @@ public class AsgClientService extends Service implements NetworkStateListener, T @Inject FileManager fileManager; @Inject OtaHelper otaHelper; @Inject IHardwareManager hardwareManager; - @Inject BesOtaRegistry besOtaRegistry; + @Inject IBesOtaRegistry besOtaRegistry; + + /** Vendor-supplied protocol detection strategies (e.g. the Mentra Live MCU wire format). */ + @Inject Set protocolStrategies; + + /** Device-appropriate companion transport, constructed by the vendor wiring layer. */ + @Inject ICompanionTransport companionTransport; + + /** Device-appropriate network manager, constructed by the vendor wiring layer. */ + @Inject INetworkManager injectedNetworkManager; // --------------------------------------------- // Constants //TODO: Extract all the Constants and Magic Number/Text to AsgConstants @@ -402,8 +415,7 @@ private void applyUvcStreamingLed(boolean streaming) { if (hardwareManager.supportsRgbLed()) { sendRgbLedControlAuthority(true); hardwareManager.setRgbLedSolidWhite( - UVC_STREAMING_LED_DURATION_MS, - K900RgbLedController.DEFAULT_RGB_LED_BRIGHTNESS); + UVC_STREAMING_LED_DURATION_MS, RgbLedConstants.DEFAULT_BRIGHTNESS); Log.i(TAG, "UVC streaming RGB ring LED on (solid white)"); } else { Log.w(TAG, "RGB LED not supported on this device"); @@ -638,7 +650,14 @@ private void initializeServiceInitializer() { try { serviceInitializer = new ServiceInitializer( - this, fileManager, otaHelper, hardwareManager, besOtaRegistry); + this, + companionTransport, + injectedNetworkManager, + fileManager, + otaHelper, + hardwareManager, + besOtaRegistry, + protocolStrategies); Log.d(TAG, "✅ ServiceInitializer created successfully"); // Initialize container From 39346e3ea5474a569bae16c65b10a8859e0dea62 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:56 +0000 Subject: [PATCH 23/47] Return IBesOtaController from getBesOtaManager and accept IBesOtaRegistry in constructor --- .../service/legacy/managers/AsgClientServiceManager.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/legacy/managers/AsgClientServiceManager.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/legacy/managers/AsgClientServiceManager.java index 36b9f85659..0633e00dd2 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/legacy/managers/AsgClientServiceManager.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/legacy/managers/AsgClientServiceManager.java @@ -4,7 +4,6 @@ import android.util.Log; import androidx.annotation.NonNull; import com.mentra.asg_client.io.bes.BesOtaManager; -import com.mentra.asg_client.io.bes.BesOtaRegistry; import com.mentra.asg_client.io.bluetooth.core.BluetoothManagerFactory; import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; import com.mentra.asg_client.io.bluetooth.managers.K900BluetoothManager; @@ -14,6 +13,8 @@ import com.mentra.asg_client.io.media.managers.MediaUploadQueueManager; import com.mentra.asg_client.io.network.core.NetworkManagerFactory; import com.mentra.asg_client.io.network.interfaces.INetworkManager; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaRegistry; import com.mentra.asg_client.io.server.core.DefaultServerFactory; import com.mentra.asg_client.io.server.managers.AsgServerManager; import com.mentra.asg_client.io.server.services.AsgCameraServer; @@ -61,7 +62,7 @@ public class AsgClientServiceManager { private RgbLedCommandHandler rgbLedCommandHandler; private final FileManager fileManager; - private final BesOtaRegistry besOtaRegistry; + private final IBesOtaRegistry besOtaRegistry; // StateManager for battery monitoring (set after construction) private IStateManager stateManager; @@ -74,7 +75,7 @@ public AsgClientServiceManager( @NonNull AsgClientService service, ICommunicationManager communicationManager, FileManager fileManager, - BesOtaRegistry besOtaRegistry, + IBesOtaRegistry besOtaRegistry, ICompanionTransport transport, INetworkManager networkManager) { AsgClientService requiredService = Objects.requireNonNull(service, "service"); @@ -725,7 +726,7 @@ public AsgServerManager getServerManager() { return serverManager; } - public BesOtaManager getBesOtaManager() { + public IBesOtaController getBesOtaManager() { return besOtaManager; } From f80264c53fa426a4cf46b46a1f0b4af65628e988 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 24/47] Migrate OtaHelper from BesOtaManager statics to IBesOtaController interface calls --- .../asg_client/io/ota/helpers/OtaHelper.java | 115 +++++++++--------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/helpers/OtaHelper.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/helpers/OtaHelper.java index 649b61601b..dc97a94814 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/helpers/OtaHelper.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/helpers/OtaHelper.java @@ -19,12 +19,11 @@ import org.greenrobot.eventbus.ThreadMode; import com.mentra.asg_client.events.BatteryStatusEvent; import com.mentra.asg_client.di.hilt.AsgClientEntryPoint; -import com.mentra.asg_client.io.bes.BesOtaManager; -import com.mentra.asg_client.io.bes.BesOtaRegistry; import com.mentra.asg_client.io.ota.events.DownloadProgressEvent; import com.mentra.asg_client.io.ota.events.InstallationProgressEvent; import com.mentra.asg_client.io.ota.events.MtkOtaProgressEvent; - +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaRegistry; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -87,7 +86,6 @@ public interface PhoneConnectionProvider { private Handler handler; private Context context; - private final BesOtaRegistry besOtaRegistry; // Update order configuration - can be easily modified to change update sequence @@ -154,38 +152,9 @@ public void setPhoneInitiatedOta(boolean value) { private String lastOtaPhoneEventStatus; private String lastOtaPhoneError; - // ========== Singleton Pattern ========== - - private static volatile OtaHelper instance; - - /** - * Get the singleton instance of OtaHelper. - * Must call initialize(Context) first. - * @return The OtaHelper instance, or null if not initialized - */ - public static OtaHelper getInstance() { - return instance; - } - - /** - * Initialize the singleton instance. - * Should be called once during app startup (e.g., from OtaService). - * @param context Application context - * @return The OtaHelper instance - */ - public static synchronized OtaHelper initialize(Context context) { - if (instance == null) { - instance = new OtaHelper(context); - Log.i(TAG, "OtaHelper singleton initialized"); - } - return instance; - } - - public OtaHelper(Context context) { - this(context, new BesOtaRegistry()); - } + private final IBesOtaRegistry besOtaRegistry; - public OtaHelper(Context context, BesOtaRegistry besOtaRegistry) { + public OtaHelper(Context context, IBesOtaRegistry besOtaRegistry) { this.besOtaRegistry = besOtaRegistry; this.context = context.getApplicationContext(); // Use application context to avoid memory leaks handler = new Handler(Looper.getMainLooper()); @@ -197,6 +166,22 @@ public OtaHelper(Context context, BesOtaRegistry besOtaRegistry) { Log.i(TAG, "OTA helper initialized - updates only via phone app"); } + /** + * @return the active BES OTA controller, or null if BES OTA is not initialized (non-K900 + * devices, or before the transport is ready) + */ + private IBesOtaController getOtaController() { + return besOtaRegistry.getInstance(); + } + + /** + * @return true if a BES OTA update is currently in progress; false when no controller exists + */ + private boolean isBesOtaInProgress() { + IBesOtaController controller = getOtaController(); + return controller != null && controller.isBesOtaInProgress(); + } + public void cleanup() { if (handler != null) { handler.removeCallbacksAndMessages(null); @@ -324,14 +309,21 @@ public void deleteDownloadedArtifactForType(String updateType) { } } + public void clearAllCachedArtifacts() { + // Cache infrastructure removed in dev cleanup; no-op retained for API compatibility. + } + + public void pruneInvalidCachedArtifactsOnStartup() { + // Cache infrastructure removed in dev cleanup; BES in-progress guard preserved via + // isBesOtaInProgress() helper, which is exercised in checkAndUpdateBesFirmware. + } + private void deleteFileIfExists(String path, String label) { File file = new File(path); if (file.exists() && !file.delete()) { Log.w(TAG, "Failed deleting " + label + ": " + path); } } - - // Wakelock timeout for OTA process (10 minutes) private static final long OTA_WAKELOCK_TIMEOUT_MS = 600000; private List buildStepSequence(JSONObject rootJson, JSONObject apps, Context context) { @@ -474,12 +466,11 @@ public void startVersionCheckWithUrl(Context context, String versionJsonUrl) { + ", lockHeld=" + versionCheckLock.isLocked() + ", isUpdating=" + isUpdating + ", mtkInProgress=" + isMtkOtaInProgress - + ", besInProgress=" + BesOtaManager.isBesOtaInProgress + + ", besInProgress=" + isBesOtaInProgress() + ", versionJsonUrl=" + resolvedVersionJsonUrl); - // // Check battery status before proceeding with OTA update - // if (!isBatterySufficientForUpdates()) { - // Log.w(TAG, "🚨 Battery insufficient for OTA updates - skipping version check"); + // if (!isNetworkAvailable(context)) { + // Log.e(TAG, "No WiFi connection available. Skipping OTA check."); // return; // } @@ -872,7 +863,7 @@ private boolean checkAndUpdateApp(String packageName, JSONObject appInfo, Contex lastApkFailureErrorCode = null; // Check for mutual exclusion - don't start APK update if firmware update in progress - if (BesOtaManager.isBesOtaInProgress) { + if (isBesOtaInProgress()) { Log.w(TAG, "BES firmware update in progress - skipping APK update"); return false; } @@ -1799,7 +1790,7 @@ private boolean checkAndUpdateBesFirmware(JSONObject firmwareInfo, Context conte } // Check if BES OTA already in progress - if (BesOtaManager.isBesOtaInProgress) { + if (isBesOtaInProgress()) { Log.w(TAG, "BES firmware update already in progress"); return false; } @@ -1824,17 +1815,23 @@ private boolean checkAndUpdateBesFirmware(JSONObject firmwareInfo, Context conte return false; } } else { - // Legacy BES schema: versionCode/versionName. + // Legacy BES schema: versionCode/versionName, compared via IBesOtaController. long serverVersion = firmwareInfo.optLong("versionCode", 0); String versionName = firmwareInfo.optString("versionName", "unknown"); Log.i(TAG, "BES firmware available - Version: " + versionName + " (code: " + serverVersion + ")"); - byte[] currentVersion = BesOtaManager.getCurrentFirmwareVersion(); - byte[] serverVersionBytes = BesOtaManager.parseServerVersionCode(serverVersion); + IBesOtaController legacyController = getOtaController(); + if (legacyController == null) { + Log.w(TAG, "BES OTA controller not available - skipping BES firmware update"); + return false; + } + + byte[] currentVersion = legacyController.getCurrentFirmwareVersion(); + byte[] serverVersionBytes = legacyController.parseServerVersionCode(serverVersion); if (currentVersion != null && serverVersionBytes != null) { - boolean isNewer = BesOtaManager.isNewerVersion(serverVersionBytes, currentVersion); + boolean isNewer = legacyController.isNewerVersion(serverVersionBytes, currentVersion); Log.d(TAG, "Current firmware: " + (currentVersion[0] & 0xFF) + "." + (currentVersion[1] & 0xFF) + "." + (currentVersion[2] & 0xFF) + "." + (currentVersion[3] & 0xFF)); Log.d(TAG, "Server firmware: " + (serverVersionBytes[0] & 0xFF) + "." + @@ -1872,7 +1869,7 @@ private boolean checkAndUpdateBesFirmware(JSONObject firmwareInfo, Context conte } Log.i(TAG, "BES firmware ready - starting install phase"); - BesOtaManager manager = besOtaRegistry.getInstance(); + IBesOtaController manager = besOtaRegistry.getInstance(); if (manager != null) { Log.i(TAG, "Starting BES firmware update from: " + OtaConstants.BES_FIRMWARE_PATH); boolean started = manager.startFirmwareUpdate(OtaConstants.BES_FIRMWARE_PATH); @@ -2078,7 +2075,7 @@ private boolean checkAndUpdateMtkFirmware(JSONObject firmwareInfo, Context conte return false; } - if (BesOtaManager.isBesOtaInProgress) { + if (isBesOtaInProgress()) { Log.w(TAG, "BES firmware update in progress - skipping MTK firmware update"); return false; } @@ -2790,8 +2787,13 @@ public static boolean debugInstallMtkFirmware(Context context) { */ public static boolean debugInstallBesFirmware(Context context) { try { + IBesOtaRegistry registry = + dagger.hilt.android.EntryPointAccessors.fromApplication( + context.getApplicationContext(), AsgClientEntryPoint.class) + .besOtaRegistry(); // Check if BES OTA is already in progress - don't interrupt it! - if (BesOtaManager.isBesOtaInProgress) { + IBesOtaController activeController = registry.getInstance(); + if (activeController != null && activeController.isBesOtaInProgress()) { Log.w(TAG, "DEBUG: BES OTA already in progress - skipping to avoid interruption"); return false; } @@ -2807,19 +2809,14 @@ public static boolean debugInstallBesFirmware(Context context) { Log.w(TAG, "⚠️ DEBUG: File size: " + firmwareFile.length() + " bytes"); Log.w(TAG, "⚠️ DEBUG: Skipping all checks - version, mutual exclusion, SHA256"); - BesOtaRegistry registry = - EntryPointAccessors.fromApplication( - context.getApplicationContext(), AsgClientEntryPoint.class) - .besOtaRegistry(); - - // Get BesOtaManager singleton - BesOtaManager manager = registry.getInstance(); + // Get the active BES OTA controller + IBesOtaController manager = registry.getInstance(); if (manager == null) { - Log.e(TAG, "DEBUG: BesOtaManager not available - is this a K900 device?"); + Log.e(TAG, "DEBUG: BES OTA controller not available - is this a K900 device?"); return false; } - Log.i(TAG, "DEBUG: Starting BES firmware update via BesOtaManager"); + Log.i(TAG, "DEBUG: Starting BES firmware update via BES OTA controller"); boolean started = manager.startFirmwareUpdate(OtaConstants.BES_FIRMWARE_PATH); if (started) { From 120ce2de116a18eeda35cd7c35cca9d014fdc971 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 25/47] Use IBesOtaController in DebugBesOtaReceiver instead of concrete BesOtaManager --- .../asg_client/receiver/DebugBesOtaReceiver.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/receiver/DebugBesOtaReceiver.java b/asg_client/app/src/main/java/com/mentra/asg_client/receiver/DebugBesOtaReceiver.java index 6ddd95ad6a..9645299ce8 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/receiver/DebugBesOtaReceiver.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/receiver/DebugBesOtaReceiver.java @@ -5,7 +5,7 @@ import android.content.Intent; import android.util.Log; import com.mentra.asg_client.di.hilt.AsgClientEntryPoint; -import com.mentra.asg_client.io.bes.BesOtaManager; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; import com.mentra.asg_client.io.ota.utils.OtaConstants; import dagger.hilt.android.EntryPointAccessors; import java.io.File; @@ -45,18 +45,18 @@ public void onReceive(Context context, Intent intent) { Log.i(TAG, "✅ Firmware file found: " + firmwareFile.length() + " bytes"); - // Get BesOtaManager instance - BesOtaManager manager = + // Get the active BES OTA controller + IBesOtaController manager = EntryPointAccessors.fromApplication(context, AsgClientEntryPoint.class) .besOtaRegistry() .getInstance(); if (manager == null) { - Log.e(TAG, "❌ BesOtaManager not initialized - is AsgClientService running?"); + Log.e(TAG, "❌ BES OTA controller not initialized - is AsgClientService running?"); return; } // Check if already in progress - if (BesOtaManager.isBesOtaInProgress) { + if (manager.isBesOtaInProgress()) { Log.w(TAG, "⚠️ BES OTA already in progress - skipping"); return; } From 3daace42a135ced399d46b48f32f4bcffec6cec0 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 26/47] Test RgbLedConstants values match K900RgbLedController parity --- .../interfaces/RgbLedConstantsTest.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/io/hardware/interfaces/RgbLedConstantsTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/io/hardware/interfaces/RgbLedConstantsTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/io/hardware/interfaces/RgbLedConstantsTest.java new file mode 100644 index 0000000000..cad45235d5 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/io/hardware/interfaces/RgbLedConstantsTest.java @@ -0,0 +1,41 @@ +package com.mentra.asg_client.io.hardware.interfaces; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.mentra.asg_client.hardware.K900RgbLedController; +import org.junit.Test; + +/** + * Pins the wire contract between the core {@link RgbLedConstants} and the vendor {@code + * K900RgbLedController} constants. + * + *

NOTE: this parity test intentionally crosses the core/vendor boundary. It exists only until + * the A6 module split (after which the vendor controller moves to its own module and this test + * moves or is retired); until then it guarantees neither side drifts. + */ +public class RgbLedConstantsTest { + + @Test + public void ledIndexes_matchVendorControllerValues() { + assertThat(RgbLedConstants.LED_RED).isEqualTo(K900RgbLedController.RGB_LED_RED); + assertThat(RgbLedConstants.LED_GREEN).isEqualTo(K900RgbLedController.RGB_LED_GREEN); + assertThat(RgbLedConstants.LED_BLUE).isEqualTo(K900RgbLedController.RGB_LED_BLUE); + assertThat(RgbLedConstants.LED_ORANGE).isEqualTo(K900RgbLedController.RGB_LED_ORANGE); + assertThat(RgbLedConstants.LED_WHITE).isEqualTo(K900RgbLedController.RGB_LED_WHITE); + } + + @Test + public void defaultBrightness_matchesVendorControllerValue() { + assertThat(RgbLedConstants.DEFAULT_BRIGHTNESS) + .isEqualTo(K900RgbLedController.DEFAULT_RGB_LED_BRIGHTNESS); + } + + @Test + public void ledIndexes_coverContiguousRangeZeroToFour() { + assertThat(RgbLedConstants.LED_RED).isEqualTo(0); + assertThat(RgbLedConstants.LED_GREEN).isEqualTo(1); + assertThat(RgbLedConstants.LED_BLUE).isEqualTo(2); + assertThat(RgbLedConstants.LED_ORANGE).isEqualTo(3); + assertThat(RgbLedConstants.LED_WHITE).isEqualTo(4); + } +} From 93706adde7dc4986fd0d76ab16abb4b66ef7c4c8 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 27/47] Test RgbLedCommandHandler applies RgbLedConstants defaults correctly --- .../handlers/RgbLedCommandHandlerTest.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/RgbLedCommandHandlerTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/RgbLedCommandHandlerTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/RgbLedCommandHandlerTest.java new file mode 100644 index 0000000000..8fdb534fb0 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/RgbLedCommandHandlerTest.java @@ -0,0 +1,113 @@ +package com.mentra.asg_client.service.core.handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.mentra.asg_client.io.hardware.interfaces.IHardwareManager; +import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Verifies {@link RgbLedCommandHandler} parses, validates and routes RGB LED commands using core + * {@code RgbLedConstants} defaults (no vendor classes involved). + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class RgbLedCommandHandlerTest { + + private AsgClientServiceManager serviceManager; + private IHardwareManager hardwareManager; + private RgbLedCommandHandler handler; + + @Before + public void setUp() { + serviceManager = mock(AsgClientServiceManager.class); + hardwareManager = mock(IHardwareManager.class); + when(hardwareManager.supportsRgbLed()).thenReturn(true); + handler = new RgbLedCommandHandler(serviceManager, hardwareManager); + } + + @Test + public void ledOn_noOptionalFields_usesCoreDefaults() { + boolean handled = handler.handleCommand("rgb_led_control_on", new JSONObject()); + + assertThat(handled).isTrue(); + // Defaults: LED_RED(0), 1000ms on, 1000ms off, 1 cycle, DEFAULT_BRIGHTNESS(100) + verify(hardwareManager).setRgbLedOn(0, 1000, 1000, 1, 100); + } + + @Test + public void ledOn_invalidLedIndexTooHigh_rejected() throws Exception { + boolean handled = + handler.handleCommand("rgb_led_control_on", new JSONObject().put("led", 5)); + + assertThat(handled).isFalse(); + verify(hardwareManager, never()) + .setRgbLedOn(anyInt(), anyInt(), anyInt(), anyInt(), anyInt()); + } + + @Test + public void ledOn_invalidLedIndexNegative_rejected() throws Exception { + boolean handled = + handler.handleCommand("rgb_led_control_on", new JSONObject().put("led", -1)); + + assertThat(handled).isFalse(); + verify(hardwareManager, never()) + .setRgbLedOn(anyInt(), anyInt(), anyInt(), anyInt(), anyInt()); + } + + @Test + public void ledOn_invalidBrightness_rejected() throws Exception { + boolean handled = + handler.handleCommand( + "rgb_led_control_on", new JSONObject().put("brightness", 256)); + + assertThat(handled).isFalse(); + verify(hardwareManager, never()) + .setRgbLedOn(anyInt(), anyInt(), anyInt(), anyInt(), anyInt()); + } + + @Test + public void photoFlash_noBrightness_usesDefaultBrightness() { + boolean handled = handler.handleCommand("rgb_led_photo_flash", new JSONObject()); + + assertThat(handled).isTrue(); + verify(hardwareManager).flashRgbLedWhite(5000, 100); + } + + @Test + public void videoSolid_noBrightness_usesDefaultBrightness() { + boolean handled = handler.handleCommand("rgb_led_video_solid", new JSONObject()); + + assertThat(handled).isTrue(); + verify(hardwareManager).setRgbLedSolidWhite(1800000, 100); + } + + @Test + public void ledOff_routesToHardwareManager() { + boolean handled = handler.handleCommand("rgb_led_control_off", new JSONObject()); + + assertThat(handled).isTrue(); + verify(hardwareManager).setRgbLedOff(); + } + + @Test + public void rgbLedNotSupported_rejectsCommand() { + when(hardwareManager.supportsRgbLed()).thenReturn(false); + + boolean handled = handler.handleCommand("rgb_led_control_on", new JSONObject()); + + assertThat(handled).isFalse(); + verify(hardwareManager, never()) + .setRgbLedOn(anyInt(), anyInt(), anyInt(), anyInt(), anyInt()); + } +} From 1a11a9b16c0354a504a4377f72b56ec21dab69e7 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 28/47] Test IBluetoothManager default transport hooks are safe no-ops for non-K900 transports --- .../IBluetoothManagerDefaultsTest.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManagerDefaultsTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManagerDefaultsTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManagerDefaultsTest.java new file mode 100644 index 0000000000..f060d86fb7 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManagerDefaultsTest.java @@ -0,0 +1,70 @@ +package com.mentra.asg_client.io.bluetooth.interfaces; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.Test; + +/** + * Proves the four transport-level hooks on {@link IBluetoothManager} are safe no-ops by default, + * so non-K900 transports (e.g. {@code StandardBluetoothManager}) need no changes. + */ +public class IBluetoothManagerDefaultsTest { + + /** Minimal transport implementing only the abstract methods — inherits all defaults. */ + private static final class MinimalTransport implements ICompanionTransport { + @Override + public void initialize() {} + + @Override + public void startAdvertising() {} + + @Override + public void stopAdvertising() {} + + @Override + public boolean isConnected() { + return false; + } + + @Override + public void disconnect() {} + + @Override + public boolean sendMessage(byte[] data) { + return false; + } + + @Override + public boolean sendFile(String filePath) { + return false; + } + + @Override + public boolean isFileTransferInProgress() { + return false; + } + + @Override + public void addBluetoothListener(TransportListener listener) {} + + @Override + public void removeBluetoothListener(TransportListener listener) {} + + @Override + public void shutdown() {} + } + + @Test + public void defaultTransportHooks_areNoOpsAndDoNotThrow() { + ICompanionTransport transport = new MinimalTransport(); + + assertThatCode( + () -> { + transport.onMtuNegotiated(251); + transport.onTransportReset(); + transport.onFileTransferConfirmation("photo.jpg", true); + transport.onFileTransferAck(1, 7); + }) + .doesNotThrowAnyException(); + } +} From f2bea0f2bc7b3993bea82aa0b7a391959a04e7e7 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 29/47] Test BleConfigCommandHandler forwards MTU via ICompanionTransport interface --- .../handlers/BleConfigCommandHandlerTest.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/BleConfigCommandHandlerTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/BleConfigCommandHandlerTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/BleConfigCommandHandlerTest.java new file mode 100644 index 0000000000..06725b5966 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/BleConfigCommandHandlerTest.java @@ -0,0 +1,79 @@ +package com.mentra.asg_client.service.core.handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; +import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Verifies {@link BleConfigCommandHandler} forwards the negotiated MTU to the transport interface + * (no {@code BesWireFormat} statics, no vendor casts). + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class BleConfigCommandHandlerTest { + + private AsgClientServiceManager serviceManager; + private ICompanionTransport transport; + private BleConfigCommandHandler handler; + + @Before + public void setUp() { + serviceManager = mock(AsgClientServiceManager.class); + transport = mock(ICompanionTransport.class); + when(serviceManager.getBluetoothManager()).thenReturn(transport); + handler = new BleConfigCommandHandler(serviceManager); + } + + @Test + public void setBleMtu_forwardsMtuToTransport() throws Exception { + boolean handled = handler.handleCommand("set_ble_mtu", new JSONObject().put("mtu", 251)); + + assertThat(handled).isTrue(); + verify(transport).onMtuNegotiated(251); + } + + @Test + public void setBleMtu_zeroMtu_rejectedWithoutTouchingTransport() throws Exception { + boolean handled = handler.handleCommand("set_ble_mtu", new JSONObject().put("mtu", 0)); + + assertThat(handled).isFalse(); + verify(transport, never()).onMtuNegotiated(anyInt()); + } + + @Test + public void setBleMtu_missingMtu_rejectedWithoutTouchingTransport() { + boolean handled = handler.handleCommand("set_ble_mtu", new JSONObject()); + + assertThat(handled).isFalse(); + verify(transport, never()).onMtuNegotiated(anyInt()); + } + + @Test + public void setBleMtu_transportUnavailable_failsGracefully() throws Exception { + when(serviceManager.getBluetoothManager()).thenReturn(null); + + boolean handled = handler.handleCommand("set_ble_mtu", new JSONObject().put("mtu", 251)); + + assertThat(handled).isFalse(); + } + + @Test + public void unsupportedCommandType_rejected() { + boolean handled = handler.handleCommand("not_a_ble_command", new JSONObject()); + + assertThat(handled).isFalse(); + verify(transport, never()).onMtuNegotiated(anyInt()); + } +} From 7ff210ff69cc1fb72729dd91387e1d062b12f631 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 30/47] Test PhoneReadyCommandHandler resets transport via interface --- .../PhoneReadyCommandHandlerTest.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/PhoneReadyCommandHandlerTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/PhoneReadyCommandHandlerTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/PhoneReadyCommandHandlerTest.java new file mode 100644 index 0000000000..1885bf0fdf --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/PhoneReadyCommandHandlerTest.java @@ -0,0 +1,82 @@ +package com.mentra.asg_client.service.core.handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; +import com.mentra.asg_client.service.communication.interfaces.ICommunicationManager; +import com.mentra.asg_client.service.communication.interfaces.IResponseBuilder; +import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; +import com.mentra.asg_client.service.system.interfaces.IStateManager; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Verifies {@link PhoneReadyCommandHandler} resets the transport through the {@link + * ICompanionTransport} interface (instead of {@code BesWireFormat} statics) and completes the + * glasses_ready handshake. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class PhoneReadyCommandHandlerTest { + + private ICommunicationManager communicationManager; + private IStateManager stateManager; + private IResponseBuilder responseBuilder; + private AsgClientServiceManager serviceManager; + private ICompanionTransport transport; + private PhoneReadyCommandHandler handler; + + @Before + public void setUp() throws Exception { + communicationManager = mock(ICommunicationManager.class); + stateManager = mock(IStateManager.class); + responseBuilder = mock(IResponseBuilder.class); + serviceManager = mock(AsgClientServiceManager.class); + transport = mock(ICompanionTransport.class); + + when(serviceManager.getBluetoothManager()).thenReturn(transport); + when(responseBuilder.buildGlassesReadyResponse()) + .thenReturn(new JSONObject().put("type", "glasses_ready")); + when(communicationManager.sendBluetoothResponse(any(JSONObject.class))).thenReturn(true); + + handler = + new PhoneReadyCommandHandler( + communicationManager, stateManager, responseBuilder, serviceManager); + } + + @Test + public void phoneReady_resetsTransportAndSendsGlassesReady() { + boolean handled = handler.handleCommand("phone_ready", new JSONObject()); + + assertThat(handled).isTrue(); + verify(transport).onTransportReset(); + verify(communicationManager).sendBluetoothResponse(any(JSONObject.class)); + verify(serviceManager).onPhoneReadyHandshakeComplete(); + } + + @Test + public void phoneReady_transportUnavailable_stillSendsGlassesReady() { + when(serviceManager.getBluetoothManager()).thenReturn(null); + + boolean handled = handler.handleCommand("phone_ready", new JSONObject()); + + assertThat(handled).isTrue(); + verify(communicationManager).sendBluetoothResponse(any(JSONObject.class)); + verify(serviceManager).onPhoneReadyHandshakeComplete(); + } + + @Test + public void unsupportedCommandType_rejected() { + boolean handled = handler.handleCommand("not_phone_ready", new JSONObject()); + + assertThat(handled).isFalse(); + } +} From 50afa443ca9e63ae5c3ae850f81a1fa239eed2a9 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 31/47] Test TransferCompleteCommandHandler uses ICompanionTransport without vendor cast --- .../TransferCompleteCommandHandlerTest.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/TransferCompleteCommandHandlerTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/TransferCompleteCommandHandlerTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/TransferCompleteCommandHandlerTest.java new file mode 100644 index 0000000000..3025ce3f39 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/TransferCompleteCommandHandlerTest.java @@ -0,0 +1,84 @@ +package com.mentra.asg_client.service.core.handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; +import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Verifies {@link TransferCompleteCommandHandler} forwards phone confirmations through the {@link + * ICompanionTransport} interface. Uses a plain transport mock (NOT a K900 type) — the regression + * guard for the old {@code (K900BluetoothManager)} cast, which would have thrown {@code + * ClassCastException} here. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class TransferCompleteCommandHandlerTest { + + private AsgClientServiceManager serviceManager; + private ICompanionTransport transport; + private TransferCompleteCommandHandler handler; + + @Before + public void setUp() { + serviceManager = mock(AsgClientServiceManager.class); + transport = mock(ICompanionTransport.class); + when(serviceManager.getBluetoothManager()).thenReturn(transport); + handler = new TransferCompleteCommandHandler(serviceManager); + } + + @Test + public void transferComplete_success_forwardedToTransport() throws Exception { + boolean handled = + handler.handleCommand( + "transfer_complete", + new JSONObject().put("fileName", "photo.jpg").put("success", true)); + + assertThat(handled).isTrue(); + verify(transport).onFileTransferConfirmation("photo.jpg", true); + } + + @Test + public void transferComplete_failure_forwardedToTransport() throws Exception { + boolean handled = + handler.handleCommand( + "transfer_complete", + new JSONObject().put("fileName", "video.mp4").put("success", false)); + + assertThat(handled).isTrue(); + verify(transport).onFileTransferConfirmation("video.mp4", false); + } + + @Test + public void transferComplete_missingFileName_rejected() throws Exception { + boolean handled = + handler.handleCommand("transfer_complete", new JSONObject().put("success", true)); + + assertThat(handled).isFalse(); + verify(transport, never()).onFileTransferConfirmation(anyString(), anyBoolean()); + } + + @Test + public void transferComplete_transportUnavailable_failsGracefully() throws Exception { + when(serviceManager.getBluetoothManager()).thenReturn(null); + + boolean handled = + handler.handleCommand( + "transfer_complete", + new JSONObject().put("fileName", "photo.jpg").put("success", true)); + + assertThat(handled).isFalse(); + } +} From 5b6bf87e5625865b0af0a377ee7e0a60308a37ca Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 32/47] Test FileTransferAckEventSubscriber routes ACK via ICompanionTransport --- .../FileTransferAckEventSubscriberTest.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/FileTransferAckEventSubscriberTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/FileTransferAckEventSubscriberTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/FileTransferAckEventSubscriberTest.java new file mode 100644 index 0000000000..ab1b81ee5a --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/FileTransferAckEventSubscriberTest.java @@ -0,0 +1,61 @@ +package com.mentra.asg_client.service.core.handlers.subscribers; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; +import com.mentra.asg_client.io.peripheral.events.FileTransferAckEvent; +import com.mentra.asg_client.io.peripheral.events.ShutdownEvent; +import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Verifies {@link FileTransferAckEventSubscriber} forwards MCU ACK frames through the {@link + * ICompanionTransport} interface. Uses a plain transport mock (NOT a K900 type) — regression guard + * for the old {@code (K900BluetoothManager)} cast. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class FileTransferAckEventSubscriberTest { + + private AsgClientServiceManager serviceManager; + private ICompanionTransport transport; + private FileTransferAckEventSubscriber subscriber; + + @Before + public void setUp() { + serviceManager = mock(AsgClientServiceManager.class); + transport = mock(ICompanionTransport.class); + when(serviceManager.getBluetoothManager()).thenReturn(transport); + subscriber = new FileTransferAckEventSubscriber(serviceManager); + } + + @Test + public void fileTransferAck_forwardedToTransport() { + subscriber.onMcuEvent(new FileTransferAckEvent(1, 7)); + + verify(transport).onFileTransferAck(1, 7); + } + + @Test + public void otherMcuEvent_ignored() { + subscriber.onMcuEvent(new ShutdownEvent()); + + verify(transport, never()).onFileTransferAck(anyInt(), anyInt()); + } + + @Test + public void transportUnavailable_doesNotThrow() { + when(serviceManager.getBluetoothManager()).thenReturn(null); + + subscriber.onMcuEvent(new FileTransferAckEvent(0, 3)); + // No exception expected; nothing to forward without a transport. + } +} From 3bd90ebf2f7f1ecda99d244c6eecc7b81dd50643 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 33/47] Update BesOtaAuthEventSubscriberTest to mock IBesOtaController --- .../subscribers/BesOtaAuthEventSubscriberTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthEventSubscriberTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthEventSubscriberTest.java index bdb4358263..12298794ab 100644 --- a/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthEventSubscriberTest.java +++ b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthEventSubscriberTest.java @@ -5,7 +5,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.mentra.asg_client.io.bes.BesOtaManager; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; import com.mentra.asg_client.io.peripheral.events.BesOtaAuthEvent; import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; import org.junit.Before; @@ -14,19 +14,23 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -/** Verifies {@link BesOtaAuthEventSubscriber} routes the OTA authorization result correctly. */ +/** + * Verifies {@link BesOtaAuthEventSubscriber} routes the OTA authorization result correctly. The + * controller is mocked as the core {@link IBesOtaController} interface — the subscriber must not + * depend on the vendor implementation. + */ @RunWith(RobolectricTestRunner.class) @Config(sdk = 33) public class BesOtaAuthEventSubscriberTest { private AsgClientServiceManager serviceManager; - private BesOtaManager besOtaManager; + private IBesOtaController besOtaManager; private BesOtaAuthEventSubscriber subscriber; @Before public void setUp() { serviceManager = mock(AsgClientServiceManager.class); - besOtaManager = mock(BesOtaManager.class); + besOtaManager = mock(IBesOtaController.class); when(serviceManager.getBesOtaManager()).thenReturn(besOtaManager); subscriber = new BesOtaAuthEventSubscriber(serviceManager); } From 4816513a1e1b8ae73cedc8273a5c3617ff3b2e5b Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:33:57 +0000 Subject: [PATCH 34/47] Test CommandProtocolDetector base behavior and runtime strategy registration --- .../CommandProtocolDetectorTest.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/service/core/processors/CommandProtocolDetectorTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/service/core/processors/CommandProtocolDetectorTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/processors/CommandProtocolDetectorTest.java new file mode 100644 index 0000000000..89a55aba95 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/processors/CommandProtocolDetectorTest.java @@ -0,0 +1,122 @@ +package com.mentra.asg_client.service.core.processors; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.mentra.asg_client.io.bluetooth.managers.mentralive.internal.K900ProtocolStrategy; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Verifies the {@link CommandProtocolDetector} ships with only the base (vendor-free) strategies, + * and that runtime registration of {@link K900ProtocolStrategy} restores MCU wire-format detection + * without disturbing JSON or chunked routing — the seam introduced for the A6 module split. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class CommandProtocolDetectorTest { + + private static JSONObject standardJson() throws Exception { + return new JSONObject().put("type", "ping").put("mId", 42); + } + + private static JSONObject cFieldWithValidJson() throws Exception { + return new JSONObject().put("C", new JSONObject().put("type", "ping").toString()); + } + + /** Mentra Live MCU frame: the C payload is a bare command string, not JSON. */ + private static JSONObject mcuFrame() throws Exception { + return new JSONObject().put("C", "cs_flts").put("B", new JSONObject().put("state", 1)); + } + + private static JSONObject chunkFrame() throws Exception { + JSONObject chunk = + new JSONObject() + .put("type", "chunked_msg") + .put("chunkId", "abc") + .put("chunk", 0) + .put("total", 2) + .put("data", "payload-part"); + return new JSONObject().put("C", chunk.toString()); + } + + @Test + public void baseDetector_standardJson_isJsonCommand() throws Exception { + CommandProtocolDetector detector = new CommandProtocolDetector(); + + CommandProtocolDetector.ProtocolDetectionResult result = + detector.detectProtocol(standardJson()); + + assertThat(result.protocolType()) + .isEqualTo(CommandProtocolDetector.ProtocolType.JSON_COMMAND); + assertThat(result.commandType()).isEqualTo("ping"); + assertThat(result.messageId()).isEqualTo(42); + } + + @Test + public void baseDetector_cFieldWithValidJson_isJsonCommand() throws Exception { + CommandProtocolDetector detector = new CommandProtocolDetector(); + + CommandProtocolDetector.ProtocolDetectionResult result = + detector.detectProtocol(cFieldWithValidJson()); + + assertThat(result.protocolType()) + .isEqualTo(CommandProtocolDetector.ProtocolType.JSON_COMMAND); + } + + @Test + public void baseDetector_mcuFrame_isUnknown_coreNoLongerKnowsK900() throws Exception { + CommandProtocolDetector detector = new CommandProtocolDetector(); + + CommandProtocolDetector.ProtocolDetectionResult result = + detector.detectProtocol(mcuFrame()); + + assertThat(result.protocolType()).isEqualTo(CommandProtocolDetector.ProtocolType.UNKNOWN); + } + + @Test + public void registeredK900Strategy_mcuFrame_isK900Protocol() throws Exception { + CommandProtocolDetector detector = new CommandProtocolDetector(); + detector.addDetectionStrategy(new K900ProtocolStrategy()); + + CommandProtocolDetector.ProtocolDetectionResult result = + detector.detectProtocol(mcuFrame()); + + assertThat(result.protocolType()) + .isEqualTo(CommandProtocolDetector.ProtocolType.K900_PROTOCOL); + assertThat(result.isValid()).isTrue(); + } + + @Test + public void registeredK900Strategy_doesNotStealJsonCommands() throws Exception { + CommandProtocolDetector detector = new CommandProtocolDetector(); + detector.addDetectionStrategy(new K900ProtocolStrategy()); + + assertThat(detector.detectProtocol(standardJson()).protocolType()) + .isEqualTo(CommandProtocolDetector.ProtocolType.JSON_COMMAND); + assertThat(detector.detectProtocol(cFieldWithValidJson()).protocolType()) + .isEqualTo(CommandProtocolDetector.ProtocolType.JSON_COMMAND); + } + + @Test + public void chunkedSupport_withK900Registered_chunksStillRouteToChunkedStrategy() + throws Exception { + // Mirrors CommandProcessor wiring order: vendor strategies first, then chunked support. + CommandProtocolDetector detector = new CommandProtocolDetector(); + detector.addDetectionStrategy(new K900ProtocolStrategy()); + detector.addChunkedMessageSupport(new ChunkReassembler()); + + CommandProtocolDetector.ProtocolDetectionResult result = + detector.detectProtocol(chunkFrame()); + + // A first chunk is consumed by the chunked strategy and reported as in-progress — + // proving neither the K900 nor the JSON strategy intercepted it. + assertThat(result.commandType()).isEqualTo("chunk_in_progress"); + + // And the MCU frame still routes to K900 with chunked support active. + assertThat(detector.detectProtocol(mcuFrame()).protocolType()) + .isEqualTo(CommandProtocolDetector.ProtocolType.K900_PROTOCOL); + } +} From b26f7a553ca9a5684f1561578343e391b84cfdbf Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:34:26 +0000 Subject: [PATCH 35/47] Test TransportModule provider instances and unscoped lifecycle --- .../di/hilt/TransportModuleTest.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/di/hilt/TransportModuleTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/di/hilt/TransportModuleTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/di/hilt/TransportModuleTest.java new file mode 100644 index 0000000000..2c238ceff5 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/di/hilt/TransportModuleTest.java @@ -0,0 +1,76 @@ +package com.mentra.asg_client.di.hilt; + +import static org.assertj.core.api.Assertions.assertThat; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; +import com.mentra.asg_client.io.bluetooth.managers.StandardBluetoothManager; +import com.mentra.asg_client.io.bluetooth.managers.mentralive.internal.K900ProtocolStrategy; +import com.mentra.asg_client.io.network.interfaces.INetworkManager; +import com.mentra.asg_client.service.core.processors.CommandProtocolDetector; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowBuild; + +/** + * Verifies {@link TransportModule} vendor-wiring providers: device-appropriate transport/network + * selection and the unscoped (fresh-instance-per-injection) lifecycle contract. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class TransportModuleTest { + + private static Context genericDeviceContext() { + ShadowBuild.setModel("Pixel 7"); + ShadowBuild.setProduct("pixel_7"); + ShadowBuild.setManufacturer("Google"); + return ApplicationProvider.getApplicationContext(); + } + + @Test + public void provideK900ProtocolStrategy_returnsMcuWireFormatStrategy() { + CommandProtocolDetector.ProtocolDetectionStrategy strategy = + TransportModule.provideK900ProtocolStrategy(); + + assertThat(strategy).isInstanceOf(K900ProtocolStrategy.class); + assertThat(strategy.getProtocolType()) + .isEqualTo(CommandProtocolDetector.ProtocolType.K900_PROTOCOL); + } + + @Test + public void provideCompanionTransport_genericDevice_returnsStandardManager() { + Context context = genericDeviceContext(); + + ICompanionTransport transport = TransportModule.provideCompanionTransport(context); + + assertThat(transport).isInstanceOf(StandardBluetoothManager.class); + } + + @Test + public void provideCompanionTransport_unscoped_returnsFreshInstancePerCall() { + // Lifecycle contract: AsgClientServiceManager.cleanup() shuts the transport down on + // service destroy. A cached/singleton transport would be dead after a service restart, + // so every injection must build a fresh instance. + Context context = genericDeviceContext(); + + ICompanionTransport first = TransportModule.provideCompanionTransport(context); + ICompanionTransport second = TransportModule.provideCompanionTransport(context); + + assertThat(first).isNotSameAs(second); + } + + @Test + public void provideNetworkManager_returnsManagerAndFreshInstancePerCall() { + Context context = genericDeviceContext(); + + INetworkManager first = TransportModule.provideNetworkManager(context); + INetworkManager second = TransportModule.provideNetworkManager(context); + + assertThat(first).isNotNull(); + assertThat(second).isNotNull(); + assertThat(first).isNotSameAs(second); + } +} From 8069c4ce9878b5955dbdbfedcdd93c46b218d8cd Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:34:26 +0000 Subject: [PATCH 36/47] Test BesOtaRegistry via IBesOtaRegistry interface --- .../asg_client/io/bes/BesOtaRegistryTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/io/bes/BesOtaRegistryTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/io/bes/BesOtaRegistryTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/io/bes/BesOtaRegistryTest.java new file mode 100644 index 0000000000..17dd9f7b5e --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/io/bes/BesOtaRegistryTest.java @@ -0,0 +1,39 @@ +package com.mentra.asg_client.io.bes; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaRegistry; +import org.junit.Test; +import org.mockito.Mockito; + +/** Verifies {@link BesOtaRegistry} behavior through the core {@link IBesOtaRegistry} interface. */ +public class BesOtaRegistryTest { + + @Test + public void getInstance_initiallyNull() { + IBesOtaRegistry registry = new BesOtaRegistry(); + + assertThat(registry.getInstance()).isNull(); + } + + @Test + public void setInstance_thenGetInstance_returnsSameController() { + IBesOtaRegistry registry = new BesOtaRegistry(); + IBesOtaController controller = Mockito.mock(IBesOtaController.class); + + registry.setInstance(controller); + + assertThat(registry.getInstance()).isSameAs(controller); + } + + @Test + public void clear_removesController() { + IBesOtaRegistry registry = new BesOtaRegistry(); + registry.setInstance(Mockito.mock(IBesOtaController.class)); + + registry.clear(); + + assertThat(registry.getInstance()).isNull(); + } +} From ab67e89faf2d0eec08a1147bcf7fbf8fe246aec1 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:34:26 +0000 Subject: [PATCH 37/47] Test BesOtaManager version methods through IBesOtaController --- .../io/bes/BesOtaManagerVersionTest.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/io/bes/BesOtaManagerVersionTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/io/bes/BesOtaManagerVersionTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/io/bes/BesOtaManagerVersionTest.java new file mode 100644 index 0000000000..758e5140c7 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/io/bes/BesOtaManagerVersionTest.java @@ -0,0 +1,107 @@ +package com.mentra.asg_client.io.bes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import com.mentra.asg_client.io.bluetooth.managers.mentralive.internal.SerialPortBridge; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; +import com.mentra.asg_client.service.core.handlers.K900CommandHandler; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Exercises the version-comparison methods of {@code BesOtaManager} through the core {@link + * IBesOtaController} interface (these were previously statics consumed directly by {@code + * OtaHelper}). + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class BesOtaManagerVersionTest { + + private IBesOtaController controller; + private boolean previousInProgressFlag; + + @Before + public void setUp() { + Context context = ApplicationProvider.getApplicationContext(); + controller = + new BesOtaManager( + mock(SerialPortBridge.class), context, mock(K900CommandHandler.class)); + previousInProgressFlag = BesOtaManager.isBesOtaInProgress; + } + + @After + public void tearDown() { + // The in-progress flag is static — restore it so state never leaks across tests. + BesOtaManager.isBesOtaInProgress = previousInProgressFlag; + } + + @Test + public void parseServerVersionCode_splitsMajorMinorPatch() { + // XYYYZZZ format: 17026001 -> major 17, minor 26, patch 1, build 0 + byte[] version = controller.parseServerVersionCode(17026001L); + + assertThat(version).containsExactly((byte) 17, (byte) 26, (byte) 1, (byte) 0); + } + + @Test + public void parseServerVersionCode_zero_isAllZero() { + assertThat(controller.parseServerVersionCode(0L)) + .containsExactly((byte) 0, (byte) 0, (byte) 0, (byte) 0); + } + + @Test + public void isNewerVersion_newerMajor_true() { + byte[] v1 = {2, 0, 0, 0}; + byte[] v2 = {1, 9, 9, 9}; + + assertThat(controller.isNewerVersion(v1, v2)).isTrue(); + } + + @Test + public void isNewerVersion_olderPatch_false() { + byte[] v1 = {1, 2, 3, 0}; + byte[] v2 = {1, 2, 4, 0}; + + assertThat(controller.isNewerVersion(v1, v2)).isFalse(); + } + + @Test + public void isNewerVersion_equalVersions_false() { + byte[] v = {1, 2, 3, 4}; + + assertThat(controller.isNewerVersion(v, v)).isFalse(); + } + + @Test + public void isNewerVersion_newerBuild_true() { + byte[] v1 = {1, 2, 3, 5}; + byte[] v2 = {1, 2, 3, 4}; + + assertThat(controller.isNewerVersion(v1, v2)).isTrue(); + } + + @Test + public void isNewerVersion_nullOrShortInput_false() { + byte[] valid = {1, 2, 3, 4}; + + assertThat(controller.isNewerVersion(null, valid)).isFalse(); + assertThat(controller.isNewerVersion(valid, null)).isFalse(); + assertThat(controller.isNewerVersion(new byte[] {1, 2}, valid)).isFalse(); + } + + @Test + public void isBesOtaInProgress_reflectsStaticFlag() { + BesOtaManager.isBesOtaInProgress = false; + assertThat(controller.isBesOtaInProgress()).isFalse(); + + BesOtaManager.isBesOtaInProgress = true; + assertThat(controller.isBesOtaInProgress()).isTrue(); + } +} From 373ce460d8f17fe6aa9f5f4888558d4f5d3adc46 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:34:26 +0000 Subject: [PATCH 38/47] Test OtaModule interface binding and OtaHelper wiring --- .../asg_client/di/hilt/OtaModuleTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/di/hilt/OtaModuleTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/di/hilt/OtaModuleTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/di/hilt/OtaModuleTest.java new file mode 100644 index 0000000000..528449ced9 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/di/hilt/OtaModuleTest.java @@ -0,0 +1,39 @@ +package com.mentra.asg_client.di.hilt; + +import static org.assertj.core.api.Assertions.assertThat; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import com.mentra.asg_client.io.bes.BesOtaRegistry; +import com.mentra.asg_client.io.ota.helpers.OtaHelper; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Verifies {@link OtaModule} providers bind the core OTA interfaces to the vendor singletons. */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class OtaModuleTest { + + @Test + public void provideBesOtaRegistry_returnsSameUnderlyingInstance() { + BesOtaRegistry impl = new BesOtaRegistry(); + + IBesOtaRegistry bound = OtaModule.provideBesOtaRegistry(impl); + + assertThat(bound).isSameAs(impl); + } + + @Test + public void provideOtaHelper_wiresHelperWithRegistry() { + Context context = ApplicationProvider.getApplicationContext(); + IBesOtaRegistry registry = OtaModule.provideBesOtaRegistry(new BesOtaRegistry()); + + OtaHelper helper = OtaModule.provideOtaHelper(context, registry); + + assertThat(helper).isNotNull(); + helper.cleanup(); + } +} From f5d9f22998ba042aa7d8c20be5fe7e75392c3c24 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:34:27 +0000 Subject: [PATCH 39/47] Test OtaHelper null-safe IBesOtaRegistry access --- .../io/ota/helpers/OtaHelperBesGuardTest.java | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/io/ota/helpers/OtaHelperBesGuardTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/io/ota/helpers/OtaHelperBesGuardTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/io/ota/helpers/OtaHelperBesGuardTest.java new file mode 100644 index 0000000000..a59c4f1078 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/io/ota/helpers/OtaHelperBesGuardTest.java @@ -0,0 +1,110 @@ +package com.mentra.asg_client.io.ota.helpers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaRegistry; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Focused guard-path tests for {@link OtaHelper} verifying null-safe {@link IBesOtaRegistry} + * access. The cache-prune methods were removed in the dev cleanup; the in-progress guard is + * verified through construction and cleanup without NPE. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class OtaHelperBesGuardTest { + + /** Simple registry stub whose controller can be swapped per test. */ + private static final class StubRegistry implements IBesOtaRegistry { + private IBesOtaController controller; + + @Override + public IBesOtaController getInstance() { + return controller; + } + + @Override + public void setInstance(IBesOtaController controller) { + this.controller = controller; + } + + @Override + public void clear() { + this.controller = null; + } + } + + private OtaHelper otaHelper; + + @After + public void tearDown() { + if (otaHelper != null) { + otaHelper.cleanup(); + } + } + + private OtaHelper newHelper(StubRegistry registry) { + Context context = ApplicationProvider.getApplicationContext(); + otaHelper = new OtaHelper(context, registry); + return otaHelper; + } + + @Test + public void noController_constructionAndCleanup_doesNotThrow() { + // Non-K900 reality: the registry never gets a controller. Construction and cleanup + // must not NPE even when getOtaController() always returns null. + assertThatCode( + () -> { + OtaHelper helper = newHelper(new StubRegistry()); + helper.cleanup(); + otaHelper = null; + }) + .doesNotThrowAnyException(); + } + + @Test + public void activeController_isBesOtaInProgress_reflectsControllerState() { + // Directly verify the IBesOtaController contract: the helper must delegate to the + // interface, not a static flag. + StubRegistry registry = new StubRegistry(); + IBesOtaController controller = mock(IBesOtaController.class); + when(controller.isBesOtaInProgress()).thenReturn(false); + registry.setInstance(controller); + + OtaHelper helper = newHelper(registry); + + // While the helper is alive and the controller says false, nothing blows up. + assertThatCode(helper::pruneInvalidCachedArtifactsOnStartup).doesNotThrowAnyException(); + assertThatCode(helper::clearAllCachedArtifacts).doesNotThrowAnyException(); + } + + @Test + public void controllerRemovedAfterConstruction_doesNotThrow() { + // Registry is a late-binding seam: the controller can disappear during service shutdown. + StubRegistry registry = new StubRegistry(); + registry.setInstance(mock(IBesOtaController.class)); + OtaHelper helper = newHelper(registry); + + registry.clear(); + + assertThatCode(helper::pruneInvalidCachedArtifactsOnStartup).doesNotThrowAnyException(); + assertThatCode(helper::clearAllCachedArtifacts).doesNotThrowAnyException(); + } + + @Test + public void besOtaRegistry_nullSafeGetInstance_returnsNullWhenUnset() { + IBesOtaRegistry registry = new StubRegistry(); + + assertThat(registry.getInstance()).isNull(); + } +} From ef78d9dbaf55dda954dc19ecdb41db9091292d14 Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:34:27 +0000 Subject: [PATCH 40/47] Integration test: peripheral bus routes BES OTA auth events to IBesOtaController --- .../BesOtaAuthRoutingIntegrationTest.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthRoutingIntegrationTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthRoutingIntegrationTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthRoutingIntegrationTest.java new file mode 100644 index 0000000000..77f443d9e5 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/handlers/subscribers/BesOtaAuthRoutingIntegrationTest.java @@ -0,0 +1,80 @@ +package com.mentra.asg_client.service.core.handlers.subscribers; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; +import com.mentra.asg_client.io.ota.interfaces.IBesOtaController; +import com.mentra.asg_client.io.peripheral.IPeripheralBus; +import com.mentra.asg_client.io.peripheral.SimplePeripheralBus; +import com.mentra.asg_client.io.peripheral.events.BesOtaAuthEvent; +import com.mentra.asg_client.io.peripheral.events.FileTransferAckEvent; +import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Integration test for the event-bus-to-interface seam: a REAL {@link SimplePeripheralBus} with + * REAL subscribers, asserting MCU events land on the core {@link IBesOtaController} and {@link + * ICompanionTransport} interfaces — the exact boundary the A6 module split cuts. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class BesOtaAuthRoutingIntegrationTest { + + private AsgClientServiceManager serviceManager; + private IBesOtaController otaController; + private ICompanionTransport transport; + private IPeripheralBus bus; + + @Before + public void setUp() { + serviceManager = mock(AsgClientServiceManager.class); + otaController = mock(IBesOtaController.class); + transport = mock(ICompanionTransport.class); + when(serviceManager.getBesOtaManager()).thenReturn(otaController); + when(serviceManager.getBluetoothManager()).thenReturn(transport); + + bus = new SimplePeripheralBus(); + bus.subscribe(new BesOtaAuthEventSubscriber(serviceManager)); + bus.subscribe(new FileTransferAckEventSubscriber(serviceManager)); + } + + @Test + public void authGrantedEvent_reachesControllerInterface() { + bus.publish(new BesOtaAuthEvent(true)); + + verify(otaController).onAuthorizationGranted(); + verify(otaController, never()).onAuthorizationDenied(); + } + + @Test + public void authDeniedEvent_reachesControllerInterface() { + bus.publish(new BesOtaAuthEvent(false)); + + verify(otaController).onAuthorizationDenied(); + verify(otaController, never()).onAuthorizationGranted(); + } + + @Test + public void fileTransferAckEvent_reachesTransportInterface() { + bus.publish(new FileTransferAckEvent(1, 12)); + + verify(transport).onFileTransferAck(1, 12); + } + + @Test + public void eventsRouteIndependently_ackDoesNotTouchOtaController() { + bus.publish(new FileTransferAckEvent(0, 3)); + + verify(otaController, never()).onAuthorizationGranted(); + verify(otaController, never()).onAuthorizationDenied(); + verify(transport, never()).onMtuNegotiated(anyInt()); + } +} From 1328561de093112a6394ce37c78790304dc8a1dd Mon Sep 17 00:00:00 2001 From: Nicolo Micheletti Date: Tue, 23 Jun 2026 04:34:27 +0000 Subject: [PATCH 41/47] Integration test: CommandProcessor routes commands via injected vendor protocol strategy --- ...ommandProcessorRoutingIntegrationTest.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/service/core/processors/CommandProcessorRoutingIntegrationTest.java diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/service/core/processors/CommandProcessorRoutingIntegrationTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/processors/CommandProcessorRoutingIntegrationTest.java new file mode 100644 index 0000000000..52880e75b1 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/service/core/processors/CommandProcessorRoutingIntegrationTest.java @@ -0,0 +1,151 @@ +package com.mentra.asg_client.service.core.processors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import com.mentra.asg_client.io.bluetooth.interfaces.ICompanionTransport; +import com.mentra.asg_client.io.bluetooth.managers.mentralive.internal.K900ProtocolStrategy; +import com.mentra.asg_client.io.file.core.FileManager; +import com.mentra.asg_client.io.peripheral.IPeripheralBus; +import com.mentra.asg_client.io.peripheral.events.FileTransferAckEvent; +import com.mentra.asg_client.io.peripheral.events.McuEvent; +import com.mentra.asg_client.service.communication.interfaces.ICommunicationManager; +import com.mentra.asg_client.service.communication.interfaces.IResponseBuilder; +import com.mentra.asg_client.service.core.handlers.OtaCommandHandler; +import com.mentra.asg_client.service.core.handlers.RgbLedCommandHandler; +import com.mentra.asg_client.service.legacy.managers.AsgClientServiceManager; +import com.mentra.asg_client.service.media.interfaces.IMediaManager; +import com.mentra.asg_client.service.system.interfaces.IConfigurationManager; +import com.mentra.asg_client.service.system.interfaces.IStateManager; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Set; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * End-to-end JVM integration test for the protocol-strategy injection seam: raw bytes through a + * REAL {@link CommandProcessor}, {@code CommandParser}, {@link CommandProtocolDetector} (with the + * vendor strategy injected at runtime), {@code K900CommandHandler}, {@code McuEventParser} and out + * to the peripheral bus / transport interface. This is the path the A6 module split will cut + * across modules. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +public class CommandProcessorRoutingIntegrationTest { + + private AsgClientServiceManager serviceManager; + private ICompanionTransport transport; + private IPeripheralBus peripheralBus; + + @Before + public void setUp() { + serviceManager = mock(AsgClientServiceManager.class); + transport = mock(ICompanionTransport.class); + peripheralBus = mock(IPeripheralBus.class); + when(serviceManager.getBluetoothManager()).thenReturn(transport); + } + + private CommandProcessor newProcessor( + Set strategies) { + Context context = ApplicationProvider.getApplicationContext(); + return new CommandProcessor( + context, + mock(ICommunicationManager.class), + mock(IStateManager.class), + mock(IMediaManager.class), + mock(IResponseBuilder.class), + mock(IConfigurationManager.class), + serviceManager, + mock(FileManager.class), + mock(RgbLedCommandHandler.class), + mock(OtaCommandHandler.class), + peripheralBus, + strategies); + } + + private static byte[] bytes(JSONObject json) { + return json.toString().getBytes(StandardCharsets.UTF_8); + } + + @Test + public void mcuFrame_withInjectedK900Strategy_publishesTypedEventOnBus() throws Exception { + CommandProcessor processor = newProcessor(Set.of(new K900ProtocolStrategy())); + JSONObject mcuFrame = + new JSONObject() + .put("C", "cs_flts") + .put("B", new JSONObject().put("state", 1).put("index", 7)); + + processor.processCommand(bytes(mcuFrame)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(McuEvent.class); + verify(peripheralBus).publish(captor.capture()); + assertThat(captor.getValue()).isInstanceOf(FileTransferAckEvent.class); + FileTransferAckEvent ack = (FileTransferAckEvent) captor.getValue(); + assertThat(ack.getState()).isEqualTo(1); + assertThat(ack.getIndex()).isEqualTo(7); + } + + @Test + public void mcuFrame_withoutVendorStrategies_isDroppedWithoutException() throws Exception { + CommandProcessor processor = newProcessor(Collections.emptySet()); + JSONObject mcuFrame = + new JSONObject() + .put("C", "cs_flts") + .put("B", new JSONObject().put("state", 1).put("index", 7)); + + assertThatCode(() -> processor.processCommand(bytes(mcuFrame))) + .doesNotThrowAnyException(); + + verify(peripheralBus, never()).publish(any()); + } + + @Test + public void setBleMtu_jsonCommand_reachesTransportThroughInterface() throws Exception { + CommandProcessor processor = newProcessor(Set.of(new K900ProtocolStrategy())); + JSONObject command = new JSONObject().put("type", "set_ble_mtu").put("mtu", 251); + + processor.processCommand(bytes(command)); + + // Bytes -> parser -> detector -> registry -> BleConfigCommandHandler -> transport hook. + verify(transport).onMtuNegotiated(251); + } + + @Test + public void setBleMtu_stillRoutes_whenNoVendorStrategiesRegistered() throws Exception { + CommandProcessor processor = newProcessor(Collections.emptySet()); + JSONObject command = new JSONObject().put("type", "set_ble_mtu").put("mtu", 185); + + processor.processCommand(bytes(command)); + + verify(transport).onMtuNegotiated(185); + } + + @Test + public void transferComplete_jsonCommand_reachesTransportWithoutVendorCast() throws Exception { + // The transport is a plain ICompanionTransport mock — the old code would have thrown + // ClassCastException casting it to K900BluetoothManager. + CommandProcessor processor = newProcessor(Set.of(new K900ProtocolStrategy())); + JSONObject command = + new JSONObject() + .put("type", "transfer_complete") + .put("fileName", "photo.jpg") + .put("success", true); + + processor.processCommand(bytes(command)); + + verify(transport).onFileTransferConfirmation("photo.jpg", true); + } +} From db858a9a174240b57cb3850a0acebebe6506b0a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 23 Jun 2026 05:16:29 +0000 Subject: [PATCH 42/47] Remove unused EntryPointAccessors import and dead no-op cache stubs from OtaHelper Co-authored-by: Nicolo Micheletti --- .../mentra/asg_client/io/ota/helpers/OtaHelper.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/helpers/OtaHelper.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/helpers/OtaHelper.java index dc97a94814..964df193dc 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/helpers/OtaHelper.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/helpers/OtaHelper.java @@ -10,7 +10,6 @@ import android.os.Looper; import android.util.Log; import android.content.Intent; -import dagger.hilt.android.EntryPointAccessors; import org.json.JSONException; import org.json.JSONObject; @@ -309,15 +308,6 @@ public void deleteDownloadedArtifactForType(String updateType) { } } - public void clearAllCachedArtifacts() { - // Cache infrastructure removed in dev cleanup; no-op retained for API compatibility. - } - - public void pruneInvalidCachedArtifactsOnStartup() { - // Cache infrastructure removed in dev cleanup; BES in-progress guard preserved via - // isBesOtaInProgress() helper, which is exercised in checkAndUpdateBesFirmware. - } - private void deleteFileIfExists(String path, String label) { File file = new File(path); if (file.exists() && !file.delete()) { From 803fafed9eb77785db95ab5067d589721ed24e2a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 23 Jun 2026 05:16:29 +0000 Subject: [PATCH 43/47] Update OtaHelperBesGuardTest after removing dead no-op methods Co-authored-by: Nicolo Micheletti --- .../io/ota/helpers/OtaHelperBesGuardTest.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/io/ota/helpers/OtaHelperBesGuardTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/io/ota/helpers/OtaHelperBesGuardTest.java index a59c4f1078..60aace1919 100644 --- a/asg_client/app/src/test/java/com/mentra/asg_client/io/ota/helpers/OtaHelperBesGuardTest.java +++ b/asg_client/app/src/test/java/com/mentra/asg_client/io/ota/helpers/OtaHelperBesGuardTest.java @@ -17,8 +17,9 @@ /** * Focused guard-path tests for {@link OtaHelper} verifying null-safe {@link IBesOtaRegistry} - * access. The cache-prune methods were removed in the dev cleanup; the in-progress guard is - * verified through construction and cleanup without NPE. + * access. The isBesOtaInProgress() guard is exercised by the production paths inside + * checkAndUpdateBesFirmware; these tests verify the structural invariants (null controller = no + * NPE, registry correctly stores and clears controllers). */ @RunWith(RobolectricTestRunner.class) @Config(sdk = 33) @@ -61,8 +62,8 @@ private OtaHelper newHelper(StubRegistry registry) { @Test public void noController_constructionAndCleanup_doesNotThrow() { - // Non-K900 reality: the registry never gets a controller. Construction and cleanup - // must not NPE even when getOtaController() always returns null. + // Non-K900 reality: the registry never gets a controller; construction and cleanup + // must not NPE when getOtaController() always returns null. assertThatCode( () -> { OtaHelper helper = newHelper(new StubRegistry()); @@ -73,19 +74,15 @@ public void noController_constructionAndCleanup_doesNotThrow() { } @Test - public void activeController_isBesOtaInProgress_reflectsControllerState() { - // Directly verify the IBesOtaController contract: the helper must delegate to the - // interface, not a static flag. + public void activeController_isBesOtaInProgress_delegatesToController() { StubRegistry registry = new StubRegistry(); IBesOtaController controller = mock(IBesOtaController.class); when(controller.isBesOtaInProgress()).thenReturn(false); registry.setInstance(controller); - OtaHelper helper = newHelper(registry); - - // While the helper is alive and the controller says false, nothing blows up. - assertThatCode(helper::pruneInvalidCachedArtifactsOnStartup).doesNotThrowAnyException(); - assertThatCode(helper::clearAllCachedArtifacts).doesNotThrowAnyException(); + // Helper constructs and cleans up without NPE when a controller is present. + assertThatCode(() -> newHelper(registry).cleanup()).doesNotThrowAnyException(); + otaHelper = null; } @Test @@ -97,8 +94,8 @@ public void controllerRemovedAfterConstruction_doesNotThrow() { registry.clear(); - assertThatCode(helper::pruneInvalidCachedArtifactsOnStartup).doesNotThrowAnyException(); - assertThatCode(helper::clearAllCachedArtifacts).doesNotThrowAnyException(); + // Any entry point that reaches isBesOtaInProgress() must handle a null controller. + assertThatCode(helper::cleanup).doesNotThrowAnyException(); } @Test From 84901dc645ad25cb664f4e954186f38fcd3856e2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 23 Jun 2026 05:16:30 +0000 Subject: [PATCH 44/47] Defer ICompanionTransport and INetworkManager construction via Provider until after ensureForegroundStarted Co-authored-by: Nicolo Micheletti --- .../service/core/AsgClientService.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/AsgClientService.java b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/AsgClientService.java index 01e7ddaa5b..d3cf27f17c 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/service/core/AsgClientService.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/service/core/AsgClientService.java @@ -52,6 +52,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Inject; +import javax.inject.Provider; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -77,11 +78,18 @@ public class AsgClientService extends Service implements NetworkStateListener, T /** Vendor-supplied protocol detection strategies (e.g. the Mentra Live MCU wire format). */ @Inject Set protocolStrategies; - /** Device-appropriate companion transport, constructed by the vendor wiring layer. */ - @Inject ICompanionTransport companionTransport; + /** + * Provider for the device-appropriate companion transport. Using {@link Provider} defers + * construction until after {@link #ensureForegroundStarted()} so the K900 serial-port thread + * does not open before the foreground notification is posted. + */ + @Inject Provider companionTransportProvider; - /** Device-appropriate network manager, constructed by the vendor wiring layer. */ - @Inject INetworkManager injectedNetworkManager; + /** + * Provider for the device-appropriate network manager, deferred for the same reason as + * {@link #companionTransportProvider}. + */ + @Inject Provider networkManagerProvider; // --------------------------------------------- // Constants //TODO: Extract all the Constants and Magic Number/Text to AsgConstants @@ -651,8 +659,8 @@ private void initializeServiceInitializer() { serviceInitializer = new ServiceInitializer( this, - companionTransport, - injectedNetworkManager, + companionTransportProvider.get(), + networkManagerProvider.get(), fileManager, otaHelper, hardwareManager, From b740dffc8ca5e166c0cc4bcc9048dcdf6100140c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 23 Jun 2026 05:16:30 +0000 Subject: [PATCH 45/47] Update AGENTS.md: Hilt is now used in the service layer Co-authored-by: Nicolo Micheletti --- asg_client/AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asg_client/AGENTS.md b/asg_client/AGENTS.md index da9c44c849..7952e6b27e 100644 --- a/asg_client/AGENTS.md +++ b/asg_client/AGENTS.md @@ -154,7 +154,7 @@ asg_client/ ### Architecture -- **Dependency Injection**: Manual factories under `io/*/core/*Factory.java` and `service/utils/DeviceProfile`. No Dagger/Hilt today (`/di` only has `AppModule.java`). +- **Dependency Injection**: Hilt is used for the service layer (`AsgClientService` and the `di/hilt/` modules). Manual factories remain under `io/*/core/*Factory.java` and `service/utils/DeviceProfile` for device-detection paths. - **Error Reporting**: Use Sentry via reporting package - **Logging**: Use Android Logcat with appropriate tags - **Services**: Follow Android foreground service best practices From b9e093f3d65163160c40a548bdcdf4d037ae0ec2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 23 Jun 2026 05:16:30 +0000 Subject: [PATCH 46/47] Update OTA README to reflect two-arg OtaHelper constructor Co-authored-by: Nicolo Micheletti --- .../app/src/main/java/com/mentra/asg_client/io/ota/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/README.md b/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/README.md index ced10f15ff..0943f602d7 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/README.md +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/ota/README.md @@ -252,8 +252,9 @@ if (success) { ### **OTA Helper Operations** ```java -// Create OTA helper -OtaHelper otaHelper = new OtaHelper(context); +// OtaHelper is Hilt-injected; obtain the instance via @Inject or AsgClientEntryPoint. +// For testing or non-Hilt contexts use the two-arg constructor: +// OtaHelper otaHelper = new OtaHelper(context, new BesOtaRegistry()); // Start version check boolean checkStarted = otaHelper.startVersionCheck(context); From a6d1a67ae6cfc384627bde02454d4b6ad3413626 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Jul 2026 10:23:48 +0000 Subject: [PATCH 47/47] Implement new sendMessage overloads in IBluetoothManagerDefaultsTest Co-authored-by: Nicolo Micheletti --- .../interfaces/IBluetoothManagerDefaultsTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManagerDefaultsTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManagerDefaultsTest.java index f060d86fb7..58e490dd73 100644 --- a/asg_client/app/src/test/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManagerDefaultsTest.java +++ b/asg_client/app/src/test/java/com/mentra/asg_client/io/bluetooth/interfaces/IBluetoothManagerDefaultsTest.java @@ -34,6 +34,19 @@ public boolean sendMessage(byte[] data) { return false; } + @Override + public boolean sendMessage(byte[] data, IBluetoothManager.SendMessageCallback callback) { + return false; + } + + @Override + public boolean sendMessage( + byte[] data, + IBluetoothManager.SendMessageCallback callback, + IBluetoothManager.SendMessageGate gate) { + return false; + } + @Override public boolean sendFile(String filePath) { return false;