# 手机端-设备端 动态绑定方案设计文档

## 1. 现状分析

### 1.1 当前架构

系统由三个项目组成：

| 项目 | 路径 | 角色 |
|------|------|------|
| Django 服务器 | `qy_lty-main/` | 后端 API + WebSocket 中转 |
| Unity 手机端 | `LTY_App_Project_URP/` | 用户操控 App |
| Unity 设备端 | `LTY_Project/` | 实体硬件设备程序 |

### 1.2 当前通信流程

```
手机端 WebSocketNetworking.cs          服务器                    设备端 WebSocketConnection.cs
        │                                │                              │
        │  POST /api/user/auth/mac/login/│                              │
        │  mac_address="00:55:11:44:f3:22"                              │
        │──────────────────────────────>│                              │
        │           token_A             │                              │
        │<──────────────────────────────│                              │
        │                                │  POST /api/user/auth/mac/login/
        │                                │  mac_address="00:55:11:44:f3:22"
        │                                │<──────────────────────────────│
        │                                │           token_B             │
        │                                │──────────────────────────────>│
        │                                │                              │
        │  WSS /ws/device/token/{token_A}│                              │
        │──────────────────────────────>│  WSS /ws/device/token/{token_B}
        │       加入 group: device_{uid} │<──────────────────────────────│
        │                                │       加入 group: device_{uid}
        │                                │                              │
        │          ← 同一个 group，消息互通 →                            │
```

### 1.3 核心问题

1. **MAC 地址硬编码**：手机端 `WebSocketNetworking.cs` 和设备端 `WebSocketConnection.cs` 都写死了 `macAddress = "00:55:11:44:f3:22"`
2. **手机端不应该用 MAC 登录**：手机端是用户操作的 App，应使用手机号/账号登录，而非设备 MAC 地址登录
3. **绑定关系是假的**：两端碰巧用同一个 MAC → 查到同一个 User → 进入同一个 WebSocket group，并非真正的动态绑定
4. **无法支持多设备**：所有设备都用同一个 MAC，无法区分
5. **手机端绑定 UI 是空壳**：`DeviceDetailsViewMediator.cs` 中解绑按钮只有 `Debug.Log("解绑设备")`

---

## 2. 目标设计

### 2.1 设计原则

- **手机端用用户身份登录**，不再用 MAC 地址登录
- **设备端用自身真实 MAC 地址登录**，每台设备有唯一身份
- **通过服务器绑定 API 建立用户-设备关联**
- **WebSocket 消息基于绑定关系精准路由**
- **支持一个用户绑定多台设备，一台设备只能绑定一个用户**

### 2.2 目标流程总览

```
┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   手机端     │     │    服务器     │     │   设备端     │
│  (用户 App)  │     │   (Django)   │     │ (实体硬件)   │
└──────┬──────┘     └──────┬───────┘     └──────┬──────┘
       │                    │                     │
  ①用户登录(手机号)          │                     │
       │──────────────────>│                     │
       │    user_token      │                     │
       │<──────────────────│                     │
       │                    │           ②设备开机，用真实MAC登录
       │                    │<────────────────────│
       │                    │     device_token     │
       │                    │────────────────────>│
       │                    │                     │
  ③蓝牙扫描发现设备          │            ④设备连WebSocket
       │                    │<════════════════════│
       │                    │  ws/device/token/{device_token}
  ⑤调用绑定API               │                     │
  (传入扫描到的MAC)           │                     │
       │──────────────────>│                     │
       │   绑定成功          │                     │
       │<──────────────────│                     │
       │                    │                     │
  ⑥手机连WebSocket           │                     │
       │═══════════════════>│                     │
       │  ws/device/token/{user_token}            │
       │                    │                     │
  ⑦发送指令(touch/dance等)    │                     │
       │──────────────────>│   转发到设备          │
       │                    │────────────────────>│
       │                    │   设备回报状态        │
       │    转发到手机       │<────────────────────│
       │<──────────────────│                     │
```

---

## 3. 服务器端改动

### 3.1 现有可复用部分（无需改动）

服务器已有完整的数据模型和绑定 API，以下部分 **可直接使用**：

| 组件 | 文件 | 说明 |
|------|------|------|
| Device 模型 | `device_interaction/models.py` | `mac_address` 唯一字段已存在 |
| UserDevice 模型 | `device_interaction/models.py` | 用户-设备绑定关系已存在 |
| 绑定 API | `device_interaction/views.py` → `UserDeviceViewSet.bind()` | `POST /api/device/user-devices/bind/` 已实现 |
| 解绑 API | `device_interaction/views.py` → `UserDeviceViewSet.destroy()` | `DELETE /api/device/user-devices/{id}/` 已实现 |
| 设备列表 | `device_interaction/views.py` → `UserDeviceViewSet.list()` | 已能查询用户绑定的设备 |
| MAC 登录 | `userapp/views.py` → `MacAddressLoginView` | 设备端 MAC 登录已实现 |
| 用户登录 | `userapp/views.py` → `PhoneLoginView` 等 | 手机号/邮箱/用户名登录已实现 |

### 3.2 需要修改：WebSocket Consumer 消息路由

**问题**：当前 `DeviceConsumer` 使用 `device_{user_id}` 作为 group name，手机端和设备端如果是不同 user，无法进入同一个 group。

**改动思路**：设备端用 MAC 登录后获得的 token 对应一个"设备用户"，需要改为通过绑定关系找到实际用户，用实际用户的 group 通信。

#### 方案 A：设备连接时查绑定关系，加入绑定用户的 group（推荐）

修改 `device_interaction/consumers.py` 的 `connect()` 方法：

```python
class DeviceConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        try:
            # ... 现有认证逻辑保持不变 ...

            user = self.scope['user']
            self.user_id = str(user.id)

            # 新增：判断连接来源（设备端 vs 手机端）
            self.device_mac = self.scope.get('device_mac', None)
            self.is_device = False
            self.groups_joined = []

            if self.device_mac:
                # 设备端连接：通过绑定关系找到关联用户
                bound_user_id = await self.get_bound_user_id(self.device_mac)
                if bound_user_id:
                    self.is_device = True
                    group_name = f"device_{bound_user_id}"
                    self.groups_joined.append(group_name)
                else:
                    # 设备未绑定，加入自己的 group 等待绑定
                    group_name = f"device_unbound_{self.user_id}"
                    self.groups_joined.append(group_name)
            else:
                # 手机端连接：使用自己的 user_id
                group_name = f"device_{self.user_id}"
                self.groups_joined.append(group_name)

            self.group_name = group_name

            await self.channel_layer.group_add(
                self.group_name,
                self.channel_name
            )

            await self.accept()

        except Exception as e:
            logger.error(f"Error in WebSocket connect: {str(e)}")
            await self.close(code=4002)

    @database_sync_to_async
    def get_bound_user_id(self, mac_address):
        """通过 MAC 地址查找绑定的用户 ID"""
        try:
            from .models import Device, UserDevice
            device = Device.objects.get(mac_address=mac_address)
            user_device = UserDevice.objects.filter(device=device).first()
            if user_device:
                return str(user_device.user.id)
            return None
        except Device.DoesNotExist:
            return None
```

#### 方案 B（更简单）：MAC 登录时直接返回绑定用户的 token

修改 `MacAddressLoginView`，当设备 MAC 已绑定用户时，返回 **绑定用户的 token** 而非设备自身的 token。这样设备端用该 token 连 WebSocket 就自动进入正确的 group。

```python
# userapp/views.py - MacAddressLoginView.post()
class MacAddressLoginView(APIView):
    def post(self, request):
        mac_address = request.data.get('mac_address')

        # 查找设备
        device = Device.objects.filter(mac_address=mac_address).first()
        if not device:
            return error_response("设备不存在", code=404)

        # 查找绑定的用户
        user_device = UserDevice.objects.filter(device=device).first()
        if not user_device:
            return error_response("设备未绑定用户", code=400)

        # 用绑定用户的身份生成 token
        bound_user = user_device.user
        token = generate_token(bound_user)

        return success_response({
            "token": token,
            "user_id": bound_user.id,
            "device_code": device.device_code,
            "mac_address": mac_address,
            "bound": True
        })
```

> **推荐方案 B**，因为改动最小——只改 MAC 登录接口的返回逻辑，手机端和设备端的 WebSocket 连接代码无需改变，Consumer 也不用改。

### 3.3 需要新增：设备注册接口（可选）

当前设备必须预先在后台管理系统录入数据库。如果希望设备即插即用，可新增自动注册接口：

```python
# POST /api/device/devices/register/
# 设备首次开机时调用，如果 MAC 不存在则自动创建 Device 记录
{
    "mac_address": "AA:BB:CC:DD:EE:FF",
    "device_type_code": "T01",       # 设备类型代码
    "firmware_version": "1.0.0"
}
```

### 3.4 需要新增：绑定状态查询接口

供设备端查询自己是否已被绑定：

```python
# GET /api/device/devices/bind_status/?mac_address=AA:BB:CC:DD:EE:FF
# 返回：
{
    "bound": true,
    "user_id": 123,
    "nickname": "客厅的莉拉"
}
```

---

## 4. 设备端（LTY_Project）改动

### 4.1 获取真实 MAC 地址

替换硬编码 MAC，改为从设备硬件读取：

```csharp
// WebSocketConnection.cs

// 旧代码（删除）：
// private string macAddress = "00:55:11:44:f3:22";

// 新代码：
private string macAddress;

void Awake()
{
    macAddress = GetDeviceMacAddress();
    Debug.Log($"设备 MAC 地址: {macAddress}");
}

/// <summary>
/// 获取设备真实 MAC 地址
/// 优先级：蓝牙 MAC > WiFi MAC > 设备唯一标识
/// </summary>
private string GetDeviceMacAddress()
{
    // 方案1：从 Android 系统获取蓝牙 MAC（需要 Android 插件）
    #if UNITY_ANDROID && !UNITY_EDITOR
    try
    {
        using (var bluetoothManager = new AndroidJavaObject("android.bluetooth.BluetoothAdapter"))
        {
            var adapter = bluetoothManager.CallStatic<AndroidJavaObject>("getDefaultAdapter");
            if (adapter != null)
            {
                string mac = adapter.Call<string>("getAddress");
                if (!string.IsNullOrEmpty(mac) && mac != "02:00:00:00:00:00")
                    return mac.ToUpper();
            }
        }
    }
    catch (System.Exception e)
    {
        Debug.LogWarning($"获取蓝牙 MAC 失败: {e.Message}");
    }
    #endif

    // 方案2：从配置文件读取（工厂烧录时写入）
    string configPath = System.IO.Path.Combine(Application.persistentDataPath, "device_config.json");
    if (System.IO.File.Exists(configPath))
    {
        string json = System.IO.File.ReadAllText(configPath);
        var config = JsonUtility.FromJson<DeviceConfig>(json);
        if (!string.IsNullOrEmpty(config.mac_address))
            return config.mac_address;
    }

    // 方案3：使用设备唯一标识符生成伪 MAC（最后兜底方案）
    string deviceId = SystemInfo.deviceUniqueIdentifier;
    return GenerateMacFromId(deviceId);
}

private string GenerateMacFromId(string id)
{
    // 用设备 ID 的哈希值生成固定的 MAC 格式
    byte[] hash = System.Security.Cryptography.MD5.Create()
        .ComputeHash(System.Text.Encoding.UTF8.GetBytes(id));
    return string.Format("{0:X2}:{1:X2}:{2:X2}:{3:X2}:{4:X2}:{5:X2}",
        hash[0], hash[1], hash[2], hash[3], hash[4], hash[5]);
}

[System.Serializable]
private class DeviceConfig
{
    public string mac_address;
    public string device_type;
}
```

### 4.2 设备端启动流程改造

```csharp
// WebSocketConnection.cs 启动流程

private bool isBound = false;

IEnumerator Start()
{
    macAddress = GetDeviceMacAddress();

    // 步骤1：用 MAC 地址登录
    yield return StartCoroutine(LoginWithMac());

    if (string.IsNullOrEmpty(token))
    {
        Debug.LogError("MAC 登录失败，设备未注册或未绑定");
        // 进入"等待绑定"状态，显示设备 MAC / 二维码供手机扫描
        ShowWaitingForBind();
        yield break;
    }

    isBound = true;

    // 步骤2：连接 WebSocket
    ConnectWebSocket();
}

private void ShowWaitingForBind()
{
    // 在设备屏幕上显示 MAC 地址或二维码，供手机端扫描绑定
    Debug.Log($"等待绑定，设备 MAC: {macAddress}");
    // 可定时轮询绑定状态
    StartCoroutine(PollBindStatus());
}

IEnumerator PollBindStatus()
{
    while (!isBound)
    {
        string url = $"{httpBaseUrl}/api/device/devices/bind_status/?mac_address={macAddress}";
        using (UnityWebRequest request = UnityWebRequest.Get(url))
        {
            yield return request.SendWebRequest();
            if (request.result == UnityWebRequest.Result.Success)
            {
                var response = JsonUtility.FromJson<BindStatusResponse>(request.downloadHandler.text);
                if (response.bound)
                {
                    isBound = true;
                    // 重新登录获取绑定用户的 token
                    yield return StartCoroutine(LoginWithMac());
                    ConnectWebSocket();
                    yield break;
                }
            }
        }
        yield return new WaitForSeconds(5f); // 每5秒查一次
    }
}

[System.Serializable]
private class BindStatusResponse
{
    public bool bound;
    public int user_id;
    public string nickname;
}
```

### 4.3 getJson.cs MAC 地址改造

```csharp
// getJson.cs
// 旧代码（删除）：
// string url = "https://qy-lty.airlabs.art/api/device/rtc-token/get_by_mac/?mac_address=00:55:11:44:f3:22";

// 新代码：从 WebSocketConnection 获取动态 MAC
string mac = WebSocketConnection.Instance.MacAddress; // 需要暴露为公共属性
string url = $"https://qy-lty.airlabs.art/api/device/rtc-token/get_by_mac/?mac_address={mac}";
```

---

## 5. 手机端（LTY_App_Project_URP）改动

### 5.1 登录方式变更

手机端不再使用 MAC 地址登录，改为用户账号登录：

```csharp
// WebSocketNetworking.cs

// 旧代码（删除）：
// private string macAddress = "00:55:11:44:f3:22";
// LoginRequest loginRequest = new LoginRequest { mac_address = macAddress };
// 调用 /api/user/auth/mac/login/

// 新代码：使用手机号/账号登录获取的 token
// token 来自登录页面（PhoneLoginView / UsernameLoginView）
// 存储在全局管理器中

public class UserSession
{
    public static UserSession Instance { get; private set; }

    public string Token { get; private set; }
    public int UserId { get; private set; }
    public string Username { get; private set; }

    public void SetLogin(string token, int userId, string username)
    {
        Token = token;
        UserId = userId;
        Username = username;
    }

    public void Clear()
    {
        Token = null;
        UserId = 0;
        Username = null;
    }
}
```

### 5.2 WebSocket 连接改造

```csharp
// WebSocketNetworking.cs

// 使用用户 token 连接 WebSocket（不再需要先 MAC 登录）
public void ConnectWebSocket()
{
    string token = UserSession.Instance.Token;
    if (string.IsNullOrEmpty(token))
    {
        Debug.LogError("用户未登录，无法连接 WebSocket");
        return;
    }

    string wsUrl = $"{wsBaseUrl}/ws/device/token/{token}/";
    Debug.Log($"连接 WebSocket: {wsUrl}");

    webSocket = new WebSocket(new Uri(wsUrl));
    webSocket.OnOpen += OnWebSocketOpen;
    webSocket.OnMessage += OnMessageReceived;
    webSocket.OnError += OnError;
    webSocket.OnClosed += OnClosed;
    webSocket.Open();
}
```

### 5.3 蓝牙扫描 → 获取 MAC → 调用绑定 API

> **绑定入口**：绑定逻辑将在 **JarConnection_Panel**（"添加新罐子"按钮）中实现，参考 `BluetoothCentral.cs` 的蓝牙扫描逻辑，但不直接修改 `BluetoothCentral.cs`。

绑定 API 调用封装如下：

```csharp
// 新增：DeviceBindManager.cs
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class DeviceBindManager : MonoBehaviour
{
    public static DeviceBindManager Instance { get; private set; }

    private string baseUrl = "https://qy-lty.airlabs.art/api";

    void Awake()
    {
        Instance = this;
    }

    /// <summary>
    /// 绑定设备（蓝牙扫描到 MAC 后调用）
    /// </summary>
    public void BindDevice(string macAddress, string nickname = null,
        System.Action<UserDeviceInfo> onSuccess = null,
        System.Action<string> onError = null)
    {
        StartCoroutine(BindDeviceCoroutine(macAddress, nickname, onSuccess, onError));
    }

    private IEnumerator BindDeviceCoroutine(string macAddress, string nickname,
        System.Action<UserDeviceInfo> onSuccess, System.Action<string> onError)
    {
        string url = $"{baseUrl}/device/user-devices/bind/";
        string token = UserSession.Instance.Token;

        // 构建请求体
        var bindData = new BindRequest
        {
            mac_address = macAddress,
            nickname = nickname ?? "我的莉拉",
            is_primary = true
        };
        string jsonBody = JsonUtility.ToJson(bindData);

        using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
        {
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody);
            request.uploadHandler = new UploadHandlerRaw(bodyRaw);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");
            request.SetRequestHeader("Authorization", $"Bearer {token}");

            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                var response = JsonUtility.FromJson<BindResponse>(request.downloadHandler.text);
                Debug.Log($"设备绑定成功: {response.data.device_code}");
                onSuccess?.Invoke(response.data);
            }
            else
            {
                string error = request.downloadHandler.text;
                Debug.LogError($"设备绑定失败: {error}");
                onError?.Invoke(error);
            }
        }
    }

    /// <summary>
    /// 解绑设备
    /// </summary>
    public void UnbindDevice(int userDeviceId,
        System.Action onSuccess = null,
        System.Action<string> onError = null)
    {
        StartCoroutine(UnbindDeviceCoroutine(userDeviceId, onSuccess, onError));
    }

    private IEnumerator UnbindDeviceCoroutine(int userDeviceId,
        System.Action onSuccess, System.Action<string> onError)
    {
        string url = $"{baseUrl}/device/user-devices/{userDeviceId}/";
        string token = UserSession.Instance.Token;

        using (UnityWebRequest request = UnityWebRequest.Delete(url))
        {
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Authorization", $"Bearer {token}");

            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                Debug.Log("设备解绑成功");
                onSuccess?.Invoke();
            }
            else
            {
                onError?.Invoke(request.downloadHandler.text);
            }
        }
    }

    /// <summary>
    /// 获取已绑定设备列表
    /// </summary>
    public void GetBoundDevices(System.Action<UserDeviceInfo[]> onSuccess)
    {
        StartCoroutine(GetBoundDevicesCoroutine(onSuccess));
    }

    private IEnumerator GetBoundDevicesCoroutine(System.Action<UserDeviceInfo[]> onSuccess)
    {
        string url = $"{baseUrl}/device/user-devices/";
        string token = UserSession.Instance.Token;

        using (UnityWebRequest request = UnityWebRequest.Get(url))
        {
            request.SetRequestHeader("Authorization", $"Bearer {token}");

            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                var response = JsonUtility.FromJson<DeviceListResponse>(request.downloadHandler.text);
                onSuccess?.Invoke(response.results);
            }
        }
    }

    // ---- 数据类 ----

    [System.Serializable]
    public class BindRequest
    {
        public string mac_address;
        public string nickname;
        public bool is_primary;
    }

    [System.Serializable]
    public class BindResponse
    {
        public bool success;
        public UserDeviceInfo data;
    }

    [System.Serializable]
    public class UserDeviceInfo
    {
        public int id;
        public string device_code;
        public string device_type;
        public string device_status;
        public int battery_level;
        public string nickname;
        public bool is_primary;
        public string mac_address;
    }

    [System.Serializable]
    public class DeviceListResponse
    {
        public UserDeviceInfo[] results;
    }
}
```

### 5.4 绑定入口（JarConnection_Panel）

绑定逻辑将在 JarConnection_Panel（"添加新罐子"）中实现，核心调用模式：

```csharp
// JarConnection_Panel 中，蓝牙连接设备成功后调用绑定 API：
void OnDeviceConnectedForBind(string deviceMacAddress)
{
    string formattedMac = FormatMacAddress(deviceMacAddress);

    DeviceBindManager.Instance.BindDevice(
        macAddress: formattedMac,
        nickname: "我的莉拉",
        onSuccess: (deviceInfo) =>
        {
            Debug.Log($"绑定成功，设备码: {deviceInfo.device_code}");
            // 绑定成功后连接 WebSocket
            _ = WebSocketNetworking.instance.ConnectWebSocket();
        },
        onError: (error) => Debug.LogError($"绑定失败: {error}")
    );
}
```

### 5.5 DeviceDetailsViewMediator 对接解绑

```csharp
// DeviceDetailsViewMediator.cs

// 替换空壳的解绑按钮：
view.btnUnbind.onClick.AddListener(() =>
{
    // 确认弹窗（根据项目 UI 框架实现）
    // 确认后调用解绑
    int userDeviceId = currentDeviceId; // 从设备详情数据中获取
    DeviceBindManager.Instance.UnbindDevice(
        userDeviceId,
        onSuccess: () =>
        {
            Debug.Log("解绑成功");
            // 断开 WebSocket
            WebSocketNetworking.Instance.Disconnect();
            // 返回设备列表页
            SendNotification(PureNotification.HIDE_PANEL, this);
        },
        onError: (error) =>
        {
            Debug.LogError($"解绑失败: {error}");
        }
    );
});
```

---

## 6. 完整绑定流程时序图

### 6.1 首次绑定流程

```
用户                手机端App              服务器               设备端               蓝牙
 │                    │                     │                    │                   │
 │  打开App           │                     │                    │                   │
 │──────────────────>│                     │                    │                   │
 │                    │  手机号+验证码登录    │                    │                   │
 │                    │─────────────────────>│                    │                   │
 │                    │    user_token        │                    │                   │
 │                    │<─────────────────────│                    │                   │
 │                    │                     │                    │                   │
 │                    │                     │     设备开机        │                   │
 │                    │                     │    MAC登录          │                   │
 │                    │                     │<───────────────────│                   │
 │                    │                     │  返回"未绑定"错误    │                   │
 │                    │                     │───────────────────>│                   │
 │                    │                     │                    │ 显示等待绑定界面     │
 │                    │                     │                    │ (显示MAC/二维码)    │
 │                    │                     │                    │                   │
 │  点击"添加设备"     │                     │                    │                   │
 │──────────────────>│                     │                    │                   │
 │                    │  开始蓝牙扫描        │                    │                   │
 │                    │────────────────────────────────────────────────────────────>│
 │                    │                     │                    │                   │
 │                    │    发现 LiLa 设备列表 │                    │                   │
 │                    │<────────────────────────────────────────────────────────────│
 │                    │                     │                    │                   │
 │  选择设备           │                     │                    │                   │
 │──────────────────>│                     │                    │                   │
 │                    │  蓝牙连接设备         │                    │                   │
 │                    │────────────────────────────────────────────────────────────>│
 │                    │    连接成功，获得MAC   │                    │                   │
 │                    │<────────────────────────────────────────────────────────────│
 │                    │                     │                    │                   │
 │                    │  POST /bind/         │                    │                   │
 │                    │  {mac_address: MAC}  │                    │                   │
 │                    │─────────────────────>│                    │                   │
 │                    │                     │  创建 UserDevice    │                   │
 │                    │                     │  激活 Device        │                   │
 │                    │    绑定成功响应       │                    │                   │
 │                    │<─────────────────────│                    │                   │
 │                    │                     │                    │                   │
 │                    │  连接 WebSocket       │                    │                   │
 │                    │  (user_token)        │                    │                   │
 │                    │═════════════════════>│                    │                   │
 │                    │  加入 device_{uid}    │                    │                   │
 │                    │                     │                    │                   │
 │                    │                     │    设备轮询发现已绑定 │                   │
 │                    │                     │<───────────────────│                   │
 │                    │                     │    重新MAC登录       │                   │
 │                    │                     │<───────────────────│                   │
 │                    │                     │  返回绑定用户token   │                   │
 │                    │                     │───────────────────>│                   │
 │                    │                     │                    │                   │
 │                    │                     │    设备连接WebSocket │                   │
 │                    │                     │<═══════════════════│                   │
 │                    │                     │  加入 device_{uid}  │                   │
 │                    │                     │                    │                   │
 │                    │     ══════  双向通信建立  ══════           │                   │
 │                    │                     │                    │                   │
 │  点击"摸头"         │                     │                    │                   │
 │──────────────────>│  发送 touch:head     │                    │                   │
 │                    │─────────────────────>│  转发到 group       │                   │
 │                    │                     │───────────────────>│                   │
 │                    │                     │                    │  播放摸头动画       │
```

### 6.2 再次启动流程（已绑定）

```
手机端App                    服务器                     设备端
   │                          │                          │
   │  用户登录(token缓存)      │                          │
   │─────────────────────────>│                          │
   │  user_token              │                          │
   │<─────────────────────────│                          │
   │                          │                          │
   │  查询已绑定设备列表        │          设备开机          │
   │  GET /user-devices/      │    MAC登录                │
   │─────────────────────────>│<─────────────────────────│
   │  设备列表                 │    返回绑定用户token       │
   │<─────────────────────────│─────────────────────────>│
   │                          │                          │
   │  连接WebSocket            │    连接WebSocket          │
   │  (user_token)            │    (bound_user_token)    │
   │═════════════════════════>│<═════════════════════════│
   │  加入 device_{uid}        │    加入 device_{uid}      │
   │                          │                          │
   │       ══════  立即可以双向通信  ══════                 │
```

---

## 7. API 接口清单

### 7.1 现有接口（可直接使用）

| 方法 | 路径 | 调用方 | 说明 |
|------|------|--------|------|
| `POST` | `/api/user/auth/phone/login/` | 手机端 | 手机号验证码登录 |
| `POST` | `/api/user/auth/phone/verify/` | 手机端 | 发送验证码 |
| `POST` | `/api/user/auth/mac/login/` | 设备端 | MAC 地址登录 |
| `POST` | `/api/device/user-devices/bind/` | 手机端 | 绑定设备（传 mac_address） |
| `DELETE` | `/api/device/user-devices/{id}/` | 手机端 | 解绑设备 |
| `GET` | `/api/device/user-devices/` | 手机端 | 获取已绑定设备列表 |
| `POST` | `/api/device/user-devices/{id}/set_primary/` | 手机端 | 设为主设备 |
| `GET` | `/api/device/rtc-token/get_by_mac/` | 设备端 | 获取 RTC token |

### 7.2 需要新增的接口

| 方法 | 路径 | 调用方 | 说明 |
|------|------|--------|------|
| `GET` | `/api/device/devices/bind_status/` | 设备端 | 查询绑定状态 |
| `POST` | `/api/device/devices/register/` | 设备端 | 设备自注册（可选） |

### 7.3 需要修改的接口

| 方法 | 路径 | 改动说明 |
|------|------|---------|
| `POST` | `/api/user/auth/mac/login/` | 已绑定时返回绑定用户的 token；未绑定时返回明确的未绑定状态 |

---

## 8. 数据模型关系

```
┌─────────────────┐       ┌──────────────────┐       ┌─────────────────┐
│  ParadiseUser   │       │   UserDevice     │       │    Device       │
│─────────────────│       │──────────────────│       │─────────────────│
│ id              │1─────*│ user_id (FK)     │*─────1│ id              │
│ username        │       │ device_id (FK)   │       │ device_code     │
│ phone           │       │ nickname         │       │ mac_address (U) │
│ ...             │       │ is_primary       │       │ is_active       │
│                 │       │ bound_at         │       │ status          │
│                 │       │                  │       │ battery_level   │
│                 │       │ unique(user,     │       │ device_type (FK)│
│                 │       │   device)        │       │ batch (FK)      │
└─────────────────┘       └──────────────────┘       └─────────────────┘

约束：
- 一个用户可绑定多个设备（UserDevice 多条记录）
- 一个设备同时只能绑定一个用户（DeviceBindSerializer 中校验）
- 绑定时自动激活设备（is_active=True）
```

---

## 9. 改动文件清单

### 9.1 服务器（qy_lty-main）

| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `userapp/views.py` | **修改** | `MacAddressLoginView`：已绑定时返回绑定用户的 token |
| `device_interaction/views.py` | **新增方法** | `DeviceViewSet.bind_status()`：查询设备绑定状态 |
| `device_interaction/views.py` | **新增方法（可选）** | `DeviceViewSet.register()`：设备自注册 |
| `device_interaction/consumers.py` | **修改** | 新增 `device_info` 消息类型处理 + `update_device_status` 方法 |
| `device_interaction/serializers.py` | **新增** | `DeviceRegisterSerializer` |

### 9.2 设备端（LTY_Project）

| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `Assets/Scripts/WebSocketConnection.cs` | **修改** | 动态获取 MAC；绑定状态处理；WiFi连接后上报 `device_info` |
| `Assets/Scripts/getJson.cs` | **修改** | MAC 地址改为从 WebSocketConnection 动态获取 |

### 9.3 手机端（LTY_App_Project_URP）

| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `Assets/Scripts/Manager/WebSocketNetworking.cs` | **修改** | 移除 MAC 登录，改用用户 token；处理 `device_info` 消息 |
| `Assets/Scripts/Manager/DeviceBindManager.cs` | **新增** | 绑定/解绑/设备列表 API 调用封装 |
| `Assets/Scripts/Manager/UserSession.cs` | **新增** | 用户登录态全局管理 |
| `JarConnection_Panel`（新面板） | **新增** | 绑定入口："添加新罐子"，参考 BluetoothCentral 蓝牙扫描逻辑 |
| `Assets/Scripts/PureMVC/View/DeviceDetailsViewMediator.cs` | **修改** | 解绑按钮对接真实解绑 API |
| `Assets/Scripts/getJson.cs` | **修改** | MAC 地址改为动态获取（从 device_info 缓存） |

---

## 10. 实施步骤（建议顺序）

### 阶段一：服务器端准备

1. 修改 `MacAddressLoginView`，支持返回绑定用户 token
2. 新增 `bind_status` 接口
3. 部署并测试 API

### 阶段二：设备端改造

4. 修改 `WebSocketConnection.cs`，动态获取 MAC
5. 增加未绑定状态处理和轮询逻辑
6. 修改 `getJson.cs`，MAC 动态化
7. 设备端联调测试

### 阶段三：手机端改造

8. 新增 `UserSession.cs` 和 `DeviceBindManager.cs`
9. 改造 `WebSocketNetworking.cs`，移除 MAC 登录
10. 打通蓝牙扫描 → 绑定流程
11. 实现解绑功能
12. 手机端联调测试

### 阶段四：三端联调

13. 手机端登录 → 蓝牙扫描 → 绑定 → WebSocket 互通
14. 设备端开机 → 等待绑定 → 被绑定后自动连接
15. 指令双向通信测试（touch/dance/weather 等）
16. 解绑后设备回到等待绑定状态

---

## 11. 关键设计问答

### Q1：手机端 MAC 地址不固定（切换网络），设备端怎么给手机端发指令？

**手机端不需要 MAC 地址**。整个通信架构中，手机端的身份标识是 `user_id`（从登录 token 中获取），而非 MAC 地址。

通信原理：
```
手机端登录 → 获得 user_token → 连接 WebSocket → 加入 group: device_{user_id}
设备端MAC登录 → 获得绑定用户的 token → 连接 WebSocket → 加入 group: device_{user_id}
                                                         ↑ 同一个 group
```

- **手机→设备**：手机端发消息到 WebSocket → 服务器广播到 `device_{user_id}` group → 设备端收到
- **设备→手机**：设备端发消息到 WebSocket → 服务器广播到 `device_{user_id}` group → 手机端收到

两端都在同一个 group 里，**双向通信天然支持**，不需要知道对方的 MAC/IP 地址。手机随便切换 WiFi/4G/5G，只要 WebSocket 连着（或重连成功），消息就能到达。

### Q2：设备端连上 WiFi 后如何把 MAC 地址传给手机端和服务器？

设备端连接 WiFi 后的 MAC 上报流程：

```
设备端                          服务器                      手机端
  │                              │                          │
  │  连WiFi成功                   │                          │
  │  ↓                           │                          │
  │  POST /devices/register/     │                          │
  │  {mac_address, device_type}  │                          │
  │─────────────────────────────>│  创建/更新 Device 记录     │
  │  返回 device_code            │                          │
  │<─────────────────────────────│                          │
  │                              │                          │
  │  (若已绑定) MAC登录            │                          │
  │─────────────────────────────>│                          │
  │  绑定用户 token               │                          │
  │<─────────────────────────────│                          │
  │                              │                          │
  │  连接 WebSocket               │                          │
  │══════════════════════════════>│                          │
  │                              │                          │
  │  发送 device_info 消息         │    转发到 group           │
  │  {type:"device_info",         │─────────────────────────>│
  │   message:{mac, device_code,  │                          │
  │   firmware, battery...}}      │                          │  手机端收到设备信息
  │                              │                          │  可显示在UI上
```

具体实现分三层：
1. **服务器记录**：设备调用 `POST /devices/register/` 自注册，MAC 地址写入 Device 表
2. **WebSocket 上报**：设备连接 WebSocket 后，发送一条 `device_info` 类型消息，把 MAC、电量、固件版本等信息推给手机端
3. **手机端接收**：手机端在 WebSocket 收到 `device_info` 消息后，更新本地设备信息 UI

### Q3：手机端绑定入口在哪里？

绑定逻辑将在 **JarConnection_Panel**（"添加新罐子"按钮）中执行，参考 `BluetoothCentral.cs` 的蓝牙扫描逻辑，但不直接修改 `BluetoothCentral.cs`。流程：

```
JarConnection_Panel
  └─ "添加新罐子"按钮
       └─ 蓝牙扫描 LiLa 设备（参考 BluetoothCentral 逻辑）
            └─ 用户选择设备
                 └─ 蓝牙连接成功，获得设备蓝牙 MAC
                      └─ 调用 POST /user-devices/bind/ (传 MAC)
                           └─ 绑定成功 → 连接 WebSocket
```

---

## 12. 新增 WebSocket 消息类型：device_info

### 目的

设备端连接 WebSocket 后，主动上报自身信息（MAC地址、电量、固件版本等），让手机端实时获知设备状态。

### 消息格式

```json
// 设备端 → 服务器 → 手机端
{
    "type": "device_info",
    "message": {
        "mac_address": "AA:BB:CC:DD:EE:FF",
        "device_code": "T01-AUTO-T01-00001",
        "firmware_version": "1.0.0",
        "battery_level": 85,
        "wifi_name": "HomeWiFi",
        "brightness": 50
    }
}
```

### 需要改动

| 项目 | 文件 | 改动 |
|------|------|------|
| 服务器 | `consumers.py` | 新增 `device_info` 消息类型处理（广播到 group） |
| 设备端 | `WebSocketConnection.cs` | WebSocket 连接成功后发送 `device_info` 消息 |
| 手机端 | `WebSocketNetworking.cs` / `PhoneManager.cs` | 收到 `device_info` 时更新设备信息 UI |

---

## 13. 注意事项

1. **MAC 地址格式统一**：蓝牙扫描得到的 MAC 可能是 `AA-BB-CC-DD-EE-FF` 或 `AABBCCDDEEFF`，需统一转换为 `AA:BB:CC:DD:EE:FF` 格式
2. **Android 蓝牙权限**：Android 12+ 需要 `BLUETOOTH_SCAN` 和 `BLUETOOTH_CONNECT` 权限
3. **Token 过期处理**：设备端长期运行，需要处理 token 过期后的自动刷新
4. **断线重连**：设备端已有重连逻辑（最多5次），需确保重连时 token 仍有效
5. **并发绑定**：服务器端 `unique_together = ['user', 'device']` 约束可防止重复绑定
6. **安全性**：绑定 API 需要用户认证（已配置 `RedisTokenAuthentication`），防止未授权绑定
7. **手机端无需 MAC 标识**：手机端通过 user_id 标识，切换网络不影响 WebSocket group 归属
8. **设备信息实时上报**：设备连接 WebSocket 后应立即发送 `device_info`，手机端可据此更新 UI
