Compare commits
5 Commits
8e879b009a
...
8074ba71c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8074ba71c0 | ||
|
|
ae7a68aad1 | ||
|
|
20bec0f7e3 | ||
|
|
99ffc19f60 | ||
|
|
320ece473f |
1
.gitignore
vendored
1
.gitignore
vendored
@ -78,3 +78,4 @@ ProjectSettings/ProjectVersion.txt
|
||||
Assets/Resources/BillingMode.json
|
||||
Assets/Resources/BillingMode.json.meta
|
||||
*_mapping.txt
|
||||
.dbg/
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -1,6 +1,5 @@
|
||||
{
|
||||
"dotnet.defaultSolution": "Kill.sln",
|
||||
"dotnet.dotnetPath": "C:/Program Files/dotnet/dotnet.exe",
|
||||
"dotrush.roslyn.dotnetSdkDirectory": "C:/Program Files/dotnet/sdk/8.0.113",
|
||||
"dotrush.msbuildProperties": {
|
||||
"DefineConstants": "UNITY_EDITOR"
|
||||
|
||||
@ -128,7 +128,7 @@ namespace Kill.Bluetooth
|
||||
/// <summary>
|
||||
/// 更新当前设备状态,并仅在值发生变化时广播状态事件。
|
||||
/// </summary>
|
||||
private bool UpdateDeviceState(string deviceMac, BLEDeviceStateField field, Action<BLEDeviceState> update)
|
||||
private bool UpdateDeviceState(string deviceMac, BLEDeviceStateField field, Func<BLEDeviceState, bool> update)
|
||||
{
|
||||
if (update == null)
|
||||
{
|
||||
@ -162,10 +162,9 @@ namespace Kill.Bluetooth
|
||||
return changed;
|
||||
}
|
||||
|
||||
private bool UpdateStateFromResponse(BLEResponse response, BLEDeviceStateField field, Action<BLEDeviceState> update)
|
||||
private bool UpdateStateFromResponse(BLEResponse response, BLEDeviceStateField field, Func<BLEDeviceState, bool> update)
|
||||
{
|
||||
return response != null && response.IsSuccess &&
|
||||
UpdateDeviceState(GetCurrentDeviceMac(), field, update);
|
||||
return response.IsSuccess && UpdateDeviceState(GetStateMac(), field, update);
|
||||
}
|
||||
|
||||
private static string NormalizeDeviceMac(string deviceMac)
|
||||
@ -295,11 +294,13 @@ namespace Kill.Bluetooth
|
||||
{
|
||||
Log($"蓝牙已断开: {address}");
|
||||
|
||||
// 使用断开参数作为设备状态键;部分平台会在清空地址前触发事件。
|
||||
string stateMac = NormalizeDeviceMac(address);
|
||||
// 断开参数通常是 BLE 地址,优先使用已记录的真实 MAC 作为状态键。
|
||||
string stateMac = string.IsNullOrEmpty(_lastStateMac)
|
||||
? NormalizeDeviceMac(address)
|
||||
: _lastStateMac;
|
||||
if (string.IsNullOrEmpty(stateMac))
|
||||
{
|
||||
stateMac = string.IsNullOrEmpty(_lastStateMac) ? GetCurrentDeviceMac() : _lastStateMac;
|
||||
stateMac = GetCurrentDeviceMac();
|
||||
}
|
||||
_lastStateMac = string.IsNullOrEmpty(stateMac) ? _lastStateMac : stateMac;
|
||||
UpdateDeviceState(
|
||||
@ -881,6 +882,15 @@ namespace Kill.Bluetooth
|
||||
Log($"定时任务设置成功: {tasks[0]}");
|
||||
else
|
||||
Log($"定时任务批量设置成功,共 {tasks.Length} 条任务");
|
||||
|
||||
ScheduleTask[] normalizedTasks = new ScheduleTask[tasks.Length];
|
||||
for (int i = 0; i < tasks.Length; i++)
|
||||
{
|
||||
normalizedTasks[i] = tasks[i];
|
||||
normalizedTasks[i].TaskId = (byte)i;
|
||||
}
|
||||
UpdateStateFromResponse(response, BLEDeviceStateField.ScheduleTasks,
|
||||
state => state.SetScheduleTasks(normalizedTasks));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1601,6 +1611,8 @@ namespace Kill.Bluetooth
|
||||
SendFrame(frame, (response) =>
|
||||
{
|
||||
var setting = MillimeterWaveSetting.FromBytes(response.Data);
|
||||
UpdateStateFromResponse(response, BLEDeviceStateField.MillimeterWave,
|
||||
state => state.SetMillimeterWave(setting));
|
||||
callback?.Invoke(setting);
|
||||
OnMillimeterWaveSettingReceived?.Invoke(setting);
|
||||
Log($"读取毫米波雷达设置: {setting}");
|
||||
@ -1630,9 +1642,15 @@ namespace Kill.Bluetooth
|
||||
{
|
||||
bool success = response.IsSuccess;
|
||||
if (success)
|
||||
Log($"毫米波雷达设置成功: {setting}");
|
||||
{
|
||||
Log($"毫米波设置成功: {setting}");
|
||||
UpdateStateFromResponse(response, BLEDeviceStateField.MillimeterWave,
|
||||
state => state.SetMillimeterWave(setting));
|
||||
}
|
||||
else
|
||||
LogError($"毫米波雷达设置失败, 状态码={response.Status:X2}");
|
||||
{
|
||||
LogError($"毫米波设置失败, 状态码={response.Status:X2}");
|
||||
}
|
||||
callback?.Invoke(success);
|
||||
});
|
||||
}
|
||||
@ -2431,7 +2449,7 @@ namespace Kill.Bluetooth
|
||||
if (!CheckConnection())
|
||||
{
|
||||
LogError($"[BLE-DEBUG] 蓝牙未就绪,无法发送指令: {description}");
|
||||
callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR });
|
||||
callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR, Data = new byte[0] });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2447,7 +2465,7 @@ namespace Kill.Bluetooth
|
||||
if (!CheckConnection())
|
||||
{
|
||||
LogError("[BLE-DEBUG] CheckConnection failed, invoking callback with error");
|
||||
callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR });
|
||||
callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR, Data = new byte[0] });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2498,7 +2516,7 @@ namespace Kill.Bluetooth
|
||||
{
|
||||
LogError($"发送帧异常: {ex.Message}");
|
||||
IsWaitingResponse = false;
|
||||
callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR });
|
||||
callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR, Data = new byte[0] });
|
||||
}
|
||||
}
|
||||
|
||||
@ -2612,7 +2630,7 @@ namespace Kill.Bluetooth
|
||||
|
||||
// 通知帧优先处理,并且绝对不影响命令队列(IsWaitingResponse / _pendingCallback / _responseTimer)
|
||||
// 设备主动通知是异步旁路通道,与当前等待响应的命令是两条独立链路
|
||||
if (frame.ReadWrite == BLEConstants.RW_NOTIFY || frame.Command == BLEConstants.NOTIFY_COMMAND_CODE)
|
||||
if (frame.ReadWrite == BLEConstants.RW_NOTIFY && frame.Command == BLEConstants.NOTIFY_COMMAND_CODE)
|
||||
{
|
||||
HandleNotification(frame.Data);
|
||||
return;
|
||||
@ -2728,6 +2746,12 @@ namespace Kill.Bluetooth
|
||||
|
||||
private void HandleDeviceStateChangeNotification(byte[] data)
|
||||
{
|
||||
if (data == null || data.Length < 2)
|
||||
{
|
||||
LogWarning("设备状态变化通知数据长度不足");
|
||||
return;
|
||||
}
|
||||
|
||||
var notify = DeviceStateChangeNotification.FromBytes(data);
|
||||
if (notify.WorkMode > (byte)WorkMode.Eliminate)
|
||||
{
|
||||
@ -2737,7 +2761,7 @@ namespace Kill.Bluetooth
|
||||
|
||||
Log($"设备状态变化通知: 工作模式=0x{notify.WorkMode:X2}");
|
||||
UpdateDeviceState(
|
||||
GetCurrentDeviceMac(),
|
||||
GetStateMac(),
|
||||
BLEDeviceStateField.WorkMode,
|
||||
state => state.SetWorkMode((WorkMode)notify.WorkMode));
|
||||
try
|
||||
@ -2752,11 +2776,17 @@ namespace Kill.Bluetooth
|
||||
|
||||
private void HandleErrorStatusNotification(byte[] data)
|
||||
{
|
||||
if (data == null || data.Length < 8)
|
||||
{
|
||||
LogWarning("错误状态通知数据长度不足");
|
||||
return;
|
||||
}
|
||||
|
||||
var notify = ErrorStatusNotification.FromBytes(data);
|
||||
if (data != null && data.Length >= 8)
|
||||
{
|
||||
UpdateDeviceState(
|
||||
GetCurrentDeviceMac(),
|
||||
GetStateMac(),
|
||||
BLEDeviceStateField.HardwareStatus,
|
||||
state => state.SetHardwareStatus(notify.Status));
|
||||
}
|
||||
@ -2773,6 +2803,12 @@ namespace Kill.Bluetooth
|
||||
|
||||
private void HandleParameterChangeNotification(byte[] data)
|
||||
{
|
||||
if (data == null || data.Length < 3)
|
||||
{
|
||||
LogWarning("参数变化通知数据长度不足");
|
||||
return;
|
||||
}
|
||||
|
||||
var notify = ParameterChangeNotification.FromBytes(data);
|
||||
Log($"参数变化通知: 命令=0x{notify.CommandCode:X2}, 数据长度={(notify.Payload?.Length ?? 0)}");
|
||||
|
||||
@ -2794,7 +2830,7 @@ namespace Kill.Bluetooth
|
||||
/// </summary>
|
||||
private void ApplyParameterChangeToDeviceState(ParameterChangeNotification notify)
|
||||
{
|
||||
string deviceMac = GetCurrentDeviceMac();
|
||||
string deviceMac = GetStateMac();
|
||||
if (string.IsNullOrEmpty(deviceMac) || notify.Payload == null)
|
||||
{
|
||||
return;
|
||||
@ -2805,35 +2841,52 @@ namespace Kill.Bluetooth
|
||||
switch (notify.CommandCode)
|
||||
{
|
||||
case BLEConstants.CMD_LANGUAGE_SETTING:
|
||||
if (notify.Payload.Length < 1) return;
|
||||
UpdateDeviceState(deviceMac, BLEDeviceStateField.Language,
|
||||
state => state.SetLanguage(LanguageSetting.FromBytes(notify.Payload)));
|
||||
break;
|
||||
case BLEConstants.CMD_SCHEDULE_TASK:
|
||||
if (notify.Payload.Length < 7) return;
|
||||
UpdateDeviceState(deviceMac, BLEDeviceStateField.ScheduleTasks,
|
||||
state => state.SetScheduleTasks(ScheduleTaskListResponse.FromBytes(0x00, notify.Payload).Tasks));
|
||||
break;
|
||||
case BLEConstants.CMD_DEVICE_LOCK:
|
||||
if (notify.Payload.Length < 1) return;
|
||||
UpdateDeviceState(deviceMac, BLEDeviceStateField.LockState,
|
||||
state => state.SetLockState(DeviceLockControl.FromBytes(notify.Payload)));
|
||||
break;
|
||||
case BLEConstants.CMD_FILL_LIGHT_CONTROL:
|
||||
if (notify.Payload.Length < 3) return;
|
||||
UpdateDeviceState(deviceMac, BLEDeviceStateField.FillLight,
|
||||
state => state.SetFillLight(FillLightControl.FromBytes(notify.Payload)));
|
||||
break;
|
||||
case BLEConstants.CMD_ANGLE_CONTROL:
|
||||
if (notify.Payload.Length < 2) return;
|
||||
UpdateDeviceState(deviceMac, BLEDeviceStateField.Angle,
|
||||
state => state.SetAngle(AngleControl.FromBytes(notify.Payload)));
|
||||
break;
|
||||
case BLEConstants.CMD_DISTANCE_CONTROL:
|
||||
if (notify.Payload.Length < 4) return;
|
||||
UpdateDeviceState(deviceMac, BLEDeviceStateField.Distance,
|
||||
state => state.SetDistance(DistanceControl.FromBytes(notify.Payload)));
|
||||
break;
|
||||
case BLEConstants.CMD_VISUAL_DETECTION_SETTING:
|
||||
if (notify.Payload.Length < 2) return;
|
||||
UpdateDeviceState(deviceMac, BLEDeviceStateField.VisualDetection,
|
||||
state => state.SetVisualDetection(VisualDetectionSetting.FromBytes(notify.Payload)));
|
||||
break;
|
||||
case BLEConstants.CMD_MILLIMETER_WAVE_SETTING:
|
||||
if (notify.Payload.Length < 4) return;
|
||||
UpdateDeviceState(deviceMac, BLEDeviceStateField.MillimeterWave,
|
||||
state => state.SetMillimeterWave(MillimeterWaveSetting.FromBytes(notify.Payload)));
|
||||
break;
|
||||
case BLEConstants.CMD_FILL_LIGHT_CONNECTION_STATUS:
|
||||
if (notify.Payload.Length < 1) return;
|
||||
UpdateDeviceState(deviceMac, BLEDeviceStateField.FillLightConnection,
|
||||
state => state.SetFillLightConnection(FillLightConnectionStatus.FromBytes(notify.Payload)));
|
||||
break;
|
||||
default:
|
||||
// 未映射的字段:不写仓库,避免误覆盖
|
||||
// 暂不写入本地状态,避免异常参数覆盖正确值
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -2845,11 +2898,17 @@ namespace Kill.Bluetooth
|
||||
|
||||
private void HandleCapacitorStateNotification(byte[] data)
|
||||
{
|
||||
if (data == null || data.Length < 2)
|
||||
{
|
||||
LogWarning("蓄能状态通知数据长度不足");
|
||||
return;
|
||||
}
|
||||
|
||||
var notify = CapacitorStateNotification.FromBytes(data);
|
||||
if (data != null && data.Length >= 2)
|
||||
{
|
||||
UpdateDeviceState(
|
||||
GetCurrentDeviceMac(),
|
||||
GetStateMac(),
|
||||
BLEDeviceStateField.CapacitorState,
|
||||
state => state.SetCapacitorState(notify));
|
||||
}
|
||||
@ -2866,12 +2925,18 @@ namespace Kill.Bluetooth
|
||||
|
||||
private void HandleFillLightConnectionNotification(byte[] data)
|
||||
{
|
||||
if (data == null || data.Length < 2)
|
||||
{
|
||||
LogWarning("补光灯连接状态通知数据长度不足");
|
||||
return;
|
||||
}
|
||||
|
||||
var notify = FillLightConnectionNotification.FromBytes(data);
|
||||
if (data != null && data.Length >= 2)
|
||||
{
|
||||
var status = new FillLightConnectionStatus { IsConnected = notify.IsConnected };
|
||||
UpdateDeviceState(
|
||||
GetCurrentDeviceMac(),
|
||||
GetStateMac(),
|
||||
BLEDeviceStateField.FillLightConnection,
|
||||
state => state.SetFillLightConnection(status));
|
||||
}
|
||||
@ -2898,7 +2963,9 @@ namespace Kill.Bluetooth
|
||||
|
||||
var timeoutResponse = new BLEResponse
|
||||
{
|
||||
Status = BLEConstants.STATUS_TIMEOUT
|
||||
Status = BLEConstants.STATUS_TIMEOUT,
|
||||
// 超时无数据,给空数组避免回调里 BitConverter.ToString(response.Data) 抛 ArgumentNullException
|
||||
Data = new byte[0]
|
||||
};
|
||||
|
||||
_pendingCallback?.Invoke(timeoutResponse);
|
||||
|
||||
@ -52,6 +52,8 @@ namespace Kill.Bluetooth
|
||||
|
||||
public VisualDetectionSetting? VisualDetection { get; private set; }
|
||||
|
||||
public MillimeterWaveSetting? MillimeterWave { get; private set; }
|
||||
|
||||
public LanguageSetting? Language { get; private set; }
|
||||
|
||||
public HardwareStatus? HardwareStatus { get; private set; }
|
||||
@ -183,6 +185,21 @@ namespace Kill.Bluetooth
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool SetMillimeterWave(MillimeterWaveSetting setting)
|
||||
{
|
||||
if (MillimeterWave.HasValue &&
|
||||
MillimeterWave.Value.Enable == setting.Enable &&
|
||||
MillimeterWave.Value.Sensitivity == setting.Sensitivity &&
|
||||
MillimeterWave.Value.ActualSafeDistance == setting.ActualSafeDistance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
MillimeterWave = setting;
|
||||
MarkUpdated();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool SetLanguage(LanguageSetting language)
|
||||
{
|
||||
if (Language.HasValue && Language.Value.Language == language.Language)
|
||||
|
||||
@ -202,6 +202,18 @@ namespace Kill.Bluetooth
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||
// 安卓:蓝牙已被系统关闭时直接静默跳过。
|
||||
// 断开清理/自动重连等后台流程会反复触发初始化,若继续走到 CheckAndEnableBluetooth
|
||||
// 会弹"蓝牙未开启"Toast 并广播 OnError,造成关蓝牙后一连串误报。
|
||||
// 用户主动触发的初始化走 ForceInitialize,仍能得到提示。
|
||||
if (!CheckIfBluetoothIsOn())
|
||||
{
|
||||
Log("蓝牙未开启,跳过蓝牙初始化");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
Log("正在初始化蓝牙...");
|
||||
|
||||
// 先检查蓝牙是否开启,如果没有则尝试启用
|
||||
@ -218,7 +230,10 @@ namespace Kill.Bluetooth
|
||||
public void ForceInitialize()
|
||||
{
|
||||
_userDeniedBluetooth = false;
|
||||
Initialize();
|
||||
// 用户主动触发:不走 Initialize 的静默跳过守卫,
|
||||
// 蓝牙未开启时由 CheckAndEnableBluetooth 给出提示
|
||||
Log("正在初始化蓝牙...");
|
||||
CheckAndEnableBluetooth(DoInitialize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -476,7 +491,8 @@ namespace Kill.Bluetooth
|
||||
}
|
||||
if (IsConnected)
|
||||
{
|
||||
Disconnect();
|
||||
// 蓝牙已被系统关闭,0x70 协议告别帧发不出去,跳过直接清理本地状态
|
||||
Disconnect(sendProtocolCommand: false);
|
||||
}
|
||||
if (IsScanning)
|
||||
{
|
||||
@ -595,6 +611,13 @@ namespace Kill.Bluetooth
|
||||
}
|
||||
else
|
||||
{
|
||||
// 蓝牙已被系统关闭时,插件所有操作(写入/扫描/断开)的失败都会进入这个全局错误回调,
|
||||
// 此刻并非初始化失败,状态已由 MonitorBluetoothState 清理过,只记录不广播,避免误报
|
||||
if (!CheckIfBluetoothIsOn())
|
||||
{
|
||||
Log($"蓝牙已关闭,忽略蓝牙错误: {error}");
|
||||
return;
|
||||
}
|
||||
Log($"蓝牙初始化失败: {error}");
|
||||
_enableBtCoroutine = null;
|
||||
OnError?.Invoke($"初始化失败: {error}");
|
||||
@ -676,6 +699,14 @@ namespace Kill.Bluetooth
|
||||
/// <param name="timeout">扫描超时时间(秒),默认10秒</param>
|
||||
public void StartScan(float timeout = 999f)
|
||||
{
|
||||
// 蓝牙已被系统关闭时直接跳过:此时 BluetoothLeScanner 为 null,
|
||||
// 插件扫描调用会抛 NullPointerException(曾导致主页 Init 中断、设备状态不刷新)
|
||||
if (!CheckIfBluetoothIsOn())
|
||||
{
|
||||
Log("蓝牙未开启,跳过扫描");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsInitialized)
|
||||
{
|
||||
Log("蓝牙未初始化,自动触发初始化流程...");
|
||||
@ -693,6 +724,12 @@ namespace Kill.Bluetooth
|
||||
/// </summary>
|
||||
private void DoStartScan(float timeout)
|
||||
{
|
||||
if (!CheckIfBluetoothIsOn())
|
||||
{
|
||||
Log("蓝牙未开启,跳过扫描");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsScanning)
|
||||
{
|
||||
Log("已经在扫描中,先停止当前扫描...");
|
||||
@ -907,7 +944,7 @@ namespace Kill.Bluetooth
|
||||
/// 断开当前连接(带协议通知)
|
||||
/// 先发送0x70断开命令给设备,再断开BLE连接
|
||||
/// </summary>
|
||||
public void Disconnect(bool backHome=false)
|
||||
public void Disconnect(bool backHome=false, bool sendProtocolCommand=true)
|
||||
{
|
||||
needBackHome=backHome;
|
||||
if (!IsConnected)
|
||||
@ -933,21 +970,23 @@ namespace Kill.Bluetooth
|
||||
}
|
||||
|
||||
// 先通过BLE协议发送断开命令(0x70)
|
||||
Log($"检查BLECommunicationManager: Instance={(BLECommunicationManager.Instance != null ? "存在" : "null")}");
|
||||
if (BLECommunicationManager.Instance != null)
|
||||
if (sendProtocolCommand && BLECommunicationManager.Instance != null)
|
||||
{
|
||||
Log("发送断开命令(0x70)到设备...");
|
||||
BLECommunicationManager.Instance.Disconnect((success) =>
|
||||
{
|
||||
Log($"断开命令发送结果: {(success ? "成功" : "失败")}");
|
||||
// 0x70 设备不会回复,success 仅表示已提交插件写入,不代表设备已收到
|
||||
Log($"断开命令已提交: {(success ? "是" : "否")}");
|
||||
// 无论命令是否成功,都执行BLE断开
|
||||
PerformBLEDisconnect(addressToDisconnect);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("BLECommunicationManager.Instance 为 null,跳过0x70命令,直接断开BLE");
|
||||
// 如果没有BLE通信管理器,直接断开BLE
|
||||
Log(!sendProtocolCommand
|
||||
? "蓝牙不可用,跳过0x70命令,直接断开BLE"
|
||||
: "BLECommunicationManager.Instance 为 null,跳过0x70命令,直接断开BLE");
|
||||
// 如果没有BLE通信管理器或蓝牙不可用,直接断开BLE
|
||||
PerformBLEDisconnect(addressToDisconnect);
|
||||
}
|
||||
}
|
||||
@ -1000,7 +1039,9 @@ namespace Kill.Bluetooth
|
||||
|
||||
// ChangeAimMac 切换设备时是 Disconnect + StartScan 同步调用,
|
||||
// 本次清理会停掉刚启动的扫描,这里若仍有连接目标则自动重新扫描,避免连接流程中断
|
||||
if (!string.IsNullOrEmpty(aimMac) && !IsConnected)
|
||||
// 蓝牙已被系统关闭时不重扫(扫描只会触发一轮失败的初始化),
|
||||
// 等蓝牙重新打开后由 MonitorBluetoothState 的自动初始化流程接管
|
||||
if (!string.IsNullOrEmpty(aimMac) && !IsConnected && CheckIfBluetoothIsOn())
|
||||
{
|
||||
StartScan(10);
|
||||
}
|
||||
|
||||
@ -199,7 +199,7 @@ namespace Kill.Bluetooth
|
||||
|
||||
private IEnumerator StartUpgradeCoroutineInternal(BLEFrame frame, Action<bool> callback)
|
||||
{
|
||||
BLEResponse response = new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR };
|
||||
BLEResponse response = new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR, Data = new byte[0] };
|
||||
bool completed = false;
|
||||
|
||||
SendFrame(frame, (resp) =>
|
||||
@ -495,7 +495,7 @@ namespace Kill.Bluetooth
|
||||
private IEnumerator SendFrameCoroutine(BLEFrame frame, Action<BLEResponse> callback)
|
||||
{
|
||||
bool completed = false;
|
||||
BLEResponse response = new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR };
|
||||
BLEResponse response = new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR, Data = new byte[0] };
|
||||
|
||||
SendFrame(frame, (resp) =>
|
||||
{
|
||||
|
||||
@ -494,6 +494,8 @@ namespace Kill.Bluetooth.Protocol
|
||||
public static BLEResponse FromFrame(BLEFrame frame)
|
||||
{
|
||||
var response = new BLEResponse();
|
||||
// 无载荷的响应(如写ACK)也保证 Data 非空,避免回调中 BitConverter.ToString(null) 崩溃
|
||||
response.Data = new byte[0];
|
||||
if (frame.Data != null && frame.Data.Length > 0)
|
||||
{
|
||||
response.Status = frame.Data[0];
|
||||
|
||||
@ -1,296 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using Firebase.Extensions;
|
||||
namespace Kill.Managers
|
||||
{
|
||||
public class FireBaseCtrl : MonoBehaviour
|
||||
{
|
||||
Firebase.Auth.FirebaseAuth auth;
|
||||
Firebase.Auth.FirebaseUser user;
|
||||
Firebase.FirebaseApp app;
|
||||
public static FireBaseCtrl Instance;
|
||||
Action<bool, string> loginStatusCallBack;
|
||||
|
||||
// 登录状态属性
|
||||
public bool IsLoggedIn { get; private set; } = false;
|
||||
public string CurrentUserId { get; private set; } = "";
|
||||
|
||||
// 添加网络状态和初始化状态
|
||||
public bool IsInitialized { get; private set; } = false;
|
||||
public bool IsNetworkAvailable { get; private set; } = true; // 默认假设网络可用
|
||||
|
||||
// 添加登录状态枚举
|
||||
public enum LoginState
|
||||
{
|
||||
Unknown, // 未知状态
|
||||
NotLoggedIn, // 用户未登录
|
||||
LoggedIn, // 用户已登录
|
||||
Initializing, // 正在初始化
|
||||
NetworkError, // 网络错误
|
||||
AuthPending // 认证正在进行中
|
||||
}
|
||||
|
||||
public LoginState CurrentLoginState { get; private set; } = LoginState.Unknown;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
public async Task Init()
|
||||
{
|
||||
|
||||
await Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task =>
|
||||
{
|
||||
var dependencyStatus = task.Result;
|
||||
if (dependencyStatus == Firebase.DependencyStatus.Available)
|
||||
{
|
||||
// Create and hold a reference to your FirebaseApp,
|
||||
// where app is a Firebase.FirebaseApp property of your application class.
|
||||
app = Firebase.FirebaseApp.DefaultInstance;
|
||||
|
||||
// Set a flag here to indicate whether Firebase is ready to use by your app.
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError(string.Format(
|
||||
"===Could not resolve all Firebase dependencies: {0}", dependencyStatus));
|
||||
// Firebase Unity SDK is not safe to use here.
|
||||
}
|
||||
});
|
||||
CurrentLoginState = LoginState.Initializing;
|
||||
|
||||
try
|
||||
{
|
||||
// 检查Firebase依赖
|
||||
var dependencyStatus = await Firebase.FirebaseApp.CheckAndFixDependenciesAsync();
|
||||
|
||||
if (dependencyStatus == Firebase.DependencyStatus.Available)
|
||||
{
|
||||
auth = Firebase.Auth.FirebaseAuth.DefaultInstance;
|
||||
auth.StateChanged += AuthStateChanged;
|
||||
AuthStateChanged(this, null);
|
||||
IsInitialized = true;
|
||||
Debug.Log("===FirebaseCtrl Init");
|
||||
|
||||
// 检查初始状态
|
||||
CheckInitialState();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"===Firebase依赖不可用: {dependencyStatus}");
|
||||
CurrentLoginState = LoginState.NetworkError;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"===Firebase初始化失败: {ex.Message}");
|
||||
CurrentLoginState = LoginState.NetworkError;
|
||||
}
|
||||
}
|
||||
|
||||
void CheckInitialState()
|
||||
{
|
||||
// 检查是否有当前用户
|
||||
if (auth.CurrentUser != null)
|
||||
{
|
||||
CurrentLoginState = LoginState.LoggedIn;
|
||||
IsLoggedIn = true;
|
||||
CurrentUserId = auth.CurrentUser.UserId;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentLoginState = LoginState.NotLoggedIn;
|
||||
IsLoggedIn = false;
|
||||
CurrentUserId = "";
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLoginStatusCallBack(Action<bool, string> callBack)
|
||||
{
|
||||
loginStatusCallBack = callBack;
|
||||
if (user != null)
|
||||
loginStatusCallBack?.Invoke(true, user.UserId);
|
||||
}
|
||||
|
||||
void AuthStateChanged(object sender, System.EventArgs eventArgs)
|
||||
{
|
||||
if (auth.CurrentUser != user)
|
||||
{
|
||||
bool signedIn = user != auth.CurrentUser && auth.CurrentUser != null;
|
||||
if (!signedIn && user != null)
|
||||
{
|
||||
Debug.Log("===用户登出 " + user.UserId);
|
||||
IsLoggedIn = false;
|
||||
CurrentUserId = "";
|
||||
CurrentLoginState = LoginState.NotLoggedIn;
|
||||
loginStatusCallBack?.Invoke(false, user.UserId);
|
||||
}
|
||||
user = auth.CurrentUser;
|
||||
if (signedIn)
|
||||
{
|
||||
Debug.Log("===用户登录 " + user.UserId);
|
||||
IsLoggedIn = true;
|
||||
CurrentUserId = user.UserId;
|
||||
CurrentLoginState = LoginState.LoggedIn;
|
||||
loginStatusCallBack?.Invoke(true, user.UserId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 同步检查当前登录状态的方法
|
||||
public bool CheckLoginStatus()
|
||||
{
|
||||
return auth != null && auth.CurrentUser != null;
|
||||
}
|
||||
|
||||
// 更详细的登录状态检查方法
|
||||
public LoginStateDetail GetDetailedLoginStatus()
|
||||
{
|
||||
var detail = new LoginStateDetail();
|
||||
|
||||
// 检查Firebase是否已初始化
|
||||
if (!IsInitialized)
|
||||
{
|
||||
detail.State = LoginState.Initializing;
|
||||
detail.Description = "Firebase正在初始化";
|
||||
return detail;
|
||||
}
|
||||
|
||||
// 检查网络状态
|
||||
if (!IsNetworkAvailable)
|
||||
{
|
||||
detail.State = LoginState.NetworkError;
|
||||
detail.Description = "网络不可用,请检查网络连接";
|
||||
return detail;
|
||||
}
|
||||
|
||||
// 检查认证状态
|
||||
if (auth != null)
|
||||
{
|
||||
if (auth.CurrentUser != null)
|
||||
{
|
||||
detail.State = LoginState.LoggedIn;
|
||||
detail.Description = "用户已登录";
|
||||
detail.UserId = auth.CurrentUser.UserId;
|
||||
detail.DisplayName = auth.CurrentUser.DisplayName;
|
||||
}
|
||||
else
|
||||
{
|
||||
detail.State = LoginState.NotLoggedIn;
|
||||
detail.Description = "用户未登录";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
detail.State = LoginState.Unknown;
|
||||
detail.Description = "认证服务未准备好";
|
||||
}
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
public void CreatUserByEmail(string email, string password, Action<bool, string> callBack)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
{
|
||||
callBack(false, "服务未初始化完成,请稍后重试");
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentLoginState = LoginState.AuthPending;
|
||||
|
||||
auth.CreateUserWithEmailAndPasswordAsync(email, password).ContinueWith(task =>
|
||||
{
|
||||
if (task.IsCanceled)
|
||||
{
|
||||
Debug.Log("===取消创建用户");
|
||||
CurrentLoginState = LoginState.NotLoggedIn;
|
||||
callBack(false, "取消创建用户");
|
||||
return;
|
||||
}
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
Debug.Log("===创建用户失败 " + task.Exception);
|
||||
callBack(false, GetFriendlyErrorMessage(task.Exception));
|
||||
return;
|
||||
}
|
||||
|
||||
Firebase.Auth.AuthResult result = task.Result;
|
||||
CurrentLoginState = LoginState.LoggedIn;
|
||||
callBack(true, "");
|
||||
Debug.LogFormat("===创建用户成功: {0} ({1})",
|
||||
result.User.DisplayName, result.User.UserId);
|
||||
});
|
||||
}
|
||||
|
||||
public void LoginWithEmail(string email, string password, Action<bool, string> callBack)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
{
|
||||
callBack(false, "服务未初始化完成,请稍后重试");
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentLoginState = LoginState.AuthPending;
|
||||
|
||||
auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task =>
|
||||
{
|
||||
if (task.IsCanceled)
|
||||
{
|
||||
Debug.Log("===登录取消");
|
||||
CurrentLoginState = LoginState.NotLoggedIn;
|
||||
callBack(false, "登录取消");
|
||||
return;
|
||||
}
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
string errorMessage = GetFriendlyErrorMessage(task.Exception);
|
||||
Debug.Log("===登录出错" + task.Exception);
|
||||
callBack(false, errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
Firebase.Auth.AuthResult result = task.Result;
|
||||
CurrentLoginState = LoginState.LoggedIn;
|
||||
callBack(true, result.User.UserId);
|
||||
Debug.LogFormat("登录成功: {0} ({1})",
|
||||
string.IsNullOrEmpty(result.User.DisplayName) ? "" : result.User.DisplayName,
|
||||
result.User.UserId);
|
||||
});
|
||||
}
|
||||
|
||||
public void Logout()
|
||||
{
|
||||
auth.SignOut();
|
||||
CurrentLoginState = LoginState.NotLoggedIn;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 获取友好的错误信息
|
||||
string GetFriendlyErrorMessage(Exception exception)
|
||||
{
|
||||
if (exception == null) return "未知错误";
|
||||
|
||||
var firebaseEx = exception.GetBaseException() as Firebase.FirebaseException;
|
||||
return $"{firebaseEx.ErrorCode}:{firebaseEx.Message}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 登录状态详细信息类
|
||||
public class LoginStateDetail
|
||||
{
|
||||
public FireBaseCtrl.LoginState State { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b783fd033f637a4ca20115bfeeee4fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -3,7 +3,9 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
using UnityEngine;
|
||||
|
||||
namespace Kill.Managers
|
||||
|
||||
@ -60,6 +60,7 @@ namespace Kill.UI.Pages
|
||||
private int TOTAL_STATUS_COUNT = 8; // 总共需要查询的状态数量
|
||||
private float statusQueryTimeout = 15f; // 状态查询超时时间(秒)
|
||||
private Coroutine statusQueryTimeoutCoroutine; // 超时协程
|
||||
private Coroutine deviceStatusQueryCoroutine; // 首次状态全量查询协程
|
||||
|
||||
// 串行查询相关
|
||||
private Queue<Action> statusQueryQueue = new Queue<Action>(); // 状态查询队列
|
||||
@ -286,6 +287,12 @@ namespace Kill.UI.Pages
|
||||
/// </summary>
|
||||
private void StartDeviceStateInitialization()
|
||||
{
|
||||
if (deviceStatusQueryCoroutine != null)
|
||||
{
|
||||
StopCoroutine(deviceStatusQueryCoroutine);
|
||||
deviceStatusQueryCoroutine = null;
|
||||
}
|
||||
|
||||
isDeviceStateInitializing = true;
|
||||
pendingStatusCount = TOTAL_STATUS_COUNT;
|
||||
|
||||
@ -334,6 +341,11 @@ namespace Kill.UI.Pages
|
||||
StopCoroutine(statusQueryTimeoutCoroutine);
|
||||
statusQueryTimeoutCoroutine = null;
|
||||
}
|
||||
if (deviceStatusQueryCoroutine != null)
|
||||
{
|
||||
StopCoroutine(deviceStatusQueryCoroutine);
|
||||
deviceStatusQueryCoroutine = null;
|
||||
}
|
||||
bluetoothDeviceInfoCoroutine=StartCoroutine(CheckDeviceError());
|
||||
Debug.Log("[HomePageCtrl] 设备状态初始化完成");
|
||||
}
|
||||
@ -363,6 +375,11 @@ namespace Kill.UI.Pages
|
||||
isBluetoothUserLoggedIn = false;
|
||||
isQueryingStatus = false;
|
||||
statusQueryQueue?.Clear();
|
||||
if (deviceStatusQueryCoroutine != null)
|
||||
{
|
||||
StopCoroutine(deviceStatusQueryCoroutine);
|
||||
deviceStatusQueryCoroutine = null;
|
||||
}
|
||||
UIManager.Instance?.ClearBackAction();
|
||||
// 刷新首页设备连接状态UI(延迟到主循环执行,与连接成功时保持一致)
|
||||
pendingUIUpdates.Add(() =>
|
||||
@ -372,6 +389,7 @@ namespace Kill.UI.Pages
|
||||
if(bluetoothDeviceInfoCoroutine!=null)
|
||||
{
|
||||
StopCoroutine(bluetoothDeviceInfoCoroutine);
|
||||
bluetoothDeviceInfoCoroutine = null;
|
||||
}
|
||||
// 设备断开重新加载主页(保持原行为,当前在主页也会重新加载)
|
||||
// 但切换设备(ChangeAimMac)导致的断开不重新加载,避免主页被销毁重建导致并发初始化
|
||||
@ -398,7 +416,7 @@ namespace Kill.UI.Pages
|
||||
StartDeviceStateInitialization();
|
||||
|
||||
// 串行查询设备状态
|
||||
StartCoroutine(QueryAllDeviceStatusSequential());
|
||||
deviceStatusQueryCoroutine = StartCoroutine(QueryAllDeviceStatusSequential());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -493,9 +511,13 @@ namespace Kill.UI.Pages
|
||||
/// <summary>
|
||||
/// 按 MAC 隔离:当前设备不匹配(已切换/未连接/选中设备不同)则丢弃通知
|
||||
/// </summary>
|
||||
private bool IsNotificationForCurrentDevice()
|
||||
private bool IsNotificationForCurrentDevice(BLEDeviceState state = null)
|
||||
{
|
||||
string mac = BLECommunicationManager.Instance?.GetCurrentDeviceMac();
|
||||
string mac = state?.DeviceMac;
|
||||
if (string.IsNullOrEmpty(mac))
|
||||
{
|
||||
mac = BLECommunicationManager.Instance?.GetCurrentDeviceMac();
|
||||
}
|
||||
string selectedMac = selectedDevice?.ble_mac;
|
||||
if (string.IsNullOrEmpty(mac) || string.IsNullOrEmpty(selectedMac)) return false;
|
||||
return string.Equals(mac, selectedMac, StringComparison.OrdinalIgnoreCase);
|
||||
@ -509,7 +531,7 @@ namespace Kill.UI.Pages
|
||||
private void OnDeviceStateChanged(BLEDeviceState state, BLEDeviceStateField field)
|
||||
{
|
||||
if (state == null) return;
|
||||
if (!IsNotificationForCurrentDevice()) return;
|
||||
if (!IsNotificationForCurrentDevice(state)) return;
|
||||
if (isDeviceStateInitializing) return;
|
||||
|
||||
switch (field)
|
||||
@ -519,7 +541,10 @@ namespace Kill.UI.Pages
|
||||
{
|
||||
int mode = (int)state.WorkMode.Value;
|
||||
currentWorkMode = state.WorkMode.Value;
|
||||
if (DataManager.Instance.deviceConfig != null)
|
||||
{
|
||||
DataManager.Instance.deviceConfig.work_mode = DeviceConfig.ToServerString(state.WorkMode.Value);
|
||||
}
|
||||
pendingUIUpdates.Add(() => deviceCtrl?.InitWrokMode(mode));
|
||||
}
|
||||
break;
|
||||
@ -575,13 +600,16 @@ namespace Kill.UI.Pages
|
||||
break;
|
||||
case BLEDeviceStateField.Language:
|
||||
if (state.Language.HasValue)
|
||||
{
|
||||
if (DataManager.Instance.deviceConfig != null)
|
||||
{
|
||||
DataManager.Instance.deviceConfig.language = state.Language.Value.IsChinese ? 1 : 0;
|
||||
DataManager.Instance.SyncDeviceConfigToServer();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// 其他字段(Connection / LockState / HardwareStatus / CapacitorState / FillLightConnection)暂不在主页直接刷新
|
||||
// 其他字段(Connection / HardwareStatus / CapacitorState / FillLightConnection)暂不在主页直接刷新
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -977,6 +1005,24 @@ namespace Kill.UI.Pages
|
||||
}
|
||||
|
||||
public async Task Init()
|
||||
{
|
||||
// Init 在 async 链中抛异常会被静默吞掉:LoadingUI 引用计数无法归零(表现为卡 loading),
|
||||
// 且页面状态刷新(UpdateDeviceState)不会执行。这里统一兜底:记录完整异常并保证 Loading 收尾。
|
||||
try
|
||||
{
|
||||
await InitInternal();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[HomePageCtrl] Init 异常: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingUI.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitInternal()
|
||||
{
|
||||
|
||||
if (DataManager.Instance.userInfo.device_count == 0 || (OwnedDevices.Count == 0 && SharedDevices.Count == 0))
|
||||
|
||||
135
README.md
135
README.md
@ -1,3 +1,134 @@
|
||||
# killapp
|
||||
# killapp —— 激光灭蚊设备控制 App
|
||||
|
||||
灭蚊app
|
||||
控制「激光灭害虫设备」(photonmatrix)的配套手机 App,Unity 制作,Android + iOS 双平台。
|
||||
用户通过 App 完成设备绑定(BLE 扫描 + 扫码)、配网、日常控制(工作模式 / FOV / 透镜 / 补光灯 / 定时任务)、
|
||||
杀蚊数据统计(柱状图 / 雷达热力图 / 杀蚊视频)、固件 OTA 升级、设备共享等。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈与环境
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| Unity | 2022.3.62f3c1 |
|
||||
| 应用包名 | `com.photonmatrix.photonmatrix`(Android / iOS 相同) |
|
||||
| 当前版本 | bundleVersion 1.0.7,AndroidBundleVersionCode 4(均在 `ProjectSettings/ProjectSettings.asset`) |
|
||||
| UI 方案 | UGUI + DOTween,无 Addressables |
|
||||
| 热更方案 | 自研 AssetBundle 热更(`LoadRes` + `NetworkCtrl`) |
|
||||
| 第三方 | Firebase SDK(仅 Auth)、apple-signin-unity、Shatalmic BLE 插件、zxing、NativeGallery、ExternalDependencyManager |
|
||||
| 主场景 | `Assets/Scenes/MainScene.unity`(唯一业务场景);`BlueToothTest.unity` 为蓝牙调试场景 |
|
||||
|
||||
`Packages/manifest.json` 非常精简(ugui、timeline、visualscripting、assetbundlebrowser、android-logcat 等),
|
||||
Firebase 是以 Assets 方式导入而非 UPM。
|
||||
|
||||
## 整体架构
|
||||
|
||||
**单场景 + 常驻 Manager + AssetBundle 动态页面**:
|
||||
|
||||
- `MainScene` 的 `Manager` 节点挂载全部管理器:`NetworkCtrl`、`DataManager`、`UIManager`、`LoadRes`、`LanguageManager`、`FirebaseAuthManager`。
|
||||
- `Canvas/bg` 是页面容器,页面按 prefab 从 AssetBundle 动态加载实例化(旧的销毁、新的实例化)。
|
||||
- 页面逻辑全部写在 prefab 挂的 Ctrl 类里,各单例之间直接互相引用(`HomePageCtrl.Instance`、`DataManager.Instance` 等)。
|
||||
|
||||
**启动流程**(`NetworkCtrl.Init()`):
|
||||
`LoadRes.Init`(AB 环境)→ 创建 `HttpRequestManager` / `RequestQueueManager` / `NetworkStateManager` →
|
||||
从服务器查 AB 版本并差量热更下载(MD5 校验)→ `DataManager.Init`(token 自动登录)→ `UIManager.Init`(有 token 进主页,否则进登录页)。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
Assets/
|
||||
├── Scenes/ MainScene(主场景)、BlueToothTest(蓝牙调试)
|
||||
├── Scripts/
|
||||
│ ├── Bluetooth/ BLE 核心(当前开发重点)
|
||||
│ │ ├── BluetoothManager.cs Shatalmic 插件封装:扫描/连接/断开事件
|
||||
│ │ ├── BLECommunicationManager.cs 协议层:指令收发、事件分发(全项目最大文件)
|
||||
│ │ ├── BLEDeviceState.cs 按 MAC 隔离的设备运行状态模型
|
||||
│ │ ├── OTAManager.cs 固件版本解析与升级传输
|
||||
│ │ ├── Protocol/ 帧结构体 BLEProtocolStructs.cs + CRC16.cs
|
||||
│ │ └── BLE通信方案V2.2.md BLE 协议文档(App ↔ BLE 透传 ↔ STM32H7 MCU)
|
||||
│ ├── Core/ Loom(BLE 回调线程切主线程)、状态栏沉浸、安卓文件选择
|
||||
│ ├── Managers/ DataManager / DataBase(DTO) / FirebaseAuthManager / LanguageManager / LoadRes / FireBaseCtrl(死代码)
|
||||
│ ├── Network/ NetworkCtrl(门面+启动编排) / HttpRequestManager / RequestQueueManager / NetworkStateManager
|
||||
│ ├── UI/
|
||||
│ │ ├── UIManager.cs 页面管理、安卓返回键、token 自动登录
|
||||
│ │ ├── Pages/ 各页面逻辑(Home / Login / ConnectDevice / DeviceInfo / Video / Self / Rank / Safetylearning)
|
||||
│ │ └── Components/ Toast / Loading / Barchart / RadarHeatmap / SectorScanEffect / QRCode 等通用控件
|
||||
│ └── Utils/ MainThread、ValidationUtils、iOSWiFiHelper
|
||||
├── Res/ AB 包源资源(bluetooth / common / language / ui)
|
||||
├── StreamingAssets/ 首包 AB:common.ab、language.ab、ui_common.ab + list.json
|
||||
├── Plugins/ Shatalmic 运行脚本、NativeGallery、DOTween、zxing、iOS 原生桥(.mm)
|
||||
├── AppleAuth/ Sign in with Apple
|
||||
└── Firebase/ Firebase SDK(实际只用 Auth)
|
||||
```
|
||||
|
||||
## 核心模块备忘
|
||||
|
||||
### 蓝牙 BLE(开发重点)
|
||||
|
||||
- 链路:App ↔ BLE 模块(透传,Service `FFE0`,读/写/Notify 均走 `FFE1`)↔ MCU(UART9)。
|
||||
- `BLECommunicationManager` 是几十条指令的收发与事件分发中心:
|
||||
注册/指纹认证、定时任务、语言、LCD/RGB、WiFi 配网、补光灯、激光、角度/距离、
|
||||
毫米波雷达、视觉检测、工作模式、硬件状态、统计数据、蚊子数据等。
|
||||
每条指令对应 `XxxRead` / `XxxWrite` 方法 + `OnXxxReceived` 事件。
|
||||
- 设备**主动通知**(状态变化 0x01 / 错误 0x02 / 参数变化 0x06 / 蓄能 0x08 / 补光灯连接 0x09 等)
|
||||
汇入 `OnDeviceNotificationReceived`,再**按设备 MAC 隔离**后广播;分支名
|
||||
`feature/ble-config-notification` 即指这套「配置变更主动上报」机制。
|
||||
- 设备状态保存在 `BLEDeviceState`(按 MAC 为边界),统一通过
|
||||
`OnDeviceStateChanged(BLEDeviceState, BLEDeviceStateField)` 通知 UI。
|
||||
- Shatalmic 回调来自非主线程,UI 相关处理必须 `Loom.QueueOnMainThread` 切回主线程。
|
||||
- Android/iOS 无法直接取 BLE 真实 MAC,设备 MAC 通过 BLE 命令从设备端获取(设备序列号用于绑定)。
|
||||
|
||||
### 网络与后端
|
||||
|
||||
- **服务器地址**(配置在 MainScene 里 `NetworkCtrl.serverBaseUrls`):
|
||||
测试 `https://nextreal.cn/photon-matrix-api`,正式 `https://api.photonmatrixlab.com`。
|
||||
**切换后门**:首页左下角连点 5 次(`enableServerBackdoor` 控制开关,发布前应置 0);
|
||||
环境持久化在 PlayerPrefs `network_server_env`。
|
||||
- `HttpRequestManager`:UnityWebRequest 封装,重试(指数退避)、**SSE 流式**(WiFi OTA 进度)、多平台超时。
|
||||
- `RequestQueueManager`:优先级请求队列,并发上限 5。
|
||||
- `NetworkStateManager`:每 3 秒轮询 `internetReachability`,发连接/断开事件。
|
||||
- `ResponseCode`:业务码统一处理,**601 = Token 失效 → 全局登出**。
|
||||
- 主要接口(`{Base}/api/v1/...`):认证(login/register/email-code/token-login/firebase-bind)、
|
||||
设备(bind/list/config/schedule-tasks/fingerprints/share/command)、统计上报与查询
|
||||
(kill-count/records/videos/heatmap/leaderboard)、通知、OTA(latest/push/transfer 进度 SSE)、
|
||||
考试、反馈、`app/version/bundle`(AB 热更清单)。
|
||||
|
||||
### 数据与账号
|
||||
|
||||
- `DataManager`:运行时数据中枢(token、用户信息、自有/共享设备、当前选中设备、设备配置、指纹、定时任务)。
|
||||
PlayerPrefs 持久化:`token`、`userData`、`selectedDeviceMac`、设备列表缓存 `ownedDevicesCache_<userId>` 等。
|
||||
- `DataBase.cs`:纯 DTO 定义(登录/用户/设备/配置/OTA/消息/统计/考试…),非 Mono。
|
||||
- 第三方登录:`FirebaseAuthManager`(Google 用 FederatedOAuthProvider,Apple 用 apple-signin-unity + nonce),
|
||||
结果换自家后端 token(`auth/firebase/bind`)。旧版 `FireBaseCtrl.cs` 已无任何引用,属死代码。
|
||||
|
||||
### UI 框架
|
||||
|
||||
- `UIManager.PageName` 枚举 8 个主页面:login / safetylearning / home / connectDevice / deviceInfo / self / video / rank。
|
||||
- 页面 prefab 按 bundle 名 `ui_xxxpage` 从 AB 加载(配置在 MainScene 的 UIManager `pages` 列表)。
|
||||
- 设置类子弹窗不走 UIManager,由各 Ctrl 自己 Instantiate + `RegisterBackAction`(返回键栈,双击退出)。
|
||||
- 首页 `HomePageCtrl` 是功能核心:蓝牙/WiFi 双通道控制、状态订阅、
|
||||
**锁扣防误触(LockButtonPlane,180 秒无操作自动上锁)**、下拉刷新、统计上报、跳转子页面。
|
||||
|
||||
### 资源与热更
|
||||
|
||||
- `LoadRes`:编辑器「本地」模式直读 `Assets/Res/`;真机「资源包」模式优先 `persistentDataPath/*.ab`(热更落盘),
|
||||
回退 StreamingAssets 首包;bundle 缓存在内存字典。
|
||||
- 热更清单来自 `app/version/bundle`,由 `NetworkCtrl` 做差量下载 + MD5 校验,进度显示在 `updateLoading`。
|
||||
- 语言词条也在 AB 里:`language/language.json`、`country.json`(`LanguageManager` 读取,PlayerPrefs `LanguageType`)。
|
||||
|
||||
### 其他关键组件
|
||||
|
||||
- `Loom`:线程池 + 主线程队列派发(BLE 回调切主线程用)。`Utils/MainThread.cs` 功能重复,并存。
|
||||
- `RadarHeatmap`:按角度(0-180)+距离换算极坐标摆放热力点(数据来自 `stats/device/heatmap`)。
|
||||
- `SectorScanEffect`:首页扇形扫描波纹动画(fillAmount 表现 FOV、缩放表现探测距离)。
|
||||
- `iOSWiFiHelper`:iOS14+ 取当前 WiFi SSID(需定位权限),配网页自动填 SSID;Android 直接读。
|
||||
- `NativeGallery`:杀蚊视频保存到系统相册。
|
||||
|
||||
## 构建与发布备忘
|
||||
|
||||
- 版本号:`ProjectSettings/ProjectSettings.asset` 的 `bundleVersion` / `AndroidBundleVersionCode`。
|
||||
- Android 签名:`user.keystore`(项目根目录)。
|
||||
- iOS:`ITSAppUsesNonExemptEncryption` 已配置,避免每次手动答加密问卷(commit 457fee5)。
|
||||
- 根目录的 `*.apk` / `*_mapping.txt` 为历史构建产物(含 Release 符号表),命名习惯如
|
||||
`photonmatrix_1.0.7_0903.apk`;测试构建会带后缀如 `ota蓝牙优先`。
|
||||
- 打正式包前检查:`NetworkCtrl.enableServerBackdoor` 应为 0(场景内字段)。
|
||||
Loading…
x
Reference in New Issue
Block a user