diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c545112 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +xcuserdata/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..1c81f04 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,681 @@ +# ARCHITECTURE.md + +Comprehensive architecture reference for the QIUI-API iOS application. The running codebase is the source of truth. + +--- + +## System Overview + +QIUI-API is an Objective-C iOS app that controls QIUI Bluetooth hardware (smart locks, key pods) through a hybrid cloud+BLE architecture. The app never generates device commands locally — all commands are fetched from the QIUI cloud API and then forwarded to the physical device over Bluetooth Low Energy. + +``` +┌─────────────┐ HTTPS/JSON ┌──────────────────┐ +│ iOS App │ ◄──────────────────► │ QIUI Cloud API │ +│ (this app) │ │ openapi.qiuitoy │ +└──────┬──────┘ └──────────────────┘ + │ + │ BLE (FFF0/FFF1/FFF2) + │ +┌──────▼──────┐ +│ QIUI Device │ +│ (Key Pod / │ +│ Smart Lock)│ +└─────────────┘ +``` + +The app does NOT generate BLE commands itself. Every command (lock, unlock, shock, etc.) is: +1. Requested from the cloud API with device identifiers +2. Returned as an opaque string in the API response `data` field +3. Sent verbatim to the device via BLE write characteristic `FFF1` + +--- + +## Authentication Flow + +Two-tier token system: + +``` +┌──────────────┐ clientId + grantType ┌─────────────────────┐ +│ │ ──────────────────────► │ getPlatformApiToken │ +│ Platform │ │ │ +│ API Token │ ◄────────────────────── │ Returns: │ +│ (12h TTL) │ platformApiToken │ platformApiToken │ +│ │ expiresTime │ expiresTime │ +└──────┬───────┘ └─────────────────────┘ + │ + │ Used as Authorization header for all subsequent calls + │ + ▼ +┌──────────────┐ bluetoothAddress + ┌─────────────────────┐ +│ │ serialNumber + typeId │ getDeviceToken │ +│ Device │ ──────────────────────► │ │ +│ Token │ │ Returns: │ +│ (per-conn) │ ◄────────────────────── │ deviceToken │ +│ │ deviceToken (string) │ (changes each │ +└──────────────┘ │ connection) │ + └─────────────────────┘ +``` + +- **Platform API Token**: OAuth2 `client_credentials` grant. Valid 12 hours. Same token works for TEST and PRODUCT environments. +- **Device Token**: Per-connection token. Changes every time the device connects. Must be obtained after BLE connection to the device's UUID is established. Sent to device as first BLE message after connection. +- **Refresh**: Use `refreshPlatformApiToken` endpoint with the old token in the `Authorization` header before it expires. + +--- + +## Complete API Reference + +**Base URL**: `https://openapi.qiuitoy.com` + +All endpoints are `POST` with `Content-Type: application/json`. All require `Environment` header. + +### Common Headers + +| Header | Required | Values | Notes | +|--------|----------|--------|-------| +| `Content-Type` | Yes | `application/json` | All requests | +| `Environment` | Yes | `TEST`, `PRODUCT` | Determines environment | +| `Authorization` | Most | `` | Not needed for getPlatformApiToken | + +### Common Response Format + +```json +{ + "code": 0, + "data": { ... }, + "message": "" +} +``` + +### Error Codes + +| Code | Description | Action | +|------|-------------|--------| +| 200 | Success | — | +| 400 | Bad parameters / validation failed | Check request body | +| 401 | Token expired or not logged in | Re-authenticate | +| 403 | No permissions | Contact administrator | +| 404 | No data found | Check request | +| 500 | Server error | Retry or contact support | + +--- + +### API Service Endpoints + +#### 1. Get Platform API Token +``` +POST /system/api/device/common/getPlatformApiToken +``` +No Authorization header needed. + +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| clientId | body | string | Your client ID | +| grantType | body | string | Always `client_credentials` | + +**Response data**: `{ "platformApiToken": "...", "expiresTime": 0 }` + +**Code**: `selectorBtn1` in `APIHomeViewController.m:166` + +--- + +#### 2. Refresh Platform API Token +``` +POST /system/api/device/common/refreshPlatformApiToken +``` +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| Authorization | header | string | Old (current) API token | +| clientId | body | string | Your client ID | +| grantType | body | string | Always `client_credentials` | + +**Response data**: `{ "platformApiToken": "...", "expiresTime": 0 }` + +**Code**: Not yet implemented in codebase. + +--- + +#### 3. Platform Bind Device +``` +POST /system/api/platform/device/addDeviceInfo +``` +Binds a device to the platform. Only bound devices can receive commands. + +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| Authorization | header | string | API token | +| bluetoothAddress | body | string | MAC address (e.g. `00:11:22:33:FF:EE`) | + +**Response data**: +```json +{ + "bluetoothAddress": "", "createBy": "", "createTime": "", + "environmentType": 0, "iccid": "", "id": 0, "remark": "", + "serialNumber": "", "typeId": 0, "updateBy": "", + "updateTime": "", "userId": 0 +} +``` + +**Code**: `selectorBtn3` in `APIHomeViewController.m:232` + +--- + +#### 4. Query Bound Device Information +``` +POST /system/api/platform/device/queryDeviceInfo +``` +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| Authorization | header | string | API token | +| bluetoothAddress | body | string | MAC address | + +**Response data**: Same structure as bind response. + +**Code**: `selectorBtn4` in `APIHomeViewController.m:310` + +--- + +### Bluetooth Command Endpoints + +All Bluetooth command endpoints share a common pattern: +- Require `Authorization` and `Environment` headers +- Accept device identifiers in the body +- Return an opaque command string in `data` to send via BLE + +#### 5. Get Device Token +``` +POST /system/api/device/common/getDeviceToken +``` +Token changes every connection. Call after BLE connection established. + +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| bluetoothAddress | body | string | MAC address | +| serialNumber | body | string | Device serial | +| typeId | body | string | Device type | + +**Response data**: string (the device token) + +**Code**: `selectorBtn5` in `APIHomeViewController.m:367` + +**Note**: The API docs show a different URL path (`/system/api/device/cellMate/bluetooth/buildCellMatePro4GTokenCmd`) than what the codebase uses (`/system/api/device/common/getDeviceToken`). The codebase URL is the source of truth. + +--- + +#### 6. Get Unlock Command +``` +POST /system/api/device/keyPod/getKeyPodUnlockCmd +``` +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| bluetoothAddress | body | string | MAC address | +| serialNumber | body | string | Device serial | +| typeId | body | string | Device type | + +**Response data**: string (BLE command to send) + +**Code**: `selectorBtn6` → `sendLockCommandWithURL:` in `APIHomeViewController.m:424` + +**API Docs alternative URL**: `/system/api/device/cellMate/bluetooth/buildCellMatePro4GUnLockCmd` + +--- + +#### 7. Get Lock Command +``` +POST /system/api/device/keyPod/getKeyPodLockCmd +``` +Same parameters as unlock. + +**Code**: `selectorBtn7` → `sendLockCommandWithURL:` in `APIHomeViewController.m:429` + +**API Docs alternative URL**: `/system/api/device/cellMate/bluetooth/buildCellMatePro4GLockCmd` + +--- + +#### 8. Decrypt Bluetooth Command +``` +POST /system/api/device/cellMate/bluetooth/decryBluetoothCommand +``` +Decrypts a command string returned by the device over BLE. + +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| lockCommand | body | string | Encrypted command from device | +| serialNumber | body | string | Device serial | + +**Response data**: `{ "battery": 0, "commentType": "", "isUnlocking": true }` + +**Code**: Not yet implemented in codebase. + +--- + +#### 9. Scheduled Unlock +``` +POST /system/api/device/cellMate/bluetooth/buildCellMateProTimingUnlock +``` +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| shockVolt | body | int32 | Voltage level 0–4 | +| timingDuration | body | int32 | Duration 1–8640000 seconds (max ~100 days) | +| basicDeviceApiReq.bluetoothAddress | body | string | MAC address | +| basicDeviceApiReq.serialNumber | body | string | Device serial | +| basicDeviceApiReq.typeId | body | int32 | Device type (e.g. 10) | + +**Code**: Not yet implemented in codebase. + +--- + +#### 10. Clear Timed Unlock +``` +POST /system/api/device/cellMate/bluetooth/buildCellMateProClearTimingUnlock +``` +Standard device params (`bluetoothAddress`, `serialNumber`, `typeId`). + +**Code**: Not yet implemented in codebase. + +--- + +#### 11. Scheduled Electric Shock +``` +POST /system/api/device/cellMate/bluetooth/buildToyTimingElectricShock +``` +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| shockDuration | body | int32 | Duration 1–5 seconds | +| shockModel | body | int32 | Mode 1–3 | +| shockVolt | body | int32 | Level 0–4 | +| timingDuration | body | int32 | Timing 1–8640000 seconds | +| basicDeviceApiReq.* | body | — | Standard device params | + +**Code**: Not yet implemented in codebase. + +--- + +#### 12. Clear Timed Shock +``` +POST /system/api/device/cellMate/bluetooth/buildClearAllElectricShockCmd +``` +Standard device params. + +**Code**: Not yet implemented in codebase. + +--- + +#### 13. Shock for One Second +``` +POST /system/api/device/cellMate/bluetooth/buildCellMateProShockImmediately +``` +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| shockModel | body | int32 | Mode 1–3 | +| shockVolt | body | int32 | Level 0–4 | +| basicDeviceApiReq.* | body | — | Standard device params | + +**Code**: Not yet implemented in codebase. + +--- + +#### 14. Shock for Five Seconds +``` +POST /system/api/device/cellMate/bluetooth/buildCellMateProShockContinuedFi[ve] +``` +Same params as shock for one second. + +**Code**: Not yet implemented in codebase. + +--- + +#### 15. Stop All Shocks +``` +POST /system/api/device/cellMate/bluetooth/buildCellMateProStopAllShockCmd +``` +Standard device params. + +**Code**: Not yet implemented in codebase. + +--- + +#### 16. Set Screen Orientation +``` +POST /system/api/device/cellMate/bluetooth/buildDisplayDirectionCmd +``` +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| direction | body | int32 | 0 = Forward, 1 = Reverse | +| basicDeviceApiReq.* | body | — | Standard device params | + +**Code**: Not yet implemented in codebase. + +--- + +#### 17. Set MQTT Service +``` +POST /system/api/device/cellMate/bluetooth/buildServerIpAndPortCmd +``` +Standard device params. Configures MQTT connection on the device. + +**Code**: Not yet implemented in codebase. + +--- + +#### 18. Set Working Mode +``` +POST /system/api/device/cellMate/bluetooth/buildCellMatePro4GWorkModelCmd +``` +| Param | Location | Type | Description | +|-------|----------|------|-------------| +| workStatus | body | int32 | 00 = Off, 01 = Long Connection, 02 = Periodic Connection | +| basicDeviceApiReq.* | body | — | Standard device params | + +**Code**: Not yet implemented in codebase. + +--- + +## App Workflow (As Implemented) + +### Complete Sequence Diagram + +``` +User APIHomeVC QIUI Cloud API BLE Manager Device + │ │ │ │ │ + │─ Btn1 ────────►│ │ │ │ + │ │── POST getToken─►│ │ │ + │ │◄─ platformToken──│ │ │ + │ │ │ │ │ + │─ Btn2 ────────►│ │ │ │ + │ │── push BLE list─►│ │ │ + │ │ (scan starts) │ │ │ + │─ select device─│◄─ notification ──│ │ │ + │ │ (MAC + periph) │ │ │ + │ │ │ │ │ + │─ Btn3 ────────►│ │ │ │ + │ │── POST addDevice►│ │ │ + │ │◄─ serialNumber,──│ │ │ + │ │ typeId │ │ │ + │ │ │ │ │ + │─ Btn4 ────────►│ │ │ │ + │ │── POST query ───►│ │ │ + │ │◄─ device info ──-│ │ │ + │ │ │ │ │ + │─ Btn5 ────────►│ │ │ │ + │ │── POST getDevTkn►│ │ │ + │ │◄─ deviceToken ───│ │ │ + │ │ │ connectPeripheral│ │ + │ │─────────────────────────────────────►│ │ + │ │ │ discover services│ │ + │ │ │ │──────────────►│ + │ │ │ connectSuccess │ │ + │ │◄─────────────────────────────────────│ │ + │ │ │ write deviceToken│ │ + │ │─────────────────────────────────────►│──── FFF1 ───►│ + │ │ │ │ │ + │─ Btn6 ────────►│ │ │ │ + │ │── POST unlock ──►│ │ │ + │ │◄─ command str ───│ │ │ + │ │ │ write command │ │ + │ │─────────────────────────────────────►│──── FFF1 ───►│ + │ │ │ │ │ + │─ Btn7 ────────►│ │ │ │ + │ │── POST lock ────►│ │ │ + │ │◄─ command str ───│ │ │ + │ │ │ write command │ │ + │ │─────────────────────────────────────►│──── FFF1 ───►│ +``` + +### State Dependencies Between Steps + +| Step | Requires | Produces | Stored In | +|------|----------|----------|-----------| +| Btn1 (Get Token) | `clientId` | `platformApiToken`, `expiresTime` | `_platformApiToken`, `_expiresTime` | +| Btn2 (Scan) | — | `bluetoothAddress`, `peripheral` | `_bluetoothAddress`, `_peripheral` | +| Btn3 (Bind) | `platformApiToken`, `bluetoothAddress` | `serialNumber`, `typeId` | `_serialNumber`, `_typeId` | +| Btn4 (Query) | `platformApiToken`, `bluetoothAddress` | `serialNumber`, `typeId` | `_serialNumber`, `_typeId` | +| Btn5 (Device Token) | `platformApiToken`, `bluetoothAddress`, `serialNumber`, `typeId`, `peripheral` | `deviceToken` + BLE connection | `_deviceToken`, `_babyMgr` | +| Btn6 (Unlock) | `platformApiToken`, `bluetoothAddress`, `serialNumber`, `typeId` + active BLE | command sent to device | — | +| Btn7 (Lock) | Same as Btn6 | command sent to device | — | + +--- + +## Source File Reference + +### APIHomeViewController.m (504 lines) +Primary controller. Owns the full API workflow and BLE command relay. + +| Method | Line | Purpose | +|--------|------|---------| +| `EncryptedBluetoothAddress()` | 25 | AES-encrypt a MAC address (currently unused) | +| `APIEndpoints()` | 31 | Returns prod/dev URL dictionary based on `kUseProduction` flag | +| `LocalizedMessages()` | 63 | EN/ZH string tables for UI alerts | +| `L()` | 96 | Locale-aware string lookup | +| `viewDidLoad` | 125 | Layout adjustment + notification observer for device selection | +| `connectToySynchronize:` | 139 | Notification handler — receives selected device MAC + peripheral | +| `connectSuccess` | 148 | BLE delegate — sets UUIDs (FFF0/FFF1/FFF2), sends deviceToken | +| `connectFailed` | 156 | BLE delegate — logs failure | +| `disconnectPeripheral:` | 160 | BLE delegate — logs disconnect | +| `selectorBtn1:` | 166 | HTTP: Get platform API token | +| `selectorBtn2:` | 226 | Navigation: Push BLE scanner | +| `selectorBtn3:` | 232 | HTTP: Bind device (POST bluetoothAddress) | +| `selectorBtn4:` | 310 | HTTP: Query device info | +| `selectorBtn5:` | 367 | HTTP: Get device token + initiate BLE connection | +| `selectorBtn6:` | 424 | HTTP: Get unlock command → send via BLE | +| `selectorBtn7:` | 429 | HTTP: Get lock command → send via BLE | +| `selectorBtn8:` | 434 | BLE: Disconnect all peripherals | +| `sendLockCommandWithURL:` | 442 | Shared HTTP handler for lock/unlock — sends API response to BLE | +| `functionUnsend:` | 486 | Sends a UTF-8 string to BLE device via `write:` | + +### APIBuletoothListViewController.m (213 lines) +BLE device scanner and MAC address extractor. + +| Method | Line | Purpose | +|--------|------|---------| +| `viewDidLoad` | 60 | Init table, register cell NIB, start BLE scan | +| `viewDidDisappear:` | 80 | Stop scanning | +| `tableView:cellForRowAtIndexPath:` | 92 | Configure cell, wire up connect button block | +| `tableView:didSelectRowAtIndexPath:` | 117 | Stop scan on row tap | +| `systemBluetoothClose` | 124 | Delegate: BT off | +| `sysytemBluetoothOpen` | 128 | Delegate: BT on → restart scan | +| `getScanResultPeripherals:` | 133 | Core scan handler — extracts MAC from manufacturer data bytes | +| `requestNetwork:info:` | 202 | Add device to datasource and reload table | + +**MAC extraction logic** (lines 148–193): Reads `kCBAdvDataManufacturerData` bytes, extracts 6 bytes for MAC address. Byte offset and ordering depend on `_typeId`: +- typeId 4 or 9: bytes 3–8, reversed order +- typeId 6: bytes 0–5, normal order +- Default: bytes 2–7, normal order + +### HKBabyBluetoothManager.m (307 lines) +Singleton BLE abstraction layer wrapping `BabyBluetooth`. + +| Method | Line | Purpose | +|--------|------|---------| +| `sharedManager` | 66 | Singleton accessor | +| `initBabyBluetooth` | 83 | Create BabyBluetooth instance, wire delegates | +| `babyBluetoothDelegate` | 90 | Configure all BLE callbacks (scan filter, connect, disconnect, characteristics, read) | +| `startScanPeripheral` | 200 | Begin BLE scan | +| `stopScanPeripheral` | 206 | Cancel scan, clear peripheral array | +| `scanResultPeripheral:advertisementData:rssi:` | 212 | Dedup and store discovered peripherals | +| `connectPeripheral:` | 230 | Cancel existing connections, connect to selected peripheral | +| `connectSuccess` | 245 | Forward connection success to delegate | +| `connectFailed` | 252 | Forward connection failure to delegate | +| `disconnectPeripheral:` | 259 | Forward disconnect to delegate | +| `getCurrentPeripherals` | 265 | Return list of connected peripherals | +| `searchServerAndCharacteristicUUID` | 269 | Re-discover services on current peripheral | +| `disconnectAllPeripherals` | 280 | Cancel all connections | +| `disconnectLastPeripheral:` | 284 | Cancel specific connection | +| `write:` | 288 | Write data to device (WriteWithoutResponse on FFF1) | +| `readData:` | 299 | Handle incoming BLE data from FFF2 | + +**Scan filter** (lines 101–116): Accepts peripherals by name prefix (`qiui`, `QIUI`, `OKU`) or by advertised service UUID (`FEE7`, `FFF0`, `A6ED0201`, `FDAA`, `FEE5`). + +**Characteristic discovery** (lines 155–175): Matches service UUID against `serverUUIDString`. Properties bitmask `0xC` = writable, `0x10` = notify-capable. Special handling for `isTAILS` devices requiring exact UUID match on write characteristic. + +### AESCipher.m (114 lines) +AES-256-CBC encryption/decryption. + +| Function | Purpose | +|----------|---------| +| `cipherOperation()` | Core CCCrypt wrapper (encrypt or decrypt) | +| `aesEncryptString(content, key)` | String → AES → Base64 | +| `aesDecryptString(content, key)` | Base64 → AES → String | +| `aesEncryptData(data, key)` | Raw data encrypt | +| `aesDecryptData(data, key)` | Raw data decrypt | +| `+aesEncryptString:key:` | Class method wrapper for encrypt | +| `+aesDecryptString:key:` | Class method wrapper for decrypt | + +Config: IV = `0123456789abcdef`, key size = 32 bytes, PKCS7 padding. + +**Status**: Encryption is compiled but functionally disabled — `EncryptedBluetoothAddress()` output is never used. + +### NSDictionary+JMJson.m (51 lines) +| Method | Purpose | +|--------|---------| +| `getJsonValue:` | Safe value extraction with nil/NSNull→empty string, double formatting to 2 decimal places | +| `isPureInt:` | Check if string is integer | +| `isPureDouble:` | Check if string is double | +| `isPureFloat:` | Check if string is float | + +### NSMutableDictionary+JMJson.m (17 lines) +| Method | Purpose | +|--------|---------| +| `setJsonValue:key:` | Safe set — converts nil values to empty string, ignores nil/empty keys | + +### BuletoothListCell.m (39 lines) +| Method | Purpose | +|--------|---------| +| `setBlueData:` | Set cell label text from `bluetoothAddress` key | +| `selectorConnectBtn:` | IBAction — fires `checkConnectBtnBlock` with device info dict | + +--- + +## BLE Protocol Details + +### Service & Characteristic UUIDs + +| UUID | Role | Properties | +|------|------|------------| +| `FFF0` | Primary service | — | +| `FFF1` | Write characteristic | Write Without Response | +| `FFF2` | Read characteristic | Notify | + +### Device Name Prefixes (Scan Filter) + +| Prefix | Device Family | +|--------|--------------| +| `qiui` | QIUI standard | +| `QIUI` | QIUI standard (uppercase) | +| `OKU` | OKU series | +| `FEE7` | Service UUID filter | +| `FFF0` | Key Pod service | +| `A6ED0201` | SP series | +| `FDAA` | IIKEY series | +| `FEE5` | TAILS series | + +### BLE Message Flow + +1. **After connection**: Device token (UTF-8 string) is written to `FFF1` via `functionUnsend:` +2. **Commands**: API response `data` string is UTF-8 encoded and written to `FFF1` via `functionUnsend:` → `write:` +3. **Responses**: Device sends data via notifications on `FFF2`, received in `readData:` +4. **Write type**: `CBCharacteristicWriteWithoutResponse` — no acknowledgment from device + +### Manufacturer Data → MAC Address + +Advertisement data contains `kCBAdvDataManufacturerData`. MAC extraction: + +``` +Default (most devices): bytes[2..7] → AA:BB:CC:DD:EE:FF (normal order) +typeId 4 or 9: bytes[3..8] → FF:EE:DD:CC:BB:AA (reversed) +typeId 6: bytes[0..5] → AA:BB:CC:DD:EE:FF (normal order) +``` + +--- + +## Configuration + +### Environment Toggle + +`APIHomeViewController.m:29`: +```objc +static BOOL const kUseProduction = YES; +``` +- `YES` → `https://openapi.qiuitoy.com` +- `NO` → `http://192.168.31.163:8115` + +The `APIEndpoints()` function at line 31 maps logical names to full URLs based on this flag. However, the button handlers currently use hardcoded production URLs directly rather than going through `APIEndpoints()`. + +### AES Encryption (Disabled) + +Key: `4DDC49E7D9A648348B5844E2479C5B22` (line 26) +IV: `0123456789abcdef` (fixed in AESCipher.m) + +Intentionally disabled per commit `667ac35` ("取消加解密"). The bind endpoint now receives plain MAC addresses. + +--- + +## Unimplemented API Features + +The following endpoints are documented in `Docs/` PDFs but have no corresponding code in the app: + +| Feature | Endpoint | PDF | +|---------|----------|-----| +| Refresh Token | `refreshPlatformApiToken` | Refresh API.pdf | +| Decrypt BLE Response | `decryBluetoothCommand` | Decrypt Bluetooth.pdf | +| Scheduled Unlock | `buildCellMateProTimingUnlock` | Scheduled Unlock.pdf | +| Clear Timed Unlock | `buildCellMateProClearTimingUnlock` | Clear Timed Lock.pdf | +| Scheduled Shock | `buildToyTimingElectricShock` | Schedule Shock.pdf | +| Clear Timed Shock | `buildClearAllElectricShockCmd` | Clear Timed Shock.pdf | +| Shock 1 Second | `buildCellMateProShockImmediately` | Shock for one second.pdf | +| Shock 5 Seconds | `buildCellMateProShockContinuedFi` | Shock for 5 seconds.pdf | +| Stop All Shocks | `buildCellMateProStopAllShockCmd` | Stop Shocks.pdf | +| Screen Orientation | `buildDisplayDirectionCmd` | screen orientation.pdf | +| MQTT Service | `buildServerIpAndPortCmd` | MQTT.pdf | +| Working Mode | `buildCellMatePro4GWorkModelCmd` | working mode.pdf | + +### Parameter Reference for Unimplemented Endpoints + +**Shock parameters**: +- `shockVolt`: 0–4 (voltage level) +- `shockModel`: 1–3 (shock mode) +- `shockDuration`: 1–5 seconds + +**Timing parameters**: +- `timingDuration`: 1–8640000 seconds (max ~100 days) + +**Working mode values**: +- `00`: Off +- `01`: Long Connection +- `02`: Periodic Connection + +**Screen direction**: +- `0`: Forward +- `1`: Reverse + +**Nested device params** (for endpoints using `basicDeviceApiReq`): +```json +{ + "basicDeviceApiReq": { + "bluetoothAddress": "00:11:22:33:FF:EE", + "serialNumber": "QIUIrjJb07523964", + "typeId": 10 + } +} +``` + +--- + +## Known Issues & Discrepancies + +1. **Hardcoded URLs**: Button handlers use hardcoded production URLs instead of the `APIEndpoints()` lookup function that was set up for exactly this purpose. + +2. **URL path mismatch with docs**: The app uses `/system/api/device/keyPod/getKeyPodUnlockCmd` and `getKeyPodLockCmd`, while the API docs reference `/system/api/device/cellMate/bluetooth/buildCellMatePro4GUnLockCmd` and `buildCellMatePro4GLockCmd`. Both may work — the app's URLs are the source of truth since the code runs successfully. + +3. **`APIEndpoints()` unused**: The endpoint dictionary is defined but never called by any button handler. + +4. **`typeId` in scan controller**: `_typeId` is used in MAC extraction logic (line 162) but is never set in `APIBuletoothListViewController` — it defaults to `0`, which means MAC extraction always uses the default byte range (2–7). The typeId-specific branches (4/6/9) are unreachable. + +5. **Encryption disabled**: `EncryptedBluetoothAddress()` is called at line 253 but its return value is discarded. Plain MAC is sent instead. + +6. **Response code comparison**: API responses check `code` as string `@"200"`, but the API docs show `code` as `integer(int64)`. The `stringWithFormat:@"%@"` conversion makes this work, but it's fragile. + +7. **No token refresh logic**: Platform API token expires after 12 hours but there's no automatic refresh mechanism. + +8. **`readData:` from BLE unused**: The delegate method exists but `APIHomeViewController` doesn't implement it — device responses are received but not processed. + +9. **Stale trailing semicolon**: `APIHomeViewController.m:499` has an orphan `;` after `write:`. diff --git a/Docs/API Token.pdf b/Docs/API Token.pdf new file mode 100644 index 0000000..d6f16d5 Binary files /dev/null and b/Docs/API Token.pdf differ diff --git a/Docs/Clear Timed Lock.pdf b/Docs/Clear Timed Lock.pdf new file mode 100644 index 0000000..5f5722b Binary files /dev/null and b/Docs/Clear Timed Lock.pdf differ diff --git a/Docs/Clear Timed Shock.pdf b/Docs/Clear Timed Shock.pdf new file mode 100644 index 0000000..406635d Binary files /dev/null and b/Docs/Clear Timed Shock.pdf differ diff --git a/Docs/Decrypt Bluetooth.pdf b/Docs/Decrypt Bluetooth.pdf new file mode 100644 index 0000000..0654120 Binary files /dev/null and b/Docs/Decrypt Bluetooth.pdf differ diff --git a/Docs/Device Token.pdf b/Docs/Device Token.pdf new file mode 100644 index 0000000..e249d6f Binary files /dev/null and b/Docs/Device Token.pdf differ diff --git a/Docs/Lock.pdf b/Docs/Lock.pdf new file mode 100644 index 0000000..ca07c1c Binary files /dev/null and b/Docs/Lock.pdf differ diff --git a/Docs/MQTT.pdf b/Docs/MQTT.pdf new file mode 100644 index 0000000..5833635 Binary files /dev/null and b/Docs/MQTT.pdf differ diff --git a/Docs/Platform.pdf b/Docs/Platform.pdf new file mode 100644 index 0000000..c9ff4a8 Binary files /dev/null and b/Docs/Platform.pdf differ diff --git a/Docs/Query Bound.pdf b/Docs/Query Bound.pdf new file mode 100644 index 0000000..88c6410 Binary files /dev/null and b/Docs/Query Bound.pdf differ diff --git a/Docs/Refresh API.pdf b/Docs/Refresh API.pdf new file mode 100644 index 0000000..e230ff7 Binary files /dev/null and b/Docs/Refresh API.pdf differ diff --git a/Docs/Schedule Shock.pdf b/Docs/Schedule Shock.pdf new file mode 100644 index 0000000..9e14bf4 Binary files /dev/null and b/Docs/Schedule Shock.pdf differ diff --git a/Docs/Scheduled Unlock.pdf b/Docs/Scheduled Unlock.pdf new file mode 100644 index 0000000..9ad3c54 Binary files /dev/null and b/Docs/Scheduled Unlock.pdf differ diff --git a/Docs/Shock for 5 seconds.pdf b/Docs/Shock for 5 seconds.pdf new file mode 100644 index 0000000..ff7372e Binary files /dev/null and b/Docs/Shock for 5 seconds.pdf differ diff --git a/Docs/Shock for one second.pdf b/Docs/Shock for one second.pdf new file mode 100644 index 0000000..c0cbfc0 Binary files /dev/null and b/Docs/Shock for one second.pdf differ diff --git a/Docs/Stop Shocks.pdf b/Docs/Stop Shocks.pdf new file mode 100644 index 0000000..e2c42be Binary files /dev/null and b/Docs/Stop Shocks.pdf differ diff --git a/Docs/Unlock.pdf b/Docs/Unlock.pdf new file mode 100644 index 0000000..ee27400 Binary files /dev/null and b/Docs/Unlock.pdf differ diff --git a/Docs/plans/2026-02-08-localization-singleton-design.md b/Docs/plans/2026-02-08-localization-singleton-design.md new file mode 100644 index 0000000..1e6bd63 --- /dev/null +++ b/Docs/plans/2026-02-08-localization-singleton-design.md @@ -0,0 +1,97 @@ +# Design: Unified Localization Singleton + +## Problem + +Three files each contain their own inline localization dictionaries and `L()` functions: + +- `HKBabyBluetoothManager.m` — 9 BLE log keys, flat `en`/`zh` dicts, `LocalizedMessage()` function +- `APIBuletoothListViewController.m` — 7 scan keys, nested `en`/`zh-Hans` dict, `LocalizedMessages()` function +- `APIHomeViewController.m` — 12 UI/API keys, nested `en`/`zh-Hans` dict, `LocalizedMessages()` function + +Additionally, ~18 hardcoded English strings bypass localization entirely (alert titles, step validation messages, progress HUD text). Three network handlers swallow errors with log-only output and no UI feedback. + +Debugging in mixed Chinese/English console output is difficult because language detection re-runs on every `L()` call and the dictionaries are scattered across files. + +## Design + +### QIUILocalization Singleton + +New files: `QIUI-API/Manager/QIUILocalization.h` and `QIUILocalization.m` + +```objc +@interface QIUILocalization : NSObject + ++ (instancetype)shared; + +@property (nonatomic, copy, readonly) NSString *languageCode; + +- (NSString *)localizedString:(NSString *)key; + +@end + +#define L(key) [[QIUILocalization shared] localizedString:(key)] +``` + +### Language Detection (Phase 3) + +Runs once in singleton `init`: + +1. Read `[NSLocale preferredLanguages].firstObject` +2. Lowercase and check prefix +3. `zh*` → `@"zh-Hans"`, everything else → `@"en"` +4. Cache in `self.languageCode` + +No runtime switching — detected at launch, cached for session. + +### Fallback Chain + +`detected_lang[key]` → `zh-Hans[key]` → raw key + +Chinese (`zh-Hans`) is the default/complete language. English is fully populated but Chinese is the fallback because the codebase and API responses are Chinese-first. + +### String Catalog (Phase 2) + +All strings organized by key prefix: + +| Prefix | Source | Count | Description | +|--------|--------|-------|-------------| +| `ble.*` | HKBabyBluetoothManager | 9 | BLE connection/read/write log messages | +| `scan.*` | APIBuletoothListViewController | 7 | Scan view log messages | +| `api.*` | APIHomeViewController | 12 | Network result / UI alert messages | +| `step.*` | APIHomeViewController | ~8 | Prerequisite validation alerts | +| `cmd.*` | APIHomeViewController | ~6 | Command result alerts (success/fail) | +| `hud.*` | APIHomeViewController | ~5 | MBProgressHUD status text | + +Both `en` and `zh-Hans` dictionaries will be fully populated. Any Chinese-only strings get English translations; any English-only strings get Chinese translations. + +### Swallowed Error Fixes (Phase 1) + +Three network completion handlers missing `if (error)` guards: + +1. **selectorBtn1** (Get API Token) — line 304 +2. **selectorBtn3** (Bind Device) — line 389 +3. **selectorBtn4** (Query Device) — line 456 + +Fix: Add `if (error) { showAlert + return; }` before parsing `responseObject`. Uses new key `api.networkError`. + +### Adding a New Language + +Add a third top-level key (e.g., `@"ja"`) to the dictionary in `QIUILocalization.m`. Partial translations are fine — missing keys fall through to `zh-Hans`. + +## Files Modified + +| File | Action | +|------|--------| +| `QIUI-API/Manager/QIUILocalization.h` | **NEW** — singleton header + `L()` macro | +| `QIUI-API/Manager/QIUILocalization.m` | **NEW** — singleton impl, language detection, full string catalog | +| `QIUI-API/HOME/.../APIHomeViewController.m` | Remove inline `LocalizedMessages()`, `L()`, `API_LANG_KEY`. Replace all hardcoded strings with `L()`. Add 3 error guards. | +| `QIUI-API/HOME/.../APIBuletoothListViewController.m` | Remove inline `LocalizedMessages()`, `L()`. Import `QIUILocalization.h`. | +| `QIUI-API/Manager/蓝牙/HKBLE/HKBabyBluetoothManager.m` | Remove inline `LocalizedMessage()`, `L()` macro. Import `QIUILocalization.h`. | + +## Verification + +1. Build: `xcodebuild -workspace QIUI-API.xcworkspace -scheme QIUI-API -destination 'id=00008140-0005549101C0801C' build` +2. Deploy + launch on device +3. Test: All 8 buttons produce localized alerts +4. Test: Network errors show actual error detail (disconnect WiFi, try Step 1) +5. Test: Console logs are bilingual based on device language diff --git a/Docs/screen orientation.pdf b/Docs/screen orientation.pdf new file mode 100644 index 0000000..178a018 Binary files /dev/null and b/Docs/screen orientation.pdf differ diff --git a/Docs/working mode.pdf b/Docs/working mode.pdf new file mode 100644 index 0000000..d10352f Binary files /dev/null and b/Docs/working mode.pdf differ diff --git a/QIUI-API.xcodeproj/project.pbxproj b/QIUI-API.xcodeproj/project.pbxproj index 715846c..db60c61 100644 --- a/QIUI-API.xcodeproj/project.pbxproj +++ b/QIUI-API.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 04D9072C2D5DD53900861ABB /* APIBuletoothListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D907292D5DD53900861ABB /* APIBuletoothListViewController.m */; }; 04D907312D5DDC7300861ABB /* BuletoothListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 04D907302D5DDC7300861ABB /* BuletoothListCell.xib */; }; 04D907322D5DDC7300861ABB /* BuletoothListCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D9072F2D5DDC7300861ABB /* BuletoothListCell.m */; }; + AA00000126020800000A0001 /* QIUILocalization.m in Sources */ = {isa = PBXBuildFile; fileRef = AA00000126020800000A0003 /* QIUILocalization.m */; }; E6D57FBF0C3179567D88852B /* libPods-QIUI-API.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03DE60837BFBD419A6322A3E /* libPods-QIUI-API.a */; }; /* End PBXBuildFile section */ @@ -83,6 +84,8 @@ 04D9072E2D5DDC7300861ABB /* BuletoothListCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BuletoothListCell.h; sourceTree = ""; }; 04D9072F2D5DDC7300861ABB /* BuletoothListCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BuletoothListCell.m; sourceTree = ""; }; 04D907302D5DDC7300861ABB /* BuletoothListCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BuletoothListCell.xib; sourceTree = ""; }; + AA00000126020800000A0002 /* QIUILocalization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QIUILocalization.h; sourceTree = ""; }; + AA00000126020800000A0003 /* QIUILocalization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QIUILocalization.m; sourceTree = ""; }; B8D2C347587FC292C34495DE /* Pods-QIUI-API.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QIUI-API.release.xcconfig"; path = "Target Support Files/Pods-QIUI-API/Pods-QIUI-API.release.xcconfig"; sourceTree = ""; }; ECD7E1CFAFB1E5B61CFC3B37 /* Pods-QIUI-API.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QIUI-API.debug.xcconfig"; path = "Target Support Files/Pods-QIUI-API/Pods-QIUI-API.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -220,6 +223,8 @@ 04D907172D5D97F000861ABB /* Manager */ = { isa = PBXGroup; children = ( + AA00000126020800000A0002 /* QIUILocalization.h */, + AA00000126020800000A0003 /* QIUILocalization.m */, 04C791FB2D5EE92400C02514 /* AES加密方式 */, 04D907252D5DD17500861ABB /* 蓝牙 */, 04D9071C2D5D981B00861ABB /* Json处理 */, @@ -460,6 +465,7 @@ 04D907262D5DD17500861ABB /* HKPeripheralInfo.m in Sources */, 04D9072C2D5DD53900861ABB /* APIBuletoothListViewController.m in Sources */, 04D907272D5DD17500861ABB /* HKBabyBluetoothManager.m in Sources */, + AA00000126020800000A0001 /* QIUILocalization.m in Sources */, 0496CCF22D5C3C410037EB13 /* AppDelegate.m in Sources */, 0496CD032D5C3C420037EB13 /* main.m in Sources */, ); @@ -637,11 +643,14 @@ isa = XCBuildConfiguration; baseConfigurationReference = ECD7E1CFAFB1E5B61CFC3B37 /* Pods-QIUI-API.debug.xcconfig */; buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + "ARCHS[sdk=*]" = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = J3Y763278L; + DEVELOPMENT_TEAM = BB4S225265; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "QIUI-API/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -670,11 +679,14 @@ isa = XCBuildConfiguration; baseConfigurationReference = B8D2C347587FC292C34495DE /* Pods-QIUI-API.release.xcconfig */; buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + "ARCHS[sdk=*]" = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = J3Y763278L; + DEVELOPMENT_TEAM = BB4S225265; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "QIUI-API/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/QIUI-API.xcodeproj/xcshareddata/xcschemes/QIUI-API.xcscheme b/QIUI-API.xcodeproj/xcshareddata/xcschemes/QIUI-API.xcscheme new file mode 100644 index 0000000..3d53eb1 --- /dev/null +++ b/QIUI-API.xcodeproj/xcshareddata/xcschemes/QIUI-API.xcscheme @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIBuletoothListViewController.m" "b/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIBuletoothListViewController.m" index 1c35868..643623a 100644 --- "a/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIBuletoothListViewController.m" +++ "b/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIBuletoothListViewController.m" @@ -3,230 +3,168 @@ // QIUI-API // // Created by mac on 2025/2/13. +// Refactored by MrWizard 2025/7/23 // #import "APIBuletoothListViewController.h" #import "BuletoothListCell.h" - -//蓝牙 #import "HKBabyBluetoothManager.h" +#import "QIUILocalization.h" + +#pragma mark - Implementation -@interface APIBuletoothListViewController () +@interface APIBuletoothListViewController () @property (weak, nonatomic) IBOutlet UITableView *tableView; -@property (strong, nonatomic) HKBabyBluetoothManager * babyMgr; +@property (strong, nonatomic) HKBabyBluetoothManager *babyMgr; @property (nonatomic, strong) NSMutableArray *dataSource; @property (nonatomic, strong) CBPeripheral *peripheral; - @end @implementation APIBuletoothListViewController - (void)viewDidLoad { [super viewDidLoad]; - + + NSLog(@"%@", L(@"scan.viewLoaded")); + _dataSource = [[NSMutableArray alloc] init]; self.tableView.dataSource = self; self.tableView.delegate = self; - - //注册cell - UINib *BuletoothListCellNib=[UINib nibWithNibName:@"BuletoothListCell" bundle:nil]; - [self.tableView registerNib:BuletoothListCellNib forCellReuseIdentifier:@"BuletoothListCell"]; + + UINib *nib = [UINib nibWithNibName:@"BuletoothListCell" bundle:nil]; + [self.tableView registerNib:nib forCellReuseIdentifier:@"BuletoothListCell"]; [self.tableView reloadData]; - _babyMgr = [HKBabyBluetoothManager sharedManager]; _babyMgr.delegate = self; + + NSLog(@"%@", L(@"scan.startScan")); [_babyMgr startScanPeripheral]; } --(void)Languagesettings -{ -} --(void)viewDidDisappear:(BOOL)animated -{ +- (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; + NSLog(@"%@", L(@"scan.stopScan")); [_babyMgr stopScanPeripheral]; } -#pragma mark - UITableView --(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ + +#pragma mark - UITableView DataSource / Delegate + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataSource.count; } --(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ - - - BuletoothListCell *cell = [tableView cellForRowAtIndexPath:indexPath]; - //解决xib复用数据混乱问题 - if (nil == cell) { - - cell= (BuletoothListCell *)[[[NSBundle mainBundle] loadNibNamed:@"BuletoothListCell" owner:self options:nil] lastObject]; - - }else{ - //删除cell的所有子视图 - while ([cell.contentView.subviews lastObject] != nil) - { - [(UIView*)[cell.contentView.subviews lastObject] removeFromSuperview]; - } - +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + BuletoothListCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BuletoothListCell"]; + if (!cell) { + cell = [[[NSBundle mainBundle] loadNibNamed:@"BuletoothListCell" owner:nil options:nil] lastObject]; } - [cell setBlueData:self.dataSource[indexPath.row]]; - cell.checkConnectBtnBlock = ^(NSDictionary * bluetoothInfo) { - [[NSNotificationCenter defaultCenter] postNotificationName:@"connectToySynchronize" object:nil userInfo:bluetoothInfo]; - [self.navigationController popViewControllerAnimated:YES]; - }; - + NSDictionary *bluetoothInfo = self.dataSource[indexPath.row]; + [cell setBlueData:bluetoothInfo]; + + __weak typeof(self) weakSelf = self; + cell.checkConnectBtnBlock = ^(NSDictionary *bluetoothInfo) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"connectToySynchronize" + object:nil + userInfo:bluetoothInfo]; + [weakSelf.navigationController popViewControllerAnimated:YES]; + }; return cell; } --(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ - [_babyMgr stopScanPeripheral]; -} --(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 44; } -#pragma mark HKBabyBluetoothManageDelegate 代理回调 +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + NSLog(@"%@", L(@"scan.rowSelected")); + [_babyMgr stopScanPeripheral]; +} + +#pragma mark - HKBabyBluetoothManageDelegate + - (void)systemBluetoothClose { - // 系统蓝牙被关闭、提示用户去开启蓝牙 + NSLog(@"%@", L(@"scan.bluetoothClosed")); } - (void)sysytemBluetoothOpen { - // 系统蓝牙已开启、开始扫描周边的蓝牙设备 + NSLog(@"%@", L(@"scan.bluetoothOpen")); [_babyMgr startScanPeripheral]; } - (void)getScanResultPeripherals:(NSArray *)peripheralInfoArr { - // 这里获取到扫描到的蓝牙外设数组、添加至数据源中 - if (self.dataSource.count>0) { - [self.dataSource removeAllObjects]; - } - - for (HKPeripheralInfo *info in peripheralInfoArr) { - NSData *data = info.advertisementData[@"kCBAdvDataManufacturerData"]; - NSUInteger len = [data length]; - Byte *byteData = (Byte*)malloc(len); - memcpy(byteData, [data bytes], len); - - NSMutableArray * bytesAry = [[NSMutableArray alloc] init]; - - for (int i = 0; i < len; i++) { - NSLog(@"byteData : %hhu",byteData[i]); - NSString *hexString= [NSString stringWithFormat:@"%@",[[NSString alloc] initWithFormat:@"%1hhx",byteData[i]]]; - if(hexString.length <=1) - { - hexString = [NSString stringWithFormat:@"0%@",hexString]; + NSLog(@"%@", L(@"scan.scanningDevices")); + + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.dataSource.count > 0) { + [self.dataSource removeAllObjects]; + } + + for (HKPeripheralInfo *info in peripheralInfoArr) { + NSData *data = info.advertisementData[@"kCBAdvDataManufacturerData"]; + if (!data || data.length < 8) { + NSLog(@"[BLE] %@", L(@"scan.noManufacturerData")); + continue; } - if(_typeId == 4 || _typeId == 9) - { - switch (i) { - case 3: - [bytesAry addObject:hexString]; - break; - case 4: - [bytesAry addObject:hexString]; - break; - case 5: - [bytesAry addObject:hexString]; - break; - case 6: - [bytesAry addObject:hexString]; - break; - case 7: - [bytesAry addObject:hexString]; - break; - case 8: - [bytesAry addObject:hexString]; - break; - - default: - break; - } - }else if(_typeId == 6) - { - switch (i) { - case 0: - [bytesAry addObject:hexString]; - break; - case 1: - [bytesAry addObject:hexString]; - break; - case 2: - [bytesAry addObject:hexString]; - break; - case 3: - [bytesAry addObject:hexString]; - break; - case 4: - [bytesAry addObject:hexString]; - break; - case 5: - [bytesAry addObject:hexString]; - break; - - default: - break; + NSUInteger len = data.length; + Byte *byteData = (Byte *)malloc(len); + memcpy(byteData, [data bytes], len); + + NSMutableArray *bytesAry = [[NSMutableArray alloc] init]; + for (int i = 0; i < len; i++) { + NSString *hexString = [NSString stringWithFormat:@"%02x", byteData[i]]; + if (_typeId == 4 || _typeId == 9) { + if (i >= 3 && i <= 8) [bytesAry addObject:hexString]; + } else if (_typeId == 6) { + if (i >= 0 && i <= 5) [bytesAry addObject:hexString]; + } else { + if (i >= 2 && i <= 7) [bytesAry addObject:hexString]; } - } - else - { - switch (i) { - case 2: - [bytesAry addObject:hexString]; - break; - case 3: - [bytesAry addObject:hexString]; - break; - case 4: - [bytesAry addObject:hexString]; - break; - case 5: - [bytesAry addObject:hexString]; - break; - case 6: - [bytesAry addObject:hexString]; - break; - case 7: - [bytesAry addObject:hexString]; - break; - - default: - break; + + free(byteData); + + if (bytesAry.count >= 6) { + NSString *macAddr = @""; + if (_typeId == 4 || _typeId == 9) { + macAddr = [NSString stringWithFormat:@"%@:%@:%@:%@:%@:%@", + bytesAry[5], bytesAry[4], bytesAry[3], + bytesAry[2], bytesAry[1], bytesAry[0]]; + } else { + macAddr = [NSString stringWithFormat:@"%@:%@:%@:%@:%@:%@", + bytesAry[0], bytesAry[1], bytesAry[2], + bytesAry[3], bytesAry[4], bytesAry[5]]; } + NSLog(@"[BLE] %@: %@", L(@"scan.foundPeripheral"), macAddr); + + NSMutableDictionary *dic = [[NSMutableDictionary alloc] init]; + dic[@"bluetoothAddress"] = macAddr; + dic[@"peripheralInfo"] = info; + [self.dataSource addObject:dic]; + } else { + NSLog(@"[BLE] %@", L(@"scan.invalidMACData")); } } - if(bytesAry.count >5){ - NSLog(@"%@:%@:%@:%@:%@:%@",bytesAry[0],bytesAry[1],bytesAry[2],bytesAry[3],bytesAry[4],bytesAry[5]); - if(_typeId == 4 || _typeId == 9) - { - [self requestNetwork:[NSString stringWithFormat:@"%@:%@:%@:%@:%@:%@",bytesAry[5],bytesAry[4],bytesAry[3],bytesAry[2],bytesAry[1],bytesAry[0]] info:info]; - }else - { - [self requestNetwork:[NSString stringWithFormat:@"%@:%@:%@:%@:%@:%@",bytesAry[0],bytesAry[1],bytesAry[2],bytesAry[3],bytesAry[4],bytesAry[5]] info:info]; + NSLog(@"[BLE] %@", L(@"scan.reloadingTable")); + [self.tableView reloadData]; + }); +} - } - - } - } - -} --(void)requestNetwork:(NSString *) macAddr info:(HKPeripheralInfo*)info -{ - NSMutableDictionary * dic = [[NSMutableDictionary alloc] init]; - [dic setObject:macAddr forKey:@"bluetoothAddress"]; - [dic setObject:info forKey:@"peripheralInfo"]; +- (void)requestNetwork:(NSString *)macAddr info:(HKPeripheralInfo *)info { + NSMutableDictionary *dic = [NSMutableDictionary dictionary]; + dic[@"bluetoothAddress"] = macAddr; + dic[@"peripheralInfo"] = info; [self.dataSource addObject:dic]; [self.tableView reloadData]; - + NSLog(@"%@", L(@"peripheralAdded")); } - @end diff --git "a/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIHomeViewController.m" "b/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIHomeViewController.m" index b3a5501..e824a6e 100644 --- "a/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIHomeViewController.m" +++ "b/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIHomeViewController.m" @@ -1,11 +1,10 @@ // // APIHomeViewController.m // QIUI-API +// Refactored by MrWizard // -// Created by mac on 2025/2/12. -// -#define XP_StatusBarAndNavigationBarHeight (XP_iPhoneX ? 92.f : 64.f) -// 判断是否为iPhone X 系列 这样写消除了在Xcode10上的警告。 + +#define XP_StatusBarAndNavigationBarHeight (XP_iPhoneX ? 92.f : 64.f) #define XP_iPhoneX \ ({BOOL isPhoneX = NO;\ if (@available(iOS 11.0, *)) {\ @@ -18,34 +17,65 @@ #import #import #import "NSMutableDictionary+JMJson.h" -//蓝牙 #import "HKBabyBluetoothManager.h" #import "AESCipher.h" - #import "APIBuletoothListViewController.h" +#import "QIUILocalization.h" -@interface APIHomeViewController () -@property (weak, nonatomic) IBOutlet UIView *mainView; +#pragma mark - API Endpoint Configuration +NSString *EncryptedBluetoothAddress(NSString *plainAddress) { + NSString *key = @"4DDC49E7D9A648348B5844E2479C5B22"; // Move to a constant or config + return [AESCipher aesEncryptString:plainAddress key:key]; +} +static BOOL const kUseProduction = YES; -@property (weak, nonatomic) IBOutlet UIButton * btn1;//获取API TOKEN -@property (weak, nonatomic) IBOutlet UIButton * btn2;//连接设备 -@property (weak, nonatomic) IBOutlet UIButton * btn3;//绑定设备 -@property (weak, nonatomic) IBOutlet UIButton * btn4;//获取TOKEN -@property (weak, nonatomic) IBOutlet UIButton * btn5;//开锁 -@property (weak, nonatomic) IBOutlet UIButton * btn6;//关锁 -@property (weak, nonatomic) IBOutlet UIButton * btn7;//断开连接 - -@property (copy, nonatomic) NSString * platformApiToken;//平台Api Token -@property (copy, nonatomic) NSString * expiresTime;//平台Api Token到期时间 -//蓝牙模块 -@property (strong, nonatomic) HKBabyBluetoothManager * babyMgr; -@property (nonatomic, strong) CBPeripheral *peripheral; +static NSString *APIBase() { + return kUseProduction ? + @"https://openapi.qiuitoy.com" : + @"http://192.168.31.163:8115"; +} -@property (copy, nonatomic) NSString * bluetoothAddress;//设备蓝牙地址 -@property (copy, nonatomic) NSString * serialNumber;//设备编码 -@property (nonatomic, copy) NSString * typeId; -@property (copy, nonatomic) NSString * deviceToken;//设备Token +static NSDictionary *APIEndpoints() { + NSString *base = APIBase(); + return @{ + @"getToken": [base stringByAppendingString:@"/system/api/device/common/getPlatformApiToken"], + @"addDevice": [base stringByAppendingString:@"/system/api/platform/device/addDeviceInfo"], + @"queryDevice": [base stringByAppendingString:@"/system/api/platform/device/queryDeviceInfo"], + @"getDeviceToken": [base stringByAppendingString:@"/system/api/device/cellMate/bluetooth/buildCellMatePro4GTokenCmd"], + @"getUnlockCmd": [base stringByAppendingString:@"/system/api/device/cellMate/bluetooth/buildCellMatePro4GUnLockCmd"], + @"getLockCmd": [base stringByAppendingString:@"/system/api/device/cellMate/bluetooth/buildCellMatePro4GLockCmd"], + @"decryptCmd": [base stringByAppendingString:@"/system/api/device/cellMate/decryBluetoothCommand"] + }; +} + +typedef NS_ENUM(NSInteger, BLEPendingOperation) { + BLEPendingNone = 0, + BLEPendingTokenHandshake, + BLEPendingUnlock, + BLEPendingLock +}; +@interface APIHomeViewController () + +@property (weak, nonatomic) IBOutlet UIView *mainView; +@property (weak, nonatomic) IBOutlet UIButton *btn1; +@property (weak, nonatomic) IBOutlet UIButton *btn2; +@property (weak, nonatomic) IBOutlet UIButton *btn3; +@property (weak, nonatomic) IBOutlet UIButton *btn4; +@property (weak, nonatomic) IBOutlet UIButton *btn5; +@property (weak, nonatomic) IBOutlet UIButton *btn6; +@property (weak, nonatomic) IBOutlet UIButton *btn7; + +@property (copy, nonatomic) NSString *platformApiToken; +@property (copy, nonatomic) NSString *expiresTime; +@property (strong, nonatomic) HKBabyBluetoothManager *babyMgr; +@property (nonatomic, strong) CBPeripheral *peripheral; +@property (copy, nonatomic) NSString *bluetoothAddress; +@property (copy, nonatomic) NSString *serialNumber; +@property (nonatomic, copy) NSString *typeId; +@property (copy, nonatomic) NSString *deviceToken; +@property (nonatomic, assign) BLEPendingOperation pendingBLEOperation; +@property (nonatomic, assign) BOOL tokenHandshakeComplete; @end @@ -53,512 +83,653 @@ @implementation APIHomeViewController - (void)viewDidLoad { [super viewDidLoad]; - - CGRect mianframe = self.mainView.frame; - mianframe.origin.y = XP_StatusBarAndNavigationBarHeight; - mianframe.size.height -= XP_StatusBarAndNavigationBarHeight; - self.mainView.frame = mianframe; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(connectToySynchronize:) name:@"connectToySynchronize" object:nil]; + CGRect frame = self.mainView.frame; + frame.origin.y = XP_StatusBarAndNavigationBarHeight; + frame.size.height -= XP_StatusBarAndNavigationBarHeight; + self.mainView.frame = frame; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(connectToySynchronize:) + name:@"connectToySynchronize" + object:nil]; +} + +- (void)showProgress:(NSString *)status { + MBProgressHUD *hud = [MBProgressHUD HUDForView:self.view]; + if (!hud) { + hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; + } + hud.mode = MBProgressHUDModeIndeterminate; + hud.label.text = status; + hud.label.numberOfLines = 0; } --(void)connectToySynchronize:(NSNotification*)notification -{ +- (void)connectToySynchronize:(NSNotification *)notification { NSDictionary *theData = [notification userInfo]; - _bluetoothAddress = [theData objectForKey:@"bluetoothAddress"]; - HKPeripheralInfo * info = [theData objectForKey:@"peripheralInfo"]; + _bluetoothAddress = theData[@"bluetoothAddress"]; + HKPeripheralInfo *info = theData[@"peripheralInfo"]; _peripheral = info.peripheral; - + NSLog(@"[BLE] Selected device: %@ (%@)", _peripheral.name, _bluetoothAddress); } +#pragma mark - BLE Delegate Methods - (void)connectSuccess { - // 连接成功 写入UUID值【替换成自己的蓝牙设备UUID值】 - _babyMgr.serverUUIDString = @"FFF0"; - _babyMgr.writeUUIDString = @"FFF1"; - _babyMgr.readUUIDString = @"FFF2"; - - [self functionUnsend:_deviceToken]; + _babyMgr.serverUUIDString = @"FEE7"; + _babyMgr.writeUUIDString = @"36F5"; + _babyMgr.readUUIDString = @"36F6"; + NSLog(@"[BLE] %@ — peripheral: %@ (state=%ld)", L(@"api.connectionSuccess"), _peripheral.name, (long)_peripheral.state); - NSLog(@"连接成功"); -} -//将获取到到token写入设备 --(void)functionUnsend:(NSString *)message -{ - NSData *data = [self hexToBytes:message]; - [_babyMgr write:data]; + [self showProgress:L(@"hud.fetchingToken")]; + [self fetchDeviceToken]; } -//str转nsdata --(NSData*)hexToBytes:(NSString*)str { - - NSString *string = str; - const char *buf = [string UTF8String]; - NSMutableData *data = [NSMutableData data]; - if (buf){ - long len = strlen(buf); - - char singleNumberString[3] = {'\0', '\0', '\0'}; - uint32_t singleNumber = 0; - for(uint32_t i = 0 ; i < len; i+=2) { - if ( ((i+1) < len) && isxdigit(buf[i]) && (isxdigit(buf[i+1]))) { - singleNumberString[0] = buf[i]; - singleNumberString[1] = buf[i + 1]; - sscanf(singleNumberString, "%x", &singleNumber); - uint8_t tmp = (uint8_t)(singleNumber & 0x000000FF); - [data appendBytes:(void *)(&tmp)length:1]; + +- (void)fetchDeviceToken { + NSLog(@"[HTTP] ===== FETCH DEVICE TOKEN ====="); + NSLog(@"[HTTP] BT: %@, SN: %@, typeId: %@", _bluetoothAddress, _serialNumber, _typeId); + + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + [params setJsonValue:_bluetoothAddress key:@"bluetoothAddress"]; + [params setJsonValue:_serialNumber key:@"serialNumber"]; + [params setJsonValue:_typeId key:@"typeId"]; + + NSString *urlStr = APIEndpoints()[@"getDeviceToken"]; + SBJson5Writer *writer = [[SBJson5Writer alloc] init]; + NSString *jsonStr = [writer stringWithObject:params]; + + NSLog(@"[HTTP] URL: %@", urlStr); + NSLog(@"[HTTP] Body: %@", jsonStr); + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr] + cachePolicy:0 + timeoutInterval:60]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [jsonStr dataUsingEncoding:NSUTF8StringEncoding]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request addValue:@"TEST" forHTTPHeaderField:@"Environment"]; + [request addValue:_platformApiToken forHTTPHeaderField:@"Authorization"]; + + AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; + manager.responseSerializer = [AFHTTPResponseSerializer serializer]; + + [[manager dataTaskWithRequest:request + uploadProgress:nil + downloadProgress:nil + completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { + [MBProgressHUD hideHUDForView:self.view animated:YES]; + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + + if (error) { + NSLog(@"[HTTP] Device token network error: %@", error.localizedDescription); + [self showAlert:L(@"api.getTokenError") message:error.localizedDescription]; + return; + } + + NSString *responseStr = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; + NSLog(@"[HTTP] Device Token HTTP %ld — Response: %@", (long)httpResponse.statusCode, responseStr); + + NSData *data = [responseStr dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *state = [NSString stringWithFormat:@"%@", json[@"code"]]; + + if ([state isEqualToString:@"200"]) { + id tokenData = json[@"data"]; + if (!tokenData || [tokenData isKindOfClass:[NSNull class]] || + ([tokenData isKindOfClass:[NSString class]] && [tokenData length] == 0)) { + NSLog(@"[HTTP] Device token is null/empty"); + _deviceToken = nil; + [self showAlert:L(@"api.deviceTokenTitle") message:L(@"cmd.noTokenReturned")]; } else { - break; + _deviceToken = [NSString stringWithFormat:@"%@", tokenData]; + NSLog(@"[HTTP] Device token: '%@' (len=%lu) — writing to BLE...", + _deviceToken, (unsigned long)_deviceToken.length); + + [self showProgress:L(@"hud.bleHandshake")]; + + // Write token command to BLE device for handshake + _pendingBLEOperation = BLEPendingTokenHandshake; + NSData *cmdData = [self dataFromHexString:_deviceToken]; + NSLog(@"[BLE] Writing token command: %lu bytes → %@", (unsigned long)cmdData.length, cmdData); + [_babyMgr write:cmdData]; } + } else { + NSString *msg = json[@"message"] ?: json[@"msg"] ?: L(@"api.getTokenError"); + NSLog(@"[HTTP] Get token error: code=%@, msg=%@", state, msg); + [self showAlert:L(@"api.getTokenError") message:[NSString stringWithFormat:@"Code: %@\n%@", state, msg]]; } - - } - - return data; + }] resume]; } -- (void)readData:(NSData *)data { - // 获取到蓝牙设备发来的数据 - NSLog(@"蓝牙发来的数据 = %@",[NSString stringWithFormat:@"%@",data]); - NSUInteger len = [data length]; - Byte *byteData = (Byte*)malloc(len); - memcpy(byteData, [data bytes], len); - - NSMutableArray * bytesAry = [[NSMutableArray alloc] init]; - - for (int i = 0; i < len; i++) { - NSLog(@"byteData : %hhu",byteData[i]); - NSString *hexString= [NSString stringWithFormat:@"%@",[[NSString alloc] initWithFormat:@"%1hhx",byteData[i]]]; - if(hexString.length <=1) - { - hexString = [NSString stringWithFormat:@"0%@",hexString]; - } - hexString = [hexString uppercaseStringWithLocale:[NSLocale currentLocale]]; - [bytesAry addObject:hexString]; - } - NSString * bytesStr = [bytesAry componentsJoinedByString:@""];; - NSLog(@"bytesStr%@",bytesStr); +- (void)readData:(NSData *)valueData { + NSString *hexResponse = [self hexStringFromData:valueData]; + NSLog(@"[BLE] ===== READ DATA ====="); + NSLog(@"[BLE] Raw bytes (%lu): %@", (unsigned long)valueData.length, valueData); + NSLog(@"[BLE] Hex: %@", hexResponse); + NSLog(@"[BLE] Pending operation: %ld", (long)_pendingBLEOperation); - - if(bytesStr.length == 32){ - [self functionDecryptToy:bytesStr]; + if (_pendingBLEOperation == BLEPendingNone) { + NSLog(@"[BLE] No pending operation — ignoring initial characteristic read"); + return; } + [self showProgress:L(@"hud.validating")]; + [self sendDecryptCommand:hexResponse]; +} + +- (void)connectFailed { + NSLog(@"[BLE] %@", L(@"api.connectionFailed")); + _pendingBLEOperation = BLEPendingNone; + [self showAlert:L(@"api.connectionFailed") message:L(@"api.connectionFailed")]; +} + +- (void)disconnectPeripheral:(CBPeripheral *)peripheral { + NSLog(@"[BLE] %@: %@", L(@"disconnect"), peripheral.name); + _pendingBLEOperation = BLEPendingNone; + _tokenHandshakeComplete = NO; } -//获取API TOKEN --(IBAction)selectorBtn1:(id)sender -{ +#pragma mark - Button Actions + +- (IBAction)selectorBtn1:(id)sender { + NSLog(@"[HTTP] Getting API token..."); [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - [params setJsonValue:@"Client_C11BAFF8287A495BB339BFF79E18A03E" key:@"clientId"];//平台的clientId - [params setJsonValue:@"client_credentials" key:@"grantType"];//授权方式,该参数为固定字符串'client_credentials',即客户端凭证模式 - //http://192.168.31.163:8115 - NSString *getPlatformApiToken = @"http://192.168.31.163:8115/system/api/device/common/getPlatformApiToken"; + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + // [params setJsonValue:@"Client_C11BAFF8287A495BB339BFF79E18A03E" key:@"clientId"]; + + [params setJsonValue:@"Client_B592ABDF952C4874868B5ABA91DF65BB" key:@"clientId"]; + [params setJsonValue:@"client_credentials" key:@"grantType"]; + + + NSString *urlStr = @"https://openapi.qiuitoy.com/system/api/device/common/getPlatformApiToken"; SBJson5Writer *writer = [[SBJson5Writer alloc] init]; NSString *jsonStr = [writer stringWithObject:params]; - NSURL *url = [NSURL URLWithString:getPlatformApiToken]; - //创建请求request - NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:60]; - //设置请求方式为POST + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr] + cachePolicy:0 + timeoutInterval:60]; request.HTTPMethod = @"POST"; - //设置请求内容格式 request.HTTPBody = [jsonStr dataUsingEncoding:NSUTF8StringEncoding]; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; [request setValue:@"TEST" forHTTPHeaderField:@"Environment"]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; - manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html",@"image/jpeg",@"text/plain", nil]; - - [[manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { - - NSString * str =[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; - NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *tempDictQueryDiamond = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *state = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"code"]]; + manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/plain", nil]; + + [[manager dataTaskWithRequest:request + uploadProgress:nil + downloadProgress:nil + completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { + if (error) { + NSLog(@"[HTTP] API Token network error: %@", error.localizedDescription); + [self showAlert:L(@"api.getTokenError") message:[NSString stringWithFormat:L(@"api.networkError"), error.localizedDescription]]; + [MBProgressHUD hideHUDForView:self.view animated:YES]; + return; + } + + NSString *responseStr = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; + NSLog(@"[HTTP] API Token Response: %@", responseStr); + + NSData *data = [responseStr dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *state = [NSString stringWithFormat:@"%@", json[@"code"]]; + if ([state isEqualToString:@"200"]) { - NSDictionary * data = tempDictQueryDiamond[@"data"]; - _platformApiToken = [NSString stringWithFormat:@"%@",[data objectForKey:@"platformApiToken"]]; - _expiresTime = [NSString stringWithFormat:@"%@",[data objectForKey:@"expiresTime"]]; - UIAlertController *alertC = [UIAlertController alertControllerWithTitle:@"API TOKEN" message:_platformApiToken preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *actionCancle1 = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - [self.navigationController popToViewController:self.navigationController.viewControllers[0] animated:YES]; - }]; - - [alertC addAction:actionCancle1]; - - [self presentViewController:alertC animated:YES completion:nil]; - - }else - { - - NSString *message = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"message"]]; - if([message isEqualToString:@"(null)"]){ - message = @"请检查网络连接是否正常"; - } - + NSLog(@"[HTTP] Token Received: %@", _platformApiToken); + NSDictionary *tokenData = json[@"data"]; + _platformApiToken = tokenData[@"platformApiToken"]; + _expiresTime = [NSString stringWithFormat:@"%@", tokenData[@"expiresTime"]]; + NSLog(@"[HTTP] API Token: %@", _platformApiToken); + + UIAlertController *alert = [UIAlertController alertControllerWithTitle:L(@"api.apiTokenTitle") + message:_platformApiToken + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:L(@"api.ok") + style:UIAlertActionStyleDefault + handler:nil]]; + [self presentViewController:alert animated:YES completion:nil]; + } else { + NSString *msg = json[@"message"] ?: json[@"msg"] ?: L(@"api.checkNetwork"); + NSLog(@"[HTTP] Error: %@", msg); + [self showAlert:L(@"api.getTokenError") message:msg]; } + [MBProgressHUD hideHUDForView:self.view animated:YES]; }] resume]; - } -//连接设备 --(IBAction)selectorBtn2:(id)sender -{ - APIBuletoothListViewController * list = [[APIBuletoothListViewController alloc] init]; +- (IBAction)selectorBtn2:(id)sender { + NSLog(@"[BLE] Navigating to device list..."); + APIBuletoothListViewController *list = [[APIBuletoothListViewController alloc] init]; [self.navigationController pushViewController:list animated:YES]; } -//绑定设备 --(IBAction)selectorBtn3:(id)sender -{ + +- (IBAction)selectorBtn3:(id)sender { + NSLog(@"[DEBUG] selectorBtn3: fired"); + if (!_platformApiToken || [_platformApiToken isEqualToString:@"(null)"]) { - //请先获取APITOKEN + NSLog(@"[DEBUG] No platform API token."); + [self showAlert:L(@"step.required") message:L(@"step.needToken")]; return; } + if (!_bluetoothAddress || [_bluetoothAddress isEqualToString:@"(null)"]) { - //请先连接设备 + NSLog(@"[DEBUG] No Bluetooth address."); + [self showAlert:L(@"step.required") message:L(@"step.needDevice")]; return; } + + NSLog(@"[HTTP] Binding device..."); [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + + NSLog(@"[DEBUG] Using platformApiToken: %@", _platformApiToken); + + + + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + NSString *encryptedAddress = EncryptedBluetoothAddress(_bluetoothAddress); + // [params setJsonValue:encryptedAddress key:@"bluetoothAddress"]; + [params setJsonValue:_bluetoothAddress key:@"bluetoothAddress"]; - NSString *addDeviceInfo = @"http://192.168.31.163:8115/system/api/platform/device/addDeviceInfo"; + NSString *urlStr = @"https://openapi.qiuitoy.com/system/api/platform/device/addDeviceInfo"; SBJson5Writer *writer = [[SBJson5Writer alloc] init]; - NSString *parametersStr = [writer stringWithObject:params]; + NSString *jsonStr = [writer stringWithObject:params]; - NSURL *url = [NSURL URLWithString:addDeviceInfo]; - //创建请求request - NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:60]; - //设置请求方式为POST + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr] + cachePolicy:0 + timeoutInterval:60]; request.HTTPMethod = @"POST"; - //设置请求内容格式 - request.HTTPBody = [parametersStr dataUsingEncoding:NSUTF8StringEncoding]; + request.HTTPBody = [jsonStr dataUsingEncoding:NSUTF8StringEncoding]; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; [request addValue:@"TEST" forHTTPHeaderField:@"Environment"]; [request addValue:_platformApiToken forHTTPHeaderField:@"Authorization"]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; - manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html",@"image/jpeg",@"text/plain", nil]; - - [[manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { - - NSString * decryptedText =[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; - NSData *data = [decryptedText dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *tempDictQueryDiamond = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *state = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"code"]]; - if ([state isEqualToString:@"200"]) { - NSDictionary * data = tempDictQueryDiamond[@"data"]; - NSString * message = [NSString stringWithFormat:@"createBy:%@ # createTime:%@ # environmentType:%@ # iccid:%@ # serialNumber:%@ # typeId:%@",data[@"createBy"],data[@"createTime"],data[@"environmentType"],data[@"iccid"],data[@"serialNumber"],data[@"typeId"]]; - UIAlertController *alertC = [UIAlertController alertControllerWithTitle:@"设备信息" message:message preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *actionCancle1 = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - [self.navigationController popToViewController:self.navigationController.viewControllers[0] animated:YES]; - }]; - - [alertC addAction:actionCancle1]; - - [self presentViewController:alertC animated:YES completion:nil]; - - _typeId = [NSString stringWithFormat:@"%@",data[@"typeId"]]; - _serialNumber = [NSString stringWithFormat:@"%@",data[@"serialNumber"]]; - }else - { - NSString *message = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"message"]]; - if([message isEqualToString:@"(null)"]){ - message = @"请检查网络连接是否正常"; - } - - UIAlertController *alertC = [UIAlertController alertControllerWithTitle:@"AddDeviceInfo" message:message preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *actionCancle1 = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - [self.navigationController popToViewController:self.navigationController.viewControllers[0] animated:YES]; - }]; + NSLog(@"[DEBUG] Binding request to: %@", urlStr); + NSLog(@"[DEBUG] Payload: %@", jsonStr); + [[manager dataTaskWithRequest:request + uploadProgress:nil + downloadProgress:nil + completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { + if (error) { + NSLog(@"[HTTP] Bind network error: %@", error.localizedDescription); + [self showAlert:L(@"api.addDeviceInfoError") message:[NSString stringWithFormat:L(@"api.networkError"), error.localizedDescription]]; + [MBProgressHUD hideHUDForView:self.view animated:YES]; + return; + } - [alertC addAction:actionCancle1]; + NSString *responseStr = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; + NSLog(@"[HTTP] Bind Response: %@", responseStr); + - [self presentViewController:alertC animated:YES completion:nil]; + NSData *data = [responseStr dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *state = [NSString stringWithFormat:@"%@", json[@"code"]]; + if ([state isEqualToString:@"200"]) { + NSDictionary *info = json[@"data"]; + _typeId = [NSString stringWithFormat:@"%@", info[@"typeId"]]; + _serialNumber = info[@"serialNumber"]; + + NSString *msg = [NSString stringWithFormat:@"createBy:%@ # createTime:%@ # environmentType:%@ # iccid:%@ # serialNumber:%@ # typeId:%@", + info[@"createBy"], info[@"createTime"], info[@"environmentType"], + info[@"iccid"], _serialNumber, _typeId]; + + [self showAlert:L(@"api.deviceInfoTitle") message:msg]; + } else { + NSString *msg = json[@"message"] ?: json[@"msg"] ?: L(@"api.checkNetwork"); + NSLog(@"[HTTP] Bind failed: %@", msg); + NSString *hint = [NSString stringWithFormat:@"%@\n\n%@", msg, L(@"step.alreadyBound")]; + [self showAlert:L(@"api.addDeviceInfoError") message:hint]; } + [MBProgressHUD hideHUDForView:self.view animated:YES]; }] resume]; - } -//获取设备信息 --(IBAction)selectorBtn4:(id)sender -{ + +- (IBAction)selectorBtn4:(id)sender { + NSLog(@"[HTTP] Fetching device info..."); + + if (!_platformApiToken || [_platformApiToken isEqualToString:@"(null)"]) { + [self showAlert:L(@"step.required") message:L(@"step.needToken")]; + return; + } + + if (!_bluetoothAddress || [_bluetoothAddress isEqualToString:@"(null)"]) { + [self showAlert:L(@"step.required") message:L(@"step.needDevice")]; + return; + } + [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + + NSMutableDictionary *params = [NSMutableDictionary dictionary]; [params setJsonValue:_bluetoothAddress key:@"bluetoothAddress"]; - NSString *addDeviceInfo = @"http://192.168.31.163:8115/system/api/platform/device/queryDeviceInfo"; + NSString *urlStr = @"https://openapi.qiuitoy.com/system/api/platform/device/queryDeviceInfo"; // ✅ Updated SBJson5Writer *writer = [[SBJson5Writer alloc] init]; - NSString *parametersStr = [writer stringWithObject:params]; + NSString *jsonStr = [writer stringWithObject:params]; - NSURL *url = [NSURL URLWithString:addDeviceInfo]; - //创建请求request - NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:60]; - //设置请求方式为POST + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr] + cachePolicy:0 + timeoutInterval:60]; request.HTTPMethod = @"POST"; - //设置请求内容格式 - request.HTTPBody = [parametersStr dataUsingEncoding:NSUTF8StringEncoding]; + request.HTTPBody = [jsonStr dataUsingEncoding:NSUTF8StringEncoding]; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; [request addValue:@"TEST" forHTTPHeaderField:@"Environment"]; [request addValue:_platformApiToken forHTTPHeaderField:@"Authorization"]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; - manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html",@"image/jpeg",@"text/plain", nil]; - - [[manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { - - NSString * decryptedText =[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; - NSData *data = [decryptedText dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *tempDictQueryDiamond = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *state = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"code"]]; + + [[manager dataTaskWithRequest:request + uploadProgress:nil + downloadProgress:nil + completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { + if (error) { + NSLog(@"[HTTP] Device info network error: %@", error.localizedDescription); + [self showAlert:L(@"api.deviceInfoError") message:[NSString stringWithFormat:L(@"api.networkError"), error.localizedDescription]]; + [MBProgressHUD hideHUDForView:self.view animated:YES]; + return; + } + + NSString *responseStr = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; + NSLog(@"[HTTP] Device Info Response: %@", responseStr); + + NSData *data = [responseStr dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *state = [NSString stringWithFormat:@"%@", json[@"code"]]; + if ([state isEqualToString:@"200"]) { - NSDictionary * data = tempDictQueryDiamond[@"data"]; - NSString * message = [NSString stringWithFormat:@"createBy:%@ # createTime:%@ # environmentType:%@ # iccid:%@ # serialNumber:%@ # typeId:%@",data[@"createBy"],data[@"createTime"],data[@"environmentType"],data[@"iccid"],data[@"serialNumber"],data[@"typeId"]]; - UIAlertController *alertC = [UIAlertController alertControllerWithTitle:@"设备信息" message:message preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *actionCancle1 = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - [self.navigationController popToViewController:self.navigationController.viewControllers[0] animated:YES]; - }]; - - [alertC addAction:actionCancle1]; - - [self presentViewController:alertC animated:YES completion:nil]; - - _typeId = [NSString stringWithFormat:@"%@",data[@"typeId"]]; - _serialNumber = [NSString stringWithFormat:@"%@",data[@"serialNumber"]]; - }else - { - NSString *message = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"message"]]; - if([message isEqualToString:@"(null)"]){ - message = @"请检查网络连接是否正常"; - } + NSDictionary *info = json[@"data"]; + _typeId = [NSString stringWithFormat:@"%@", info[@"typeId"]]; + _serialNumber = info[@"serialNumber"]; + + NSString *msg = [NSString stringWithFormat:@"createBy:%@ # createTime:%@ # environmentType:%@ # iccid:%@ # serialNumber:%@ # typeId:%@", + info[@"createBy"], info[@"createTime"], info[@"environmentType"], + info[@"iccid"], _serialNumber, _typeId]; + + [self showAlert:L(@"api.deviceInfoTitle") message:msg]; + } else { + NSString *msg = json[@"message"] ?: json[@"msg"] ?: L(@"api.checkNetwork"); + NSLog(@"[HTTP] Device info error: %@", msg); + [self showAlert:L(@"api.deviceInfoError") message:msg]; } + [MBProgressHUD hideHUDForView:self.view animated:YES]; }] resume]; - } -//获取TOKEN --(IBAction)selectorBtn5:(id)sender -{ - - [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - [params setJsonValue:_bluetoothAddress key:@"bluetoothAddress"];//平台的clientId - [params setJsonValue:_serialNumber key:@"serialNumber"];//Device Code - [params setJsonValue:_typeId key:@"typeId"];//设备编号 +- (IBAction)selectorBtn5:(id)sender { + NSLog(@"[BLE] Connecting to device..."); - NSString *addDeviceInfo = @"http://192.168.31.163:8115/system/api/device/common/getDeviceToken"; - SBJson5Writer *writer = [[SBJson5Writer alloc] init]; - NSString *parametersStr = [writer stringWithObject:params]; + if (!_platformApiToken || [_platformApiToken isEqualToString:@"(null)"]) { + [self showAlert:L(@"step.required") message:L(@"step.needToken")]; + return; + } - NSURL *url = [NSURL URLWithString:addDeviceInfo]; - //创建请求request - NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:60]; - //设置请求方式为POST - request.HTTPMethod = @"POST"; - //设置请求内容格式 - request.HTTPBody = [parametersStr dataUsingEncoding:NSUTF8StringEncoding]; - [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - [request addValue:@"TEST" forHTTPHeaderField:@"Environment"]; - [request addValue:_platformApiToken forHTTPHeaderField:@"Authorization"]; + if (!_bluetoothAddress || [_bluetoothAddress isEqualToString:@"(null)"]) { + [self showAlert:L(@"step.required") message:L(@"step.needDevice")]; + return; + } - AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; - manager.responseSerializer = [AFHTTPResponseSerializer serializer]; - manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html",@"image/jpeg",@"text/plain", nil]; - - [[manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { - - NSString * decryptedText =[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; - NSData *data = [decryptedText dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *tempDictQueryDiamond = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *state = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"code"]]; - if ([state isEqualToString:@"200"]) { - UIAlertController *alertC = [UIAlertController alertControllerWithTitle:@"设备TOKEN" message:tempDictQueryDiamond[@"data"] preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *actionCancle1 = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - [self.navigationController popToViewController:self.navigationController.viewControllers[0] animated:YES]; - }]; - - [alertC addAction:actionCancle1]; - - [self presentViewController:alertC animated:YES completion:nil]; - - _deviceToken = [NSString stringWithFormat:@"%@",tempDictQueryDiamond[@"data"]]; - - _babyMgr = [HKBabyBluetoothManager sharedManager]; - _babyMgr.delegate = self; - [_babyMgr connectPeripheral:_peripheral]; - - }else - { - NSString *message = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"message"]]; - if([message isEqualToString:@"(null)"]){ - message = @"请检查网络连接是否正常"; - } - } - [MBProgressHUD hideHUDForView:self.view animated:YES]; - }] resume]; + if (!_serialNumber || !_typeId) { + [self showAlert:L(@"step.required") message:L(@"step.needBind")]; + return; + } + + if (!_peripheral) { + [self showAlert:L(@"step.required") message:L(@"step.needDevice")]; + return; + } + + _tokenHandshakeComplete = NO; + NSLog(@"[BLE] Will connect to peripheral: %@", _peripheral); + + [self showProgress:L(@"hud.connecting")]; + _babyMgr = [HKBabyBluetoothManager sharedManager]; + _babyMgr.delegate = self; + [_babyMgr connectPeripheral:_peripheral]; } -//开锁 --(IBAction)selectorBtn6:(id)sender -{ - [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - [params setJsonValue:_bluetoothAddress key:@"bluetoothAddress"];//平台的clientId - [params setJsonValue:_serialNumber key:@"serialNumber"];//Device Code - [params setJsonValue:_typeId key:@"typeId"];//设备编号 +- (IBAction)selectorBtn6:(id)sender { + NSLog(@"[HTTP] Sending unlock command..."); - NSString *addDeviceInfo = @"http://192.168.31.163:8115/system/api/device/keyPod/getKeyPodUnlockCmd"; - SBJson5Writer *writer = [[SBJson5Writer alloc] init]; - NSString *parametersStr = [writer stringWithObject:params]; + if (!_babyMgr || !_peripheral || _peripheral.state != CBPeripheralStateConnected) { + [self showAlert:L(@"step.required") message:L(@"step.needConnect")]; + return; + } - NSURL *url = [NSURL URLWithString:addDeviceInfo]; - //创建请求request - NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:60]; - //设置请求方式为POST - request.HTTPMethod = @"POST"; - //设置请求内容格式 - request.HTTPBody = [parametersStr dataUsingEncoding:NSUTF8StringEncoding]; - [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - [request addValue:@"TEST" forHTTPHeaderField:@"Environment"]; - [request addValue:_platformApiToken forHTTPHeaderField:@"Authorization"]; + if (!_tokenHandshakeComplete) { + [self showAlert:L(@"step.required") message:L(@"step.needHandshake")]; + return; + } - AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; - manager.responseSerializer = [AFHTTPResponseSerializer serializer]; - manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html",@"image/jpeg",@"text/plain", nil]; - - [[manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { - - NSString * decryptedText =[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; - NSData *data = [decryptedText dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *tempDictQueryDiamond = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *state = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"code"]]; - if ([state isEqualToString:@"200"]) { - [self functionUnsend:tempDictQueryDiamond[@"data"]]; - }else - { - NSString *message = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"message"]]; - if([message isEqualToString:@"(null)"]){ - message = @"请检查网络连接是否正常"; - } - } - [MBProgressHUD hideHUDForView:self.view animated:YES]; - }] resume]; + _pendingBLEOperation = BLEPendingUnlock; + [self sendLockCommandWithURL:APIEndpoints()[@"getUnlockCmd"]]; +} + +- (IBAction)selectorBtn7:(id)sender { + NSLog(@"[HTTP] Sending lock command..."); + + if (!_babyMgr || !_peripheral || _peripheral.state != CBPeripheralStateConnected) { + [self showAlert:L(@"step.required") message:L(@"step.needConnect")]; + return; + } + + if (!_tokenHandshakeComplete) { + [self showAlert:L(@"step.required") message:L(@"step.needHandshake")]; + return; + } + _pendingBLEOperation = BLEPendingLock; + [self sendLockCommandWithURL:APIEndpoints()[@"getLockCmd"]]; } -//关锁 --(IBAction)selectorBtn7:(id)sender -{ - [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - [params setJsonValue:_bluetoothAddress key:@"bluetoothAddress"];//平台的clientId - [params setJsonValue:_serialNumber key:@"serialNumber"];//Device Code - [params setJsonValue:_typeId key:@"typeId"];//设备编号 - NSString *addDeviceInfo = @"http://192.168.31.163:8115/system/api/device/keyPod/getKeyPodLockCmd"; +- (IBAction)selectorBtn8:(id)sender { + NSLog(@"[BLE] Disconnecting from all devices."); + [_babyMgr stopScanPeripheral]; + [_babyMgr disconnectAllPeripherals]; +} + +#pragma mark - Helper Methods + +- (void)showAlert:(NSString *)title message:(NSString *)message { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:L(@"api.ok") style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alert animated:YES completion:nil]; +} + +- (void)sendLockCommandWithURL:(NSString *)urlStr { + [self showProgress:L(@"hud.sendingCommand")]; + + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + [params setJsonValue:_bluetoothAddress key:@"bluetoothAddress"]; + [params setJsonValue:_serialNumber key:@"serialNumber"]; + [params setJsonValue:_typeId key:@"typeId"]; + [params setJsonValue:_deviceToken key:@"deviceToken"]; + SBJson5Writer *writer = [[SBJson5Writer alloc] init]; - NSString *parametersStr = [writer stringWithObject:params]; + NSString *jsonStr = [writer stringWithObject:params]; - NSURL *url = [NSURL URLWithString:addDeviceInfo]; - //创建请求request - NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:60]; - //设置请求方式为POST + NSLog(@"[HTTP] ===== LOCK/UNLOCK REQUEST ====="); + NSLog(@"[HTTP] URL: %@", urlStr); + NSLog(@"[HTTP] Body: %@", jsonStr); + NSLog(@"[HTTP] Auth: %@", _platformApiToken); + NSLog(@"[HTTP] DeviceToken: %@ (len=%lu)", _deviceToken, (unsigned long)_deviceToken.length); + NSLog(@"[HTTP] BLE connected: %@, peripheral state: %ld", + _peripheral.name, (long)_peripheral.state); + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr] + cachePolicy:0 + timeoutInterval:60]; request.HTTPMethod = @"POST"; - //设置请求内容格式 - request.HTTPBody = [parametersStr dataUsingEncoding:NSUTF8StringEncoding]; + request.HTTPBody = [jsonStr dataUsingEncoding:NSUTF8StringEncoding]; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; [request addValue:@"TEST" forHTTPHeaderField:@"Environment"]; [request addValue:_platformApiToken forHTTPHeaderField:@"Authorization"]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; - manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html",@"image/jpeg",@"text/plain", nil]; - - [[manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { - - NSString * decryptedText =[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; - NSData *data = [decryptedText dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *tempDictQueryDiamond = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *state = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"code"]]; + + [[manager dataTaskWithRequest:request + uploadProgress:nil + downloadProgress:nil + completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + NSString *responseStr = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; + + NSLog(@"[HTTP] ===== LOCK/UNLOCK RESPONSE ====="); + NSLog(@"[HTTP] HTTP Status: %ld", (long)httpResponse.statusCode); + NSLog(@"[HTTP] Response: %@", responseStr); + + if (error) { + NSLog(@"[HTTP] Network error: %@", error.localizedDescription); + NSString *detail = [NSString stringWithFormat:@"Network error: %@\n\nURL: %@", error.localizedDescription, urlStr]; + [self showAlert:L(@"cmd.commandFailed") message:detail]; + [MBProgressHUD hideHUDForView:self.view animated:YES]; + return; + } + + NSData *data = [responseStr dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *state = [NSString stringWithFormat:@"%@", json[@"code"]]; + if ([state isEqualToString:@"200"]) { - [self functionUnsend:tempDictQueryDiamond[@"data"]]; - }else - { - NSString *message = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"message"]]; - if([message isEqualToString:@"(null)"]){ - message = @"请检查网络连接是否正常"; - } + NSLog(@"[HTTP] Command SUCCESS — data: %@", json[@"data"]); + [self showProgress:L(@"hud.sendingCommand")]; + [self functionUnsend:json[@"data"]]; + } else { + NSString *msg = json[@"message"] ?: json[@"msg"] ?: L(@"api.checkNetwork"); + NSLog(@"[HTTP] Lock/unlock command failed: code=%@, msg=%@", state, msg); + NSString *detail = [NSString stringWithFormat: + @"Code: %@\nMessage: %@\n\n— Request —\nURL: %@\nBT: %@\nSN: %@\ntypeId: %@\ndeviceToken: %@", + state, msg, urlStr, _bluetoothAddress, _serialNumber, _typeId, _deviceToken]; + [self showAlert:L(@"cmd.commandFailed") message:detail]; } + [MBProgressHUD hideHUDForView:self.view animated:YES]; }] resume]; - } -//断开连接 --(IBAction)selectorBtn8:(id)sender -{ - [_babyMgr stopScanPeripheral]; - [_babyMgr disconnectAllPeripherals]; +- (void)functionUnsend:(NSString *)hexCommand { + if (!hexCommand || hexCommand.length == 0) { + NSLog(@"[BLE] No command to send."); + return; + } + NSData *data = [self dataFromHexString:hexCommand]; + if (!data || data.length == 0) { + NSLog(@"[BLE] Failed to convert hex command to data."); + return; + } + + NSLog(@"[BLE] Writing command: %@ → %lu bytes", hexCommand, (unsigned long)data.length); + [_babyMgr write:data]; } -- (void)connectFailed { - // 连接失败、做连接失败的处理 +#pragma mark - Hex Conversion + +- (NSData *)dataFromHexString:(NSString *)hex { + hex = [hex stringByReplacingOccurrencesOfString:@" " withString:@""]; + NSMutableData *data = [NSMutableData dataWithCapacity:hex.length / 2]; + for (NSUInteger i = 0; i + 1 < hex.length; i += 2) { + unsigned int byte = 0; + [[NSScanner scannerWithString:[hex substringWithRange:NSMakeRange(i, 2)]] scanHexInt:&byte]; + uint8_t b = (uint8_t)byte; + [data appendBytes:&b length:1]; + } + return data; } -- (void)disconnectPeripheral:(CBPeripheral *)peripheral { - // 获取到当前断开的设备 这里可做断开UI提示处理 - + +- (NSString *)hexStringFromData:(NSData *)data { + const uint8_t *bytes = data.bytes; + NSMutableString *hex = [NSMutableString stringWithCapacity:data.length * 2]; + for (NSUInteger i = 0; i < data.length; i++) { + [hex appendFormat:@"%02X", bytes[i]]; + } + return hex; } -//解密蓝牙返回的命令 --(void)functionDecryptToy:(NSString *)str -{ - - [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - [params setJsonValue:str key:@"lockCommand"];//Command Returned by Device - [params setJsonValue:_serialNumber key:@"serialNumber"];//Device Code - NSString *addDeviceInfo = @"http://192.168.31.163:8115/system/api/device/keyPod/decryBluetoothCommand"; +#pragma mark - Decrypt BLE Command + +- (void)sendDecryptCommand:(NSString *)lockCommand { + NSLog(@"[HTTP] ===== DECRYPT BLE COMMAND ====="); + NSLog(@"[HTTP] lockCommand: %@ (len=%lu)", lockCommand, (unsigned long)lockCommand.length); + NSLog(@"[HTTP] serialNumber: %@", _serialNumber); + NSLog(@"[HTTP] pending op: %ld", (long)_pendingBLEOperation); + + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + [params setJsonValue:lockCommand key:@"lockCommand"]; + [params setJsonValue:_serialNumber key:@"serialNumber"]; + + NSString *urlStr = APIEndpoints()[@"decryptCmd"]; SBJson5Writer *writer = [[SBJson5Writer alloc] init]; - NSString *parametersStr = [writer stringWithObject:params]; + NSString *jsonStr = [writer stringWithObject:params]; + + NSLog(@"[HTTP] URL: %@", urlStr); + NSLog(@"[HTTP] Body: %@", jsonStr); - NSURL *url = [NSURL URLWithString:addDeviceInfo]; - //创建请求request - NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:60]; - //设置请求方式为POST + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr] + cachePolicy:0 + timeoutInterval:60]; request.HTTPMethod = @"POST"; - //设置请求内容格式 - request.HTTPBody = [parametersStr dataUsingEncoding:NSUTF8StringEncoding]; + request.HTTPBody = [jsonStr dataUsingEncoding:NSUTF8StringEncoding]; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; [request addValue:@"TEST" forHTTPHeaderField:@"Environment"]; [request addValue:_platformApiToken forHTTPHeaderField:@"Authorization"]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; - manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html",@"image/jpeg",@"text/plain", nil]; - - [[manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { - - NSString * decryptedText =[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; - NSData *data = [decryptedText dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *tempDictQueryDiamond = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *state = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"code"]]; - if ([state isEqualToString:@"200"]) { - }else - { - NSString *message = [NSString stringWithFormat:@"%@",[tempDictQueryDiamond objectForKey:@"message"]]; - if([message isEqualToString:@"(null)"]){ - message = @"请检查网络连接是否正常"; - } - } + + BLEPendingOperation op = _pendingBLEOperation; + + [[manager dataTaskWithRequest:request + uploadProgress:nil + downloadProgress:nil + completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; [MBProgressHUD hideHUDForView:self.view animated:YES]; - }] resume]; + if (error) { + NSLog(@"[HTTP] Decrypt network error: %@", error.localizedDescription); + [self showAlert:L(@"cmd.decryptFailed") message:error.localizedDescription]; + _pendingBLEOperation = BLEPendingNone; + return; + } + + NSString *responseStr = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; + NSLog(@"[HTTP] Decrypt HTTP %ld — Response: %@", (long)httpResponse.statusCode, responseStr); + NSData *data = [responseStr dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *state = [NSString stringWithFormat:@"%@", json[@"code"]]; + + if ([state isEqualToString:@"200"] || [state isEqualToString:@"0"]) { + NSDictionary *decryptData = json[@"data"]; + NSLog(@"[HTTP] Decrypt SUCCESS — data: %@", decryptData); + + if (op == BLEPendingTokenHandshake) { + _tokenHandshakeComplete = YES; + NSString *info = [NSString stringWithFormat:L(@"cmd.handshakeComplete"), + decryptData[@"battery"] ?: @"?", decryptData[@"isUnlocking"] ?: @"?"]; + [self showAlert:L(@"cmd.handshakeOK") message:info]; + } else { + NSString *action = (op == BLEPendingUnlock) ? L(@"cmd.unlock") : L(@"cmd.lock"); + NSString *info = [NSString stringWithFormat:L(@"cmd.actionDetail"), + action, decryptData[@"battery"] ?: @"?", decryptData[@"isUnlocking"] ?: @"?"]; + [self showAlert:[NSString stringWithFormat:L(@"cmd.actionSuccess"), action] message:info]; + } + } else { + NSString *msg = json[@"message"] ?: json[@"msg"] ?: L(@"api.checkNetwork"); + NSLog(@"[HTTP] Decrypt failed: code=%@, msg=%@", state, msg); + [self showAlert:L(@"cmd.decryptFailed") message:[NSString stringWithFormat:@"Code: %@\n%@\n\nlockCommand: %@", state, msg, lockCommand]]; + } + + _pendingBLEOperation = BLEPendingNone; + }] resume]; } @end diff --git "a/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIHomeViewController.xib" "b/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIHomeViewController.xib" index 321c019..832b649 100644 --- "a/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIHomeViewController.xib" +++ "b/QIUI-API/HOME/\344\272\214\344\273\243\351\222\245\345\214\231\347\233\222/VIEW CONTROLLER/APIHomeViewController.xib" @@ -45,90 +45,90 @@ - - - - - - - -