feat: add MQTT client for camera configuration and enhance camera management
This commit is contained in:
@@ -9,8 +9,9 @@ import {
|
|||||||
getRefreshToken,
|
getRefreshToken,
|
||||||
setAccessToken,
|
setAccessToken,
|
||||||
} from '@/utils/storage';
|
} from '@/utils/storage';
|
||||||
import { history, request, RequestConfig } from '@umijs/max';
|
import { history, RequestConfig } from '@umijs/max';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
// Trạng thái dùng chung cho cơ chế refresh token + hàng đợi
|
// Trạng thái dùng chung cho cơ chế refresh token + hàng đợi
|
||||||
let refreshingTokenPromise: Promise<string | null> | null = null;
|
let refreshingTokenPromise: Promise<string | null> | null = null;
|
||||||
@@ -125,7 +126,7 @@ export const handleRequestConfig: RequestConfig = {
|
|||||||
const isRefreshRequest = response.config.url?.includes(
|
const isRefreshRequest = response.config.url?.includes(
|
||||||
API_PATH_REFRESH_TOKEN,
|
API_PATH_REFRESH_TOKEN,
|
||||||
);
|
);
|
||||||
console.log('Is refresh request:', isRefreshRequest);
|
// console.log('Is refresh request:', isRefreshRequest);
|
||||||
|
|
||||||
// Xử lý 401: đưa request vào hàng đợi, gọi refresh token, rồi gọi lại
|
// Xử lý 401: đưa request vào hàng đợi, gọi refresh token, rồi gọi lại
|
||||||
if (
|
if (
|
||||||
@@ -161,19 +162,30 @@ export const handleRequestConfig: RequestConfig = {
|
|||||||
|
|
||||||
// Rebuild request options from config
|
// Rebuild request options from config
|
||||||
const originalConfig = response.config;
|
const originalConfig = response.config;
|
||||||
|
|
||||||
|
// Parse data if it is a JSON string to avoid double serialization when using axios directly
|
||||||
|
let data = originalConfig.data;
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parse error, use original string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newOptions = {
|
const newOptions = {
|
||||||
|
url: originalConfig.url,
|
||||||
method: originalConfig.method,
|
method: originalConfig.method,
|
||||||
headers: {
|
headers: {
|
||||||
...(originalConfig.headers || {}),
|
...(originalConfig.headers || {}),
|
||||||
Authorization: `${newToken}`,
|
Authorization: `${newToken}`,
|
||||||
},
|
},
|
||||||
data: originalConfig.data,
|
data: data,
|
||||||
params: originalConfig.params,
|
params: originalConfig.params,
|
||||||
skipAuthRefresh: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gọi lại request gốc với accessToken mới
|
// Gọi lại request gốc với accessToken mới bằng axios để tránh double-unwrap
|
||||||
return request(originalConfig.url, newOptions);
|
return axios(newOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
getRefreshToken,
|
getRefreshToken,
|
||||||
setAccessToken,
|
setAccessToken,
|
||||||
} from '@/utils/storage';
|
} from '@/utils/storage';
|
||||||
import { history, request, RequestConfig } from '@umijs/max';
|
import { history, RequestConfig } from '@umijs/max';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
// Trạng thái dùng chung cho cơ chế refresh token + hàng đợi
|
// Trạng thái dùng chung cho cơ chế refresh token + hàng đợi
|
||||||
let refreshingTokenPromise: Promise<string | null> | null = null;
|
let refreshingTokenPromise: Promise<string | null> | null = null;
|
||||||
@@ -52,7 +53,8 @@ export const handleRequestConfig: RequestConfig = {
|
|||||||
return (
|
return (
|
||||||
(status >= 200 && status < 300) ||
|
(status >= 200 && status < 300) ||
|
||||||
status === HTTPSTATUS.HTTP_NOTFOUND ||
|
status === HTTPSTATUS.HTTP_NOTFOUND ||
|
||||||
status === HTTPSTATUS.HTTP_UNAUTHORIZED
|
status === HTTPSTATUS.HTTP_UNAUTHORIZED ||
|
||||||
|
status === HTTPSTATUS.HTTP_FORBIDDEN
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
@@ -123,15 +125,17 @@ export const handleRequestConfig: RequestConfig = {
|
|||||||
// Unwrap data from backend response
|
// Unwrap data from backend response
|
||||||
responseInterceptors: [
|
responseInterceptors: [
|
||||||
async (response: any, options: any) => {
|
async (response: any, options: any) => {
|
||||||
const isRefreshRequest = response.url?.includes(API_PATH_REFRESH_TOKEN);
|
// const isRefreshRequest = response.url?.includes(API_PATH_REFRESH_TOKEN); // response.url may be undefined or different in prod
|
||||||
const alreadyRetried = options?.skipAuthRefresh === true;
|
const isRefreshRequest = response.config.url?.includes(
|
||||||
|
API_PATH_REFRESH_TOKEN,
|
||||||
|
);
|
||||||
|
// const alreadyRetried = options?.skipAuthRefresh === true;
|
||||||
|
|
||||||
// Xử lý 401: đưa request vào hàng đợi, gọi refresh token, rồi gọi lại
|
// Xử lý 401: đưa request vào hàng đợi, gọi refresh token, rồi gọi lại
|
||||||
if (
|
if (
|
||||||
response.status === HTTPSTATUS.HTTP_UNAUTHORIZED &&
|
response.status === HTTPSTATUS.HTTP_UNAUTHORIZED &&
|
||||||
// Không tự refresh cho chính API refresh token để tránh vòng lặp vô hạn
|
// Không tự refresh cho chính API refresh token để tránh vòng lặp vô hạn
|
||||||
!isRefreshRequest &&
|
!isRefreshRequest
|
||||||
!alreadyRetried
|
|
||||||
) {
|
) {
|
||||||
const newToken = await getValidAccessToken();
|
const newToken = await getValidAccessToken();
|
||||||
|
|
||||||
@@ -151,24 +155,35 @@ export const handleRequestConfig: RequestConfig = {
|
|||||||
|
|
||||||
// Rebuild request options from config
|
// Rebuild request options from config
|
||||||
const originalConfig = response.config;
|
const originalConfig = response.config;
|
||||||
|
|
||||||
|
// Parse data if it is a JSON string to avoid double serialization when using axios directly
|
||||||
|
let data = originalConfig.data;
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parse error, use original string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newOptions = {
|
const newOptions = {
|
||||||
|
url: originalConfig.url,
|
||||||
method: originalConfig.method,
|
method: originalConfig.method,
|
||||||
headers: {
|
headers: {
|
||||||
...(originalConfig.headers || {}),
|
...(originalConfig.headers || {}),
|
||||||
Authorization: `${newToken}`,
|
Authorization: `${newToken}`,
|
||||||
},
|
},
|
||||||
data: originalConfig.data,
|
data: data,
|
||||||
params: originalConfig.params,
|
params: originalConfig.params,
|
||||||
skipAuthRefresh: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gọi lại request gốc với accessToken mới
|
// Gọi lại request gốc với accessToken mới bằng axios để tránh double-unwrap
|
||||||
return request(originalConfig.url, newOptions);
|
return axios(newOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
response.status === HTTPSTATUS.HTTP_UNAUTHORIZED &&
|
response.status === HTTPSTATUS.HTTP_UNAUTHORIZED &&
|
||||||
(isRefreshRequest || alreadyRetried)
|
isRefreshRequest
|
||||||
) {
|
) {
|
||||||
clearAllData();
|
clearAllData();
|
||||||
history.push(ROUTE_LOGIN);
|
history.push(ROUTE_LOGIN);
|
||||||
|
|||||||
303
docs/mqtt-client.md
Normal file
303
docs/mqtt-client.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# MQTT Client - Hướng dẫn sử dụng
|
||||||
|
|
||||||
|
## Tổng quan
|
||||||
|
|
||||||
|
MQTT Client (`mqttClient`) là một utility singleton được thiết kế để quản lý kết nối MQTT trong ứng dụng. File này nằm tại `src/utils/mqttClient.ts`.
|
||||||
|
|
||||||
|
## Cài đặt
|
||||||
|
|
||||||
|
Package `mqtt` được cài đặt qua npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install mqtt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cấu trúc
|
||||||
|
|
||||||
|
```
|
||||||
|
src/utils/
|
||||||
|
├── mqttClient.ts # MQTT Client utility
|
||||||
|
└── wsClient.ts # WebSocket Client utility (legacy)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cách sử dụng
|
||||||
|
|
||||||
|
### 1. Import
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { mqttClient } from '@/utils/mqttClient';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Kết nối
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Lấy credentials từ user metadata
|
||||||
|
const { frontend_thing_id, frontend_thing_key } =
|
||||||
|
initialState?.currentUserProfile?.metadata || {};
|
||||||
|
|
||||||
|
// Kết nối
|
||||||
|
mqttClient.connect({
|
||||||
|
username: frontend_thing_id,
|
||||||
|
password: frontend_thing_key,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Đăng ký Event Handlers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Khi kết nối thành công
|
||||||
|
const unConnect = mqttClient.onConnect(() => {
|
||||||
|
console.log('MQTT Connected!');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Khi có lỗi
|
||||||
|
const unError = mqttClient.onError((error) => {
|
||||||
|
console.error('MQTT Error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Khi kết nối đóng
|
||||||
|
const unClose = mqttClient.onClose(() => {
|
||||||
|
console.log('MQTT Closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup khi component unmount
|
||||||
|
return () => {
|
||||||
|
unConnect();
|
||||||
|
unError();
|
||||||
|
unClose();
|
||||||
|
mqttClient.disconnect();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Publish Message
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const topic = `channels/${cfg_channel_id}/messages/cameraconfig/gmsv6`;
|
||||||
|
const payload = JSON.stringify(senmlData);
|
||||||
|
|
||||||
|
if (mqttClient.isConnected()) {
|
||||||
|
mqttClient.publish(topic, payload);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Subscribe Topic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Subscribe
|
||||||
|
mqttClient.subscribe('channels/123/messages/#');
|
||||||
|
|
||||||
|
// Nhận message
|
||||||
|
const unMessage = mqttClient.onMessage((topic, message, packet) => {
|
||||||
|
console.log('Received:', topic, message.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unsubscribe
|
||||||
|
mqttClient.unsubscribe('channels/123/messages/#');
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
| Method | Mô tả |
|
||||||
|
| ---------------------------- | ---------------------------------- |
|
||||||
|
| `connect(credentials, url?)` | Kết nối MQTT với username/password |
|
||||||
|
| `disconnect()` | Ngắt kết nối |
|
||||||
|
| `subscribe(topic)` | Subscribe vào topic |
|
||||||
|
| `unsubscribe(topic)` | Unsubscribe khỏi topic |
|
||||||
|
| `publish(topic, payload)` | Publish message |
|
||||||
|
| `onConnect(callback)` | Đăng ký callback khi kết nối |
|
||||||
|
| `onClose(callback)` | Đăng ký callback khi đóng |
|
||||||
|
| `onError(callback)` | Đăng ký callback khi lỗi |
|
||||||
|
| `onMessage(callback)` | Đăng ký callback khi nhận message |
|
||||||
|
| `isConnected()` | Kiểm tra trạng thái kết nối |
|
||||||
|
| `getClient()` | Lấy MQTT client gốc |
|
||||||
|
|
||||||
|
## Cấu hình Proxy (Development)
|
||||||
|
|
||||||
|
Trong môi trường development, MQTT được proxy qua config trong `config/proxy.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
"/mqtt": {
|
||||||
|
target: "https://gms.smatec.com.vn",
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
## Format SenML
|
||||||
|
|
||||||
|
Dữ liệu MQTT được format theo chuẩn SenML:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const senml = [
|
||||||
|
{
|
||||||
|
bn: `urn:dev:mac:${mac}:`, // Base name
|
||||||
|
n: 'ack', // Name
|
||||||
|
t: Date.now() / 1000, // Timestamp (seconds)
|
||||||
|
vs: uuidv4(), // Value string (ACK ID)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 'user@email.com', // Email (@ → :)
|
||||||
|
t: Date.now() / 1000,
|
||||||
|
vs: JSON.stringify(config), // Config dạng JSON string
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Topic Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
channels/${channel_id}/messages/${type}/${gw_type}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Phần | Mô tả |
|
||||||
|
| ------------ | ------------------------------------------------------- |
|
||||||
|
| `channel_id` | ID của kênh (từ `thing.metadata.cfg_channel_id`) |
|
||||||
|
| `type` | Loại message: `cameraconfig`, `config`, `log`, `notify` |
|
||||||
|
| `gw_type` | Loại gateway: `gmsv6`, `gmsv5` |
|
||||||
|
|
||||||
|
## Ví dụ hoàn chỉnh
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { mqttClient } from '@/utils/mqttClient';
|
||||||
|
import { useModel } from '@umijs/max';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const MyComponent = ({ thing }) => {
|
||||||
|
const { initialState } = useModel('@@initialState');
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { frontend_thing_id, frontend_thing_key } =
|
||||||
|
initialState?.currentUserProfile?.metadata || {};
|
||||||
|
|
||||||
|
if (!frontend_thing_id || !frontend_thing_key) return;
|
||||||
|
|
||||||
|
mqttClient.connect({
|
||||||
|
username: frontend_thing_id,
|
||||||
|
password: frontend_thing_key,
|
||||||
|
});
|
||||||
|
|
||||||
|
const unConnect = mqttClient.onConnect(() => setConnected(true));
|
||||||
|
const unClose = mqttClient.onClose(() => setConnected(false));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unConnect();
|
||||||
|
unClose();
|
||||||
|
mqttClient.disconnect();
|
||||||
|
};
|
||||||
|
}, [initialState]);
|
||||||
|
|
||||||
|
const handlePublish = () => {
|
||||||
|
const { cfg_channel_id, external_id } = thing.metadata;
|
||||||
|
const topic = `channels/${cfg_channel_id}/messages/cameraconfig/gmsv6`;
|
||||||
|
|
||||||
|
const payload = [
|
||||||
|
{
|
||||||
|
bn: `urn:dev:mac:${external_id.replaceAll('-', '')}:`,
|
||||||
|
n: 'ack',
|
||||||
|
t: Date.now() / 1000,
|
||||||
|
vs: uuidv4(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: initialState?.currentUserProfile?.email?.replaceAll('@', ':'),
|
||||||
|
t: Date.now() / 1000,
|
||||||
|
vs: JSON.stringify({ record_type: 'all' }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (mqttClient.isConnected()) {
|
||||||
|
mqttClient.publish(topic, JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handlePublish} disabled={!connected}>
|
||||||
|
{connected ? 'Publish' : 'Connecting...'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## So sánh mqttClient vs wsClient
|
||||||
|
|
||||||
|
Dự án có 2 utilities để giao tiếp real-time:
|
||||||
|
|
||||||
|
### Bảng so sánh
|
||||||
|
|
||||||
|
| Tiêu chí | mqttClient | wsClient |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Thư viện** | `mqtt` (MQTT.js) | `reconnecting-websocket` |
|
||||||
|
| **Protocol** | MQTT over WebSocket | WebSocket thuần |
|
||||||
|
| **Xác thực** | Username/Password (MQTT credentials) | Token (access_token) hoặc không |
|
||||||
|
| **Topics** | Hỗ trợ MQTT topics, subscribe/unsubscribe | Không có khái niệm topic |
|
||||||
|
| **Publish** | `publish(topic, payload)` | `send(data)` |
|
||||||
|
| **Subscribe** | `subscribe(topic)` + `onMessage()` | `subscribe(callback)` |
|
||||||
|
| **Reconnect** | Tự động (built-in) | Tự động (reconnecting-websocket) |
|
||||||
|
| **Use case** | Giao tiếp với IoT Gateway/Devices | Giao tiếp WebSocket server |
|
||||||
|
|
||||||
|
### Khi nào dùng mqttClient?
|
||||||
|
|
||||||
|
✅ **Dùng mqttClient khi:**
|
||||||
|
|
||||||
|
- Gửi/nhận dữ liệu với IoT Gateways (GMSv5, GMSv6)
|
||||||
|
- Cấu hình thiết bị (camera, nodes, schedules)
|
||||||
|
- Cần subscribe nhiều topics khác nhau
|
||||||
|
- Làm việc với Mainflux platform
|
||||||
|
|
||||||
|
### Khi nào dùng wsClient?
|
||||||
|
|
||||||
|
✅ **Dùng wsClient khi:**
|
||||||
|
|
||||||
|
- Cần WebSocket connection đơn giản
|
||||||
|
- Giao tiếp với WebSocket server không phải MQTT broker
|
||||||
|
- Xác thực bằng access_token
|
||||||
|
|
||||||
|
### Code comparison
|
||||||
|
|
||||||
|
**mqttClient:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { mqttClient } from '@/utils/mqttClient';
|
||||||
|
|
||||||
|
// Connect với username/password
|
||||||
|
mqttClient.connect({
|
||||||
|
username: 'thing_id',
|
||||||
|
password: 'thing_key',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe topic
|
||||||
|
mqttClient.subscribe('channels/123/messages/#');
|
||||||
|
|
||||||
|
// Publish với topic
|
||||||
|
mqttClient.publish('channels/123/messages/config', payload);
|
||||||
|
|
||||||
|
// Nhận message
|
||||||
|
mqttClient.onMessage((topic, message) => {
|
||||||
|
console.log(topic, message.toString());
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**wsClient:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { wsClient } from '@/utils/wsClient';
|
||||||
|
|
||||||
|
// Connect với hoặc không có token
|
||||||
|
wsClient.connect('/mqtt', false);
|
||||||
|
|
||||||
|
// Không có subscribe topic
|
||||||
|
// Chỉ nhận tất cả messages
|
||||||
|
wsClient.subscribe((data) => {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gửi data (không có topic)
|
||||||
|
wsClient.send({ action: 'update', data: payload });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Xem thêm
|
||||||
|
|
||||||
|
- [Mainflux.md](../Mainflux.md) - Tài liệu giao tiếp MQTT chi tiết
|
||||||
|
- [mqttClient.ts](../src/utils/mqttClient.ts) - Source code MQTT Client
|
||||||
|
- [wsClient.ts](../src/utils/wsClient.ts) - Source code WebSocket Client
|
||||||
373
package-lock.json
generated
373
package-lock.json
generated
@@ -14,13 +14,16 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"mqtt": "^5.15.0",
|
||||||
"ol": "^10.6.1",
|
"ol": "^10.6.1",
|
||||||
"react-chartjs-2": "^5.3.1",
|
"react-chartjs-2": "^5.3.1",
|
||||||
"reconnecting-websocket": "^4.4.0"
|
"reconnecting-websocket": "^4.4.0",
|
||||||
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.33",
|
"@types/react": "^18.0.33",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"baseline-browser-mapping": "^2.9.6",
|
"baseline-browser-mapping": "^2.9.6",
|
||||||
"husky": "^9",
|
"husky": "^9",
|
||||||
@@ -4044,6 +4047,15 @@
|
|||||||
"redux": ">= 3.7.2"
|
"redux": ">= 3.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/readable-stream": {
|
||||||
|
"version": "4.0.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz",
|
||||||
|
"integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.6",
|
"version": "1.20.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
|
||||||
@@ -4068,6 +4080,22 @@
|
|||||||
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
|
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "16.0.11",
|
"version": "16.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz",
|
||||||
@@ -7066,6 +7094,18 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/abort-controller": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -7952,6 +7992,58 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "6.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
|
||||||
|
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/readable-stream": "^4.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl/node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bn.js": {
|
"node_modules/bn.js": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
|
||||||
@@ -8037,6 +8129,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/broker-factory": {
|
||||||
|
"version": "3.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.13.tgz",
|
||||||
|
"integrity": "sha512-H2VALe31mEtO/SRcNp4cUU5BAm1biwhc/JaF77AigUuni/1YT0FLCJfbUxwIEs9y6Kssjk2fmXgf+Y9ALvmKlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"fast-unique-numbers": "^9.0.26",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-factory": "^7.0.48"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brorand": {
|
"node_modules/brorand": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
||||||
@@ -8707,6 +8811,12 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commist": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/common-path-prefix": {
|
"node_modules/common-path-prefix": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
|
||||||
@@ -8770,6 +8880,21 @@
|
|||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/connect-history-api-fallback": {
|
"node_modules/connect-history-api-fallback": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
|
||||||
@@ -10751,6 +10876,15 @@
|
|||||||
"es5-ext": "~0.10.14"
|
"es5-ext": "~0.10.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-target-shim": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eventemitter3": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
@@ -10963,6 +11097,19 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-unique-numbers": {
|
||||||
|
"version": "9.0.26",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.26.tgz",
|
||||||
|
"integrity": "sha512-3Mtq8p1zQinjGyWfKeuBunbuFoixG72AUkk4VvzbX4ykCW9Q4FzRaNyIlfQhUjnKw2ARVP+/CKnoyr6wfHftig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
@@ -11866,6 +12013,12 @@
|
|||||||
"he": "bin/he"
|
"he": "bin/he"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/help-me": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/history": {
|
"node_modules/history": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
||||||
@@ -12370,6 +12523,15 @@
|
|||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -14310,6 +14472,15 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimist-options": {
|
"node_modules/minimist-options": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
|
||||||
@@ -14351,6 +14522,95 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mqtt": {
|
||||||
|
"version": "5.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.0.tgz",
|
||||||
|
"integrity": "sha512-KC+wAssYk83Qu5bT8YDzDYgUJxPhbLeVsDvpY2QvL28PnXYJzC2WkKruyMUgBAZaQ7h9lo9k2g4neRNUUxzgMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/readable-stream": "^4.0.21",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"commist": "^3.2.0",
|
||||||
|
"concat-stream": "^2.0.0",
|
||||||
|
"debug": "^4.4.1",
|
||||||
|
"help-me": "^5.0.0",
|
||||||
|
"lru-cache": "^10.4.3",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"mqtt-packet": "^9.0.2",
|
||||||
|
"number-allocator": "^1.0.14",
|
||||||
|
"readable-stream": "^4.7.0",
|
||||||
|
"rfdc": "^1.4.1",
|
||||||
|
"socks": "^2.8.6",
|
||||||
|
"split2": "^4.2.0",
|
||||||
|
"worker-timers": "^8.0.23",
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mqtt": "build/bin/mqtt.js",
|
||||||
|
"mqtt_pub": "build/bin/pub.js",
|
||||||
|
"mqtt_sub": "build/bin/sub.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt-packet": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^6.0.8",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"process-nextick-args": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt/node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/mqtt/node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -14731,6 +14991,26 @@
|
|||||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/number-allocator": {
|
||||||
|
"version": "1.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz",
|
||||||
|
"integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.1",
|
||||||
|
"js-sdsl": "4.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/number-allocator/node_modules/js-sdsl": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/js-sdsl"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -18029,7 +18309,6 @@
|
|||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/rimraf": {
|
"node_modules/rimraf": {
|
||||||
@@ -18789,6 +19068,30 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||||
|
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.0.1",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sonic-boom": {
|
"node_modules/sonic-boom": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz",
|
||||||
@@ -21015,6 +21318,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -21358,6 +21667,19 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist-node/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache": {
|
"node_modules/v8-compile-cache": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz",
|
||||||
@@ -22198,6 +22520,53 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/worker-factory": {
|
||||||
|
"version": "7.0.48",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.48.tgz",
|
||||||
|
"integrity": "sha512-CGmBy3tJvpBPjUvb0t4PrpKubUsfkI1Ohg0/GGFU2RvA9j/tiVYwKU8O7yu7gH06YtzbeJLzdUR29lmZKn5pag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"fast-unique-numbers": "^9.0.26",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers": {
|
||||||
|
"version": "8.0.30",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.30.tgz",
|
||||||
|
"integrity": "sha512-8P7YoMHWN0Tz7mg+9oEhuZdjBIn2z6gfjlJqFcHiDd9no/oLnMGCARCDkV1LR3ccQus62ZdtIp7t3aTKrMLHOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-timers-broker": "^8.0.15",
|
||||||
|
"worker-timers-worker": "^9.0.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers-broker": {
|
||||||
|
"version": "8.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.15.tgz",
|
||||||
|
"integrity": "sha512-Te+EiVUMzG5TtHdmaBZvBrZSFNauym6ImDaCAnzQUxvjnw+oGjMT2idmAOgDy30vOZMLejd0bcsc90Axu6XPWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"broker-factory": "^3.1.13",
|
||||||
|
"fast-unique-numbers": "^9.0.26",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-timers-worker": "^9.0.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers-worker": {
|
||||||
|
"version": "9.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.13.tgz",
|
||||||
|
"integrity": "sha512-qjn18szGb1kjcmh2traAdki1eiIS5ikFo+L90nfMOvSRpuDw1hAcR1nzkP2+Hkdqz5thIRnfuWx7QSpsEUsA6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-factory": "^7.0.48"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||||
|
|||||||
@@ -25,13 +25,16 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"mqtt": "^5.15.0",
|
||||||
"ol": "^10.6.1",
|
"ol": "^10.6.1",
|
||||||
"react-chartjs-2": "^5.3.1",
|
"react-chartjs-2": "^5.3.1",
|
||||||
"reconnecting-websocket": "^4.4.0"
|
"reconnecting-websocket": "^4.4.0",
|
||||||
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.33",
|
"@types/react": "^18.0.33",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"baseline-browser-mapping": "^2.9.6",
|
"baseline-browser-mapping": "^2.9.6",
|
||||||
"husky": "^9",
|
"husky": "^9",
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ const LoginPage = () => {
|
|||||||
setInitialState((s: any) => ({
|
setInitialState((s: any) => ({
|
||||||
...s,
|
...s,
|
||||||
currentUserProfile: userInfo,
|
currentUserProfile: userInfo,
|
||||||
|
theme:
|
||||||
|
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') || 'light',
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -148,6 +150,9 @@ const LoginPage = () => {
|
|||||||
setInitialState((s: any) => ({
|
setInitialState((s: any) => ({
|
||||||
...s,
|
...s,
|
||||||
currentUserProfile: userInfo,
|
currentUserProfile: userInfo,
|
||||||
|
theme:
|
||||||
|
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') ||
|
||||||
|
'light',
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -175,6 +180,9 @@ const LoginPage = () => {
|
|||||||
setInitialState((s: any) => ({
|
setInitialState((s: any) => ({
|
||||||
...s,
|
...s,
|
||||||
currentUserProfile: userInfo,
|
currentUserProfile: userInfo,
|
||||||
|
theme:
|
||||||
|
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') ||
|
||||||
|
'light',
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Select,
|
Select,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
// Camera types
|
// Camera types
|
||||||
const CAMERA_TYPES = [
|
const CAMERA_TYPES = [
|
||||||
@@ -31,20 +32,58 @@ interface CameraFormValues {
|
|||||||
interface CameraFormModalProps {
|
interface CameraFormModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: (values: CameraFormValues) => void;
|
onSubmit: (camera: MasterModel.Camera) => void;
|
||||||
|
isOnline?: boolean;
|
||||||
|
editingCamera?: MasterModel.Camera | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
||||||
open,
|
open,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
isOnline = true,
|
||||||
|
editingCamera,
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm<CameraFormValues>();
|
const [form] = Form.useForm<CameraFormValues>();
|
||||||
|
const isEditMode = !!editingCamera;
|
||||||
|
|
||||||
|
// Populate form when editing
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && editingCamera) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: editingCamera.name || '',
|
||||||
|
type: editingCamera.cate_id || 'HIKVISION',
|
||||||
|
account: editingCamera.username || '',
|
||||||
|
password: editingCamera.password || '',
|
||||||
|
ipAddress: editingCamera.ip || '',
|
||||||
|
rtspPort: editingCamera.rtsp_port || 554,
|
||||||
|
httpPort: editingCamera.http_port || 80,
|
||||||
|
stream: editingCamera.stream || 0,
|
||||||
|
channel: editingCamera.channel || 0,
|
||||||
|
});
|
||||||
|
} else if (open) {
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
}, [open, editingCamera, form]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
onSubmit(values);
|
// Convert form values to MasterModel.Camera format
|
||||||
|
const camera: MasterModel.Camera = {
|
||||||
|
// Keep existing ID when editing, generate new ID when creating
|
||||||
|
id: editingCamera?.id || `cam_${Date.now()}`,
|
||||||
|
name: values.name,
|
||||||
|
cate_id: values.type,
|
||||||
|
ip: values.ipAddress,
|
||||||
|
rtsp_port: values.rtspPort,
|
||||||
|
http_port: values.httpPort,
|
||||||
|
stream: values.stream,
|
||||||
|
channel: values.channel,
|
||||||
|
username: values.account,
|
||||||
|
password: values.password,
|
||||||
|
};
|
||||||
|
onSubmit(camera);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Validation failed:', error);
|
console.error('Validation failed:', error);
|
||||||
@@ -58,7 +97,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Tạo mới camera"
|
title={isEditMode ? 'Chỉnh sửa camera' : 'Tạo mới camera'}
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
footer={[
|
footer={[
|
||||||
@@ -66,7 +105,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
|||||||
Hủy
|
Hủy
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button key="submit" type="primary" onClick={handleSubmit}>
|
<Button key="submit" type="primary" onClick={handleSubmit}>
|
||||||
Đồng ý
|
{isEditMode ? 'Cập nhật' : 'Đồng ý'}
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
width={500}
|
width={500}
|
||||||
|
|||||||
@@ -4,22 +4,30 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Button, Card, Checkbox, Space, Table, theme } from 'antd';
|
import { Button, Card, Space, Table, theme, Tooltip } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
interface CameraTableProps {
|
interface CameraTableProps {
|
||||||
cameraData: MasterModel.Camera[] | null;
|
cameraData: MasterModel.Camera[] | null;
|
||||||
onCreateCamera: () => void;
|
onCreateCamera: () => void;
|
||||||
|
onEditCamera?: (camera: MasterModel.Camera) => void;
|
||||||
|
onDeleteCameras?: (cameraIds: string[]) => void;
|
||||||
onReload?: () => void;
|
onReload?: () => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
isOnline?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CameraTable: React.FC<CameraTableProps> = ({
|
const CameraTable: React.FC<CameraTableProps> = ({
|
||||||
cameraData,
|
cameraData,
|
||||||
onCreateCamera,
|
onCreateCamera,
|
||||||
|
onEditCamera,
|
||||||
|
onDeleteCameras,
|
||||||
onReload,
|
onReload,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
isOnline = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
|
||||||
const handleReload = () => {
|
const handleReload = () => {
|
||||||
console.log('Reload cameras');
|
console.log('Reload cameras');
|
||||||
@@ -27,22 +35,18 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
console.log('Delete selected cameras');
|
if (selectedRowKeys.length === 0) {
|
||||||
// TODO: Implement delete functionality
|
return;
|
||||||
|
}
|
||||||
|
onDeleteCameras?.(selectedRowKeys as string[]);
|
||||||
|
setSelectedRowKeys([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (camera: MasterModel.Camera) => {
|
const handleEdit = (camera: MasterModel.Camera) => {
|
||||||
console.log('Edit camera:', camera);
|
onEditCamera?.(camera);
|
||||||
// TODO: Implement edit functionality
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
dataIndex: 'checkbox',
|
|
||||||
width: 50,
|
|
||||||
render: () => <Checkbox />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Tên',
|
title: 'Tên',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
@@ -67,11 +71,14 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
|||||||
title: 'Thao tác',
|
title: 'Thao tác',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
render: (_: any, record: MasterModel.Camera) => (
|
render: (_: any, record: MasterModel.Camera) => (
|
||||||
<Button
|
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
||||||
size="small"
|
<Button
|
||||||
icon={<EditOutlined />}
|
size="small"
|
||||||
onClick={() => handleEdit(record)}
|
icon={<EditOutlined />}
|
||||||
/>
|
onClick={() => handleEdit(record)}
|
||||||
|
disabled={!isOnline}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -79,11 +86,29 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Card bodyStyle={{ padding: 16 }}>
|
<Card bodyStyle={{ padding: 16 }}>
|
||||||
<Space style={{ marginBottom: 16 }}>
|
<Space style={{ marginBottom: 16 }}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={onCreateCamera}>
|
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
||||||
Tạo mới camera
|
<Button
|
||||||
</Button>
|
type="primary"
|
||||||
<Button icon={<ReloadOutlined />} onClick={handleReload} />
|
icon={<PlusOutlined />}
|
||||||
<Button icon={<DeleteOutlined />} onClick={handleDelete} />
|
onClick={onCreateCamera}
|
||||||
|
disabled={!isOnline}
|
||||||
|
>
|
||||||
|
Tạo mới camera
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={handleReload}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
||||||
|
<Button
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={!isOnline || selectedRowKeys.length === 0}
|
||||||
|
danger
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
@@ -92,6 +117,12 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
size="small"
|
size="small"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
rowSelection={{
|
||||||
|
type: 'checkbox',
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (newSelectedRowKeys) =>
|
||||||
|
setSelectedRowKeys(newSelectedRowKeys),
|
||||||
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
size: 'small',
|
size: 'small',
|
||||||
showTotal: (total: number, range: [number, number]) =>
|
showTotal: (total: number, range: [number, number]) =>
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { apiQueryConfigAlarm } from '@/services/master/MessageController';
|
import { apiQueryConfigAlarm } from '@/services/master/MessageController';
|
||||||
import { useModel } from '@umijs/max';
|
import { useModel } from '@umijs/max';
|
||||||
import { Button, Card, Col, Row, Select, theme, Typography } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
theme,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@@ -15,15 +24,24 @@ const RECORDING_MODES = [
|
|||||||
interface CameraV6Props {
|
interface CameraV6Props {
|
||||||
thing: MasterModel.Thing | null;
|
thing: MasterModel.Thing | null;
|
||||||
cameraConfig?: MasterModel.CameraV6 | null;
|
cameraConfig?: MasterModel.CameraV6 | null;
|
||||||
|
onSubmit?: (config: {
|
||||||
|
recordingMode: MasterModel.CameraV6['record_type'];
|
||||||
|
selectedAlerts: string[];
|
||||||
|
}) => void;
|
||||||
|
isOnline?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
const CameraV6: React.FC<CameraV6Props> = ({
|
||||||
|
thing,
|
||||||
|
cameraConfig,
|
||||||
|
onSubmit,
|
||||||
|
isOnline = false,
|
||||||
|
}) => {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const { initialState } = useModel('@@initialState');
|
const { initialState } = useModel('@@initialState');
|
||||||
const [selectedAlerts, setSelectedAlerts] = useState<string[]>([]);
|
const [selectedAlerts, setSelectedAlerts] = useState<string[]>([]);
|
||||||
const [recordingMode, setRecordingMode] = useState<'none' | 'alarm' | 'all'>(
|
const [recordingMode, setRecordingMode] =
|
||||||
'none',
|
useState<MasterModel.CameraV6['record_type']>('none');
|
||||||
);
|
|
||||||
const [alarmConfig, setAlarmConfig] = useState<MasterModel.Alarm[] | null>(
|
const [alarmConfig, setAlarmConfig] = useState<MasterModel.Alarm[] | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -95,31 +113,39 @@ const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
|||||||
setSelectedAlerts([]);
|
setSelectedAlerts([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitAlerts = () => {
|
const handleSubmitConfig = () => {
|
||||||
console.log('Submit alerts:', {
|
onSubmit?.({
|
||||||
recordingMode,
|
recordingMode,
|
||||||
selectedAlerts,
|
selectedAlerts,
|
||||||
});
|
});
|
||||||
// TODO: Call API to save alert configuration
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
{/* Recording Mode */}
|
{/* Recording Mode */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Text strong className="block mb-2">
|
<div className="flex flex-col sm:flex-row gap-2 sm:gap-4 items-start sm:items-end">
|
||||||
Ghi dữ liệu camera
|
<div className="w-full sm:w-1/3 lg:w-1/4">
|
||||||
</Text>
|
<Text strong className="block mb-2">
|
||||||
<div className="flex gap-8 items-center">
|
Ghi dữ liệu camera
|
||||||
<Select
|
</Text>
|
||||||
value={recordingMode}
|
<Select
|
||||||
onChange={setRecordingMode}
|
value={recordingMode}
|
||||||
options={RECORDING_MODES}
|
onChange={setRecordingMode}
|
||||||
className="w-full sm:w-1/2 md:w-1/3 lg:w-1/4"
|
options={RECORDING_MODES}
|
||||||
/>
|
className="w-full"
|
||||||
<Button type="primary" onClick={handleSubmitAlerts}>
|
popupMatchSelectWidth={false}
|
||||||
Gửi đi
|
/>
|
||||||
</Button>
|
</div>
|
||||||
|
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSubmitConfig}
|
||||||
|
disabled={!isOnline}
|
||||||
|
>
|
||||||
|
Gửi đi
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -155,7 +181,7 @@ const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
|||||||
size="small"
|
size="small"
|
||||||
hoverable
|
hoverable
|
||||||
onClick={() => handleAlertToggle(alarmId)}
|
onClick={() => handleAlertToggle(alarmId)}
|
||||||
className="cursor-pointer h-20 flex items-center justify-center"
|
className="cursor-pointer h-24 flex items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
borderColor: isSelected
|
borderColor: isSelected
|
||||||
? token.colorPrimary
|
? token.colorPrimary
|
||||||
@@ -166,14 +192,21 @@ const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
|||||||
: token.colorBgContainer,
|
: token.colorBgContainer,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="p-2 text-center w-full">
|
<div className="p-1 text-center w-full flex items-center justify-center h-full">
|
||||||
<Text
|
<Text
|
||||||
className="text-xs break-words"
|
className="text-xs"
|
||||||
style={{
|
style={{
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? token.colorPrimary
|
? token.colorPrimary
|
||||||
: token.colorText,
|
: token.colorText,
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 3,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
lineHeight: '1.2em',
|
||||||
}}
|
}}
|
||||||
|
title={alarm.name}
|
||||||
>
|
>
|
||||||
{alarm.name}
|
{alarm.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { apiQueryCamera } from '@/services/master/MessageController';
|
import { apiQueryCamera } from '@/services/master/MessageController';
|
||||||
import { apiGetThingDetail } from '@/services/master/ThingController';
|
import { apiGetThingDetail } from '@/services/master/ThingController';
|
||||||
import { wsClient } from '@/utils/wsClient';
|
import { mqttClient } from '@/utils/mqttClient';
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { history, useModel, useParams } from '@umijs/max';
|
import { history, useModel, useParams } from '@umijs/max';
|
||||||
import { Button, Col, Row, Space, Spin } from 'antd';
|
import { Badge, Button, Col, message, Row, Space, Spin } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import CameraFormModal from './components/CameraFormModal';
|
import CameraFormModal from './components/CameraFormModal';
|
||||||
import CameraTable from './components/CameraTable';
|
import CameraTable from './components/CameraTable';
|
||||||
import ConfigCameraV5 from './components/ConfigCameraV5';
|
import ConfigCameraV5 from './components/ConfigCameraV5';
|
||||||
@@ -21,18 +22,53 @@ const CameraConfigPage = () => {
|
|||||||
const [cameraConfig, setCameraConfig] = useState<MasterModel.CameraV6 | null>(
|
const [cameraConfig, setCameraConfig] = useState<MasterModel.CameraV6 | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [mqttConnected, setMqttConnected] = useState(false);
|
||||||
|
const [isOnline, setIsOnline] = useState(false);
|
||||||
|
const [editingCamera, setEditingCamera] = useState<MasterModel.Camera | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const { initialState } = useModel('@@initialState');
|
const { initialState } = useModel('@@initialState');
|
||||||
|
|
||||||
|
// Initialize MQTT connection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
wsClient.connect('/mqtt', false);
|
const { frontend_thing_id, frontend_thing_key } =
|
||||||
const unsubscribe = wsClient.subscribe((data: any) => {
|
initialState?.currentUserProfile?.metadata || {};
|
||||||
console.log('Received WS data:', data);
|
|
||||||
|
if (!frontend_thing_id || !frontend_thing_key) return;
|
||||||
|
|
||||||
|
// Connect using mqttClient utility
|
||||||
|
mqttClient.connect({
|
||||||
|
username: frontend_thing_id,
|
||||||
|
password: frontend_thing_key,
|
||||||
|
});
|
||||||
|
|
||||||
|
const unConnect = mqttClient.onConnect(() => {
|
||||||
|
console.log('MQTT Connected successfully!');
|
||||||
|
setMqttConnected(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const unError = mqttClient.onError((error) => {
|
||||||
|
console.error('MQTT Error:', error);
|
||||||
|
setMqttConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const unClose = mqttClient.onClose(() => {
|
||||||
|
console.log('MQTT Connection closed');
|
||||||
|
setMqttConnected(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unConnect();
|
||||||
|
unError();
|
||||||
|
unClose();
|
||||||
|
mqttClient.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [initialState]);
|
||||||
|
|
||||||
|
// Check device online status using connected property
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOnline(thing?.metadata?.connected ?? false);
|
||||||
|
}, [thing]);
|
||||||
|
|
||||||
// Fetch thing info on mount
|
// Fetch thing info on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -99,14 +135,120 @@ const CameraConfigPage = () => {
|
|||||||
|
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
|
setEditingCamera(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitCamera = (values: any) => {
|
// Core function to send camera config via MQTT
|
||||||
console.log('Camera values:', values);
|
const sendCameraConfig = (configPayload: MasterModel.CameraV6) => {
|
||||||
// TODO: Call API to create camera
|
// Check if device is online
|
||||||
|
if (!isOnline) {
|
||||||
|
message.error('Thiết bị đang ngoại tuyến, không thể gửi cấu hình');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!thing?.metadata?.cfg_channel_id || !thing?.metadata?.external_id) {
|
||||||
|
message.error('Thiếu thông tin cấu hình thiết bị');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cfg_channel_id, external_id } = thing.metadata;
|
||||||
|
const pubTopic = `channels/${cfg_channel_id}/messages/cameraconfig/${thing.metadata?.type}`;
|
||||||
|
const mac = external_id?.replaceAll('-', '');
|
||||||
|
const ack = uuidv4();
|
||||||
|
const now = Date.now() / 1000;
|
||||||
|
|
||||||
|
const senml: MasterModel.CameraV6ConfigRequest[] = [
|
||||||
|
{
|
||||||
|
bn: `urn:dev:mac:${mac?.toLowerCase()}:`,
|
||||||
|
n: 'ack',
|
||||||
|
t: now,
|
||||||
|
vs: ack,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: initialState?.currentUserProfile?.email?.replaceAll('@', ':') || '',
|
||||||
|
vs: JSON.stringify(configPayload),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const payload = JSON.stringify(senml);
|
||||||
|
|
||||||
|
if (mqttClient.isConnected()) {
|
||||||
|
mqttClient.publish(pubTopic, payload);
|
||||||
|
message.success('Đã gửi cấu hình thành công');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
message.error('MQTT chưa kết nối');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle camera list config submission (add/edit/delete cameras)
|
||||||
|
const handleSubmitCameraConfig = (updatedCams: MasterModel.Camera[]) => {
|
||||||
|
const configPayload: MasterModel.CameraV6 = {
|
||||||
|
cams: updatedCams,
|
||||||
|
record_type: cameraConfig?.record_type || 'all',
|
||||||
|
...(cameraConfig?.record_type === 'alarm' && {
|
||||||
|
record_alarm_list: cameraConfig?.record_alarm_list || [],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sendCameraConfig(configPayload)) {
|
||||||
|
// Update local state
|
||||||
|
setCameraConfig(configPayload);
|
||||||
|
setCameras(updatedCams);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle open edit modal - open modal with camera data for editing
|
||||||
|
const handleOpenEditModal = (camera: MasterModel.Camera) => {
|
||||||
|
setEditingCamera(camera);
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle submit camera (add or edit)
|
||||||
|
const handleSubmitCamera = (camera: MasterModel.Camera) => {
|
||||||
|
if (editingCamera) {
|
||||||
|
// Edit mode: update existing camera
|
||||||
|
const updatedCams = (cameraConfig?.cams || []).map((cam) =>
|
||||||
|
cam.id === camera.id ? camera : cam,
|
||||||
|
);
|
||||||
|
handleSubmitCameraConfig(updatedCams);
|
||||||
|
} else {
|
||||||
|
// Add mode: add new camera
|
||||||
|
const updatedCams = [...(cameraConfig?.cams || []), camera];
|
||||||
|
handleSubmitCameraConfig(updatedCams);
|
||||||
|
}
|
||||||
handleCloseModal();
|
handleCloseModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle delete cameras - submit config with cameras removed
|
||||||
|
const handleDeleteCameras = (cameraIds: string[]) => {
|
||||||
|
const updatedCams = (cameraConfig?.cams || []).filter(
|
||||||
|
(cam) => !cameraIds.includes(cam.id || ''),
|
||||||
|
);
|
||||||
|
handleSubmitCameraConfig(updatedCams);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle recording config submission from ConfigCameraV6
|
||||||
|
const handleSubmitConfig = (configPayload: {
|
||||||
|
recordingMode: MasterModel.CameraV6['record_type'];
|
||||||
|
selectedAlerts: string[];
|
||||||
|
}) => {
|
||||||
|
// Chỉ gửi record_alarm_list khi record_type = alarm
|
||||||
|
const fullConfig: MasterModel.CameraV6 = {
|
||||||
|
cams: cameraConfig?.cams || [],
|
||||||
|
record_type: configPayload.recordingMode,
|
||||||
|
...(configPayload.recordingMode === 'alarm' && {
|
||||||
|
record_alarm_list: configPayload.selectedAlerts,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sendCameraConfig(fullConfig)) {
|
||||||
|
// Update local state
|
||||||
|
setCameraConfig(fullConfig);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Helper function to determine which camera component to render
|
// Helper function to determine which camera component to render
|
||||||
const renderCameraRecordingComponent = () => {
|
const renderCameraRecordingComponent = () => {
|
||||||
const thingType = thing?.metadata?.type;
|
const thingType = thing?.metadata?.type;
|
||||||
@@ -116,10 +258,24 @@ const CameraConfigPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (thingType === 'spole' || thingType === 'gmsv6') {
|
if (thingType === 'spole' || thingType === 'gmsv6') {
|
||||||
return <ConfigCameraV6 thing={thing} cameraConfig={cameraConfig} />;
|
return (
|
||||||
|
<ConfigCameraV6
|
||||||
|
thing={thing}
|
||||||
|
cameraConfig={cameraConfig}
|
||||||
|
onSubmit={handleSubmitConfig}
|
||||||
|
isOnline={isOnline}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ConfigCameraV6 thing={thing} cameraConfig={cameraConfig} />;
|
return (
|
||||||
|
<ConfigCameraV6
|
||||||
|
thing={thing}
|
||||||
|
cameraConfig={cameraConfig}
|
||||||
|
onSubmit={handleSubmitConfig}
|
||||||
|
isOnline={isOnline}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -134,6 +290,10 @@ const CameraConfigPage = () => {
|
|||||||
onClick={() => history.push('/manager/devices')}
|
onClick={() => history.push('/manager/devices')}
|
||||||
/>
|
/>
|
||||||
<span>{thing?.name || 'Loading...'}</span>
|
<span>{thing?.name || 'Loading...'}</span>
|
||||||
|
<Badge
|
||||||
|
status={isOnline ? 'success' : 'default'}
|
||||||
|
text={isOnline ? 'Trực tuyến' : 'Ngoại tuyến'}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
@@ -144,8 +304,11 @@ const CameraConfigPage = () => {
|
|||||||
<CameraTable
|
<CameraTable
|
||||||
cameraData={cameras}
|
cameraData={cameras}
|
||||||
onCreateCamera={handleOpenModal}
|
onCreateCamera={handleOpenModal}
|
||||||
|
onEditCamera={handleOpenEditModal}
|
||||||
|
onDeleteCameras={handleDeleteCameras}
|
||||||
onReload={fetchCameraConfig}
|
onReload={fetchCameraConfig}
|
||||||
loading={cameraLoading}
|
loading={cameraLoading}
|
||||||
|
isOnline={isOnline}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
@@ -155,11 +318,13 @@ const CameraConfigPage = () => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{/* Create Camera Modal */}
|
{/* Create/Edit Camera Modal */}
|
||||||
<CameraFormModal
|
<CameraFormModal
|
||||||
open={isModalVisible}
|
open={isModalVisible}
|
||||||
onCancel={handleCloseModal}
|
onCancel={handleCloseModal}
|
||||||
onSubmit={handleSubmitCamera}
|
onSubmit={handleSubmitCamera}
|
||||||
|
isOnline={isOnline}
|
||||||
|
editingCamera={editingCamera}
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ export async function apiQueryLogs(
|
|||||||
params: MasterModel.SearchLogPaginationBody,
|
params: MasterModel.SearchLogPaginationBody,
|
||||||
type: MasterModel.LogTypeRequest,
|
type: MasterModel.LogTypeRequest,
|
||||||
) {
|
) {
|
||||||
return request<MasterModel.LogResponse>(`${API_READER}/${type}/messages`, {
|
return request<MasterModel.MesageReaderResponse>(
|
||||||
params: params,
|
`${API_READER}/${type}/messages`,
|
||||||
});
|
{
|
||||||
|
params: params,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/services/master/typings/camera.d.ts
vendored
Normal file
36
src/services/master/typings/camera.d.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
declare namespace MasterModel {
|
||||||
|
interface Camera {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
cate_id?: string;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
rtsp_port?: number;
|
||||||
|
http_port?: number;
|
||||||
|
channel?: number;
|
||||||
|
ip?: string;
|
||||||
|
stream?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CameraV5 {
|
||||||
|
cams?: Camera[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CameraV6 extends CameraV5 {
|
||||||
|
record_type?: 'none' | 'alarm' | 'all';
|
||||||
|
record_alarm_list?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type CameraMessage = Message<CameraV5>;
|
||||||
|
type CameraV6Message = Message<CameraV6>;
|
||||||
|
|
||||||
|
type CameraMessageResponse = MesageReaderResponse<CameraV5>;
|
||||||
|
type CameraV6MessageResponse = MesageReaderResponse<CameraV6>;
|
||||||
|
|
||||||
|
interface CameraV6ConfigRequest {
|
||||||
|
bn?: string;
|
||||||
|
n?: string;
|
||||||
|
t?: number;
|
||||||
|
vs?: string | CameraV6;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/services/master/typings/log.d.ts
vendored
27
src/services/master/typings/log.d.ts
vendored
@@ -29,12 +29,10 @@ declare namespace MasterModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Response types cho từng domain
|
// Response types cho từng domain
|
||||||
type CameraMessageResponse = MesageReaderResponse<CameraV5>;
|
|
||||||
type CameraV6MessageResponse = MesageReaderResponse<CameraV6>;
|
|
||||||
type AlarmMessageResponse = MesageReaderResponse<Alarm>;
|
type AlarmMessageResponse = MesageReaderResponse<Alarm>;
|
||||||
type NodeConfigMessageResponse = MesageReaderResponse<NodeConfig[]>;
|
type NodeConfigMessageResponse = MesageReaderResponse<NodeConfig[]>;
|
||||||
|
|
||||||
type MessageDataType = NodeConfig[] | CameraV5 | CameraV6;
|
type MessageDataType = NodeConfig[];
|
||||||
|
|
||||||
interface Message<T = MessageDataType> extends MessageBasicInfo {
|
interface Message<T = MessageDataType> extends MessageBasicInfo {
|
||||||
string_value?: string;
|
string_value?: string;
|
||||||
@@ -42,31 +40,8 @@ declare namespace MasterModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Message types cho từng domain
|
// Message types cho từng domain
|
||||||
type CameraMessage = Message<CameraV5>;
|
|
||||||
type CameraV6Message = Message<CameraV6>;
|
|
||||||
type NodeConfigMessage = Message<NodeConfig[]>;
|
type NodeConfigMessage = Message<NodeConfig[]>;
|
||||||
|
|
||||||
interface CameraV5 {
|
|
||||||
cams?: Camera[];
|
|
||||||
}
|
|
||||||
interface CameraV6 extends CameraV5 {
|
|
||||||
record_type?: 'none' | 'alarm' | 'all';
|
|
||||||
record_alarm_list?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Camera {
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
cate_id?: string;
|
|
||||||
username?: string;
|
|
||||||
password?: string;
|
|
||||||
rtsp_port?: number;
|
|
||||||
http_port?: number;
|
|
||||||
channel?: number;
|
|
||||||
ip?: string;
|
|
||||||
stream?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Alarm {
|
interface Alarm {
|
||||||
id: string;
|
id: string;
|
||||||
type: Type;
|
type: Type;
|
||||||
|
|||||||
151
src/utils/mqttClient.ts
Normal file
151
src/utils/mqttClient.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import mqtt, { IClientOptions, MqttClient, Packet } from 'mqtt';
|
||||||
|
|
||||||
|
type MessageHandler = (topic: string, message: Buffer, packet: Packet) => void;
|
||||||
|
type ConnectionHandler = () => void;
|
||||||
|
type ErrorHandler = (error: Error) => void;
|
||||||
|
|
||||||
|
interface MqttCredentials {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MQTTClientManager {
|
||||||
|
private client: MqttClient | null = null;
|
||||||
|
private messageHandlers = new Set<MessageHandler>();
|
||||||
|
private connectHandlers = new Set<ConnectionHandler>();
|
||||||
|
private closeHandlers = new Set<ConnectionHandler>();
|
||||||
|
private errorHandlers = new Set<ErrorHandler>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kết nối tới MQTT broker.
|
||||||
|
* @param credentials Thông tin xác thực (username, password)
|
||||||
|
* @param url Địa chỉ MQTT broker (mặc định là /mqtt)
|
||||||
|
*/
|
||||||
|
connect(credentials: MqttCredentials, url: string = '/mqtt') {
|
||||||
|
if (this.client?.connected) return;
|
||||||
|
|
||||||
|
// Build WebSocket URL
|
||||||
|
let mqttUrl = url;
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
mqttUrl = `${protocol}//${window.location.host}${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts: IClientOptions = {
|
||||||
|
clean: true,
|
||||||
|
username: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
reconnectPeriod: 5000,
|
||||||
|
connectTimeout: 30 * 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.client = mqtt.connect(mqttUrl, opts);
|
||||||
|
|
||||||
|
this.client.on('connect', () => {
|
||||||
|
console.log('MQTT Connected successfully!');
|
||||||
|
this.connectHandlers.forEach((fn) => fn());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('close', () => {
|
||||||
|
console.log('MQTT Connection closed');
|
||||||
|
this.closeHandlers.forEach((fn) => fn());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('error', (error: Error) => {
|
||||||
|
console.error('MQTT Error:', error);
|
||||||
|
this.errorHandlers.forEach((fn) => fn(error));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('message', (topic, message, packet) => {
|
||||||
|
this.messageHandlers.forEach((fn) => fn(topic, message, packet));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ngắt kết nối MQTT và giải phóng tài nguyên.
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
if (this.client) {
|
||||||
|
this.client.end();
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe vào một topic.
|
||||||
|
* @param topic Topic cần subscribe
|
||||||
|
*/
|
||||||
|
subscribe(topic: string | string[]) {
|
||||||
|
this.client?.subscribe(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe khỏi một topic.
|
||||||
|
* @param topic Topic cần unsubscribe
|
||||||
|
*/
|
||||||
|
unsubscribe(topic: string | string[]) {
|
||||||
|
this.client?.unsubscribe(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish message tới một topic.
|
||||||
|
* @param topic Topic để publish
|
||||||
|
* @param payload Payload (string hoặc object sẽ được stringify)
|
||||||
|
*/
|
||||||
|
publish(topic: string, payload: string | object) {
|
||||||
|
const payloadStr =
|
||||||
|
typeof payload === 'string' ? payload : JSON.stringify(payload);
|
||||||
|
this.client?.publish(topic, payloadStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Đăng ký callback khi nhận được message.
|
||||||
|
* @param cb Hàm callback xử lý message
|
||||||
|
* @returns Hàm hủy đăng ký callback
|
||||||
|
*/
|
||||||
|
onMessage(cb: MessageHandler) {
|
||||||
|
this.messageHandlers.add(cb);
|
||||||
|
return () => this.messageHandlers.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Đăng ký callback khi kết nối thành công.
|
||||||
|
*/
|
||||||
|
onConnect(cb: ConnectionHandler) {
|
||||||
|
this.connectHandlers.add(cb);
|
||||||
|
return () => this.connectHandlers.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Đăng ký callback khi kết nối bị đóng.
|
||||||
|
*/
|
||||||
|
onClose(cb: ConnectionHandler) {
|
||||||
|
this.closeHandlers.add(cb);
|
||||||
|
return () => this.closeHandlers.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Đăng ký callback khi có lỗi.
|
||||||
|
*/
|
||||||
|
onError(cb: ErrorHandler) {
|
||||||
|
this.errorHandlers.add(cb);
|
||||||
|
return () => this.errorHandlers.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kiểm tra trạng thái kết nối MQTT.
|
||||||
|
* @returns true nếu đã kết nối, ngược lại là false
|
||||||
|
*/
|
||||||
|
isConnected() {
|
||||||
|
return this.client?.connected ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy instance client MQTT gốc (nếu cần thao tác nâng cao).
|
||||||
|
*/
|
||||||
|
getClient() {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mqttClient = new MQTTClientManager();
|
||||||
Reference in New Issue
Block a user