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:
parent
0c303ffccb
commit
b3258c2b0d
@ -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 機台可安裝)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -26,8 +26,7 @@ public final class PortProfile {
|
||||
|
||||
// 未設定時的預設方案。通用版本為 LEGACY(ttyS1/ttyS2)。
|
||||
// ⚠️代樂美等星科技主板專用建置需改為 STARTECH(ttyS7/ttyS6,裝上即用免手動切),該改動勿進通用主線。
|
||||
public static final String DEFAULT_PROFILE = STARTECH; // 岱樂妹專版:星科技主板(ttyS7/ttyS6),裝上即用免手動切
|
||||
|
||||
public static final String DEFAULT_PROFILE = LEGACY;
|
||||
|
||||
private PortProfile() {}
|
||||
|
||||
|
||||
@ -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 7)searchSuPath 找不到 su → suPath 為 null。
|
||||
// 若直接 Runtime.exec(null) 會 NPE(String.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 7,ttyS1=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();
|
||||
|
||||
@ -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 會丟 CalledFromWrongThreadException(「Only 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user