485总线通信+TCP协议通信+OpenCV+QML——水处理系统

目录

项目概述

技术架构

核心技术栈

系统架构设计

核心功能特性

1. 实时监控系统

2. 自动化控制系统

3. 智能调节算法

4. 用户界面设计

技术亮点

1. 通信协议整合

2. 模块化设计

3. 实时数据处理

4. 容错机制

项目核心技术架构:

一、485总线通信技术实现

1.1 Modbus RTU协议实现(步进电机控制)

关键技术特点:

1.2 自定义校验和协议实现(继电器控制)

1.3 485总线资源共享机制

二、TCP协议通信技术实现

2.1 TCP服务器架构设计

2.2 JSON协议数据处理

2.3 TCP客户端实现

2.4 自动重连机制

三、通信协议数据格式

3.1 TCP协议JSON数据格式

3.2 485总线协议格式

四、系统通信架构优势

4.1 分层通信设计

4.2 容错与可靠性

4.3 扩展性设计

QML、OpenCV视频处理与TCP通信协议深度解析

一、QML(Qt建模语言)技术详解

1.1 QML语言特性与架构

1.2 组件化开发模式

1.3 信号槽机制与C++交互

1.4 动画系统与视觉效果

1.5 数据可视化 - Qt Charts集成

二、OpenCV视频处理技术

2.1 RTSP视频流处理架构

2.2 图像提供器模式

2.3 性能优化策略

三、TCP通信协议深度实现

3.1 异步TCP服务器架构

3.2 JSON协议设计与解析

3.3 数据广播与推送机制

3.4 客户端重连机制

智能水处理系统核心技术理论详解

一、QML(Qt建模语言)理论深度解析

1.1 QML语言设计哲学

1.2 组件化架构设计

1.3 JavaScript引擎集成

1.4 信号槽架构与C++集成

二、OpenCV计算机视觉理论分析

2.1 实时视频处理原理

2.2 网络视频流协议

2.3 图像处理算法基础

2.4 性能优化理论

三、TCP通信协议理论深度剖析

3.1 TCP协议基础理论

3.2 应用层协议设计

3.3 网络架构与拓扑

3.4 网络安全与可靠性

3.5 实时性与性能优化

stepmotor.py

ji_dian_qi.py

control_server.py

main.py

example.json

main.qml

ChartsPage.qml

VideoViewer.qml


智能水处理反应器控制与监控系统

项目概述

这是一个基于Python和Qt技术栈开发的智能水处理反应器控制与监控系统。该系统集成了实时视频监控、多传感器数据采集、自动化设备控制和数据可视化功能,为水处理工艺提供了完整的自动化解决方案。

技术架构

核心技术栈

后端框架: Python + PySide6 (Qt for Python)

前端界面: QML (Qt建模语言)

通信协议: TCP Socket + Modbus RTU + 串口通信

视频处理: OpenCV

数据可视化: Qt Charts

系统架构设计

系统采用客户端-服务器架构,主要包含以下组件:

1. 主控制程序 (`main.py`): 负责GUI界面管理和RTSP视频流处理

2. 控制服务器(`control_server.py`): 处理设备控制指令和传感器数据采集

3. 设备控制模块:

    步进电机控制 (`stepmotor.py`)

    继电器控制 (`ji_dian_qi.py`)

4. 用户界面: QML界面文件提供现代化的用户交互体验

核心功能特性

1. 实时监控系统

RTSP视频流监控: 支持实时视频流接入,监控反应器内部状况

多传感器数据采集:

   温度传感器:实时监测反应器温度

   pH传感器:监控溶液酸碱度

   水位传感器:检测培养基液面高度

   重量传感器:监测碱液剩余量

2. 自动化控制系统

继电器控制模块: 控制4路继电器,管理:

   碱泵 (0号继电器)

   气泵 (1号继电器)

   搅拌+潜水泵 (2号继电器)

   加热器 (3号继电器)

步进电机控制: 精确控制两台步进电机:

   水泵 (1号电机):控制进水流量

   培养基泵 (2号电机):控制培养基投加

3. 智能调节算法

pH自动调节: 根据pH传感器数据自动投加碱液调节pH值

温度控制: 智能温控系统维持设定温度

液位管理: 自动监控和维持最适宜的液位高度

4. 用户界面设计

现代化UI: 采用QML设计的响应式界面

多页面布局:

   控制面板页:设备状态监控和手动控制

   图表页面:历史数据趋势展示

实时数据展示: 温度、pH、液位等关键参数实时更新

技术亮点

1. 通信协议整合

系统巧妙地整合了多种通信协议:

TCP Socket: 用于前后端数据通信

Modbus RTU: 用于与传感器和步进电机通信

串口通信: 用于继电器控制

2. 模块化设计

class ControlServer(QObject):
    """TCP服务器,用于接收和处理控制指令"""
    
    clientConnected = Signal(str)
    clientDisconnected = Signal(str)

系统采用高度模块化的设计,每个功能模块独立开发,便于维护和扩展。

3. 实时数据处理

control_server.py

def _jian_process(self):
def _temp_process(self):

系统实现了多个定时器机制,分别处理:

传感器数据采集(3秒间隔)

pH检测和调节(可配置间隔)

温度监控和控制

4. 容错机制

系统具备完善的错误处理和重连机制,确保在设备故障或通信中断时能够自动恢复。

项目核心技术架构

本项目重点采用了485总线通信(含CRC校验)和TCP协议通信两大核心技术,构建了一个完整的工业级水处理自动化控制系统。

一、485总线通信技术实现

1.1 Modbus RTU协议实现(步进电机控制)

项目中使用Modbus RTU协议通过485总线控制步进电机,具体实现在stepmotor.py中:

# 全局Modbus客户端配置
g_modbus_client = ModbusSerialClient(
    port='COM3',           # 485串口
    baudrate=9600,         # 波特率
    bytesize=8,            # 数据位
    parity='N',            # 无校验位
    stopbits=1,            # 停止位
    timeout=1              # 超时时间
)
关键技术特点:

1.32位数据的大端序编码处理

def set_start_speed(self, speed, slave_id):
    """设置起始速度 - 32位数据处理"""
    try:
        # 使用BinaryPayloadBuilder处理32位数据的大端序编码
        builder = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.BIG)
        builder.add_32bit_uint(speed)
        registers = builder.to_registers()
        
        # 写入到Modbus寄存器
        result = self.client.write_registers(address=0x4007, values=registers, slave=slave_id)
        if result.isError():
            self.logger.error(f"设置起始速度失败: {result}")
            return False
        return True
    except Exception as e:
        self.logger.error(f"设置起始速度时发生错误: {e}")
        return False

2. 多从站设备管理

系统支持同时控制多个步进电机设备,每个设备有独立的slave_id:

def set_motor_state(self, state, slave_id):
    """设置电机启停状态"""
    try:
        result = self.client.write_coil(address=0x0001, value=state, slave=slave_id)
        if result.isError():
            self.logger.error(f"设置电机状态失败: {result}")
            return False
        self.logger.info(f"电机状态已设置为: {'启动' if state else '停止'}")
        return True
    except Exception as e:
        self.logger.error(f"设置电机状态时发生错误: {e}")
        return False

1.2 自定义校验和协议实现(继电器控制)

在ji_dian_qi.py中实现了基于485总线的自定义协议,采用简单累加校验:

def send_string(self, string):
    """发送带校验和的485命令"""
    try:
        # 将16进制字符串转换为字节数组
        hex_values = string.strip().split()
        command_bytes = bytes([int(x, 16) for x in hex_values])

        # 计算校验和:所有字节累加后对256取模
        checksum = sum(command_bytes) % 256
        # 将校验和追加到命令字节中
        command_bytes += bytes([checksum])
        self.logger.debug(f"添加校验和后的命令: {command_bytes.hex()}")
        
        # 发送转换后的bytes
        return self.send_command(command_bytes)
    except Exception as e:
        self.logger.error(f"字符串转换或发送失败: {e}")
        return b''

协议格式分析:

帧头:55 (固定标识)

设备ID:01-02 (支持2个继电器设备)

功能码:11(关闭) / 12(打开) / 21(定时开启)

数据字段:时间参数或线圈编号

校验和:所有字节累加取模

继电器定时控制实现

def set_coil_on_sometime(self, dev_id, coil_id, sometime):
    """设置继电器定时开启 - 大端序时间编码"""
    # 将时间(ms)转换为3字节大端序格式
    high_byte = (sometime >> 16) & 0xFF
    mid_byte = (sometime >> 8) & 0xFF
    low_byte = sometime & 0xFF
    
    time_hex = f"{high_byte:02X} {mid_byte:02X} {low_byte:02X}"
    cmd_str = f"55 {dev_id:02X} 21 {time_hex} {coil_id:02X}"
    self.send_string(cmd_str)

1.3 485总线资源共享机制

项目实现了485串口资源的共享管理:

class JiDianQiController:
    # 存储共享的串口连接
    _shared_clients = {}
    
    def __init__(self, port="COM4", baudrate=9600, name=None):
        # 使用共享的Serial实例避免端口冲突
        client_key = f"{port}:{baudrate}"
        if client_key not in self._shared_clients:
            self._shared_clients[client_key] = serial.Serial(
                port=port,
                baudrate=baudrate,
                timeout=1,
                bytesize=8,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE
            )
        self.client = self._shared_clients[client_key]

二、TCP协议通信技术实现

2.1 TCP服务器架构设计

在control_server.py中实现了完整的TCP服务器:

class ControlServer(QObject):
    """TCP服务器 - 设备控制中心"""
    
    def __init__(self, port=28888, parent=None):
        super().__init__(parent)
        self.server = QTcpServer(self)
        self.clients = {}  # 存储客户端连接映射
        self.port = port
        
        # 连接信号槽
        self.server.newConnection.connect(self._handle_new_connection)
        
    def start(self):
        """启动TCP服务器"""
        if not self.server.listen(QHostAddress.Any, self.port):
            self.logger.error(f"无法启动服务器: {self.server.errorString()}")
            return False
        
        self.logger.info(f"服务器已启动,监听端口: {self.port}")
        self.sensor_timer.start()  # 启动传感器数据推送
        return True

多客户端连接管理

def _handle_new_connection(self):
    """处理新客户端连接"""
    client_socket = self.server.nextPendingConnection()
    if not client_socket:
        return
    
    client_address = f"{client_socket.peerAddress().toString()}:{client_socket.peerPort()}"
    self.clients[client_socket] = client_address
    
    # 绑定客户端事件
    client_socket.readyRead.connect(lambda: self._read_data(client_socket))
    client_socket.disconnected.connect(lambda: self._handle_disconnect(client_socket))
    
    self.logger.info(f"客户端已连接: {client_address}")

2.2 JSON协议数据处理

数据接收与解析

def _read_data(self, socket):
    """读取并解析客户端JSON数据"""
    if socket.bytesAvailable() <= 0:
        return
    
    data = socket.readAll().data().decode('utf-8')
    client_address = self.clients.get(socket, "未知客户端")
    
    self.logger.info(f"收到来自 {client_address} 的数据: {data}")
    
    try:
        # 解析JSON命令
        command = json.loads(data)
        command_type = command.get('type')
        
        # 路由到对应的处理函数
        if command_type == 'relay_control':
            self._handle_open_relay(command, socket)
        elif command_type == 'motor_control':
            self._handle_motor_control(command, socket)
        elif command_type == 'set_param':
            self._handle_set_param(command, socket)
        # ... 其他命令类型
            
    except json.JSONDecodeError:
        self.logger.error("JSON解析错误")
        self._send_response(socket, {"status": "error", "message": "无效的JSON格式"})

数据广播机制

def _broadcast_message(self, message):
    """向所有客户端广播传感器数据"""
    message_json = json.dumps(message)
    message_bytes = QByteArray(message_json.encode('utf-8'))
    
    for socket in list(self.clients.keys()):
        if socket.state() == QTcpSocket.ConnectedState:
            socket.write(message_bytes)

2.3 TCP客户端实现

在main.py中实现了GUI端的TCP客户端:

class RTSPController(QObject):
    serverDataReceived = Signal(dict)  # 接收服务器数据信号
    serverConnectionChanged = Signal(bool)  # 连接状态信号
    
    def __init__(self, image_provider):
        super().__init__()
        # 初始化TCP Socket
        self._socket = QTcpSocket(self)
        self._socket.connected.connect(self._handle_connected)
        self._socket.disconnected.connect(self._handle_disconnected)
        self._socket.readyRead.connect(self._handle_ready_read)
        self._socket.errorOccurred.connect(self._handle_error)
        
        # 自动连接服务器
        self._connect_to_server()
        
    def _handle_ready_read(self):
        """处理服务器推送的数据"""
        try:
            while self._socket.bytesAvailable():
                data = self._socket.readLine().data().decode().strip()
                if data:
                    json_data = json.loads(data)
                    # 发送信号到QML界面更新
                    self.serverDataReceived.emit(json_data)
        except json.JSONDecodeError as e:
            print(f"JSON解析错误: {e}")

命令发送接口

@Slot(dict)
def send_command(self, command):
    """发送控制命令到服务器"""
    if not self._socket.state() == QTcpSocket.ConnectedState:
        print("未连接到服务器")
        return
        
    try:
        json_str = json.dumps(command)
        print(f"发送命令: {json_str}")
        self._socket.write(json_str.encode())
    except Exception as e:
        print(f"发送命令错误: {e}")

2.4 自动重连机制

def _handle_disconnected(self):
    """处理连接断开 - 实现自动重连"""
    self.serverConnectionChanged.emit(False)
    print("与服务器断开连接")
    # 5秒后自动重连
    QTimer.singleShot(5000, self._connect_to_server)

三、通信协议数据格式

3.1 TCP协议JSON数据格式

传感器数据上报

{
    "type": "sensor_data",
    "data": {
        "temperature": 25.0,
        "ph": 7.0,
        "water_level": 0.5,
        "weight": 12.0
    }
}

设备控制命令

{
    "type": "relay_control",
    "data": {
        "relay_num": 1,
        "state": true
    }
}

步进电机控制

{
    "type": "motor_control",
    "data": {
        "motor_num": 1,
        "speed": 1000
    }
}

3.2 485总线协议格式

Modbus RTU帧结构

从站地址: 1字节

功能码: 1字节 (01H读线圈, 05H写单个线圈, 06H写单个寄存器)

数据: N字节

CRC校验: 2字节 (由pymodbus库自动处理)

自定义继电器协议

55 [设备ID] [功能码] [数据] [校验和]

55: 帧头标识

设备ID: 01-02

功能码: 11(关闭)/12(打开)/21(定时)

数据: 时间参数或线圈号

校验和: 累加取模256

四、系统通信架构优势

4.1 分层通信设计

应用层: QML界面 ↔ TCP客户端

网络层: TCP协议通信 (JSON数据)

设备层: 485总线 (Modbus RTU + 自定义协议)

4.2 容错与可靠性

TCP自动重连机制

JSON格式验证

485通信校验和验证

多客户端并发支持

4.3 扩展性设计

模块化的协议处理

插件式设备控制器

标准化的JSON接口

这种双协议架构设计既保证了上位机与控制器之间的可靠通信,又实现了控制器与底层设备的实时精确控制,为工业水处理自动化提供了完整的通信解决方案。

QML、OpenCV视频处理与TCP通信协议深度解析

一、QML(Qt建模语言)技术详解

1.1 QML语言特性与架构

QML是基于JavaScript的声明式语言,专为现代UI设计。项目中展现了QML的核心特性:

声明式UI结构

// main.qml - 主窗口布局
Window {
    id: root
    width: 1200
    height: 600
    visible: true
    title: "反应器控制与监控系统"
    
    // SwipeView实现页面切换
    SwipeView {
        id: swipeView
        Layout.fillWidth: true
        Layout.fillHeight: true
        clip: true
        interactive: true
        
        // 第一页:控制面板
        Item {
            id: controlPage
            RowLayout {
                VideoViewer { /* 视频显示区 */ }
                ControlPanel { /* 控制面板区 */ }
            }
        }
        
        // 第二页:数据图表
        ChartsPage { id: chartsPage }
    }
}

属性绑定与状态管理

// ControlPanel.qml - 动态属性系统
Rectangle {
    id: controlPanelRoot
    
    // 属性定义 - 支持类型推断和双向绑定
    property bool heatingEnabled: false
    property real setTemperature: 25
    property real currentTemperature: 23.5
    property color accentColor: "#00b4ff"
    
    // 计算属性 - 自动响应依赖变化
    property string modeText: p_isAuto ? "操作模式 (自动)" : "操作模式 (手动)"
    
    // 函数定义 - JavaScript语法
    function updateTemperature(temp) {
        currentTemperature = temp;
    }
}

1.2 组件化开发模式

自定义组件封装

// ControlGroup.qml - 可复用控制组件
Rectangle {
    id: controlGroupRoot
    
    // 公开接口属性
    property string title: ""
    property bool switchEnabled: false
    property color accentColor: "#00b4ff"
    property alias contentItem: contentArea.children
    
    // 信号定义
    signal switchToggled(bool enabled)
    
    // 内部实现
    ColumnLayout {
        // 标题栏
        RowLayout {
            Text {
                text: title
                color: textColor
                font.pixelSize: titleFontSize
            }
            
            Switch {
                checked: switchEnabled
                onToggled: switchToggled(checked)
            }
        }
        
        // 内容区域 - 通过contentItem暴露给外部
        Item {
            id: contentArea
            Layout.fillWidth: true
        }
    }
}

1.3 信号槽机制与C++交互

QML与C++对象绑定

// main.qml - C++对象注册和使用
Connections {
    target: controller  // C++中注册的RTSPController对象
    
    // 处理C++信号
    function onServerConnectionChanged(connected) {
        serverStatusText.text = connected ? "服务器已连接" : "服务器未连接"
    }
    
    // 处理JSON数据
    function onServerDataReceived(data) {
        if (data.type === "sensor_data") {
            var parameters = data.data
            // 更新UI显示
            chartsPage.updateCharts(
                parameters.temperature, 
                parameters.ph, 
                parameters.water_level * 1000
            );
            controlPanel.updateTemperature(parameters.temperature);
        }
    }
}
// main.cpp - C++对象暴露给QML
int main() {
    QQmlApplicationEngine engine;
    
    // 创建控制器对象
    RTSPController controller(frame_provider);
    
    // 注册到QML上下文
    engine.rootContext().setContextProperty("controller", &controller);
    
    // 加载QML文件
    engine.load(QUrl::fromLocalFile("main.qml"));
}

1.4 动画系统与视觉效果

状态转换动画

Rectangle {
    id: lightControl
    property bool isOn: false
    
    // 颜色渐变动画
    color: isOn ? "#00b4ff" : "#272a36"
    Behavior on color { 
        ColorAnimation { duration: 200 } 
    }
    
    // 图片切换
    Image {
        source: parent.isOn ? "deng_on.png" : "deng_off.png"
        fillMode: Image.PreserveAspectFit
    }
    
    MouseArea {
        anchors.fill: parent
        onClicked: {
            lightControl.isOn = !lightControl.isOn
            // 调用C++方法
            controller.send_command({
                "type": "relay_control_2",
                "data": {
                    "relay_num": 1,
                    "state": lightControl.isOn
                }
            })
        }
    }
}

1.5 数据可视化 - Qt Charts集成

实时图表组件

// ChartsPage.qml - 动态图表实现
ChartView {
    id: temperatureChart
    title: "温度曲线"
    antialiasing: true
    backgroundColor: "#1a1c23"
    
    // Y轴配置
    ValueAxis {
        id: tempAxisY
        min: 10
        max: 40
        color: "#ffffff"
        labelsColor: "#ffffff"
        gridLineColor: "#2a2d3a"
    }
    
    // X轴时间轴
    DateTimeAxis {
        id: tempAxisX
        format: "HH:mm:ss"
        tickCount: 10
    }
    
    // 数据系列
    LineSeries {
        id: tempSeries
        axisX: tempAxisX
        axisY: tempAxisY
        color: "#00b4ff"
        width: 2
    }
    
    // 初始化数据
    Component.onCompleted: {
        var now = new Date();
        tempAxisX.min = new Date(now.getTime() - 600000); // 10分钟窗口
        tempAxisX.max = now;
        
        // 添加初始数据点
        for (var i = -30; i <= 0; i++) {
            tempSeries.append(now.getTime() + i * 20000, 0);
        }
    }
}

// 数据更新函数
function updateCharts(tempValue, phValue, heightValue) {
    var now = new Date();
    
    // 滑动时间窗口
    tempAxisX.max = now;
    tempAxisX.min = new Date(now.getTime() - 600000);
    
    // 添加新数据点
    tempSeries.append(now.getTime(), tempValue);
    
    // 移除过期数据
    while (tempSeries.count > 0 && 
           tempSeries.at(0).x < tempAxisX.min.getTime()) {
        tempSeries.remove(0);
    }
}

二、OpenCV视频处理技术

2.1 RTSP视频流处理架构

视频捕获与解码

// main.py - RTSPController类中的视频处理
class RTSPController(QObject):
    newFrameAvailable = Signal()
    
    @Slot(str)
    def connect_to_stream(self, url):
        """连接RTSP视频流"""
        try:
            # 创建VideoCapture对象
            self._cap = cv2.VideoCapture(url)
            if not self._cap.isOpened():
                self.statusChanged.emit("无法打开RTSP流")
                return
                
            # 启动定时器 - 30ms间隔 (约33fps)
            self._timer.start(30)
            self._is_connected = True
            self.connectionStatusChanged.emit(True)
            
        except Exception as e:
            self.statusChanged.emit(f"连接流错误: {str(e)}")

帧处理与格式转换

def update_frame(self):
    """实时帧处理流程"""
    if not hasattr(self, '_cap') or self._cap is None:
        return
        
    # 读取视频帧
    ret, frame = self._cap.read()
    if not ret:
        self.stop_stream()
        return
        
    # 颜色空间转换: BGR -> RGB
    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    
    # 获取图像尺寸信息
    h, w, ch = frame_rgb.shape
    bytes_per_line = ch * w
    
    # 确保内存连续性 - 关键优化点
    if not frame_rgb.flags['C_CONTIGUOUS']:
        frame_rgb = np.ascontiguousarray(frame_rgb)
        
    # 创建QImage对象
    q_img = QImage(
        frame_rgb.data,          # 数据指针
        w, h,                    # 尺寸
        bytes_per_line,          # 行字节数
        QImage.Format_RGB888     # 格式
    )
    
    # 深度拷贝确保数据安全
    q_img = q_img.copy()
    
    # 更新图像提供器
    self._image_provider.updateImage(q_img)
    
    # 通知QML更新显示
    self.newFrameAvailable.emit()

2.2 图像提供器模式

自定义图像提供器

class FrameImageProvider(QQuickImageProvider):
    """为QML提供视频帧的图像提供器"""
    
    def __init__(self):
        super().__init__(QQuickImageProvider.Image)
        # 初始化默认图像
        self._image = QImage(4000, 2000, QImage.Format_RGB888)
        self._image.fill(0)  # 黑色背景
        
    def requestImage(self, id, size, requestedSize):
        """QML请求图像时调用"""
        return self._image
        
    def updateImage(self, image):
        """更新图像数据"""
        self._image = image

QML中的图像显示

// VideoViewer.qml - 视频显示组件
Rectangle {
    id: videoViewerRoot
    color: "black"
    
    // 监听帧更新信号
    Connections {
        target: controller
        
        function onNewFrameAvailable() {
            // 强制刷新图像 - 添加随机参数避免缓存
            videoOutput.source = "image://frames/frame?" + Math.random();
        }
    }
    
    Image {
        id: videoOutput
        anchors.fill: parent
        fillMode: Image.Stretch
        cache: false  // 禁用缓存确保实时性
    }
}

2.3 性能优化策略

内存管理优化

def update_frame(self):
    """优化的帧处理"""
    # 1. 检查内存连续性
    if not frame_rgb.flags['C_CONTIGUOUS']:
        frame_rgb = np.ascontiguousarray(frame_rgb)
    
    # 2. 使用数据指针直接创建QImage
    q_img = QImage(frame_rgb.data, w, h, bytes_per_line, QImage.Format_RGB888)
    
    # 3. 深度拷贝确保线程安全
    q_img = q_img.copy()
    
    # 4. 异步更新UI
    self.newFrameAvailable.emit()

帧率控制与缓冲

# 定时器配置 - 控制帧率
self._timer = QTimer()
self._timer.timeout.connect(self.update_frame)
self._timer.start(30)  # 30ms = ~33fps

# 错误处理与重连
def update_frame(self):
    ret, frame = self._cap.read()
    if not ret:
        self.stop_stream()
        self.statusChanged.emit("流已结束或连接丢失")
        return

三、TCP通信协议深度实现

3.1 异步TCP服务器架构

Qt信号槽驱动的服务器

// control_server.py - 事件驱动的TCP服务器
class ControlServer(QObject):
    """基于Qt信号槽的异步TCP服务器"""
    
    def __init__(self, port=28888):
        super().__init__()
        self.server = QTcpServer(self)
        self.clients = {}  # 客户端连接池
        
        # 绑定信号 - 异步事件处理
        self.server.newConnection.connect(self._handle_new_connection)
        
    def start(self):
        """启动服务器"""
        if not self.server.listen(QHostAddress.Any, self.port):
            self.logger.error(f"服务器启动失败: {self.server.errorString()}")
            return False
        
        self.logger.info(f"TCP服务器启动成功,端口: {self.port}")
        return True

客户端连接管理

def _handle_new_connection(self):
    """处理新客户端连接 - 异步回调"""
    client_socket = self.server.nextPendingConnection()
    if not client_socket:
        return
    
    # 获取客户端信息
    client_address = f"{client_socket.peerAddress().toString()}:{client_socket.peerPort()}"
    self.clients[client_socket] = client_address
    
    # 绑定客户端事件 - Lambda表达式捕获socket
    client_socket.readyRead.connect(
        lambda: self._read_data(client_socket)
    )
    client_socket.disconnected.connect(
        lambda: self._handle_disconnect(client_socket)
    )
    
    self.logger.info(f"客户端连接: {client_address}")
    self.clientConnected.emit(client_address)

3.2 JSON协议设计与解析

协议消息格式

# 传感器数据推送格式
sensor_message = {
    "type": "sensor_data",
    "timestamp": time.time(),
    "data": {
        "temperature": 25.0,
        "ph": 7.0,
        "water_level": 0.5,
        "weight": 12.0
    }
}

# 设备控制指令格式
control_command = {
    "type": "relay_control",
    "data": {
        "relay_num": 1,
        "state": True,
        "duration": 1000  # 可选参数
    }
}

消息路由与处理

def _process_command(self, data, socket):
    """命令路由分发器"""
    try:
        command = json.loads(data)
        command_type = command.get('type')
        
        # 路由表映射
        handlers = {
            'set_param': self._handle_set_param,
            'relay_control': self._handle_open_relay,
            'motor_control': self._handle_motor_control,
            'auto_process': self._handle_auto_process
        }
        
        handler = handlers.get(command_type)
        if handler:
            handler(command, socket)
        else:
            self._send_error(socket, f"未知命令类型: {command_type}")
            
    except json.JSONDecodeError:
        self._send_error(socket, "JSON格式错误")

继电器控制处理器

def _handle_open_relay(self, command, socket):
    """继电器控制指令处理"""
    params = command.get('data', {})
    relay_num = params.get('relay_num')
    state = params.get('state', False)
    
    # 参数验证
    if relay_num is None or not 0 <= relay_num <= 3:
        self._send_response(socket, {
            "status": "error", 
            "message": "继电器编号无效"
        })
        return
    
    try:
        # 调用硬件控制
        if self.relay_controller:
            # 映射到物理设备
            device_map = {0: (1, 1), 1: (1, 2), 2: (1, 3), 3: (1, 4)}
            dev_id, coil_id = device_map[relay_num]
            
            self.relay_controller.set_coil(dev_id, coil_id, state)
            
            self._send_response(socket, {
                "status": "success",
                "message": f"继电器{relay_num}已{'开启' if state else '关闭'}"
            })
        else:
            self._send_response(socket, {
                "status": "error",
                "message": "继电器控制器未初始化"
            })
            
    except Exception as e:
        self._send_response(socket, {
            "status": "error",
            "message": str(e)
        })

3.3 数据广播与推送机制

定时数据推送

def __init__(self):
    # 创建传感器数据推送定时器
    self.sensor_timer = QTimer(self)
    self.sensor_timer.setInterval(3000)  # 3秒间隔
    self.sensor_timer.timeout.connect(self._read_and_push_sensor_data)
    
def _read_and_push_sensor_data(self):
    """定时读取传感器并广播"""
    try:
        if hasattr(self, 'sensor_controller'):
            # 读取所有传感器
            sensor_data = self.sensor_controller.read_all_sensors()
            
            # 构建推送消息
            message = {
                "type": "sensor_data",
                "timestamp": time.time(),
                "data": sensor_data
            }
            
            # 广播给所有客户端
            self._broadcast_message(message)
            
    except Exception as e:
        self.logger.error(f"传感器数据推送失败: {e}")

广播实现

def _broadcast_message(self, message):
    """向所有连接的客户端广播消息"""
    message_json = json.dumps(message)
    message_bytes = QByteArray(message_json.encode('utf-8'))
    
    # 遍历所有活跃连接
    for socket in list(self.clients.keys()):
        if socket.state() == QTcpSocket.ConnectedState:
            try:
                socket.write(message_bytes)
            except Exception as e:
                self.logger.error(f"广播失败: {e}")
                # 清理失效连接
                self._handle_disconnect(socket)

3.4 客户端重连机制

自动重连策略

// main.py - 客户端重连实现
def _handle_disconnected(self):
    """连接断开处理"""
    self.serverConnectionChanged.emit(False)
    print("与服务器断开连接")
    
    # 指数退避重连策略
    QTimer.singleShot(5000, self._connect_to_server)

def _connect_to_server(self):
    """连接到服务器"""
    try:
        self._socket.connectToHost("localhost", 28888)
    except Exception as e:
        print(f"连接失败: {e}")
        # 递增重连间隔
        QTimer.singleShot(10000, self._connect_to_server)

连接状态管理

def _handle_connected(self):
    """连接成功处理"""
    self.serverConnectionChanged.emit(True)
    print("已连接到服务器")
    
def _handle_error(self, socket_error):
    """网络错误处理"""
    error_msg = self._socket.errorString()
    print(f"Socket错误: {error_msg}")
    
    # 根据错误类型决定重连策略
    if socket_error in [QTcpSocket.ConnectionRefusedError, 
                       QTcpSocket.HostNotFoundError]:
        QTimer.singleShot(5000, self._connect_to_server)

智能水处理系统核心技术理论详解

一、QML(Qt建模语言)理论深度解析

1.1 QML语言设计哲学

声明式编程范式

QML采用声明式编程范式,这是一种与传统命令式编程截然不同的思维方式。在水处理系统中,传统的C++界面编程需要逐步描述"如何"创建界面元素,而QML只需要描述界面"是什么样的"。

声明式优势体现:

直观性:控制面板的温度显示、pH值监控等组件可以直接用类似HTML的标记语言描述

维护性:界面结构一目了然,修改温度显示范围或添加新的传感器监控只需修改属性值

响应式设计:当传感器数据变化时,界面会自动响应更新,无需手动刷新

数据绑定机制

QML的核心是基于属性的数据绑定系统。在水处理项目中,这种机制使得:

实时同步:当后端传感器读取到新的pH值时,前端的pH显示组件会自动更新

双向绑定:用户在界面上调整目标温度,后端控制系统立即感知到变化

依赖追踪:当加热器状态改变时,所有依赖于此状态的UI元素(指示灯、温度显示色彩等)会自动更新

1.2 组件化架构设计

可复用组件体系

QML的组件化设计使得水处理系统的界面开发具有高度的模块化特性:

组件层次结构:

原子组件:基础的开关、滑块、数值显示器

分子组件:由多个原子组件组成的控制组,如温度控制组(包含当前值显示、目标值设置、开关控制)

页面组件:完整的功能页面,如控制面板页、图表分析页

组件通信模式:

属性传递:父组件向子组件传递配置参数

信号发射:子组件向父组件报告状态变化

全局状态:通过上下文属性共享全局状态(如连接状态、系统模式)

主题系统与样式管理

QML的样式系统为工业应用提供了专业级的视觉设计能力:

深色主题:适合长时间监控的深色背景,减少操作员视觉疲劳

状态色彩:红色表示警告(温度过高)、绿色表示正常、蓝色表示信息提示

响应式布局:支持不同尺寸的工业显示器,从7寸触摸屏到21寸监控屏

1.3 JavaScript引擎集成

业务逻辑层

QML内嵌的JavaScript引擎为界面提供了强大的逻辑处理能力:

数据处理能力:

实时计算:根据传感器数据计算趋势、效率指标

数据验证:用户输入参数的合理性检查(温度范围、pH范围等)

格式化显示:数值的单位转换、精度控制、科学计数法显示

状态机模式:

系统状态管理:自动/手动模式切换、启动/停止状态转换

设备状态同步:多个设备状态的一致性维护

错误状态处理:异常状态的界面响应和恢复机制

1.4 信号槽架构与C++集成

跨语言通信机制

QML与C++的集成为工业控制系统提供了完美的分层架构:

架构分层:

表示层(QML):负责用户交互、数据展示、动画效果

业务层(C++):负责设备控制、数据处理、通信协议

数据层(C++):负责数据存储、历史记录、配置管理

异步通信模式:

非阻塞操作:界面操作不会因为设备响应延迟而卡顿

事件驱动:基于Qt的信号槽机制,实现松耦合的组件通信

线程安全:自动处理多线程环境下的数据同步问题

二、OpenCV计算机视觉理论分析

2.1 实时视频处理原理

视频流处理管道

OpenCV在水处理系统中构建了完整的视频处理管道:

数据流路径:

采集层:RTSP协议从网络摄像头获取H.264/H.265编码的视频流

解码层:硬件或软件解码器将压缩的视频帧解码为原始像素数据

处理层:OpenCV对每帧进行颜色空间转换、滤波、分析等操作

显示层:将处理后的帧数据传递给Qt显示系统

内存管理策略:

零拷贝技术:尽可能减少内存拷贝操作,提高处理效率

缓冲池管理:预分配固定数量的帧缓冲区,避免频繁的内存分配

异步处理:将耗时的图像处理操作放在后台线程执行

颜色空间理论

在工业视觉应用中,颜色空间的选择和转换至关重要:

BGR到RGB转换:

硬件兼容性:摄像头通常输出BGR格式,而显示系统需要RGB格式

处理效率:OpenCV针对BGR格式优化,转换过程经过高度优化

颜色准确性:确保监控画面的颜色真实性,便于操作员观察水质变化

2.2 网络视频流协议

RTSP协议深度解析

实时流协议(RTSP)为水处理系统提供了标准化的视频传输解决方案:

协议特性:

实时性:低延迟的视频传输,满足监控系统的实时性要求

可控性:支持播放、暂停、快进等控制操作

适应性:根据网络状况自动调整码率和分辨率

网络优化策略:

缓冲管理:平衡延迟和流畅性,设置合适的缓冲区大小

错误恢复:网络中断时的自动重连机制

带宽适应:根据网络带宽动态调整视频质量

编解码技术

现代工业监控系统中的视频编解码技术:

压缩算法选择:

H.264:广泛支持,压缩比适中,延迟较低

H.265:更高压缩比,适合高分辨率监控

MJPEG:无帧间压缩,适合需要单帧分析的应用

2.3 图像处理算法基础

实时图像增强

为了提高水处理过程的可视化效果,OpenCV提供了丰富的图像增强算法:

基础增强技术:

直方图均衡化:改善水下或低光照环境的图像对比度

噪声抑制:去除摄像头传感器噪声,提高图像清晰度

锐化滤波:增强边缘细节,便于观察水质变化

高级分析功能:

运动检测:监测反应器内的搅拌状态、液面波动

颜色分析:通过颜色变化判断化学反应进程

形态学操作:分析泡沫形态、沉淀物分布等

计算机视觉在工业中的应用

OpenCV在工业自动化中的典型应用模式:

质量监控:

缺陷检测:识别设备磨损、管道堵塞等问题

液位检测:通过图像分析精确测量液位高度

浊度分析:评估水质的透明度和清洁程度

2.4 性能优化理论

多线程处理架构

现代OpenCV应用采用多线程并行处理架构:

线程分工:

采集线程:专门负责从摄像头读取数据

处理线程:执行图像处理算法

显示线程:将处理结果传递给UI系统

负载均衡策略:

CPU vs GPU:根据算法特性选择最适合的处理器

内存带宽:优化内存访问模式,减少内存带宽瓶颈

缓存友好:设计缓存友好的数据结构和算法

三、TCP通信协议理论深度剖析

3.1 TCP协议基础理论

可靠性保证机制

TCP作为面向连接的协议,为工业控制系统提供了可靠的数据传输保证:

可靠性特性:

序号机制:确保数据包的顺序正确性,关键控制指令不会乱序执行

确认应答:每个数据包都有确认机制,确保控制指令的送达

重传机制:丢失的数据包会自动重传,保证控制指令的完整性

流量控制:防止发送方过快发送数据,避免接收方缓冲区溢出

工业应用优势:

在水处理系统中,TCP的可靠性确保了:

继电器开关指令的准确执行

传感器数据的完整传输

系统状态的一致性维护

连接管理机制

TCP的连接管理为分布式控制系统提供了稳定的通信基础:

三次握手建立连接:

兼容性检查:确认通信双方的协议版本和能力

参数协商:协商窗口大小、最大报文段长度等参数

状态同步:建立初始序号,为后续通信做准备

四次挥手断开连接:

优雅关闭:确保所有数据传输完成后再关闭连接

资源释放:及时释放系统资源,避免资源泄漏

3.2 应用层协议设计

JSON协议选择理由

在众多应用层协议中,选择JSON作为通信格式具有以下优势:

可读性与调试性:

人类可读:便于开发调试和问题诊断

结构清晰:层次化的数据结构便于理解和维护

标准化:跨平台、跨语言的标准格式

扩展性与灵活性:

动态字段:可以方便地添加新的传感器类型或控制参数

版本兼容:新版本协议可以保持对旧版本的兼容

类型安全:支持多种数据类型,减少类型转换错误

协议分层设计

工业控制系统的通信协议采用分层设计:

协议栈结构:

传输层:TCP提供可靠的字节流传输

会话层:建立客户端与服务器的会话关系

表示层:JSON编解码,数据序列化/反序列化

应用层:具体的业务逻辑处理

消息类型设计:

控制消息:设备开关、参数设置等操作指令

查询消息:获取设备状态、传感器读数等

推送消息:主动推送的传感器数据、警报信息

响应消息:对请求的确认和结果返回

3.3 网络架构与拓扑

客户端-服务器模式

水处理系统采用经典的C/S架构:

服务器角色:

设备控制中心:统一管理所有硬件设备

数据处理中心:收集、处理、存储传感器数据

业务逻辑中心:执行自动化控制算法

客户端角色:

人机界面:为操作员提供监控和控制接口

数据展示:实时显示系统状态和传感器数据

命令发起:接受用户操作并转换为控制指令

多客户端并发处理

现代工业系统通常需要支持多个客户端同时访问:

并发模型:

事件驱动:基于事件循环的非阻塞I/O模型

连接池管理:有效管理多个客户端连接

负载均衡:在多个处理线程间分配客户端请求

数据一致性:

状态同步:确保所有客户端看到一致的系统状态

冲突解决:处理多个客户端同时操作同一设备的情况

事务处理:确保复杂操作的原子性

3.4 网络安全与可靠性

安全机制设计

工业控制系统的网络安全至关重要:

访问控制:

身份认证:确保只有授权用户能够访问系统

权限管理:不同用户具有不同的操作权限

会话管理:安全的会话建立和维护机制

数据保护:

传输加密:使用TLS/SSL加密通信数据

完整性校验:确保数据在传输过程中未被篡改

防重放攻击:使用时间戳和随机数防止重放攻击

容错与恢复机制

工业系统的高可用性要求:

故障检测:

心跳机制:定期检测连接的有效性

超时处理:合理设置超时时间,及时发现故障

健康检查:定期检查系统各组件的健康状态

故障恢复:

自动重连:网络中断后的自动重连机制

状态恢复:重连后的状态同步和恢复

降级服务:在部分故障情况下的降级运行模式

3.5 实时性与性能优化

实时性要求分析

水处理系统对实时性有严格要求:

实时性分类:

硬实时:紧急停机、安全保护等必须在规定时间内响应

软实时:温度调节、pH控制等允许一定的延迟

非实时:历史数据查询、报告生成等对时间要求不严格

延迟优化策略:

网络优化:选择合适的网络拓扑和设备

协议优化:减少协议开销,简化通信流程

缓存策略:合理使用缓存减少重复计算和传输

性能监控与调优

系统性能的持续监控和优化:

性能指标:

吞吐量:单位时间内处理的消息数量

延迟:从发送到接收的时间间隔

错误率:通信过程中的错误频率

资源利用率:CPU、内存、网络带宽的使用情况

调优方法:

参数调整:TCP缓冲区大小、超时时间等参数优化

算法优化:改进数据处理算法,减少计算复杂度

架构优化:调整系统架构,消除性能瓶颈

stepmotor.py

from pymodbus.client import ModbusSerialClient
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder
import time
import logging


g_modbus_client = ModbusSerialClient(
        port='COM3',
        baudrate=9600,
        bytesize=8,
        parity='N',
        stopbits=1,
        timeout=1)

if g_modbus_client.connect():
    print("stepmotor连接成功")
else:
    print("stepmotor连接失败")

class StepMotorController:
    # 类变量,用于存储共享的串口连接
    
    def __init__(self, name=None):

        self.name = name
        self.logger = self._setup_logger()
        
        # 使用全局的ModbusClient
        self.client = g_modbus_client
        self.logger.info(f"使用全局ModbusClient连接端口")

        self.set_start_speed(0, 1)
        self.set_start_speed(0, 2)
    
    def _setup_logger(self):
        """设置日志记录器"""
        logger_name = f"StepMotorController_{self.name}" if self.name else "StepMotorController"
        logger = logging.getLogger(logger_name)
        logger.setLevel(logging.INFO)
        
        if not logger.handlers:
            handler = logging.StreamHandler()
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
            handler.setFormatter(formatter)
            logger.addHandler(handler)
            
        return logger
    
    def disconnect(self):
        """断开与Modbus设备的连接"""
        self.client.close()
        self.logger.info("已断开连接")
    
    def set_motor_state(self, state, slave_id):
        """
        设置电机启停状态
        
        Args:
            state: True表示启动,False表示停止
        
        Returns:
            bool: 操作是否成功
        """
        try:
            result = self.client.write_coil(address=0x0001, value=state, slave=slave_id)
            if result.isError():
                self.logger.error(f"设置电机状态失败: {result}")
                return False
            self.logger.info(f"电机状态已设置为: {'启动' if state else '停止'}")
            return True
        except Exception as e:
            self.logger.error(f"设置电机状态时发生错误: {e}")
            return False
    
    def get_motor_state(self, slave_id):
        """
        获取电机启停状态
        
        Returns:
            bool or None: True表示启动,False表示停止,None表示读取失败
        """
        try:
            result = self.client.read_coils(address=0x0001, count=1, slave=slave_id)
            if result.isError():
                self.logger.error(f"读取电机状态失败: {result}")
                return None
            state = result.bits[0]
            self.logger.info(f"当前电机状态: {'启动' if state else '停止'}")
            return state
        except Exception as e:
            self.logger.error(f"读取电机状态时发生错误: {e}")
            return None
    
    def set_direction(self, direction, slave_id):
        """
        设置电机旋转方向
        
        Args:
            direction: True表示正向,False表示反向
        
        Returns:
            bool: 操作是否成功
        """
        try:
            result = self.client.write_coil(address=0x0002, value=direction, slave=slave_id)
            if result.isError():
                self.logger.error(f"设置电机方向失败: {result}")
                return False
            self.logger.info(f"电机方向已设置为: {'正向' if direction else '反向'}")
            return True
        except Exception as e:
            self.logger.error(f"设置电机方向时发生错误: {e}")
            return False
    
    def get_direction(self, slave_id):
        """
        获取电机旋转方向
        
        Returns:
            bool or None: True表示正向,False表示反向,None表示读取失败
        """
        try:
            result = self.client.read_coils(address=0x0002, count=1, slave=slave_id)
            if result.isError():
                self.logger.error(f"读取电机方向失败: {result}")
                return None
            direction = result.bits[0]
            self.logger.info(f"当前电机方向: {'正向' if direction else '反向'}")
            return direction
        except Exception as e:
            self.logger.error(f"读取电机方向时发生错误: {e}")
            return None
    
    def set_slave_address(self, address, slave_id):
        """
        设置从站通讯地址
        
        Args:
            address: 新的从站地址 (uint16_t)
        
        Returns:
            bool: 操作是否成功
        """
        if not 1 <= address <= 247:
            self.logger.error(f"无效的从站地址: {address},地址必须在1-247之间")
            return False
            
        try:
            result = self.client.write_register(address=0x4001, value=address, slave=slave_id)
            if result.isError():
                self.logger.error(f"设置从站地址失败: {result}")
                return False
            self.logger.info(f"从站地址已设置为: {address}")
            # 更新当前实例的从站ID
            self.slave_id = address
            return True
        except Exception as e:
            self.logger.error(f"设置从站地址时发生错误: {e}")
            return False
    
    
    def set_start_speed(self, speed, slave_id):
        """
        设置起始速度
        
        Args:
            speed: 起始速度 (uint32_t)
        
        Returns:
            bool: 操作是否成功
        """
        try:
            # 使用新的API替换BinaryPayloadBuilder
            # registers = self.client.convert_to_registers(
            #     value=speed,
            #     data_type=[("speed", "uint32")],  # 使用列表形式指定数据类型
            # )

            builder = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.BIG)
            builder.add_32bit_uint(speed)
            registers = builder.to_registers()
            
            result = self.client.write_registers(address=0x4007, values=registers, slave=slave_id)
            if result.isError():
                self.logger.error(f"设置起始速度失败: {result}")
                return False
            self.logger.info(f"起始速度已设置为: {speed}")
            return True
        except Exception as e:
            self.logger.error(f"设置起始速度时发生错误: {e}")
            return False
    
    def get_start_speed(self, slave_id):
        """
        获取起始速度
        
        Returns:
            int or None: 起始速度,None表示读取失败
        """
        try:
            result = self.client.read_holding_registers(address=0x4007, count=2, slave=slave_id)
            if result.isError():
                self.logger.error(f"读取起始速度失败: {result}")
                return None
            
            # 使用新的API替换BinaryPayloadDecoder
            speed = self.client.convert_from_registers(
                registers=result.registers,
                data_format="uint32",
                byteorder=Endian.BIG,
                wordorder=Endian.BIG
            )
            self.logger.info(f"当前起始速度: {speed}")
            return speed
        except Exception as e:
            self.logger.error(f"读取起始速度时发生错误: {e}")
            return None
    
    def set_run_speed(self, speed, slave_id):
        """
        设置运行速度
        
        Args:
            speed: 运行速度 (uint32_t)
        
        Returns:
            bool: 操作是否成功
        """
        try:
            # 使用新的API替换BinaryPayloadBuilder
            builder = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.BIG)
            builder.add_32bit_uint(speed)
            registers = builder.to_registers()
            
            # 或者如果要继续使用convert_to_registers,这样使用:
            # registers = list(self.client.convert_to_registers(
            #     speed,
            #     data_type="uint32"
            # ).values())
            
            result = self.client.write_registers(address=0x4009, values=registers, slave=slave_id)
            if result.isError():
                self.logger.error(f"设置运行速度失败: {result}")
                return False
            self.logger.info(f"运行速度已设置为: {speed}")
            return True
        except Exception as e:
            self.logger.error(f"设置运行速度时发生错误: {e}")
            return False
    
    def get_run_speed(self, slave_id):
        """
        获取运行速度
        
        Returns:
            int or None: 运行速度,None表示读取失败
        """
        try:
            result = self.client.read_holding_registers(address=0x4009, count=2, slave=slave_id)
            if result.isError():
                self.logger.error(f"读取运行速度失败: {result}")
                return None
            
            # 使用新的API替换BinaryPayloadDecoder
            speed = self.client.convert_from_registers(
                registers=result.registers,
                data_format="uint32",
                byteorder=Endian.BIG,
                wordorder=Endian.BIG
            )
            self.logger.info(f"当前运行速度: {speed}")
            return speed
        except Exception as e:
            self.logger.error(f"读取运行速度时发生错误: {e}")
            return None
        
    def set_motor1_speed(self, speed):
        """设置电机1的速度"""
        if speed == 0:
            self.set_motor_state(False, 1)
        else:
            self.set_run_speed(speed, 1)
            self.set_motor_state(True, 1)

    def set_motor2_speed(self, speed):
        """设置电机2的速度"""
        if speed == 0:
            self.set_motor_state(False, 2)
        else:
            self.set_run_speed(speed, 2)
            self.set_motor_state(True, 2)


class SensorController:
    """传感器控制器类,用于读取温度、pH值和水面高度"""
    
    def __init__(self, name=None):
        """
        初始化传感器控制器
        
        Args:
            client: ModbusClient实例,如果为None则使用全局client
            port: 串口设备名,当client为None时使用
            baudrate: 波特率,当client为None时使用
            name: 控制器名称,用于日志标识
        """
        self.name = name
        self.logger = self._setup_logger()
        
        # 使用全局client
        self.client = g_modbus_client
        self.logger.info(f"初始化传感器控制器 {name if name else ''}")
    
    def _setup_logger(self):
        """设置日志记录器"""
        logger_name = f"SensorController_{self.name}" if self.name else "SensorController"
        logger = logging.getLogger(logger_name)
        logger.setLevel(logging.INFO)
        
        if not logger.handlers:
            handler = logging.StreamHandler()
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
            handler.setFormatter(formatter)
            logger.addHandler(handler)
            
        return logger
    
    def read_temperature(self):
        """
        读取温度传感器数据
        
        Returns:
            float or None: 温度值(摄氏度),None表示读取失败
        """
        try:
            # 读取地址为3的温度检测设备的保持寄存器(地址为00 00, 数量为2)
            result = self.client.read_holding_registers(address=0x0000, count=2, slave=3)
            if result.isError():
                self.logger.error(f"读取温度失败: {result}")
                return None
            
            
            # 处理两个int16类型的温度值
            temperature1 = result.registers[0] * 0.1  # 第一个温度值
            temperature2 = result.registers[1] * 0.1  # 第二个温度值
            temperature = (temperature1 + temperature2) / 2  # 取平均值
            self.logger.info(f"当前温度: {temperature}°C")

            return temperature
        except Exception as e:
            self.logger.error(f"读取温度时发生错误: {e}")
            return None
    
    def read_ph(self):
        """
        读取pH值传感器数据
        
        Returns:
            float or None: pH值,None表示读取失败
        """
        try:
            # 读取地址为4的pH检测设备的保持寄存器(地址为00 00, 数量为1)
            result = self.client.read_holding_registers(address=0x0000, count=1, slave=4)
            if result.isError():
                self.logger.error(f"读取pH值失败: {result}")
                return None
            
            # 解码数据并乘以0.01
            ph_value = result.registers[0] * 0.01
            self.logger.info(f"当前pH值: {ph_value}")
            return ph_value
        except Exception as e:
            self.logger.error(f"读取pH值时发生错误: {e}")
            return None
    
    def read_water_level(self):
        """
        读取水面高度传感器数据
        
        Returns:
            float or None: 水面高度(米),None表示读取失败
        """
        try:
            # 读取地址为6的水面高度检测设备的保持寄存器(地址为01 00, 数量为1)
            result = self.client.read_holding_registers(address=0x0100, count=1, slave=6)
            if result.isError():
                self.logger.error(f"读取水面高度失败: {result}")
                return None
            
            total_height = 0.4
            
            # 解码数据并乘以0.001
            water_level = result.registers[0] * 0.001
            water_level = total_height - water_level
            self.logger.info(f"当前水面高度: {water_level}m")
            return water_level
        except Exception as e:
            self.logger.error(f"读取水面高度时发生错误: {e}")
            return None
        

    def read_weight(self):
        """
        读取重量传感器数据
        
        Returns:
            float or None: 重量(千克),None表示读取失败
        """
        try:
            # 读取地址为9的重量检测设备的保持寄存器(地址为01 00, 数量为1)
            result = self.client.read_holding_registers(address=0x0000, count=2, slave=9)
            if result.isError():
                self.logger.error(f"读取重量失败: {result}")
                return None
            
            # 解码数据并乘以0.001
            # 使用两个寄存器的值,将高位和低位组合
            weight_raw = (result.registers[0] << 16) + result.registers[1]
            weight = weight_raw * 0.001
            self.logger.info(f"当前重量: {weight}kg")
            return weight
        except Exception as e:
            self.logger.error(f"读取重量时发生错误: {e}")
            return None
        
    def read_all_sensors(self):
        """读取所有传感器数据"""
        try:
            temperature = self.read_temperature()
            ph = self.read_ph()
            water_level = self.read_water_level()   
            weight = self.read_weight()

            if temperature == None:
                temperature = 0
            if ph == None:
                ph = 0
            if water_level == None:
                water_level = 0
            if weight == None:
                weight = 0

            
            return {
                "temperature": temperature,
                "ph": ph,
                "water_level": water_level,
                "weight": weight
            }
        except Exception as e:
            self.logger.error(f"读取传感器数据时发生错误: {e}")
            return {
                "temperature": temperature,
                "ph": ph,
                "water_level": water_level,
                "weight": weight
            }



# 使用示例
if __name__ == "__main__":
    sensor_controller = SensorController()
    # 测试传感器控制器
    try:
        print("开始测试传感器控制器...")
        
        # 读取所有传感器数据
        print("\n读取所有传感器数据:")
        all_data = sensor_controller.read_all_sensors()
        if all_data:
            print(f"温度: {all_data['temperature']}°C")
            print(f"pH值: {all_data['ph']}")
            print(f"水面高度: {all_data['water_level']}mm")
            print(f"重量: {all_data['weight']}kg")
        
        # 单独测试各个传感器
        print("\n单独测试各个传感器:")
        
        # 测试温度传感器
        temperature = sensor_controller.read_temperature()
        print(f"温度: {temperature}°C")
        
        # 测试pH值传感器
        ph = sensor_controller.read_ph()
        print(f"pH值: {ph}")
        
        # 测试水面高度传感器
        water_level = sensor_controller.read_water_level()
        print(f"水面高度: {water_level}mm")
        
        # 测试重量传感器
        weight = sensor_controller.read_weight()
        print(f"重量: {weight}kg")
        
    except Exception as e:
        print(f"测试传感器控制器时发生错误: {e}")

    # # 创建控制器实例 - 使用配置文件方式
    # motor1 = StepMotorController(name="motor_jingshui")
    # motor2 = StepMotorController(name="motor_shangliao")
    
    # # 连接到设备 - 两个电机共用一个串口
    # if motor1.connect():
    #     try:
    #         # 控制第一个电机
    #         print(f"电机 {motor1.name} 当前状态:", motor1.get_motor_state())
    #         motor1.set_motor_state(True)  # 启动电机
    #         motor1.set_direction(True)    # 设置为正向
    #         motor1.set_start_speed(101)  # 设置起始速度
    #         motor1.set_run_speed(2000)    # 设置运行速度
    #         print(f"电机 {motor1.name} 地址:", motor1.get_slave_address())
    #         time.sleep(3)
    #         motor1.set_motor_state(False) # 停止电机
            
    #         # 控制第二个电机
    #         print(f"电机 {motor2.name} 当前状态:", motor2.get_motor_state())
    #         motor2.set_motor_state(True)  # 启动电机
    #         motor2.set_direction(False)   # 设置为反向
    #         motor2.set_start_speed(102)  # 设置起始速度
    #         motor2.set_run_speed(2022)    # 设置运行速度
    #         print(f"电机 {motor2.name} 地址:", motor2.get_slave_address())
    #         time.sleep(3)
    #         motor2.set_motor_state(False) # 停止电机
            
    #     finally:
    #         # 断开连接
    #         motor1.disconnect()

ji_dian_qi.py

import logging
import time
import serial

class JiDianQiController:
    """继电器控制器类,用于控制继电器的开关状态"""
    
    # 存储共享的串口连接
    _shared_clients = {}
    
    def __init__(self, port="COM4", baudrate=9600, name=None):
        """
        初始化继电器控制器
        
        Args:
            port: 串口名称,如'COM1'或'/dev/ttyUSB0'
            baudrate: 波特率,默认9600
            slave_id: 从站ID,默认1
            name: 控制器名称,用于日志标识
        """
        self.port = port
        self.baudrate = baudrate
        self.name = name
        self.logger = self._setup_logger()
        
        # 使用共享的Serial实例
        client_key = f"{port}:{baudrate}"
        if client_key not in self._shared_clients:
            self._shared_clients[client_key] = serial.Serial(
                port=port,
                baudrate=baudrate,
                timeout=1,
                bytesize=8,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE
            )
        self.client = self._shared_clients[client_key]
    
    def _setup_logger(self):
        """设置日志记录器"""
        logger_name = f"JiDianQiController_{self.name}" if self.name else "JiDianQiController"
        logger = logging.getLogger(logger_name)
        logger.setLevel(logging.INFO)
        
        if not logger.handlers:
            handler = logging.StreamHandler()
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
            handler.setFormatter(formatter)
            logger.addHandler(handler)
            
        return logger
    
    def connect(self):
        """连接到串口设备"""
        try:
            if not self.client.is_open:
                self.client.open()
            self.logger.info("连接成功")
            return True
        except Exception as e:
            self.logger.error(f"连接失败: {e}")
            return False
    
    def disconnect(self):
        """断开与串口设备的连接"""
        if self.client.is_open:
            self.client.close()
            self.logger.info("已断开连接")


    # dev_id: 设备ID,1-2
    # coil_id: 继电器ID,1-4
    # value: 继电器状态,False-关闭,True-打开
    def set_coil(self, dev_id, coil_id, value):
        """设置继电器状态"""
        cmd_str = f"55 {dev_id:02X} 12 00 00 00 {coil_id:02X}" if value else f"55 {dev_id:02X} 11 00 00 00 {coil_id:02X}"
        self.send_string(cmd_str)
            
        
    # dev_id: 设备ID,1-2
    # coil_id: 继电器ID,1-4
    # sometime: 继电器on状态时间,单位为ms
    def set_coil_on_sometime(self, dev_id, coil_id, sometime):
        """设置继电器on状态时间"""
        # 将sometime(ms)转换为3个字节的大端模式
        # 大端模式:高位字节在前,低位字节在后
        high_byte = (sometime >> 16) & 0xFF
        mid_byte = (sometime >> 8) & 0xFF
        low_byte = sometime & 0xFF
        
        # 格式化为16进制字符串
        time_hex = f"{high_byte:02X} {mid_byte:02X} {low_byte:02X}"
        cmd_str = f"55 {dev_id:02X} 21 {time_hex} {coil_id:02X}"
        self.send_string(cmd_str)


    def send_string(self, string):
        """
        发送字符串
        
        Args:
            string: 要发送的字符串,格式为以空格分隔的16进制数,如"01 05 00 00 FF 00"
        """
        try:
            # 将空格分隔的16进制字符串转换为bytes
            hex_values = string.strip().split()
            command_bytes = bytes([int(x, 16) for x in hex_values])

            # 计算校验和:将所有字节累加,对256求余
            checksum = sum(command_bytes) % 256
            # 将校验和追加到命令字节中
            command_bytes += bytes([checksum])
            self.logger.debug(f"添加校验和后的命令: {command_bytes.hex()}")
            
            # 发送转换后的bytes
            return self.send_command(command_bytes)
        except Exception as e:
            self.logger.error(f"字符串转换或发送失败: {e}")
            return b''
    
    def send_command(self, command_bytes):
        """
        发送指定的命令字节
        
        Args:
            command_bytes: 要发送的字节数据
        
        Returns:
            bytes: 接收到的响应数据
        """
        try:
            self.client.write(command_bytes)
            self.logger.debug(f"已发送: {command_bytes.hex()}")
            
            # 等待响应
            time.sleep(0.1)
            response = b''
            while self.client.in_waiting:
                response += self.client.read(self.client.in_waiting)
            
            if response:
                self.logger.debug(f"收到响应: {response.hex()}")
            return response
        except Exception as e:
            self.logger.error(f"发送命令失败: {e}")
            return b''


if __name__ == "__main__":
    controller = JiDianQiController(port='COM3', baudrate=9600, slave_id=1, name='继电器控制器')
    controller.connect()
    # 示例:发送一个关闭继电器1的命令
    # controller.send_command(b'\x01\x05\x00\x00\x00\x00\x8C\x3A')
    controller.set_coil(2, 1, True)
    time.sleep(4)
    # 示例:发送一个打开继电器1的命令
    # controller.send_command(b'\x01\x05\x00\x00\xFF\x00\xCD\xCA')
    controller.set_coil_on_sometime(1, 1, 500)

    time.sleep(1)
    controller.set_coil(2, 1, False)
    controller.disconnect()

control_server.py

import json
import logging
from PySide6.QtCore import QObject, Signal, Slot, QByteArray, QTimer
from PySide6.QtNetwork import QTcpServer, QTcpSocket, QHostAddress
from ji_dian_qi import JiDianQiController
from stepmotor import StepMotorController, SensorController

class ControlServer(QObject):
    """TCP服务器,用于接收和处理控制指令"""
    
    clientConnected = Signal(str)
    clientDisconnected = Signal(str)
    commandReceived = Signal(str, str)  # 客户端地址, 命令内容
    
    def __init__(self, port=28888, parent=None):
        """
        初始化TCP服务器
        
        Args:
            port: 服务器监听端口
            parent: 父QObject
        """
        super().__init__(parent)
        self.logger = self._setup_logger()
        self.server = QTcpServer(self)
        self.clients = {}  # 存储客户端连接 {socket: address}
        self.port = port

        self.m_nJianCheckInteval = 60*1000  # ms
        self.m_nTempCheckInteval = 1000  # ms 
        self.m_nJianTime = 500  #投碱持续时间 ms
        self.m_fpHThreshold = 10.0  # pH阈值
        self.m_fTempThreshold = 25.0  # 温度阈值
        self.m_fWaterLevelWarningThreshold = 0.2  # 水位报警阈值
        self.m_fTargetTemp = 23.0
        self.m_fTargetpH = 7.0
        self.m_fFeedSpeed = 20.0  # 进料速度 ml/min
        self.m_fWaterSpeed = 20.0 # 进水速度 ml/min
        
        # 加载继电器控制器
        try:
            self.relay_controller = JiDianQiController(
                name='继电器控制器'
            )
            self.relay_controller.connect()

            self.stepmotor_controller = StepMotorController()
            self.sensor_controller = SensorController()
                
        except Exception as e:
            self.logger.error(f"初始化继电器控制器失败: {e}")
            self.relay_controller = None
        
        # 连接信号
        self.server.newConnection.connect(self._handle_new_connection)
        
        # 创建定时器,每3000ms读取一次传感器数据并推送
        self.sensor_timer = QTimer(self)
        self.sensor_timer.setInterval(3000)
        self.sensor_timer.timeout.connect(self._read_and_push_sensor_data)

        self.jian_timer = QTimer(self)
        self.jian_timer.setInterval(self.m_nJianCheckInteval)
        self.jian_timer.timeout.connect(self._jian_process)

        self.temp_timer = QTimer(self)
        self.temp_timer.setInterval(3000)
        self.temp_timer.timeout.connect(self._temp_process)
    
    def _setup_logger(self):
        """设置日志记录器"""
        logger = logging.getLogger("ControlServer")
        logger.setLevel(logging.INFO)
        
        if not logger.handlers:
            handler = logging.StreamHandler()
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
            handler.setFormatter(formatter)
            logger.addHandler(handler)
            
        return logger
    
    def start(self):
        """启动TCP服务器"""
        if not self.server.listen(QHostAddress.Any, self.port):
            self.logger.error(f"无法启动服务器: {self.server.errorString()}")
            return False
        
        self.logger.info(f"服务器已启动,监听端口: {self.port}")
        # 启动传感器数据读取定时器
        self.sensor_timer.start()
        return True
    
    def stop(self):
        """停止TCP服务器"""
        # 停止传感器数据读取定时器
        self.sensor_timer.stop()
        
        # 关闭所有客户端连接
        for socket in list(self.clients.keys()):
            socket.disconnectFromHost()
        
        self.server.close()
        if self.relay_controller:
            self.relay_controller.disconnect()
        
        self.logger.info("服务器已停止")
    
    def _read_and_push_sensor_data(self):
        """读取传感器数据并推送给所有客户端"""
        self.logger.info("读取传感器数据,并推送给所有客户端")
        try:
            if hasattr(self, 'sensor_controller') and self.sensor_controller:
                # 读取传感器数据
                self.sensor_data = self.sensor_controller.read_all_sensors()
                
                # 构建数据包
                data_package = {
                    "type": "sensor_data",
                    "data": self.sensor_data
                }
                
                # 推送给所有客户端
                self._broadcast_message(data_package)
                self.logger.debug(f"推送传感器数据: {self.sensor_data}")
        except Exception as e:
            self.logger.error(f"读取或推送传感器数据时发生错误: {e}")
    
    def _broadcast_message(self, message):
        """向所有客户端广播消息"""
        message_json = json.dumps(message)
        message_bytes = QByteArray(message_json.encode('utf-8'))
        
        for socket in list(self.clients.keys()):
            if socket.state() == QTcpSocket.ConnectedState:
                socket.write(message_bytes)
    
    def _handle_new_connection(self):
        """处理新的客户端连接"""
        client_socket = self.server.nextPendingConnection()
        if not client_socket:
            return
        
        client_address = f"{client_socket.peerAddress().toString()}:{client_socket.peerPort()}"
        self.clients[client_socket] = client_address
        
        # 连接信号
        client_socket.readyRead.connect(lambda: self._read_data(client_socket))
        client_socket.disconnected.connect(lambda: self._handle_disconnect(client_socket))
        
        self.logger.info(f"客户端已连接: {client_address}")
        self.clientConnected.emit(client_address)
    
    def _handle_disconnect(self, socket):
        """处理客户端断开连接"""
        if socket in self.clients:
            client_address = self.clients[socket]
            self.logger.info(f"客户端已断开连接: {client_address}")
            self.clientDisconnected.emit(client_address)
            del self.clients[socket]
        
        socket.deleteLater()
    
    def _read_data(self, socket):
        """读取客户端发送的数据"""
        if socket.bytesAvailable() <= 0:
            return
        
        data = socket.readAll().data().decode('utf-8')
        client_address = self.clients.get(socket, "未知客户端")
        
        self.logger.info(f"收到来自 {client_address} 的数据: {data}")
        self.commandReceived.emit(client_address, data)
        
        try:
            self._process_command(data, socket)
        except Exception as e:
            self.logger.error(f"处理命令时发生错误: {e}")
            self._send_response(socket, {"status": "error", "message": str(e)})
    
    def _process_command(self, data, socket):
        """处理客户端发送的命令"""
        try:
            command = json.loads(data)
            command_type = command.get('type')
            
            if command_type == 'set_param':
                self._handle_set_param(command, socket)
            elif command_type == 'relay_control':
                self._handle_open_relay(command, socket)
            elif command_type == 'relay_control_2':
                self._handle_open_relay_2(command, socket)
            elif command_type == 'motor_control':
                self._handle_motor_control(command, socket)
            elif command_type == 'auto_process':
                self._handle_auto_process(command, socket)
            else:
                self.logger.warning(f"未知命令类型: {command_type}")
                self._send_response(socket, {"status": "error", "message": f"未知命令类型: {command_type}"})
        except json.JSONDecodeError:
            self.logger.error("JSON解析错误")
            self._send_response(socket, {"status": "error", "message": "无效的JSON格式"})
    
    def _handle_set_param(self, command, socket):
        """处理设置参数命令"""
        params = command.get('data', {})
        self.logger.info(f"设置参数: {params}")
        # 这里可以添加参数处理逻辑
        param = params.get('param')
        value = params.get('value')
        if param == 'target_temp':
            self.m_fTargetTemp = value
        elif param == 'target_ph':
            self.m_fTargetpH = value
        elif param == 'water_speed':
            self.m_fWaterSpeed = value
        elif param == 'feed_speed':
            self.m_fFeedSpeed = value
        elif param == 'water_level_warning_threshold':
            self.m_fWaterLevelWarningThreshold = value
        elif param == 'jian_single_time':
            self.m_nJianTime = value
        elif param == 'jian_check_interval':
            self.m_nJianCheckInteval = value
        elif param == 'temp_check_interval':
            self.m_nTempCheckInteval = value


        self._send_response(socket, {"status": "success", "message": "参数设置成功"})
    
    def _handle_open_relay(self, command, socket):
        """处理打开继电器命令"""
        params = command.get('data', {})
        relay_num = params.get('relay_num')
        state = params.get('state', False)
        
        if relay_num is None:
            self._send_response(socket, {"status": "error", "message": "缺少继电器编号"})
            return
        
        if not self.relay_controller:
            self._send_response(socket, {"status": "error", "message": "继电器控制器未初始化"})
            return
        
        try:
            relay_num = int(relay_num)
            if not 0 <= relay_num <= 3:
                self._send_response(socket, {"status": "error", "message": "继电器编号必须在0-3之间"})
                return
            
            if relay_num != 0:  #需要确定碱泵的继电器编号
                result = self.relay_controller.set_coil(1, relay_num, state)
            else:
                result = self.relay_controller.set_coil_on_sometime(1, relay_num, self.m_nJianTime)
            if result:
                self._send_response(socket, {"status": "success", "message": f"继电器 {relay_num} 已{'打开' if state else '关闭'}"})
            else:
                self._send_response(socket, {"status": "error", "message": f"设置继电器 {relay_num} 状态失败"})
        except ValueError:
            self._send_response(socket, {"status": "error", "message": "继电器编号必须是整数"})


    def _handle_open_relay_2(self, command, socket):
        """处理打开继电器命令"""
        params = command.get('data', {})
        relay_num = params.get('relay_num') #继电器编号
        state = params.get('state', False) #继电器状态

        if relay_num is None:
            self._send_response(socket, {"status": "error", "message": "缺少继电器编号"})
            return  
        
        if not self.relay_controller:
            self._send_response(socket, {"status": "error", "message": "继电器控制器未初始化"})
            return
        
        try:
            relay_num = int(relay_num)
            if not 0 <= relay_num <= 3:
                self._send_response(socket, {"status": "error", "message": "继电器编号必须在0-3之间"})
                return
            
            result = self.relay_controller.set_coil(2, relay_num, state)
            if result:
                self._send_response(socket, {"status": "success", "message": f"继电器 {relay_num} 已{'打开' if state else '关闭'}"})
            else:
                self._send_response(socket, {"status": "error", "message": f"设置继电器 {relay_num} 状态失败"})
        except ValueError:
            self._send_response(socket, {"status": "error", "message": "继电器编号必须是整数"})


    def _handle_motor_control(self, command, socket):
        """处理电机控制命令"""
        params = command.get('data', {})
        motor_num = params.get('motor_num')
        speed = int(params.get('speed') * 100)   # 换算关系待定
        
        if motor_num is None:
            self._send_response(socket, {"status": "error", "message": "缺少电机编号"})
            return
        
        if not self.stepmotor_controller:
            self._send_response(socket, {"status": "error", "message": "步进电机控制器未初始化"})
            return
        
        try:
            motor_num = int(motor_num)
            if not 1 <= motor_num <= 2:
                self._send_response(socket, {"status": "error", "message": "电机编号必须在1-2之间"})
                return
            
            if speed is None:
                self._send_response(socket, {"status": "error", "message": "缺少速度值"})
                return
            
            if speed < 0:
                self._send_response(socket, {"status": "error", "message": "速度值必须大于0"})
                return
            
            if motor_num == 1:
                self.stepmotor_controller.set_motor1_speed(speed)
                self.m_fWaterSpeed = params.get('speed')
            elif motor_num == 2:
                self.stepmotor_controller.set_motor2_speed(speed)
                self.m_fFeedSpeed = params.get('speed')
            
            self._send_response(socket, {"status": "success", "message": f"电机 {motor_num} 已设置为 {speed} 速度"})
        except Exception as e:
            self._send_response(socket, {"status": "error", "message": str(e)})


    def _handle_auto_process(self, command, socket):
        params = command.get('data', {})
        isStart = params.get("start", False)
        if isStart:
            self.jian_timer.start()
            self.temp_timer.start()
        else:
            self.jian_timer.stop()
            self.temp_timer.stop()


            
            
    
    def _send_response(self, socket, response):
        """发送响应给客户端"""
        if not socket or socket.state() != QTcpSocket.ConnectedState:
            return
        
        response_json = json.dumps(response)
        socket.write(QByteArray(response_json.encode('utf-8')))

    def _jian_process(self):
        """碱泵自动控制"""
        self.logger.info("碱泵自动控制")
        try:
            # 读取当前pH值
            current_pH = self.sensor_data.get('ph')
            self.logger.info(f"当前pH值: {current_pH}")
            
            # 判断是否需要投碱
            if current_pH < self.m_fTargetpH:
                self.logger.info("需要投碱")
                # 打开继电器,持续jiantime
                self.relay_controller.set_coil_on_sometime(1, 4, self.m_nJianTime)
            else:
                self.logger.info("不需要投碱")
        except Exception as e:
            self.logger.error(f"碱泵自动控制时发生错误: {e}")


    def _temp_process(self):
        """温度自动控制"""
        self.logger.info("温度自动控制")
        try:
            # 读取当前温度
            current_temp = self.sensor_data.get('temperature')
            self.logger.info(f"当前温度: {current_temp}")
            
            # 判断是否需要加热
            if current_temp < self.m_fTargetTemp:
                self.logger.info("需要加热")
                # 打开继电器,持续加热时间
                self.relay_controller.set_coil(1, 1 , True)
            else:
                self.logger.info("不需要加热")
                self.relay_controller.set_coil(1, 1 , False)
        except Exception as e:
            self.logger.error(f"温度自动控制时发生错误: {e}")
    
            


def main():
    import sys
    from PySide6.QtWidgets import QApplication

    app = QApplication(sys.argv)
    server = ControlServer()
    server.start()
    sys.exit(app.exec())

if __name__ == '__main__':
    main()


main.py

import sys
import cv2
import numpy as np
from PySide6.QtCore import QUrl, QObject, Signal, Slot, Property, QTimer, QResource
from PySide6.QtGui import QGuiApplication, QImage
from PySide6.QtQuick import QQuickImageProvider
from PySide6.QtQml import QQmlApplicationEngine
from PySide6 import QtWidgets
from PySide6.QtNetwork import QTcpSocket
from PySide6.QtCore import QJsonDocument, QByteArray
import json

import multiprocessing
import os
import importlib.util

class FrameImageProvider(QQuickImageProvider):
    def __init__(self):
        super().__init__(QQuickImageProvider.Image)
        self._image = QImage(4000, 2000, QImage.Format_RGB888)
        self._image.fill(0)  # 初始化为黑色图像
        # self._image = None
        
    def requestImage(self, id, size, requestedSize):
        # 正确的签名应该是3个参数(除了self)
        return self._image
        
    def updateImage(self, image):
        self._image = image


class RTSPController(QObject):
    statusChanged = Signal(str)
    newFrameAvailable = Signal()
    connectionStatusChanged = Signal(bool)
    # 添加新的信号用于更新UI状态
    serverDataReceived = Signal(dict)  # 用于发送接收到的JSON数据到QML
    serverConnectionChanged = Signal(bool)  # 用于通知服务器连接状态
    
    def __init__(self, image_provider):
        super().__init__()
        self._image_provider = image_provider
        self._is_connected = False
        self._cap = None
        self._timer = QTimer()
        self._timer.timeout.connect(self.update_frame)
        
        # 初始化TCP Socket
        self._socket = QTcpSocket(self)
        self._socket.connected.connect(self._handle_connected)
        self._socket.disconnected.connect(self._handle_disconnected)
        self._socket.readyRead.connect(self._handle_ready_read)
        self._socket.errorOccurred.connect(self._handle_error)
        
        # 自动连接到服务器
        self._connect_to_server()
        
    def _connect_to_server(self):
        """连接到本地TCP服务器"""
        self._socket.connectToHost("localhost", 28888)
    
    def _handle_connected(self):
        """处理连接成功事件"""
        self.serverConnectionChanged.emit(True)
        print("已连接到服务器")
    
    def _handle_disconnected(self):
        """处理断开连接事件"""
        self.serverConnectionChanged.emit(False)
        print("与服务器断开连接")
        # 可以在这里添加重连逻辑
        QTimer.singleShot(5000, self._connect_to_server)
    
    def _handle_error(self, socket_error):
        """处理Socket错误"""
        print(f"Socket错误: {self._socket.errorString()}")
    
    def _handle_ready_read(self):
        """处理接收到的数据"""
        try:
            while self._socket.bytesAvailable():
                # 读取数据直到找到换行符
                data = self._socket.readLine().data().decode().strip()
                if data:
                    # 解析JSON数据
                    json_data = json.loads(data)
                    # 发送信号到QML
                    self.serverDataReceived.emit(json_data)
        except json.JSONDecodeError as e:
            print(f"JSON解析错误: {e}")
        except Exception as e:
            print(f"数据处理错误: {e}")
    
    @Slot(dict)
    def send_command(self, command):
        """发送命令到服务器
        
        command: 命令数据, 字典类型
        """
        if not self._socket.state() == QTcpSocket.ConnectedState:
            print("未连接到服务器")
            return
            
        try:
            # 将数据转换为JSON字符串,并添加换行符
            # json_str = json.dumps(command) + "\n"
            json_str = json.dumps(command)
            # 发送数据
            print(f"发送命令: {json_str}")
            self._socket.write(json_str.encode())
        except Exception as e:
            print(f"发送命令错误: {e}")
    
    @Slot(str)
    def connect_to_stream(self, url):
        if hasattr(self, '_cap') and self._cap is not None:
            self.stop_stream()
            
        if not url:
            self.statusChanged.emit("请输入RTSP URL")
            return
            
        try:
            self._cap = cv2.VideoCapture(url)
            if not self._cap.isOpened():
                self.statusChanged.emit("无法打开RTSP流")
                self._cap = None
                return
                
            # Start the timer to update frames
            self._timer.start(30)  # Update every 30ms (approx 33 fps)
            self._is_connected = True
            self.connectionStatusChanged.emit(True)
            self.statusChanged.emit("已连接")
        except Exception as e:
            self.statusChanged.emit(f"连接流错误: {str(e)}")
            self._cap = None
    
    @Slot()
    def stop_stream(self):
        if hasattr(self, '_cap') and self._cap is not None:
            self._timer.stop()
            self._cap.release()
            self._cap = None
            self._is_connected = False
            self.connectionStatusChanged.emit(False)
            self.statusChanged.emit("未连接")
            
    def update_frame(self):
        if not hasattr(self, '_cap') or self._cap is None:
            return
            
        ret, frame = self._cap.read()
        if not ret:
            self.stop_stream()
            self.statusChanged.emit("流已结束或连接丢失")
            return
            
        # Convert the frame from BGR to RGB format
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        
        # 使用更可靠的方式创建QImage
        h, w, ch = frame_rgb.shape
        bytes_per_line = ch * w
        
        # 首先确保numpy数组是连续的
        if not frame_rgb.flags['C_CONTIGUOUS']:
            frame_rgb = np.ascontiguousarray(frame_rgb)
            
        # 创建QImage时不调用tobytes(),直接使用数据指针
        q_img = QImage(frame_rgb.data, w, h, bytes_per_line, QImage.Format_RGB888)
        q_img = q_img.copy()  # 深度复制确保数据独立
        
        # Update the image provider with the new frame
        self._image_provider.updateImage(q_img)
        
        # Notify QML that a new frame is available
        self.newFrameAvailable.emit()
    
    def __del__(self):
        # 确保在对象销毁时关闭socket
        if hasattr(self, '_socket'):
            self._socket.close()
        if hasattr(self, '_cap') and self._cap is not None:
            self._cap.release()





def start_control_server():
    """在单独的进程中启动control_server.py"""
    try:
        # 获取当前文件所在目录
        current_dir = os.path.dirname(os.path.abspath(__file__))
        # 构建control_server.py的完整路径
        server_path = os.path.join(current_dir, "control_server.py")
        
        # 直接使用Python解释器运行脚本
        import subprocess
        subprocess.Popen([sys.executable, server_path])
        print(f"已启动control_server.py作为独立进程")
    except Exception as e:
        print(f"启动control_server.py时出错: {e}")

if __name__ == "__main__":
    # 创建并启动control_server进程
    control_server_process = multiprocessing.Process(
        target=start_control_server,
        name="ControlServerProcess",
        daemon=True  # 设置为守护进程,这样主程序退出时它会自动终止
    )
    control_server_process.start()
    print(f"Control server started with PID: {control_server_process.pid}")

    
    # app = QGuiApplication(sys.argv)
    app = QtWidgets.QApplication(sys.argv)
    
    # Create QML engine
    engine = QQmlApplicationEngine()
    
    # Create and register the image provider - must register before loading QML
    frame_provider = FrameImageProvider()
    engine.addImageProvider("frames", frame_provider)
    
    # Create controller and expose to QML
    controller = RTSPController(frame_provider)
    
    # Expose controller to QML
    engine.rootContext().setContextProperty("controller", controller)
    
    # Load QML
    engine.load(QUrl.fromLocalFile("main.qml"))
    


    if not engine.rootObjects():
        sys.exit(-1)
        
    sys.exit(app.exec()) 

example.json


// 传感器数据上报
{
    "type": "sensor_data",
    "data": {
        "temperature": 25.0,
        "ph": 7.0,
        "water_level": 0.5,   //培养基液面高度
        "jian_weight": 12.0   // 碱液剩余重量
    }
}

// 控制命令1
{
    "type": "relay_control",       
    "data": {
        "relay_num": 1,   //碱泵: 0,  气泵: 1,  搅拌+潜水泵: 2,  加热: 3,
        "state": true/false
    }
}

// 控制命令2
{
    "type": "motor_control",
    "data": {
        "motor_num": 1,      // 水泵: 1,   培养基泵: 2
        "speed": 1000
    }
}


// 设置参数
{
    "type": "set_param",
    "data": {
        "param": "target_temp",   // 目标温度
        "value": 23.0
    }
}

{
    "type": "set_param",
    "data": {
        "param": "target_ph",  // 目标pH
        "value": 7.0
    }
}


{
    "type": "set_param",
    "data": {
        "param": "ph_check_interval",  // pH检测间隔
        "value": 30
    }
}

{
    "type": "set_param",
    "data": {
        "param": "jian_single_time",   // 单次投碱量
        "value": 500
    }
}


{
    "type": "set_param",
    "data": {
        "param": "water_speed",   // 加水流量
        "value": 5.0
    }
}

{
    "type": "set_param",
    "data": {
        "param": "liao_speed",   // 培养基流量
        "value": 5.0
    }
}

{
    "type": "set_param",
    "data": {
        "param": "water_level_warning_threshold",   // 培养基页面高度报警阈值
        "value": 0.2
    }
}




main.qml

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15

Window {
    id: root
    width: 1200
    height: 600
    visible: true
    title: "反应器控制与监控系统"
    
    Component.onCompleted: {
        root.showMaximized()
        // root.showFullScreen()
    }
    
    // 主布局
    ColumnLayout {
        anchors.fill: parent
        spacing: 0
        
        // SwipeView 作为主容器
        SwipeView {
            id: swipeView
            Layout.fillWidth: true
            Layout.fillHeight: true
            clip: true
            interactive: true // 允许滑动切换页面
            
            // 第一页:控制面板
            Item {
                id: controlPage
                
                RowLayout {
                    anchors.fill: parent
                    spacing: 0
                    
                    // 视频显示区域
                    VideoViewer {
                        id: videoViewer
                        Layout.preferredWidth: root.width * 0.7
                        Layout.fillHeight: true
                    }
                    
                    // 控制面板区域
                    ControlPanel {
                        id: controlPanel
                        Layout.preferredWidth: root.width * 0.3
                        Layout.fillHeight: true
                        
                        // 处理控制变化
                        onControlChanged: function(controlName, enabled, parameters) {
                            console.log(controlName + " changed to " + enabled + 
                                      " with parameters: " + JSON.stringify(parameters))
                            
                            // 在这里可以添加与后端的交互代码
                            // 例如: controller.setHeating(enabled, parameters.setTemperature)
                        }
                    }
                }
            }
            
            // 第二页:曲线图表 - 使用独立组件
            ChartsPage {
                id: chartsPage
            }
        }
        
        // 页面指示器
        PageIndicator {
            id: indicator
            count: swipeView.count
            currentIndex: swipeView.currentIndex
            
            Layout.alignment: Qt.AlignHCenter
            spacing: 10
            
            delegate: Rectangle {
                implicitWidth: 10
                implicitHeight: 10
                radius: width / 2
                color: index === indicator.currentIndex ? "#00b4ff" : "#444857"
                
                Behavior on color { ColorAnimation { duration: 200 } }
                
                MouseArea {
                    anchors.fill: parent
                    onClicked: swipeView.setCurrentIndex(index)
                }
            }
        }
    }
    
    // // 数据定时器 - 用于模拟数据更新
    // Timer {
    //     interval: 1000
    //     running: true
    //     repeat: true
    //     onTriggered: {
    //         // 生成模拟数据
    //         var tempValue = 23 + Math.random() * 2;
    //         var phValue = 7.0 + Math.random() * 0.5;
    //         var heightValue = 50 + Math.sin(new Date().getTime() * 0.0001) * 10;
            
    //         // 更新图表
    //         chartsPage.updateCharts(tempValue, phValue, heightValue);
            
    //         // 更新控制面板显示
    //         controlPanel.updateTemperature(tempValue);
    //         controlPanel.updatepH(phValue);
    //     }
    // }
    
    // 连接信号
    Connections {
        target: controller  // 假设您的controller实例名称为controller
        
        // 处理服务器连接状态变化
        function onServerConnectionChanged(connected) {
            // 更新UI显示连接状态
            serverStatusText.text = connected ? "服务器已连接" : "服务器未连接"
        }
        
        // 处理接收到的服务器数据
        function onServerDataReceived(data) {
            // 根据接收到的数据更新UI
            // 例如:
            if (data.type === "sensor_data") {
                var parameters = data.data
                var tempValue = parameters.temperature
                var phValue = parameters.ph
                var water_level = parameters.water_level
                var remaining_alkali_amount = parameters.weight

                remaining_alkali_amount -= 3.61
                if (remaining_alkali_amount < 0) {
                    remaining_alkali_amount = 0
                }
                // 更新图表
                chartsPage.updateCharts(tempValue, phValue, (water_level*1000).toFixed(0));
                
                // 更新控制面板显示
                controlPanel.updateTemperature(tempValue);
                controlPanel.updatepH(phValue);
                controlPanel.updateWaterLevel(water_level);
                controlPanel.updateRemainingAlkaliAmount(remaining_alkali_amount);
            }
        }
    }

    // 灯控制组件 - 显示在最顶层
    Rectangle {
        id: lightControl
        width: 60
        height: 60
        radius: 30
        color: isOn? "#00b4ff" : "#272a36"
        visible: swipeView.currentIndex === 0
        
        // 定位在左上角
        anchors.left: parent.left
        anchors.top: parent.top
        anchors.margins: 10
        
        // 确保在最顶层显示
        z: 100
        
        // 灯的状态
        property bool isOn: false
        
        // 灯图片
        Image {
            id: lightImage
            anchors.centerIn: parent
            width: parent.width - 10
            height: parent.height - 10
            source: parent.isOn ? "deng_on.png" : "deng_off.png"
            fillMode: Image.PreserveAspectFit
        }
        
        // 点击切换灯的状态
        MouseArea {
            anchors.fill: parent
            onClicked: {
                lightControl.isOn = !lightControl.isOn
                
                // 可以在这里添加控制实际灯的代码
                // 例如:
                controller.send_command({
                    "type": "relay_control_2",
                    "data": {
                        "relay_num": 1,  // 假设灯连接到继电器1
                        "state": lightControl.isOn
                    }
                })
            }
        }
    }
} 

ChartsPage.qml

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtCharts 2.15

Item {
    id: chartsPage
    
    // 属性定义
    // property alias tempSeries: tempSeries
    // property alias phSeries: phSeries
    // property alias heightSeries: heightSeries
    // property alias tempAxisX: tempAxisX
    // property alias phAxisX: phAxisX
    // property alias heightAxisX: heightAxisX
    
    // 背景
    Rectangle {
        anchors.fill: parent
        color: "#0d0e13" // 与控制面板背景色匹配
        
        ColumnLayout {
            anchors.fill: parent
            anchors.margins: 20
            spacing: 15
            
            Text {
                text: "一体化设备实时数据曲线"
                font.pixelSize: 24
                font.bold: true
                font.family: "Segoe UI, Arial, sans-serif"
                color: "#ffffff"
                Layout.alignment: Qt.AlignHCenter
            }
            
            // 温度曲线
            ChartView {
                id: temperatureChart
                Layout.fillWidth: true
                Layout.fillHeight: true
                title: "温度曲线"
                antialiasing: true
                legend.visible: false
                backgroundColor: "#1a1c23"
                titleColor: "#ffffff"
                
                ValueAxis {
                    id: tempAxisY
                    min: 10
                    max: 40
                    color: "#ffffff"
                    labelsColor: "#ffffff"
                    gridLineColor: "#2a2d3a"
                }
                
                DateTimeAxis {
                    id: tempAxisX
                    format: "HH:mm:ss"
                    tickCount: 10
                    color: "#ffffff"
                    labelsColor: "#ffffff"
                    gridLineColor: "#2a2d3a"
                }
                
                LineSeries {
                    id: tempSeries
                    name: "温度"
                    axisX: tempAxisX
                    axisY: tempAxisY
                    color: "#00b4ff"
                    width: 2
                }
                
                Component.onCompleted: {
                    // 初始数据
                    var now = new Date();
                    tempAxisX.min = new Date(now.getTime() - 600000); // 10分钟前
                    tempAxisX.max = now;
                    
                    // 添加一些初始点
                    for (var i = -30; i <= 0; i++) {
                        tempSeries.append(now.getTime() + i * 20000, 0);
                    }
                }
            }
            
            // // pH曲线
            ChartView {
                id: phChart
                Layout.fillWidth: true
                Layout.fillHeight: true
                title: "pH值曲线"
                antialiasing: true
                legend.visible: false
                backgroundColor: "#1a1c23"
                titleColor: "#ffffff"
                
                ValueAxis {
                    id: phAxisY
                    min: 6
                    max: 9
                    color: "#ffffff"
                    labelsColor: "#ffffff"
                    gridLineColor: "#2a2d3a"
                }
                
                DateTimeAxis {
                    id: phAxisX
                    format: "HH:mm:ss"
                    tickCount: 10
                    color: "#ffffff"
                    labelsColor: "#ffffff"
                    gridLineColor: "#2a2d3a"
                }
                
                LineSeries {
                    id: phSeries
                    name: "pH值"
                    axisX: phAxisX
                    axisY: phAxisY
                    color: "#ff4757"
                    width: 2
                }
                
                Component.onCompleted: {
                    // 初始数据
                    var now = new Date();
                    phAxisX.min = new Date(now.getTime() - 600000); // 10分钟前
                    phAxisX.max = now;
                    
                    // 添加一些初始点
                    for (var i = -30; i <= 0; i++) {
                        phSeries.append(now.getTime() + i * 20000, 0);
                    }
                }
            }
            
            // // 培养基高度曲线
            ChartView {
                id: heightChart
                Layout.fillWidth: true
                Layout.fillHeight: true
                title: "培养基高度曲线"
                antialiasing: true
                legend.visible: false
                backgroundColor: "#1a1c23"
                titleColor: "#ffffff"
                
                ValueAxis {
                    id: heightAxisY
                    min: 0
                    max: 600
                    color: "#ffffff"
                    labelsColor: "#ffffff"
                    gridLineColor: "#2a2d3a"
                }
                
                DateTimeAxis {
                    id: heightAxisX
                    format: "HH:mm:ss"
                    tickCount: 10
                    color: "#ffffff"
                    labelsColor: "#ffffff"
                    gridLineColor: "#2a2d3a"
                }
                
                LineSeries {
                    id: heightSeries
                    name: "培养基高度"
                    axisX: heightAxisX
                    axisY: heightAxisY
                    color: "#2ed573"
                    width: 2
                }
                
                Component.onCompleted: {
                    // 初始数据
                    var now = new Date();
                    heightAxisX.min = new Date(now.getTime() - 600000); // 10分钟前
                    heightAxisX.max = now;
                    
                    // 添加一些初始点
                    for (var i = -30; i <= 0; i++) {
                        heightSeries.append(now.getTime() + i * 20000, 0);
                    }
                }
            }


        }
    }
    
    // 更新数据的函数
    function updateCharts(tempValue, phValue, heightValue) {
        var now = new Date();
        
        // 更新横轴时间范围
        tempAxisX.max = now;
        tempAxisX.min = new Date(now.getTime() - 600000);
        
        phAxisX.max = now;
        phAxisX.min = new Date(now.getTime() - 600000);
        
        heightAxisX.max = now;
        heightAxisX.min = new Date(now.getTime() - 600000);
        
        // 添加新数据点
        tempSeries.append(now.getTime(), tempValue);
        phSeries.append(now.getTime(), phValue);
        heightSeries.append(now.getTime(), heightValue);
        
        // 限制数据点数量,避免内存占用过大
        if (tempSeries.count > 500) {
            tempSeries.remove(0);
            phSeries.remove(0);
            heightSeries.remove(0);
        }
    }
} 

VideoViewer.qml

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

Rectangle {
    id: videoViewerRoot

    property string rtspUrl: "rtsp://admin:123456@192.168.1.123/video2"

    function connectToStream(url) {
        if (url) {
            rtspUrl = url;
            controller.connect_to_stream(rtspUrl);
        }

    }

    color: "black"

    // Connect to controller signals
    Connections {
        function onNewFrameAvailable() {
            videoOutput.source = "image://frames/frame?" + Math.random();
        }

        function onStatusChanged(status) {
            statusText.text = status;
            statusText.visible = videoOutput.source === "";
        }

        target: controller
    }

    Image {
        id: videoOutput

        anchors.fill: parent
        fillMode: Image.Stretch
        cache: false
        Component.onCompleted: {
            videoViewer.connectToStream(rtspUrl);
        }
    }

    Text {
        id: statusText

        anchors.centerIn: parent
        color: "white"
        font.pixelSize: 20
        text: "No stream connected"
        visible: videoOutput.source === ""
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值