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 8951aa9..0fe7b03 100644 --- a/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java +++ b/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java @@ -136,6 +136,89 @@ public class MqttService extends Service { Log.e(TAG, "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..."); + try { + int amount = 0; + if (command.getPayload() != null && command.getPayload().isJsonObject()) { + com.google.gson.JsonObject payloadObj = command.getPayload().getAsJsonObject(); + if (payloadObj.has("amount")) { + amount = payloadObj.get("amount").getAsInt(); + } + } + + if (amount <= 0) { + throw new Exception("Invalid or missing amount: " + amount); + } + + 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); + } + + @Override + public void onSuccess() { + Log.i(TAG, "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); + } + }); + + payoutManager.startPayout(amount); + // 注意:ACK 會在 PayoutListener 的回調中發送,不在此處發送。 + } catch (Exception e) { + Log.e(TAG, "Failed to parse or start change command: " + e.getMessage()); + mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage()); + } + } else if ("dispense".equals(cmd)) { + Log.i(TAG, "Executing remote dispense command..."); + try { + int slot = -1; + if (command.getPayload() != null && command.getPayload().isJsonObject()) { + com.google.gson.JsonObject payloadObj = command.getPayload().getAsJsonObject(); + if (payloadObj.has("slot_no")) { + slot = payloadObj.get("slot_no").getAsInt(); + } else if (payloadObj.has("slot")) { + slot = payloadObj.get("slot").getAsInt(); + } + } + + if (slot <= 0) { + throw new Exception("Invalid or missing slot_no: " + slot); + } + + 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); + mqttManager.publishCommandAck(cmdId, "success", "Product dispensed from slot: " + finalSlot); + } + + @Override + public void onFailure(String error) { + Log.e(TAG, "Remote dispense failed: " + error); + mqttManager.publishCommandAck(cmdId, "failed", "Dispense failed: " + error); + } + }); + + dispenseManager.startDispense(slot); + } catch (Exception e) { + Log.e(TAG, "Failed to parse or start dispense command: " + e.getMessage()); + mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage()); + } } else { Log.w(TAG, "Unknown MQTT command: " + cmd); mqttManager.publishCommandAck(cmdId, "failed", "Unknown command"); diff --git a/app/src/main/java/com/unibuy/smartdevice/tools/CashPayoutManager.java b/app/src/main/java/com/unibuy/smartdevice/tools/CashPayoutManager.java new file mode 100644 index 0000000..ececb4e --- /dev/null +++ b/app/src/main/java/com/unibuy/smartdevice/tools/CashPayoutManager.java @@ -0,0 +1,235 @@ +package com.unibuy.smartdevice.tools; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.unibuy.smartdevice.exception.ErrorCode; +import com.unibuy.smartdevice.tools.SimpleCashAcceptorHandler; + +import java.util.ArrayList; +import java.util.List; + +/** + * 遠端找零管理器 + * 負責處理 MDB 設備的連線、初始化、庫存查詢及退幣/退鈔指令發送。 + */ +public class CashPayoutManager { + private static final String TAG = "CashPayoutManager"; + + public interface PayoutListener { + void onProgress(String message); + void onSuccess(); + void onFailure(String error); + } + + private final Context context; + private final PayoutListener listener; + private SimpleCashAcceptorHandler handler; + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + + // 庫存變數 + private int coin50_count = 0; + private int coin10_count = 0; + private int coin5_count = 0; + private int coin1_count = 0; + private int note100_count = 0; + + public CashPayoutManager(Context context, PayoutListener listener) { + this.context = context; + this.listener = listener; + } + + public void startPayout(int amount) { + Log.i(TAG, "Starting remote payout for amount: " + amount); + listener.onProgress("正在初始化退幣系統..."); + + handler = new SimpleCashAcceptorHandler(context, event -> { + Log.d(TAG, "MDB Event: " + event); + handleMdbEvent(event); + }); + + new Thread(() -> { + String connectResult = handler.connect(); + if ("成功連接到 USB 轉接器!".equals(connectResult)) { + mainHandler.post(() -> { + handler.startReading(); + performInitializationSequence(amount); + }); + } else { + mainHandler.post(() -> listener.onFailure("MDB 連線失敗: " + connectResult)); + } + }).start(); + } + + private void performInitializationSequence(int amount) { + // 依序發送初始化與查詢指令 + // 指令序列參考 CashMachineSettings 與 CashPayDialog + sendMdbCommand("0C00000000", 0, () -> { // 先禁止硬幣接收 + sendMdbCommand("3400000000", 500, () -> { // 禁止紙鈔接收 + sendMdbCommand("370100000002", 500, () -> { // 設定紙鈔類型 + sendMdbCommand("3703", 500, () -> { // 查詢設置 + sendMdbCommand("3705", 500, () -> { // 查詢紙鈔庫存 + sendMdbCommand("0A", 1000, () -> { // 查詢硬幣庫存 + calculateAndExecutePayout(amount); + });});});});});}); + } + + private void calculateAndExecutePayout(int amount) { + listener.onProgress("正在計算退幣組合..."); + List commands = new ArrayList<>(); + int remaining = amount; + + // 1. 100元紙鈔 + int hundredsToGive = Math.min(remaining / 100, note100_count); + if (hundredsToGive > 0) { + String cmd = "370600000" + String.format("%01X", hundredsToGive); + commands.add(new ChangeCommand(cmd, 100, hundredsToGive)); + remaining -= hundredsToGive * 100; + } + + // 2. 50元硬幣 + int fiftiesToGive = Math.min(remaining / 50, coin50_count); + if (fiftiesToGive > 0) { + String cmd = "0D" + String.format("%01X", fiftiesToGive) + "4"; + commands.add(new ChangeCommand(cmd, 50, fiftiesToGive)); + remaining -= fiftiesToGive * 50; + } + + // 3. 10元硬幣 + int tensToGive = Math.min(remaining / 10, coin10_count); + if (tensToGive > 0) { + String cmd = "0D" + String.format("%01X", tensToGive) + "2"; + commands.add(new ChangeCommand(cmd, 10, tensToGive)); + remaining -= tensToGive * 10; + } + + // 4. 5元硬幣 + int fivesToGive = Math.min(remaining / 5, coin5_count); + if (fivesToGive > 0) { + String cmd = "0D" + String.format("%01X", fivesToGive) + "1"; + commands.add(new ChangeCommand(cmd, 5, fivesToGive)); + remaining -= fivesToGive * 5; + } + + // 5. 1元硬幣 + int onesToGive = Math.min(remaining, coin1_count); + if (onesToGive > 0) { + String cmd = "0D" + String.format("%01X", onesToGive) + "0"; + commands.add(new ChangeCommand(cmd, 1, onesToGive)); + remaining -= onesToGive; + } + + if (remaining > 0) { + Log.w(TAG, "Insufficient inventory to payout full amount. Missing: " + remaining); + // 依然執行現有的,但回報錯誤訊息 + } + + if (commands.isEmpty()) { + listener.onFailure("無需找零或庫存不足以找零"); + close(); + return; + } + + executePayoutCommands(commands, 0); + } + + private void executePayoutCommands(List commands, int index) { + if (index >= commands.size()) { + listener.onProgress("退幣完成"); + // 延遲關閉,確保指令處理完畢 + mainHandler.postDelayed(() -> { + close(); + listener.onSuccess(); + }, 2000); + return; + } + + ChangeCommand current = commands.get(index); + listener.onProgress("正在執行: " + current.denomination + "元 x " + current.count); + + sendMdbCommand(current.command, 0, () -> { + // 退幣指令後需要較長延遲 + executePayoutCommands(commands, index + 1); + }); + } + + private void sendMdbCommand(String hexCommand, int delay, Runnable onComplete) { + mainHandler.postDelayed(() -> { + new Thread(() -> { + try { + byte[] command = hexStringToByteArray(hexCommand); + handler.writeData(command); + if (onComplete != null) { + mainHandler.post(onComplete); + } + } catch (Exception e) { + Log.e(TAG, "Error sending MDB command: " + hexCommand, e); + } + }).start(); + }, Math.max(delay, 1000)); // 每個指令至少間隔 1 秒,退幣可能需要更長 + } + + private void handleMdbEvent(String event) { + if (event == null || event.trim().isEmpty()) return; + String[] parts = event.trim().split("\\s+"); + + // 解析硬幣庫存 (0A 回傳) + if (parts.length == 19) { + try { + coin1_count = Integer.parseInt(parts[2], 16); + coin5_count = Integer.parseInt(parts[3], 16); + coin10_count = Integer.parseInt(parts[4], 16); + coin50_count = Integer.parseInt(parts[6], 16); + Log.d(TAG, String.format("Inventory Update: 1=%d, 5=%d, 10=%d, 50=%d", + coin1_count, coin5_count, coin10_count, coin50_count)); + } catch (Exception e) { + Log.e(TAG, "Parse coin inventory error", e); + } + } + + // 解析百鈔庫存 (3705 回傳) + if (parts.length >= 30) { + try { + int noteCount = Integer.parseInt(parts[3], 16); + if (noteCount >= 0 && noteCount <= 100) { + note100_count = noteCount; + Log.d(TAG, "Inventory Update: 100=" + note100_count); + } + } catch (Exception e) { + Log.e(TAG, "Parse bill inventory error", e); + } + } + } + + private void close() { + if (handler != null) { + handler.stopReading(); + handler.close(); + handler = null; + } + } + + private byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } + + private static class ChangeCommand { + final String command; + final int denomination; + final int count; + + ChangeCommand(String command, int denomination, int count) { + this.command = command; + this.denomination = denomination; + this.count = count; + } + } +} diff --git a/app/src/main/java/com/unibuy/smartdevice/tools/ProductDispenseManager.java b/app/src/main/java/com/unibuy/smartdevice/tools/ProductDispenseManager.java new file mode 100644 index 0000000..4f6fbca --- /dev/null +++ b/app/src/main/java/com/unibuy/smartdevice/tools/ProductDispenseManager.java @@ -0,0 +1,175 @@ +package com.unibuy.smartdevice.tools; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.unibuy.smartdevice.MyApp; +import com.unibuy.smartdevice.controller.DevController; +import com.unibuy.smartdevice.controller.DevXinYuanController; +import com.unibuy.smartdevice.devices.PortTools; + +/** + * 遠端出貨管理器 + * 負責在背景執行商品出貨流程,不依賴於 UI (DispatchDialog)。 + */ +public class ProductDispenseManager { + private static final String TAG = "ProductDispenseManager"; + + public interface DispenseListener { + void onProgress(String message); + void onSuccess(); + void onFailure(String error); + } + + private final Context context; + private final DispenseListener listener; + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private int checkRetryCount = 0; + + public ProductDispenseManager(Context context, DispenseListener listener) { + this.context = context; + this.listener = listener; + } + + public void startDispense(int slot) { + Log.i(TAG, "Starting background dispense for slot: " + slot); + listener.onProgress("正在檢查貨道狀態..."); + + DevXinYuanController controller = MyApp.getInstance().getDevXinYuanController(); + if (controller == null || !controller.isConnected()) { + listener.onFailure("下位機控制器未連線"); + return; + } + + // 步驟 1: 檢查貨道狀態 (CMD 01 -> VMC 02) + controller.getReadBufferByNow(new DevController.ReadBufferOnScheduler(new HandlerMain(context, null) { + @Override + protected void execute(Context context, int commandCode, String message) {} + }) { + @Override + protected HandlerMain setHandlerMain() { return getSrcHandlerMain(); } + @Override + protected Class setCls() { return getClass(); } + + @Override + public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) { + boolean isCMD = false; + boolean isStatusOk = false; + + 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)); + // 01: OK, 02: PDT_EMPTY (有些機型空也會允許出貨指令), 04: TMP_USE + if (buffer[1] == 0x01 || buffer[1] == 0x02 || buffer[1] == 0x04) { + isStatusOk = true; + } + break; + } + } + + if (isCMD) { + if (isStatusOk) { + mainHandler.post(() -> executeActualDispense(slot)); + } else { + mainHandler.post(() -> listener.onFailure("貨道狀態異常,無法出貨")); + } + } else { + if (checkRetryCount < 3) { + checkRetryCount++; + controller.addSendBuffer(controller.getDevXinYuan().checkSlot(slot)); + controller.getReadBufferByNow(this, 1000); + } else { + mainHandler.post(() -> listener.onFailure("下位機檢查貨道無回應")); + } + } + } + }, 1000); + + controller.addSendBuffer(controller.getDevXinYuan().checkSlot(slot)); + } + + private void executeActualDispense(int slot) { + listener.onProgress("貨道正常,正在發送出貨指令..."); + DevXinYuanController controller = MyApp.getInstance().getDevXinYuanController(); + + // 步驟 2: 發送出貨指令並監聽狀態 (CMD 06 -> VMC 04) + controller.getReadBufferByNow(new DevController.ReadBufferOnScheduler(new HandlerMain(context, null) { + @Override + protected void execute(Context context, int commandCode, String message) {} + }) { + @Override + protected HandlerMain setHandlerMain() { return getSrcHandlerMain(); } + @Override + protected Class setCls() { return getClass(); } + + @Override + public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) { + boolean isCMD = 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 (可能是某些特定機型的成功代碼) + if (buffer[1] == 0x02 || buffer[1] == 0x24) { + isSuccess = true; + break; + } + // 01: NOW (處理中), 10: PLATFORM_UP, 11: PLATFORM_DOWN + else if (buffer[1] == 0x01 || buffer[1] == 0x10 || buffer[1] == 0x11) { + continue; + } + else { + isFailed = true; + errorDetail = String.format("%02X", buffer[1]); + } + } + } + + if (isSuccess) { + mainHandler.post(() -> { + updateLocalStock(slot); + listener.onSuccess(); + }); + } else if (isFailed) { + String finalErrorDetail = errorDetail; + mainHandler.post(() -> listener.onFailure("出貨失敗,錯誤代碼: " + finalErrorDetail)); + } else { + // 繼續輪詢直到成功或失敗 + controller.getReadBufferByNow(this, 1000); + } + } + }, 1000); + + controller.addSendBuffer(controller.getDevXinYuan().getProduct(slot)); + } + + private void updateLocalStock(int slot) { + try { + // 預設為 VMC 貨道 (Field 1) + int field = com.unibuy.smartdevice.devices.SlotField.VMC.getField(); + com.unibuy.smartdevice.structure.SlotStructure slotData = MyApp.getInstance().getSlotData(field, slot); + int currentCount = slotData.getCount(); + if (currentCount > 0) { + int newCount = currentCount - 1; + slotData.setCount(newCount); + Log.i(TAG, "Updated slot " + slot + " count: " + currentCount + " -> " + newCount); + + // 儲存至資料庫 + 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()); + } + } +}