u
This commit is contained in:
@@ -1,39 +0,0 @@
|
||||
export async function fetchWithRetry(
|
||||
url: string,
|
||||
options: {
|
||||
method: string
|
||||
headers: Record<string, string>
|
||||
body: string
|
||||
timeout: number
|
||||
maxRetries: number
|
||||
}
|
||||
): Promise<Response> {
|
||||
let lastError: unknown = null
|
||||
|
||||
for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
options.timeout
|
||||
)
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
return response
|
||||
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error))
|
||||
if (attempt < options.maxRetries) {
|
||||
await new Promise(resolve =>
|
||||
setTimeout(resolve, 1000 * (attempt + 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError instanceof Error ? lastError : new Error(String(lastError || '请求失败'))
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import debug from "debug"
|
||||
import net from 'node:net';
|
||||
|
||||
const log = {
|
||||
app: debug('ip_monitor')
|
||||
};
|
||||
|
||||
// IP 监控配置接口
|
||||
interface IPMonitorConfig {
|
||||
timeout?: number; // 超时时间(毫秒)
|
||||
retries?: number; // 重试次数
|
||||
interval?: number; // 监控间隔(毫秒)
|
||||
}
|
||||
|
||||
// IP 监控结果接口
|
||||
export interface IPMonitorResult {
|
||||
success: boolean; // 是否成功
|
||||
responseTime?: number; // 响应时间(毫秒)
|
||||
packetLoss?: number; // 丢包率(百分比)
|
||||
error?: string; // 错误信息
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG: IPMonitorConfig = {
|
||||
timeout: 1000,
|
||||
retries: 3,
|
||||
interval: 60000
|
||||
};
|
||||
|
||||
// IP 监控类
|
||||
export class IPMonitor {
|
||||
private config: IPMonitorConfig;
|
||||
private monitoring: Map<string, NodeJS.Timeout> = new Map();
|
||||
|
||||
constructor(config: IPMonitorConfig = {}) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
// 开始监控指定 IP
|
||||
async startMonitor(ip: string, callback: (result: IPMonitorResult) => void) {
|
||||
if (this.monitoring.has(ip)) {
|
||||
log.app(`IP ${ip} 已经在监控中`);
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = async () => {
|
||||
try {
|
||||
const result = await this.ping(ip);
|
||||
callback(result);
|
||||
} catch (error) {
|
||||
log.app(`监控 IP ${ip} 时发生错误:`, error);
|
||||
callback({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 立即执行一次
|
||||
await monitor();
|
||||
|
||||
// 设置定时监控
|
||||
const interval = setInterval(monitor, this.config.interval);
|
||||
this.monitoring.set(ip, interval);
|
||||
log.app(`开始监控 IP: ${ip}`);
|
||||
}
|
||||
|
||||
// 停止监控指定 IP
|
||||
stopMonitor(ip: string) {
|
||||
const interval = this.monitoring.get(ip);
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
this.monitoring.delete(ip);
|
||||
log.app(`停止监控 IP: ${ip}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 停止所有监控
|
||||
stopAll() {
|
||||
for (const [ip, interval] of this.monitoring) {
|
||||
clearInterval(interval);
|
||||
this.monitoring.delete(ip);
|
||||
}
|
||||
log.app('已停止所有 IP 监控');
|
||||
}
|
||||
|
||||
// 执行 ping 操作
|
||||
private async ping(ip: string): Promise<IPMonitorResult> {
|
||||
const results: number[] = [];
|
||||
let successCount = 0;
|
||||
let totalTime = 0;
|
||||
|
||||
for (let i = 0; i < this.config.retries!; i++) {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const result = await this.tcpPing(ip);
|
||||
const endTime = Date.now();
|
||||
|
||||
if (result) {
|
||||
const responseTime = endTime - startTime;
|
||||
results.push(responseTime);
|
||||
totalTime += responseTime;
|
||||
successCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
log.app(`Ping 尝试 ${i + 1} 失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算平均响应时间和丢包率
|
||||
const avgResponseTime = successCount > 0 ? totalTime / successCount : undefined;
|
||||
const packetLoss = ((this.config.retries! - successCount) / this.config.retries!) * 100;
|
||||
|
||||
return {
|
||||
success: successCount > 0,
|
||||
responseTime: avgResponseTime,
|
||||
packetLoss: packetLoss
|
||||
};
|
||||
}
|
||||
|
||||
// TCP ping 实现
|
||||
private async tcpPing(ip: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new net.Socket();
|
||||
let timeout = false;
|
||||
|
||||
// 设置超时
|
||||
const timer = setTimeout(() => {
|
||||
timeout = true;
|
||||
socket.destroy();
|
||||
reject(new Error('连接超时'));
|
||||
}, this.config.timeout);
|
||||
|
||||
socket.on('connect', () => {
|
||||
clearTimeout(timer);
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
clearTimeout(timer);
|
||||
if (!timeout) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试连接
|
||||
socket.connect(80, ip);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const ipMonitor = new IPMonitor();
|
||||
@@ -5,5 +5,4 @@ export const logger = {
|
||||
api: debug('backend:api'),
|
||||
db: debug('backend:db'),
|
||||
middleware: debug('backend:middleware'),
|
||||
k8s: debug('backend:k8s')
|
||||
};
|
||||
@@ -1,149 +0,0 @@
|
||||
import { Socket } from "node:net";
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
interface ModbusRTUTestOptions {
|
||||
ip: string;
|
||||
port: number;
|
||||
frameHexStrings?: string[];
|
||||
}
|
||||
|
||||
interface ModbusRTUTestResult {
|
||||
connected: boolean;
|
||||
message: string;
|
||||
response?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modbus RTU设备连接
|
||||
* @param options 连接参数 {ip, port, frameHexStrings}
|
||||
* @returns 连接结果
|
||||
*/
|
||||
export async function modbusRTUConnection(
|
||||
options: ModbusRTUTestOptions
|
||||
): Promise<ModbusRTUTestResult> {
|
||||
try {
|
||||
const { ip, port, frameHexStrings } = options;
|
||||
|
||||
if (!ip || !port) {
|
||||
throw new Error("缺少IP或端口参数");
|
||||
}
|
||||
|
||||
const socket = new Socket();
|
||||
const timeout = 2000;
|
||||
// 默认测试帧: 设备地址2, 功能码4(读取输入寄存器), 起始地址0, 读取2个寄存器
|
||||
const defaultTestFrame = [0x02, 0x04, 0x00, 0x00, 0x00, 0x02, 0x71, 0xF8];
|
||||
let testFrame;
|
||||
|
||||
if (frameHexStrings) {
|
||||
// 16进制字符串数组格式处理
|
||||
if (!Array.isArray(frameHexStrings)) {
|
||||
throw new Error("测试帧格式错误: 必须是16进制字符串数组");
|
||||
}
|
||||
try {
|
||||
testFrame = Buffer.from(frameHexStrings.map(s => parseInt(s, 16)));
|
||||
} catch (e) {
|
||||
throw new Error("测试帧格式错误: 16进制字符串转换失败");
|
||||
}
|
||||
} else {
|
||||
testFrame = Buffer.from(defaultTestFrame);
|
||||
}
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
socket.setTimeout(timeout);
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.write(testFrame);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const hexResponse = data.toString('hex');
|
||||
socket.destroy();
|
||||
resolve({
|
||||
connected: true,
|
||||
message: "连接成功",
|
||||
response: hexResponse
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
resolve({
|
||||
connected: false,
|
||||
message: "连接超时"
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
socket.destroy();
|
||||
resolve({
|
||||
connected: false,
|
||||
message: `连接失败: ${error.message}`
|
||||
});
|
||||
});
|
||||
|
||||
socket.connect(Number(port), ip);
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error("Modbus RTU测试连接错误:", error);
|
||||
const message = error instanceof Error ? error.message : "未知错误";
|
||||
return {
|
||||
connected: false,
|
||||
message: `测试连接失败: ${message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取Modbus RTU寄存器数据
|
||||
* @param address 设备地址
|
||||
* @param register 寄存器地址
|
||||
* @param length 读取长度
|
||||
* @returns 寄存器数据数组
|
||||
*/
|
||||
export async function readModbusRTU(
|
||||
address: string,
|
||||
register: number,
|
||||
length: number
|
||||
): Promise<number[] | null> {
|
||||
try {
|
||||
const [ip, portStr] = address.split(':');
|
||||
const port = parseInt(portStr);
|
||||
|
||||
if (!ip || !port) {
|
||||
throw new Error("设备地址格式错误,应为IP:PORT");
|
||||
}
|
||||
|
||||
// 构建读取输入寄存器命令帧
|
||||
const frame = [
|
||||
0x02, // 设备地址
|
||||
0x04, // 功能码(读取输入寄存器)
|
||||
(register >> 8) & 0xFF, // 寄存器地址高字节
|
||||
register & 0xFF, // 寄存器地址低字节
|
||||
(length >> 8) & 0xFF, // 长度高字节
|
||||
length & 0xFF, // 长度低字节
|
||||
0x71, 0xF8 // CRC校验(示例值)
|
||||
];
|
||||
|
||||
const result = await modbusRTUConnection({
|
||||
ip,
|
||||
port,
|
||||
frameHexStrings: frame.map(b => b.toString(16))
|
||||
});
|
||||
|
||||
if (result.connected && result.response) {
|
||||
// 解析返回数据(示例)
|
||||
const bytes = Buffer.from(result.response, 'hex');
|
||||
const data = [];
|
||||
for (let i = 3; i < bytes.length - 2; i++) {
|
||||
data.push(bytes[i]);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("读取Modbus寄存器失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import debug from 'debug';
|
||||
import { MetricType } from '@/share/monitorTypes';
|
||||
|
||||
const log = debug('app:monitor-utils');
|
||||
|
||||
/**
|
||||
* 解析温湿度传感器十六进制数据
|
||||
* @param hexData 十六进制数据字符串,格式如"[02 04 04 01 02 02 65 A9 F3]"
|
||||
* @returns { temperature: number, humidity: number } 解析后的温湿度对象
|
||||
* @throws 如果数据格式无效会抛出错误
|
||||
*/
|
||||
export function parseTemperatureHumidity(hexData: string): {
|
||||
temperature: { value: number; unit: string };
|
||||
humidity: { value: number; unit: string };
|
||||
error?: string;
|
||||
} {
|
||||
try {
|
||||
// 验证数据格式
|
||||
if (!/^\[\s*(?:[0-9A-Fa-f]{2}\s*)+\]$/.test(hexData)) {
|
||||
return {
|
||||
temperature: { value: 0, unit: '°C' },
|
||||
humidity: { value: 0, unit: '%' },
|
||||
error: '数据格式错误: 必须以方括号开头和结尾'
|
||||
};
|
||||
}
|
||||
|
||||
// 提取十六进制字节数组
|
||||
const bytes = hexData
|
||||
.replace(/[\[\]\s]/g, '')
|
||||
.match(/.{1,2}/g)
|
||||
?.map(byte => parseInt(byte, 16)) || [];
|
||||
|
||||
if (bytes.length < 7) {
|
||||
return {
|
||||
temperature: { value: 0, unit: '°C' },
|
||||
humidity: { value: 0, unit: '%' },
|
||||
error: '数据长度不足: 至少需要7个十六进制字节'
|
||||
};
|
||||
}
|
||||
|
||||
// 解析温度值 (01位)
|
||||
const tempInt = bytes[3]; // 整数部分
|
||||
const tempFrac = bytes[4]; // 小数部分
|
||||
const temperature = parseFloat(`${tempInt}.${tempFrac}`);
|
||||
|
||||
// 解析湿度值 (02位)
|
||||
const humidityInt = bytes[5]; // 整数部分
|
||||
const humidityFrac = bytes[6]; // 小数部分
|
||||
const humidity = parseFloat(`${humidityInt}.${humidityFrac}`);
|
||||
|
||||
return {
|
||||
temperature: { value: temperature, unit: '°C' },
|
||||
humidity: { value: humidity, unit: '%' }
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
temperature: { value: 0, unit: '°C' },
|
||||
humidity: { value: 0, unit: '%' },
|
||||
error: '数据解析失败: ' + (error instanceof Error ? error.message : String(error))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MODBUS设备读取传感器数据
|
||||
*/
|
||||
export async function readModbusSensorData(
|
||||
address: string,
|
||||
metricType: string
|
||||
): Promise<{ value: number; unit: string } | null> {
|
||||
try {
|
||||
// 根据传感器类型设置不同的寄存器地址
|
||||
let registerAddress = 0;
|
||||
switch (metricType) {
|
||||
case MetricType.TEMPERATURE:
|
||||
registerAddress = 0x1000; // 温度寄存器地址
|
||||
break;
|
||||
case MetricType.HUMIDITY:
|
||||
registerAddress = 0x1002; // 湿度寄存器地址
|
||||
break;
|
||||
case 'smoke':
|
||||
registerAddress = 0x1004; // 烟感寄存器地址
|
||||
break;
|
||||
case 'water':
|
||||
registerAddress = 0x1006; // 水浸寄存器地址
|
||||
break;
|
||||
}
|
||||
|
||||
const { readModbusRTU } = await import('@/server/utils/modbus_rtu');
|
||||
const result = await readModbusRTU(address, registerAddress, 2);
|
||||
|
||||
// 解析返回的数据
|
||||
if (result && result.length >= 2) {
|
||||
const value = (result[0] << 8) | result[1]; // 组合高低字节
|
||||
return {
|
||||
value: metricType === 'smoke' || metricType === 'water'
|
||||
? value > 0 ? 1 : 0 // 烟感/水浸为开关量
|
||||
: value / 10, // 温湿度为模拟量,除以10得到实际值
|
||||
unit: metricType === MetricType.TEMPERATURE ? '°C' :
|
||||
metricType === MetricType.HUMIDITY ? '%' : ''
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
log(`读取MODBUS传感器数据失败: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import type { Context } from 'hono'
|
||||
import type {
|
||||
DeviceStatus,
|
||||
SmsItem,
|
||||
SmsConfig,
|
||||
SmsApiRequest,
|
||||
SmsApiResponse,
|
||||
SmsError,
|
||||
SmsMetrics
|
||||
} from '../types/smsTypes.js'
|
||||
import { getSystemSettings } from './systemSettings.js'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { fetchWithRetry } from './http.js'
|
||||
import { logger } from './logger.js'
|
||||
|
||||
// 加密密钥(应从环境变量获取)
|
||||
const ENCRYPT_KEY = process.env.SMS_ENCRYPT_KEY || 'default-encrypt-key'
|
||||
|
||||
// 性能指标
|
||||
const smsMetrics: SmsMetrics = {
|
||||
requestCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
averageLatency: 0
|
||||
}
|
||||
|
||||
// 加密函数
|
||||
function encryptPassword(password: string): string {
|
||||
return createHash('sha256')
|
||||
.update(password + ENCRYPT_KEY)
|
||||
.digest('hex')
|
||||
}
|
||||
|
||||
// 生成Basic认证头
|
||||
function generateAuthHeader(username: string, password: string): string {
|
||||
const text = `${username}:${password}`
|
||||
const bytes = new TextEncoder().encode(text)
|
||||
const token = btoa(String.fromCharCode(...bytes))
|
||||
return `Basic ${token}`
|
||||
}
|
||||
|
||||
// 获取短信配置
|
||||
async function getSmsConfig(): Promise<SmsConfig> {
|
||||
const settings = await getSystemSettings()
|
||||
return {
|
||||
apiUrl: settings.apiUrl,
|
||||
username: settings.username,
|
||||
encryptedPassword: settings.encryptedPassword,
|
||||
timeout: settings.timeout ?? 5000,
|
||||
maxRetries: settings.maxRetries ?? 3
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟数据存储
|
||||
const mockDeviceStatus: DeviceStatus = {
|
||||
signalStrength: 85,
|
||||
carrier: '中国移动',
|
||||
mode: '短信'
|
||||
}
|
||||
|
||||
const mockSmsList: SmsItem[] = []
|
||||
|
||||
export const SmsController = {
|
||||
async login(ctx: Context) {
|
||||
const { username, password } = await ctx.req.json<SmsApiRequest>()
|
||||
|
||||
if (username === 'vsmsd' && password === 'Vsmsd123') {
|
||||
return ctx.json({
|
||||
success: true,
|
||||
token: 'dummy-token-for-demo'
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.json({ success: false }, 401)
|
||||
},
|
||||
|
||||
async getDeviceStatus(ctx: Context) {
|
||||
return ctx.json({
|
||||
data: {
|
||||
status: mockDeviceStatus,
|
||||
list: mockSmsList
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async sendSms(ctx: Context) {
|
||||
const { phone, content } = await ctx.req.json<SmsApiRequest>()
|
||||
if (!phone || !content) {
|
||||
return ctx.json({
|
||||
success: false,
|
||||
message: '手机号和短信内容不能为空'
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const taskId = `task-${Date.now()}`
|
||||
const newSms: SmsItem = {
|
||||
id: Date.now().toString(),
|
||||
phone,
|
||||
content,
|
||||
taskId,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await getSmsConfig()
|
||||
const startTime = Date.now()
|
||||
|
||||
const response = await fetchWithRetry(config.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': generateAuthHeader(config.username, config.encryptedPassword)
|
||||
},
|
||||
body: JSON.stringify({ phone, content }),
|
||||
timeout: config.timeout ?? 5000,
|
||||
maxRetries: config.maxRetries ?? 3
|
||||
})
|
||||
|
||||
const data: SmsApiResponse = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
newSms.status = 'success'
|
||||
smsMetrics.successCount++
|
||||
logger.info(`短信发送成功: ${taskId}`, { phone, taskId })
|
||||
} else {
|
||||
newSms.status = 'failed'
|
||||
smsMetrics.failureCount++
|
||||
logger.error(`短信发送失败: ${data.code} - ${data.message}`, {
|
||||
phone,
|
||||
taskId,
|
||||
error: data
|
||||
})
|
||||
}
|
||||
|
||||
const latency = Date.now() - startTime
|
||||
smsMetrics.requestCount++
|
||||
smsMetrics.averageLatency =
|
||||
(smsMetrics.averageLatency * (smsMetrics.requestCount - 1) + latency) /
|
||||
smsMetrics.requestCount
|
||||
smsMetrics.lastRequestTime = new Date().toISOString()
|
||||
|
||||
} catch (error) {
|
||||
// 真实接口失败时使用模拟发送作为fallback
|
||||
newSms.status = 'success' // 模拟成功
|
||||
if (error instanceof Error) {
|
||||
logger.warn(`使用模拟短信发送: ${error.message}`, {
|
||||
phone,
|
||||
taskId,
|
||||
error: error.stack
|
||||
})
|
||||
} else {
|
||||
logger.warn(`使用模拟短信发送: ${String(error)}`, {
|
||||
phone,
|
||||
taskId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mockSmsList.unshift(newSms)
|
||||
|
||||
return ctx.json({
|
||||
success: true,
|
||||
data: newSms
|
||||
})
|
||||
},
|
||||
|
||||
async getSmsResult(ctx: Context) {
|
||||
const id = ctx.req.param('id')
|
||||
const sms = mockSmsList.find(item => item.id === id)
|
||||
|
||||
if (!sms) {
|
||||
return ctx.json({
|
||||
success: false,
|
||||
message: '未找到该短信记录'
|
||||
}, 404)
|
||||
}
|
||||
|
||||
// 模拟短信发送结果详情
|
||||
return ctx.json({
|
||||
success: true,
|
||||
data: {
|
||||
...sms,
|
||||
results: [
|
||||
{
|
||||
serial: '001',
|
||||
carrier: '中国移动',
|
||||
time: new Date().toISOString(),
|
||||
status: sms.status === 'pending' ? '处理中' :
|
||||
sms.status === 'success' ? '成功' : '失败'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async getMetrics(ctx: Context) {
|
||||
return ctx.json({
|
||||
success: true,
|
||||
data: smsMetrics
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user