diff --git a/CLAUDE.md b/CLAUDE.md index 6db4bf84..630cb30e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1661,8 +1661,12 @@ google 的 aab 需要单独出)。原来靠打包时写进 `.env` 的 `Channel 纯 Dart 只订阅 `ABC0`,真机上"扫得到、连得上、设备说没连"。设计与踩坑记录见 [docs/Smartcar车载香薰-接入设计与落地文档.md](docs/Smartcar车载香薰-接入设计与落地文档.md) 5.5。 +- **2026-09-22 Dart 侧整体改名 Smartcar → EaiCar**(目录 `lib/devices/eaicar/`、`lib/modules/eaicar/`、类名/文案键/路由)。 + **刻意不改的**:原生插件名 `device_smartcar`(Kotlin 包 `com.yimai.device_smartcar`、Swift 包)、持久化的厂商键 + `DeviceVendors.eaicar = 'smartcar'`、已铺进装机编排的模块 id `AgentModules.eaicarChat = 'smartcarChat'`、 + GetStorage 键 `smartcar_authorized_macs` / `smartcar_remembered_devices`。改这些值会让老装机的编排/白名单/记住的设备全部丢失。 - 链路在原生插件 `apps/client/local_plugins/device_smartcar`(自持 GATT,同一条连接复用 AE00 + ABC0), - Dart 侧 `lib/devices/smartcar/` 只做组帧/广播解析/DevicePlugin 适配。 + Dart 侧 `lib/devices/eaicar/` 只做组帧/广播解析/DevicePlugin 适配。 - ⚠️ iOS 的 `JL_BLEKit` 是**动态库**,只能有一份:由 `device_jieli` 的 `Package.swift` 以 `jl-blekit` 产品暴露,`device_smartcar` 按路径依赖。再 vendor 一份会「Class implemented in both」。 - ⚠️ Android 若搬 lumi 的认证(`jl_rcsp`/`jl_watch`/`jl_bluetooth_connect` aar),**不要再放 diff --git a/apps/client/lib/core/translations/language/ar_sa.dart b/apps/client/lib/core/translations/language/ar_sa.dart index ec53db6c..003f3cfc 100644 --- a/apps/client/lib/core/translations/language/ar_sa.dart +++ b/apps/client/lib/core/translations/language/ar_sa.dart @@ -2059,12 +2059,12 @@ const Map arSA = { 'inCallBusyDesc': 'الميكروفون ومسار الصوت مشغولان بالمكالمة. حاول مرة أخرى بعد إنهاء المكالمة.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'مجاني', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/bg_bg.dart b/apps/client/lib/core/translations/language/bg_bg.dart index 55baf8c9..fa9c6fec 100644 --- a/apps/client/lib/core/translations/language/bg_bg.dart +++ b/apps/client/lib/core/translations/language/bg_bg.dart @@ -2122,12 +2122,12 @@ const Map bgBG = { 'inCallBusyDesc': 'Микрофонът и аудио каналът се използват от разговора. Опитайте отново след затваряне на разговора.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Безплатно', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/cs_cz.dart b/apps/client/lib/core/translations/language/cs_cz.dart index fa5c339f..fe4507c4 100644 --- a/apps/client/lib/core/translations/language/cs_cz.dart +++ b/apps/client/lib/core/translations/language/cs_cz.dart @@ -2090,12 +2090,12 @@ const Map csCZ = { 'inCallBusyDesc': 'Mikrofon a zvukovou cestu používá hovor. Zkuste to znovu po zavěšení.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Zdarma', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/da_dk.dart b/apps/client/lib/core/translations/language/da_dk.dart index c7ff446c..1106b9dd 100644 --- a/apps/client/lib/core/translations/language/da_dk.dart +++ b/apps/client/lib/core/translations/language/da_dk.dart @@ -2092,12 +2092,12 @@ const Map daDK = { 'inCallBusyDesc': 'Mikrofonen og lydvejen bruges af opkaldet. Prøv igen, når du har lagt på.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Gratis', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/de_de.dart b/apps/client/lib/core/translations/language/de_de.dart index 49ff88f0..8bdc2015 100644 --- a/apps/client/lib/core/translations/language/de_de.dart +++ b/apps/client/lib/core/translations/language/de_de.dart @@ -2159,12 +2159,12 @@ const Map deDE = { 'notNow': 'Nicht jetzt', 'permanentlyDisallow': 'Nicht erlauben', 'otaBesNotSupported': 'Dieses Headset unterstützt das Firmware-Upgrade in der App noch nicht. Bitte wenden Sie sich an den Support.', - 'smartcarConnected': 'Verbunden', - 'smartcarConnectFailed': 'Verbindung fehlgeschlagen', - 'smartcarConnecting': 'Verbinde…', - 'smartcarSearching': 'Suche nach Auto-Diffusoren in der Nähe…', - 'smartcarNotFound': 'Kein Gerät gefunden', - 'smartcarScanHint': 'Stellen Sie sicher, dass das Gerät eingeschaltet und in der Nähe ist', + 'eaicarConnected': 'Verbunden', + 'eaicarConnectFailed': 'Verbindung fehlgeschlagen', + 'eaicarConnecting': 'Verbinde…', + 'eaicarSearching': 'Suche nach Auto-Diffusoren in der Nähe…', + 'eaicarNotFound': 'Kein Gerät gefunden', + 'eaicarScanHint': 'Stellen Sie sicher, dass das Gerät eingeschaltet und in der Nähe ist', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/el_gr.dart b/apps/client/lib/core/translations/language/el_gr.dart index 5fc79f7e..df534d57 100644 --- a/apps/client/lib/core/translations/language/el_gr.dart +++ b/apps/client/lib/core/translations/language/el_gr.dart @@ -2126,12 +2126,12 @@ const Map elGR = { 'inCallBusyDesc': 'Το μικρόφωνο και η διαδρομή ήχου χρησιμοποιούνται από την κλήση. Δοκιμάστε ξανά αφού τερματίσετε την κλήση.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Δωρεάν', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/en_us.dart b/apps/client/lib/core/translations/language/en_us.dart index 6358b714..f9fffae7 100644 --- a/apps/client/lib/core/translations/language/en_us.dart +++ b/apps/client/lib/core/translations/language/en_us.dart @@ -2115,12 +2115,12 @@ const Map enUS = { 'unmarkFavorite': 'Remove from Favorites', 'unnamedDevice': 'Unnamed Device', 'userDataParseFailed': 'Failed to parse user data', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/es_es.dart b/apps/client/lib/core/translations/language/es_es.dart index 9fba8acf..3e79e8bd 100644 --- a/apps/client/lib/core/translations/language/es_es.dart +++ b/apps/client/lib/core/translations/language/es_es.dart @@ -2153,12 +2153,12 @@ const Map esES = { 'notNow': 'Ahora no', 'permanentlyDisallow': 'No permitir', 'otaBesNotSupported': 'Estos auriculares aún no admiten la actualización de firmware desde la app. Ponte en contacto con el soporte.', - 'smartcarConnected': 'Conectado', - 'smartcarConnectFailed': 'Error de conexión', - 'smartcarConnecting': 'Conectando…', - 'smartcarSearching': 'Buscando difusores de coche cercanos…', - 'smartcarNotFound': 'No se encontró ningún dispositivo', - 'smartcarScanHint': 'Asegúrate de que el dispositivo esté encendido y cerca del teléfono', + 'eaicarConnected': 'Conectado', + 'eaicarConnectFailed': 'Error de conexión', + 'eaicarConnecting': 'Conectando…', + 'eaicarSearching': 'Buscando difusores de coche cercanos…', + 'eaicarNotFound': 'No se encontró ningún dispositivo', + 'eaicarScanHint': 'Asegúrate de que el dispositivo esté encendido y cerca del teléfono', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/et_ee.dart b/apps/client/lib/core/translations/language/et_ee.dart index 0c38c555..b73a9d7a 100644 --- a/apps/client/lib/core/translations/language/et_ee.dart +++ b/apps/client/lib/core/translations/language/et_ee.dart @@ -2086,12 +2086,12 @@ const Map etEE = { 'inCallBusyDesc': 'Mikrofon ja heliliin on kõne kasutuses. Proovi uuesti pärast kõne lõpetamist.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Tasuta', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/fa_ir.dart b/apps/client/lib/core/translations/language/fa_ir.dart index 1322f779..78901bf0 100644 --- a/apps/client/lib/core/translations/language/fa_ir.dart +++ b/apps/client/lib/core/translations/language/fa_ir.dart @@ -2077,12 +2077,12 @@ const Map faIR = { 'inCallBusyDesc': 'میکروفون و مسیر صدا در اختیار تماس است. پس از پایان تماس دوباره تلاش کنید.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'رایگان', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/fi_fi.dart b/apps/client/lib/core/translations/language/fi_fi.dart index 0adacc6b..b83ef828 100644 --- a/apps/client/lib/core/translations/language/fi_fi.dart +++ b/apps/client/lib/core/translations/language/fi_fi.dart @@ -2096,12 +2096,12 @@ const Map fiFI = { 'inCallBusyDesc': 'Mikrofoni ja äänireitti ovat puhelun käytössä. Yritä uudelleen puhelun päätyttyä.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Ilmainen', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/fr_fr.dart b/apps/client/lib/core/translations/language/fr_fr.dart index 6c9e7851..ccc9c571 100644 --- a/apps/client/lib/core/translations/language/fr_fr.dart +++ b/apps/client/lib/core/translations/language/fr_fr.dart @@ -2172,12 +2172,12 @@ const Map frFR = { 'notNow': 'Pas maintenant', 'permanentlyDisallow': 'Ne pas autoriser', 'otaBesNotSupported': 'Ce casque ne prend pas encore en charge la mise à jour du firmware depuis l\'application. Veuillez contacter le support.', - 'smartcarConnected': 'Connecté', - 'smartcarConnectFailed': 'Échec de la connexion', - 'smartcarConnecting': 'Connexion…', - 'smartcarSearching': 'Recherche de diffuseurs auto à proximité…', - 'smartcarNotFound': 'Aucun appareil trouvé', - 'smartcarScanHint': 'Vérifiez que l’appareil est allumé et proche du téléphone', + 'eaicarConnected': 'Connecté', + 'eaicarConnectFailed': 'Échec de la connexion', + 'eaicarConnecting': 'Connexion…', + 'eaicarSearching': 'Recherche de diffuseurs auto à proximité…', + 'eaicarNotFound': 'Aucun appareil trouvé', + 'eaicarScanHint': 'Vérifiez que l’appareil est allumé et proche du téléphone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/hi_in.dart b/apps/client/lib/core/translations/language/hi_in.dart index 59395e8b..8e8e2495 100644 --- a/apps/client/lib/core/translations/language/hi_in.dart +++ b/apps/client/lib/core/translations/language/hi_in.dart @@ -2095,12 +2095,12 @@ const Map hiIN = { 'inCallBusyDesc': 'माइक और ऑडियो पाथ कॉल द्वारा उपयोग में हैं। कॉल समाप्त करने के बाद पुनः प्रयास करें।', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'मुफ़्त', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/hr_hr.dart b/apps/client/lib/core/translations/language/hr_hr.dart index cf29f4b2..c300c351 100644 --- a/apps/client/lib/core/translations/language/hr_hr.dart +++ b/apps/client/lib/core/translations/language/hr_hr.dart @@ -2111,12 +2111,12 @@ const Map hrHR = { 'inCallBusyDesc': 'Mikrofon i audio put koristi poziv. Pokušajte ponovno nakon prekida poziva.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Besplatno', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/hu_hu.dart b/apps/client/lib/core/translations/language/hu_hu.dart index c4a55d63..d6d0f1f9 100644 --- a/apps/client/lib/core/translations/language/hu_hu.dart +++ b/apps/client/lib/core/translations/language/hu_hu.dart @@ -2114,12 +2114,12 @@ const Map huHU = { 'inCallBusyDesc': 'A mikrofont és a hangútvonalat a hívás használja. Próbálja újra a hívás befejezése után.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Ingyenes', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/id_id.dart b/apps/client/lib/core/translations/language/id_id.dart index 6f2d6eb9..efe8ebf8 100644 --- a/apps/client/lib/core/translations/language/id_id.dart +++ b/apps/client/lib/core/translations/language/id_id.dart @@ -2095,12 +2095,12 @@ const Map idID = { 'inCallBusyDesc': 'Mikrofon dan jalur audio sedang digunakan oleh panggilan. Coba lagi setelah menutup panggilan.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Gratis', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/is_is.dart b/apps/client/lib/core/translations/language/is_is.dart index f6330ac5..e5a5d665 100644 --- a/apps/client/lib/core/translations/language/is_is.dart +++ b/apps/client/lib/core/translations/language/is_is.dart @@ -2092,12 +2092,12 @@ const Map isIS = { 'inCallBusyDesc': 'Hljóðneminn og hljóðleiðin eru í notkun í símtalinu. Reyndu aftur eftir að þú leggur á.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Ókeypis', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/it_it.dart b/apps/client/lib/core/translations/language/it_it.dart index ddfcd140..8466154c 100644 --- a/apps/client/lib/core/translations/language/it_it.dart +++ b/apps/client/lib/core/translations/language/it_it.dart @@ -2133,12 +2133,12 @@ const Map itIT = { 'inCallBusyDesc': 'Il microfono e il percorso audio sono in uso dalla chiamata. Riprova dopo aver riagganciato.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Gratis', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/ja_jp.dart b/apps/client/lib/core/translations/language/ja_jp.dart index 64c20ca5..798c0ccf 100644 --- a/apps/client/lib/core/translations/language/ja_jp.dart +++ b/apps/client/lib/core/translations/language/ja_jp.dart @@ -1954,12 +1954,12 @@ const Map jaJP = { 'notNow': '今はしない', 'permanentlyDisallow': '許可しない', 'otaBesNotSupported': 'このイヤホンはアプリ内でのファームウェアアップグレードにまだ対応していません。サポートにお問い合わせください。', - 'smartcarConnected': '接続しました', - 'smartcarConnectFailed': '接続に失敗しました', - 'smartcarConnecting': '接続中…', - 'smartcarSearching': '近くの車載アロマを検索中…', - 'smartcarNotFound': 'デバイスが見つかりません', - 'smartcarScanHint': '電源が入っていること、スマホの近くにあることを確認してください', + 'eaicarConnected': '接続しました', + 'eaicarConnectFailed': '接続に失敗しました', + 'eaicarConnecting': '接続中…', + 'eaicarSearching': '近くの車載アロマを検索中…', + 'eaicarNotFound': 'デバイスが見つかりません', + 'eaicarScanHint': '電源が入っていること、スマホの近くにあることを確認してください', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/ko_kr.dart b/apps/client/lib/core/translations/language/ko_kr.dart index dfff6250..4dc2ed08 100644 --- a/apps/client/lib/core/translations/language/ko_kr.dart +++ b/apps/client/lib/core/translations/language/ko_kr.dart @@ -1947,12 +1947,12 @@ const Map koKR = { 'notNow': '나중에', 'permanentlyDisallow': '허용 안 함', 'otaBesNotSupported': '이 이어폰은 아직 앱 내 펌웨어 업그레이드를 지원하지 않습니다. 고객 지원에 문의해 주세요.', - 'smartcarConnected': '연결됨', - 'smartcarConnectFailed': '연결 실패', - 'smartcarConnecting': '연결 중…', - 'smartcarSearching': '주변 차량용 방향제 검색 중…', - 'smartcarNotFound': '기기를 찾을 수 없습니다', - 'smartcarScanHint': '기기 전원이 켜져 있고 휴대폰 근처에 있는지 확인하세요', + 'eaicarConnected': '연결됨', + 'eaicarConnectFailed': '연결 실패', + 'eaicarConnecting': '연결 중…', + 'eaicarSearching': '주변 차량용 방향제 검색 중…', + 'eaicarNotFound': '기기를 찾을 수 없습니다', + 'eaicarScanHint': '기기 전원이 켜져 있고 휴대폰 근처에 있는지 확인하세요', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/lt_lt.dart b/apps/client/lib/core/translations/language/lt_lt.dart index f7a0b63b..8f39f215 100644 --- a/apps/client/lib/core/translations/language/lt_lt.dart +++ b/apps/client/lib/core/translations/language/lt_lt.dart @@ -2110,12 +2110,12 @@ const Map ltLT = { 'inCallBusyDesc': 'Mikrofoną ir garso kelią naudoja skambutis. Bandykite dar kartą padėję ragelį.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Nemokama', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/lv_lv.dart b/apps/client/lib/core/translations/language/lv_lv.dart index 9dd9d9d6..1f14f813 100644 --- a/apps/client/lib/core/translations/language/lv_lv.dart +++ b/apps/client/lib/core/translations/language/lv_lv.dart @@ -2115,12 +2115,12 @@ const Map lvLV = { 'inCallBusyDesc': 'Mikrofonu un audio ceļu izmanto zvans. Mēģiniet vēlreiz pēc zvana beigšanas.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Bezmaksas', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/ms_my.dart b/apps/client/lib/core/translations/language/ms_my.dart index 0d603fa5..48ea0c5e 100644 --- a/apps/client/lib/core/translations/language/ms_my.dart +++ b/apps/client/lib/core/translations/language/ms_my.dart @@ -2099,12 +2099,12 @@ const Map msMY = { 'inCallBusyDesc': 'Mikrofon dan laluan audio sedang digunakan oleh panggilan. Cuba lagi selepas menamatkan panggilan.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Percuma', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/nb_no.dart b/apps/client/lib/core/translations/language/nb_no.dart index eae05da5..3e84045d 100644 --- a/apps/client/lib/core/translations/language/nb_no.dart +++ b/apps/client/lib/core/translations/language/nb_no.dart @@ -2093,12 +2093,12 @@ const Map nbNO = { 'inCallBusyDesc': 'Mikrofonen og lydbanen brukes av samtalen. Prøv igjen etter at du har lagt på.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Gratis', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/nl_nl.dart b/apps/client/lib/core/translations/language/nl_nl.dart index 6f738066..1eb61288 100644 --- a/apps/client/lib/core/translations/language/nl_nl.dart +++ b/apps/client/lib/core/translations/language/nl_nl.dart @@ -2147,12 +2147,12 @@ const Map nlNL = { 'inCallBusyDesc': 'De microfoon en het audiopad worden gebruikt door het gesprek. Probeer het opnieuw na het ophangen.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Gratis', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/pl_pl.dart b/apps/client/lib/core/translations/language/pl_pl.dart index a5c5adbd..55a542f1 100644 --- a/apps/client/lib/core/translations/language/pl_pl.dart +++ b/apps/client/lib/core/translations/language/pl_pl.dart @@ -2121,12 +2121,12 @@ const Map plPL = { 'inCallBusyDesc': 'Mikrofon i tor audio są używane przez rozmowę. Spróbuj ponownie po rozłączeniu.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Bezpłatne', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/pt_pt.dart b/apps/client/lib/core/translations/language/pt_pt.dart index af5b61fb..d50bdac3 100644 --- a/apps/client/lib/core/translations/language/pt_pt.dart +++ b/apps/client/lib/core/translations/language/pt_pt.dart @@ -2159,12 +2159,12 @@ const Map ptPT = { 'notNow': 'Agora não', 'permanentlyDisallow': 'Não permitir', 'otaBesNotSupported': 'Estes auscultadores ainda não suportam a atualização de firmware na app. Contacte o suporte.', - 'smartcarConnected': 'Ligado', - 'smartcarConnectFailed': 'Falha na ligação', - 'smartcarConnecting': 'A ligar…', - 'smartcarSearching': 'A procurar difusores de carro próximos…', - 'smartcarNotFound': 'Nenhum dispositivo encontrado', - 'smartcarScanHint': 'Certifique-se de que o dispositivo está ligado e perto do telemóvel', + 'eaicarConnected': 'Ligado', + 'eaicarConnectFailed': 'Falha na ligação', + 'eaicarConnecting': 'A ligar…', + 'eaicarSearching': 'A procurar difusores de carro próximos…', + 'eaicarNotFound': 'Nenhum dispositivo encontrado', + 'eaicarScanHint': 'Certifique-se de que o dispositivo está ligado e perto do telemóvel', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/ro_ro.dart b/apps/client/lib/core/translations/language/ro_ro.dart index abf93fa6..7f2321fd 100644 --- a/apps/client/lib/core/translations/language/ro_ro.dart +++ b/apps/client/lib/core/translations/language/ro_ro.dart @@ -2123,12 +2123,12 @@ const Map roRO = { 'inCallBusyDesc': 'Microfonul și calea audio sunt folosite de apel. Reîncearcă după ce închizi.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Gratuit', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/ru_ru.dart b/apps/client/lib/core/translations/language/ru_ru.dart index 445069ac..1ba0181d 100644 --- a/apps/client/lib/core/translations/language/ru_ru.dart +++ b/apps/client/lib/core/translations/language/ru_ru.dart @@ -2134,12 +2134,12 @@ const Map ruRU = { 'notNow': 'Не сейчас', 'permanentlyDisallow': 'Не разрешать', 'otaBesNotSupported': 'Эта гарнитура пока не поддерживает обновление прошивки из приложения. Обратитесь в службу поддержки.', - 'smartcarConnected': 'Подключено', - 'smartcarConnectFailed': 'Ошибка подключения', - 'smartcarConnecting': 'Подключение…', - 'smartcarSearching': 'Поиск автомобильных ароматизаторов поблизости…', - 'smartcarNotFound': 'Устройство не найдено', - 'smartcarScanHint': 'Убедитесь, что устройство включено и находится рядом с телефоном', + 'eaicarConnected': 'Подключено', + 'eaicarConnectFailed': 'Ошибка подключения', + 'eaicarConnecting': 'Подключение…', + 'eaicarSearching': 'Поиск автомобильных ароматизаторов поблизости…', + 'eaicarNotFound': 'Устройство не найдено', + 'eaicarScanHint': 'Убедитесь, что устройство включено и находится рядом с телефоном', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/sk_sk.dart b/apps/client/lib/core/translations/language/sk_sk.dart index 1e236e74..506c29a9 100644 --- a/apps/client/lib/core/translations/language/sk_sk.dart +++ b/apps/client/lib/core/translations/language/sk_sk.dart @@ -2104,12 +2104,12 @@ const Map skSK = { 'inCallBusyDesc': 'Mikrofón a zvukovú cestu používa hovor. Skúste to znova po zavesení.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Zadarmo', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/sl_si.dart b/apps/client/lib/core/translations/language/sl_si.dart index 99792b9c..98be2ba9 100644 --- a/apps/client/lib/core/translations/language/sl_si.dart +++ b/apps/client/lib/core/translations/language/sl_si.dart @@ -2107,12 +2107,12 @@ const Map slSI = { 'inCallBusyDesc': 'Mikrofon in zvočno pot uporablja klic. Poskusite znova, ko odložite.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Brezplačno', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/sr_rs.dart b/apps/client/lib/core/translations/language/sr_rs.dart index b357c2de..9e352192 100644 --- a/apps/client/lib/core/translations/language/sr_rs.dart +++ b/apps/client/lib/core/translations/language/sr_rs.dart @@ -2103,12 +2103,12 @@ const Map srRS = { 'inCallBusyDesc': 'Mikrofon i audio putanja su zauzeti pozivom. Pokušajte ponovo nakon završetka poziva.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Besplatno', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/sv_se.dart b/apps/client/lib/core/translations/language/sv_se.dart index 7b4b94fb..79e24064 100644 --- a/apps/client/lib/core/translations/language/sv_se.dart +++ b/apps/client/lib/core/translations/language/sv_se.dart @@ -2099,12 +2099,12 @@ const Map svSE = { 'inCallBusyDesc': 'Mikrofonen och ljudvägen används av samtalet. Försök igen när du har lagt på.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Gratis', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/th_th.dart b/apps/client/lib/core/translations/language/th_th.dart index 40396647..5d97acb2 100644 --- a/apps/client/lib/core/translations/language/th_th.dart +++ b/apps/client/lib/core/translations/language/th_th.dart @@ -2052,12 +2052,12 @@ const Map thTH = { 'inCallBusyDesc': 'ไมโครโฟนและเส้นทางเสียงกำลังถูกใช้โดยการโทร กรุณาลองใหม่หลังวางสาย', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'ฟรี', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/tl_ph.dart b/apps/client/lib/core/translations/language/tl_ph.dart index 584ab985..6ee4fbde 100644 --- a/apps/client/lib/core/translations/language/tl_ph.dart +++ b/apps/client/lib/core/translations/language/tl_ph.dart @@ -2137,12 +2137,12 @@ const Map tlPH = { 'inCallBusyDesc': 'Ginagamit ng tawag ang mikropono at audio path. Subukang muli pagkatapos ibaba ang tawag.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Libre', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/tr_tr.dart b/apps/client/lib/core/translations/language/tr_tr.dart index 54297ccb..ba5e2dca 100644 --- a/apps/client/lib/core/translations/language/tr_tr.dart +++ b/apps/client/lib/core/translations/language/tr_tr.dart @@ -2093,12 +2093,12 @@ const Map trTR = { 'inCallBusyDesc': 'Mikrofon ve ses yolu arama tarafından kullanılıyor. Aramayı kapattıktan sonra tekrar deneyin.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Ücretsiz', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/uk_ua.dart b/apps/client/lib/core/translations/language/uk_ua.dart index ab096e44..83828b43 100644 --- a/apps/client/lib/core/translations/language/uk_ua.dart +++ b/apps/client/lib/core/translations/language/uk_ua.dart @@ -2111,12 +2111,12 @@ const Map ukUA = { 'inCallBusyDesc': 'Мікрофон і аудіотракт зайняті дзвінком. Спробуйте ще раз після завершення дзвінка.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Безкоштовно', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/vi_vn.dart b/apps/client/lib/core/translations/language/vi_vn.dart index 0ea0b2bd..7d38d9e9 100644 --- a/apps/client/lib/core/translations/language/vi_vn.dart +++ b/apps/client/lib/core/translations/language/vi_vn.dart @@ -2071,12 +2071,12 @@ const Map viVN = { 'inCallBusyDesc': 'Micro và đường âm thanh đang được cuộc gọi sử dụng. Hãy thử lại sau khi cúp máy.', 'vipSectionTitle': 'VIP', 'freeSectionTitle': 'Miễn phí', - 'smartcarConnected': 'Connected', - 'smartcarConnectFailed': 'Connection failed', - 'smartcarConnecting': 'Connecting…', - 'smartcarSearching': 'Searching for nearby car diffusers…', - 'smartcarNotFound': 'No device found', - 'smartcarScanHint': 'Make sure the device is powered on and close to your phone', + 'eaicarConnected': 'Connected', + 'eaicarConnectFailed': 'Connection failed', + 'eaicarConnecting': 'Connecting…', + 'eaicarSearching': 'Searching for nearby car diffusers…', + 'eaicarNotFound': 'No device found', + 'eaicarScanHint': 'Make sure the device is powered on and close to your phone', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/zh_cn.dart b/apps/client/lib/core/translations/language/zh_cn.dart index 37feebee..ce16990a 100644 --- a/apps/client/lib/core/translations/language/zh_cn.dart +++ b/apps/client/lib/core/translations/language/zh_cn.dart @@ -1927,12 +1927,12 @@ const Map zhCN = { 'unmarkFavorite': '取消收藏', 'unnamedDevice': '未命名设备', 'userDataParseFailed': '用户数据解析失败', - 'smartcarConnected': '已连接', - 'smartcarConnectFailed': '连接失败', - 'smartcarConnecting': '正在连接…', - 'smartcarSearching': '正在搜索附近的车载香薰…', - 'smartcarNotFound': '没有找到设备', - 'smartcarScanHint': '请确认设备已通电,并靠近手机', + 'eaicarConnected': '已连接', + 'eaicarConnectFailed': '连接失败', + 'eaicarConnecting': '正在连接…', + 'eaicarSearching': '正在搜索附近的车载香薰…', + 'eaicarNotFound': '没有找到设备', + 'eaicarScanHint': '请确认设备已通电,并靠近手机', 'vipRequiredTitle': '需要会员', 'vipRequiredDesc': '翻译与会议纪要需要会员。续费会员,或绑定带会员的设备即可使用。', 'vipRequiredGo': '去开通', diff --git a/apps/client/lib/core/translations/language/zh_hk.dart b/apps/client/lib/core/translations/language/zh_hk.dart index 587f7d0b..6178ebac 100644 --- a/apps/client/lib/core/translations/language/zh_hk.dart +++ b/apps/client/lib/core/translations/language/zh_hk.dart @@ -1924,12 +1924,12 @@ const Map zhHK = { 'notNow': '暫不', 'permanentlyDisallow': '永久不允許', 'otaBesNotSupported': '此耳機暫不支援在 App 內升級韌體,請聯絡客服獲取升級方式', - 'smartcarConnected': '已連接', - 'smartcarConnectFailed': '連接失敗', - 'smartcarConnecting': '正在連接…', - 'smartcarSearching': '正在搜尋附近的車載香薰…', - 'smartcarNotFound': '沒有找到裝置', - 'smartcarScanHint': '請確認裝置已通電,並靠近手機', + 'eaicarConnected': '已連接', + 'eaicarConnectFailed': '連接失敗', + 'eaicarConnecting': '正在連接…', + 'eaicarSearching': '正在搜尋附近的車載香薰…', + 'eaicarNotFound': '沒有找到裝置', + 'eaicarScanHint': '請確認裝置已通電,並靠近手機', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/core/translations/language/zh_tw.dart b/apps/client/lib/core/translations/language/zh_tw.dart index 9ad08901..3b4271ee 100644 --- a/apps/client/lib/core/translations/language/zh_tw.dart +++ b/apps/client/lib/core/translations/language/zh_tw.dart @@ -1924,12 +1924,12 @@ const Map zhTW = { 'notNow': '暫不', 'permanentlyDisallow': '永久不允許', 'otaBesNotSupported': '此耳機暫不支援在 App 內升級韌體,請聯絡客服取得升級方式', - 'smartcarConnected': '已連接', - 'smartcarConnectFailed': '連接失敗', - 'smartcarConnecting': '正在連接…', - 'smartcarSearching': '正在搜尋附近的車載香氛…', - 'smartcarNotFound': '沒有找到裝置', - 'smartcarScanHint': '請確認裝置已通電,並靠近手機', + 'eaicarConnected': '已連接', + 'eaicarConnectFailed': '連接失敗', + 'eaicarConnecting': '正在連接…', + 'eaicarSearching': '正在搜尋附近的車載香氛…', + 'eaicarNotFound': '沒有找到裝置', + 'eaicarScanHint': '請確認裝置已通電,並靠近手機', 'vipRequiredTitle': 'Membership required', 'vipRequiredDesc': 'Translation and meeting notes require a membership. Renew, or pair a device that includes one.', 'vipRequiredGo': 'Get membership', diff --git a/apps/client/lib/data/models/agent_module.dart b/apps/client/lib/data/models/agent_module.dart index 314c49a8..af1acf97 100644 --- a/apps/client/lib/data/models/agent_module.dart +++ b/apps/client/lib/data/models/agent_module.dart @@ -41,7 +41,7 @@ class AgentModule { /// /// **设备按键唤醒时用它决定接哪个助手**:`DeviceAiSessionService` 按 /// 厂商 → 品类 → 默认编排第一个模块 → 这个字段,找到该说话的那个 agent。 - /// 车载香薰唤醒的是「Smartcar 车载助手」而不是 EMAI,就是靠这一条。 + /// 车载香薰唤醒的是「EaiCar 车载助手」而不是 EMAI,就是靠这一条。 /// /// ⚠️ 放在 data 层的基类而不是 `AgentModuleDescriptor`(modules 层),是为了让 /// `DeviceAiSessionService`(data 层)读得到 —— **data 层不反向依赖 modules 层**。 @@ -73,8 +73,8 @@ class AgentModules { /// EMAI 助手:原来 AI 页底部那颗悬浮球,改成广场里的一个正式 agent static const String emaiAssistant = 'emaiAssistant'; - /// Smartcar 车载助手(车载香薰的对话 agent,另一个百炼应用) - static const String smartcarChat = 'smartcarChat'; + /// EaiCar 车载助手(车载香薰的对话 agent,另一个百炼应用) + static const String eaicarChat = 'smartcarChat'; // 已铺进装机编排的模块 id,改名后值不动 static const String textTranslation = 'textTranslation'; static const String pictureTranslation = 'pictureTranslation'; @@ -121,14 +121,14 @@ class AgentModules { DeviceCategories.noteCard: [meeting, fieldRecording, callRecording], // AI耳机(恒玄):通话 + 翻译整套都能跑,铺开全量默认编排 DeviceCategories.earphone: defaultEnabled, - // AI车载香薰(Smartcar):随时开聊 + 现场录音 + 语音纪要。 + // AI车载香薰(EaiCar):随时开聊 + 现场录音 + 语音纪要。 // 这三个都不依赖设备能力(只有通话录音/翻译才声明 requiredCapabilities), // 所以连上香薰就能直接用,不会灰。 // ⚠️ 这是**默认编排不是权限限制**——用户仍可去广场把别的功能加回来, // 这是产品有意接受的(落地文档 6.5)。 - // ⚠️ 2026-09-11 从「只有 smartcarChat」改成这三个;已经铺过旧编排的装机 + // ⚠️ 2026-09-11 从「只有 eaicarChat」改成这三个;已经铺过旧编排的装机 // 由 AgentModuleService 的一次性补铺处理,见那边的 _migrateCarAromaDefaults。 - DeviceCategories.carAroma: [smartcarChat, fieldRecording, meeting], + DeviceCategories.carAroma: [eaicarChat, fieldRecording, meeting], }; static List defaultsForCategory(String? categoryId) { diff --git a/apps/client/lib/data/models/assistant_directive.dart b/apps/client/lib/data/models/assistant_directive.dart index 9cec5e0c..40867aa5 100644 --- a/apps/client/lib/data/models/assistant_directive.dart +++ b/apps/client/lib/data/models/assistant_directive.dart @@ -161,7 +161,7 @@ class AssistantDirective { /// 解析 `extra_info.commands` —— **另一种指令形状**。 /// - /// ⚠️ 2026-09-11 真机发现:Smartcar 车载助手下发的指令不在 `tool_calls` 里, + /// ⚠️ 2026-09-11 真机发现:EaiCar 车载助手下发的指令不在 `tool_calls` 里, /// 而是 `extra_info.commands`,形如 /// ```json /// "commands":[{"intent_info":{"domain":"general_command","intent":"quit"}, diff --git a/apps/client/lib/data/models/device_category.dart b/apps/client/lib/data/models/device_category.dart index a796a79e..f578151e 100644 --- a/apps/client/lib/data/models/device_category.dart +++ b/apps/client/lib/data/models/device_category.dart @@ -106,7 +106,7 @@ class DeviceCategories { icon: Icons.air, gradient: [Color(0xFFFFB74D), Color(0xFFE65100)], imageAsset: '${DeviceImages.dir}EaiCar.png', - nameKeywords: ['smartcar', '车载', '香薰', 'aroma', 'diffuser'], + nameKeywords: ['eaicar', 'smartcar', '车载', '香薰', 'aroma', 'diffuser'], ), ]; diff --git a/apps/client/lib/data/services/agent_module_defaults.dart b/apps/client/lib/data/services/agent_module_defaults.dart index 7edc7216..8f46ad1a 100644 --- a/apps/client/lib/data/services/agent_module_defaults.dart +++ b/apps/client/lib/data/services/agent_module_defaults.dart @@ -17,7 +17,7 @@ class AgentDefaults { static const Map _vendorCategory = { DeviceVendors.bes: DeviceCategories.earphone, DeviceVendors.holder: DeviceCategories.phoneHolder, - DeviceVendors.smartcar: DeviceCategories.carAroma, + DeviceVendors.eaicar: DeviceCategories.carAroma, }; /// 厂商 → 品类。设备按键唤醒时用它找该接哪个 agent(见 diff --git a/apps/client/lib/data/services/agent_module_service.dart b/apps/client/lib/data/services/agent_module_service.dart index 397c1b8b..bab1885a 100644 --- a/apps/client/lib/data/services/agent_module_service.dart +++ b/apps/client/lib/data/services/agent_module_service.dart @@ -44,7 +44,7 @@ class AgentModuleService extends GetxService { // ⚠️ 当前这份编排如果是**设备默认铺下来的**(用户没动过),三个一次性迁移 // 一律跳过。它们是给「用户自己的编排」补新功能用的,而设备编排本来就该 // 只有那几项 —— 尤其 _migrateDefaultLayout 会把 defaultEnabled 里缺的全部 - // 追加回来,等于把「连上香薰只显示 Smartcar/现场录音/语音纪要」直接抹掉。 + // 追加回来,等于把「连上香薰只显示 EaiCar/现场录音/语音纪要」直接抹掉。 // // 2026-09-14 线上就是这么坏的:香薰编排铺成 3 项 → 重启 App → 迁移追加成 // 11 项 → 而基线 _lastAppliedKey 还停在 3 项 → 之后 applyDeviceDefaults @@ -226,7 +226,7 @@ class AgentModuleService extends GetxService { /// 连上某品类的设备时,把 AI 页铺成这个品类的默认编排。 /// /// 规则只有一条:**只要用户没自己动过编排,AI 页就跟着当前设备走**。 - /// 于是「连上车载香薰 → 只剩 Smartcar / 现场录音 / 语音纪要」「换回耳机 → 恢复全量」 + /// 于是「连上车载香薰 → 只剩 EaiCar / 现场录音 / 语音纪要」「换回耳机 → 恢复全量」 /// 都成立,而用户一旦增删或排序过,就再也不会被自动改动。 /// /// 判据是 [_lastAppliedKey]:上一次**自动铺**下去的那份清单。 diff --git a/apps/client/lib/data/services/bailian_multimodal_service.dart b/apps/client/lib/data/services/bailian_multimodal_service.dart index 61cf74e9..77508fcc 100644 --- a/apps/client/lib/data/services/bailian_multimodal_service.dart +++ b/apps/client/lib/data/services/bailian_multimodal_service.dart @@ -94,7 +94,7 @@ class BailianEvent { /// 也就是说 EMAI 与设备唤醒会话的行为一个字节都没变。 /// /// 之所以做成实例级而不是改全局:不同 agent 用的是百炼里不同的应用 -/// (EMAI 一个、Smartcar 车载助手另一个),改全局值会把所有 agent 一起换掉。 +/// (EMAI 一个、EaiCar 车载助手另一个),改全局值会把所有 agent 一起换掉。 /// /// ⚠️ 每个应用**必须各自 new 一个实例**,不能共用:`start()` 第一件事就是 `stop()`, /// 共用会让两边互相把对方的会话掐掉,下行音频还会串到对方的播放器上。 diff --git a/apps/client/lib/data/services/device_ai_session_service.dart b/apps/client/lib/data/services/device_ai_session_service.dart index a9b3dbe4..3c8684d1 100644 --- a/apps/client/lib/data/services/device_ai_session_service.dart +++ b/apps/client/lib/data/services/device_ai_session_service.dart @@ -44,7 +44,7 @@ class DeviceAiSessionService extends GetxService { /// 共用一个单例的话两边会互相 stop 掉对方、下行音频也会串。 /// /// ⚠️ **接哪个百炼应用要看是哪台设备唤醒的**:耳机/支架接 EMAI,车载香薰接 - /// 「Smartcar 车载助手」(各自的 app_id 不同)。所以这里不能是 final 单例, + /// 「EaiCar 车载助手」(各自的 app_id 不同)。所以这里不能是 final 单例, /// 由 [_ensureService] 按厂商换实例;app_id 没变就复用,不白开 WebSocket。 BailianMultimodalService _svc = BailianMultimodalService(); @@ -355,7 +355,7 @@ class DeviceAiSessionService extends GetxService { Logger.e(_tag, '设备未处于已连接状态,拒绝本次唤醒'); return; } - // ★ 先按厂商选中该说话的那个 agent(车载香薰 → Smartcar 车载助手), + // ★ 先按厂商选中该说话的那个 agent(车载香薰 → EaiCar 车载助手), // 再去看 isConfigured —— 反过来的话检查的是上一台设备的应用 _ensureService(_resolveAgentAppId(session.vendor)); Logger.i(_tag, diff --git a/apps/client/lib/data/utils/emai_audio_session.dart b/apps/client/lib/data/utils/emai_audio_session.dart index bf114906..4bfe9ed5 100644 --- a/apps/client/lib/data/utils/emai_audio_session.dart +++ b/apps/client/lib/data/utils/emai_audio_session.dart @@ -11,7 +11,7 @@ enum EmaiAudioProfile { /// 手机麦克风 + 手机放音(EMAI 页「按住说话」)。必须 playAndRecord。 phoneMic, - /// 只放音,麦克风在**设备**上(Smartcar 车载香薰按键唤醒:采音走 BLE opus, + /// 只放音,麦克风在**设备**上(EaiCar 车载香薰按键唤醒:采音走 BLE opus, /// 手机一个字节都不录)。 /// /// 这一档必须用 playback 而不是 playAndRecord —— 见 [ensure] 里的说明。 @@ -52,7 +52,7 @@ class EmaiAudioSession { /// /// ⚠️ **playbackOnly 这一档是有来历的**(2026-09-14 修):车载香薰唤醒 AI 后 /// 回复声音从**手机扬声器**出来,而不是从香薰喇叭出来。香薰的下行本来就设计成 - /// 走经典蓝牙 A2DP(`_SmartcarPhoneSpeaker` 的注释写的就是「A2DP 连着时从设备 + /// 走经典蓝牙 A2DP(`_EaiCarPhoneSpeaker` 的注释写的就是「A2DP 连着时从设备 /// 喇叭出来」),坏在这份会话配置上,两处叠加: /// 1. `defaultToSpeaker` 把输出钉在内置扬声器上; /// 2. 选项里只有 `allowBluetooth`(那是 HFP 免提),**没有 allowBluetoothA2dp** diff --git a/apps/client/lib/devices/device_auth.dart b/apps/client/lib/devices/device_auth.dart index 02e7a788..0987c77d 100644 --- a/apps/client/lib/devices/device_auth.dart +++ b/apps/client/lib/devices/device_auth.dart @@ -9,8 +9,8 @@ import 'bes/bes_device_plugin.dart'; import 'device_bind_registry.dart'; import 'device_hub.dart'; import 'holder/holder_device_plugin.dart'; -import 'smartcar/smartcar_device_auth.dart'; -import 'smartcar/smartcar_device_plugin.dart'; +import 'eaicar/eaicar_device_auth.dart'; +import 'eaicar/eaicar_device_plugin.dart'; /// 确权结论(厂商无关)。 /// @@ -38,7 +38,7 @@ class DeviceAuth { /// 登出:换账号后同一台设备要重新登记到新账号名下。**白名单不清**。 static void reset() { BesDeviceAuth.reset(); - SmartcarDeviceAuth.reset(); + EaiCarDeviceAuth.reset(); // 换账号后同一台设备要重新登记到新账号名下(服务端会接管) DeviceBindRegistry.reset(); } @@ -49,13 +49,13 @@ class DeviceAuth { /// 而各家白名单是按 MAC 存的、互不相干,多清一次无副作用。 static Future forget(String mac) async { await BesDeviceAuth.forget(mac); - await SmartcarDeviceAuth.forget(mac); + await EaiCarDeviceAuth.forget(mac); } /// 【调试/售后】清空白名单 static Future clearWhitelist() async { await BesDeviceAuth.clearWhitelist(); - await SmartcarDeviceAuth.clearWhitelist(); + await EaiCarDeviceAuth.clearWhitelist(); } /// 补校当前连着的、需要确权的设备(顺带完成绑定登记)。 @@ -76,9 +76,9 @@ class DeviceAuth { } else if (p is HolderDevicePlugin) { // 支架没有单独的确权类,登记就在服务自己身上(本进程内已成功的会去重) await p.link.bindConnectedToServer(); - } else if (p is SmartcarDevicePlugin) { - // Smartcar 的 MAC 来自广播,两端都拿得到(不像恒玄 iOS 只有 UUID) - await SmartcarDeviceAuth.verify( + } else if (p is EaiCarDevicePlugin) { + // EaiCar 的 MAC 来自广播,两端都拿得到(不像恒玄 iOS 只有 UUID) + await EaiCarDeviceAuth.verify( mac: s.info.macAddress, name: s.info.name, ); diff --git a/apps/client/lib/devices/device_bootstrap.dart b/apps/client/lib/devices/device_bootstrap.dart index 68ba6d20..61da417e 100644 --- a/apps/client/lib/devices/device_bootstrap.dart +++ b/apps/client/lib/devices/device_bootstrap.dart @@ -6,7 +6,7 @@ import 'bes/bes_device_auth.dart'; import 'bes/bes_device_plugin.dart'; import 'device_hub.dart'; import 'holder/holder_device_plugin.dart'; -import 'smartcar/smartcar_device_plugin.dart'; +import 'eaicar/eaicar_device_plugin.dart'; /// 装配设备中枢:注册所有厂商插件并初始化。由 InitialBinding 调用一次。 /// @@ -26,9 +26,9 @@ void installDeviceHub() { _register(hub, HolderDevicePlugin()); - // Smartcar 车载香薰。BLE 走 flutter_blue_plus(纯 Dart,无原生插件)。 + // EaiCar 车载香薰。BLE 走 flutter_blue_plus(纯 Dart,无原生插件)。 // initialize 里不申请权限也不起扫描,冷启动零副作用。 - _register(hub, SmartcarDevicePlugin()); + _register(hub, EaiCarDevicePlugin()); } void _register(DeviceHub hub, DevicePlugin p) { diff --git a/apps/client/lib/devices/device_vendors.dart b/apps/client/lib/devices/device_vendors.dart index 4b5ffd09..f5cca98e 100644 --- a/apps/client/lib/devices/device_vendors.dart +++ b/apps/client/lib/devices/device_vendors.dart @@ -9,6 +9,6 @@ class DeviceVendors { /// EaiRec 录音支架,BLE + Opus static const String holder = 'holder'; - /// Smartcar 车载香薰(协议待接) - static const String smartcar = 'smartcar'; + /// EaiCar 车载香薰(协议待接) + static const String eaicar = 'smartcar'; // 持久化的厂商键,改名后值不动 } diff --git a/apps/client/lib/devices/smartcar/smartcar_device_auth.dart b/apps/client/lib/devices/eaicar/eaicar_device_auth.dart similarity index 95% rename from apps/client/lib/devices/smartcar/smartcar_device_auth.dart rename to apps/client/lib/devices/eaicar/eaicar_device_auth.dart index d3b67019..6c89c1e2 100644 --- a/apps/client/lib/devices/smartcar/smartcar_device_auth.dart +++ b/apps/client/lib/devices/eaicar/eaicar_device_auth.dart @@ -8,7 +8,7 @@ import '../../data/services/network/api.dart'; import '../device_auth.dart'; import '../device_bind_registry.dart'; -/// 「账号 ↔ Smartcar」的确权 + 绑定登记,走服务端 `user_binddevice`。 +/// 「账号 ↔ EaiCar」的确权 + 绑定登记,走服务端 `user_binddevice`。 /// /// 与恒玄那套([BesDeviceAuth])同一个套路,判定口径逐条对齐, /// 但有**一个关键差别**—— @@ -18,7 +18,7 @@ import '../device_bind_registry.dart'; /// 恒玄链路上 iOS 拿不到真 MAC(CoreBluetooth 只给 peripheral UUID, /// 每台手机看同一只耳机都不一样),所以那边 iOS 一律放行、闸门等于没有。 /// -/// Smartcar 的**经典蓝牙 MAC 就写在 BLE 广播里**(厂商数据 CID 之后的 6 字节,原样、不异或), +/// EaiCar 的**经典蓝牙 MAC 就写在 BLE 广播里**(厂商数据 CID 之后的 6 字节,原样、不异或), /// 两端解出来完全一致。所以这条链路的确权在 iOS 上是真生效的。 /// ⚠️ 别照抄恒玄那句「非 Android 直接返回 null」。 /// @@ -38,10 +38,10 @@ import '../device_bind_registry.dart'; /// [DeviceAuthResult.unknown] 放行是刻意选的失败方向:判严的代价是 /// **合法用户一断网就用不了自己的设备**;判宽只是未登记设备离线时能用一会儿, /// 联网第一次确权就会被拦下。前者严重得多。 -class SmartcarDeviceAuth { - SmartcarDeviceAuth._(); +class EaiCarDeviceAuth { + EaiCarDeviceAuth._(); - static const String _tag = 'SmartcarDeviceAuth'; + static const String _tag = 'EaiCarDeviceAuth'; /// 确权通过的 MAC 白名单(持久化,存大写) static const String _authorizedKey = 'smartcar_authorized_macs'; @@ -92,7 +92,7 @@ class SmartcarDeviceAuth { _reported.clear(); } - /// 校验一台刚连上的 Smartcar。 + /// 校验一台刚连上的 EaiCar。 /// /// [mac] 来自广播(CID 之后 6 字节原样)。拿不到就 [DeviceAuthResult.unknown]。 static Future verify({ diff --git a/apps/client/lib/devices/smartcar/smartcar_device_plugin.dart b/apps/client/lib/devices/eaicar/eaicar_device_plugin.dart similarity index 84% rename from apps/client/lib/devices/smartcar/smartcar_device_plugin.dart rename to apps/client/lib/devices/eaicar/eaicar_device_plugin.dart index f25e434d..4431da2c 100644 --- a/apps/client/lib/devices/smartcar/smartcar_device_plugin.dart +++ b/apps/client/lib/devices/eaicar/eaicar_device_plugin.dart @@ -13,14 +13,14 @@ import '../../data/utils/pcm_stream_player.dart'; import '../../core/utils/permission_util.dart'; import '../device_auth.dart'; import '../device_vendors.dart'; -import 'smartcar_device_auth.dart'; -import 'smartcar_link.dart'; -import 'smartcar_ota_port.dart'; -import 'smartcar_protocol.dart'; +import 'eaicar_device_auth.dart'; +import 'eaicar_link.dart'; +import 'eaicar_ota_port.dart'; +import 'eaicar_protocol.dart'; /// EaiCar(车载香薰,旧名 Smartcar)厂商插件。 /// -/// 把 [SmartcarLink] 的 C2 私有协议适配成 [DevicePlugin] / [DeviceSession] 契约, +/// 把 [EaiCarLink] 的 C2 私有协议适配成 [DevicePlugin] / [DeviceSession] 契约, /// 业务层看到的只是「一台能读电量、能报 MAC、能发私有命令的设备」。 /// /// ## 本期(阶段 1~3)范围 @@ -30,16 +30,16 @@ import 'smartcar_protocol.dart'; /// /// 还没做、要等后续阶段的: /// - `micUplink` / `aiWake`:Opus 上行(阶段 6),要在同一条 GATT 上订阅 -/// [SmartcarProtocol.audioNotifyCharUuid],**不能另起连接** +/// [EaiCarProtocol.audioNotifyCharUuid],**不能另起连接** /// - `ota`:阶段 7,搬杰理 OTA SDK /// - 经典蓝牙 A2DP 音乐(阶段 5),要先给 `classic_bluetooth` 补 `connectDevice` -class SmartcarDevicePlugin implements DevicePlugin { - static const String _tag = 'SmartcarDevicePlugin'; +class EaiCarDevicePlugin implements DevicePlugin { + static const String _tag = 'EaiCarDevicePlugin'; /// 记住的设备(自动回连用)。存 `{id, name, mac}`。 static const String _rememberedKey = 'smartcar_remembered_devices'; - final SmartcarLink link = SmartcarLink(); + final EaiCarLink link = EaiCarLink(); final GetStorage _storage = GetStorage(); final _events = StreamController.broadcast(); @@ -47,22 +47,22 @@ class SmartcarDevicePlugin implements DevicePlugin { /// /// 业务层**不能** import 厂商目录(`device_hub_boundary_test` 守着), /// 所以诊断信息只能顺着插件事件流出去。 - static const String kScanDiagnosticKey = 'smartcar.scan_diagnostic'; + static const String kScanDiagnosticKey = 'eaicar.scan_diagnostic'; - SmartcarSession? _session; + EaiCarSession? _session; StreamSubscription? _connSub; StreamSubscription>? _diagSub; bool _disposed = false; bool _scanning = false; @override - String get vendorKey => DeviceVendors.smartcar; + String get vendorKey => DeviceVendors.eaicar; @override String get displayName => 'EaiCar (车载香薰)'; @override - Set get capabilities => SmartcarSession.caps; + Set get capabilities => EaiCarSession.caps; @override DevicePluginConfigSchema get configSchema => DevicePluginConfigSchema.empty; @@ -137,7 +137,7 @@ class SmartcarDevicePlugin implements DevicePlugin { // ───────────────────────────── 扫描 ───────────────────────────── - StreamSubscription? _scanSub; + StreamSubscription? _scanSub; @override Future startScan({DeviceScanFilter? filter, Duration? timeout}) async { @@ -213,7 +213,7 @@ class SmartcarDevicePlugin implements DevicePlugin { await link.connect( deviceId, timeout: options?.timeout ?? const Duration(seconds: 45), - adv: adv is SmartcarAdvertisement ? adv : null, + adv: adv is EaiCarAdvertisement ? adv : null, ); } on TimeoutException catch (e) { throw DeviceException(DeviceErrorCode.connectTimeout, '$e'); @@ -221,7 +221,7 @@ class SmartcarDevicePlugin implements DevicePlugin { throw DeviceException(DeviceErrorCode.connectFailed, '$e'); } - final s = SmartcarSession( + final s = EaiCarSession( deviceId: deviceId, link: link, plugin: this, @@ -242,8 +242,8 @@ class SmartcarDevicePlugin implements DevicePlugin { // 确权闸门。放在握手之后:要拿设备名去登记,而设备名是握手时才确定的。 // // ⚠️ 只有 denied 才断开。unknown(未登录/断网/没解出 MAC)一律放行—— - // 判严的代价是合法用户断网就用不了自己的设备,见 [SmartcarDeviceAuth] 类注释。 - final auth = await SmartcarDeviceAuth.verify( + // 判严的代价是合法用户断网就用不了自己的设备,见 [EaiCarDeviceAuth] 类注释。 + final auth = await EaiCarDeviceAuth.verify( mac: s.info.macAddress, name: s.info.name, ); @@ -345,16 +345,16 @@ class SmartcarDevicePlugin implements DevicePlugin { } void _checkAlive() { - if (_disposed) throw StateError('SmartcarDevicePlugin 已 dispose'); + if (_disposed) throw StateError('EaiCarDevicePlugin 已 dispose'); } } -/// Smartcar 的会话句柄 -class SmartcarSession implements DeviceSession { - SmartcarSession({ +/// EaiCar 的会话句柄 +class EaiCarSession implements DeviceSession { + EaiCarSession({ required this.deviceId, - required SmartcarLink link, - required SmartcarDevicePlugin plugin, + required EaiCarLink link, + required EaiCarDevicePlugin plugin, String? macAddress, }) : _link = link, _plugin = plugin, @@ -370,11 +370,11 @@ class SmartcarSession implements DeviceSession { DeviceCapability.scan, DeviceCapability.connect, DeviceCapability.battery, - // Smartcar 的 MAC 在广播里,**iOS 也拿得到**——这点比恒玄强, + // EaiCar 的 MAC 在广播里,**iOS 也拿得到**——这点比恒玄强, // 恒玄在 iOS 上只有 peripheral UUID,确权等于没有。 DeviceCapability.macAddress, DeviceCapability.customCommand, - // 杰理 OTA:原生 cmdOTAData + stage2 扫描回连(从 lumi 搬),见 [SmartcarOtaPort] + // 杰理 OTA:原生 cmdOTAData + stage2 扫描回连(从 lumi 搬),见 [EaiCarOtaPort] DeviceCapability.ota, // 设备麦克风上行:opus → 16k PCM(原生 SmartcarAudio 解码) DeviceCapability.micUplink, @@ -383,28 +383,28 @@ class SmartcarSession implements DeviceSession { DeviceCapability.aiWake, // ⚠️ 刻意**不声明** speakerDownlink:AI 回复不走 BLE,走经典蓝牙 A2DP // (手机正常播放,设备当音箱)。openSpeaker(ai) 返回的是"手机扬声器"这个汇, - // 见 [_SmartcarPhoneSpeaker]。 + // 见 [_EaiCarPhoneSpeaker]。 }; @override final String deviceId; - final SmartcarLink _link; - /// OTA 结束后回连要用([SmartcarOtaPort.close]) - final SmartcarDevicePlugin _plugin; + final EaiCarLink _link; + /// OTA 结束后回连要用([EaiCarOtaPort.close]) + final EaiCarDevicePlugin _plugin; final String? _mac; - StreamSubscription? _frameSub; + StreamSubscription? _frameSub; final _events = StreamController.broadcast(); final _wake = StreamController.broadcast(); - _SmartcarMic? _mic; - _SmartcarPhoneSpeaker? _spk; + _EaiCarMic? _mic; + _EaiCarPhoneSpeaker? _spk; DeviceConnectionState _state = DeviceConnectionState.linkConnected; DeviceInfo _info = const DeviceInfo(id: '', name: '', vendor: ''); @override - String get vendor => DeviceVendors.smartcar; + String get vendor => DeviceVendors.eaicar; @override DeviceConnectionState get state => _state; @@ -424,7 +424,7 @@ class SmartcarSession implements DeviceSession { /// 握手:先宣告「APP 已连接」(0x10),再取一次固件版本 + 参数汇总。 /// /// ⚠️ 0x10 不能省:杰理认证通过、0x15 也能应答的情况下,设备屏幕照样显示 - /// BLE 未连接,直到收到这条(真机 2026-09-10 验证,见 [SmartcarProtocol.cmdAppConnected])。 + /// BLE 未连接,直到收到这条(真机 2026-09-10 验证,见 [EaiCarProtocol.cmdAppConnected])。 /// 它失败就判握手失败——"App 以为连上了、设备不认"正是这次排查的起点。 /// /// 0x15 一次就把电量、版本、各种开关全带回来了,比逐条问省好几个往返。 @@ -441,7 +441,7 @@ class SmartcarSession implements DeviceSession { await _syncTime(); Object? e15; try { - final f = await _link.send(SmartcarProtocol.cmdSyncSummary); + final f = await _link.send(EaiCarProtocol.cmdSyncSummary); if (f != null && f.success) { _applySummary(f.payload); _state = DeviceConnectionState.ready; @@ -450,22 +450,22 @@ class SmartcarSession implements DeviceSession { e15 = '设备回了 0x15 但 len=0(命令执行失败)'; } catch (e) { e15 = e; - Logger.w('SmartcarSession', '0x15 参数汇总失败,退回取版本号: $e'); + Logger.w('EaiCarSession', '0x15 参数汇总失败,退回取版本号: $e'); } Object? e01; - SmartcarFrame? v; + EaiCarFrame? v; try { - v = await _link.send(SmartcarProtocol.cmdGetVersion); + v = await _link.send(EaiCarProtocol.cmdGetVersion); } catch (e) { e01 = e; } if (v == null || !v.success) { // 两条查询命令都没回应答。最可能的原因按概率排: - // ① 命令号不对(协议文档与 lumi 代码本来就冲突,见 SmartcarProtocol 的 - // TODO(smartcar-cmd));② CRC 算法或帧格式对不上;③ 这版固件不支持。 + // ① 命令号不对(协议文档与 lumi 代码本来就冲突,见 EaiCarProtocol 的 + // TODO(eaicar-cmd));② CRC 算法或帧格式对不上;③ 这版固件不支持。 throw StateError('0x15 与 0x01 都没有有效应答 ' '(0x15: $e15; 0x01: ${e01 ?? '无应答'})。' - '多半是命令号或帧格式不对——见 SmartcarProtocol 里的 TODO(smartcar-cmd)'); + '多半是命令号或帧格式不对——见 EaiCarProtocol 里的 TODO(eaicar-cmd)'); } _applyVersion(v.payload); _state = DeviceConnectionState.ready; @@ -474,13 +474,13 @@ class SmartcarSession implements DeviceSession { /// 0x10「蓝牙连接成功」:`[设备类型, 车企编号]`,设备回 `BB 10 01 64` 才算数。 Future _announceAppConnected() async { final appType = Platform.isIOS - ? SmartcarProtocol.appTypeIos - : SmartcarProtocol.appTypeAndroid; - SmartcarFrame? f; + ? EaiCarProtocol.appTypeIos + : EaiCarProtocol.appTypeAndroid; + EaiCarFrame? f; try { f = await _link.send( - SmartcarProtocol.cmdAppConnected, - payload: [appType, SmartcarProtocol.carVendorDefault], + EaiCarProtocol.cmdAppConnected, + payload: [appType, EaiCarProtocol.carVendorDefault], ); } catch (e) { throw StateError('0x10「蓝牙连接成功」无应答:$e。' @@ -489,21 +489,21 @@ class SmartcarSession implements DeviceSession { if (f == null || !f.success) { throw StateError('设备拒绝了 0x10「蓝牙连接成功」(len=0)'); } - Logger.i('SmartcarSession', '已向设备宣告 APP 连接 (type=$appType),设备应答 ${f.payload}'); + Logger.i('EaiCarSession', '已向设备宣告 APP 连接 (type=$appType),设备应答 ${f.payload}'); } /// 杰理 RCSP 认证后的收尾:设时间 → GET_TARGET_INFO → GET_SYS_INFO。 /// /// lumi 两端的 JL SDK 都会在认证后自动发这一串(iOS `initDialSystem`、Android `WatchOpImpl`), /// 我们自己持有 GATT 就得自己发。2026-09-10 真机:0x10 + 0x15 都成功时屏幕仍显示未连接, - /// 把这串和 0x17 补上之后才亮。TODO(smartcar-handshake):还没二分出到底是哪一步点亮的, + /// 把这串和 0x17 补上之后才亮。TODO(eaicar-handshake):还没二分出到底是哪一步点亮的, /// 全部保留;失败只记日志不断链——这几条是"锦上添花",链路本身已经能用。 Future _jlPostAuth() async { try { final r = await _link.jlPostAuth(); - Logger.i('SmartcarSession', 'RCSP 认证后收尾: $r'); + Logger.i('EaiCarSession', 'RCSP 认证后收尾: $r'); } catch (e) { - Logger.w('SmartcarSession', 'RCSP 认证后收尾失败(继续): $e'); + Logger.w('EaiCarSession', 'RCSP 认证后收尾失败(继续): $e'); } } @@ -513,17 +513,17 @@ class SmartcarSession implements DeviceSession { final n = DateTime.now(); try { await _link.send( - SmartcarProtocol.cmdSyncTime, + EaiCarProtocol.cmdSyncTime, payload: [n.year % 100, n.month, n.day, n.hour, n.minute, n.second, 0x01], timeout: const Duration(seconds: 2), ); } catch (e) { - Logger.w('SmartcarSession', '0x17 时间同步无应答(继续): $e'); + Logger.w('EaiCarSession', '0x17 时间同步无应答(继续): $e'); } } void _applySummary(List p) { - final s = SmartcarAllSettings.parse(p); + final s = EaiCarAllSettings.parse(p); if (s == null) return; _info = _info.copyWith( firmwareVersion: s.firmwareVersion, @@ -541,13 +541,13 @@ class SmartcarSession implements DeviceSession { _emitInfo(); } - void _onFrame(SmartcarFrame f) { + void _onFrame(EaiCarFrame f) { if (f.isNotify) { // 设备主动上报。0xCC 0x11 = AI 唤醒:设备已经自己开麦了, // DeviceAiSessionService 收到 wakeEvents 后会 aiAccept + openMic。 - if (f.cmd == SmartcarProtocol.notifyAiWake) { - Logger.i('SmartcarSession', '设备唤醒 AI 对话 (0xCC 0x11)'); - _emitFeature('smartcar.ai_wake', const {}); + if (f.cmd == EaiCarProtocol.notifyAiWake) { + Logger.i('EaiCarSession', '设备唤醒 AI 对话 (0xCC 0x11)'); + _emitFeature('eaicar.ai_wake', const {}); if (!_wake.isClosed) { _wake.add(DeviceWakeEvent( deviceId: deviceId, @@ -557,7 +557,7 @@ class SmartcarSession implements DeviceSession { } return; } - if (f.cmd == SmartcarProtocol.cmdGetBattery && f.payload.isNotEmpty) { + if (f.cmd == EaiCarProtocol.cmdGetBattery && f.payload.isNotEmpty) { final b = f.payload[0] & 0xFF; _info = _info.copyWith( batteryPercent: b, battery: DeviceBattery(single: b)); @@ -567,9 +567,9 @@ class SmartcarSession implements DeviceSession { return; } if (f.isResponse && f.success) { - if (f.cmd == SmartcarProtocol.cmdSyncSummary) _applySummary(f.payload); - if (f.cmd == SmartcarProtocol.cmdGetVersion) _applyVersion(f.payload); - if (f.cmd == SmartcarProtocol.cmdGetBattery && f.payload.isNotEmpty) { + if (f.cmd == EaiCarProtocol.cmdSyncSummary) _applySummary(f.payload); + if (f.cmd == EaiCarProtocol.cmdGetVersion) _applyVersion(f.payload); + if (f.cmd == EaiCarProtocol.cmdGetBattery && f.payload.isNotEmpty) { final b = f.payload[0] & 0xFF; _info = _info.copyWith( batteryPercent: b, battery: DeviceBattery(single: b)); @@ -627,7 +627,7 @@ class SmartcarSession implements DeviceSession { } if (_state != DeviceConnectionState.ready) { throw DeviceException( - DeviceErrorCode.noActiveSession, 'Smartcar 会话未就绪'); + DeviceErrorCode.noActiveSession, 'EaiCar 会话未就绪'); } } @@ -642,7 +642,7 @@ class SmartcarSession implements DeviceSession { @override Future readBattery() async { _requireReady(); - final f = await _link.send(SmartcarProtocol.cmdGetBattery); + final f = await _link.send(EaiCarProtocol.cmdGetBattery); if (f == null || !f.success || f.payload.isEmpty) return null; return f.payload[0] & 0xFF; } @@ -650,7 +650,7 @@ class SmartcarSession implements DeviceSession { @override Future refreshInfo() async { _requireReady(); - final f = await _link.send(SmartcarProtocol.cmdSyncSummary); + final f = await _link.send(EaiCarProtocol.cmdSyncSummary); if (f != null && f.success) _applySummary(f.payload); return _info; } @@ -691,14 +691,14 @@ class SmartcarSession implements DeviceSession { throw DeviceException( DeviceErrorCode.notSupported, 'opus 解码器起不来,拿不到设备麦克风'); } - final src = _SmartcarMic(route, _link, _closeMic); + final src = _EaiCarMic(route, _link, _closeMic); _mic = src; return src; } /// 关掉某个麦克风源。**只有它还是当前那个才真去停解码器**—— /// 已经被新会话换掉的旧 mic 再 close 是空操作,否则会误伤新会话。 - Future _closeMic(_SmartcarMic mic) async { + Future _closeMic(_EaiCarMic mic) async { if (!identical(_mic, mic)) return; _mic = null; await _link.audioStop(); @@ -711,12 +711,12 @@ class SmartcarSession implements DeviceSession { throw DeviceException( DeviceErrorCode.notSupported, 'openSpeaker($route)'); } - // ⚠️ 与恒玄不同:Smartcar 的下行**不经 BLE**。AI 回复由手机正常播放, + // ⚠️ 与恒玄不同:EaiCar 的下行**不经 BLE**。AI 回复由手机正常播放, // 经典蓝牙 A2DP 把它送到设备喇叭。所以这个"汇"就是手机扬声器。 // 没配对 A2DP 时声音会从手机外放出来——功能仍然成立,只是不在车里响, // 所以这里只提示、不拦截。 if (!await isClassicAudioConnected()) { - Logger.w('SmartcarSession', + Logger.w('EaiCarSession', 'A2DP 未连接,AI 回复会从手机外放。设备 MAC=$_mac'); } // 同 openMic:先关旧的再建新的。这里每个 sink 自带播放器、互不共享, @@ -724,7 +724,7 @@ class SmartcarSession implements DeviceSession { final old = _spk; _spk = null; await old?.close(); - final sink = _SmartcarPhoneSpeaker(); + final sink = _EaiCarPhoneSpeaker(); _spk = sink; return sink; } @@ -742,11 +742,11 @@ class SmartcarSession implements DeviceSession { return ClassicBluetooth.isA2dpConnected(mac); } - SmartcarOtaPort? _otaPort; + EaiCarOtaPort? _otaPort; /// 杰理 OTA 端口。同一会话复用一个实例(端口内部有互斥,重复 start 抛 ota_busy)。 @override - DeviceOtaPort? otaPort() => _otaPort ??= SmartcarOtaPort(_link, _plugin); + DeviceOtaPort? otaPort() => _otaPort ??= EaiCarOtaPort(_link, _plugin); /// 车载香薰**没有**设备端文件仓库(没有 TF 卡,录音不落设备),所以恒为 null, /// 能力集里也没有 `DeviceCapability.fileStore`。 @@ -757,7 +757,7 @@ class SmartcarSession implements DeviceSession { /// 通用 feature key → C2 命令。 /// /// ⚠️ 这张表就是**冻结的协议**:同一个 key 永远发同样的字节。 - /// 改它之前先看 [SmartcarProtocol] 里那些 TODO(smartcar-cmd) 标记—— + /// 改它之前先看 [EaiCarProtocol] 里那些 TODO(eaicar-cmd) 标记—— /// 有几条命令号本身还没经真机验证。 @override Future> invokeFeature( @@ -772,16 +772,16 @@ class SmartcarSession implements DeviceSession { case 'state.refresh': await refreshInfo(); return {'ok': true}; - case 'smartcar.volume': + case 'eaicar.volume': final v = (args['value'] as num?)?.toInt() ?? 0; - await _link.send(SmartcarProtocol.cmdSetVolume, payload: [v & 0xFF]); + await _link.send(EaiCarProtocol.cmdSetVolume, payload: [v & 0xFF]); return {'ok': true}; - case 'smartcar.brightness': + case 'eaicar.brightness': final v = (args['value'] as num?)?.toInt() ?? 0; await _link - .send(SmartcarProtocol.cmdControlBrightness, payload: [v & 0xFF]); + .send(EaiCarProtocol.cmdControlBrightness, payload: [v & 0xFF]); return {'ok': true}; - case 'smartcar.app_connected': + case 'eaicar.app_connected': // 重发一次「APP 已连接」宣告(握手里已经发过;这里给排查/固件回落时用) await _announceAppConnected(); return {'ok': true}; @@ -790,10 +790,10 @@ class SmartcarSession implements DeviceSession { case DeviceFeatures.aiAccept: // 设备发完 0xCC 0x11 就自己开麦了,App 这边只要把状态回报过去, // 设备据此切"聆听中"的表情。**不需要**再下开麦命令。 - await _setAiState(SmartcarProtocol.aiStateListening); + await _setAiState(EaiCarProtocol.aiStateListening); return {'ok': true}; case DeviceFeatures.aiExit: - await _setAiState(SmartcarProtocol.aiStateClose); + await _setAiState(EaiCarProtocol.aiStateClose); await _link.audioStop(); return {'ok': true}; case DeviceFeatures.aiState: @@ -802,10 +802,10 @@ class SmartcarSession implements DeviceSession { // (2026-09-11 真机反馈"表情没反应"就是这个原因)。 final st = args['state']; final v = switch (st) { - DeviceAiState.listening => SmartcarProtocol.aiStateListening, - DeviceAiState.thinking => SmartcarProtocol.aiStateThinking, - DeviceAiState.speaking => SmartcarProtocol.aiStateSpeakStart, - DeviceAiState.idle => SmartcarProtocol.aiStateSpeakEnd, + DeviceAiState.listening => EaiCarProtocol.aiStateListening, + DeviceAiState.thinking => EaiCarProtocol.aiStateThinking, + DeviceAiState.speaking => EaiCarProtocol.aiStateSpeakStart, + DeviceAiState.idle => EaiCarProtocol.aiStateSpeakEnd, _ => null, }; if (v == null) { @@ -814,7 +814,7 @@ class SmartcarSession implements DeviceSession { } await _setAiState(v); return {'ok': true}; - case 'smartcar.ai_state': + case 'eaicar.ai_state': final v = (args['value'] as num?)?.toInt(); if (v == null) { throw DeviceException(DeviceErrorCode.invalidArgument, '缺少 value'); @@ -823,13 +823,13 @@ class SmartcarSession implements DeviceSession { return {'ok': true}; // ---------- 经典蓝牙 / 音乐 ---------- - case 'smartcar.music_playing': + case 'eaicar.music_playing': // 0x05:告诉设备手机在不在放音乐,设备据此显示"听音乐"表情 final on = args['value'] == true; - await _link.send(SmartcarProtocol.cmdMusicPlayState, + await _link.send(EaiCarProtocol.cmdMusicPlayState, payload: [on ? 0x01 : 0x00]); return {'ok': true}; - case 'smartcar.a2dp_status': + case 'eaicar.a2dp_status': return { 'mac': _mac, 'connected': await isClassicAudioConnected(), @@ -837,13 +837,13 @@ class SmartcarSession implements DeviceSession { .map((d) => {'name': d.name, 'address': d.address}) .toList(), }; - case 'smartcar.a2dp_connect': + case 'eaicar.a2dp_connect': return await connectClassicAudio(); - case 'smartcar.emotion': + case 'eaicar.emotion': final v = (args['value'] as num?)?.toInt() ?? 0; await _link - .send(SmartcarProtocol.cmdControlEmotion, payload: [v & 0xFF]); + .send(EaiCarProtocol.cmdControlEmotion, payload: [v & 0xFF]); return {'ok': true}; default: throw DeviceException(DeviceErrorCode.notSupported, 'feature $key'); @@ -854,10 +854,10 @@ class SmartcarSession implements DeviceSession { Future _setAiState(int state) async { try { await _link - .send(SmartcarProtocol.cmdAiState, payload: [state & 0xFF]); + .send(EaiCarProtocol.cmdAiState, payload: [state & 0xFF]); } catch (e) { // 状态同步失败不该中断会话——它只影响设备表情,不影响对话本身 - Logger.w('SmartcarSession', '0x07 AI 状态($state) 发送失败: $e'); + Logger.w('EaiCarSession', '0x07 AI 状态($state) 发送失败: $e'); } } @@ -876,7 +876,7 @@ class SmartcarSession implements DeviceSession { } try { final status = await ClassicBluetooth.connectDevice(mac); - Logger.i('SmartcarSession', + Logger.i('EaiCarSession', '经典蓝牙 connectDevice($mac) → $status(广播MAC=$_mac, BLE id=$deviceId)'); return { 'ok': status != null && status != 'unsupported_ios', @@ -884,7 +884,7 @@ class SmartcarSession implements DeviceSession { 'mac': mac, }; } catch (e) { - Logger.w('SmartcarSession', '经典蓝牙配对失败: $e'); + Logger.w('EaiCarSession', '经典蓝牙配对失败: $e'); return {'ok': false, 'status': 'error', 'error': '$e', 'mac': mac}; } } @@ -899,7 +899,7 @@ class SmartcarSession implements DeviceSession { /// - 广播 MAC ≠ BLE MAC → 广播给的是另一个地址,按文档当经典 MAC 用 /// - 广播 MAC == BLE MAC → 广播给的其实是 BLE 地址,按 lumi 的规则减一 /// - /// ⚠️ **未经安卓真机验证**(TODO(smartcar-a2dp))。首次真机看 + /// ⚠️ **未经安卓真机验证**(TODO(eaicar-a2dp))。首次真机看 /// `connectDevice(...)` 那行日志里三个地址的关系就能定论。 String? get classicMacCandidate { final mac = _mac; @@ -924,16 +924,16 @@ class SmartcarSession implements DeviceSession { } /// 设备麦克风 → 业务层。原生已经把 opus 解成 16k/16bit/mono PCM。 -class _SmartcarMic implements DeviceAudioSource { - _SmartcarMic(this.route, this._link, this._onClose); +class _EaiCarMic implements DeviceAudioSource { + _EaiCarMic(this.route, this._link, this._onClose); @override final AudioRoute route; - final SmartcarLink _link; + final EaiCarLink _link; /// 交给会话去决定要不要真停:这个源可能早就被新会话换掉了, /// 直接 `audioStop()` 会把新会话的解码器一起关了。 - final Future Function(_SmartcarMic) _onClose; + final Future Function(_EaiCarMic) _onClose; @override AudioFormat get format => AudioFormat.pcm16kMono; @@ -947,10 +947,10 @@ class _SmartcarMic implements DeviceAudioSource { /// AI 回复的去处:**手机扬声器**(经典蓝牙 A2DP 连着时就从设备喇叭出来)。 /// -/// 与恒玄那种"PCM 编码后经 BLE 灌给设备"完全不同——Smartcar 的下行不经 BLE, +/// 与恒玄那种"PCM 编码后经 BLE 灌给设备"完全不同——EaiCar 的下行不经 BLE, /// 所以这里不写任何字节到链路上,只是把 PCM 交给手机播放。 -class _SmartcarPhoneSpeaker implements DeviceAudioSink { - _SmartcarPhoneSpeaker(); +class _EaiCarPhoneSpeaker implements DeviceAudioSink { + _EaiCarPhoneSpeaker(); /// ⚠️ `playbackOnly: true` 是这条链路能从**香薰喇叭**出声的关键: /// 采音在设备上(BLE opus),手机一个字节都不录,会话就该用 playback 类别。 diff --git a/apps/client/lib/devices/smartcar/smartcar_link.dart b/apps/client/lib/devices/eaicar/eaicar_link.dart similarity index 87% rename from apps/client/lib/devices/smartcar/smartcar_link.dart rename to apps/client/lib/devices/eaicar/eaicar_link.dart index ca8c8500..d8064f52 100644 --- a/apps/client/lib/devices/smartcar/smartcar_link.dart +++ b/apps/client/lib/devices/eaicar/eaicar_link.dart @@ -4,13 +4,13 @@ import 'package:device_smartcar/device_smartcar.dart'; import 'package:flutter/services.dart'; import '../../core/utils/logger.dart'; -import 'smartcar_protocol.dart'; +import 'eaicar_protocol.dart'; -/// Smartcar 的 BLE 链路:扫描 / 连接 / 收发 C2 帧。 +/// EaiCar 的 BLE 链路:扫描 / 连接 / 收发 C2 帧。 /// /// ## 为什么是原生插件、不是 flutter_blue_plus(2026-09-10 真机踩出来的) /// -/// Smartcar 是杰理芯片,**固件要求先过杰理 RCSP 认证**(在服务 `AE00` 上,由闭源 +/// EaiCar 是杰理芯片,**固件要求先过杰理 RCSP 认证**(在服务 `AE00` 上,由闭源 /// `JL_BLEKit` 的 `JL_Assist` 跑挑战应答)才把手机当成已连接:认证前设备屏幕显示 /// BLE 未连接、`ABC0` 私有通道的命令也不理。lumi 两端代码里都写明了这一点 /// (Android:「全新的 BLE 连接,没有走 JLWatchManager 认证,设备拒绝响应任何 RCSP 命令」)。 @@ -20,7 +20,7 @@ import 'smartcar_protocol.dart'; /// CBPeripheralDelegate 的回调转交给 SDK,GATT 必须由自己持有,flutter_blue_plus /// 做不到。于是链路收进原生插件 `device_smartcar`,这里只剩通道封装。 /// -/// 组帧 / CRC / 广播解析仍在 Dart([SmartcarProtocol]),单测继续守着。 +/// 组帧 / CRC / 广播解析仍在 Dart([EaiCarProtocol]),单测继续守着。 /// /// ⚠️ 一部手机对同一个 peripheral **只能有一条 GATT 连接**。阶段 6 的 Opus 上行 /// 要在原生插件里复用这条连接订阅音频特征,**不能另起一条**。 @@ -32,24 +32,24 @@ import 'smartcar_protocol.dart'; /// /// ⚠️ 旧广播名 `Smartcar` 已于 2026-09-15 移除,刷着旧名字的样机从此扫不到。 /// 别"顺手加回来"——加回去准入就整个失效了。 -const List kSmartcarAllowedNames = ['EaiCar']; +const List kEaiCarAllowedNames = ['EaiCar']; -bool isAllowedSmartcarName(String raw) { +bool isAllowedEaiCarName(String raw) { final n = raw.trim().toUpperCase(); if (n.isEmpty) return false; - for (final allowed in kSmartcarAllowedNames) { + for (final allowed in kEaiCarAllowedNames) { if (n.startsWith(allowed.toUpperCase())) return true; } return false; } -class SmartcarLink { - static const String _tag = 'SmartcarLink'; +class EaiCarLink { + static const String _tag = 'EaiCarLink'; final DeviceSmartcar _native = DeviceSmartcar.instance; StreamSubscription>? _eventSub; - final _frames = StreamController.broadcast(); + final _frames = StreamController.broadcast(); final _connState = StreamController.broadcast(); final _diag = StreamController>.broadcast(); final _ota = StreamController>.broadcast(); @@ -63,7 +63,7 @@ class SmartcarLink { Stream> get otaEvents => _ota.stream; /// 解好的帧(CRC 已校验,坏帧不会出现在这里) - Stream get frames => _frames.stream; + Stream get frames => _frames.stream; /// 链路通断。**true = 杰理认证已通过**,不是 GATT 连上。 Stream get connected => _connState.stream; @@ -80,13 +80,13 @@ class SmartcarLink { String? get deviceId => _deviceId; /// 连上时记下的广播信息(MAC / License 从这里来) - SmartcarAdvertisement? advertisement; + EaiCarAdvertisement? advertisement; /// 最近一次原生上报的链路状态与原因,连接失败时拼进错误信息 String _lastState = 'disconnected'; String? _lastReason; - SmartcarLink() { + EaiCarLink() { _eventSub = _native.events.listen(_onNativeEvent, onError: (Object e) { Logger.w(_tag, '原生事件流错误: $e'); }); @@ -94,7 +94,7 @@ class SmartcarLink { // ───────────────────────────── 事件路由 ───────────────────────────── - StreamController? _scanCtrl; + StreamController? _scanCtrl; void _onNativeEvent(Map e) { switch (e['type']) { @@ -152,7 +152,7 @@ class SmartcarLink { } } - /// 启动杰理 OTA(原生 `SmartcarOta`:cmdOTAData + stage2 扫描回连)。失败抛 [StateError]。 + /// 启动杰理 OTA(原生 `EaiCarOta`:cmdOTAData + stage2 扫描回连)。失败抛 [StateError]。 Future otaStart(String filePath) async { try { await _native.otaStart(filePath); @@ -197,11 +197,11 @@ class SmartcarLink { /// 扫本品类设备。原生侧全量扫(不加 service 过滤——CoreBluetooth 的过滤只认 /// 「Service UUIDs」AD 字段,C2 的身份在厂商数据里,加了一台都扫不到),这里按两条判定收: /// 1. 厂商数据能解出合法 C2 广播且 CID == 0x8505(主判据,MAC/License 一并解出) - /// 2. 广播名含 `smartcar`(兜底) - Stream scan({ + /// 2. 广播名含 `eaicar`(兜底) + Stream scan({ Duration timeout = const Duration(seconds: 15), }) { - final ctrl = StreamController(); + final ctrl = StreamController(); _scanCtrl?.close(); _scanCtrl = ctrl; ctrl.onCancel = () async { @@ -218,7 +218,7 @@ class SmartcarLink { _diagLine('(扫描启动失败)', msg); if (!ctrl.isClosed) ctrl.addError(StateError(msg)); } on MissingPluginException { - const msg = '本平台没有 Smartcar 原生链路(只支持 Android / iOS)'; + const msg = '本平台没有 EaiCar 原生链路(只支持 Android / iOS)'; if (!ctrl.isClosed) ctrl.addError(StateError(msg)); } }(); @@ -247,15 +247,15 @@ class SmartcarLink { payload = mfg.sublist(2); } - SmartcarAdvertisement? adv; + EaiCarAdvertisement? adv; // 云衍厂商 ID 0x8505;0x0585 是字节序反过来的写法,两个都收(lumi 的常量表并列了两者) final cidMatched = companyId == 0x8505 || companyId == 0x0585; if (cidMatched) { // 补回 [长度, 0xFF, CID低, CID高]:解析器按协议文档吃完整 AD 结构 - adv = SmartcarProtocol.parseManufacturerData( + adv = EaiCarProtocol.parseManufacturerData( [payload.length + 3, 0xFF, 0x05, 0x85, ...payload]); } - final nameOk = isAllowedSmartcarName(name); + final nameOk = isAllowedEaiCarName(name); if (!_diag.isClosed) { _diag.add({ @@ -282,7 +282,7 @@ class SmartcarLink { } final c = _scanCtrl; if (c != null && !c.isClosed) { - c.add(SmartcarScanResult(id: id, name: name, rssi: rssi, advertisement: adv)); + c.add(EaiCarScanResult(id: id, name: name, rssi: rssi, advertisement: adv)); } } @@ -296,7 +296,7 @@ class SmartcarLink { Future connect( String deviceId, { Duration timeout = const Duration(seconds: 45), - SmartcarAdvertisement? adv, + EaiCarAdvertisement? adv, }) async { advertisement = adv; _lastReason = null; @@ -305,7 +305,7 @@ class SmartcarLink { } on PlatformException catch (e) { throw StateError(e.message ?? e.code); } on MissingPluginException { - throw StateError('本平台没有 Smartcar 原生链路(只支持 Android / iOS)'); + throw StateError('本平台没有 EaiCar 原生链路(只支持 Android / iOS)'); } // 原生只在 ready 时 resolve;state 事件与 result 谁先到不确定,这里补齐 if (!_isConnected) { @@ -353,10 +353,10 @@ class SmartcarLink { // ───────────────────────── 收发 ───────────────────────── /// 等应答的命令:cmd → 等待者。同一条命令不允许并发两次(应答里没有序号)。 - final Map> _pending = {}; + final Map> _pending = {}; void _onValue(List value) { - final f = SmartcarProtocol.parseFrame(value); + final f = EaiCarProtocol.parseFrame(value); if (f == null) { Logger.w(_tag, '丢弃非法帧: ${_hex(value)}'); return; @@ -368,9 +368,9 @@ class SmartcarLink { if (!_frames.isClosed) _frames.add(f); } - /// 发一条命令。[expectResponse] 不传就按 [SmartcarProtocol.expectsResponse] 判断—— + /// 发一条命令。[expectResponse] 不传就按 [EaiCarProtocol.expectsResponse] 判断—— /// 那四条设备不回包的命令若强行等,只会白等到超时并把成功记成失败。 - Future send( + Future send( int cmd, { List payload = const [], bool? expectResponse, @@ -379,17 +379,17 @@ class SmartcarLink { if (!_isConnected) { throw StateError('链路未就绪($_lastState),不能发 0x${cmd.toRadixString(16)}'); } - final wantResp = expectResponse ?? SmartcarProtocol.expectsResponse(cmd); + final wantResp = expectResponse ?? EaiCarProtocol.expectsResponse(cmd); - Completer? waiter; + Completer? waiter; if (wantResp) { final exist = _pending[cmd]; if (exist != null) return exist.future; - waiter = Completer(); + waiter = Completer(); _pending[cmd] = waiter; } - final packet = SmartcarProtocol.buildPacket(cmd, payload); + final packet = EaiCarProtocol.buildPacket(cmd, payload); final ok = await _native.writeCustom(packet); if (!ok) { _pending.remove(cmd); @@ -415,8 +415,8 @@ class SmartcarLink { } /// 一条扫描结果 -class SmartcarScanResult { - const SmartcarScanResult({ +class EaiCarScanResult { + const EaiCarScanResult({ required this.id, required this.name, required this.rssi, @@ -429,5 +429,5 @@ class SmartcarScanResult { /// 厂商广播段。**可能为 null**:仅按名字命中、厂商段解不出来时就是空的, /// 此时拿不到 MAC,绑定确权会走 unknown(放行但不登记)。 - final SmartcarAdvertisement? advertisement; + final EaiCarAdvertisement? advertisement; } diff --git a/apps/client/lib/devices/smartcar/smartcar_ota_port.dart b/apps/client/lib/devices/eaicar/eaicar_ota_port.dart similarity index 92% rename from apps/client/lib/devices/smartcar/smartcar_ota_port.dart rename to apps/client/lib/devices/eaicar/eaicar_ota_port.dart index 19d64ccf..ccfe2d56 100644 --- a/apps/client/lib/devices/smartcar/smartcar_ota_port.dart +++ b/apps/client/lib/devices/eaicar/eaicar_ota_port.dart @@ -3,12 +3,12 @@ import 'dart:async'; import 'package:device_plugin_interface/device_plugin_interface.dart'; import '../../core/utils/logger.dart'; -import 'smartcar_device_plugin.dart'; -import 'smartcar_link.dart'; +import 'eaicar_device_plugin.dart'; +import 'eaicar_link.dart'; -/// Smartcar(杰理方案)的 OTA 端口。 +/// EaiCar(杰理方案)的 OTA 端口。 /// -/// 推送本身在原生 `SmartcarOta`(从 lumi `PbMangerImp` 搬:`cmdOTAData` + 0xfc 后 +/// 推送本身在原生 `EaiCarOta`(从 lumi `PbMangerImp` 搬:`cmdOTAData` + 0xfc 后 /// 扫描回连 + 认证 + 续传),这里只做三件事: /// 1. 把原生事件翻译成 [DeviceOtaProgress]; /// 2. 终态锁定——成功后迟到的 failed/cancelled 不能把它覆盖回去(恒玄踩过); @@ -20,13 +20,13 @@ import 'smartcar_link.dart'; /// /// ⚠️ 上层 `DeviceOtaController` 的看门狗 90s 没进度就判失败,而 stage2 等待可达 3 分钟, /// 原生在这段窗口每 15s 发一次 `stage2` 心跳事件喂它。 -class SmartcarOtaPort implements DeviceOtaPort { - SmartcarOtaPort(this._link, this._plugin); +class EaiCarOtaPort implements DeviceOtaPort { + EaiCarOtaPort(this._link, this._plugin); - static const String _tag = 'SmartcarOtaPort'; + static const String _tag = 'EaiCarOtaPort'; - final SmartcarLink _link; - final SmartcarDevicePlugin _plugin; + final EaiCarLink _link; + final EaiCarDevicePlugin _plugin; final StreamController _progress = StreamController.broadcast(); diff --git a/apps/client/lib/devices/smartcar/smartcar_protocol.dart b/apps/client/lib/devices/eaicar/eaicar_protocol.dart similarity index 90% rename from apps/client/lib/devices/smartcar/smartcar_protocol.dart rename to apps/client/lib/devices/eaicar/eaicar_protocol.dart index 617e058c..19023522 100644 --- a/apps/client/lib/devices/smartcar/smartcar_protocol.dart +++ b/apps/client/lib/devices/eaicar/eaicar_protocol.dart @@ -1,9 +1,9 @@ import 'dart:typed_data'; -/// Smartcar(车载香薰,方案代号 **C2**)的 BLE 私有协议。 +/// EaiCar(车载香薰,方案代号 **C2**)的 BLE 私有协议。 /// /// 纯函数 + 纯数据,不碰蓝牙栈,方便单测把每个字节钉死 -/// (`test/devices/smartcar_protocol_test.dart`)。 +/// (`test/devices/eaicar_protocol_test.dart`)。 /// /// 帧格式: /// ``` @@ -14,8 +14,8 @@ import 'dart:typed_data'; /// CRC 覆盖**除自己以外的全部前导字节**,算法见 [crc8Maxim]。 /// /// ⚠️ 多字节字段一律**小端**。 -class SmartcarProtocol { - SmartcarProtocol._(); +class EaiCarProtocol { + EaiCarProtocol._(); // ─────────────────────────── GATT ─────────────────────────── @@ -50,10 +50,10 @@ class SmartcarProtocol { // ───────────────────────── 命令号 ───────────────────────── // - // ⚠️⚠️ 下面标了 TODO(smartcar-cmd) 的几条,**协议文档与 lumi 代码是冲突的**。 + // ⚠️⚠️ 下面标了 TODO(eaicar-cmd) 的几条,**协议文档与 lumi 代码是冲突的**。 // 按 2026-09-10 的决定:一律以 lumi 的 BleConst.kt 为准(那是真机上跑着的), // 协议文档只作参考。样机到手后逐条复核,改这里并同步更新 - // docs/Smartcar车载香薰-接入设计与落地文档.md 的 2.4 节。 + // docs/EaiCar车载香薰-接入设计与落地文档.md 的 2.4 节。 // // ⚠️ 复核时**只扫查询类命令**。0x0C 恢复出厂 / 0x0D 启停开关 / 0x0E 遥控电机 // 会真的改设备状态,别拿来试探。 @@ -105,7 +105,7 @@ class SmartcarProtocol { static const int aiStateClose = 0x03; static const int aiStateListening = 0x04; - /// TODO(smartcar-cmd): 未经真机验证 —— 协议文档里没有这条, + /// TODO(eaicar-cmd): 未经真机验证 —— 协议文档里没有这条, /// BleConst.kt 说 CMD_CONTROL_MUSIC_BEAT,取后者。 static const int cmdControlMusicBeat = 0x08; @@ -128,7 +128,7 @@ class SmartcarProtocol { /// ⚠️ 2026-09-10 真机定论:GATT 连上 + 杰理认证通过 + 0x15 都能应答之后, /// 设备屏幕**仍显示 BLE 未连接**;发了这条(`AA 10 02 03 00`)设备回 `BB 10 01 64`, /// 屏幕才认。也就是说固件把"手机已连接"这个状态交给 APP 来宣告。 - /// 所以它是握手的一部分([SmartcarSession._handshake]),不是可选功能。 + /// 所以它是握手的一部分([EaiCarSession._handshake]),不是可选功能。 /// /// 载荷 2 字节:`[设备类型, 车企编号]`,设备类型 0 车机 / 1 小程序 / 2 安卓 / 3 iOS / 4 鸿蒙, /// 车企编号 0 默认 / 1 长城。lumi 没发过这条(它不是车载产品)。 @@ -153,7 +153,7 @@ class SmartcarProtocol { static const int cmdRandomExpressionInterval = 0x13; static const int cmdMotorSwitch = 0x14; - /// 同步参数汇总。应答 data = 14 字节,见 [SmartcarAllSettings] + /// 同步参数汇总。应答 data = 14 字节,见 [EaiCarAllSettings] static const int cmdSyncSummary = 0x15; /// 同步手机时间(协议文档没写,lumi 代码有,真机回 `BB 17 01 64`): @@ -217,7 +217,7 @@ class SmartcarProtocol { /// 返回 null 表示这段不是合法帧(长度不够 / 帧头不认识 / CRC 不符)。 /// **CRC 不符一律丢弃**,不要「先用着再说」——错帧解出来的电量、版本号 /// 看着都像合法值,查起来极费劲(恒玄那次就是被错位解析出的垃圾值带偏的)。 - static SmartcarFrame? parseFrame(List raw) { + static EaiCarFrame? parseFrame(List raw) { if (raw.length < 4) return null; final header = raw[0] & 0xFF; if (!isFrameHeader(header)) return null; @@ -227,7 +227,7 @@ class SmartcarProtocol { final expected = raw[len + 3] & 0xFF; final calc = crc8Maxim(raw.sublist(0, len + 3)); if (expected != calc) return null; - return SmartcarFrame( + return EaiCarFrame( header: header, cmd: cmd, payload: Uint8List.fromList(raw.sublist(3, 3 + len)), @@ -238,13 +238,13 @@ class SmartcarProtocol { /// 解厂商自定义广播段(传完整 AD 结构:`[长度, 0xFF, CID低, CID高, ...]`)。 /// - /// **真机实测布局(2026-09-10,Smartcar 样机,与 lumi iOS `parseMacFromManufacturer` + /// **真机实测布局(2026-09-10,EaiCar 样机,与 lumi iOS `parseMacFromManufacturer` /// 的 0x0585 分支一致)**:CID 之后只有 7 字节 /// ``` /// [0] 长度 [1] Type=0xFF /// [2-3] CID 0x05 0x85 → 小端 0x8505 /// [4-9] MAC 原样、不异或、不倒序:a9 1a b3 21 e4 dd = A9:1A:B3:21:E4:DD - /// [10] 1 字节状态 TODO(smartcar-adv):未连经典蓝牙时实测为 0x00, + /// [10] 1 字节状态 TODO(eaicar-adv):未连经典蓝牙时实测为 0x00, /// 按协议文档「0x0C = 经典蓝牙已连」处理,连上 A2DP 后要复核 /// ``` /// 实测原始厂商数据:`05 85 a9 1a b3 21 e4 dd 00`,解出的 MAC 与设备已知地址逐字节相同, @@ -253,7 +253,7 @@ class SmartcarProtocol { /// ⚠️ 协议文档里那套「26 字节:BID/PID/MAC XOR 0x5A/License/设备类型」**这台固件没有发**, /// 第一版照文档写的解析器要求 26 字节,于是永远解不出、MAC 恒为空。 /// BID/PID/License 现在从广播里拿不到,字段保留为 0 / 空串。 - static SmartcarAdvertisement? parseManufacturerData(List? raw) { + static EaiCarAdvertisement? parseManufacturerData(List? raw) { if (raw == null || raw.length < 10) return null; if ((raw[1] & 0xFF) != 0xFF) return null; int le16(int off) => (raw[off] & 0xFF) | ((raw[off + 1] & 0xFF) << 8); @@ -262,7 +262,7 @@ class SmartcarProtocol { .map((b) => (b & 0xFF).toRadixString(16).padLeft(2, '0').toUpperCase()) .join(':'); final state = raw.length > 10 ? raw[10] & 0xFF : 0; - return SmartcarAdvertisement( + return EaiCarAdvertisement( cid: le16(2), bid: 0, pid: 0, @@ -276,8 +276,8 @@ class SmartcarProtocol { /// 一帧解好的数据 -class SmartcarFrame { - const SmartcarFrame({ +class EaiCarFrame { + const EaiCarFrame({ required this.header, required this.cmd, required this.payload, @@ -287,8 +287,8 @@ class SmartcarFrame { final int cmd; final Uint8List payload; - bool get isResponse => header == SmartcarProtocol.headerResponse; - bool get isNotify => header == SmartcarProtocol.headerNotify; + bool get isResponse => header == EaiCarProtocol.headerResponse; + bool get isNotify => header == EaiCarProtocol.headerNotify; /// 设备回的应答里,`len == 0` 表示这条命令**执行失败** /// (协议就是这么设计的:成功才带数据)。 @@ -296,13 +296,13 @@ class SmartcarFrame { @override String toString() => - 'SmartcarFrame(0x${header.toRadixString(16)}, cmd=0x${cmd.toRadixString(16)}, ' + 'EaiCarFrame(0x${header.toRadixString(16)}, cmd=0x${cmd.toRadixString(16)}, ' 'len=${payload.length})'; } /// 广播里带的设备身份 -class SmartcarAdvertisement { - const SmartcarAdvertisement({ +class EaiCarAdvertisement { + const EaiCarAdvertisement({ required this.cid, required this.bid, required this.pid, @@ -334,13 +334,13 @@ class SmartcarAdvertisement { final int deviceType; @override - String toString() => 'SmartcarAdv(pid=0x${pid.toRadixString(16)}, ' + String toString() => 'EaiCarAdv(pid=0x${pid.toRadixString(16)}, ' 'mac=$macAddress, btConnected=$classicBtConnected)'; } /// `0x15 同步参数汇总` 的 14 字节应答 -class SmartcarAllSettings { - const SmartcarAllSettings({ +class EaiCarAllSettings { + const EaiCarAllSettings({ required this.otaAbnormal, required this.volume, required this.randomExprFreq, @@ -382,10 +382,10 @@ class SmartcarAllSettings { /// /// ⚠️ **`[1]` 音量实测是 0**。如果这个偏移没错,说明样机当前是静音的—— /// A2DP 接上后 AI 回复也可能听不见。真机验声音前先确认这一项。 - static SmartcarAllSettings? parse(List d) { + static EaiCarAllSettings? parse(List d) { if (d.length < 14) return null; int u(int i) => d[i] & 0xFF; - return SmartcarAllSettings( + return EaiCarAllSettings( otaAbnormal: u(0) != 0, volume: u(1), randomExprFreq: u(2), diff --git a/apps/client/lib/modules/devices/views/device_category_view.dart b/apps/client/lib/modules/devices/views/device_category_view.dart index 3f92b299..8d13620f 100644 --- a/apps/client/lib/modules/devices/views/device_category_view.dart +++ b/apps/client/lib/modules/devices/views/device_category_view.dart @@ -75,8 +75,8 @@ class DeviceCategoryView extends StatelessWidget { Get.toNamed(Routes.besScan); break; case DeviceCategories.carAroma: - // Smartcar 车载香薰,走 lib/devices/smartcar 那条 BLE 链路(C2 私有协议)。 - Get.toNamed(Routes.smartcarScan); + // EaiCar 车载香薰,走 lib/devices/eaicar 那条 BLE 链路(C2 私有协议)。 + Get.toNamed(Routes.eaicarScan); break; case DeviceCategories.phoneHolder: // EaiRec 录音支架,走 recorder_holder 那条 BLE 链路。 @@ -101,7 +101,7 @@ class DeviceCategoryView extends StatelessWidget { return GestureDetector( behavior: HitTestBehavior.opaque, // 每个品类走各自的连接页(背后是 DeviceHub 里对应的厂商插件)。 - // Smartcar 的链路还没接,见 [_scanNotReady]。 + // EaiCar 的链路还没接,见 [_scanNotReady]。 onTap: () => _openCategory(category), child: Container( decoration: BoxDecoration( diff --git a/apps/client/lib/modules/devices/views/smartcar_scan_view.dart b/apps/client/lib/modules/devices/views/eaicar_scan_view.dart similarity index 93% rename from apps/client/lib/modules/devices/views/smartcar_scan_view.dart rename to apps/client/lib/modules/devices/views/eaicar_scan_view.dart index 67d152cd..da4cb5da 100644 --- a/apps/client/lib/modules/devices/views/smartcar_scan_view.dart +++ b/apps/client/lib/modules/devices/views/eaicar_scan_view.dart @@ -11,22 +11,22 @@ import '../../../devices/device_vendors.dart'; import '../../../routes/app_routes.dart'; import '../../main_tab/controllers/main_tab_controller.dart'; -/// Smartcar(AI车载香薰)的扫描 / 连接页——走 [DeviceHub] 里的 Smartcar 插件。 +/// EaiCar(AI车载香薰)的扫描 / 连接页——走 [DeviceHub] 里的 EaiCar 插件。 /// /// 与支架那页结构一致,两点不同: -/// 1. **不需要后台自启动授权**:Smartcar 的连接是纯 GATT,没有向系统注册 +/// 1. **不需要后台自启动授权**:EaiCar 的连接是纯 GATT,没有向系统注册 /// 「设备靠近就唤醒本应用」那种后台扫连,不构成自启动行为。 /// 2. 卡片上多显示一行 MAC —— 它来自 BLE 广播,是服务端确权的依据, /// 连不上时看一眼就知道是不是扫到了别的设备。 -class SmartcarScanView extends StatefulWidget { - const SmartcarScanView({super.key}); +class EaiCarScanView extends StatefulWidget { + const EaiCarScanView({super.key}); @override - State createState() => _SmartcarScanViewState(); + State createState() => _EaiCarScanViewState(); } -class _SmartcarScanViewState extends State { - DevicePlugin get _plugin => DeviceHub.to.plugin(DeviceVendors.smartcar)!; +class _EaiCarScanViewState extends State { + DevicePlugin get _plugin => DeviceHub.to.plugin(DeviceVendors.eaicar)!; final RxList _scanned = [].obs; final RxBool _isScanning = false.obs; @@ -68,7 +68,7 @@ class _SmartcarScanViewState extends State { break; case DevicePluginEventType.customEvent: // 厂商插件转发的扫描诊断(业务层不能直接 import 厂商目录) - if (e.customKey == 'smartcar.scan_diagnostic') { + if (e.customKey == 'eaicar.scan_diagnostic') { final d = e.customPayload; if (d is Map) { final id = '${d['id']}'; @@ -124,7 +124,7 @@ class _SmartcarScanViewState extends State { setState(() => _connecting = false); if (failReason == null) { - Get.snackbar('smartcarConnected'.tr, d.name, + Get.snackbar('eaicarConnected'.tr, d.name, snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 2)); // BLE 通了不等于听得见:AI 回复和音乐走的是**经典蓝牙 A2DP**(见落地文档 7.0)。 @@ -141,7 +141,7 @@ class _SmartcarScanViewState extends State { // 这些信息(设备实际暴露了哪些服务、握手卡在哪条命令)是排查的全部依据, // snackbar 几秒就消失,用户根本来不及看清更别说转述。 _scanError.value = failReason; - Get.snackbar('smartcarConnectFailed'.tr, failReason, + Get.snackbar('eaicarConnectFailed'.tr, failReason, snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 5)); } @@ -154,11 +154,11 @@ class _SmartcarScanViewState extends State { Future _hintClassicAudioIfNeeded(String name) async { try { final r = await _plugin.activeSession - ?.invokeFeature('smartcar.a2dp_status'); + ?.invokeFeature('eaicar.a2dp_status'); if (r == null || r['connected'] == true) return; Get.snackbar( - 'smartcarA2dpTitle'.tr, - 'smartcarA2dpHint'.tr.replaceAll('@name', name), + 'eaicarA2dpTitle'.tr, + 'eaicarA2dpHint'.tr.replaceAll('@name', name), snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 6), ); @@ -201,7 +201,7 @@ class _SmartcarScanViewState extends State { children: [ const CircularProgressIndicator(), SizedBox(height: 16.h), - Text('smartcarConnecting'.tr), + Text('eaicarConnecting'.tr), ], ), ) @@ -227,15 +227,15 @@ class _SmartcarScanViewState extends State { SizedBox(height: 12.h), Obx(() => Text( _isScanning.value - ? 'smartcarSearching'.tr - : 'smartcarNotFound'.tr, + ? 'eaicarSearching'.tr + : 'eaicarNotFound'.tr, style: TextStyle( fontSize: 15.sp, color: AppPalette.textSecondary(isDark)), )), SizedBox(height: 6.h), Text( - 'smartcarScanHint'.tr, + 'eaicarScanHint'.tr, textAlign: TextAlign.center, style: TextStyle( fontSize: 12.sp, color: AppPalette.textHint(isDark)), @@ -248,7 +248,7 @@ class _SmartcarScanViewState extends State { /// 诊断面板:把附近扫到的 BLE 设备摊开。 /// - /// 判据(厂商 ID 0x8505 / 名字含 smartcar)对不对,看这里最快: + /// 判据(厂商 ID 0x8505 / 名字含 eaicar)对不对,看这里最快: /// - 列表里根本没有目标设备 → 设备没在播,或不在范围内 /// - 有、但 mfg 那栏的厂商 ID 不是 0x8505 → 判据要按实际值改 /// - 有、厂商 ID 对但长度不足 22 → 广播段布局和文档对不上 diff --git a/apps/client/lib/modules/smartcar/bindings/smartcar_chat_binding.dart b/apps/client/lib/modules/eaicar/bindings/eaicar_chat_binding.dart similarity index 66% rename from apps/client/lib/modules/smartcar/bindings/smartcar_chat_binding.dart rename to apps/client/lib/modules/eaicar/bindings/eaicar_chat_binding.dart index db732f24..67e0bdd2 100644 --- a/apps/client/lib/modules/smartcar/bindings/smartcar_chat_binding.dart +++ b/apps/client/lib/modules/eaicar/bindings/eaicar_chat_binding.dart @@ -3,46 +3,46 @@ import 'package:get/get.dart'; import '../../../data/services/bailian_multimodal_service.dart'; import '../../emai/controllers/emai_controller.dart'; -/// Smartcar 车载助手的依赖装配。 +/// EaiCar 车载助手的依赖装配。 /// /// 复用 EMAI 那套对话 UI 与控制器,只把「连哪个百炼应用」换掉: -/// 这里 put 一个带 Smartcar 专属 app_id 的 [BailianMultimodalService], +/// 这里 put 一个带 EaiCar 专属 app_id 的 [BailianMultimodalService], /// 再用它构造一个同 tag 的 [EmaiController]。 /// /// ⚠️ **两者都必须带 tag**:不带 tag 会覆盖掉全局那个 EMAI 用的实例, /// 表现为退出车载助手后 EMAI 也连到了车载的应用上。 -/// `EmaiView(controllerTag: kSmartcarTag)` 靠同一个 tag 取到这里的控制器。 +/// `EmaiView(controllerTag: kEaiCarTag)` 靠同一个 tag 取到这里的控制器。 /// /// ⚠️ **service 必须是独立实例**,不能 `Get.find` 全局那个: /// `BailianMultimodalService.start()` 第一件事就是 `stop()`,共用会让两个 agent /// 互相把对方的会话掐掉,下行音频还会串到对方的播放器上。 -class SmartcarChatBinding extends Bindings { +class EaiCarChatBinding extends Bindings { @override void dependencies() { - if (!Get.isRegistered(tag: kSmartcarTag)) { + if (!Get.isRegistered(tag: kEaiCarTag)) { Get.put( - BailianMultimodalService(appId: kSmartcarBailianAppId), - tag: kSmartcarTag, + BailianMultimodalService(appId: kEaiCarBailianAppId), + tag: kEaiCarTag, permanent: true, ); } Get.lazyPut( () => EmaiController( - service: Get.find(tag: kSmartcarTag), + service: Get.find(tag: kEaiCarTag), ), - tag: kSmartcarTag, + tag: kEaiCarTag, ); } } /// GetX 实例 tag:service 与 controller 共用这一个 -const String kSmartcarTag = 'smartcar'; +const String kEaiCarTag = 'eaicar'; -/// Smartcar 车载助手在百炼里的多模态应用 ID(workspace 与 EMAI 同一个, +/// EaiCar 车载助手在百炼里的多模态应用 ID(workspace 与 EMAI 同一个, /// 所以不覆盖 workspaceId,让它走全局配置)。 /// /// ⚠️ 写死在客户端是**有意选的**(落地文档 6.3 方案 A):换 app_id 要发版。 /// 等运营提出「要后台随时改」时,再改成从 `DBAgent.serviceData` 下发—— /// 那时要给客户端手写镜像补 `source` / `serviceData` 两个字段,且**必须 nullable**, /// 否则老后端不下发就整个 `fromJson` 抛 TypeError、应用配置全空白。 -const String kSmartcarBailianAppId = 'mm_dd1eb7cb06484aafb94a0b6b2dff'; +const String kEaiCarBailianAppId = 'mm_dd1eb7cb06484aafb94a0b6b2dff'; diff --git a/apps/client/lib/modules/eaicar/module.dart b/apps/client/lib/modules/eaicar/module.dart new file mode 100644 index 00000000..b016aa71 --- /dev/null +++ b/apps/client/lib/modules/eaicar/module.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../routes/app_routes.dart'; +import '../emai/views/emai_view.dart'; +import '../module_descriptor.dart'; +import 'bindings/eaicar_chat_binding.dart'; + +/// EaiCar 车载助手(EaiCar 车载香薰的随身对话 agent) +/// +/// 与 EMAI 共用同一套对话 UI 和控制器,差别只有一个:连的是百炼里**另一个应用** +/// (见 [kEaiCarBailianAppId])。所以这里直接复用 [EmaiView],靠 `controllerTag` +/// 取到 [EaiCarChatBinding] 装配的那份带专属 app_id 的实例。 +/// +/// ⚠️ **没有声明 `requiredCapabilities`**:这个 agent 手机自己就能聊,不必连着香薰 +/// (产品定位是"随时开聊")。设备连上时它会被自动铺到 AI 页首位,那是 +/// `AgentModules.defaultsByCategory[carAroma]` 的事,与本字段无关。 +/// +/// ⚠️ **对话记录与 EMAI 共用 `EmaiChatStore`**:设备按键唤醒的轮次由 +/// `DeviceAiSessionService` 写进那个 store,页面不开也能收;这里若另起一个 store, +/// 设备发起的对话就不会出现在本页上。要拆分历史得连 `DeviceAiSessionService` +/// 一起改,本期不做。 +final AgentModuleDescriptor eaicarModule = AgentModuleDescriptor( + id: 'smartcarChat', + title: 'agentEaiCar', + subtitle: 'agentEaiCarDesc', + // 卡片图标只支持 IconData(module_card 里是 Icon(module.icon) 套渐变块), + // 用不了产品图 EaiCar_icon.png。要用图得给 AgentModule 加字段并改 module_card, + // 那是耳机/支架共用的卡片组件,本期不动。 + icon: Icons.directions_car, + // 与「AI车载香薰」品类卡同色系(device_category.dart 的 carAroma) + gradient: const [Color(0xFFFFB74D), Color(0xFFE65100)], + entryRoute: Routes.eaicarChat, + // 设备按键唤醒时接的就是这个应用(不是 EMAI)——见 AgentModuleDescriptor.bailianAppId + bailianAppId: kEaiCarBailianAppId, + pages: [ + GetPage( + name: Routes.eaicarChat, + page: () => const EmaiView( + controllerTag: kEaiCarTag, + titleKey: 'agentEaiCar', + ), + binding: EaiCarChatBinding(), + transition: Transition.rightToLeft, + transitionDuration: const Duration(milliseconds: 250), + ), + ], + // 模块自带文案:不用再去改 41 个语言文件。 + // 缺某个语种时 GetX 回退到全局表,全局也没有就显示 key 本身, + // 所以至少要保证 zh_CN / en_US 有(module_registry_test 守着这两个)。 + translations: const { + 'zh_CN': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': '您的车载助手', + 'eaicarWelcome': '欢迎回来', + 'eaicarA2dpTitle': '需要连接蓝牙音频', + 'eaicarA2dpHint': 'AI 回复和音乐要经典蓝牙才能从设备里放出来。请到「设置 → 蓝牙」里配对 @name。', + }, + 'zh_TW': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': '您的車載助手', + 'eaicarWelcome': '歡迎回來', + 'eaicarA2dpTitle': '需要連接藍牙音訊', + 'eaicarA2dpHint': 'AI 回覆和音樂要經典藍牙才能從裝置裡播出來。請到「設定 → 藍牙」配對 @name。', + }, + 'zh_HK': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': '您的車載助手', + 'eaicarWelcome': '歡迎回來', + 'eaicarA2dpTitle': '需要連接藍牙音訊', + 'eaicarA2dpHint': 'AI 回覆和音樂要經典藍牙先可以喺裝置度播出嚟。請去「設定 → 藍牙」配對 @name。', + }, + 'en_US': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': 'Your in-car assistant', + 'eaicarWelcome': 'Welcome back', + 'eaicarA2dpTitle': 'Bluetooth audio not connected', + 'eaicarA2dpHint': 'Pair @name under Settings → Bluetooth so replies and music play from the device.', + }, + 'ja_JP': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': '車載アシスタント', + 'eaicarWelcome': 'おかえりなさい', + 'eaicarA2dpTitle': 'Bluetoothオーディオ未接続', + 'eaicarA2dpHint': '「設定 → Bluetooth」で @name をペアリングすると、応答と音楽が本体から再生されます。', + }, + 'ko_KR': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': '차량용 어시스턴트', + 'eaicarWelcome': '다시 오신 것을 환영합니다', + 'eaicarA2dpTitle': '블루투스 오디오 미연결', + 'eaicarA2dpHint': '설정 → 블루투스에서 @name 을(를) 페어링하면 응답과 음악이 기기에서 재생됩니다。', + }, + 'de_DE': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': 'Ihr Auto-Assistent', + 'eaicarWelcome': 'Willkommen zurück', + 'eaicarA2dpTitle': 'Bluetooth-Audio nicht verbunden', + 'eaicarA2dpHint': 'Koppeln Sie @name unter Einstellungen → Bluetooth, damit Antworten und Musik über das Gerät laufen.', + }, + 'fr_FR': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': 'Votre assistant automobile', + 'eaicarWelcome': 'Bon retour', + 'eaicarA2dpTitle': 'Audio Bluetooth non connecté', + 'eaicarA2dpHint': 'Associez @name dans Réglages → Bluetooth pour que les réponses et la musique sortent de l\'appareil.', + }, + 'es_ES': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': 'Tu asistente para el coche', + 'eaicarWelcome': 'Bienvenido de nuevo', + 'eaicarA2dpTitle': 'Audio Bluetooth no conectado', + 'eaicarA2dpHint': 'Empareja @name en Ajustes → Bluetooth para que las respuestas y la música suenen en el dispositivo.', + }, + 'pt_PT': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': 'O seu assistente para o carro', + 'eaicarWelcome': 'Bem-vindo de volta', + 'eaicarA2dpTitle': 'Áudio Bluetooth não ligado', + 'eaicarA2dpHint': 'Emparelhe @name em Definições → Bluetooth para que as respostas e a música saiam do dispositivo.', + }, + 'ru_RU': { + 'agentEaiCar': 'EaiCar', + 'agentEaiCarDesc': 'Ваш автомобильный помощник', + 'eaicarWelcome': 'С возвращением', + 'eaicarA2dpTitle': 'Bluetooth-аудио не подключено', + 'eaicarA2dpHint': 'Подключите @name в «Настройки → Bluetooth», чтобы ответы и музыка звучали из устройства.', + }, + }, +); diff --git a/apps/client/lib/modules/emai/controllers/emai_controller.dart b/apps/client/lib/modules/emai/controllers/emai_controller.dart index c5575264..01330cf7 100644 --- a/apps/client/lib/modules/emai/controllers/emai_controller.dart +++ b/apps/client/lib/modules/emai/controllers/emai_controller.dart @@ -42,7 +42,7 @@ class EmaiController extends GetxController implements EmaiSessionOwner { /// [service] 不传就用全局那个(EMAI 的百炼应用),行为与改造前一致。 /// - /// 传了才用传的——给「同一套对话 UI、不同百炼应用」的 agent 复用(如 Smartcar + /// 传了才用传的——给「同一套对话 UI、不同百炼应用」的 agent 复用(如 EaiCar /// 车载助手)。两个 agent 各自 new 一个 service 实例,不能共用,理由见 /// [BailianMultimodalService] 的类注释。 EmaiController({BailianMultimodalService? service}) diff --git a/apps/client/lib/modules/emai/views/emai_view.dart b/apps/client/lib/modules/emai/views/emai_view.dart index 33c01254..3884e6f8 100644 --- a/apps/client/lib/modules/emai/views/emai_view.dart +++ b/apps/client/lib/modules/emai/views/emai_view.dart @@ -14,7 +14,7 @@ import 'widgets/emai_level_bar.dart'; /// 所有 AI 能力都在阿里百炼多模态应用里,页面本身只管采音、播音和展示。 class EmaiView extends GetView { /// [controllerTag]:不传 = 取全局那个 [EmaiController](EMAI 自己),行为不变。 - /// 传了则取同名 tag 的实例——给复用这套 UI 的其它 agent 用(如 Smartcar)。 + /// 传了则取同名 tag 的实例——给复用这套 UI 的其它 agent 用(如 EaiCar)。 /// [titleKey]:标题栏的 i18n key,默认 EMAI 自己的。 const EmaiView({super.key, this.controllerTag, this.titleKey = 'agentEmai'}); diff --git a/apps/client/lib/modules/module_registry.dart b/apps/client/lib/modules/module_registry.dart index 9352832d..e4a5cf6a 100644 --- a/apps/client/lib/modules/module_registry.dart +++ b/apps/client/lib/modules/module_registry.dart @@ -8,7 +8,7 @@ import 'emai/module.dart'; import 'meeting/module.dart'; import 'module_descriptor.dart'; import 'picture_translation/module.dart'; -import 'smartcar/module.dart'; +import 'eaicar/module.dart'; import 'text_translation/module.dart'; import 'translation/module.dart'; @@ -31,7 +31,7 @@ class ModuleRegistry { ...callRecordingModules, textTranslationModule, pictureTranslationModule, - smartcarModule, + eaicarModule, // ---------- 规划中(占位,方便测试增删) ---------- AgentModuleDescriptor( id: 'englishChat', diff --git a/apps/client/lib/modules/smartcar/module.dart b/apps/client/lib/modules/smartcar/module.dart deleted file mode 100644 index b7c163bf..00000000 --- a/apps/client/lib/modules/smartcar/module.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../../routes/app_routes.dart'; -import '../emai/views/emai_view.dart'; -import '../module_descriptor.dart'; -import 'bindings/smartcar_chat_binding.dart'; - -/// Smartcar 车载助手(Smartcar 车载香薰的随身对话 agent) -/// -/// 与 EMAI 共用同一套对话 UI 和控制器,差别只有一个:连的是百炼里**另一个应用** -/// (见 [kSmartcarBailianAppId])。所以这里直接复用 [EmaiView],靠 `controllerTag` -/// 取到 [SmartcarChatBinding] 装配的那份带专属 app_id 的实例。 -/// -/// ⚠️ **没有声明 `requiredCapabilities`**:这个 agent 手机自己就能聊,不必连着香薰 -/// (产品定位是"随时开聊")。设备连上时它会被自动铺到 AI 页首位,那是 -/// `AgentModules.defaultsByCategory[carAroma]` 的事,与本字段无关。 -/// -/// ⚠️ **对话记录与 EMAI 共用 `EmaiChatStore`**:设备按键唤醒的轮次由 -/// `DeviceAiSessionService` 写进那个 store,页面不开也能收;这里若另起一个 store, -/// 设备发起的对话就不会出现在本页上。要拆分历史得连 `DeviceAiSessionService` -/// 一起改,本期不做。 -final AgentModuleDescriptor smartcarModule = AgentModuleDescriptor( - id: 'smartcarChat', - title: 'agentSmartcar', - subtitle: 'agentSmartcarDesc', - // 卡片图标只支持 IconData(module_card 里是 Icon(module.icon) 套渐变块), - // 用不了产品图 Smartcar_icon.png。要用图得给 AgentModule 加字段并改 module_card, - // 那是耳机/支架共用的卡片组件,本期不动。 - icon: Icons.directions_car, - // 与「AI车载香薰」品类卡同色系(device_category.dart 的 carAroma) - gradient: const [Color(0xFFFFB74D), Color(0xFFE65100)], - entryRoute: Routes.smartcarChat, - // 设备按键唤醒时接的就是这个应用(不是 EMAI)——见 AgentModuleDescriptor.bailianAppId - bailianAppId: kSmartcarBailianAppId, - pages: [ - GetPage( - name: Routes.smartcarChat, - page: () => const EmaiView( - controllerTag: kSmartcarTag, - titleKey: 'agentSmartcar', - ), - binding: SmartcarChatBinding(), - transition: Transition.rightToLeft, - transitionDuration: const Duration(milliseconds: 250), - ), - ], - // 模块自带文案:不用再去改 41 个语言文件。 - // 缺某个语种时 GetX 回退到全局表,全局也没有就显示 key 本身, - // 所以至少要保证 zh_CN / en_US 有(module_registry_test 守着这两个)。 - translations: const { - 'zh_CN': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': '您的车载助手', - 'smartcarWelcome': '欢迎回来', - 'smartcarA2dpTitle': '需要连接蓝牙音频', - 'smartcarA2dpHint': 'AI 回复和音乐要经典蓝牙才能从设备里放出来。请到「设置 → 蓝牙」里配对 @name。', - }, - 'zh_TW': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': '您的車載助手', - 'smartcarWelcome': '歡迎回來', - 'smartcarA2dpTitle': '需要連接藍牙音訊', - 'smartcarA2dpHint': 'AI 回覆和音樂要經典藍牙才能從裝置裡播出來。請到「設定 → 藍牙」配對 @name。', - }, - 'zh_HK': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': '您的車載助手', - 'smartcarWelcome': '歡迎回來', - 'smartcarA2dpTitle': '需要連接藍牙音訊', - 'smartcarA2dpHint': 'AI 回覆和音樂要經典藍牙先可以喺裝置度播出嚟。請去「設定 → 藍牙」配對 @name。', - }, - 'en_US': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': 'Your in-car assistant', - 'smartcarWelcome': 'Welcome back', - 'smartcarA2dpTitle': 'Bluetooth audio not connected', - 'smartcarA2dpHint': 'Pair @name under Settings → Bluetooth so replies and music play from the device.', - }, - 'ja_JP': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': '車載アシスタント', - 'smartcarWelcome': 'おかえりなさい', - 'smartcarA2dpTitle': 'Bluetoothオーディオ未接続', - 'smartcarA2dpHint': '「設定 → Bluetooth」で @name をペアリングすると、応答と音楽が本体から再生されます。', - }, - 'ko_KR': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': '차량용 어시스턴트', - 'smartcarWelcome': '다시 오신 것을 환영합니다', - 'smartcarA2dpTitle': '블루투스 오디오 미연결', - 'smartcarA2dpHint': '설정 → 블루투스에서 @name 을(를) 페어링하면 응답과 음악이 기기에서 재생됩니다。', - }, - 'de_DE': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': 'Ihr Auto-Assistent', - 'smartcarWelcome': 'Willkommen zurück', - 'smartcarA2dpTitle': 'Bluetooth-Audio nicht verbunden', - 'smartcarA2dpHint': 'Koppeln Sie @name unter Einstellungen → Bluetooth, damit Antworten und Musik über das Gerät laufen.', - }, - 'fr_FR': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': 'Votre assistant automobile', - 'smartcarWelcome': 'Bon retour', - 'smartcarA2dpTitle': 'Audio Bluetooth non connecté', - 'smartcarA2dpHint': 'Associez @name dans Réglages → Bluetooth pour que les réponses et la musique sortent de l\'appareil.', - }, - 'es_ES': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': 'Tu asistente para el coche', - 'smartcarWelcome': 'Bienvenido de nuevo', - 'smartcarA2dpTitle': 'Audio Bluetooth no conectado', - 'smartcarA2dpHint': 'Empareja @name en Ajustes → Bluetooth para que las respuestas y la música suenen en el dispositivo.', - }, - 'pt_PT': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': 'O seu assistente para o carro', - 'smartcarWelcome': 'Bem-vindo de volta', - 'smartcarA2dpTitle': 'Áudio Bluetooth não ligado', - 'smartcarA2dpHint': 'Emparelhe @name em Definições → Bluetooth para que as respostas e a música saiam do dispositivo.', - }, - 'ru_RU': { - 'agentSmartcar': 'EaiCar', - 'agentSmartcarDesc': 'Ваш автомобильный помощник', - 'smartcarWelcome': 'С возвращением', - 'smartcarA2dpTitle': 'Bluetooth-аудио не подключено', - 'smartcarA2dpHint': 'Подключите @name в «Настройки → Bluetooth», чтобы ответы и музыка звучали из устройства.', - }, - }, -); diff --git a/apps/client/lib/routes/app_pages.dart b/apps/client/lib/routes/app_pages.dart index 7fd1a67b..33c1506b 100644 --- a/apps/client/lib/routes/app_pages.dart +++ b/apps/client/lib/routes/app_pages.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import '../modules/devices/views/bes_scan_view.dart'; import '../modules/devices/views/device_category_view.dart'; import '../modules/devices/views/holder_scan_view.dart'; -import '../modules/devices/views/smartcar_scan_view.dart'; +import '../modules/devices/views/eaicar_scan_view.dart'; import '../modules/feedback/bindings/feedback_bindings.dart'; import '../modules/feedback/views/feedback_view.dart'; import '../modules/goods/bindings/goodsmeet_binding.dart'; @@ -93,7 +93,7 @@ abstract class AppPages { // 添加设备第一步:选品类(纯展示,无需 binding) _page(Routes.deviceCategory, () => const DeviceCategoryView()), _page(Routes.holderScan, () => const HolderScanView()), - _page(Routes.smartcarScan, () => const SmartcarScanView()), + _page(Routes.eaicarScan, () => const EaiCarScanView()), _page(Routes.besScan, () => const BesScanView()), _page(Routes.plaza, () => const PlazaTab()), _page(Routes.myDevices, () => const MyDevicesView(), diff --git a/apps/client/lib/routes/app_routes.dart b/apps/client/lib/routes/app_routes.dart index a85d4ed8..25b73ed0 100644 --- a/apps/client/lib/routes/app_routes.dart +++ b/apps/client/lib/routes/app_routes.dart @@ -17,7 +17,7 @@ abstract class Routes { static const otaUpgrade = '/ota_upgrade'; // 固件升级 static const deviceCategory = '/device_category'; // 添加设备第一步:选择产品品类 static const holderScan = '/holder_scan'; // EaiRec 录音支架扫描/连接 - static const smartcarScan = '/smartcar_scan'; // Smartcar 车载香薰扫描/连接 + static const eaicarScan = '/eaicar_scan'; // EaiCar 车载香薰扫描/连接 static const besScan = '/bes_scan'; // 恒玄耳机(经典蓝牙)连接 static const myDevices = '/my_devices'; // 用户已绑定设备列表 static const besTest = '/bes_test'; // 恒玄协议探测页(开发者) @@ -48,7 +48,7 @@ abstract class Routes { // AI 助手 static const emai = '/emai'; // EMAI 助手(阿里百炼多模态对话) - static const smartcarChat = '/smartcar_chat'; // Smartcar 车载助手(同一套对话 UI,另一个百炼应用) + static const eaicarChat = '/eaicar_chat'; // EaiCar 车载助手(同一套对话 UI,另一个百炼应用) static const mobileElf = '/mobile_elf'; // 移动精灵 // 广场 diff --git a/apps/client/test/agent_module_defaults_test.dart b/apps/client/test/agent_module_defaults_test.dart index e623403d..93a4859a 100644 --- a/apps/client/test/agent_module_defaults_test.dart +++ b/apps/client/test/agent_module_defaults_test.dart @@ -68,11 +68,11 @@ void main() { }); // 设备编排的内容本身也守一下:香薰就该是这三个,别被顺手改掉。 - test('车载香薰的默认编排是 Smartcar / 现场录音 / 语音纪要', () { + test('车载香薰的默认编排是 EaiCar / 现场录音 / 语音纪要', () { expect( AgentModules.defaultsForCategory('carAroma'), [ - AgentModules.smartcarChat, + AgentModules.eaicarChat, AgentModules.fieldRecording, AgentModules.meeting, ], diff --git a/apps/client/test/assistant_directive_test.dart b/apps/client/test/assistant_directive_test.dart index ea08b56f..81ff0f8c 100644 --- a/apps/client/test/assistant_directive_test.dart +++ b/apps/client/test/assistant_directive_test.dart @@ -348,7 +348,7 @@ void main() { }); group('commands 形状(2026-09-11 车载助手真机帧)', () { - // Smartcar 车载助手的指令不在 tool_calls 里,而是 extra_info.commands。 + // EaiCar 车载助手的指令不在 tool_calls 里,而是 extra_info.commands。 // 只解析 tool_calls 的话这里恒为 0 条 —— 表现就是"AI 说已经关闭了、实际没反应"。 Map frame() => { 'dialog_id': 'd1', diff --git a/apps/client/test/bailian_native_parity_test.dart b/apps/client/test/bailian_native_parity_test.dart index be2202f8..7371dead 100644 --- a/apps/client/test/bailian_native_parity_test.dart +++ b/apps/client/test/bailian_native_parity_test.dart @@ -29,7 +29,7 @@ void main() { }); test('显式覆盖优先于全局配置', () { - // 两个 agent(EMAI / Smartcar 车载助手)用的是百炼里不同的应用, + // 两个 agent(EMAI / EaiCar 车载助手)用的是百炼里不同的应用, // 覆盖失效会让车载香薰唤醒到 EMAI 上去 expect(BailianMultimodalService.resolveAppId('app-x'), 'app-x'); expect(BailianMultimodalService.resolveWorkspaceId('ws-x'), 'ws-x'); diff --git a/apps/client/test/devices/device_hub_boundary_test.dart b/apps/client/test/devices/device_hub_boundary_test.dart index 6fc2ad8d..5d4d63cc 100644 --- a/apps/client/test/devices/device_hub_boundary_test.dart +++ b/apps/client/test/devices/device_hub_boundary_test.dart @@ -8,7 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; /// 与 `device_plugin_interface`,**不能** import `lib/devices//…`, /// 也不能出现厂商类名。新接一种芯片只动 `lib/devices//` 与 bootstrap。 void main() { - final vendorImport = RegExp(r"devices/(bes|holder|smartcar)/"); + final vendorImport = RegExp(r"devices/(bes|holder|eaicar)/"); final vendorTypes = RegExp( r'\b(BesBluetoothService|BesDevicePlugin|BesDeviceSession|BesDeviceAuth|' r'HolderDeviceService|HolderDevicePlugin|HolderDeviceSession|' diff --git a/apps/client/test/devices/eaicar_name_test.dart b/apps/client/test/devices/eaicar_name_test.dart new file mode 100644 index 00000000..12a8f27f --- /dev/null +++ b/apps/client/test/devices/eaicar_name_test.dart @@ -0,0 +1,30 @@ +import 'package:eaimar/devices/eaicar/eaicar_link.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('isAllowedEaiCarName —— 车载香薰广播名准入', () { + test('规定的名字放行', () { + expect(isAllowedEaiCarName('EaiCar'), isTrue); + expect(isAllowedEaiCarName(' eaicar '), isTrue); + expect(isAllowedEaiCarName('EAICAR-3F2A'), isTrue); + }); + + // ⚠️ 旧广播名不再接纳。厂商 ID(0x8505) 也不再单独放行——它只说明 + // "这是云衍方案的设备",说明不了"这是我们的正式产品"。 + test('旧名 Smartcar 一律拒绝', () { + expect(isAllowedEaiCarName('Smartcar'), isFalse); + expect(isAllowedEaiCarName('smartcar'), isFalse); + expect(isAllowedEaiCarName('SmartcarX'), isFalse); + }); + + test('空名字拒绝', () { + expect(isAllowedEaiCarName(''), isFalse); + expect(isAllowedEaiCarName(' '), isFalse); + }); + + test('别家设备拒绝', () { + expect(isAllowedEaiCarName('EaiRec'), isFalse); + expect(isAllowedEaiCarName('Echo-one'), isFalse); + }); + }); +} diff --git a/apps/client/test/devices/smartcar_protocol_test.dart b/apps/client/test/devices/eaicar_protocol_test.dart similarity index 65% rename from apps/client/test/devices/smartcar_protocol_test.dart rename to apps/client/test/devices/eaicar_protocol_test.dart index 8a77eb53..ecb40e01 100644 --- a/apps/client/test/devices/smartcar_protocol_test.dart +++ b/apps/client/test/devices/eaicar_protocol_test.dart @@ -1,23 +1,23 @@ -import 'package:eaimar/devices/smartcar/smartcar_protocol.dart'; +import 'package:eaimar/devices/eaicar/eaicar_protocol.dart'; import 'package:flutter_test/flutter_test.dart'; -/// Smartcar C2 协议的字节级金测试。 +/// EaiCar C2 协议的字节级金测试。 /// /// 协议是**冻结的**:同一个命令永远发同样的字节。这些用例就是那份契约—— /// 改动这里等于改协议,必须先在真机上验过。 void main() { group('CRC-8/MAXIM', () { test('空数据为 0', () { - expect(SmartcarProtocol.crc8Maxim(const []), 0x00); + expect(EaiCarProtocol.crc8Maxim(const []), 0x00); }); // 与 lumi BleProtocol.calcCrc 同算法(poly 0x31 反射 0x8C / init 0 / xorOut 0)。 // 这几个值是标准 CRC-8/MAXIM 的公认结果,可离线核对。 test('标准向量', () { - expect(SmartcarProtocol.crc8Maxim([0x00]), 0x00); + expect(EaiCarProtocol.crc8Maxim([0x00]), 0x00); // "123456789" 的 CRC-8/MAXIM 标准 check 值是 0xA1 expect( - SmartcarProtocol.crc8Maxim( + EaiCarProtocol.crc8Maxim( '123456789'.codeUnits), 0xA1, ); @@ -30,33 +30,33 @@ void main() { for (var i = 0; i < 8; i++) { crc = (crc & 1) != 0 ? (crc >> 1) ^ 0x8C : crc >> 1; } - expect(SmartcarProtocol.crc8Maxim([b]), crc & 0xFF); + expect(EaiCarProtocol.crc8Maxim([b]), crc & 0xFF); } }); }); group('组帧', () { test('无参命令:获取版本号', () { - final p = SmartcarProtocol.buildPacket(SmartcarProtocol.cmdGetVersion); + final p = EaiCarProtocol.buildPacket(EaiCarProtocol.cmdGetVersion); expect(p[0], 0xAA); expect(p[1], 0x01); expect(p[2], 0x00); // len expect(p.length, 4); - expect(p[3], SmartcarProtocol.crc8Maxim([0xAA, 0x01, 0x00])); + expect(p[3], EaiCarProtocol.crc8Maxim([0xAA, 0x01, 0x00])); }); test('带参命令:设音量', () { - final p = SmartcarProtocol.buildPacket( - SmartcarProtocol.cmdSetVolume, const [0x05]); + final p = EaiCarProtocol.buildPacket( + EaiCarProtocol.cmdSetVolume, const [0x05]); expect(p.sublist(0, 4), [0xAA, 0x0B, 0x01, 0x05]); expect(p.length, 5); - expect(p[4], SmartcarProtocol.crc8Maxim([0xAA, 0x0B, 0x01, 0x05])); + expect(p[4], EaiCarProtocol.crc8Maxim([0xAA, 0x0B, 0x01, 0x05])); }); test('组出来的帧能被自己解回去', () { - final p = SmartcarProtocol.buildPacket(0x42, const [1, 2, 3]); + final p = EaiCarProtocol.buildPacket(0x42, const [1, 2, 3]); // buildPacket 发的是 0xAA,parseFrame 也认这个头 - final f = SmartcarProtocol.parseFrame(p); + final f = EaiCarProtocol.parseFrame(p); expect(f, isNotNull); expect(f!.cmd, 0x42); expect(f.payload, [1, 2, 3]); @@ -67,28 +67,28 @@ void main() { /// 造一帧设备应答 List resp(int cmd, List payload) { final head = [0xBB, cmd, payload.length, ...payload]; - return [...head, SmartcarProtocol.crc8Maxim(head)]; + return [...head, EaiCarProtocol.crc8Maxim(head)]; } test('正常应答', () { - final f = SmartcarProtocol.parseFrame(resp(0x04, const [0x64])); + final f = EaiCarProtocol.parseFrame(resp(0x04, const [0x64])); expect(f, isNotNull); expect(f!.isResponse, isTrue); - expect(f.cmd, SmartcarProtocol.cmdGetBattery); + expect(f.cmd, EaiCarProtocol.cmdGetBattery); expect(f.payload, [100]); expect(f.success, isTrue); }); test('len=0 表示命令执行失败', () { - final f = SmartcarProtocol.parseFrame(resp(0x01, const [])); + final f = EaiCarProtocol.parseFrame(resp(0x01, const [])); expect(f, isNotNull); expect(f!.success, isFalse); }); test('主动上报帧头 0xCC', () { - final head = [0xCC, SmartcarProtocol.notifyAiWake, 0x00]; - final f = SmartcarProtocol - .parseFrame([...head, SmartcarProtocol.crc8Maxim(head)]); + final head = [0xCC, EaiCarProtocol.notifyAiWake, 0x00]; + final f = EaiCarProtocol + .parseFrame([...head, EaiCarProtocol.crc8Maxim(head)]); expect(f!.isNotify, isTrue); expect(f.cmd, 0x11); }); @@ -96,15 +96,15 @@ void main() { test('CRC 不符必须丢弃', () { final bad = resp(0x04, const [0x64]); bad[bad.length - 1] = (bad.last + 1) & 0xFF; - expect(SmartcarProtocol.parseFrame(bad), isNull); + expect(EaiCarProtocol.parseFrame(bad), isNull); }); test('帧头不认识 / 长度不够 一律 null', () { - expect(SmartcarProtocol.parseFrame(const [0x12, 0x01, 0x00, 0x00]), isNull); - expect(SmartcarProtocol.parseFrame(const [0xBB, 0x01]), isNull); + expect(EaiCarProtocol.parseFrame(const [0x12, 0x01, 0x00, 0x00]), isNull); + expect(EaiCarProtocol.parseFrame(const [0xBB, 0x01]), isNull); // 声明 len=5 但实际只有 1 字节 payload expect( - SmartcarProtocol.parseFrame(const [0xBB, 0x01, 0x05, 0x00, 0x00]), + EaiCarProtocol.parseFrame(const [0xBB, 0x01, 0x05, 0x00, 0x00]), isNull); }); }); @@ -122,33 +122,33 @@ void main() { ]; test('真机广播:MAC 原样取 CID 之后 6 字节', () { - final a = SmartcarProtocol.parseManufacturerData(adv())!; + final a = EaiCarProtocol.parseManufacturerData(adv())!; expect(a.cid, 0x8505); expect(a.macAddress, 'A9:1A:B3:21:E4:DD'); expect(a.classicBtConnected, isFalse); }); test('状态字节 0x0C 视为经典蓝牙已连', () { - final a = SmartcarProtocol.parseManufacturerData(adv(btState: 0x0C))!; + final a = EaiCarProtocol.parseManufacturerData(adv(btState: 0x0C))!; expect(a.classicBtConnected, isTrue); }); test('没有状态字节也能解出 MAC', () { - final a = SmartcarProtocol.parseManufacturerData(adv().sublist(0, 10))!; + final a = EaiCarProtocol.parseManufacturerData(adv().sublist(0, 10))!; expect(a.macAddress, 'A9:1A:B3:21:E4:DD'); }); test('长度不足返回 null,不能抛', () { - expect(SmartcarProtocol.parseManufacturerData(null), isNull); - expect(SmartcarProtocol.parseManufacturerData(List.filled(9, 0)), isNull); + expect(EaiCarProtocol.parseManufacturerData(null), isNull); + expect(EaiCarProtocol.parseManufacturerData(List.filled(9, 0)), isNull); // Type 不是 0xFF 的段不是厂商数据 - expect(SmartcarProtocol.parseManufacturerData([10, 0x09, ...List.filled(8, 0)]), isNull); + expect(EaiCarProtocol.parseManufacturerData([10, 0x09, ...List.filled(8, 0)]), isNull); }); }); group('0x15 参数汇总', () { test('14 字节逐项解对', () { - final s = SmartcarAllSettings.parse(const [ + final s = EaiCarAllSettings.parse(const [ 0x00, // OTA 正常 0x06, // 音量 6 0x1E, // 随机表情频次 30 @@ -176,7 +176,7 @@ void main() { }); test('短包返回 null', () { - expect(SmartcarAllSettings.parse(List.filled(13, 0)), isNull); + expect(EaiCarAllSettings.parse(List.filled(13, 0)), isNull); }); }); @@ -185,28 +185,28 @@ void main() { // 判据:lumi `BleService.swift` 的 checkMusicPlaybackState() 实际发的是 // 0x05 [0x01]/[0x00](手机在放/停止放音乐),与协议文档一致; // "遥控电机"另有其号 0x0E,所以 0x07 不是控制动作。 - expect(SmartcarProtocol.cmdMusicPlayState, 0x05); - expect(SmartcarProtocol.cmdAiState, 0x07); - expect(SmartcarProtocol.cmdRemoteMotor, 0x0E); + expect(EaiCarProtocol.cmdMusicPlayState, 0x05); + expect(EaiCarProtocol.cmdAiState, 0x07); + expect(EaiCarProtocol.cmdRemoteMotor, 0x0E); }); test('0x11 双向共号:0xAA 是陀螺仪开关,0xCC 是 AI 唤醒', () { - expect(SmartcarProtocol.cmdGyroscopeSwitch, 0x11); - expect(SmartcarProtocol.notifyAiWake, 0x11); - final wake = SmartcarProtocol.parseFrame( - SmartcarProtocol.buildPacket(SmartcarProtocol.notifyAiWake, const [0xAA]) + expect(EaiCarProtocol.cmdGyroscopeSwitch, 0x11); + expect(EaiCarProtocol.notifyAiWake, 0x11); + final wake = EaiCarProtocol.parseFrame( + EaiCarProtocol.buildPacket(EaiCarProtocol.notifyAiWake, const [0xAA]) .toList() - ..[0] = SmartcarProtocol.headerNotify); + ..[0] = EaiCarProtocol.headerNotify); // 帧头被改过,CRC 对不上,解析必须失败——这条顺带守住"CRC 覆盖帧头" expect(wake, isNull); }); test('AI 状态取值与协议文档一致', () { - expect(SmartcarProtocol.aiStateSpeakEnd, 0x00); - expect(SmartcarProtocol.aiStateThinking, 0x01); - expect(SmartcarProtocol.aiStateSpeakStart, 0x02); - expect(SmartcarProtocol.aiStateClose, 0x03); - expect(SmartcarProtocol.aiStateListening, 0x04); + expect(EaiCarProtocol.aiStateSpeakEnd, 0x00); + expect(EaiCarProtocol.aiStateThinking, 0x01); + expect(EaiCarProtocol.aiStateSpeakStart, 0x02); + expect(EaiCarProtocol.aiStateClose, 0x03); + expect(EaiCarProtocol.aiStateListening, 0x04); }); }); @@ -219,13 +219,13 @@ void main() { ]; test('电量与版本与单发命令对得上', () { - final s = SmartcarAllSettings.parse(real)!; + final s = EaiCarAllSettings.parse(real)!; expect(s.battery, 60, reason: '与 0x04 的应答 3c 一致'); expect(s.firmwareVersion, '2.12.12', reason: '与 0x01 的应答 02 0c 0c 一致'); }); test('其余字段按已钉死的偏移解出', () { - final s = SmartcarAllSettings.parse(real)!; + final s = EaiCarAllSettings.parse(real)!; expect(s.brightness, 10); expect(s.randomExprFreq, 20); expect(s.volume, 0, reason: '实测就是 0(样机静音),改这条前先在真机上复核'); @@ -236,22 +236,22 @@ void main() { group('应答期望', () { test('这四条命令设备不回包,别等超时', () { for (final c in [ - SmartcarProtocol.cmdMusicPlayState, - SmartcarProtocol.cmdControlEmotion, - SmartcarProtocol.cmdAiState, - SmartcarProtocol.cmdControlMusicBeat, + EaiCarProtocol.cmdMusicPlayState, + EaiCarProtocol.cmdControlEmotion, + EaiCarProtocol.cmdAiState, + EaiCarProtocol.cmdControlMusicBeat, ]) { - expect(SmartcarProtocol.expectsResponse(c), isFalse, + expect(EaiCarProtocol.expectsResponse(c), isFalse, reason: '0x${c.toRadixString(16)} 不该等应答'); } }); test('查询类命令要等应答', () { - expect(SmartcarProtocol.expectsResponse(SmartcarProtocol.cmdGetVersion), + expect(EaiCarProtocol.expectsResponse(EaiCarProtocol.cmdGetVersion), isTrue); - expect(SmartcarProtocol.expectsResponse(SmartcarProtocol.cmdGetBattery), + expect(EaiCarProtocol.expectsResponse(EaiCarProtocol.cmdGetBattery), isTrue); - expect(SmartcarProtocol.expectsResponse(SmartcarProtocol.cmdSyncSummary), + expect(EaiCarProtocol.expectsResponse(EaiCarProtocol.cmdSyncSummary), isTrue); }); }); diff --git a/apps/client/test/devices/smartcar_name_test.dart b/apps/client/test/devices/smartcar_name_test.dart deleted file mode 100644 index 2d78ec65..00000000 --- a/apps/client/test/devices/smartcar_name_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:eaimar/devices/smartcar/smartcar_link.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('isAllowedSmartcarName —— 车载香薰广播名准入', () { - test('规定的名字放行', () { - expect(isAllowedSmartcarName('EaiCar'), isTrue); - expect(isAllowedSmartcarName(' eaicar '), isTrue); - expect(isAllowedSmartcarName('EAICAR-3F2A'), isTrue); - }); - - // ⚠️ 旧广播名不再接纳。厂商 ID(0x8505) 也不再单独放行——它只说明 - // "这是云衍方案的设备",说明不了"这是我们的正式产品"。 - test('旧名 Smartcar 一律拒绝', () { - expect(isAllowedSmartcarName('Smartcar'), isFalse); - expect(isAllowedSmartcarName('smartcar'), isFalse); - expect(isAllowedSmartcarName('SmartcarX'), isFalse); - }); - - test('空名字拒绝', () { - expect(isAllowedSmartcarName(''), isFalse); - expect(isAllowedSmartcarName(' '), isFalse); - }); - - test('别家设备拒绝', () { - expect(isAllowedSmartcarName('EaiRec'), isFalse); - expect(isAllowedSmartcarName('Echo-one'), isFalse); - }); - }); -} diff --git a/apps/client/test/module_registry_test.dart b/apps/client/test/module_registry_test.dart index a2190db0..490a40ab 100644 --- a/apps/client/test/module_registry_test.dart +++ b/apps/client/test/module_registry_test.dart @@ -70,11 +70,11 @@ void main() { }); group('设备品类默认编排', () { - test('车载香薰连上后铺 Smartcar / 现场录音 / 语音纪要(按这个顺序)', () { + test('车载香薰连上后铺 EaiCar / 现场录音 / 语音纪要(按这个顺序)', () { expect( AgentModules.defaultsForCategory(DeviceCategories.carAroma), [ - AgentModules.smartcarChat, + AgentModules.eaicarChat, AgentModules.fieldRecording, AgentModules.meeting, ],