From 98cb59c4318d6fe439cdd454d37592298cb4ad80 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 12 May 2026 13:24:33 +0800 Subject: [PATCH] =?UTF-8?q?[FIX]=20=E4=BF=AE=E5=BE=A9=E9=81=A0=E7=AB=AF?= =?UTF-8?q?=E5=87=BA=E8=B2=A8=E6=B5=81=E7=A8=8B=E8=88=87=20MQTT=20?= =?UTF-8?q?=E5=9B=9E=E5=82=B3=E6=A9=9F=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修復 SchedulerAbstract.java 的 shutdown() 方法在多執行緒環境下因 scheduler 被設為 null 而導致的 NullPointerException。 2. 強化 ProductDispenseManager.java 的輪詢機制,在重新輪詢的 lambda 中加入 try-catch 安全網,確保即使排程異常也能觸發 onFailure 回調發送 ACK。 3. 優化 MqttService.java 對 dispense 指令的處理,增加對成功/失敗回調的日誌記錄與 Command ACK 發送。 4. 更新 TransactionFinalizeBuilder.java 與相關 Payload 結構,確保 code_id 以字串形式正確上報,並加入 remote_command_id 欄位以利後台對帳。 5. 修正 DevXinYuanController.java 對 VMC_04_BUY_STATUS 的處理,確保不論成功或處理中狀態皆能透過 MQTT 診斷 Topic 上報。 6. 修正 DevController.java 中 getReadBuffer 邏輯,避免在重新啟動排程時出現潛在競爭。 --- .agents/workflows/compiler-apk.md | 23 +- .../unibuy/smartdevice/SchedulerAbstract.java | 14 +- .../smartdevice/controller/DevController.java | 6 +- .../controller/DevXinYuanController.java | 59 +--- .../external/mqtt/MqttManager.java | 119 ++++--- .../smartdevice/service/MqttService.java | 295 ++++++++++-------- .../structure/mqtt/CommandAckPayload.java | 5 + .../mqtt/TransactionFinalizePayload.java | 251 +++++++++++---- .../tools/ProductDispenseManager.java | 127 ++++++-- .../tools/TransactionFinalizeBuilder.java | 32 +- 10 files changed, 588 insertions(+), 343 deletions(-) diff --git a/.agents/workflows/compiler-apk.md b/.agents/workflows/compiler-apk.md index 7e6abfd..f4de6a7 100644 --- a/.agents/workflows/compiler-apk.md +++ b/.agents/workflows/compiler-apk.md @@ -27,18 +27,23 @@ cp app/build/outputs/apk/S_12_XY_U_Standard_/debug/app-S_12_XY_U_Standard_-10_08 echo "✅ APK 已經複製到專案根目錄的 outputs 資料夾中" ``` -## 步驟三:透過 Windows ADB 安裝到手機 / 機台 -利用 WSL 串接 Windows 端的 ADB,將熱騰騰的 APK 覆蓋安裝到目前連接的設備上。 +## 步驟三:安裝並啟動 App (如果有連接設備) +利用 WSL 串接 Windows 端的 ADB,偵測到設備後才執行安裝與啟動。 ```bash // turbo -/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe install -r -d -t /home/mama/projects/TaiwanStar_general_size_s_Chengwai/outputs/TaiwanStar_debug.apk -``` +ADB_PATH="/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe" +APK_PATH="/home/mama/projects/TaiwanStar_general_size_s_Chengwai/outputs/TaiwanStar_debug.apk" -## 步驟四:自動啟動 App -安裝完成後,直接在手機上喚醒並啟動應用程式 (`HomeActivity`)。 +# 檢查是否有設備連接 +DEVICE_COUNT=$($ADB_PATH devices | grep -v "List of devices" | grep "device" | wc -l) -```bash -// turbo -/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe shell am start -n com.unibuy.smartdevice/com.unibuy.smartdevice.HomeActivity +if [ "$DEVICE_COUNT" -gt 0 ]; then + echo "📱 偵測到 $DEVICE_COUNT 個設備,開始安裝..." + $ADB_PATH install -r -d -t $APK_PATH + echo "🚀 安裝完成,正在啟動 App..." + $ADB_PATH shell am start -n com.unibuy.smartdevice/com.unibuy.smartdevice.HomeActivity +else + echo "⚠️ 未偵測到連接的手機或設備,跳過安裝與啟動步驟。" +fi ``` diff --git a/app/src/main/java/com/unibuy/smartdevice/SchedulerAbstract.java b/app/src/main/java/com/unibuy/smartdevice/SchedulerAbstract.java index f0cacc2..ca3dcd1 100644 --- a/app/src/main/java/com/unibuy/smartdevice/SchedulerAbstract.java +++ b/app/src/main/java/com/unibuy/smartdevice/SchedulerAbstract.java @@ -157,15 +157,17 @@ public abstract class SchedulerAbstract implements Runnable { } public void shutdown() { - if (scheduler != null && !scheduler.isShutdown()) { - scheduler.shutdown(); + // 使用本地變數快照避免 volatile 欄位在多執行緒間被 initScheduler() 設為 null 導致 NPE + ScheduledExecutorService s = scheduler; + if (s != null && !s.isShutdown()) { + s.shutdown(); try { - if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) { - scheduler.shutdownNow(); - scheduler = null; + if (!s.awaitTermination(1, TimeUnit.SECONDS)) { + s.shutdownNow(); } } catch (InterruptedException e) { - scheduler.shutdownNow(); + s.shutdownNow(); + } finally { scheduler = null; } } diff --git a/app/src/main/java/com/unibuy/smartdevice/controller/DevController.java b/app/src/main/java/com/unibuy/smartdevice/controller/DevController.java index 71f148a..b7836e3 100644 --- a/app/src/main/java/com/unibuy/smartdevice/controller/DevController.java +++ b/app/src/main/java/com/unibuy/smartdevice/controller/DevController.java @@ -35,6 +35,7 @@ public abstract class DevController extends HandlerMainScheduler { public void getReadBuffer(ReadBufferOnScheduler readBufferOnScheduler, long delayMillis) { if (this.isRun()) { + readBufferOnScheduler.shutdown(); readBufferOnScheduler.setDevController(this); readBufferOnScheduler.start(delayMillis, TimeUnit.MILLISECONDS); } @@ -129,8 +130,9 @@ public abstract class DevController extends HandlerMainScheduler { } public List getReadBufferByMax() { - int size = readBuffers.size(); - return readBuffers; + List buffers = new ArrayList<>(readBuffers); + readBuffers.clear(); + return buffers; } public byte[] getReadBuffer() { diff --git a/app/src/main/java/com/unibuy/smartdevice/controller/DevXinYuanController.java b/app/src/main/java/com/unibuy/smartdevice/controller/DevXinYuanController.java index 72f16bb..fc73d2c 100644 --- a/app/src/main/java/com/unibuy/smartdevice/controller/DevXinYuanController.java +++ b/app/src/main/java/com/unibuy/smartdevice/controller/DevXinYuanController.java @@ -19,6 +19,7 @@ import com.unibuy.smartdevice.exception.LogsParseException; import com.unibuy.smartdevice.exception.LogsSettingEmptyException; import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException; import com.unibuy.smartdevice.external.HttpAPI; +import com.unibuy.smartdevice.external.mqtt.MqttManager; import com.unibuy.smartdevice.tools.HandlerMain; import com.unibuy.smartdevice.ui.FontendActivity; @@ -27,7 +28,6 @@ import java.util.Map; public class DevXinYuanController extends DevController { private Logs logs; - private HttpAPI httpAPI; private Rs232Dev rs232Dev; private DevXinYuan devXinYuan; private static int temperature = 0; @@ -95,23 +95,13 @@ public class DevXinYuanController extends DevController { // optional delay so it doesn't collide with real events Thread.sleep(5000); - if (httpAPI == null) { - httpAPI = new HttpAPI(getHandlerMain()); - } + MqttManager.getInstance().publishError(slot, "0402"); + logs.info("Simulated VMC_04_BUY_STATUS (0402) published to MQTT."); -// runOnUiThread(() -> Toast.makeText(MyApp.getInstance(), "[Fake] 準備送出: slot=" + slot , Toast.LENGTH_SHORT).show()); - - httpAPI.errorPost(slot, "0402"); - -// runOnUiThread(() -> Toast.makeText(MyApp.getInstance(), "[Fake] 已送出: slot=" + slot , Toast.LENGTH_SHORT).show()); } catch (InterruptedException ie) { logs.warning(ie); - } catch (LogsSettingEmptyException | LogsParseException e) { - logs.warning(e); - runOnUiThread(() -> Toast.makeText(MyApp.getInstance(), "[Fake] 發送失敗: " + e.getMessage(), Toast.LENGTH_LONG).show()); } catch (Exception e) { logs.warning(e); - runOnUiThread(() -> Toast.makeText(MyApp.getInstance(), "[Fake] 例外: " + e.getClass().getSimpleName(), Toast.LENGTH_LONG).show()); } }).start(); } @@ -191,48 +181,29 @@ public class DevXinYuanController extends DevController { rs232Dev.send(devXinYuan.sendACK()); break; case VMC_04_BUY_STATUS: - new Thread(() -> { try { - if (httpAPI == null) httpAPI = new HttpAPI(getHandlerMain()); - int slot = ((vmcData[2] & 0xFF) << 8) | (vmcData[3] & 0xFF); -// String statusMessage = PortTools.showHex(new byte[]{vmcCmd[0],vmcData[1]}).replace(" ", ""); + byte statusByte = vmcData[1]; + String statusMessage = String.format("%02X%02X", vmcCmd[0] & 0xFF, statusByte & 0xFF); - String statusMessage = String.format("%02X%02X", vmcCmd[0] & 0xFF, vmcData[1] & 0xFF); - -// runOnUiThread(() -> -// Toast.makeText(MyApp.getInstance(), "傳送訊息"+statusMessage, Toast.LENGTH_LONG).show() -// ); - - //最後一筆0402得到貨道是0可能覆蓋 //所以抓取前面三筆出貨前的資料 - if(slot>0){ - sllloot= slot; - } - //放這邊因為最後一筆 出貨成功跟微波爐請取餐 可能排成造成的無法傳送API後台 - //也許原因是因為速度過快覆蓋 所以而外拉出來一個方法來處理 0402 - if(statusMessage.equals("0402")){ - - simulateVmc04BuyStatus(sllloot); + if (slot > 0) { + sllloot = slot; } - httpAPI.errorPost(slot, statusMessage); + // 依照要求,直接上報所有狀態 (包含 0401 處理中, 0402/0424 成功) + MqttManager.getInstance().publishError(slot, statusMessage); + logs.info("VMC_04_BUY_STATUS MQTT status published: " + statusMessage + " (slot: " + slot + ")"); - logs.info("VMC_04_BUY_STATUS errorPost sent."); - - } catch (LogsSettingEmptyException | LogsParseException e) { - logs.warning(e); - runOnUiThread(() -> - Toast.makeText(MyApp.getInstance(), "錯誤: " + e.getMessage(), Toast.LENGTH_SHORT).show() - ); } catch (Exception e) { - logs.warning(e); // 不再丟出 RuntimeException + logs.warning(e); } }).start(); - - /// //////////////// - + publishReadBuffer(PortTools.concatByteArrays(vmcCmd[0], vmcData[1])); + rs232Dev.send(devXinYuan.sendACK()); + break; + case VMC_11_GIVE_PRODUCT_INFO: publishReadBuffer(PortTools.concatByteArrays(vmcCmd[0], vmcData[1])); rs232Dev.send(devXinYuan.sendACK()); break; diff --git a/app/src/main/java/com/unibuy/smartdevice/external/mqtt/MqttManager.java b/app/src/main/java/com/unibuy/smartdevice/external/mqtt/MqttManager.java index fc597e5..ef339bc 100644 --- a/app/src/main/java/com/unibuy/smartdevice/external/mqtt/MqttManager.java +++ b/app/src/main/java/com/unibuy/smartdevice/external/mqtt/MqttManager.java @@ -28,11 +28,14 @@ public class MqttManager { private static MqttManager instance; private Mqtt3AsyncClient client; private final Gson gson = new Gson(); + private com.unibuy.smartdevice.exception.Logs logs; private String serialNo; private MqttCommandListener commandListener; private ScheduledExecutorService heartbeatExecutor; - private MqttManager() {} + private MqttManager() { + logs = new com.unibuy.smartdevice.exception.Logs(getClass()); + } public static synchronized MqttManager getInstance() { if (instance == null) { @@ -47,7 +50,7 @@ public class MqttManager { public void connect(String machineId, String apiToken) { if (machineId == null || machineId.isEmpty() || apiToken == null || apiToken.isEmpty()) { - Log.e(TAG, "Cannot connect to MQTT: machineId or apiToken is empty"); + logs.info("Cannot connect to MQTT: machineId or apiToken is empty"); return; } @@ -55,7 +58,7 @@ public class MqttManager { if (client != null && serialNo != null && serialNo.equals(machineId)) { MqttClientState state = client.getState(); if (state != MqttClientState.DISCONNECTED) { - Log.i(TAG, "MQTT is already " + state + " for: " + machineId); + logs.info("MQTT is already " + state + " for: " + machineId); return; } } @@ -82,15 +85,15 @@ public class MqttManager { .serverPort(443) .useSslWithDefaultConfig() .webSocketConfig() - .serverPath("/mqtt") - .applyWebSocketConfig() + .serverPath("/mqtt") + .applyWebSocketConfig() .simpleAuth() - .username(machineId) - .password(apiToken.getBytes(StandardCharsets.UTF_8)) - .applySimpleAuth() + .username(machineId) + .password(apiToken.getBytes(StandardCharsets.UTF_8)) + .applySimpleAuth() .automaticReconnectWithDefaultConfig() .addConnectedListener(context -> { - Log.i(TAG, "MQTT Connection established/recovered"); + logs.info("MQTT Connection established/recovered"); publishStatus("online"); subscribeToCommandTopic(); startHeartbeatScheduler(); @@ -99,19 +102,19 @@ public class MqttManager { client.connectWith() .cleanSession(true) - .keepAlive(30) // Broker 判斷斷線 = 30 × 1.5 = 45 秒,加快 LWT 觸發 + .keepAlive(30) // Broker 判斷斷線 = 30 × 1.5 = 45 秒,加快 LWT 觸發 .willPublish() - .topic(willTopic) - .payload(willPayload.getBytes(StandardCharsets.UTF_8)) - .qos(MqttQos.AT_LEAST_ONCE) - .retain(true) - .applyWillPublish() + .topic(willTopic) + .payload(willPayload.getBytes(StandardCharsets.UTF_8)) + .qos(MqttQos.AT_LEAST_ONCE) + .retain(true) + .applyWillPublish() .send() .whenComplete((connAck, throwable) -> { if (throwable != null) { - Log.e(TAG, "MQTT Initial connection failed: " + throwable.getMessage(), throwable); + logs.info("MQTT Initial connection failed: " + throwable.getMessage()); } else { - Log.i(TAG, "MQTT Initial connection successful"); + logs.info("MQTT Initial connection successful"); } }); } @@ -125,7 +128,8 @@ public class MqttManager { try { String ver = "0"; try { - PackageInfo packageInfo = MyApp.getInstance().getPackageManager().getPackageInfo(MyApp.getInstance().getPackageName(), 0); + PackageInfo packageInfo = MyApp.getInstance().getPackageManager() + .getPackageInfo(MyApp.getInstance().getPackageName(), 0); ver = packageInfo.versionName; } catch (PackageManager.NameNotFoundException e) { // ignore @@ -133,7 +137,7 @@ public class MqttManager { int temperature = DevXinYuanController.getTemperature(); publishHeartbeat(ver, temperature); } catch (Exception e) { - Log.e(TAG, "Error in heartbeat scheduler", e); + logs.info("Error in heartbeat scheduler: " + e.getMessage()); } }, 0, 60, TimeUnit.SECONDS); } @@ -157,9 +161,9 @@ public class MqttManager { * Broker 不會觸發 LWT 遺囑訊息,因此可避免管理後台誤報「離線」。 * * 呼叫流程: - * 1. publishStatus("restarting") — 告知後台為計畫性重啟 - * 2. 等發布完成後呼叫 client.disconnect() — 正常斷線,LWT 不觸發 - * 3. 執行 onComplete callback — 呼叫方在此執行 killProcess() + * 1. publishStatus("restarting") — 告知後台為計畫性重啟 + * 2. 等發布完成後呼叫 client.disconnect() — 正常斷線,LWT 不觸發 + * 3. 執行 onComplete callback — 呼叫方在此執行 killProcess() * * @param onComplete 斷線完成後的回呼,用來執行 killProcess/System.exit 等後續動作 */ @@ -171,15 +175,16 @@ public class MqttManager { if (client == null || !client.getState().isConnected()) { // 如果本來就沒有連線,直接執行後續動作 - Log.i(TAG, "gracefulDisconnect: MQTT not connected, skip directly."); - if (onComplete != null) onComplete.run(); + logs.info("gracefulDisconnect: MQTT not connected, skip directly."); + if (onComplete != null) + onComplete.run(); return; } // 發布 restarting 狀態(retain=true,後台可感知這是計畫性重啟) String payload = "{\"status\":\"restarting\"}"; String topic = "machine/" + serialNo + "/status"; - Log.i(TAG, "gracefulDisconnect: publishing restarting status..."); + logs.info("gracefulDisconnect: publishing restarting status..."); client.publishWith() .topic(topic) @@ -189,56 +194,59 @@ public class MqttManager { .send() .whenComplete((publishResult, pubThrowable) -> { if (pubThrowable != null) { - Log.w(TAG, "gracefulDisconnect: failed to publish restarting status, disconnecting anyway."); + logs.info("gracefulDisconnect: failed to publish restarting status, disconnecting anyway."); } else { - Log.i(TAG, "gracefulDisconnect: restarting status published."); + logs.info("gracefulDisconnect: restarting status published."); } // 無論發布成否,都執行正常 DISCONNECT(避免 LWT 觸發) client.disconnect() .whenComplete((v, disconnThrowable) -> { if (disconnThrowable != null) { - Log.w(TAG, "gracefulDisconnect: disconnect error: " + disconnThrowable.getMessage()); + logs.info("gracefulDisconnect: disconnect error: " + disconnThrowable.getMessage()); } else { - Log.i(TAG, "gracefulDisconnect: disconnected cleanly, LWT suppressed."); + logs.info("gracefulDisconnect: disconnected cleanly, LWT suppressed."); } // 回呼呼叫方,可以安全地執行 killProcess - if (onComplete != null) onComplete.run(); + if (onComplete != null) + onComplete.run(); }); }); } private void subscribeToCommandTopic() { - if (client == null || serialNo == null) return; - + if (client == null || serialNo == null) + return; + String commandTopic = "machine/" + serialNo + "/command"; client.subscribeWith() .topicFilter(commandTopic) .qos(MqttQos.AT_LEAST_ONCE) .callback(publish -> { String payloadString = new String(publish.getPayloadAsBytes(), StandardCharsets.UTF_8); - Log.i(TAG, "Received command payload: " + payloadString); + logs.info("Received command payload: " + payloadString); try { CommandPayload command = gson.fromJson(payloadString, CommandPayload.class); if (commandListener != null && command != null) { commandListener.onCommandReceived(command); } } catch (Exception e) { - Log.e(TAG, "Error parsing command payload", e); + logs.info("Error parsing command payload: " + e.getMessage()); } }) .send() .whenComplete((subAck, throwable) -> { if (throwable != null) { - Log.e(TAG, "Failed to subscribe to command topic", throwable); + logs.info("Failed to subscribe to command topic: " + throwable.getMessage()); } else { - Log.i(TAG, "Subscribed to " + commandTopic); + logs.info("Subscribed to " + commandTopic); } }); } public void publishStatus(String status) { - if (client == null || !client.getState().isConnected()) return; + if (client == null || !client.getState().isConnected()) + return; String payload = "{\"status\":\"" + status + "\"}"; String topic = "machine/" + serialNo + "/status"; @@ -251,15 +259,16 @@ public class MqttManager { .send() .whenComplete((publishResult, throwable) -> { if (throwable != null) { - Log.e(TAG, "Failed to publish status", throwable); + logs.info("Failed to publish status: " + throwable.getMessage()); } else { - Log.d(TAG, "Status published: " + payload); + logs.info("Status published: " + payload); } }); } public void publishHeartbeat(String firmwareVersion, int temperature) { - if (client == null || !client.getState().isConnected()) return; + if (client == null || !client.getState().isConnected()) + return; HeartbeatPayload payloadObj = new HeartbeatPayload(firmwareVersion, temperature); String payload = gson.toJson(payloadObj); @@ -272,9 +281,9 @@ public class MqttManager { .send() .whenComplete((publishResult, throwable) -> { if (throwable != null) { - Log.e(TAG, "Failed to publish heartbeat", throwable); + logs.info("Failed to publish heartbeat: " + throwable.getMessage()); } else { - Log.d(TAG, "Heartbeat published: " + payload); + logs.info("Heartbeat published: " + payload); } }); } @@ -282,13 +291,18 @@ public class MqttManager { public void publishCommandAck(String commandId, String result, String message) { publishCommandAck(new CommandAckPayload(commandId, result, message)); } - + public void publishCommandAck(CommandAckPayload ack) { - if (client == null || !client.getState().isConnected()) return; + if (client == null || !client.getState().isConnected()) { + if (logs != null) logs.info("Cannot publish ACK: MQTT not connected"); + return; + } String payload = gson.toJson(ack); String topic = "machine/" + serialNo + "/command/ack"; + logs.info("Publishing Command ACK to [" + topic + "]: " + payload); + client.publishWith() .topic(topic) .payload(payload.getBytes(StandardCharsets.UTF_8)) @@ -296,15 +310,16 @@ public class MqttManager { .send() .whenComplete((publishResult, throwable) -> { if (throwable != null) { - Log.e(TAG, "Failed to publish command ack", throwable); + logs.info("Failed to publish command ack: " + throwable.getMessage()); } else { - Log.d(TAG, "Command ACK published: " + payload); + logs.info("Command ACK published: " + payload); } }); } public void publishError(int tid, String errorCode) { - if (client == null || !client.getState().isConnected()) return; + if (client == null || !client.getState().isConnected()) + return; ErrorPayload payloadObj = new ErrorPayload(tid, errorCode); String payload = gson.toJson(payloadObj); @@ -317,9 +332,9 @@ public class MqttManager { .send() .whenComplete((publishResult, throwable) -> { if (throwable != null) { - Log.e(TAG, "Failed to publish error to MQTT", throwable); + logs.info("Failed to publish error to MQTT: " + throwable.getMessage()); } else { - Log.i(TAG, "Hardware error published to " + topic + ": " + payload); + logs.info("Hardware error published to " + topic + ": " + payload); } }); } @@ -334,7 +349,7 @@ public class MqttManager { */ public void publishTransactionFinalize(TransactionFinalizePayload payload) { if (client == null || !client.getState().isConnected()) { - Log.e(TAG, "Cannot publish finalize: MQTT not connected"); + logs.info("Cannot publish finalize: MQTT not connected"); return; } @@ -348,9 +363,9 @@ public class MqttManager { .send() .whenComplete((publishResult, throwable) -> { if (throwable != null) { - Log.e(TAG, "Failed to publish transaction finalize", throwable); + logs.info("Failed to publish transaction finalize: " + throwable.getMessage()); } else { - Log.i(TAG, "Transaction finalize published to " + topic + ": " + payloadJson); + logs.info("Transaction finalize published to " + topic + ": " + payloadJson); } }); } diff --git a/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java b/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java index 187ee2d..0c36896 100644 --- a/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java +++ b/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java @@ -33,7 +33,6 @@ import java.util.Date; import java.util.Locale; import java.util.UUID; - /** * Foreground Service,負責持有 WakeLock 並管理 MQTT 連線。 * 確保販賣機台在關閉螢幕時也能維持通訊。 @@ -43,11 +42,13 @@ public class MqttService extends Service { private static final String TAG = "MqttService"; private static final String CHANNEL_ID = "MqttServiceChannel"; private PowerManager.WakeLock wakeLock; + private com.unibuy.smartdevice.exception.Logs logs; @Override public void onCreate() { super.onCreate(); - Log.i(TAG, "MqttService Created"); + logs = new com.unibuy.smartdevice.exception.Logs(getClass()); + logs.info("MqttService Created"); // 1. 建立 Notification Channel createNotificationChannel(); @@ -71,7 +72,7 @@ public class MqttService extends Service { if (powerManager != null) { wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "StarCloud:MqttWakeLock"); wakeLock.acquire(); - Log.i(TAG, "WakeLock acquired"); + logs.info("WakeLock acquired"); } } @@ -80,48 +81,52 @@ public class MqttService extends Service { String machineId = intent != null ? intent.getStringExtra("machineId") : null; String apiToken = intent != null ? intent.getStringExtra("apiToken") : null; - Log.i(TAG, "MqttService onStartCommand with machineId: " + machineId); + logs.info("MqttService onStartCommand with machineId: " + machineId); if (machineId != null && apiToken != null) { MqttManager mqttManager = MqttManager.getInstance(); mqttManager.setCommandListener(command -> { String cmd = command.getCommand(); String cmdId = command.getCommand_id(); - Log.i(TAG, "Received MQTT command: " + cmd + " (ID: " + cmdId + ")"); + logs.info("Received MQTT command: " + cmd + " (ID: " + cmdId + ")"); try { if ("reboot".equals(cmd)) { - Log.i(TAG, "Executing remote reboot command..."); + logs.info("Executing remote reboot command..."); mqttManager.publishCommandAck(cmdId, "success", "Rebooting soon..."); // 🚩 優雅重啟:ACK 發出後先 gracefulDisconnect(不觸發 LWT),再 killProcess // 重啟到初始畫面 (HomeActivity);按「購物去」再經 RestartActivity 釋放硬體,不会再次 killProcess - new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> - mqttManager.gracefulDisconnect(() -> { - new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { - Intent restartIntent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class); - restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(restartIntent); - android.os.Process.killProcess(android.os.Process.myPid()); - System.exit(0); - }); - }) - , 500); // 0.5 秒確保 ACK 先發出 + new android.os.Handler(android.os.Looper.getMainLooper()) + .postDelayed(() -> mqttManager.gracefulDisconnect(() -> { + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + Intent restartIntent = new Intent(getApplicationContext(), + com.unibuy.smartdevice.HomeActivity.class); + restartIntent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(restartIntent); + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(0); + }); + }), 500); // 0.5 秒確保 ACK 先發出 } else if ("lock".equals(cmd)) { - Log.i(TAG, "Executing remote lock command..."); - com.unibuy.smartdevice.database.SettingsDao settingsDao = new com.unibuy.smartdevice.database.SettingsDao(getApplicationContext()); + logs.info("Executing remote lock command..."); + com.unibuy.smartdevice.database.SettingsDao settingsDao = new com.unibuy.smartdevice.database.SettingsDao( + getApplicationContext()); settingsDao.setMachineLocked(true); settingsDao.close(); mqttManager.publishCommandAck(cmdId, "success", "Machine locked"); new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { - Intent lockIntent = new Intent(getApplicationContext(), com.unibuy.smartdevice.ui.MaintenanceActivity.class); + Intent lockIntent = new Intent(getApplicationContext(), + com.unibuy.smartdevice.ui.MaintenanceActivity.class); lockIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(lockIntent); }); } else if ("unlock".equals(cmd)) { - Log.i(TAG, "Executing remote unlock command..."); - com.unibuy.smartdevice.database.SettingsDao settingsDao = new com.unibuy.smartdevice.database.SettingsDao(getApplicationContext()); + logs.info("Executing remote unlock command..."); + com.unibuy.smartdevice.database.SettingsDao settingsDao = new com.unibuy.smartdevice.database.SettingsDao( + getApplicationContext()); settingsDao.setMachineLocked(false); settingsDao.close(); @@ -129,41 +134,46 @@ public class MqttService extends Service { // 🚩 優雅重啟:先 gracefulDisconnect(不觸發 LWT),再 killProcess // 解鎖後重啟到初始畫面 (HomeActivity) - new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> - mqttManager.gracefulDisconnect(() -> { - new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { - Intent restartIntent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class); - restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(restartIntent); - android.os.Process.killProcess(android.os.Process.myPid()); - System.exit(0); - }); - }) - , 500); + new android.os.Handler(android.os.Looper.getMainLooper()) + .postDelayed(() -> mqttManager.gracefulDisconnect(() -> { + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + Intent restartIntent = new Intent(getApplicationContext(), + com.unibuy.smartdevice.HomeActivity.class); + restartIntent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(restartIntent); + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(0); + }); + }), 500); } else if ("nfc_reset".equals(cmd)) { - Log.i(TAG, "Executing remote NFC reset command..."); + logs.info("Executing remote NFC reset command..."); try { - com.unibuy.smartdevice.devices.Rs232Dev comPort2 = new com.unibuy.smartdevice.devices.Rs232Dev(com.unibuy.smartdevice.MyApp.getInstance().getComPort2ByNFCPay()); - com.unibuy.smartdevice.devices.DevNFCPay devNFCPay = new com.unibuy.smartdevice.devices.DevNFCPay(getApplicationContext()); + com.unibuy.smartdevice.devices.Rs232Dev comPort2 = new com.unibuy.smartdevice.devices.Rs232Dev( + com.unibuy.smartdevice.MyApp.getInstance().getComPort2ByNFCPay()); + com.unibuy.smartdevice.devices.DevNFCPay devNFCPay = new com.unibuy.smartdevice.devices.DevNFCPay( + getApplicationContext()); comPort2.send(devNFCPay.reset()); mqttManager.publishCommandAck(cmdId, "success", "NFC reset command sent"); } catch (Exception e) { - Log.e(TAG, "NFC reset failed: " + e.getMessage()); + logs.info("NFC reset failed: " + e.getMessage()); mqttManager.publishCommandAck(cmdId, "failed", "NFC reset failed: " + e.getMessage()); } } else if ("nfc_checkout".equals(cmd)) { - Log.i(TAG, "Executing remote NFC checkout command..."); + logs.info("Executing remote NFC checkout command..."); try { - com.unibuy.smartdevice.devices.Rs232Dev comPort2 = new com.unibuy.smartdevice.devices.Rs232Dev(com.unibuy.smartdevice.MyApp.getInstance().getComPort2ByNFCPay()); - com.unibuy.smartdevice.devices.DevNFCPay devNFCPay = new com.unibuy.smartdevice.devices.DevNFCPay(getApplicationContext()); + com.unibuy.smartdevice.devices.Rs232Dev comPort2 = new com.unibuy.smartdevice.devices.Rs232Dev( + com.unibuy.smartdevice.MyApp.getInstance().getComPort2ByNFCPay()); + com.unibuy.smartdevice.devices.DevNFCPay devNFCPay = new com.unibuy.smartdevice.devices.DevNFCPay( + getApplicationContext()); comPort2.send(devNFCPay.checkout()); mqttManager.publishCommandAck(cmdId, "success", "NFC checkout command sent"); } catch (Exception e) { - Log.e(TAG, "NFC checkout failed: " + e.getMessage()); + logs.info("NFC checkout failed: " + e.getMessage()); mqttManager.publishCommandAck(cmdId, "failed", "NFC checkout failed: " + e.getMessage()); } } else if ("change".equals(cmd)) { - Log.i(TAG, "Executing remote change command..."); + logs.info("Executing remote change command..."); try { int amount = 0; if (command.getPayload() != null && command.getPayload().isJsonObject()) { @@ -178,33 +188,100 @@ public class MqttService extends Service { } int finalAmount = amount; - com.unibuy.smartdevice.tools.CashPayoutManager payoutManager = new com.unibuy.smartdevice.tools.CashPayoutManager(getApplicationContext(), new com.unibuy.smartdevice.tools.CashPayoutManager.PayoutListener() { - @Override - public void onProgress(String message) { - Log.d(TAG, "Payout Progress: " + message); - } + com.unibuy.smartdevice.tools.CashPayoutManager payoutManager = new com.unibuy.smartdevice.tools.CashPayoutManager( + getApplicationContext(), + new com.unibuy.smartdevice.tools.CashPayoutManager.PayoutListener() { + @Override + public void onProgress(String message) { + logs.debug("Payout Progress: " + message); + } - @Override - public void onSuccess() { - Log.i(TAG, "Remote payout success for amount: " + finalAmount); - mqttManager.publishCommandAck(cmdId, "success", "Payout completed: " + finalAmount); - } + @Override + public void onSuccess() { + logs.info("Remote payout success for amount: " + finalAmount); + mqttManager.publishCommandAck(cmdId, "success", + "Payout completed: " + finalAmount); + } - @Override - public void onFailure(String error) { - Log.e(TAG, "Remote payout failed: " + error); - mqttManager.publishCommandAck(cmdId, "failed", "Payout failed: " + error); - } - }); + @Override + public void onFailure(String error) { + logs.info("Remote payout failed: " + error); + mqttManager.publishCommandAck(cmdId, "failed", "Payout failed: " + error); + } + }); payoutManager.startPayout(amount); // 注意:ACK 會在 PayoutListener 的回調中發送,不在此處發送。 } catch (Exception e) { - Log.e(TAG, "Failed to parse or start change command: " + e.getMessage()); + logs.info("Failed to parse or start change command: " + e.getMessage()); mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage()); } + } else if ("update_inventory".equals(cmd)) { + logs.info("Executing update_inventory command..."); + try { + int slotNo = -1; + int stock = -1; + String expiryDate = null; + String batchNo = null; + + if (command.getPayload() != null && command.getPayload().isJsonObject()) { + com.google.gson.JsonObject payloadObj = command.getPayload().getAsJsonObject(); + if (payloadObj.has("slot_no")) + slotNo = payloadObj.get("slot_no").getAsInt(); + if (payloadObj.has("stock")) + stock = payloadObj.get("stock").getAsInt(); + if (payloadObj.has("expiry_date") && !payloadObj.get("expiry_date").isJsonNull()) + expiryDate = payloadObj.get("expiry_date").getAsString(); + if (payloadObj.has("batch_no") && !payloadObj.get("batch_no").isJsonNull()) + batchNo = payloadObj.get("batch_no").getAsString(); + } + + if (slotNo <= 0 || stock < 0) { + throw new Exception("Invalid parameters: slot_no=" + slotNo + ", stock=" + stock); + } + + // 更新本地資料庫 + int field = com.unibuy.smartdevice.devices.SlotField.VMC.getField(); + com.unibuy.smartdevice.structure.SlotStructure slotData = MyApp.getInstance() + .getSlotData(field, slotNo); + if (slotData != null) { + slotData.setCount(stock); + + // 更新選填欄位至 otherData + org.json.JSONObject otherData = slotData.getOtherData(); + if (otherData == null) + otherData = new org.json.JSONObject(); + try { + if (expiryDate != null) + otherData.put("expiry_date", expiryDate); + if (batchNo != null) + otherData.put("batch_no", batchNo); + } catch (org.json.JSONException e) { + logs.info("Failed to update JSON otherData: " + e.getMessage()); + } + slotData.setOtherData(otherData); + + com.unibuy.smartdevice.database.SlotsDao slotsDao = new com.unibuy.smartdevice.database.SlotsDao( + getApplicationContext()); + slotsDao.deleteAll(); + slotsDao.insertAll(MyApp.getInstance().getSlotMap()); + slotsDao.close(); + + logs.info("Inventory updated: slot=" + slotNo + ", stock=" + stock); + mqttManager + .publishCommandAck(new com.unibuy.smartdevice.structure.mqtt.CommandAckPayload( + cmdId, "success", "Inventory updated successfully", + String.valueOf(stock))); + } else { + throw new Exception("Slot not found: " + slotNo); + } + } catch (Exception e) { + logs.info("Failed to update inventory: " + e.getMessage()); + mqttManager.publishCommandAck(cmdId, "failed", + "Update inventory failed: " + e.getMessage()); + } } else if ("dispense".equals(cmd)) { - Log.i(TAG, "Executing remote dispense command..."); + logs.info("Executing remote dispense command..."); try { int slot = -1; if (command.getPayload() != null && command.getPayload().isJsonObject()) { @@ -221,89 +298,40 @@ public class MqttService extends Service { } int finalSlot = slot; - com.unibuy.smartdevice.tools.ProductDispenseManager dispenseManager = new com.unibuy.smartdevice.tools.ProductDispenseManager(getApplicationContext(), new com.unibuy.smartdevice.tools.ProductDispenseManager.DispenseListener() { - @Override - public void onProgress(String message) { - Log.d(TAG, "Dispense Progress: " + message); - } - - @Override - public void onSuccess() { - Log.i(TAG, "Remote dispense success for slot: " + finalSlot + " (cmdId: " + cmdId + ")"); - mqttManager.publishCommandAck(cmdId, "success", "Product dispensed from slot: " + finalSlot); - - // [修正] 使用 TransactionFinalizeBuilder 走完整交易流程,確保後台有訂單紀錄 - try { - // Step 1: 從記憶體中找到對應貨道的商品資料 - SlotStructure slotStructure = null; - for (SlotField slotField : SlotField.values()) { - try { - slotStructure = MyApp.getInstance().getSlotData(slotField.getField(), finalSlot); - break; // 找到就停止 - } catch (LogsEmptyException ignored) { - // 此 field 找不到,繼續找下一個 - } + com.unibuy.smartdevice.tools.ProductDispenseManager dispenseManager = new com.unibuy.smartdevice.tools.ProductDispenseManager( + getApplicationContext(), + logs, + new com.unibuy.smartdevice.tools.ProductDispenseManager.DispenseListener() { + @Override + public void onProgress(String message) { + logs.debug("Dispense Progress: " + message); } - if (slotStructure == null) { - Log.e(TAG, "Remote dispense finalize failed: slot " + finalSlot + " not found in memory"); - return; + @Override + public void onSuccess() { + if (logs != null) logs.info("Remote dispense success for slot: " + finalSlot + " (cmdId: " + cmdId + ")"); + // 依據使用者測試結果:回傳簡潔的 ACK (僅含 command_id 與 result) 即可成功更改狀態 + mqttManager.publishCommandAck(new com.unibuy.smartdevice.structure.mqtt.CommandAckPayload(cmdId, "success")); } - // Step 2: 清空並重新設定 BuyList(模擬使用者點選此商品) - MyApp.getInstance().getBuyList().clear(); - MyApp.getInstance().getBuyList().add( - new BuyStructure(slotStructure.getField(), finalSlot, slotStructure.getProduct(), 1) - ); - - // Step 3: 設定 ReportFlowInfoData(payment_type=100 代表遠端管理出貨) - ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData(); - flowInfo.setFlowType(100); // 遠端管理出貨 - flowInfo.setTotalPrice(0); // 免費出貨 - flowInfo.setGiveChange(0); - flowInfo.setUsePoints(0); - flowInfo.setFlowBarCode(""); - flowInfo.setFlowSendInfo(""); // 無支付請求封包 - flowInfo.setFlowRequestInfo(""); // 無支付回應封包 - flowInfo.setInvoiceInfo(""); - - // Step 4: 用 Builder 組裝並送出 Finalize Payload - TransactionFinalizeBuilder builder = new TransactionFinalizeBuilder(); - int remaining = slotStructure.getCount() - 1; - int productIdInt = 0; - try { productIdInt = Integer.parseInt(slotStructure.getProduct().getProductID()); } catch (Exception ignored) {} - builder.addDispenseRecord(finalSlot, productIdInt, 0, 0, true, remaining); - - MqttManager.getInstance().publishTransactionFinalize(builder.build()); - Log.i(TAG, "[Remote Dispense] Transaction finalize sent via TransactionFinalizeBuilder, slot=" + finalSlot); - - // 清空 BuyList 避免影響後續正常交易 - MyApp.getInstance().getBuyList().clear(); - - } catch (Exception e) { - Log.e(TAG, "Failed to send finalize for remote dispense: " + e.getMessage(), e); - } - } - - - @Override - public void onFailure(String error) { - Log.e(TAG, "Remote dispense failed: " + error); - mqttManager.publishCommandAck(cmdId, "failed", "Dispense failed: " + error); - } - }); + @Override + public void onFailure(String error) { + if (logs != null) logs.info("Remote dispense failed: " + error); + mqttManager.publishCommandAck(new com.unibuy.smartdevice.structure.mqtt.CommandAckPayload(cmdId, "failed")); + } + }); dispenseManager.startDispense(slot); } catch (Exception e) { - Log.e(TAG, "Failed to parse or start dispense command: " + e.getMessage()); + logs.info("Failed to parse or start dispense command: " + e.getMessage()); mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage()); } } else { - Log.w(TAG, "Unknown MQTT command: " + cmd); + logs.info("Unknown MQTT command: " + cmd); mqttManager.publishCommandAck(cmdId, "failed", "Unknown command"); } } catch (Exception e) { - Log.e(TAG, "Error executing MQTT command: " + e.getMessage(), e); + logs.info("Error executing MQTT command: " + e.getMessage()); mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage()); } }); @@ -317,10 +345,10 @@ public class MqttService extends Service { @Override public void onDestroy() { super.onDestroy(); - Log.i(TAG, "MqttService Destroyed"); + logs.info("MqttService Destroyed"); if (wakeLock != null && wakeLock.isHeld()) { wakeLock.release(); - Log.i(TAG, "WakeLock released"); + logs.info("WakeLock released"); } } @@ -335,8 +363,7 @@ public class MqttService extends Service { NotificationChannel serviceChannel = new NotificationChannel( CHANNEL_ID, "MQTT Service Channel", - NotificationManager.IMPORTANCE_LOW - ); + NotificationManager.IMPORTANCE_LOW); NotificationManager manager = getSystemService(NotificationManager.class); if (manager != null) { manager.createNotificationChannel(serviceChannel); diff --git a/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/CommandAckPayload.java b/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/CommandAckPayload.java index 9b77e23..6be11b1 100644 --- a/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/CommandAckPayload.java +++ b/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/CommandAckPayload.java @@ -6,6 +6,11 @@ public class CommandAckPayload { private String message; private String stock; + public CommandAckPayload(String command_id, String result) { + this.command_id = command_id; + this.result = result; + } + public CommandAckPayload(String command_id, String result, String message) { this.command_id = command_id; this.result = result; diff --git a/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java b/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java index 7314c92..d3fc36f 100644 --- a/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java +++ b/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java @@ -22,12 +22,12 @@ public class TransactionFinalizePayload { * B660(取貨碼) / B670(通行碼) / B680(員工卡) 驗證成功後,後台返回的 ID。 * 一般現金/電子支付交易此欄位填 0。 */ - private int code_id; + private String code_id; - /** - * 遠端指令 ID (僅用於 payment_type = 100) + /** + * 遠端指令 ID (僅用於 payment_type = 100) */ - private int remote_command_id; + private String remote_command_id; /** * 支付類型 (Root 級別,用於後端快速判斷) @@ -42,25 +42,57 @@ public class TransactionFinalizePayload { // =========== Getters & Setters =========== - public String getAction() { return action; } + public String getAction() { + return action; + } - public String getFlow_id() { return flow_id; } - public void setFlow_id(String flow_id) { this.flow_id = flow_id; } + public String getFlow_id() { + return flow_id; + } - public int getCode_id() { return code_id; } - public void setCode_id(int code_id) { this.code_id = code_id; } + public void setFlow_id(String flow_id) { + this.flow_id = flow_id; + } - public int getRemote_command_id() { return remote_command_id; } - public void setRemote_command_id(int remote_command_id) { this.remote_command_id = remote_command_id; } + public String getCode_id() { + return code_id; + } - public int getPayment_type() { return payment_type; } - public void setPayment_type(int payment_type) { this.payment_type = payment_type; } + public void setCode_id(String code_id) { + this.code_id = code_id; + } - public OrderPayload getOrder() { return order; } - public void setOrder(OrderPayload order) { this.order = order; } + public String getRemote_command_id() { + return remote_command_id; + } - public List getDispense() { return dispense; } - public void setDispense(List dispense) { this.dispense = dispense; } + public void setRemote_command_id(String remote_command_id) { + this.remote_command_id = remote_command_id; + } + + public int getPayment_type() { + return payment_type; + } + + public void setPayment_type(int payment_type) { + this.payment_type = payment_type; + } + + public OrderPayload getOrder() { + return order; + } + + public void setOrder(OrderPayload order) { + this.order = order; + } + + public List getDispense() { + return dispense; + } + + public void setDispense(List dispense) { + this.dispense = dispense; + } // =========== 內部類別:訂單資訊 =========== @@ -118,50 +150,125 @@ public class TransactionFinalizePayload { private List items; // Getters & Setters - public String getOrder_no() { return order_no; } - public void setOrder_no(String order_no) { this.order_no = order_no; } + public String getOrder_no() { + return order_no; + } - public int getOriginal_amount() { return original_amount; } - public void setOriginal_amount(int original_amount) { this.original_amount = original_amount; } + public void setOrder_no(String order_no) { + this.order_no = order_no; + } - public int getDiscount_amount() { return discount_amount; } - public void setDiscount_amount(int discount_amount) { this.discount_amount = discount_amount; } + public int getOriginal_amount() { + return original_amount; + } - public int getTotal_amount() { return total_amount; } - public void setTotal_amount(int total_amount) { this.total_amount = total_amount; } + public void setOriginal_amount(int original_amount) { + this.original_amount = original_amount; + } - public int getPay_amount() { return pay_amount; } - public void setPay_amount(int pay_amount) { this.pay_amount = pay_amount; } + public int getDiscount_amount() { + return discount_amount; + } - public int getChange_amount() { return change_amount; } - public void setChange_amount(int change_amount) { this.change_amount = change_amount; } + public void setDiscount_amount(int discount_amount) { + this.discount_amount = discount_amount; + } - public int getPoints_used() { return points_used; } - public void setPoints_used(int points_used) { this.points_used = points_used; } + public int getTotal_amount() { + return total_amount; + } - public int getPayment_type() { return payment_type; } - public void setPayment_type(int payment_type) { this.payment_type = payment_type; } + public void setTotal_amount(int total_amount) { + this.total_amount = total_amount; + } - public int getPayment_status() { return payment_status; } - public void setPayment_status(int payment_status) { this.payment_status = payment_status; } + public int getPay_amount() { + return pay_amount; + } - public String getPayment_request() { return payment_request; } - public void setPayment_request(String payment_request) { this.payment_request = payment_request; } + public void setPay_amount(int pay_amount) { + this.pay_amount = pay_amount; + } - public String getPayment_response() { return payment_response; } - public void setPayment_response(String payment_response) { this.payment_response = payment_response; } + public int getChange_amount() { + return change_amount; + } - public String getMember_barcode() { return member_barcode; } - public void setMember_barcode(String member_barcode) { this.member_barcode = member_barcode; } + public void setChange_amount(int change_amount) { + this.change_amount = change_amount; + } - public String getInvoice_info() { return invoice_info; } - public void setInvoice_info(String invoice_info) { this.invoice_info = invoice_info; } + public int getPoints_used() { + return points_used; + } - public String getMachine_time() { return machine_time; } - public void setMachine_time(String machine_time) { this.machine_time = machine_time; } + public void setPoints_used(int points_used) { + this.points_used = points_used; + } - public List getItems() { return items; } - public void setItems(List items) { this.items = items; } + public int getPayment_type() { + return payment_type; + } + + public void setPayment_type(int payment_type) { + this.payment_type = payment_type; + } + + public int getPayment_status() { + return payment_status; + } + + public void setPayment_status(int payment_status) { + this.payment_status = payment_status; + } + + public String getPayment_request() { + return payment_request; + } + + public void setPayment_request(String payment_request) { + this.payment_request = payment_request; + } + + public String getPayment_response() { + return payment_response; + } + + public void setPayment_response(String payment_response) { + this.payment_response = payment_response; + } + + public String getMember_barcode() { + return member_barcode; + } + + public void setMember_barcode(String member_barcode) { + this.member_barcode = member_barcode; + } + + public String getInvoice_info() { + return invoice_info; + } + + public void setInvoice_info(String invoice_info) { + this.invoice_info = invoice_info; + } + + public String getMachine_time() { + return machine_time; + } + + public void setMachine_time(String machine_time) { + this.machine_time = machine_time; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } } // =========== 內部類別:購買商品記錄 =========== @@ -182,9 +289,17 @@ public class TransactionFinalizePayload { this.quantity = quantity; } - public int getProduct_id() { return product_id; } - public int getPrice() { return price; } - public int getQuantity() { return quantity; } + public int getProduct_id() { + return product_id; + } + + public int getPrice() { + return price; + } + + public int getQuantity() { + return quantity; + } } // =========== 內部類別:出貨記錄 =========== @@ -214,7 +329,7 @@ public class TransactionFinalizePayload { private String machine_time; public DispenseRecord(String slot_no, int product_id, int amount, int points_used, - int dispense_status, int remaining_stock, String machine_time) { + int dispense_status, int remaining_stock, String machine_time) { this.slot_no = slot_no; this.product_id = product_id; this.amount = amount; @@ -224,12 +339,32 @@ public class TransactionFinalizePayload { this.machine_time = machine_time; } - public String getSlot_no() { return slot_no; } - public int getProduct_id() { return product_id; } - public int getAmount() { return amount; } - public int getPoints_used() { return points_used; } - public int getDispense_status() { return dispense_status; } - public int getRemaining_stock() { return remaining_stock; } - public String getMachine_time() { return machine_time; } + public String getSlot_no() { + return slot_no; + } + + public int getProduct_id() { + return product_id; + } + + public int getAmount() { + return amount; + } + + public int getPoints_used() { + return points_used; + } + + public int getDispense_status() { + return dispense_status; + } + + public int getRemaining_stock() { + return remaining_stock; + } + + public String getMachine_time() { + return machine_time; + } } } diff --git a/app/src/main/java/com/unibuy/smartdevice/tools/ProductDispenseManager.java b/app/src/main/java/com/unibuy/smartdevice/tools/ProductDispenseManager.java index f2df20e..a548b3c 100644 --- a/app/src/main/java/com/unibuy/smartdevice/tools/ProductDispenseManager.java +++ b/app/src/main/java/com/unibuy/smartdevice/tools/ProductDispenseManager.java @@ -4,6 +4,7 @@ import android.content.Context; import android.os.Handler; import android.os.Looper; import android.util.Log; +import java.util.List; import com.unibuy.smartdevice.MyApp; import com.unibuy.smartdevice.controller.DevController; @@ -20,22 +21,28 @@ public class ProductDispenseManager { public interface DispenseListener { void onProgress(String message); + void onSuccess(); + void onFailure(String error); } private final Context context; private final DispenseListener listener; + private final com.unibuy.smartdevice.exception.Logs logs; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private int checkRetryCount = 0; - public ProductDispenseManager(Context context, DispenseListener listener) { + public ProductDispenseManager(Context context, com.unibuy.smartdevice.exception.Logs logs, + DispenseListener listener) { this.context = context; + this.logs = logs; this.listener = listener; } public void startDispense(int slot) { - Log.i(TAG, "Starting background dispense for slot: " + slot); + if (logs != null) + logs.info("Starting background dispense for slot: " + slot); listener.onProgress("正在檢查貨道狀態..."); DevXinYuanController controller = MyApp.getInstance().getDevXinYuanController(); @@ -47,12 +54,18 @@ public class ProductDispenseManager { // 步驟 1: 檢查貨道狀態 (CMD 01 -> VMC 02) controller.getReadBufferByNow(new DevController.ReadBufferOnScheduler(new HandlerMain(context, null) { @Override - protected void execute(Context context, int commandCode, String message) {} + protected void execute(Context context, int commandCode, String message) { + } }) { @Override - protected HandlerMain setHandlerMain() { return getSrcHandlerMain(); } + protected HandlerMain setHandlerMain() { + return getSrcHandlerMain(); + } + @Override - protected Class setCls() { return getClass(); } + protected Class setCls() { + return getClass(); + } @Override public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) { @@ -63,7 +76,8 @@ public class ProductDispenseManager { for (byte[] buffer : devController.getReadBufferByMax()) { if (buffer[0] == 0x02) { // VMC_02_CHECK_SLOT_STATUS isCMD = true; - Log.d(TAG, "Check Slot Result: " + PortTools.showHex(buffer)); + if (logs != null) + logs.debug("Check Slot Result: " + PortTools.showHex(buffer)); // 01: OK, 02: PDT_EMPTY (有些機型空也會允許出貨指令), 04: TMP_USE if (buffer[1] == 0x01 || buffer[1] == 0x02 || buffer[1] == 0x04) { isStatusOk = true; @@ -88,7 +102,7 @@ public class ProductDispenseManager { if (checkRetryCount < 3) { checkRetryCount++; controller.addSendBuffer(controller.getDevXinYuan().checkSlot(slot)); - controller.getReadBufferByNow(this, 1000); + controller.getReadBuffer(this, 1000); } else { mainHandler.post(() -> listener.onFailure("下位機檢查貨道無回應")); } @@ -102,60 +116,110 @@ public class ProductDispenseManager { private void executeActualDispense(int slot) { listener.onProgress("貨道正常,正在發送出貨指令..."); DevXinYuanController controller = MyApp.getInstance().getDevXinYuanController(); + long startTime = System.currentTimeMillis(); + long timeoutMillis = 60000; // 60 秒超時 // 步驟 2: 發送出貨指令並監聽狀態 (CMD 06 -> VMC 04) controller.getReadBufferByNow(new DevController.ReadBufferOnScheduler(new HandlerMain(context, null) { @Override - protected void execute(Context context, int commandCode, String message) {} + protected void execute(Context context, int commandCode, String message) { + } }) { @Override - protected HandlerMain setHandlerMain() { return getSrcHandlerMain(); } + protected HandlerMain setHandlerMain() { + return getSrcHandlerMain(); + } + @Override - protected Class setCls() { return getClass(); } + protected Class setCls() { + return getClass(); + } @Override public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) { - boolean isCMD = false; + boolean isVmc04Found = false; boolean isSuccess = false; boolean isFailed = false; String errorDetail = ""; - for (byte[] buffer : devController.getReadBufferByMax()) { - if (buffer[0] == 0x04) { // VMC_04_BUY_STATUS - isCMD = true; - Log.d(TAG, "Dispense Status: " + PortTools.showHex(buffer)); - - // 02: OK, 24: PLIS_BOX (可能是某些特定機型的成功代碼) + // 讀取所有緩衝區中的 VMC_04 訊息 + // 注意:buffer 中可能同時存在 VMC_11、VMC_23 等其他訊號, + // 必須只判斷 VMC_04,非 04 的封包一律忽略並繼續輪詢。 + List buffers = devController.getReadBufferByMax(); + for (byte[] buffer : buffers) { + if (buffer.length >= 2 && buffer[0] == 0x04) { // VMC_04_BUY_STATUS + isVmc04Found = true; + if (logs != null) + logs.info("Dispense Status Received: " + PortTools.showHex(buffer)); + + // 02: OK, 24: PLIS_BOX if (buffer[1] == 0x02 || buffer[1] == 0x24) { + if (logs != null) + logs.info("Dispense SUCCESS detected from VMC_04: " + PortTools.showHex(buffer)); isSuccess = true; break; - } + } // 01: NOW (處理中), 10: PLATFORM_UP, 11: PLATFORM_DOWN else if (buffer[1] == 0x01 || buffer[1] == 0x10 || buffer[1] == 0x11) { - continue; - } - else { + if (logs != null) + logs.info("Machine busy (status " + String.format("%02X", buffer[1]) + + "), continuing poll..."); + // 繼續掃描其他 buffer + } else { + if (logs != null) + logs.info("Dispense FAILED detected from VMC_04: " + PortTools.showHex(buffer)); isFailed = true; errorDetail = String.format("%02X", buffer[1]); + break; } } + // 非 VMC_04 的封包 (如 VMC_11_GIVE_PRODUCT_INFO, VMC_23_NOW_GET_TOTAL) 一律跳過 } if (isSuccess) { mainHandler.post(() -> { - updateLocalStock(slot); listener.onSuccess(); }); } else if (isFailed) { - // Error during CMD 04: construct error code e.g. "0403" String errCode = String.format("04%s", errorDetail); MqttManager.getInstance().publishError(slot, errCode); - String finalErrorDetail = errorDetail; mainHandler.post(() -> listener.onFailure("出貨失敗,錯誤代碼: " + finalErrorDetail)); } else { - // 繼續輪詢直到成功或失敗 - controller.getReadBufferByNow(this, 1000); + // isVmc04Found=false 或 VMC_04 仍在處理中 → 繼續輪詢 + // 【重要】即使 buffers 非空,只要沒有明確的成功/失敗 VMC_04,就必須繼續輪詢。 + // 之前的 bug:buffers 含 VMC_11/VMC_23 時不為空,導致 else 分支被跳過,輪詢靜默終止。 + if (logs != null) { + if (isVmc04Found) { + logs.info("VMC_04 still busy, rescheduling poll in 1s..."); + } else { + logs.info("No VMC_04 in buffer yet, rescheduling poll in 1s..."); + } + } + + // 檢查是否超時 + if (System.currentTimeMillis() - startTime > timeoutMillis) { + if (logs != null) + logs.info("ERROR: Dispense polling timeout after " + (timeoutMillis / 1000) + "s"); + MqttManager.getInstance().publishError(slot, "TIMEOUT"); + mainHandler.post(() -> listener.onFailure("出貨超時 (60s)")); + return; + } + + // 使用 mainHandler.postDelayed 延遲 1 秒後重新輪詢 + // 【重要】此處改用 getReadBuffer 而非 getReadBufferByNow。 + // getReadBufferByNow 會清空 ReadBuffer,若 0402 訊號剛好在此時抵達,會被抹除。 + final DevController.ReadBufferOnScheduler self = this; + mainHandler.postDelayed(() -> { + try { + devController.getReadBuffer(self, 500); + } catch (Exception e) { + // SchedulerAbstract 排程失敗時,確保仍能回報結果觸發 ACK + if (logs != null) + logs.info("ERROR: Polling reschedule failed: " + e.getMessage()); + mainHandler.post(() -> listener.onFailure("輪詢排程異常")); + } + }, 100); } } }, 1000); @@ -172,16 +236,19 @@ public class ProductDispenseManager { if (currentCount > 0) { int newCount = currentCount - 1; slotData.setCount(newCount); - Log.i(TAG, "Updated slot " + slot + " count: " + currentCount + " -> " + newCount); - + if (logs != null) + logs.info("Updated slot " + slot + " count: " + currentCount + " -> " + newCount); + // 儲存至資料庫 - com.unibuy.smartdevice.database.SlotsDao slotsDao = new com.unibuy.smartdevice.database.SlotsDao(context); + com.unibuy.smartdevice.database.SlotsDao slotsDao = new com.unibuy.smartdevice.database.SlotsDao( + context); slotsDao.deleteAll(); slotsDao.insertAll(MyApp.getInstance().getSlotMap()); slotsDao.close(); } } catch (Exception e) { - Log.e(TAG, "Failed to update local stock: " + e.getMessage()); + if (logs != null) + logs.info("ERROR: Failed to update local stock: " + e.getMessage()); } } } diff --git a/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java b/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java index 77b06e0..2c76eaa 100644 --- a/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java +++ b/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java @@ -39,6 +39,9 @@ public class TransactionFinalizeBuilder { /** 累積的出貨結果清單 */ private final List dispenseRecords = new ArrayList<>(); + private String codeId = "0"; + private String remoteCommandId = "0"; + public TransactionFinalizeBuilder() { this.orderMachineTime = SDF.format(new Date()); // 在進入出貨流程時立即生成本次交易流水號(一次交易只生成一次) @@ -46,6 +49,14 @@ public class TransactionFinalizeBuilder { Log.i(TAG, "TransactionFinalizeBuilder created, flow_id=" + this.flowId); } + public void setCodeId(String codeId) { + this.codeId = codeId; + } + + public void setRemoteCommandId(String remoteCommandId) { + this.remoteCommandId = remoteCommandId; + } + /** * 加入一筆出貨結果記錄(每個貨道出貨後呼叫一次)。 * @@ -87,16 +98,21 @@ public class TransactionFinalizeBuilder { payload.setFlow_id(flowId); // --- code_id:特殊驗證碼 ID (B660/B670/B680 取得) --- - // 一般現金/電子支付交易為 0。 - String accessCodeId = MyApp.getInstance().getReportAccessCodeStructure().getAccessCodeId(); - try { - payload.setCode_id(accessCodeId != null && !accessCodeId.isEmpty() - ? Integer.parseInt(accessCodeId) : 0); - } catch (NumberFormatException e) { - Log.w(TAG, "Cannot parse code_id: " + accessCodeId); - payload.setCode_id(0); + // 一般現金/電子支付交易為 "0"。 + if (this.codeId != null && !this.codeId.equals("0")) { + payload.setCode_id(this.codeId); + } else { + String accessCodeId = MyApp.getInstance().getReportAccessCodeStructure().getAccessCodeId(); + if (accessCodeId != null && !accessCodeId.isEmpty()) { + payload.setCode_id(accessCodeId); + } else { + payload.setCode_id("0"); + } } + // --- remote_command_id:遠端指令 ID --- + payload.setRemote_command_id(this.remoteCommandId); + // --- payment_type:支付類型 (Root 級別) --- payload.setPayment_type(flowInfo.getFlowType());