「激活」不是一道 UI 弹窗,而是三件事叠加:服务端登记 + 三重前置校验(移动网络 / 已登录 / 固件版本)+ 设备侧两条指令(activateWIFI、sendUserId),并以 PTCameraModel.isActivated 作为全 App(含 Flutter)的门禁变量。
因此「去掉激活」的正确做法不是删 UI,而是把这个门禁变量恒置为真、并把两个服务端调用短路掉;设备侧两条指令必须保留(用内置账号下发),否则相机的绑定态(is_bind/is_active)会与现状不一致。
改动面:新增 1 个开关文件(约 30 行)+ 4 处短路(合计不到 60 行 diff);不动激活 UI 文件、不动埋点、不动 Flutter/C++、不动固件。
| 问题 | 结论 |
|---|---|
| 激活必过吗? | 对「拍摄/预览」是硬门禁;Ring 设备被显式豁免(PTCameraSearchView.swift:953「Ring 设备不走激活流程」) |
| 去掉后相机还能拍吗? | 能。固件侧 is_active/is_activated 只上报、不作任何功能门禁(详见第 6 节) |
| 需要登录吗? | 改动后不再需要——激活是唯一「未登录就无法继续」的登录硬门槛(+Activation.swift:23 + PTCameraSearchView.swift:979) |
| 能保留旧行为吗? | 可以:建议用编译宏 PT_ACTIVATION_BYPASS,正式线默认关闭,回滚 = 关开关 |
排查/改造激活时最容易踩的坑:代码里存在三份互相独立的「是否已激活」,日志里必须区分打印,判断也必须按「或/与」关系组合。
| 状态 | 来源 | 代码位置 | 特点 |
|---|---|---|---|
| 服务端激活态 | GET /photon-server/api/v1/device/isActivated/{sn} | PTDeviceNetWorkService.swift:250(URL)、:729(查询) | 需网络 + 缓存(UserDefaults,key 前缀 + SN);失败时回落设备端状态 |
| 设备端激活态 | 相机 BLE 上报 is_activated → PTCameraModel.isActivated | PTCameraModel.swift:400(属性)、:1454(写入) | @Published,是全 App 门禁的实际消费变量 |
| 蓝牙配置激活态 | WiFi 配置阶段 BLE 下发的 is_activated | PTBluetoothTypes.swift:411、读点 PTCameraConnectionFlowManager.swift:101 | 用于判断「服务端已激活但设备端没激活」的错配场景 |
enum PTCameraConnectMode: Equatable, Hashable {
case empty
case searching
case connect(PTCameraConnectStep)
case connectCompleted(PTCameraConnectError?)
case activateState(PTCameraActivateState) // ← 激活态独立成一档
case setPwd(passwordType)
case setPwdCompleted
case upgradeSuccess
case upgradeFail
case locked
}
enum PTCameraActivateState: Equatable, Hashable {
case activating
case success
case failed(PTCameraActivateFailure)
}
源码:Momenta/Biz/Camera/Connection/Definitions/PTCameraConnectDefines.swift · 466 行(枚举在 250 行与 365 行)
enum PTCameraActivateFailedType {
case networkUnavailable // 网络不可用
case notLoggedIn // 未登录 → 弹强制登录层
case activationFailed // 服务端业务失败
case firmwareVersionInvalid // 四路固件版本空/不一致 → 引导升级
case firmwareVersionInvalidNetworkUnavailable
case firmwareVersionInvalidAndLowBattery
case deviceAlreadyActivated // 服务端已激活(错误码 500 + "已激活")
case snEmpty // SN 未同步完成
case activateWIFIFailed
case other, unknown
}
源码:PTCameraConnectDefines.swift:376-388(shouldAccumulateFailureCount 决定是否计入「激活失败次数」提示)
| 入口 | 触发条件 | 落点 |
|---|---|---|
| A 连接搜索页(主入口) | 连接成功页点「激活」/ 激活失败点「重试」/ 升级成功后点「激活」 | PTCameraSearchView.swift:733 / 784 / 816 → PTCameraSearchViewModel.swift:176 → 策略 activateDevice() |
| B 连接成功后自动检查 | WiFi 连接成功事件(全局 WiFi delegate,App 生命周期常驻) | PTCameraConnectionFlowManager → checkFirmwareUpgrade → checkActivationStatus → performActivationStatusCheck |
| C 连 WiFi 前的预查 | 连接状态机进入 .connect(.tryToConnectWifi) | PTCameraSearchStrategySD2402.swift:139-158:若 !isActivated 就先查服务端激活态,失败直接判 .failed(.networkUnavailable) |
| D 静默同步激活 | 服务端已激活但设备端/蓝牙配置未激活 | PTSilentActivationSyncCoordinator(同 PTCameraConnectionFlowManager.swift 内,第 26 行起):每 3 秒重试 activateWIFI,成功则 setActivatedManually(true) |
activateDevice()四个前置检查按顺序短路,任一失败即进入 .activateState(.failed):
func activateDevice() {
trackActivationStartEvent()
updateConnectMode(.activateState(.activating))
guard PTNetworkReachability.shared.isMobileDataAvailable else { // ① 网络(注意:判的是"移动数据")
updateConnectMode(.activateState(.failed(...networkUnavailable...))); return }
guard PTUserDataManager.shared.isLoggedIn else { // ② 登录
updateConnectMode(.activateState(.failed(...notLoggedIn...))); return }
guard !sn.isEmpty else { ...snEmpty...; return } // ③ 设备 SN
if isActivationFirmwareVersionInvalid() { ...引导升级...; return } // ④ 四路固件版本一致且不在强制升级清单
// —— 服务端:先查后激活 ——
let (isSuccess, isActivated, error) = await networkInterceptor.queryServiceActivateStatus()
if !isActivated {
let (ok, err) = await networkInterceptor.activateDevice(sn, mac: nil) // POST /activate/activateDevice
// 特殊分支:错误码 500 且 message 含"已激活" → deviceAlreadyActivated
}
// —— 设备侧:绑定 + 激活(这两条决定了相机内部状态)——
let (_, isCameraLocked, isBindUser) = await userInterceptor.sendUserId(PTUserDataManager.shared.getAccountId())
PTCameraModel.shared.isbindUser = isBindUser; PTCameraModel.shared.isCameraLocked = isCameraLocked
if !PTCameraModel.shared.isActivated {
let (ok, err) = await systemInterceptor.activateWIFI(PTUserDataManager.shared.getAccountId())
// 设备回 "camera is already activated,do not need to activate again" 时按成功处理
}
PTCameraModel.shared.cacheServiceActivatedStatus(true)
updateConnectMode(.activateState(.success)); trackActivationSuccessEvent()
}
源码:Momenta/Biz/Camera/Connection/Strategy/PTCameraSearchStrategySD2402+Activation.swift · 312 行(主流程 12-190 行)
// ① 提交激活:PTDeviceNetWorkService.swift:427
func activateDevice(sn: String, mac: String? = nil, completion: ...) {
let token = userDataManager.getUserToken()
guard !token.isEmpty else { completion(.failure(.invalidToken)); return } // 需要登录 token
httpService.post(activateDeviceUrl, parameters: params, headers: jsonHeaders(withToken: token)) { ... }
}
// ② 查询激活态:PTDeviceNetWorkService.swift:729(含三级回落)
func queryServiceActivateStatus(completion: @escaping (Bool, String?) -> Void) {
if !PTNetworkReachability.shared.hasInternetAccess { // 无网 → 本地缓存 → 设备端状态 → toast
if let cached = PTCameraModel.shared.cachedServiceActivatedStatus() { completion(cached, nil) }
else if !PTCameraModel.shared.currentSyncDeviceInfo.info.sn.isEmpty { completion(PTCameraModel.shared.isActivated, nil) }
else { completion(false, "当前网络异常,请稍后重试。") }
return
}
let sn = resolveActivationQuerySN() // SN 未同步完成则不发请求(防 404 误报)
queryActivateStatus(sn: sn) { ... PTCameraModel.shared.cacheServiceActivatedStatus(isActivated) ... }
}
源码:Momenta/DeviceManager/NetWork/PTDeviceNetWorkService.swift · 786 行(URL 常量 236-250 行)
func performActivationStatusCheck(completion: @escaping (PTCameraFirmwareActivationCheckResult) -> Void) {
PTDeviceNetWorkService.shared.queryServiceActivateStatus { isActivate, errorMessage in
if let errorMessage { ...showToast(errorMessage); completion(.flowHandled); return }
let mismatch = !PTCameraModel.shared.isActivated || (bleActivated == false)
if isActivate {
if mismatch, !PTUserDataManager.shared.isLoggedIn { completion(.activationRequired); return } // 强制登录
if mismatch { silentActivationSyncCoordinator?.startIfNeeded() } // 静默补激活
completion(.readyForCapture) // ← 放行
} else {
completion(.activationRequired) // ← 未激活:弹 PTCameraSearchView(.connectCompleted(nil))
}
}
}
源码:Momenta/Biz/Camera/Connection/Managers/PTCameraConnectionFlowManager.swift · 551 行(分流结果枚举在同文件 13-22 行)
| 位置 | 判断 | 行为影响 | 改动后是否需要动 |
|---|---|---|---|
PTCameraConnectionFlowManager.swift:317 | isActivate(服务端) | readyForCapture vs activationRequired(弹激活页) | 短路查询后自然放行 |
PTCameraConnectionFlowManager.swift:53 | !PTCameraModel.isActivated 才启动静默激活同步 | 不再启动重试循环 | 否(恒真即不启动) |
PTCameraSearchView.swift:977-990 | isActivated(服务端查询结果) | 已激活 → navigateAfterCameraConnected();未激活+未登录 → 强制登录层;未激活 → 显示激活界面 | 否(短路后走"已激活"分支) |
PTCameraConnectionFlowManager.swift:241 | needUpgrade && !isActivated && isRecoveryBlockedFirmwareVersion() | 弹「未激活先升级」PTUnActivateView | 否(恒真后不可达) |
PTMomentDataManager.swift:123 | isCameraConnected && isActivated | Moment/相册是否纳入相机素材 | 否(恒真后自动纳入,属预期改善) |
PTCameraRouter.swift:92、PTFlutterCameraBridge.swift:942 | isActivated && !isForcedUpgrade && !isInUpgradeFlow | 智慧感知「开启预览/拍摄」快速放行(跳过每次联网核查) | 否 |
PTFlutterDevicePayload.swift:243/284 | 把 isActivated 下发给 Flutter | Flutter 页面的设备状态展示/分支 | 否(恒真后 Flutter 侧也认为已激活) |
PTCameraSearchStrategySD2402.swift:139-158 | !PTCameraModel.isActivated 时预查服务端 | 连 WiFi 前多一次网络往返,失败即报"网络异常" | 建议改(去掉这次预查) |
一个现成的先例(本轮发现):仓库里已有调试开关 PTDebugToolsManager.shared.isShowActivate(默认 true,可在 Debug 页关闭)。
// PTCameraSearchView.swift:959
if PTDebugToolsManager.shared.isShowActivate == false {
logInfoEx("PTCameraSearchView:相机连接成功,调试工具设置为不显示激活界面,按相册权限决定去向")
navigateAfterCameraConnected()
return
}
源码:Momenta/Debug/PTDebugToolsManager.swift:33(内存变量,非持久化;读取点仅 PTCameraSearchView.swift:959)
它只跳过「激活界面展示」,并不阻止服务端查询/激活调用,也没有让 PTCameraModel.isActivated 变真(下游门禁仍按未激活走)。不要拿它当开关用,但可以佐证:把激活入口旁路掉在产品上是既有形态。
| 文件 | 行数 | 角色 | 本次改动 |
|---|---|---|---|
Biz/Camera/Connection/Strategy/PTCameraSearchStrategySD2402+Activation.swift | 312 | 激活主流程(预检 + 云端 + 设备侧) | 改 1 顶部短路 |
DeviceManager/NetWork/PTDeviceNetWorkService.swift | 786 | 两个服务端接口(查询/提交) | 改 2 查询短路 |
Biz/Capture/Models/PTCameraModel.swift | 2663 | 门禁变量 isActivated 的写入点 | 改 3 写入恒真 |
Biz/Camera/Connection/Strategy/PTCameraSearchStrategySD2402.swift | 475 | 连接状态机、连 WiFi 前预查、模式转换 | 改 4 预查短路 |
新增 Biz/Camera/Activation/PTActivationBypass.swift(约 30 行,开关集中地) | |||
Biz/Camera/Connection/Managers/PTCameraConnectionFlowManager.swift | 551 | 连接成功分流 + 静默激活同步 | 不改(上游短路后自然走 readyForCapture) |
Biz/Camera/Connection/Views/PTCameraSearchView.swift | 1307 | 激活 UI 状态映射与三个激活按钮回调 | 不改(失败分支不可达) |
Biz/Camera/Connection/Definitions/PTCameraConnectDefines.swift | 466 | PTCameraActivateState/失败类型枚举 | 不改(保留枚举,避免 diff 扩散) |
Biz/Camera/Activation/*、Connection/Views/PTCameraActivationFailed*、FirmwareUpgrade/Views/UnActivate/* | — | 激活失败/未登录/未激活先升级 UI(约 10 个视图 + 1 个 ViewModel) | 不删(保留但不可达) |
Biz/Camera/Connection/Views/PTCameraSearchViewModel.swift | — | VM 转发 + 「登录完成 → 等激活终结态」链式回调 | 不改 |
Debug/PTDebugToolsManager.swift | — | isShowActivate 调试开关 | 不改(与本次开关无关,勿复用) |
固件(sd2402/qcs8550/leaptic_app @ release-1.4.0.0,实际构建入口在 sd2402/manifest:leaptic_app → qcs8550/src/quectel-app/Zhuimi_Application/src/)里,激活相关字段是纯状态上报:
// mobile/src/UserCenter.cpp:344-384(启动归一化 + 上报口径)
if (is_actived == 0) { // 工厂态:插入/重置默认行
INSERT OR REPLACE INTO UserInfo(ID, userid, password, is_bind, is_first_connect, is_lock, is_active, lock_time)
VALUES(1, 'default_user', '', 0, 1, 0, 0, '');
}
...
LOG_INFO("userinfo.is_active is :%d", userinfo.is_active);
if (userinfo.is_active == 0 || userinfo.is_active == 1) {
CameraSettingInfo::getInstance().is_activated = true; // ← 工厂态(0)也按"已激活"上报
} else {
CameraSettingInfo::getInstance().is_activated = false;
}
// mobile/src/Subtask.cpp:103-182(App 下发 userId 即完成"激活")
json SubtaskHandler::handle_activate(json request) {
...
userinfo.is_active = 1;
persistActivateWithRetry(userinfo, ACTIVATE_IMMEDIATE_RETRY_MAX, dbError); // 只落本地 sqlite
fdclient->send_active_status(userinfo.is_active, &msg_bt); // 通知 App
CameraSettingInfo::getInstance().is_activated = true;
CAMERA_ACTIVATE(1, app_ver.c_str()); // 埋点
}
证据结论:全仓检索 is_activated / is_active 的出现点只有「赋值、落库、上报、日志」四类,没有任何 if (is_activated) 形式的功能门禁;handle_activate 全程不访问云端。⇒ 不去下发 activate,相机依然按已激活状态工作,拍摄、预览、存储、Wi-Fi 配网均不受影响。
附:固件里唯一与云相关的默认地址是 mobile/src/cloud/https_client.cpp:1351-1353 的 https://photon-test.leaptic.tech/photon-server(云存功能的控制面基址,凭据由 App 下发),与激活无关,属需求 R1 的范围。
总体思路:一个开关 + 四处短路 + 保留设备侧指令。开关形态建议编译期宏(正式线零影响、可一键回滚),并提供运行时覆盖便于真机调试。
Momenta/Biz/Camera/Activation/PTActivationBypass.swiftimport Foundation
/// 去激活开关:开启后 App 不再要求「相机激活」,也不访问服务端激活接口。
/// 编译期:Build Settings 里为对应 Configuration 增加 SWIFT_ACTIVE_COMPILATION_CONDITIONS = PT_ACTIVATION_BYPASS
/// 运行时:启动参数 -PTActivationBypass 或 UserDefaults.standard.bool(forKey: "PTActivationBypass")(便于真机开关调试)
enum PTActivationBypass {
static var isEnabled: Bool {
#if PT_ACTIVATION_BYPASS
return true
#else
return ProcessInfo.processInfo.arguments.contains("-PTActivationBypass")
|| UserDefaults.standard.bool(forKey: "PTActivationBypass")
#endif
}
/// 设备侧激活使用的账号(内置用户),见 R1 的本地账号 fixture
static var builtinAccountId: String {
PTUserDataManager.shared.getAccountId() // 未登录时由内置用户提供
}
}
+Activation.swift:12)跳过 4 项前置检查与两个服务端调用,但保留设备侧 sendUserId + activateWIFI,让相机内部绑定态与现状一致。
// BEFORE
func activateDevice() {
trackActivationStartEvent()
updateConnectMode(.activateState(.activating))
guard PTNetworkReachability.shared.isMobileDataAvailable else { ... }
guard PTUserDataManager.shared.isLoggedIn else { ... }
...
}
// AFTER
func activateDevice() {
if PTActivationBypass.isEnabled {
performBypassActivation() // 新增:直接走成功态 + 设备侧对齐
return
}
trackActivationStartEvent()
...
}
/// 去激活模式:不查云端、不提交激活,只做设备侧绑定/激活
private func performBypassActivation() {
updateConnectMode(.activateState(.activating))
Task { [weak self] in guard let self else { return }
let account = PTActivationBypass.builtinAccountId
let userInterceptor = PTSDKFactory.createUserCommand()
let systemInterceptor = PTSDKFactory.createSystemCommand()
if let (ok, locked, bound) = try? await userInterceptor.sendUserId(account), ok {
await MainActor.run {
PTCameraModel.shared.isbindUser = bound
PTCameraModel.shared.isCameraLocked = locked
}
}
_ = try? await systemInterceptor.activateWIFI(account) // 相机侧 is_active=1 / is_bind=1
await MainActor.run {
PTCameraModel.shared.setActivatedManually(true)
self.updateConnectMode(.activateState(.success))
}
}
}
PTDeviceNetWorkService.swift:729)// BEFORE
func queryServiceActivateStatus(completion: @escaping (Bool, String?) -> Void) {
logInfoEx("查询服务器端设备是否已激活,是否有网络:\(PTNetworkReachability.shared.hasInternetAccess)")
if !PTNetworkReachability.shared.hasInternetAccess { ... }
...
// AFTER(第 1 行插入)
func queryServiceActivateStatus(completion: @escaping (Bool, String?) -> Void) {
if PTActivationBypass.isEnabled { // 去激活:不再请求 /device/isActivated/
completion(true, nil)
return
}
logInfoEx(...)
}
连带效果:`/device/isActivated/{sn}` 不再被调用;无网时的「缓存 / 设备态 / toast」三级回落全部不再触发(也就不再有"网络异常"toast)。
PTCameraModel.swift:1454)// BEFORE(设备信息同步时按 BLE 上报写入)
self.isActivated = info.isActivated
// AFTER
self.isActivated = PTActivationBypass.isEnabled ? true : info.isActivated
这一处是全 App 门禁的总闸:PTCameraRouter/PTFlutterCameraBridge(快速预览放行)、PTMomentDataManager(相机素材纳入)、PTCameraConnectionFlowManager(静默同步不再启动)、以及下发给 Flutter 的 isActivated 字段,全部随之变为「已激活」。PTCameraModel.setActivatedManually(_:)(同文件 :1511)是既有的手动置位接口,无需新增机制。
PTCameraSearchStrategySD2402.swift:139)// BEFORE
case .connect(.tryToConnectWifi):
if !PTCameraModel.shared.isActivated {
PTDeviceNetWorkService.shared.queryServiceActivateStatus { isActivated, errorMessage in
// 查询失败 → 直接把连接流打成 .activateState(.failed(.networkUnavailable))
}
}
// AFTER(改动三之后其实已不可达,仍建议显式短路,避免"隐式依赖")
case .connect(.tryToConnectWifi):
if !PTActivationBypass.isEnabled, !PTCameraModel.shared.isActivated { ...原逻辑... }
PTCameraActivateFailedView/UnLoginView/FailCounter/PTUnActivate* 全部保留——短路后这些分支不可达,删除只会扩大回归面。PTCameraActivateFailedType、L10n.connectActiveFailed 等保留。PTActivationAnalytics 的激活漏斗事件保留代码(去激活模式不再产生事件;如需继续观测可后续再加本地上报)。PTUserService.sendUserId / PTSystemService.activateWIFI)不动。另一种口径(若"所有版本都去掉"):把上述四处短路里的开关判断去掉,直接按"恒真/短路"实现,并顺手把激活 UI 文件删除、把 /activate/activateDevice 与 /device/isActivated/ 常量一并清理。代价是需要回归所有连接路径(含 Ring/GoPro/座舱、升级流程、账号切换),且无法"关开关回到旧行为"。
对照要点:网络请求少 1~2 次(每次连接成功省一次 /isActivated/,未激活时再省一次 /activate/activateDevice);设备侧指令一条不少,因此相机内部状态、后续"忘记密码/解绑/换绑"等依赖绑定态的流程行为与现状一致。
| 项 | 说明 | 风险等级 |
|---|---|---|
| 服务端不再登记设备 | 云端看不到该设备(与 R2 目标一致),云相册/订阅等本已不可用 | 低(预期) |
| 多账号切换 | 固定使用内置账号:换账号后相机 is_bind 仍指向内置账号,服务端侧绑定关系不再更新 | 中(取决于是否还需要换绑) |
| 未激活先升级链路 | PTUnActivateView 不再可达;若某台相机固件过旧,将不再被这条链路引导升级 | 中(需确认是否保留替代入口:设置页的固件升级仍在) |
| 激活埋点断链 | PTActivationAnalytics 的激活漏斗不再上报;若后台报表依赖该漏斗会出现数据缺口 | 低(须知会数据方) |
| 登录不再必需 | 激活是"未登录无法继续"的唯一硬门槛;去掉后 App 能完全在未登录态使用设备功能 | 低(符合 R1 方向) |
| 开关形态 | 编译宏默认关闭 ⇒ 正式线与旧包行为一致;忘记开开关的表现=与现状相同 | 低 |
| Flutter / C++ / 固件 | 接口未变(isActivated 只是值变真),无需联动改动 | 低 |
PT_ACTIVATION_BYPASS 的配置能出包;关闭时 diff 行为为零(同一提交两个产物对比)。/isActivated/、/activate/activateDevice(抓包或日志确认)。[PTCameraConnectionFlowManager] 相机已激活,可以跳转拍摄页面。is_bind/is_active 为真(BLE 上报或调试页确认)。PTMomentDataManager 是否纳入相机素材(预期纳入)。PTCameraModel.isActivated 有既有手动置位接口,短路点集中、可审计accountId,一旦将来要恢复云功能,需要一次数据/绑定迁移设计去激活是纯 App 侧、可逆、低耦合的改动,建议按「编译宏开关 + 4 处短路 + 保留设备侧指令」实施;不建议删 UI 文件或直接改逻辑(会同时失去回滚能力与最小回归面)。真正的风险不在代码改动,而在两处业务口径:①未激活先升级链路是否还需要;②固定内置账号对"换绑/解绑"的长期影响——这两点需要产品侧拍板。