[FIX] 修復遠端出貨流程與 MQTT 回傳機制
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 邏輯,避免在重新啟動排程時出現潛在競爭。
This commit is contained in:
parent
f5619e754e
commit
98cb59c431
@ -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
|
||||
```
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<byte[]> getReadBufferByMax() {
|
||||
int size = readBuffers.size();
|
||||
return readBuffers;
|
||||
List<byte[]> buffers = new ArrayList<>(readBuffers);
|
||||
readBuffers.clear();
|
||||
return buffers;
|
||||
}
|
||||
|
||||
public byte[] getReadBuffer() {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<DispenseRecord> getDispense() { return dispense; }
|
||||
public void setDispense(List<DispenseRecord> 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<DispenseRecord> getDispense() {
|
||||
return dispense;
|
||||
}
|
||||
|
||||
public void setDispense(List<DispenseRecord> dispense) {
|
||||
this.dispense = dispense;
|
||||
}
|
||||
|
||||
// =========== 內部類別:訂單資訊 ===========
|
||||
|
||||
@ -118,50 +150,125 @@ public class TransactionFinalizePayload {
|
||||
private List<ItemRecord> 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<ItemRecord> getItems() { return items; }
|
||||
public void setItems(List<ItemRecord> 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<ItemRecord> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<ItemRecord> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<byte[]> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,6 +39,9 @@ public class TransactionFinalizeBuilder {
|
||||
/** 累積的出貨結果清單 */
|
||||
private final List<TransactionFinalizePayload.DispenseRecord> 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());
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user