瀏覽代碼

refactor(更新) :
- `CustomMarkLayer` 触摸事件处理重构,改用屏幕坐标系以修复点击不准的问题,并增大图标尺寸。
- `CustomSwitchStationLayer` 整体重构,优化了绘制性能、数据处理、点击交互及长按逻辑。
- BLE 相关逻辑优化:
- 重构了蓝牙扫描机制(`BleUtil`),提高扫描的稳定性和可靠性。
- 修复了连接调度器(`BleQueueDispatcher`)中的断开连接逻辑,并统一在断开前发送关机/重启指令。
- `ModBusController` 逻辑修改,设备初始化时默认关闭所有钥匙仓位,而非打开。
- `build.gradle` 配置 ABI splits,以按 CPU 架构生成不同的 APK 包。
- `SwitchStatusActivity` 列表项现在显示电机名称和电机编码。
- `MapView` 调整并明确了地图坐标与屏幕坐标的转换 API。
- 移除了人脸识别引擎(`ArcSoftUtil`)初始化失败时的 Toast 提示。

周文健 1 月之前
父節點
當前提交
1ceccffe94

+ 24 - 2
app/build.gradle

@@ -51,13 +51,25 @@ android {
     }
 
     android.applicationVariants.all { variant ->
-        variant.outputs.all {
+        variant.outputs.all { output ->
             def buildType = variant.buildType.name
-            outputFileName = "ISCS_${buildType.capitalize()}_v${variant.versionName}_${variant.versionCode}.apk"
+            // 获取当前 output 的 ABI;没有则视为 "universal"
+            def abi = null
+            try {
+                abi = output.getFilter(com.android.build.OutputFile.ABI)
+            } catch (Throwable ignore) {
+                def f = output.filters?.find { it.filterType?.toString()?.toLowerCase() == 'abi' }
+                abi = f?.identifier
+            }
+            if (abi == null || abi.trim().isEmpty()) {
+                abi = "universal"
+            }
+            outputFileName = "ISCS_${buildType.capitalize()}_${abi}_v${variant.versionName}_${variant.versionCode}.apk"
         }
     }
 
 
+
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
@@ -65,6 +77,16 @@ android {
     kotlinOptions {
         jvmTarget = '1.8'
     }
+
+    splits {
+        abi {
+            enable true          // 开启 ABI 分 APK
+            reset()              // 先清空默认 ABI
+            include 'armeabi-v7a', 'arm64-v8a' // 需要哪些就写哪些;如需 x86/x86_64 也可以加上
+            universalApk true    // 同时产一个“全 ABI”的通用 APK(可选)
+        }
+    }
+
 }
 
 dependencies {

+ 13 - 8
app/src/main/java/com/grkj/iscs_mars/BusinessManager.kt

@@ -16,7 +16,6 @@ import com.grkj.iscs_mars.ble.BleReturnDispatcher
 import com.grkj.iscs_mars.ble.BleSendDispatcher
 import com.grkj.iscs_mars.ble.BleUtil
 import com.grkj.iscs_mars.ble.CustomBleWriteCallback
-import com.grkj.iscs_mars.enums.NoKeyReason
 import com.grkj.iscs_mars.extentions.removeLeadingZeros
 import com.grkj.iscs_mars.extentions.serialNo
 import com.grkj.iscs_mars.extentions.startsWith
@@ -133,7 +132,7 @@ object BusinessManager {
     /**
      * 检查钥匙任务
      */
-    var checkKeyInfoTask: CheckKeyInfoTask = CheckKeyInfoTask()
+//    var checkKeyInfoTask: CheckKeyInfoTask = CheckKeyInfoTask()
 
     /**
      * 初始化消息总线
@@ -157,8 +156,8 @@ object BusinessManager {
                 }
 
                 MsgEventConstants.MSG_EVENT_INIT_KEY_COMPLETE -> {
-                    val job = CronJobScanner.scanJobs(checkKeyInfoTask)
-                    MyApplication.cronJobManager.registerJobs(job)
+//                    val job = CronJobScanner.scanJobs(checkKeyInfoTask)
+//                    MyApplication.cronJobManager.registerJobs(job)
                 }
                 // 钥匙当前模式
                 MSG_EVENT_CURRENT_MODE -> {
@@ -171,6 +170,7 @@ object BusinessManager {
                         1 -> {
                             if (it.data.res == 1) {
                                 // 只能在这里断开,不能全部断开
+                                BleCmdManager.shutdownOrRebootReq(mac = it.data.bleBean.bleDevice.mac)
                                 BleSendDispatcher.scheduleDisconnect(it.data.bleBean.bleDevice.mac)
                                 // 打开钥匙卡扣
                                 val keyBean =
@@ -212,10 +212,12 @@ object BusinessManager {
                                 ModBusController.updateKeyReadyStatus(
                                     it.data.bleBean.bleDevice.mac, true, 2
                                 )
+                                BleReturnDispatcher.scheduleDisconnect(it.data.bleBean.bleDevice.mac)
                                 // 延时再次获取当前状态,触发handleCurrentMode里工作票下发状态检查
-                                Executor.delayOnMain(500) {
-                                    getCurrentStatus(1, it.data.bleBean.bleDevice)
-                                }
+                                //切换回待机之后不再次查询状态,没有意义,单机操作没有并发需求
+//                                Executor.delayOnMain(500) {
+//                                    getCurrentStatus(1, it.data.bleBean.bleDevice)
+//                                }
                             } else {
                                 LogUtil.e("切换待机模式失败 : ${it.data.bleBean.bleDevice.mac}")
                                 Executor.delayOnMain(500) {
@@ -1525,6 +1527,7 @@ object BusinessManager {
                         }) {
                             sendLoadingEventMsg(null, false)
                             ToastUtils.tip(R.string.continue_the_ticket)
+                            BleCmdManager.shutdownOrRebootReq(mac = bleDevice.mac)
                             BleReturnDispatcher.scheduleDisconnect(bleDevice.mac)
                             // 打开卡扣,防止初始化的时候选择不处理钥匙导致无法使用
                             val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
@@ -1838,7 +1841,7 @@ object BusinessManager {
                                 ModBusController.getKeyByRfid(
                                     info.nfc
                                 )?.mac?.let {
-                                    unregisterConnectListener(it)
+                                    BleSendDispatcher.scheduleDisconnect(it)
                                 }
                                 //待机数不够就再连一把,但不能是原来那把
                                 ModBusController.getKeyByRfid(
@@ -2000,6 +2003,7 @@ object BusinessManager {
                     updateBo?.let { itBO ->
                         if (BleReturnDispatcher.isConnected(currentModeMsg.bleBean.bleDevice.mac)) {
                             LogUtil.i("当前钥匙在归还队列,断开连接")
+                            BleCmdManager.shutdownOrRebootReq(currentModeMsg.bleBean.bleDevice.mac)
                             BleReturnDispatcher.scheduleDisconnect(currentModeMsg.bleBean.bleDevice.mac)
                         }
                         if (BleSendDispatcher.isConnected(currentModeMsg.bleBean.bleDevice.mac)) {
@@ -2017,6 +2021,7 @@ object BusinessManager {
                             currentModeMsg.bleBean.bleDevice.mac, true, 4
                         )
                         sendLoadingEventMsg(null, false)
+                        BleCmdManager.shutdownOrRebootReq(mac = currentModeMsg.bleBean.bleDevice.mac)
                         BleReturnDispatcher.scheduleDisconnect(currentModeMsg.bleBean.bleDevice.mac)
                         //连上之后没有工作票要下发就断开 看是否还有设备等待连接,没有就不断开,有就让路,一般是初始化的时候
                         ThreadUtils.runOnIO {

+ 2 - 4
app/src/main/java/com/grkj/iscs_mars/MyApplication.kt

@@ -36,14 +36,12 @@ class MyApplication : Application() {
 //        LogUtil.init(instance!!, FileUtil.ROOT_APP + FileUtil.LOG_DIR)
         // 路径:sdcard/Android/data/com.grkj.iscs/files/iscs/log
         LogUtil.init(this, "${FileUtil.getRootFolder(this)?.absolutePath}$LOG_DIR")
-//        CrashUtil.instance.init(applicationContext)
         BleUtil.instance?.initBle(this)
         NetHttpManager.getInstance().initCtx(this)
 
         BusinessManager.initMsgEventBus()
-
-//        ArcSoftUtil.checkActiveStatus(this)
-
+        ArcSoftUtil.checkActiveStatus(SIKCore.getApplication())
+        ArcSoftUtil.initEngine(SIKCore.getApplication())
 
         NetApi.logout()
         SPUtils.clearLoginUser(this)

+ 1 - 1
app/src/main/java/com/grkj/iscs_mars/ble/BleCmdManager.kt

@@ -333,7 +333,7 @@ object BleCmdManager {
     fun shutdownOrRebootReq(
         mac: String?,
         isShutdown: Boolean = true,
-        callback: CustomBleWriteCallback?
+        callback: CustomBleWriteCallback? = null
     ) {
         BusinessManager.getBleDeviceByMac(mac)?.let {
             BleUtil.Companion.instance?.write(

+ 7 - 10
app/src/main/java/com/grkj/iscs_mars/ble/BleConnectionManager.kt

@@ -6,7 +6,6 @@ import android.bluetooth.BluetoothGattCharacteristic
 import androidx.appcompat.app.AppCompatActivity
 import com.grkj.iscs_mars.BusinessManager
 import com.grkj.iscs_mars.BusinessManager.deviceList
-import com.grkj.iscs_mars.BusinessManager.getBleDeviceByMac
 import com.grkj.iscs_mars.BusinessManager.isTestMode
 import com.grkj.iscs_mars.BusinessManager.removeExceptionKey
 import com.grkj.iscs_mars.BusinessManager.sendEventMsg
@@ -15,12 +14,10 @@ import com.grkj.iscs_mars.R
 import com.grkj.iscs_mars.extentions.toHexStrings
 import com.grkj.iscs_mars.modbus.ModBusController
 import com.grkj.iscs_mars.modbus.ModBusController.controlKeyCharge
-import com.grkj.iscs_mars.model.Constants
 import com.grkj.iscs_mars.model.Constants.PERMISSION_REQUEST_CODE
 import com.grkj.iscs_mars.model.eventmsg.LoadingMsg
 import com.grkj.iscs_mars.model.eventmsg.MsgEvent
 import com.grkj.iscs_mars.model.eventmsg.MsgEventConstants.MSG_EVENT_LOADING
-import com.grkj.iscs_mars.service.CheckKeyInfoTask
 import com.grkj.iscs_mars.util.ActivityUtils
 import com.grkj.iscs_mars.util.CommonUtils
 import com.grkj.iscs_mars.util.Executor
@@ -40,7 +37,6 @@ import java.util.LinkedList
 import java.util.concurrent.atomic.AtomicInteger
 import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
-import kotlin.text.clear
 
 /**
  * BLE 连接管理工具:保持原有扫描、连接、监听、取 Token 流程,
@@ -288,7 +284,7 @@ object BleConnectionManager {
                 if (mac.equals(bleMac, ignoreCase = true)) {
                     // 找到目标设备,马上停止扫描
                     LogUtil.i("蓝牙连接-找到目标设备 $bleMac,停止扫描并尝试连接")
-                    BleManager.cancelScan()
+                    BleUtil.instance?.stopScan()
                     // 立刻调用 doConnect,下一步进入连接流程
                     doConnect(newDevice, isNeedLoading, prepareDoneCallBack)
                 }
@@ -309,7 +305,7 @@ object BleConnectionManager {
             }
 
             override fun onFilter(bleDevice: BleDevice): Boolean {
-                return bleDevice.name == Constants.BLE_LOCAL_NAME
+                return bleDevice.name == BleConst.BLE_LOCAL_NAME
             }
         })
     }
@@ -623,6 +619,7 @@ object BleConnectionManager {
     /**
      * 扫描在线的蓝牙钥匙并发送指令关机
      */
+    @SuppressLint("MissingPermission")
     suspend fun scanOnlineKeyLockMacAndSwitchModeToClose(): Boolean {
         return suspendCancellableCoroutine { parentCont ->
             BleUtil.instance?.scan(object : CustomBleScanCallback() {
@@ -659,8 +656,8 @@ object BleConnectionManager {
                         devicesSnapshot.forEach {
                             val connected = tryConnectWithOptionalCharge(it.mac, false)
                             if (connected) {
-                                val sendSuccess = sendEmptyTicketJson(it)
-                                LogUtil.i("设备录入-发送切换工作模式:${it.mac},${sendSuccess}")
+                                BleCmdManager.shutdownOrRebootReq(it.mac)
+                                BleSendDispatcher.scheduleDisconnect(it.mac)
                                 handlerDeviceSize.addAndGet(1)
                             } else {
                                 handlerDeviceSize.addAndGet(1)
@@ -673,7 +670,7 @@ object BleConnectionManager {
                 }
 
                 override fun onFilter(bleDevice: BleDevice): Boolean {
-                    return bleDevice.name == Constants.BLE_LOCAL_NAME
+                    return bleDevice.name == BleConst.BLE_LOCAL_NAME
                 }
             })
         }
@@ -805,7 +802,7 @@ object BleConnectionManager {
             }
 
             override fun onFilter(bleDevice: BleDevice): Boolean {
-                return bleDevice.name == Constants.BLE_LOCAL_NAME
+                return bleDevice.name == BleConst.BLE_LOCAL_NAME
             }
         })
     }

+ 5 - 0
app/src/main/java/com/grkj/iscs_mars/ble/BleConst.kt

@@ -5,6 +5,11 @@ package com.grkj.iscs_mars.ble
  */
 object BleConst {
 
+    const val BLE_LOCAL_NAME = "keyLock"
+
+    const val SCAN_TIMEOUT = 20_000L
+    var needDeviceName = true
+
     const val MAX_KEY_STAND_BY: Int = 1
     const val MAX_KEY_CONNECT_COUNT: Int = 2
 

+ 10 - 2
app/src/main/java/com/grkj/iscs_mars/ble/BleQueueDispatcher.kt

@@ -123,8 +123,14 @@ abstract class BleQueueDispatcher {
     @SuppressLint("MissingPermission")
     @Synchronized
     fun scheduleDisconnect(mac: String, delayMillis: Long = 0) {
-        if (!connectedMacs.contains(mac)) return
         pendingDisconnectJobs[mac]?.cancel()
+        // 【修复点1】未连接也要清:把队列/连接中的该 mac 直接清掉,避免后续再被调度
+        if (!connectedMacs.contains(mac)) {
+            LogUtil.i("scheduleDisconnect: not connected → clear only | mac=$mac")
+            BleConnectionManager.unregisterConnectListener(mac)
+            clear(mac)  // 会清 taskQueue / activeMacs / connectedMacs / pendingDisconnectJobs
+            return
+        }
         val job = dispatcherScope.launch {
             delay(delayMillis)
             synchronized(this@BleQueueDispatcher) {
@@ -133,11 +139,13 @@ abstract class BleQueueDispatcher {
                         val deviceBean = BusinessManager.deviceList.find { it.bleDevice.mac == mac }
                         deviceBean?.let {
                             disconnectDeviceByMac(mac)
+                            BleConnectionManager.unregisterConnectListener(mac)
                             BusinessManager.deviceList.removeIf { it.bleDevice.mac == deviceBean.bleDevice.mac }
                         }
                         LogUtil.i("当前线程:${Thread.currentThread().name}")
                         clear(mac)
                         if ((activeMacs.size + connectedMacs.size) < maxConnections && taskQueue.isNotEmpty()) {
+                            delay(1000)
                             tryStartNext()
                         }
                     }
@@ -155,7 +163,7 @@ abstract class BleQueueDispatcher {
     private fun disconnectDeviceByMac(mac: String) {
         val device = BleManager.getAllConnectedDevice().find { it.mac == mac }
         if (device != null) {
-            BleCmdManager.bleDisconnectReq(mac, null)
+            BleCmdManager.shutdownOrRebootReq(mac)
             BleManager.disconnect(device)
         }
     }

+ 68 - 28
app/src/main/java/com/grkj/iscs_mars/ble/BleUtil.kt

@@ -5,15 +5,12 @@ import android.annotation.SuppressLint
 import android.app.Application
 import android.bluetooth.BluetoothGatt
 import android.bluetooth.le.ScanSettings
-import android.os.Build
 import android.util.Log
 import androidx.annotation.RequiresPermission
-import com.grkj.iscs_mars.model.Constants.BLE_LOCAL_NAME
 import com.grkj.iscs_mars.ble.BleConst.INDICATE_UUID
 import com.grkj.iscs_mars.ble.BleConst.MTU
 import com.grkj.iscs_mars.ble.BleConst.SERVICE_UUID
 import com.grkj.iscs_mars.extentions.toHexStrings
-import com.grkj.iscs_mars.model.Constants
 import com.grkj.iscs_mars.util.log.LogUtil
 import com.huyuhui.fastble.BleManager
 import com.huyuhui.fastble.callback.BleGattCallback
@@ -28,6 +25,7 @@ import com.sik.sikcore.thread.ThreadUtils
  * 蓝牙工具类
  */
 class BleUtil private constructor() {
+    private var inScan: Boolean = false
 
     companion object {
         var instance: BleUtil? = null
@@ -49,37 +47,79 @@ class BleUtil private constructor() {
                     splitWriteNum = 500
                     operateTimeout = OPERATE_TIMEOUT
                 }
-            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
-                //Android 12及以上不允许添加过滤器
-                val bleScanRuleConfig = BleScanRuleConfig.Builder()
-                    .setScanTimeOut(6_000L)
-                    .setDeviceName(Constants.BLE_LOCAL_NAME)
-                    .apply {
-                        setScanSettings(ScanSettings.Builder().apply {
-                            this.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
-                            this.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
-                            this.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
-                        }.build())
-                    }
-                    .build()
-                BleManager.bleScanRuleConfig = bleScanRuleConfig
-            }
         } catch (e: Exception) {
             Log.d("initBlueTooth", "蓝牙初始化:${e.message}")
         }
     }
 
-    @SuppressLint("MissingPermission")
-    fun scan(bleScanCallback: CustomBleScanCallback) {
-        if (BleManager.isSupportBle(SIKCore.getApplication())) {
-            if (BleManager.isBleEnable(SIKCore.getApplication())) {
-                BleManager.scan(bleScanCallback)
-            } else {
-                bleScanCallback.onPrompt("请打开您的蓝牙后重试")
+    @Synchronized
+    @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
+    fun scan(cb: CustomBleScanCallback) {
+        val ctx = SIKCore.getApplication()
+
+        // 0) 前置检查(权限/蓝牙开关/定位开关)
+        if (!BleManager.isSupportBle(ctx)) return cb.onPrompt("设备不支持 BLE")
+        if (!BleManager.isBleEnable(ctx)) return cb.onPrompt("请先打开蓝牙")
+        // Android 6–11:需要位置权限 + 开位置开关;12+ 视 ROM
+        // 这里建议把你项目里的权限/定位检查封装下,失败就 return
+
+        // 1) 统一设置扫描参数(含 S+)
+        val settings = ScanSettings.Builder()
+            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+            .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
+            .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
+            .setReportDelay(0)
+            .build()
+        BleManager.bleScanRuleConfig = BleScanRuleConfig.Builder()
+            .apply {
+                if (BleConst.needDeviceName) {
+                    setDeviceName(BleConst.BLE_LOCAL_NAME)
+                }
+                setScanTimeOut(BleConst.SCAN_TIMEOUT)
+                setScanSettings(settings)
             }
-        } else {
-            bleScanCallback.onPrompt("您的设备不支持蓝牙设备")
-        }
+            .build()
+
+        // 2) 保证单实例扫描:如在扫,先“同步停”,再启动
+        if (inScan) stopScan()   // 用你的 stopScan(),会置 inScan=false
+
+        // 3) 启动(先置位,避免 onScanStarted 不回调时卡死在 true)
+        inScan = true
+        BleManager.scan(object : CustomBleScanCallback() {
+            override fun onScanStarted(ok: Boolean) {
+                inScan = ok
+                cb.onScanStarted(ok)
+                if (!ok) {
+                    stopScan()
+                    cb.onPrompt("启动扫描失败,请检查权限/系统设置")
+                }
+            }
+
+            override fun onFilter(d: BleDevice): Boolean {
+                // 放宽一点,避免漏掉只在 SR 里带名的设备
+                return !BleConst.needDeviceName ||
+                        (d.name ?: "").equals(BleConst.BLE_LOCAL_NAME, ignoreCase = true)
+            }
+
+            override fun onLeScan(old: BleDevice, new: BleDevice, scannedBefore: Boolean) =
+                cb.onLeScan(old, new, scannedBefore)
+
+            override fun onScanFinished(list: List<BleDevice>) {
+                stopScan()
+                cb.onScanFinished(list)
+            }
+
+            override fun onPrompt(p: String?) = cb.onPrompt(p)
+        })
+    }
+
+    /**
+     * 停止扫描
+     */
+    @SuppressLint("MissingPermission")
+    fun stopScan() {
+        inScan = false
+        BleManager.cancelScan()
     }
 
     @SuppressLint("MissingPermission")

+ 14 - 0
app/src/main/java/com/grkj/iscs_mars/modbus/ModBusCMDHelper.kt

@@ -134,6 +134,20 @@ object ModBusCMDHelper {
             )
         )
     }
+    /**
+     * 操作钥匙/便携式底座钥匙卡扣 关闭,所有
+     */
+    fun generateAllKeyBuckleCloseCmd(type: Byte): MBFrame {
+        return MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(
+                0x00,
+                0x11,
+                (if (type == DeviceConst.DOCK_TYPE_PORTABLE) 0b00110000 else 0xff).toByte(),
+                0xff.toByte()
+            )
+        )
+    }
 
     /**
      * 操作钥匙/便携式底座钥匙充电,一次只操作一个卡扣

+ 44 - 41
app/src/main/java/com/grkj/iscs_mars/modbus/ModBusController.kt

@@ -5,6 +5,7 @@ import android.content.Context
 import com.grkj.iscs_mars.BusinessManager
 import com.grkj.iscs_mars.BusinessManager.CAN_RETURN
 import com.grkj.iscs_mars.R
+import com.grkj.iscs_mars.ble.BleCmdManager
 import com.grkj.iscs_mars.ble.BleReturnDispatcher
 import com.grkj.iscs_mars.ble.BleSendDispatcher
 import com.grkj.iscs_mars.enums.NoKeyReason
@@ -162,7 +163,7 @@ object ModBusController {
                 LogUtil.i("initDevicesStatus 设备(${bytes[0].toInt()})类型:$type")
             }
             //先打开所有钥匙仓位,再进行初始化,防止一开始锁仓没有钥匙,但是锁定状态下会出现状态锁定
-            controlAllKeyBuckleOpen()
+            controlAllKeyBuckleClose()
             // TODO 待完善
             Executor.repeatOnMain({
                 if (isInitReady) {
@@ -246,8 +247,7 @@ object ModBusController {
                                         .all {
                                             it.deviceList.filter { it.type == DeviceConst.DEVICE_TYPE_KEY }
                                                 .filterIsInstance<DockBean.KeyBean>()
-                                                .filter { it.isExist }
-                                                .all {
+                                                .filter { it.isExist }.all {
                                                     LogUtil.i("钥匙否是准备完毕:${it.rfid}")
                                                     it.rfid != null
                                                 }
@@ -255,8 +255,7 @@ object ModBusController {
                                 if (isKeyReady) {
                                     BusinessManager.sendEventMsg(
                                         MsgEvent(
-                                            MsgEventConstants.MSG_EVENT_INIT_KEY_COMPLETE,
-                                            null
+                                            MsgEventConstants.MSG_EVENT_INIT_KEY_COMPLETE, null
                                         )
                                     )
                                 }
@@ -286,11 +285,9 @@ object ModBusController {
     fun getExistsKey(): List<DockBean.KeyBean> {
         return dockList.filter {
             it.type in listOf(
-                DeviceConst.DOCK_TYPE_KEY,
-                DeviceConst.DOCK_TYPE_PORTABLE
+                DeviceConst.DOCK_TYPE_KEY, DeviceConst.DOCK_TYPE_PORTABLE
             )
-        }
-            .flatMap { it.deviceList }.filterIsInstance<DockBean.KeyBean>()
+        }.flatMap { it.deviceList }.filterIsInstance<DockBean.KeyBean>()
     }
 
     /**
@@ -749,6 +746,21 @@ object ModBusController {
             }
     }
 
+    /**
+     * 关闭所有钥匙锁仓
+     */
+    fun controlAllKeyBuckleClose() {
+        dockList.filter { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE }
+            .forEach { dock ->
+                dock.type?.let { dockType ->
+                    ModBusCMDHelper.generateAllKeyBuckleCloseCmd(dockType).let { cmd ->
+                        modBusManager?.sendTo(dock.addr, cmd) { res ->
+                        }
+                    }
+                }
+            }
+    }
+
     /**
      * 关锁并充电
      */
@@ -901,8 +913,7 @@ object ModBusController {
         }
         if (BleSendDispatcher.isConnected(
                 BusinessManager.getBleDeviceByMac(key.mac)?.bleDevice?.mac ?: ""
-            ) ||
-            BleReturnDispatcher.isConnected(
+            ) || BleReturnDispatcher.isConnected(
                 BusinessManager.getBleDeviceByMac(key.mac)?.bleDevice?.mac ?: ""
             )
         ) {
@@ -1052,19 +1063,19 @@ object ModBusController {
                 .sortedBy { it.addr }.onEach { it.deviceList.sortBy { dev -> dev.idx } }
 
         val noBelongDevice = BleManager.getAllConnectedDevice().filter {
-            !BleSendDispatcher.isConnected(it.mac) &&
-                    !BleSendDispatcher.isConnecting(it.mac) &&
-                    !BleReturnDispatcher.isConnected(it.mac) &&
-                    !BleReturnDispatcher.isConnecting(it.mac)
+            !BleSendDispatcher.isConnected(it.mac) && !BleSendDispatcher.isConnecting(it.mac) && !BleReturnDispatcher.isConnected(
+                it.mac
+            ) && !BleReturnDispatcher.isConnecting(it.mac)
         }
         LogUtil.i("检查到不属于任何队列的设备:${noBelongDevice.map { it.mac }},立即断开连接让路")
         noBelongDevice.forEach {
+            BleCmdManager.shutdownOrRebootReq(it.mac)
             BleManager.disconnect(it)
         }
         var keyList = keyDockList.flatMap { it.deviceList }.apply {
             LogUtil.i("keyStatus:${this}")
-        }.filterIsInstance<DockBean.KeyBean>()
-            .filterIndexed { idx, _ -> (idx + 1) !in slotCols }.filter { kb ->
+        }.filterIsInstance<DockBean.KeyBean>().filterIndexed { idx, _ -> (idx + 1) !in slotCols }
+            .filter { kb ->
                 !kb.rfid.isNullOrEmpty() && kb.rfid !in exceptionKeysRfid && kb.mac !in exceptionKeysMac && !kb.mac.isNullOrEmpty() && kb.isExist && !BleReturnDispatcher.isConnected(
                     kb.mac ?: ""
                 ) && !BleReturnDispatcher.isConnecting(kb.mac ?: "")
@@ -1080,14 +1091,12 @@ object ModBusController {
         sendConnectingAndConnected.filter { it !in keyList.map { it.mac } }.forEach {
             BleSendDispatcher.scheduleDisconnect(it)
         }
-        keyList = keyList.sortedWith(
-            compareByDescending<DockBean.KeyBean> {
-                BleSendDispatcher.isConnected(it.mac ?: "") || BleSendDispatcher.isConnecting(
-                    it.mac ?: ""
-                )
-            }    // 主键:在线优先
-                .thenByDescending { it.power }
-        )
+        keyList = keyList.sortedWith(compareByDescending<DockBean.KeyBean> {
+            BleSendDispatcher.isConnected(it.mac ?: "") || BleSendDispatcher.isConnecting(
+                it.mac ?: ""
+            )
+        }    // 主键:在线优先
+            .thenByDescending { it.power })
         val powerLowerLimit = ISCSDomainData.powerMin
         val connectedKey = keyList.find { BleSendDispatcher.isConnected(it.mac ?: "") }
         if (connectedKey != null) {
@@ -1096,10 +1105,9 @@ object ModBusController {
                     BleSendDispatcher.scheduleDisconnect(it)
                 }
             } else {
-                val addr =
-                    keyDockList.firstOrNull {
-                        it.getKeyList().any { it.rfid == connectedKey.rfid }
-                    }?.addr
+                val addr = keyDockList.firstOrNull {
+                    it.getKeyList().any { it.rfid == connectedKey.rfid }
+                }?.addr
                 if (addr != null) {
                     return addr to connectedKey
                 }
@@ -1115,10 +1123,9 @@ object ModBusController {
                 val result = suspendCoroutine { cont ->
                     BleSendDispatcher.submit(connectingKey.mac ?: "") { connected ->
                         if (connected) {
-                            val addr =
-                                keyDockList.firstOrNull {
-                                    it.getKeyList().any { it.rfid == connectingKey.rfid }
-                                }?.addr
+                            val addr = keyDockList.firstOrNull {
+                                it.getKeyList().any { it.rfid == connectingKey.rfid }
+                            }?.addr
                             if (addr != null) {
                                 cont.resume(addr to connectingKey)
                             } else {
@@ -1148,10 +1155,9 @@ object ModBusController {
                             }
                         } else {
                             // 找到第一个能连的:从 keyDockList 里拿同 rfid 的 addr
-                            val addr =
-                                keyDockList.firstOrNull {
-                                    it.getKeyList().any { it.rfid == kb.rfid }
-                                }?.addr
+                            val addr = keyDockList.firstOrNull {
+                                it.getKeyList().any { it.rfid == kb.rfid }
+                            }?.addr
                             if (addr != null) {
                                 if (cont.context.isActive) {
                                     cont.resume(addr to kb)
@@ -1228,10 +1234,7 @@ object ModBusController {
         return dockList.filter { it.type == DOCK_TYPE_COLLECT }.sortedBy { it.addr }
             .flatMap { it.getSwitchList() }.mapIndexed { index, switchBean ->
                 DockBean.SwitchBean(
-                    index,
-                    switchBean.switchBoardAddr,
-                    switchBean.enabled,
-                    switchBean.changed
+                    index, switchBean.switchBoardAddr, switchBean.enabled, switchBean.changed
                 )
             }.toMutableList()
     }

+ 0 - 1
app/src/main/java/com/grkj/iscs_mars/model/Constants.kt

@@ -13,7 +13,6 @@ object Constants {
     const val DEVICE_TYPE_HYBRID = "Android_Hybrid"     // 混合柜
 
     const val PERMISSION_REQUEST_CODE = 1
-    const val BLE_LOCAL_NAME = "keyLock"
 
     const val BLE_DISCONNECT_DELAY_TIME = 60_000L
 

+ 1 - 1
app/src/main/java/com/grkj/iscs_mars/receivers/BootReceiver.kt

@@ -30,7 +30,7 @@ class BootReceiver : BroadcastReceiver() {
                 );
             val mAlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
             mAlarmManager.set(
-                AlarmManager.RTC, System.currentTimeMillis() + 2000,
+                AlarmManager.RTC, System.currentTimeMillis() + 4000,
                 startAppPendingIntent
             ) // 2秒钟后重启应用
         }

+ 0 - 4
app/src/main/java/com/grkj/iscs_mars/util/ArcSoftUtil.kt

@@ -138,7 +138,6 @@ object ArcSoftUtil {
             else -> {
                 isActivated = false
                 LogUtil.e("checkActiveStatus : active failed $activeCode")
-                ToastUtils.tip(R.string.face_active_fail)
             }
         }
         val activeFileInfo = ActiveFileInfo()
@@ -158,9 +157,6 @@ object ArcSoftUtil {
             FaceEngine.ASF_FACE_DETECT or FaceEngine.ASF_AGE or FaceEngine.ASF_GENDER or FaceEngine.ASF_LIVENESS or FaceEngine.ASF_FACE_RECOGNITION
         )
         LogUtil.i("initEngine:  init: $afCode")
-        if (afCode != ErrorInfo.MOK) {
-            ToastUtils.tip(R.string.face_active_fail)
-        }
     }
 
     fun unInitEngine() {

+ 0 - 2
app/src/main/java/com/grkj/iscs_mars/view/activity/LoginActivity.kt

@@ -107,7 +107,6 @@ class LoginActivity : BaseMvpActivity<ILoginView, LoginPresenter, ActivityLoginB
 
     override fun onResume() {
         super.onResume()
-        BusinessManager.checkKeyInfoTask.isInLogin = true
         if (ModBusController.isRunning() != true) {
             BusinessManager.connectDock(true)
         }
@@ -157,7 +156,6 @@ class LoginActivity : BaseMvpActivity<ILoginView, LoginPresenter, ActivityLoginB
 
     override fun onStop() {
         super.onStop()
-        BusinessManager.checkKeyInfoTask.isInLogin = false
         cardLoginDialog?.dismiss()
         presenter?.unregisterListener()
         cardNo = ""

+ 2 - 2
app/src/main/java/com/grkj/iscs_mars/view/activity/SwitchStatusActivity.kt

@@ -97,8 +97,8 @@ class SwitchStatusActivity :
         val itemBinding = getBinding<ItemSwitchBinding>()
         val item = getModel<MotorMapInfoRespVO.IsMotorMapPoint>()
         val switchData = ModBusController.getSwitchData()
-        itemBinding.switchName.text = "${item.motorName} - ${item.motorCode}"
-        itemBinding.switchId.text = context.getString(R.string.switch_id, item.pointNfc)
+        itemBinding.switchName.text = "${item.motorName}"
+        itemBinding.switchId.text = context.getString(R.string.switch_id, item.motorCode)
         val switchStatus = switchData
             .find { it.idx == item.pointSerialNumber?.toInt() }?.enabled
             ?: (item.switchStatus == "1")

+ 11 - 16
app/src/main/java/com/grkj/iscs_mars/view/fragment/WorkshopFragment.kt

@@ -78,14 +78,9 @@ class WorkshopFragment(val changePage: (PageChangeBO) -> Unit) :
                 presenter?.getMapPointPage {
                     mPointList.clear()
                     it?.records?.forEach { itPoint ->
-                        mPointList.add(
-                            CustomPoint(
-                                PointF(
-                                    itPoint.x!!.toFloat() + 100,
-                                    itPoint.y!!.toFloat()
-                                ), itPoint.entityName!!, itPoint.entityId!!, mutableListOf()
-                            )
-                        )
+                        presenter?.mapDataHandle(itPoint)?.let {
+                            mPointList.add(it)
+                        }
                     }
                     presenter?.getWorkstationTicketList {
                         if (it == null) {
@@ -108,14 +103,14 @@ class WorkshopFragment(val changePage: (PageChangeBO) -> Unit) :
                                             Constants.getTicketKey(5)
                                         ),
                                         R.mipmap.ticket_type_placeholder,
-                                        60, 60
+                                        80, 80
                                     ) { itBitmap ->
                                         itTicket.bitmap =
                                             itBitmap ?: BitmapUtil.getResizedBitmapFromMipmap(
                                                 requireContext(),
                                                 R.mipmap.ticket_type_placeholder,
-                                                60,
-                                                60
+                                                80,
+                                                80
                                             )
                                     }
                                 } else {
@@ -123,8 +118,8 @@ class WorkshopFragment(val changePage: (PageChangeBO) -> Unit) :
                                         itTicket.bitmap = BitmapUtil.getResizedBitmapFromMipmap(
                                             requireContext(),
                                             R.mipmap.ticket_type_placeholder,
-                                            60,
-                                            60
+                                            80,
+                                            80
                                         )
                                     } else {
                                         BitmapUtil.loadBitmapFromUrl(
@@ -134,14 +129,14 @@ class WorkshopFragment(val changePage: (PageChangeBO) -> Unit) :
                                                 Constants.getTicketKey(itTicket.ticketType.toInt())
                                             ),
                                             R.mipmap.ticket_type_placeholder,
-                                            60, 60
+                                            80, 80
                                         ) { itBitmap ->
                                             itTicket.bitmap =
                                                 itBitmap ?: BitmapUtil.getResizedBitmapFromMipmap(
                                                     requireContext(),
                                                     R.mipmap.ticket_type_placeholder,
-                                                    60,
-                                                    60
+                                                    80,
+                                                    80
                                                 )
                                         }
                                     }

+ 13 - 0
app/src/main/java/com/grkj/iscs_mars/view/presenter/WorkshopPresenter.kt

@@ -1,5 +1,6 @@
 package com.grkj.iscs_mars.view.presenter
 
+import android.graphics.PointF
 import com.grkj.iscs_mars.model.Constants
 import com.grkj.iscs_mars.model.vo.map.MapInfoRespVO
 import com.grkj.iscs_mars.model.vo.map.MapPointPageRespVO
@@ -8,6 +9,7 @@ import com.grkj.iscs_mars.util.Executor
 import com.grkj.iscs_mars.util.NetApi
 import com.grkj.iscs_mars.util.SPUtils
 import com.grkj.iscs_mars.view.base.BasePresenter
+import com.grkj.iscs_mars.view.fragment.WorkshopFragment
 import com.grkj.iscs_mars.view.iview.IWorkshopView
 
 class WorkshopPresenter : BasePresenter<IWorkshopView>() {
@@ -39,4 +41,15 @@ class WorkshopPresenter : BasePresenter<IWorkshopView>() {
             }
         }
     }
+
+    fun mapDataHandle(
+        itPoint: MapPointPageRespVO.Record
+    ): WorkshopFragment.CustomPoint {
+        return WorkshopFragment.CustomPoint(
+            PointF(
+                itPoint.x!!.toFloat() + 100,
+                itPoint.y!!.toFloat()
+            ), itPoint.entityName!!, itPoint.entityId!!, mutableListOf()
+        )
+    }
 }

+ 121 - 88
app/src/main/java/com/grkj/iscs_mars/view/widget/CustomMarkLayer.kt

@@ -22,14 +22,15 @@ class CustomMarkLayer @JvmOverloads constructor(
     private var pointList: List<CustomPoint> = mutableListOf()
 ) : MapBaseLayer(mapView) {
     private var listener: MarkIsClickListener? = null
-    private var radiusMark = 0f
+    private var textSize = 18f
+    private var horizontalPadding = 10f
     private var isClickMark: Boolean = false
     private var num: Int
     private lateinit var paint: Paint
     private var btnIndex: Int
     private var currentZoom = 0f
     private var currentDegree = 0f
-    private var mBitmapSize = 60    // 默认图标大小
+    private var mBitmapSize = 80    // 默认图标大小
     private var isClickIcon: Boolean = false
     private var isFirst: Boolean = true
 
@@ -42,7 +43,6 @@ class CustomMarkLayer @JvmOverloads constructor(
     }
 
     private fun initLayer() {
-        radiusMark = setValue(10.0f)
         paint = Paint()
         paint.isAntiAlias = true
         paint.style = Paint.Style.FILL_AND_STROKE
@@ -50,94 +50,120 @@ class CustomMarkLayer @JvmOverloads constructor(
     }
 
     override fun onTouch(event: MotionEvent) {
-        if (pointList.isNotEmpty() && event.action == MotionEvent.ACTION_UP) {
-            val goal = mapView.convertMapXYToScreenXY(event.x, event.y)
+        if (pointList.isEmpty() || event.action != MotionEvent.ACTION_UP) return
 
-            for (i in pointList.indices) {
-                val list = pointList[i].ticketList.take(3)
-                var continueOuterLoop = false // 双循环跳出标志变量
+        val sx = event.x
+        val sy = event.y
 
-                // 计算文字点击
-                val width = paint.measureText(pointList[i].name) / currentZoom
+        // 初始化点击状态
+        isClickMark = false
+        isClickIcon = false
+        num = -1
+        btnIndex = -1
 
-                val left = pointList[i].pos.x - (width / 2 + radiusMark / 4.0f * 3) / currentZoom
-                val top = pointList[i].pos.y - radiusMark / 4.0f * 3 / currentZoom
-                val right = pointList[i].pos.x + (width / 2 + radiusMark / 4.0f * 3) / currentZoom
-                val bottom = pointList[i].pos.y + radiusMark / 4.0f * 3 / currentZoom
+        // 文字绘制时的度量要一致
+        paint.textSize = textSize
+
+        // 遍历所有点(屏幕坐标命中)
+        for (i in pointList.indices) {
+            val point = pointList[i]
+
+            // 1) 地图点 -> 屏幕点(与 onDraw 完全一致)
+            val goal = mapView.mapXYToScreenXY(point.pos.x, point.pos.y)
+
+            // ===== 命中 1:label 圆角矩形(与 onDraw 同公式)=====
+            val name = point.name
+            val width = paint.measureText(name)
+            val fm = paint.fontMetrics
+            val height = fm.descent - fm.ascent
+
+            val labelLeft   = goal[0] - width / 2f - horizontalPadding * 2f
+            val labelTop    = goal[1] - height / 2f - horizontalPadding / 2f
+            val labelRight  = goal[0] + width / 2f + horizontalPadding * 2f
+            val labelBottom = goal[1] + height / 2f + horizontalPadding / 2f
+
+            if (sx > labelLeft && sx < labelRight && sy > labelTop && sy < labelBottom) {
+                num = i
+                btnIndex = -1
+                isClickMark = true
+                isClickIcon = false
+                break
+            }
 
-                if (goal[0] > left && goal[0] < right && goal[1] > top && goal[1] < bottom) {
-                    num = i
-                    btnIndex = -1
-                    isClickMark = true
-                    isClickIcon = false
-                    break
-                }
+            // ===== 命中 2:上方最多 3 个 icon(与 onDraw 同公式)=====
+            val list = point.ticketList.take(3)
+            if (list.isEmpty()) continue
+
+            // icon 顶点的屏幕 Y(与 onDraw 保持)
+            val iconTopY = goal[1] - height / 2f - horizontalPadding / 2f - 10f - mBitmapSize / 2f
 
-                // 计算图标点击
-                for (j in list.indices) {
-                    val ticketX = if (list.size % 2 == 0) { // 偶数个
+            for (j in list.indices) {
+                // 与 onDraw 的水平排布保持一致
+                val ticketX = when {
+                    list.size % 2 == 0 -> { // 偶数
                         if (j + 1 <= list.size / 2) {
-                            pointList[i].pos.x - mBitmapSize * (list.size / 2 - j - 0.5f) / currentZoom
+                            goal[0] - mBitmapSize / 2f * (list.size / 2 - j)
                         } else {
-                            pointList[i].pos.x + mBitmapSize * (j - list.size / 2 + 0.5f) / currentZoom
+                            goal[0] + mBitmapSize / 2f * (j - list.size / 2)
                         }
-                    } else {    // 奇数个
+                    }
+                    else -> { // 奇数
                         if (j + 1 <= list.size / 2) {
-                            pointList[i].pos.x - mBitmapSize * (list.size / 2 - j) / currentZoom
+                            goal[0] - mBitmapSize / 2f * (list.size / 2 - j + 0.5f)
                         } else {
-                            pointList[i].pos.x + mBitmapSize * (j - list.size / 2) / currentZoom
+                            goal[0] + mBitmapSize / 2f * (j - list.size / 2 - 0.5f)
                         }
                     }
-                    val rotatedPoint = rotatePoint(
-                        ticketX,
-                        pointList[i].pos.y - mBitmapSize / currentZoom,
-                        pointList[i].pos.x,
-                        pointList[i].pos.y,
-                        currentDegree
-                    )
-
-                    val distance = MapMath.getDistanceBetweenTwoPoints(
-                        goal[0], goal[1],
-                        rotatedPoint.first,
-                        rotatedPoint.second
-                    )
+                }
 
-                    if (distance <= mBitmapSize / 2 / currentZoom) {
-                        num = i
-                        btnIndex = j
-                        isClickMark = true
-                        continueOuterLoop = true
-                        isClickIcon = true
-                        break
-                    } else {
-                        val idxDistance = MapMath.getDistanceBetweenTwoPoints(
-                            goal[0], goal[1],
-                            rotatedPoint.first,
-                            rotatedPoint.second - mBitmapSize / currentZoom
-                        )
-                        if (idxDistance <= mBitmapSize / 2 / currentZoom) {
-                            num = i
-                            btnIndex = j
-                            isClickMark = true
-                            continueOuterLoop = true
-                            isClickIcon = false
-                        }
-                    }
+                // icon 的屏幕矩形(顶点与 onDraw 对齐)
+                val iconLeft = ticketX
+                val iconTop = iconTopY
+                val iconRight = ticketX + mBitmapSize
+                val iconBottom = iconTopY + mBitmapSize
+
+                // 命中 2.1:点击到图标本体(用圆形或矩形都行,这里圆形更贴合视觉)
+                val iconCx = (iconLeft + iconRight) / 2f
+                val iconCy = (iconTop + iconBottom) / 2f
+                val dx = sx - iconCx
+                val dy = sy - iconCy
+                val r = mBitmapSize / 2f
+                val hitIcon = (dx * dx + dy * dy) <= (r * r)
+
+                if (hitIcon) {
+                    num = i
+                    btnIndex = j
+                    isClickMark = true
+                    isClickIcon = true
+                    break
                 }
 
-                // 跳出外层循环
-                if (continueOuterLoop) break
+                // 命中 2.2:点击到序号条(与 onDraw 的 number 矩形一致)
+                val numberText = if (point.ticketList.size > 3 && j == list.size - 1) "${j + 1}+" else (j + 1).toString()
+                val numberWidth = paint.measureText(numberText)
+                val numberHeight = fm.descent - fm.ascent
+
+                val numberRectLeft   = goal[0] - numberWidth / 2f - horizontalPadding * 1.5f
+                val numberRectTop    = iconTop - horizontalPadding * 2f - numberHeight - horizontalPadding / 2f
+                val numberRectRight  = goal[0] + numberWidth / 2f + horizontalPadding * 1.5f
+                val numberRectBottom = numberRectTop + numberHeight + horizontalPadding * 2f
 
-                if (i == pointList.size - 1) {
-                    isClickMark = false
+                val hitBadge = sx > numberRectLeft && sx < numberRectRight && sy > numberRectTop && sy < numberRectBottom
+                if (hitBadge) {
+                    num = i
+                    btnIndex = j
+                    isClickMark = true
+                    isClickIcon = false
+                    break
                 }
             }
 
+            if (isClickMark) break
+        }
 
-            if (listener != null && isClickMark) {
-                listener!!.markIsClick(num, btnIndex, isClickIcon)
-                mapView.refresh()
-            }
+        if (listener != null && isClickMark) {
+            listener!!.markIsClick(num, btnIndex, isClickIcon)
+            mapView.refresh()
         }
     }
 
@@ -165,17 +191,18 @@ class CustomMarkLayer @JvmOverloads constructor(
                     if (!viewport.contains(goal[0], goal[1])) return@forEach  // ← 屏外跳过
 
                     // 文字背景
-                    paint.textSize = radiusMark
+                    paint.textSize = textSize
                     val width = paint.measureText(point.name)
+                    val height = paint.fontMetrics.descent - paint.fontMetrics.ascent
                     paint.color = Color.parseColor("#70b26f")
                     paint.style = Paint.Style.FILL
                     paint.strokeWidth = 1.0f
                     // 圆角矩形
                     canvas.drawRoundRect(
-                        goal[0] - width / 2 - radiusMark / 4.0f * 3,
-                        goal[1] - radiusMark / 4.0f * 3,
-                        goal[0] + width / 2 + radiusMark / 4.0f * 3,
-                        goal[1] + radiusMark / 4.0f * 3,
+                        goal[0] - width / 2 - horizontalPadding * 2,
+                        goal[1] - height / 2 - horizontalPadding / 2,
+                        goal[0] + width / 2 + horizontalPadding * 2,
+                        goal[1] + height / 2 + horizontalPadding / 2,
                         10f,
                         10f,
                         paint
@@ -186,7 +213,7 @@ class CustomMarkLayer @JvmOverloads constructor(
                     canvas.drawText(
                         point.name,
                         goal[0] - width / 2,
-                        goal[1] + radiusMark / 3.0f,
+                        goal[1] + height / 2 - paint.fontMetrics.descent,
                         paint
                     )
 
@@ -195,18 +222,19 @@ class CustomMarkLayer @JvmOverloads constructor(
                         for (j in list.indices) {
                             val ticketX = if (list.size % 2 == 0) { // 偶数个
                                 if (j + 1 <= list.size / 2) {
-                                    goal[0] - mBitmapSize * (list.size / 2 - j)
+                                    goal[0] - mBitmapSize / 2 * (list.size / 2 - j)
                                 } else {
-                                    goal[0] + mBitmapSize * (j - list.size / 2)
+                                    goal[0] + mBitmapSize / 2 * (j - list.size / 2)
                                 }
                             } else {    // 奇数个
                                 if (j + 1 <= list.size / 2) {
-                                    goal[0] - mBitmapSize * (list.size / 2 - j + 0.5f)
+                                    goal[0] - mBitmapSize / 2 * (list.size / 2 - j + 0.5f)
                                 } else {
-                                    goal[0] + mBitmapSize * (j - list.size / 2 - 0.5f)
+                                    goal[0] + mBitmapSize / 2 * (j - list.size / 2 - 0.5f)
                                 }
                             }
-                            val ticketY = goal[1] - mBitmapSize / 2 * 3
+                            val ticketY =
+                                goal[1] - height / 2 - horizontalPadding / 2 - 10 - mBitmapSize / 2
 
                             // 绘制图标
                             val srcBm = if (point.ticketList.size > 3 && j == 2)
@@ -222,10 +250,14 @@ class CustomMarkLayer @JvmOverloads constructor(
                                 if (point.ticketList.size > 3 && j == list.size - 1) "${j + 1}+" else (j + 1).toString()
                             val numberWidth = paint.measureText(numberText)
                             val numberHeight = paint.fontMetrics.descent - paint.fontMetrics.ascent
-                            val numberRectLeft = ticketX + mBitmapSize / 2 - radiusMark / 4.0f * 3
-                            val numberRectTop = ticketY - mBitmapSize / 2 - radiusMark / 4.0f * 3
-                            val numberRectRight = ticketX + mBitmapSize / 2 + radiusMark / 4.0f * 3
-                            val numberRectBottom = ticketY - mBitmapSize / 2 + radiusMark / 4.0f * 3
+                            val numberRectLeft =
+                                goal[0] - numberWidth / 2 - horizontalPadding * 1.5f
+                            val numberRectTop =
+                                ticketY - horizontalPadding * 2 - numberHeight - horizontalPadding / 2
+                            val numberRectRight =
+                                goal[0] + numberWidth / 2 + horizontalPadding * 1.5f
+                            val numberRectBottom =
+                                numberRectTop + numberHeight + horizontalPadding * 2
 
                             // 绘制序号背景
                             paint.color = Color.parseColor("#00AAFF")
@@ -243,13 +275,14 @@ class CustomMarkLayer @JvmOverloads constructor(
                             paint.color = Color.WHITE
                             canvas.drawText(
                                 numberText,
-                                ticketX + mBitmapSize / 2 - numberWidth / 2,
-                                ticketY - mBitmapSize / 2 + radiusMark / 3.0f,
+                                goal[0] - numberWidth / 2,
+                                numberRectBottom - horizontalPadding - paint.fontMetrics.descent,
                                 paint
                             )
                         }
                     }
                     // 定位点,仅调试用,不要显示
+//                    paint.color = Color.RED
 //                    canvas.drawCircle(goal[0], goal[1], 2f, paint)
                 }
             }

+ 125 - 32
app/src/main/java/com/grkj/iscs_mars/view/widget/CustomSwitchStationLayer.kt

@@ -1,6 +1,11 @@
 package com.grkj.iscs_mars.view.widget
 
-import android.graphics.*
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.PointF
 import android.os.SystemClock
 import android.view.MotionEvent
 import androidx.core.content.ContextCompat
@@ -10,7 +15,6 @@ import com.grkj.iscs_mars.modbus.DockBean
 import com.onlylemi.mapview.library.MapView
 import com.onlylemi.mapview.library.layer.MapBaseLayer
 import kotlin.math.abs
-import kotlin.math.sin
 
 /**
  * 自定义开关层(保持对外 API 兼容):
@@ -52,14 +56,20 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     // ===== 同步 & 选择 =====
     private val dataLock = Any()
 
-    @Volatile private var selectedEntityIdForKeep: Long? = null
-    @Volatile private var selectedNameForKeep: String? = null
+    @Volatile
+    private var selectedEntityIdForKeep: Long? = null
+    @Volatile
+    private var selectedNameForKeep: String? = null
 
-    @Volatile private var selectToken: Long = 0L
-    private inline fun withLiveToken(token: Long, block: () -> Unit) { if (token == selectToken) block() }
+    @Volatile
+    private var selectToken: Long = 0L
+    private inline fun withLiveToken(token: Long, block: () -> Unit) {
+        if (token == selectToken) block()
+    }
 
     // ===== 动画参数 =====
-    @Volatile var inDraw: Boolean = false
+    @Volatile
+    var inDraw: Boolean = false
         private set
 
     private val breathePeriod = 1200f
@@ -104,13 +114,29 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     private val appCtx get() = MyApplication.instance!!.applicationContext
     private val colOn by lazy { ContextCompat.getColor(appCtx, R.color.common_switch_enable) }
     private val colOff by lazy { ContextCompat.getColor(appCtx, R.color.common_switch_disable) }
-    private val colRed by lazy { runCatching { ContextCompat.getColor(appCtx, R.color.red_500) }.getOrDefault(Color.parseColor("#EF4444")) }
-    private val colOrange by lazy { runCatching { ContextCompat.getColor(appCtx, R.color.orange_500) }.getOrDefault(Color.parseColor("#F97316")) }
+    private val colRed by lazy {
+        runCatching {
+            ContextCompat.getColor(
+                appCtx,
+                R.color.red_500
+            )
+        }.getOrDefault(Color.parseColor("#EF4444"))
+    }
+    private val colOrange by lazy {
+        runCatching {
+            ContextCompat.getColor(
+                appCtx,
+                R.color.orange_500
+            )
+        }.getOrDefault(Color.parseColor("#F97316"))
+    }
     private val colSelectRing = Color.argb(180, 66, 133, 244)
 
     // 长按探测
-    private val longPressTimeoutMs: Long = android.view.ViewConfiguration.getLongPressTimeout().toLong()
-    private val touchSlop: Float = android.view.ViewConfiguration.get(mapView?.context ?: appCtx).scaledTouchSlop * 1.5f
+    private val longPressTimeoutMs: Long =
+        android.view.ViewConfiguration.getLongPressTimeout().toLong()
+    private val touchSlop: Float =
+        android.view.ViewConfiguration.get(mapView?.context ?: appCtx).scaledTouchSlop * 1.5f
     private val touchSlopSq: Float = touchSlop * touchSlop
     private var pendingLongPress = false
     private var longPressTriggered = false
@@ -134,7 +160,8 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     }
 
     // ===== 兼容原版:对外回调 =====
-    var onLongPressListener: ((point: IsolationPoint, screenX: Float, screenY: Float, mapX: Float, mapY: Float) -> Unit)? = null
+    var onLongPressListener: ((point: IsolationPoint, screenX: Float, screenY: Float, mapX: Float, mapY: Float) -> Unit)? =
+        null
 
     init {
         paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL_AND_STROKE }
@@ -178,12 +205,24 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                     changedCenters.add(add.pos)
                 } else {
                     var dirty = false
-                    if (!dst.pos.approxEq(src.pos)) { dst.pos = PointF(src.pos.x, src.pos.y); dirty = true }
-                    if (dst.status != src.status) { dst.status = src.status; dirty = true }
-                    if (dst.motorCode != src.motorCode) { dst.motorCode = src.motorCode; dirty = true }
-                    if (dst.pointNfc != src.pointNfc) { dst.pointNfc = src.pointNfc; dirty = true }
-                    if (dst.pointSerialNumber != src.pointSerialNumber) { dst.pointSerialNumber = src.pointSerialNumber; dirty = true }
-                    if (src.icon != null && dst.icon !== src.icon) { dst.icon = src.icon; dirty = true }
+                    if (!dst.pos.approxEq(src.pos)) {
+                        dst.pos = PointF(src.pos.x, src.pos.y); dirty = true
+                    }
+                    if (dst.status != src.status) {
+                        dst.status = src.status; dirty = true
+                    }
+                    if (dst.motorCode != src.motorCode) {
+                        dst.motorCode = src.motorCode; dirty = true
+                    }
+                    if (dst.pointNfc != src.pointNfc) {
+                        dst.pointNfc = src.pointNfc; dirty = true
+                    }
+                    if (dst.pointSerialNumber != src.pointSerialNumber) {
+                        dst.pointSerialNumber = src.pointSerialNumber; dirty = true
+                    }
+                    if (src.icon != null && dst.icon !== src.icon) {
+                        dst.icon = src.icon; dirty = true
+                    }
 
                     dst.isSelected = when {
                         !keepSelection -> false
@@ -239,7 +278,8 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         if (w == 0 || h == 0) return
         val pts = floatArrayOf(point.x, point.y)
         mapView.currentMatrix.mapPoints(pts)
-        val x = pts[0]; val y = pts[1]
+        val x = pts[0];
+        val y = pts[1]
         if (x + margin >= 0 && x - margin <= w && y + margin >= 0 && y - margin <= h) {
             throttleInvalidate()
         }
@@ -264,20 +304,25 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                     }
                 }
             }
+
             MotionEvent.ACTION_MOVE -> {
                 lastScreenX = event.x; lastScreenY = event.y
                 if (pendingLongPress && !longPressTriggered) {
                     val dx = event.x - downScreenX
                     val dy = event.y - downScreenY
-                    if (dx*dx + dy*dy > touchSlopSq) {
+                    if (dx * dx + dy * dy > touchSlopSq) {
                         pendingLongPress = false
                         mapView.removeCallbacks(longPressRunnable)
                     }
                 }
             }
+
             MotionEvent.ACTION_UP -> {
                 mapView.removeCallbacks(longPressRunnable)
-                if (longPressTriggered) { pendingLongPress = false; longPressTriggered = false; currentPressed = null; return }
+                if (longPressTriggered) {
+                    pendingLongPress = false; longPressTriggered = false; currentPressed =
+                        null; return
+                }
                 val mp = screenToMap(event.x, event.y) ?: return
                 val hit = hitTest(mp.x, mp.y) ?: return
                 val token = ++selectToken
@@ -287,10 +332,22 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                 val eid = hit.entityId
                 val name = hit.motorCode
 
-                currentMapCenter()?.let { cur -> mapView?.animateCenterOnPoint(cur.x, cur.y, clickZoomDurationMs, targetScale) }
+                currentMapCenter()?.let { cur ->
+                    mapView?.animateCenterOnPoint(
+                        cur.x,
+                        cur.y,
+                        clickZoomDurationMs,
+                        targetScale
+                    )
+                }
                 mapView?.postDelayed({
                     withLiveToken(token) {
-                        mapView?.animateCenterOnPoint(targetCenter.x, targetCenter.y, clickCenterDurationMs, targetScale)
+                        mapView?.animateCenterOnPoint(
+                            targetCenter.x,
+                            targetCenter.y,
+                            clickCenterDurationMs,
+                            targetScale
+                        )
                     }
                 }, clickZoomDurationMs)
                 mapView?.postDelayed({
@@ -302,6 +359,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                     }
                 }, clickZoomDurationMs + clickCenterDurationMs)
             }
+
             MotionEvent.ACTION_CANCEL -> {
                 pendingLongPress = false
                 longPressTriggered = false
@@ -312,7 +370,12 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     }
 
     // ================= 绘制 =================
-    override fun draw(canvas: Canvas, currentMatrix: Matrix, currentZoom: Float, currentRotateDegrees: Float) {
+    override fun draw(
+        canvas: Canvas,
+        currentMatrix: Matrix,
+        currentZoom: Float,
+        currentRotateDegrees: Float
+    ) {
         if (!isVisible || inDraw) return
         inDraw = true
         this.currentZoom = currentZoom
@@ -345,14 +408,17 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                         paint.color = colOn; paint.alpha = 255
                         canvas.drawCircle(c.x, c.y, mapR, paint)
                     }
+
                     STATUS_OFF -> {
                         paint.color = colOff; paint.alpha = 255
                         canvas.drawCircle(c.x, c.y, mapR, paint)
                     }
+
                     STATUS_ALARM -> {
                         val t = pulsePhase
                         val color = if (t < 0.5f) colRed else colOrange
-                        val a = (160 + 95 * kotlin.math.abs(kotlin.math.sin(2f * Math.PI.toFloat() * t))).toInt()
+                        val a =
+                            (160 + 95 * kotlin.math.abs(kotlin.math.sin(2f * Math.PI.toFloat() * t))).toInt()
                         paint.color = color; paint.alpha = a
                         val r = mapR * (1.0f + 0.10f * kotlin.math.sin(2f * Math.PI.toFloat() * t))
                         canvas.drawCircle(c.x, c.y, r, paint)
@@ -360,6 +426,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                         canvas.drawCircle(c.x, c.y, r * 1.25f, paint)
                         paint.alpha = 255
                     }
+
                     else -> {
                         paint.color = Color.DKGRAY; paint.alpha = 200
                         canvas.drawCircle(c.x, c.y, mapR, paint)
@@ -384,7 +451,13 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                     paint.strokeWidth = 0f
                     paint.color = Color.WHITE
                     val maxTextWidthScreen = (screenR * 2f) - 6f
-                    val screenTextSize = fitTextSizeScreen(paint, text, maxTextWidthScreen, screenTextMin, screenTextMax)
+                    val screenTextSize = fitTextSizeScreen(
+                        paint,
+                        text,
+                        maxTextWidthScreen,
+                        screenTextMin,
+                        screenTextMax
+                    )
                     val mapTextSize = screenTextSize
                     paint.textSize = mapTextSize
                     val textW = paint.measureText(text)
@@ -399,7 +472,13 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         }
     }
 
-    private fun fitTextSizeScreen(p: Paint, text: String, maxWidthScreenPx: Float, minSp: Float, maxSp: Float): Float {
+    private fun fitTextSizeScreen(
+        p: Paint,
+        text: String,
+        maxWidthScreenPx: Float,
+        minSp: Float,
+        maxSp: Float
+    ): Float {
         if (text.isEmpty()) return minSp
         // 在“屏幕像素语义”下先估字号,再回到 draw() 里除以 zoom 赋给 paint.textSize
         p.textSize = maxSp
@@ -414,7 +493,6 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     }
 
 
-
     // ================= 工具 =================
     private fun getSwitchR(): Float {
         // 保持原有 ratio 语义 + 轻微随 zoom 增益
@@ -462,8 +540,10 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
             val c = centerOf(p)
             val dx = mapX - c.x
             val dy = mapY - c.y
-            val d2 = dx*dx + dy*dy
-            if (d2 <= hitR2 && d2 < bestD2) { bestD2 = d2; hit = p }
+            val d2 = dx * dx + dy * dy
+            if (d2 <= hitR2 && d2 < bestD2) {
+                bestD2 = d2; hit = p
+            }
         }
         return hit
     }
@@ -478,7 +558,13 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         val mv = mapView ?: return throttleInvalidate()
         var didPartial = false
         runCatching {
-            val m = mv.javaClass.getMethod("invalidateRect", Int::class.java, Int::class.java, Int::class.java, Int::class.java)
+            val m = mv.javaClass.getMethod(
+                "invalidateRect",
+                Int::class.java,
+                Int::class.java,
+                Int::class.java,
+                Int::class.java
+            )
             for (c in centers) {
                 val l = (c.x - radiusPx).toInt()
                 val t = (c.y - radiusPx).toInt()
@@ -513,6 +599,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         if (isAlarm(bean)) return STATUS_ALARM
         return if (bean.enabled) STATUS_ON else STATUS_OFF
     }
+
     private fun isAlarm(bean: DockBean.SwitchBean?): Boolean {
         // TODO: 按业务判断
         return false
@@ -521,7 +608,13 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     private fun deepCopyPoint(p: PointF) = PointF(p.x, p.y)
     private fun deepCopyItem(it: IsolationPoint) = it.copy(pos = deepCopyPoint(it.pos))
 
-    private fun fitTextSize(p: Paint, text: String, maxWidth: Float, minSp: Float = 8f, maxSp: Float = 22f): Float {
+    private fun fitTextSize(
+        p: Paint,
+        text: String,
+        maxWidth: Float,
+        minSp: Float = 8f,
+        maxSp: Float = 22f
+    ): Float {
         if (text.isEmpty()) return minSp
         p.textSize = maxSp
         var w = p.measureText(text)

+ 8 - 2
app/src/main/java/com/onlylemi/mapview/library/MapView.java

@@ -274,7 +274,13 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         this.mapViewListener = listener;
     }
 
-    public float[] convertMapXYToScreenXY(float x, float y) {
+    public float[] mapXYToScreenXY(float x, float y) {
+        float[] pts = { x, y };
+        currentMatrix.mapPoints(pts); // map -> screen(与 onDraw 完全一致)
+        return pts;
+    }
+
+    public float[] screenXYToMapXY(float x, float y) {
         Matrix inv = new Matrix();
         currentMatrix.invert(inv);
         float[] pts = {x, y};
@@ -415,7 +421,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
     }
 
     public boolean withFloorPlan(float x, float y) {
-        float[] pts = convertMapXYToScreenXY(x, y);
+        float[] pts = screenXYToMapXY(x, y);
         Picture img = mapLayer != null ? mapLayer.getImage() : null;
         return img != null && pts[0] > 0 && pts[0] < img.getWidth() && pts[1] > 0 && pts[1] < img.getHeight();
     }