feat: add MQTT client for camera configuration and enhance camera management
This commit is contained in:
@@ -108,6 +108,8 @@ const LoginPage = () => {
|
||||
setInitialState((s: any) => ({
|
||||
...s,
|
||||
currentUserProfile: userInfo,
|
||||
theme:
|
||||
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') || 'light',
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -148,6 +150,9 @@ const LoginPage = () => {
|
||||
setInitialState((s: any) => ({
|
||||
...s,
|
||||
currentUserProfile: userInfo,
|
||||
theme:
|
||||
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') ||
|
||||
'light',
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -175,6 +180,9 @@ const LoginPage = () => {
|
||||
setInitialState((s: any) => ({
|
||||
...s,
|
||||
currentUserProfile: userInfo,
|
||||
theme:
|
||||
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') ||
|
||||
'light',
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Row,
|
||||
Select,
|
||||
} from 'antd';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// Camera types
|
||||
const CAMERA_TYPES = [
|
||||
@@ -31,20 +32,58 @@ interface CameraFormValues {
|
||||
interface CameraFormModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: CameraFormValues) => void;
|
||||
onSubmit: (camera: MasterModel.Camera) => void;
|
||||
isOnline?: boolean;
|
||||
editingCamera?: MasterModel.Camera | null;
|
||||
}
|
||||
|
||||
const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
||||
open,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
isOnline = true,
|
||||
editingCamera,
|
||||
}) => {
|
||||
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 () => {
|
||||
try {
|
||||
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();
|
||||
} catch (error) {
|
||||
console.error('Validation failed:', error);
|
||||
@@ -58,7 +97,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Tạo mới camera"
|
||||
title={isEditMode ? 'Chỉnh sửa camera' : 'Tạo mới camera'}
|
||||
open={open}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
@@ -66,7 +105,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
||||
Hủy
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleSubmit}>
|
||||
Đồng ý
|
||||
{isEditMode ? 'Cập nhật' : 'Đồng ý'}
|
||||
</Button>,
|
||||
]}
|
||||
width={500}
|
||||
|
||||
@@ -4,22 +4,30 @@ import {
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
} 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 {
|
||||
cameraData: MasterModel.Camera[] | null;
|
||||
onCreateCamera: () => void;
|
||||
onEditCamera?: (camera: MasterModel.Camera) => void;
|
||||
onDeleteCameras?: (cameraIds: string[]) => void;
|
||||
onReload?: () => void;
|
||||
loading?: boolean;
|
||||
isOnline?: boolean;
|
||||
}
|
||||
|
||||
const CameraTable: React.FC<CameraTableProps> = ({
|
||||
cameraData,
|
||||
onCreateCamera,
|
||||
onEditCamera,
|
||||
onDeleteCameras,
|
||||
onReload,
|
||||
loading = false,
|
||||
isOnline = false,
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
const handleReload = () => {
|
||||
console.log('Reload cameras');
|
||||
@@ -27,22 +35,18 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
console.log('Delete selected cameras');
|
||||
// TODO: Implement delete functionality
|
||||
if (selectedRowKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
onDeleteCameras?.(selectedRowKeys as string[]);
|
||||
setSelectedRowKeys([]);
|
||||
};
|
||||
|
||||
const handleEdit = (camera: MasterModel.Camera) => {
|
||||
console.log('Edit camera:', camera);
|
||||
// TODO: Implement edit functionality
|
||||
onEditCamera?.(camera);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'checkbox',
|
||||
width: 50,
|
||||
render: () => <Checkbox />,
|
||||
},
|
||||
{
|
||||
title: 'Tên',
|
||||
dataIndex: 'name',
|
||||
@@ -67,11 +71,14 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
||||
title: 'Thao tác',
|
||||
key: 'action',
|
||||
render: (_: any, record: MasterModel.Camera) => (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
/>
|
||||
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
disabled={!isOnline}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -79,11 +86,29 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
||||
return (
|
||||
<Card bodyStyle={{ padding: 16 }}>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={onCreateCamera}>
|
||||
Tạo mới camera
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReload} />
|
||||
<Button icon={<DeleteOutlined />} onClick={handleDelete} />
|
||||
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
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>
|
||||
|
||||
<Table
|
||||
@@ -92,6 +117,12 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
rowSelection={{
|
||||
type: 'checkbox',
|
||||
selectedRowKeys,
|
||||
onChange: (newSelectedRowKeys) =>
|
||||
setSelectedRowKeys(newSelectedRowKeys),
|
||||
}}
|
||||
pagination={{
|
||||
size: 'small',
|
||||
showTotal: (total: number, range: [number, number]) =>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { apiQueryConfigAlarm } from '@/services/master/MessageController';
|
||||
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';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -15,15 +24,24 @@ const RECORDING_MODES = [
|
||||
interface CameraV6Props {
|
||||
thing: MasterModel.Thing | 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 { initialState } = useModel('@@initialState');
|
||||
const [selectedAlerts, setSelectedAlerts] = useState<string[]>([]);
|
||||
const [recordingMode, setRecordingMode] = useState<'none' | 'alarm' | 'all'>(
|
||||
'none',
|
||||
);
|
||||
const [recordingMode, setRecordingMode] =
|
||||
useState<MasterModel.CameraV6['record_type']>('none');
|
||||
const [alarmConfig, setAlarmConfig] = useState<MasterModel.Alarm[] | null>(
|
||||
null,
|
||||
);
|
||||
@@ -95,31 +113,39 @@ const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
||||
setSelectedAlerts([]);
|
||||
};
|
||||
|
||||
const handleSubmitAlerts = () => {
|
||||
console.log('Submit alerts:', {
|
||||
const handleSubmitConfig = () => {
|
||||
onSubmit?.({
|
||||
recordingMode,
|
||||
selectedAlerts,
|
||||
});
|
||||
// TODO: Call API to save alert configuration
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
{/* Recording Mode */}
|
||||
<div className="mb-6">
|
||||
<Text strong className="block mb-2">
|
||||
Ghi dữ liệu camera
|
||||
</Text>
|
||||
<div className="flex gap-8 items-center">
|
||||
<Select
|
||||
value={recordingMode}
|
||||
onChange={setRecordingMode}
|
||||
options={RECORDING_MODES}
|
||||
className="w-full sm:w-1/2 md:w-1/3 lg:w-1/4"
|
||||
/>
|
||||
<Button type="primary" onClick={handleSubmitAlerts}>
|
||||
Gửi đi
|
||||
</Button>
|
||||
<div className="flex flex-col sm:flex-row gap-2 sm:gap-4 items-start sm:items-end">
|
||||
<div className="w-full sm:w-1/3 lg:w-1/4">
|
||||
<Text strong className="block mb-2">
|
||||
Ghi dữ liệu camera
|
||||
</Text>
|
||||
<Select
|
||||
value={recordingMode}
|
||||
onChange={setRecordingMode}
|
||||
options={RECORDING_MODES}
|
||||
className="w-full"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -155,7 +181,7 @@ const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
||||
size="small"
|
||||
hoverable
|
||||
onClick={() => handleAlertToggle(alarmId)}
|
||||
className="cursor-pointer h-20 flex items-center justify-center"
|
||||
className="cursor-pointer h-24 flex items-center justify-center"
|
||||
style={{
|
||||
borderColor: isSelected
|
||||
? token.colorPrimary
|
||||
@@ -166,14 +192,21 @@ const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
||||
: 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
|
||||
className="text-xs break-words"
|
||||
className="text-xs"
|
||||
style={{
|
||||
color: isSelected
|
||||
? token.colorPrimary
|
||||
: token.colorText,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: '1.2em',
|
||||
}}
|
||||
title={alarm.name}
|
||||
>
|
||||
{alarm.name}
|
||||
</Text>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { apiQueryCamera } from '@/services/master/MessageController';
|
||||
import { apiGetThingDetail } from '@/services/master/ThingController';
|
||||
import { wsClient } from '@/utils/wsClient';
|
||||
import { mqttClient } from '@/utils/mqttClient';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
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 { v4 as uuidv4 } from 'uuid';
|
||||
import CameraFormModal from './components/CameraFormModal';
|
||||
import CameraTable from './components/CameraTable';
|
||||
import ConfigCameraV5 from './components/ConfigCameraV5';
|
||||
@@ -21,18 +22,53 @@ const CameraConfigPage = () => {
|
||||
const [cameraConfig, setCameraConfig] = useState<MasterModel.CameraV6 | null>(
|
||||
null,
|
||||
);
|
||||
const [mqttConnected, setMqttConnected] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState(false);
|
||||
const [editingCamera, setEditingCamera] = useState<MasterModel.Camera | null>(
|
||||
null,
|
||||
);
|
||||
const { initialState } = useModel('@@initialState');
|
||||
|
||||
// Initialize MQTT connection
|
||||
useEffect(() => {
|
||||
wsClient.connect('/mqtt', false);
|
||||
const unsubscribe = wsClient.subscribe((data: any) => {
|
||||
console.log('Received WS data:', data);
|
||||
const { frontend_thing_id, frontend_thing_key } =
|
||||
initialState?.currentUserProfile?.metadata || {};
|
||||
|
||||
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 () => {
|
||||
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
|
||||
useEffect(() => {
|
||||
@@ -99,14 +135,120 @@ const CameraConfigPage = () => {
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalVisible(false);
|
||||
setEditingCamera(null);
|
||||
};
|
||||
|
||||
const handleSubmitCamera = (values: any) => {
|
||||
console.log('Camera values:', values);
|
||||
// TODO: Call API to create camera
|
||||
// Core function to send camera config via MQTT
|
||||
const sendCameraConfig = (configPayload: MasterModel.CameraV6) => {
|
||||
// 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();
|
||||
};
|
||||
|
||||
// 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
|
||||
const renderCameraRecordingComponent = () => {
|
||||
const thingType = thing?.metadata?.type;
|
||||
@@ -116,10 +258,24 @@ const CameraConfigPage = () => {
|
||||
}
|
||||
|
||||
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 (
|
||||
@@ -134,6 +290,10 @@ const CameraConfigPage = () => {
|
||||
onClick={() => history.push('/manager/devices')}
|
||||
/>
|
||||
<span>{thing?.name || 'Loading...'}</span>
|
||||
<Badge
|
||||
status={isOnline ? 'success' : 'default'}
|
||||
text={isOnline ? 'Trực tuyến' : 'Ngoại tuyến'}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
}}
|
||||
@@ -144,8 +304,11 @@ const CameraConfigPage = () => {
|
||||
<CameraTable
|
||||
cameraData={cameras}
|
||||
onCreateCamera={handleOpenModal}
|
||||
onEditCamera={handleOpenEditModal}
|
||||
onDeleteCameras={handleDeleteCameras}
|
||||
onReload={fetchCameraConfig}
|
||||
loading={cameraLoading}
|
||||
isOnline={isOnline}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -155,11 +318,13 @@ const CameraConfigPage = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Create Camera Modal */}
|
||||
{/* Create/Edit Camera Modal */}
|
||||
<CameraFormModal
|
||||
open={isModalVisible}
|
||||
onCancel={handleCloseModal}
|
||||
onSubmit={handleSubmitCamera}
|
||||
isOnline={isOnline}
|
||||
editingCamera={editingCamera}
|
||||
/>
|
||||
</PageContainer>
|
||||
</Spin>
|
||||
|
||||
@@ -5,7 +5,10 @@ export async function apiQueryLogs(
|
||||
params: MasterModel.SearchLogPaginationBody,
|
||||
type: MasterModel.LogTypeRequest,
|
||||
) {
|
||||
return request<MasterModel.LogResponse>(`${API_READER}/${type}/messages`, {
|
||||
params: params,
|
||||
});
|
||||
return request<MasterModel.MesageReaderResponse>(
|
||||
`${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
|
||||
type CameraMessageResponse = MesageReaderResponse<CameraV5>;
|
||||
type CameraV6MessageResponse = MesageReaderResponse<CameraV6>;
|
||||
type AlarmMessageResponse = MesageReaderResponse<Alarm>;
|
||||
type NodeConfigMessageResponse = MesageReaderResponse<NodeConfig[]>;
|
||||
|
||||
type MessageDataType = NodeConfig[] | CameraV5 | CameraV6;
|
||||
type MessageDataType = NodeConfig[];
|
||||
|
||||
interface Message<T = MessageDataType> extends MessageBasicInfo {
|
||||
string_value?: string;
|
||||
@@ -42,31 +40,8 @@ declare namespace MasterModel {
|
||||
}
|
||||
|
||||
// Message types cho từng domain
|
||||
type CameraMessage = Message<CameraV5>;
|
||||
type CameraV6Message = Message<CameraV6>;
|
||||
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 {
|
||||
id: string;
|
||||
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