feat(cash): 現金連線方案選擇 — 串口/連線設定與相關調整

MainActivity/MyApp/PortProfile/Rs232Dev/DispatchDialog + build.gradle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
terrylee 2026-07-15 11:59:54 +08:00
parent 0c303ffccb
commit b3258c2b0d
6 changed files with 84 additions and 38 deletions

View File

@ -10,7 +10,7 @@ plugins {
//
// =========================================================================
def verMinor = 2
def verPatch = 40
def verPatch = 41
@ -43,7 +43,7 @@ android {
S9XYU {
dimension "hardware"
minSdk 28
minSdk 24 // Android 7.0 28=Android 9 24 Android 7
}
}

View File

@ -584,9 +584,27 @@ public class MainActivity extends AppCompatActivityAbstract {
if (access.isEmpty() || password.isEmpty()) {
getLogs().info("[LoginTrace][" + traceId + "] blocked_empty_input");
Toast.makeText(context, "請輸入帳號和密碼", Toast.LENGTH_SHORT).show();
} else if (MyApp.getInstance().getMachine().getMachineID().isEmpty()
&& "90543814".equals(access) && "1234abcd".equals(password)) {
// 未綁定機台無法向雲端驗證帳密雲端沒有這台的綁定資料login 一定失敗
// 提供預設離線帳密放行讓現場人員能進後台完成綁定綁定後(machineID 非空)
// 此分支不成立即恢復正常雲端帳密驗證
getLogs().info("[LoginTrace][" + traceId + "] unbound_default_login_ok");
MyApp.getInstance().setSuAccess(access); // 標記已登入與雲端成功路徑一致
MyApp.getInstance().setLoginToken("");
if (rememberAccountCb.isChecked()) {
prefs.edit().putString("saved_account", access).apply();
} else {
prefs.edit().remove("saved_account").apply();
}
pendingAction = onSuccess;
// 走與雲端相同的成功回調updateLoginStatus + 關窗 + pendingAction
getHandlerMain().start("MainActivity", 200, "登入成功");
getTools().hideSystemBars((Activity) getCtx());
return;
} else {
Toast.makeText(context, "正在驗證登入...", Toast.LENGTH_SHORT).show();
// 為了處理 StarCloudAPI 登入成功後的回調
// 我們重新設計 handlerMain 讓它能夠在 onSuccess 回調中跑 pendingAction
pendingAction = onSuccess;

View File

@ -635,10 +635,22 @@ public class MyApp extends Application {
// 若不活躍則重連
if (!isAlive) {
staticLogs.info("下位機失連,重新連線中...");
HandlerMain oldHandlerMain = devXinYuanController != null
? devXinYuanController.getHandlerMain() : null;
if (devXinYuanController != null) {
devXinYuanController.shutdown();
// 必須用 releasePort()setRun(false)+closePort()+shutdown()完整關閉舊控制器
// 讓舊的讀取 while(isRun()) 執行緒真正跳出釋放 ttyS7
// 之前只呼叫 shutdown()僅停排程器舊讀取執行緒還活著舊串口還開著
// 接著又 new DevXinYuanController() 把同一個 ttyS7 再開一次 兩條執行緒搶讀同一串口
// 位元組框架被撕裂VMC_41_POLL 認不出出貨指令送不出去 出貨失敗VMC_52 洪水即此徵兆
devXinYuanController.releasePort();
}
devXinYuanController = new DevXinYuanController(devXinYuanController.getHandlerMain());
// 給舊讀取執行緒一點時間因 closePort() 拋例外跳出迴圈釋放串口再重開新串口避免搶佔
try {
Thread.sleep(150);
} catch (InterruptedException ignore) {
}
devXinYuanController = new DevXinYuanController(oldHandlerMain);
devXinYuanController.start();
devXinYuanController.addSendBuffer(
devXinYuanController.getDevXinYuan().syncPackNo((byte) 0x01)

View File

@ -26,8 +26,7 @@ public final class PortProfile {
// 未設定時的預設方案通用版本為 LEGACYttyS1/ttyS2
// ️代樂美等星科技主板專用建置需改為 STARTECHttyS7/ttyS6裝上即用免手動切該改動勿進通用主線
public static final String DEFAULT_PROFILE = STARTECH; // 岱樂妹專版星科技主板(ttyS7/ttyS6)裝上即用免手動切
public static final String DEFAULT_PROFILE = LEGACY;
private PortProfile() {}

View File

@ -95,8 +95,8 @@ public class Rs232Dev {
}
public void clearBuffer() throws LogsIOException {
if (suMissing || isMockMode || inputStream == null) {
if (showLogs) logs.info((suMissing || isMockMode) ? "[MockMode] Skip clearBuffer" : "InputStream 為 null無法清除緩衝區");
if (isMockMode || inputStream == null) {
if (showLogs) logs.info(isMockMode ? "[MockMode] Skip clearBuffer" : "InputStream 為 null無法清除緩衝區");
return;
}
@ -127,6 +127,14 @@ public class Rs232Dev {
}
try {
// 防呆未取得 root 的機台如未 root rk3288 Android 7searchSuPath 找不到 su suPath null
// 若直接 Runtime.exec(null) NPEString.length() App MyApp.onCreate 建立 DevXinYuanController
// 時整個啟動崩潰屢次停止運作這裡先擋掉 su 就略過 chmod回報此埠不可用改由上層進 Mock 模式
// 至少讓 App 能開起來串口需 root 或該埠本身可讀寫才會真正運作
if (suPath == null || suPath.trim().isEmpty()) {
if (showLogs) this.logs.info("suPath 為 null未取得 root/su略過 chmod" + fileComPort.getAbsolutePath());
return false;
}
if (showLogs) this.logs.info("suPath:" + suPath);
Process su = Runtime.getRuntime().exec(suPath);
@ -179,7 +187,10 @@ public class Rs232Dev {
}
public void connectPort(ComPort comPort) throws LogsUnsupportedOperationException, LogsIOException {
if (suMissing || isMockMode) {
// 不再因 root(suMissing)就整個假連線只要埠本身可存取(chmod666File 已回 true進了
// rs232DeviceList)就照開比照有 root 機台(Android 9)的行為讓未 root 但串口權限已開放的
// 機台( rk3288 Android 7ttyS1=crwxrwxrwx)也能真正連下位機真正不可用的埠仍走 isMockMode
if (isMockMode) {
Log.i("Rs232Dev", "[MockMode] Faking connection to: " + comPort.getFilepath());
return;
}
@ -212,6 +223,7 @@ public class Rs232Dev {
public byte[] readToMatch(byte[] stx, int moveLimit) throws LogsIOException {
byte[] buffer = new byte[0];
if (isMockMode || this.serialPort == null) return buffer; // 防呆Mock/未開串口時不讀避免 NPE
try {
InputStream inputStream = this.serialPort.getInputStream();
int moveCount = 0;
@ -234,6 +246,7 @@ public class Rs232Dev {
}
public byte[] readListen(int readMax, long delayMillis) throws LogsIOException {
if (isMockMode || this.serialPort == null) return new byte[0]; // 防呆Mock/未開串口時不讀避免 NPE
InputStream inputStream = this.serialPort.getInputStream();
byte[] buffer = new byte[0];
int readCount = 0;
@ -262,7 +275,7 @@ public class Rs232Dev {
}
public int read(byte[] buffer) throws LogsIOException {
if (suMissing || isMockMode || this.serialPort == null) return 0;
if (isMockMode || this.serialPort == null) return 0;
try {
InputStream inputStream = this.serialPort.getInputStream();
int size = inputStream.read(buffer);
@ -274,7 +287,7 @@ public class Rs232Dev {
}
public void send(byte[] buffer) throws LogsIOException {
if (suMissing || isMockMode || this.serialPort == null) {
if (isMockMode || this.serialPort == null) {
Log.i("Rs232Dev", "[MockMode] send buffer: " + PortTools.showHex(buffer));
return;
}
@ -289,7 +302,7 @@ public class Rs232Dev {
}
public int transmission(byte[] sendBuffer, byte[] readBuffer) throws LogsIOException {
if (suMissing || isMockMode || this.serialPort == null) {
if (isMockMode || this.serialPort == null) {
Log.i("Rs232Dev", "[MockMode] send buffer: " + PortTools.showHex(sendBuffer));
return 0;
}
@ -330,7 +343,7 @@ public class Rs232Dev {
// 新增真正非阻塞讀取方法
public byte[] readNonBlocking() throws LogsIOException {
if (suMissing || isMockMode || this.serialPort == null) return new byte[0];
if (isMockMode || this.serialPort == null) return new byte[0];
try {
InputStream inputStream = serialPort.getInputStream();
int available = inputStream.available();

View File

@ -466,7 +466,7 @@ public class DispatchDialog extends DialogAbstract {
getLogs().warning(e);
}
// 找不到替換檔或解碼失敗 用內建預設圖
binding.imgDeliveryFailed.setImageResource(R.drawable.qrcode_msg_dailemei);
binding.imgDeliveryFailed.setImageResource(R.drawable.qrcode_msg);
}
/** 依螢幕尺寸做降採樣解碼,避免大圖 OOM。 */
@ -1289,26 +1289,16 @@ public class DispatchDialog extends DialogAbstract {
buttonControlCountdown.setCountdown(25);
}
// 出貨前最後一道保險確認下位機連線失連就先自動重連並等到活著再送出貨指令
// 這是百分百在出貨當下遇失連可自動補出貨的關鍵ensure 內含 ping/重連的 Thread.sleep
// 必須背景執行完成後回主執行緒才送 checkSlot(檢查放這裡而非按下直接購買時因為付款到
// 出貨可能隔數十秒當時連著不代表出貨時還連著下位機常在重啟後短暫失聯)
setTextViewCountdown(getCtx().getString(R.string.device_initializing), 65);
new Thread(() -> {
boolean alive = MyApp.ensureXinYuanConnectedForDispatch(8000);
getLogs().info("出貨前確認下位機連線結果: " + alive);
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
if (isFinalizing)
return;
getLogs().info("11111");
MyApp.getInstance().getDevXinYuanController()
.getReadBufferByNow(new DevCheckSlotByVMC(getHandlerMain(), slot));
getLogs().info("22222");
MyApp.getInstance().getDevXinYuanController()
.addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
getLogs().info("33333");
});
}).start();
// 出貨前做預先重連機台通常是好的直接送 checkSlot
// (曾加過出貨前主動重連 isXinYuanAlive NFC 付款期間 VMC 這條安靜 >4s 時會誤判失連
// 對健康機台做破壞性重連觸發 checkAndReconnectDev 雙讀取執行緒 bug VMC_52 洪水POLL 認不出
// 出貨指令送不出去 出貨失敗改回直接送真的失連交由下方 checkSlot 重試輪的後備重連處理)
getLogs().info("11111");
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevCheckSlotByVMC(getHandlerMain(), slot));
getLogs().info("22222");
MyApp.getInstance().getDevXinYuanController()
.addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
getLogs().info("33333");
}
private void devRunElectric(int field, int slot, int increase)
@ -1430,9 +1420,23 @@ public class DispatchDialog extends DialogAbstract {
}
public void setTextViewCountdown(String message, int size) {
if (isChangeStatus) {
binding.dispatchtips.setText(message);
binding.dispatchtips.setTextSize(size);
// 一律回主執行緒更新 UI本方法常被 checkSlot/getProduct 的背景排程回調呼叫
// 若直接在背景執行緒碰 View 會丟 CalledFromWrongThreadExceptionOnly the original thread
// 該例外被上層 catch 會連帶跳過緊接其後的 getProduct(吐貨指令)送出 出貨永遠卡在出貨中
// (Android 9 機台時序上剛好沒觸發Android 7 上必現改為 post 到主執行緒即可根治)
android.app.Activity act = (getCtx() instanceof android.app.Activity) ? (android.app.Activity) getCtx() : null;
Runnable r = () -> {
if (isChangeStatus) {
try {
binding.dispatchtips.setText(message);
binding.dispatchtips.setTextSize(size);
} catch (Exception ignore) {}
}
};
if (act != null) {
act.runOnUiThread(r);
} else {
new android.os.Handler(android.os.Looper.getMainLooper()).post(r);
}
}