feat: 串口方案設定、取貨口/連線判斷重構、下位機當掉修正、客服圖外部化 (verPatch 37)

- 版號 verPatch 36 → 37
- 串口方案(本機設定):後台系統設定可切換 舊版本(下位機 ttyS1/刷卡機 ttyS2) ↔
  星科技(ttyS7/ttyS6),因應不同 IPC 主板串口位置不一致;切換後重啟套用。
  (PortProfile / ComPort.getFilepath / SystemSetActivity / activity_system_set.xml)
- 進銷售平台不再檢查/擋導航列(隱藏提示)。(MainActivity)
- 取貨口+連線判斷改為「點選商品時一次主動問答」(比照舊專案 DaiLemei):
  早退、~3.5 秒封頂;收到 05/門/升降立即擋、01 立即放行;拿不到 VMC_02 但機台
  仍在線(近期有任何 VMC 通訊)→保守擋住請客人稍候再點,避免漏放有東西;完全無通訊
  才自動重連重試 3 次。加單飛守衛、查詢前清殘留佇列。
  (BasicSaleFlow / DevXinYuanController: getSlotStatusSince/isRecentlyAlive/lastVmcCommTime)
- 修下位機「連續判斷後整個當掉」:送出佇列過載(>50)由 setRun(false) 停掉讀取迴圈
  改為「丟最舊、留最新」(load shedding),讀取迴圈持續運作、過載緩解自動恢復。
  (DevController.addSendBuffer)
- 出貨失敗客服 QR 圖改為執行期優先讀裝置替換檔(/sdcard 或 App 外部目錄的 cs_qrcode.jpg/png),
  找不到才用內建預設;換客戶不用重編。本版預設圖設為 qrcode_msg_dailemei(專給代樂美)。
  (DispatchDialog / dialog_dispatch.xml / qrcode_msg_dailemei.jpg)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
terrylee 2026-07-08 14:56:20 +08:00
parent 436e9b8328
commit 954d4a3716
12 changed files with 372 additions and 56 deletions

View File

@ -10,7 +10,7 @@ plugins {
//
// =========================================================================
def verMinor = 2
def verPatch = 36
def verPatch = 37

View File

@ -210,10 +210,12 @@ public class MainActivity extends AppCompatActivityAbstract {
}
// 🔒 販賣機機台導航列沒真的關掉就擋下並提醒後門長按不受此限
if (!isNavigationReallyHidden()) {
showMustHideSystemBarsDialog();
return;
}
// 依需求隱藏此提示進銷售平台不再檢查導航列直接進入
// 保留原判斷程式碼於此日後如要恢復把下方三行取消註解即可
// if (!isNavigationReallyHidden()) {
// showMustHideSystemBarsDialog();
// return;
// }
enterShoppingPlatform();
});

View File

@ -24,9 +24,12 @@ public abstract class DevController extends HandlerMainScheduler {
if (this.isRun()) {
logs.info("sendBuffers max:" + sendBuffers.size());
if (sendBuffers.size() > 50) {
setRun(false);
return;
// 過載保護佇列堆積過多時丟棄最舊的堆積指令(load shedding)而非停掉讀取迴圈
// 舊作法 setRun(false) 會把下位機讀取迴圈整個停掉 永久失連且無法自行恢復
//連續查詢後卡未連線當掉的主因改為丟舊留新確保剛送的新指令( checkSlot)仍能送出
// 讀取迴圈持續運作過載一緩解即自動恢復
while (sendBuffers.size() > 50) {
sendBuffers.remove(0);
}
sendBuffers.add(buffer);

View File

@ -50,6 +50,15 @@ public class DevXinYuanController extends DevController {
private static volatile int lastSlotStatus = -1;
private static volatile long lastSlotStatusTime = 0;
/** 最近一次收到任何合法 VMC 幀的時間只要有在收POLL/ACK/金錢/狀態就代表下位機在線
* 不侷限於 VMC_02供連線判斷機台可能在忙其他協定而暫不回報取貨口狀態但仍是連線正常 */
private static volatile long lastVmcCommTime = 0;
/** 下位機近 withinMs 毫秒內是否有任何 VMC 通訊(=是否在線)。與取貨口狀態無關。 */
public static boolean isRecentlyAlive(long withinMs) {
return lastVmcCommTime > 0 && System.currentTimeMillis() - lastVmcCommTime < withinMs;
}
/** 取得最近 withinMs 毫秒內 VMC_02 回報的貨道/取貨口狀態位元組;無近期資料回 -1。 */
public static int getRecentSlotStatus(long withinMs) {
if (lastSlotStatus < 0) return -1;
@ -57,6 +66,18 @@ public class DevXinYuanController extends DevController {
return lastSlotStatus;
}
/**
* 取得sinceTime之後才回報的取貨口/貨道狀態位元組最近一次回報早於 sinceTime 則回 -1
* 點選商品主動查詢用 checkSlot 後只採信新到的 VMC_02 回覆避免採到查詢前的舊快取
* 因下位機為輪詢式主從送出 checkSlot 後機台會於下一個 POLL 主動回一筆 VMC_02通常數百毫秒內即到
* 故可搭配早退收到即判定不需空等廣播週期
*/
public static int getSlotStatusSince(long sinceTime) {
if (lastSlotStatus < 0) return -1;
if (lastSlotStatusTime < sinceTime) return -1;
return lastSlotStatus;
}
public DevXinYuan getDevXinYuan() {
return devXinYuan;
}
@ -172,6 +193,9 @@ public class DevXinYuanController extends DevController {
DevXinYuan.VMC vmc = vmcMap.get(key);
// 收到任何合法 VMC 下位機在線更新最後通訊時間供連線判斷與取貨口狀態脫鉤
lastVmcCommTime = System.currentTimeMillis();
if (vmc != DevXinYuan.VMC.VMC_41_POLL) {
getLogs().debug("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd));
getLogs().debug("XinYuan vmcData:" + PortTools.showHex(vmcData));

View File

@ -46,13 +46,17 @@ public enum ComPort {
}
public String getFilepath() {
// 下位機/刷卡機串口S12XYU S9XYU 兩個 flavor 皆使用 ttyS1下位機 XinYuan/ ttyS2刷卡機 NFCPay
// S12XYU ttyS7/ttyS62026-07 統一改為 ttyS1/ttyS2
// 下位機(XinYuan)/刷卡機(NFCPay)串口依串口方案(本機設定 PortProfile)決定
// 以因應不同 IPC 主板的串口位置不一致
// - 舊版本 (LEGACY )下位機 ttyS1刷卡機 ttyS2
// - 星科技 (STARTECH)下位機 ttyS7刷卡機 ttyS6
// 於後台系統設定切換切換後需重啟 app 套用預設 LEGACY(ttyS1/ttyS2)
boolean starTech = PortProfile.isStarTech();
if (this == COM_PORT_S1) {
return "/dev/ttyS1";
return starTech ? "/dev/ttyS7" : "/dev/ttyS1";
}
if (this == COM_PORT_S2) {
return "/dev/ttyS2";
return starTech ? "/dev/ttyS6" : "/dev/ttyS2";
}
return filepath;
}

View File

@ -0,0 +1,62 @@
package com.unibuy.smartdevice.devices;
import android.content.Context;
import android.content.SharedPreferences;
import com.unibuy.smartdevice.MyApp;
/**
* 串口方案本機硬體設定不回寫雲端
*
* 不同 IPC 主板的下位機(主櫃 XinYuan)與刷卡機(NFCPay)串口位置不一致需可現場切換
* - LEGACY (舊版本 )下位機 /dev/ttyS1刷卡機 /dev/ttyS2
* - STARTECH(星科技版本)下位機 /dev/ttyS7刷卡機 /dev/ttyS6
*
* SharedPreferences 持久化ComPort.getFilepath() 依此回傳實際 tty 路徑
* 串口在 app 啟動時就綁定故切換方案後需重啟 app 才會套用
* 預設 LEGACY與切換功能加入前的行為(ttyS1/ttyS2)相同
*/
public final class PortProfile {
private static final String PREFS = "device_port_profile";
private static final String KEY_PROFILE = "profile";
public static final String LEGACY = "legacy"; // 舊版本 ttyS1 / ttyS2
public static final String STARTECH = "startech"; // 星科技版本ttyS7 / ttyS6
private PortProfile() {}
private static SharedPreferences sp(Context ctx) {
return ctx.getApplicationContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE);
}
/** 目前串口方案;預設 LEGACY。無 Context 版供 enum/背景使用。 */
public static String getProfile() {
try {
return sp(MyApp.getInstance()).getString(KEY_PROFILE, LEGACY);
} catch (Exception e) {
return LEGACY;
}
}
public static String getProfile(Context ctx) {
return sp(ctx).getString(KEY_PROFILE, LEGACY);
}
/** 是否為星科技方案(ttyS7/ttyS6)。 */
public static boolean isStarTech() {
return STARTECH.equals(getProfile());
}
/** 用 commit同步落地確保切換後即使立刻重啟也讀得到。 */
public static void setProfile(Context ctx, String profile) {
String v = STARTECH.equals(profile) ? STARTECH : LEGACY;
sp(ctx).edit().putString(KEY_PROFILE, v).commit();
}
/** 方案的中文顯示名稱。 */
public static String displayName(String profile) {
return STARTECH.equals(profile) ? "星科技(下位機 ttyS7刷卡機 ttyS6"
: "舊版本(下位機 ttyS1刷卡機 ttyS2";
}
}

View File

@ -27,13 +27,24 @@ public class BasicSaleFlow extends SaleFlowHandler {
private static final int PICKUP_HAVE_THINGS = 1; // 取貨口有東西 (02 05)
private static final int PICKUP_DOOR = 2; // 取貨口門未關 (02 06 / 02 09)
private static final int PICKUP_ELEVATOR = 3; // 升降梯未回原點 (02 07)
private static final int PICKUP_NO_RESPONSE = 4; // 下位機無回應
private static final int PICKUP_NO_RESPONSE = 4; // 下位機完全無通訊(可能真的斷線)
private static final int PICKUP_ALIVE_UNKNOWN = 5; // 下位機在線但未回報取貨口狀態(忙其他協定)放行交後續階段再判
// 點選商品階段已判斷過取貨口的旗標與時效直購路徑把取貨口判斷提前到點選當下
// 之後 BuyDialog 開啟時若旗標仍新鮮就跳過重驗取貨口避免剛連線就偵測導致漏判也免重複跳窗
private static final long PICKUP_PRECHECK_TTL_MS = 30000;
private static volatile long lastPickupOkAtMs = 0L;
// 單飛守衛同一時間只允許一個點選商品檢查在進行連續快速點商品時若不擋
// 每次點擊都會疊加一輪 checkSlot/syncPackNo 到送出佇列塞爆後觸發過載下位機失連
private static volatile boolean pickupCheckRunning = false;
// 點選商品階段的合併檢查開關連線取貨口比照舊專案 DaiLemei
// true=點下商品當下就用一次主動問答同時判下位機是否連線取貨口是否有東西早退~3.5 秒封頂
// 正常開詳情頁並記旗標(直接購買略過重複連線判斷)有東西//升降完全無回應視為未連線擋住
// false=點商品直接開詳情頁連線判斷延到直接購買時做
private static final boolean PICKUP_PRECHECK_AT_SELECT = true;
/** 直購取貨口預檢旗標是否仍在時效內;是則消費掉(一次性)並回 true。BuyDialog 開啟時呼叫以決定是否略過重驗。 */
public static boolean consumeFreshPickupPreCheck() {
boolean fresh = lastPickupOkAtMs > 0
@ -148,12 +159,13 @@ public class BasicSaleFlow extends SaleFlowHandler {
*/
@Override
public boolean handleProductSelected(SlotStructure slot) {
// 第一階段點選商品直購購物車關閉且點的是主櫃(VMC)商品點選當下判斷取貨口
// 有東西//升降梯異常 直接跳提示擋住連詳情頁都不開正常才開詳情頁並記旗標
// 之後 BuyDialog 開啟時按直接購買若旗標仍新鮮就不再重驗取貨口比照 LockBoard-Honlifeng
// 第一階段點選商品直購購物車關閉且點的是主櫃(VMC)商品點選當下就用一次主動問答
// 同時判下位機是否連線取貨口是否有東西比照舊專案 DaiLemei~3.5 秒封頂早退
// 有東西//升降異常 直接跳提示擋住連詳情頁都不開完全無回應 視為未連線擋住
// 正常才開詳情頁並記旗標之後按直接購買若旗標仍新鮮就不再重做連線判斷避免重複佔用串口
boolean cartOn = MyApp.getInstance().getDevSet().isShoppingCar();
if (!cartOn && isMainCabinetSlot(slot)) {
checkPickupOnly(() ->
if (PICKUP_PRECHECK_AT_SELECT && !cartOn && isMainCabinetSlot(slot)) {
checkPickupOnly(slot.getSlot(), () ->
new com.unibuy.smartdevice.ui.dialog.DialogProductDetail(context, handlerMain, slot).show());
return true;
}
@ -169,19 +181,30 @@ public class BasicSaleFlow extends SaleFlowHandler {
}
/**
* 第一階段點選商品只判斷取貨口有沒有東西不做下位機連線判斷
* 取貨口狀態 VMC_02 是下位機自發廣播controller 背景持續解析並快取此處純聆聽快取
* 不重連不清讀緩衝清了會丟掉剛收到的有東西廣播正是漏判主因
* 有東西//升降梯 跳提示擋住不開詳情頁正常或聽不到 onPass 開詳情頁並記旗標
* 第一階段點選商品一次主動問答同時判下位機是否連線取貨口是否有東西比照舊專案 DaiLemei
* 有東西//升降 跳提示擋住不開詳情頁完全無回應 視為下位機未連線擋住
* 正常 onPass 開詳情頁並記旗標直接購買據此略過重複連線判斷BuyDialog 也略過取貨口重驗
*/
private void checkPickupOnly(Runnable onPass) {
final android.app.AlertDialog progress = buildProgressDialog("取貨口檢查中......");
private void checkPickupOnly(int slotnum, Runnable onPass) {
// 單飛已有一個檢查在進行(轉圈提示中)就忽略這次點擊避免疊加查詢把送出佇列塞爆導致失連
if (pickupCheckRunning) return;
pickupCheckRunning = true;
final android.app.AlertDialog progress = buildProgressDialog("下位機連線檢查中......");
try { progress.show(); } catch (Exception ignore) {}
new Thread(() -> {
final int pickup = probePickupHasThing();
new com.unibuy.smartdevice.exception.Logs(BasicSaleFlow.class).info(
"點選商品-取貨口檢查 pickup=" + pickup + " (0=OK,1=有東西,2=門,3=升降梯,4=無回應)");
com.unibuy.smartdevice.exception.Logs logs = new com.unibuy.smartdevice.exception.Logs(BasicSaleFlow.class);
int result = probePickupHasThing(slotnum);
// 真正無回應(連近 7 秒廣播都沒有下位機確實沒在回)才自動重連下位機後再判斷最多 3
for (int attempt = 1; result == PICKUP_NO_RESPONSE && attempt <= 3; attempt++) {
logs.info("點選商品-下位機無回應,自動重連並重試 (" + attempt + "/3)");
try { MyApp.checkAndReconnectDev(); } catch (Exception ignore) {}
result = probePickupHasThing(slotnum);
}
final int pickup = result;
logs.info("點選商品-連線/取貨口檢查 pickup=" + pickup
+ " (0=OK,1=有東西,2=門,3=升降梯,4=無通訊,5=在線但狀態未知)");
postMain(() -> {
pickupCheckRunning = false; // 檢查結束復位允許下一次點擊
dismissQuietly(progress);
switch (pickup) {
case PICKUP_HAVE_THINGS:
@ -194,53 +217,84 @@ public class BasicSaleFlow extends SaleFlowHandler {
showPickupBlockedDialog("升降梯未回到原點,請洽服務人員處理。");
return;
case PICKUP_OK:
// 正常(01)= 機台有回應(下位機在線)且取貨口空 記旗標
// 正常(01)= 下位機在線且取貨口空 記旗標
// 後續直接購買據此略過重複連線判斷BuyDialog 也略過取貨口重驗
lastPickupOkAtMs = android.os.SystemClock.elapsedRealtime();
onPass.run();
break;
case PICKUP_ALIVE_UNKNOWN:
// 下位機在線但此刻沒回報取貨口狀態(忙其他協定3.5 秒內沒等到 VMC_02)
// 選擇保守擋住而非放行寧可多擋也不漏放取貨口有東西請客人稍候再點一次
// 屆時多半已收到取貨口狀態廣播即可正常判定(非未連線故不走重連提示)
showPickupBlockedDialog("正在確認取貨口狀態,請稍候 1~2 秒再點選一次。");
break;
default:
// PICKUP_NO_RESPONSE完全沒聽到機台廣播(不確定是否在線) 不記旗標仍放行開詳情頁
// 讓後續直接購買維持正常的下位機連線判斷
onPass.run();
// PICKUP_NO_RESPONSE機台連任何 VMC 通訊都沒有2 次重連重試仍無回應 判定真的斷線
// 擋住不開詳情頁連線恢復後客戶重點商品即可
showNotConnectedDialog();
}
});
}).start();
}
/**
* 純聆聽下位機自發廣播判斷取貨口狀態不重連不清讀緩衝
* VMC_02有東西時約每 4 秒一次空櫃時約每 8~11 秒一次controller 背景已持續解析並快取
* 策略偏向抓有東西以免漏判開頭補送一次 checkSlot 促請回報不清讀緩衝避免丟掉剛到的廣播
* 最長聆聽 ~9 期間背景快取一出現 05/06/07/09 立即擋聽滿都沒異常才放行
* 主動問答式的連線取貨口判斷比照舊專案 DaiLemei
* 下位機為輪詢式主從 checkSlot 後機台會於下一個 POLL 主動回一筆 VMC_02通常數百毫秒內即到
* controller 收到即更新 lastSlotStatus故此處送出後只採信送出時間之後才到的新回覆getSlotStatusSince
* 一收到明確狀態就立即回傳早退不空等~12 秒仍無回應才補送 syncPackNo 自癒~3.5 秒封頂
* 不清讀緩衝保留廣播快取當有東西(05)的保險避免清緩衝丟掉剛到的廣播過去漏判主因
*/
private int probePickupHasThing() {
private int probePickupHasThing(int slotnum) {
com.unibuy.smartdevice.controller.DevXinYuanController dev =
MyApp.getInstance().getDevXinYuanController();
if (dev == null) return PICKUP_NO_RESPONSE;
// 只補送不清讀緩衝清了會把剛收到的有東西(05)廣播丟掉正是漏判主因
if (dev == null || !dev.isRun()) return PICKUP_NO_RESPONSE;
final long startAt = System.currentTimeMillis();
// 送出佇列若已堆積browse 階段只會是前次查詢殘留的 checkSlot/syncPackNo先清掉
// 確保這次的 checkSlot 立即排到最前面送出不排在一堆舊指令後面而在時效內送不出去
// 這是連續點商品時越查越慢最後全部無回應的關鍵不清會一路堆到過載
try {
dev.addSendBuffer(dev.getDevXinYuan().checkSlotPre(1, 1));
dev.addSendBuffer(dev.getDevXinYuan().checkSlot(1));
if (dev.getCountBySendBuffer() > 6) dev.clearSendBuffers();
} catch (Exception ignore) {}
final int ROUNDS = 36; // 36 × 250ms = 9 秒上限有東西每~4秒一次足以抓到
boolean heardNormal = false;
for (int i = 0; i < ROUNDS; i++) {
if (i == 12 || i == 24) { // 約每 3 秒補送一次含同步包修序號多給下位機回報機會
try {
dev.addSendBuffer(dev.getDevXinYuan().syncPackNo((byte) 0x01));
dev.addSendBuffer(dev.getDevXinYuan().checkSlot(1));
} catch (Exception ignore) {}
}
int st = dev.getRecentSlotStatus(1500); // 只採信近 1.5 秒剛廣播的狀態
// 主動送 checkSlot 促請下位機立即回報取貨口/貨道狀態不清讀緩衝
try {
dev.addSendBuffer(dev.getDevXinYuan().checkSlotPre(slotnum, 1));
dev.addSendBuffer(dev.getDevXinYuan().checkSlot(slotnum));
} catch (Exception ignore) {}
final int MAX_MS = 3500; // 封頂 ~3.5 機台在線通常 <1 秒就回早退提前結束
int retries = 0;
while (System.currentTimeMillis() - startAt < MAX_MS) {
// 只採信送出後才到的新 VMC_02 回覆避免採到查詢前的舊快取
int st = com.unibuy.smartdevice.controller.DevXinYuanController.getSlotStatusSince(startAt);
if (st == 0x05) return PICKUP_HAVE_THINGS;
if (st == 0x06 || st == 0x09) return PICKUP_DOOR;
if (st == 0x07) return PICKUP_ELEVATOR;
if (st == 0x01) heardNormal = true;
try { Thread.sleep(250); } catch (InterruptedException e) { return PICKUP_NO_RESPONSE; }
if (st == 0x01) return PICKUP_OK; // 一收到正常立即放行不空等
// ~1 秒仍無回應補送同步包修序號再查一次比照舊專案 syncPackNo 自癒最多 2
long elapsed = System.currentTimeMillis() - startAt;
if (retries < 2 && elapsed >= (retries + 1) * 1000L) {
retries++;
try {
dev.addSendBuffer(dev.getDevXinYuan().syncPackNo((byte) 0x01));
dev.addSendBuffer(dev.getDevXinYuan().checkSlot(slotnum));
} catch (Exception ignore) {}
}
try { Thread.sleep(150); } catch (InterruptedException e) { return PICKUP_NO_RESPONSE; }
}
// 聽滿 9 秒沒出現任何異常期間至少聽到一次正常(01) 放行(OK)完全沒聽到 02 廣播 無回應
return heardNormal ? PICKUP_OK : PICKUP_NO_RESPONSE;
// 主動查詢在 3.5 秒內沒等到送出後的新回覆不要立刻判斷線先退回最近的 VMC_02 廣播
// 只要近 7 秒內收過任何 VMC_02就代表下位機仍在線(這次只是回覆稍慢)用該狀態判定
// 避免把回覆慢誤判成主櫃尚未連線(實測多數假性未連線的來源)
int cached = com.unibuy.smartdevice.controller.DevXinYuanController.getRecentSlotStatus(7000);
if (cached == 0x05) return PICKUP_HAVE_THINGS;
if (cached == 0x06 || cached == 0x09) return PICKUP_DOOR;
if (cached == 0x07) return PICKUP_ELEVATOR;
if (cached == 0x01) return PICKUP_OK;
// 沒有 VMC_02但機台近 4 秒仍有其他 VMC 通訊(POLL/ACK/金錢/狀態等) 下位機其實在線
// 只是此狀態下沒回報取貨口狀態視為連線正常但取貨口狀態未知別誤判未連線也別做無謂重連
if (com.unibuy.smartdevice.controller.DevXinYuanController.isRecentlyAlive(4000)) {
return PICKUP_ALIVE_UNKNOWN;
}
// 連近 7 秒的廣播 4 秒的任何通訊都沒有 真的沒回應(交由呼叫端自動重連重試)
return PICKUP_NO_RESPONSE;
}
private void postMain(Runnable r) {

View File

@ -52,9 +52,80 @@ public class SystemSetActivity extends AppCompatActivityAbstract {
applyShoppingModeView();
updateCheckBoxes();
applyRolePermission();
applyPortProfileView();
binding.buttonBack.setOnClickListener(v -> finish());
binding.buttonSave.setOnClickListener(v -> onSaveClicked());
binding.buttonPortProfile.setOnClickListener(v -> showPortProfileDialog());
}
/**
* 串口方案本機硬體設定不回寫雲端顯示目前方案並提供切換因應不同 IPC 主板的
* 下位機(主櫃 XinYuan)/刷卡機(NFCPay)串口位置不一致舊版本 ttyS1/ttyS2星科技 ttyS7/ttyS6
*/
private void applyPortProfileView() {
String profile = com.unibuy.smartdevice.devices.PortProfile.getProfile(getCtx());
binding.textViewPortProfile.setText(
com.unibuy.smartdevice.devices.PortProfile.displayName(profile));
}
/** 顯示串口方案選單;選到不同方案時持久化並提示重啟套用(串口於 app 啟動時綁定)。 */
private void showPortProfileDialog() {
final String current = com.unibuy.smartdevice.devices.PortProfile.getProfile(getCtx());
final String[] values = {
com.unibuy.smartdevice.devices.PortProfile.LEGACY,
com.unibuy.smartdevice.devices.PortProfile.STARTECH
};
final String[] labels = {
"舊版本(下位機 ttyS1刷卡機 ttyS2",
"星科技(下位機 ttyS7刷卡機 ttyS6"
};
int checked = com.unibuy.smartdevice.devices.PortProfile.STARTECH.equals(current) ? 1 : 0;
new android.app.AlertDialog.Builder(this)
.setTitle("選擇串口方案(本機)")
.setSingleChoiceItems(labels, checked, null)
.setPositiveButton("套用", (dialog, which) -> {
int pos = ((android.app.AlertDialog) dialog).getListView().getCheckedItemPosition();
if (pos < 0) return;
String selected = values[pos];
if (selected.equals(current)) {
android.widget.Toast.makeText(getCtx(), "方案未變更", android.widget.Toast.LENGTH_SHORT).show();
return;
}
com.unibuy.smartdevice.devices.PortProfile.setProfile(getApplicationContext(), selected);
applyPortProfileView();
getLogs().info("[串口方案] 切換為 " + selected + ",將重啟 app 套用");
confirmRestartForPortProfile(selected);
})
.setNegativeButton("取消", null)
.show();
}
/** 切換方案後需重啟 app 才會以新串口開啟;提示並重啟。 */
private void confirmRestartForPortProfile(String selected) {
new android.app.AlertDialog.Builder(this)
.setTitle("需重啟套用")
.setMessage("已設定為「" + com.unibuy.smartdevice.devices.PortProfile.displayName(selected)
+ "」。\n\n串口於 app 啟動時綁定,需重啟 app 才會以新串口連線。是否立即重啟?")
.setCancelable(false)
.setPositiveButton("立即重啟", (d, w) -> restartApp())
.setNegativeButton("稍後重啟", null)
.show();
}
/** 清空 Task 重新啟動首頁並結束本行程,重新以新串口方案接管硬體。 */
private void restartApp() {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
android.content.Intent intent = new android.content.Intent(
getApplicationContext(), com.unibuy.smartdevice.AppEntry.initialActivity());
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK
| android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
| android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finishAffinity();
android.os.Process.killProcess(android.os.Process.myPid());
});
}
/**

View File

@ -188,8 +188,9 @@ public class DispatchDialog extends DialogAbstract {
setTextViewSmall(message);
setButtonOkText(0);
// 顯示錯誤圖片
// 顯示錯誤圖片客服 QR優先讀裝置上的替換檔沒有才用內建預設
binding.linearLayout4.setVisibility(View.GONE);
applyDeliveryFailedImage();
binding.imgDeliveryFailed.setVisibility(View.VISIBLE);
binding.tvCountdown.setVisibility(View.VISIBLE);
// 啟動倒數計時
@ -433,6 +434,58 @@ public class DispatchDialog extends DialogAbstract {
currentlyProcessingGoods();
}
/**
* 出貨失敗時顯示的客服 QR 為了換客戶不用重編 APK改成執行期優先讀取
* 裝置上的替換檔找不到才用內建預設圖 {@code R.drawable.qrcode_msg}
*
* 替換檔路徑 USB/檔案管理員/adb 放到這裡檔名固定
* Android/data/com.unibuy.smartdevice/files/cs_qrcode.jpg cs_qrcode.png
* adb 範例adb push cs_qrcode.jpg /sdcard/Android/data/com.unibuy.smartdevice/files/
* 換另一個客戶只要覆蓋這個檔即可不用重編不用改程式
*/
private void applyDeliveryFailedImage() {
try {
java.io.File dir = getContext().getExternalFilesDir(null);
if (dir != null) {
java.io.File[] candidates = {
new java.io.File(dir, "cs_qrcode.jpg"),
new java.io.File(dir, "cs_qrcode.png"),
};
for (java.io.File f : candidates) {
if (f.exists() && f.length() > 0) {
android.graphics.Bitmap bmp = decodeSampledBitmap(f);
if (bmp != null) {
binding.imgDeliveryFailed.setImageBitmap(bmp);
getLogs().info("出貨失敗客服圖:使用裝置替換檔 " + f.getAbsolutePath());
return;
}
}
}
}
} catch (Exception e) {
getLogs().warning(e);
}
// 找不到替換檔或解碼失敗 用內建預設圖此版專給代樂美預設為 qrcode_msg_dailemei
binding.imgDeliveryFailed.setImageResource(R.drawable.qrcode_msg_dailemei);
}
/** 依螢幕尺寸做降採樣解碼,避免大圖 OOM。 */
private android.graphics.Bitmap decodeSampledBitmap(java.io.File f) {
android.graphics.BitmapFactory.Options opts = new android.graphics.BitmapFactory.Options();
opts.inJustDecodeBounds = true;
android.graphics.BitmapFactory.decodeFile(f.getAbsolutePath(), opts);
int reqW = getContext().getResources().getDisplayMetrics().widthPixels;
int reqH = getContext().getResources().getDisplayMetrics().heightPixels;
if (reqW <= 0) reqW = 1080;
if (reqH <= 0) reqH = 1920;
int sample = 1;
int halfW = opts.outWidth / 2, halfH = opts.outHeight / 2;
while (halfW / sample >= reqW && halfH / sample >= reqH) sample *= 2;
opts.inSampleSize = sample;
opts.inJustDecodeBounds = false;
return android.graphics.BitmapFactory.decodeFile(f.getAbsolutePath(), opts);
}
private void startCountdown() {
countDownTimer = new CountDownTimer(15_000, 1_000) { // 10 每秒 tick 一次
public void onTick(long millisUntilFinished) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -50,6 +50,49 @@
android:textStyle="bold"
android:visibility="gone" />
<!-- 串口方案(本機硬體設定,不回寫雲端):因應不同 IPC 主板的下位機/刷卡機串口位置不一致 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="串口方案(本機)"
android:textColor="@color/black"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textViewPortProfile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@color/primary"
android:textSize="20sp" />
</LinearLayout>
<Button
android:id="@+id/buttonPortProfile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="切換"
android:textSize="20sp" />
</LinearLayout>
</LinearLayout>
<ScrollView

View File

@ -149,7 +149,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:src="@drawable/qrcode_msg"
android:src="@drawable/qrcode_msg_dailemei"
android:scaleType="fitXY" />
<TextView