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:
parent
436e9b8328
commit
954d4a3716
@ -10,7 +10,7 @@ plugins {
|
||||
// 避免不同尺寸機台間版本號產生衝突。
|
||||
// =========================================================================
|
||||
def verMinor = 2
|
||||
def verPatch = 36
|
||||
def verPatch = 37
|
||||
|
||||
|
||||
|
||||
|
||||
@ -210,10 +210,12 @@ public class MainActivity extends AppCompatActivityAbstract {
|
||||
}
|
||||
|
||||
// 🔒 販賣機機台:導航列沒真的關掉就擋下並提醒(後門長按不受此限)
|
||||
if (!isNavigationReallyHidden()) {
|
||||
showMustHideSystemBarsDialog();
|
||||
return;
|
||||
}
|
||||
// → 依需求隱藏此提示:進銷售平台不再檢查導航列,直接進入。
|
||||
// (保留原判斷程式碼於此,日後如要恢復把下方三行取消註解即可。)
|
||||
// if (!isNavigationReallyHidden()) {
|
||||
// showMustHideSystemBarsDialog();
|
||||
// return;
|
||||
// }
|
||||
|
||||
enterShoppingPlatform();
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -46,13 +46,17 @@ public enum ComPort {
|
||||
}
|
||||
|
||||
public String getFilepath() {
|
||||
// 下位機/刷卡機串口:S12XYU 與 S9XYU 兩個 flavor 皆使用 ttyS1(下位機 XinYuan)/ ttyS2(刷卡機 NFCPay)。
|
||||
// (原 S12XYU 為 ttyS7/ttyS6,2026-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;
|
||||
}
|
||||
|
||||
@ -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)";
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
* 一收到明確狀態就立即回傳(早退),不空等;~1、2 秒仍無回應才補送 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) {
|
||||
|
||||
@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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) {
|
||||
|
||||
BIN
app/src/main/res/drawable/qrcode_msg_dailemei.jpg
Normal file
BIN
app/src/main/res/drawable/qrcode_msg_dailemei.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user