PHP 驱动的工业自动化报表系统:利用全栈架构实现传感器实时数据采集与动态图表展示

各位工友们,大家晚上好!

欢迎来到今天的“硬核修仙”大会。今天我们不谈架构图上的饼图和柱状图,也不谈那些在PPT里跳来跳去的K线图。今天,我们要聊的是稍微“粗糙”一点、稍微“工业”一点的东西——工业自动化报表系统

很多人听到“PHP”,第一反应是:“哟,做博客的,写CRUD的,前端切图仔的。” 哎呀呀,这种偏见就像说“诺基亚只能打接电话”一样过时了。PHP这门语言,只要用对了姿势,它不仅能做高并发,还能当调度员,甚至能当那个拿着喇叭喊“开工”的监工。

今天,我们要利用全栈架构,干一件大事:从传感器里抠数据,把冷冰冰的电压电流变成热气腾腾的实时折线图。

准备好了吗?我们要开始给生产线“通灵”了。

第一部分:先别急着敲代码,先搞清楚你在跟谁打交道

在工业自动化领域,最可怕的不是程序跑不通,而是数据没跟上

想象一下,你是一个温度传感器,你的工作是监测炼钢炉里的温度。你每毫秒产生一个数值,这个数值如果不被采集,炉子就会爆炸(或者生产出来的螺丝全是废品)。

我们的目标是构建一个系统:传感器 -> 数据管道 -> 报表系统 -> 大屏幕 -> CEO点头。

在这个系统里,PHP是大脑,WebSocket是神经,Redis是仓库,Chart.js是画笔。别觉得PHP不行,PHP就像瑞士军刀,虽然它没有手术刀那么专业,但在工业现场,它能把你那个螺丝钉精准地装上去。

第二部分:模拟传感器——让数据自己动起来

没有传感器?没关系,我们这里先来点“玄学编程”。我们写一个脚本来模拟传感器数据。在真实世界里,这是Python跑在树莓派上,或者C++跑在单片机上。但在我们的PHP演示里,我们要展示PHP的“并发模拟能力”。

我们要模拟正态分布的数据。为什么是正态分布?因为现实世界不是均匀分布的,它是“大部分正常,偶尔发疯”。

<?php
// sensor_simulation.php
// 这是一个伪装成传感器的PHP脚本,别告诉任何人

class Sensor {
    private $baseTemp = 25; // 基础温度25度
    private $fluctuation = 5; // 波动范围5度

    // 模拟正态分布的随机数(数学不好别慌,这里用Box-Muller变换偷个懒)
    private function getNormalRandom($mean, $stddev) {
        $u1 = mt_rand(-100000, 100000) / 100000;
        $u2 = mt_rand(-100000, 100000) / 100000;
        $z0 = sqrt(-2.0 * log($u1)) * cos(2.0 * M_PI * $u2);
        return $z0 * $stddev + $mean;
    }

    public function read() {
        $currentTemp = $this->getNormalRandom($this->baseTemp, $this->fluctuation);

        // 随机制造一个“故障”信号,模拟传感器失效
        if (mt_rand(1, 1000) > 990) {
            return [
                'status' => 'error',
                'value' => null,
                'message' => '传感器连接丢失!'
            ];
        }

        return [
            'status' => 'ok',
            'value' => round($currentTemp, 2),
            'timestamp' => time(),
            'machine_id' => 'MACH-' . mt_rand(1, 10)
        ];
    }
}

$sensor = new Sensor();

// 轮询输出数据,假装我们在不断地读取硬件
while (true) {
    $data = $sensor->read();
    echo json_encode($data) . "n";
    // 模拟硬件采样周期:100毫秒
    usleep(100000); 
}

看,这段代码虽然简单,但它模拟了现实中的“噪音”和“断点”。接下来,我们的PHP后端要像捕食者一样,死死盯住这个输出,把数据抢过来,然后通过 WebSocket 传给前端。

第三部分:后端架构——为什么我们需要 WebSocket?

你说用 AJAX 轮询行不行?行,当然行。但是,这就像是你每隔10分钟去问女朋友“你爱我吗”,如果她回答“爱”,你很高兴;如果她不回答,你还在等。这种效率在工业现场是致命的。

我们需要的是WebSocket。它像是一条永远不挂断的电话线。

为了实现这个,我们要用 Ratchet,一个著名的 PHP WebSocket 库。当然,在生产环境,我会推荐用 Swoole,那个简直是加速器。但为了保持代码通俗易懂,我们用 Ratchet,它基于 Swoole 的思想,但不需要你安装复杂的扩展。

首先,安装 Ratchet:
composer require cboden/ratchet

然后,我们的“接线员”代码来了:

<?php
// server.php
require dirname(__FILE__) . '/vendor/autoload.php';

use RatchetMessageComponentInterface;
use RatchetConnectionInterface;

class MachineDataServer implements MessageComponentInterface {
    protected $clients;

    public function __construct() {
        // 所有的客户端都住在这个房间里
        $this->clients = new SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn) {
        // 客户端上线了,给他分配个ID,顺便欢迎一下
        $conn->resourceId = $conn->resourceId;
        $this->clients->attach($conn);
        echo "新客户端加入: {$conn->resourceId}n";

        // 给新来的发个问候,告诉他现在几点了
        $conn->send(json_encode(['type' => 'system', 'message' => '欢迎加入工业监控大屏!']));
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        // 客户端发来的消息通常心跳包或者指令,这里我们暂不处理
        echo "收到客户端 {$from->resourceId} 的消息: $msgn";
    }

    public function onClose(ConnectionInterface $conn) {
        // 客户端下线了,从房间里赶出去
        $this->clients->detach($conn);
        echo "客户端 {$conn->resourceId} 离开n";
    }

    public function onError(ConnectionInterface $conn, Exception $e) {
        // 客户端挂了,或者协议搞错了,默默处理
        $conn->close();
        echo "发生错误: {$e->getMessage()}n";
    }

    // 核心功能:广播数据
    public function broadcastData($data) {
        // 遍历房间里的每一个人,把数据塞进他们手里
        foreach ($this->clients as $client) {
            // 防止发送失败导致整个服务崩溃(虽然Ratchet处理得不错,但保险起见)
            if ($client->isConnected()) {
                $client->send(json_encode($data));
            }
        }
    }
}

// 启动服务器
$loop   = ReactEventLoopFactory::create();
$websock = new RatchetServerIoServer(
    new RatchetHttpHttpServer(
        new RatchetWebSocketWsServer(
            new MachineDataServer
        )
    ),
    8080 // 监听8080端口
);

echo "服务器启动中,监听端口 8080...n";
$websock->run();

这段代码的逻辑很简单:监听8080端口,谁连接进来,就告诉服务器“我来了”。然后,服务器每隔一点点时间(比如10毫秒),就把传感器读到的数据推送给所有连接的人。

这叫什么?这就叫“不问自取”。

第四部分:前端展示——让图表飞起来

现在,后端已经是一个忠实的信使了。接下来是前端。我们需要一个 HTML 页面,它能连接 WebSocket,然后画图。

很多人用 ECharts,它确实牛逼,但对于这种实时流,Chart.js 更轻量,更容易理解。我们来做个简单的仪表盘。

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>我是AI,但我也是工厂监工</title>
    <!-- 引入 Chart.js 和 Bootstrap 简单美化一下 -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-dark text-white">
    <div class="container mt-5">
        <h1 class="text-center mb-4">🏭 工业大数据实时看板</h1>

        <div class="row">
            <div class="col-md-6">
                <div class="card bg-secondary mb-3">
                    <div class="card-header">实时温度监控</div>
                    <div class="card-body">
                        <canvas id="tempChart"></canvas>
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="card bg-success mb-3">
                    <div class="card-header">系统状态</div>
                    <div class="card-body">
                        <p>连接状态: <span id="status" class="badge bg-danger">未连接</span></p>
                        <p>最新数值: <span id="lastValue" class="display-4">--</span> °C</p>
                        <p>采样率: 100ms</p>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        // 1. 配置图表
        const ctx = document.getElementById('tempChart').getContext('2d');
        const chart = new Chart(ctx, {
            type: 'line',
            data: {
                labels: [], // 时间轴
                datasets: [{
                    label: '实时温度 (°C)',
                    data: [], // 数据轴
                    borderColor: 'rgb(255, 99, 132)',
                    backgroundColor: 'rgba(255, 99, 132, 0.2)',
                    tension: 0.4, // 让曲线变平滑
                    fill: true
                }]
            },
            options: {
                scales: {
                    x: { display: false }, // 隐藏时间轴,看起来更像心跳图
                    y: { beginAtZero: false }
                },
                animation: false, // 关闭动画,提高性能
                plugins: { legend: { display: false } }
            }
        });

        // 2. WebSocket 连接
        const socket = new WebSocket('ws://localhost:8080');

        socket.onopen = function() {
            document.getElementById('status').innerText = "在线";
            document.getElementById('status').className = "badge bg-success";
        };

        socket.onmessage = function(event) {
            const data = JSON.parse(event.data);

            // 如果是系统消息
            if (data.type === 'system') {
                console.log(data.message);
                return;
            }

            // 如果是传感器数据
            if (data.status === 'ok') {
                updateChart(data.value);
            } else {
                // 传感器报错
                alert('警告:' + data.message);
            }
        };

        function updateChart(newValue) {
            const now = new Date().toLocaleTimeString();

            // 添加数据点
            chart.data.labels.push(now);
            chart.data.datasets[0].data.push(newValue);

            // 保持数据点数量不超过50个,防止内存溢出(如果真的连接了一万台机器,这里需要优化)
            if (chart.data.labels.length > 50) {
                chart.data.labels.shift();
                chart.data.datasets[0].data.shift();
            }

            // 更新 DOM 显示的数值
            document.getElementById('lastValue').innerText = newValue;

            chart.update();
        }

        // 错误处理
        socket.onerror = function(error) {
            console.error('WebSocket Error: ', error);
            document.getElementById('status').innerText = "错误";
        };

        socket.onclose = function() {
            document.getElementById('status').innerText = "断开";
            document.getElementById('status').className = "badge bg-warning";
        };
    </script>
</body>
</html>

看到没?代码这么少,效果却很震撼。前端做的事情非常纯粹:连接 -> 收消息 -> 塞进图表 -> 刷新。没有任何繁琐的 DOM 操作,这就是现代前端框架(哪怕是原生JS)的魅力。

第五部分:数据处理——Redis 是你的避难所

好了,如果你直接运行上面的代码,可能会发现一个性能瓶颈:当有1000个传感器同时发送数据,而且有5000个客户端在同时监听,这个 PHP 进程可能会瞬间崩溃。

为什么?因为 PHP 是单线程的(除非你用了 Swoole),IO 操作(比如写数据库、写文件、发 WebSocket)在默认情况下是阻塞的。当所有人都排队等厕所的时候,厕所(CPU/内存)就堵死了。

这时候,我们需要引入 Redis。Redis 是个内存数据库,它的速度比 MySQL 快 100 倍,而且是单线程模型,天然不怕并发。

我们的新架构变成了:传感器 -> Redis List (队列) -> PHP Worker (监听队列) -> WebSocket -> 客户端

修改一下我们的 Worker 代码,让它去抢 Redis 里的活:

<?php
// worker.php
require dirname(__FILE__) . '/vendor/autoload.php';

use RatchetMessageComponentInterface;
use RatchetConnectionInterface;
use RatchetServerIoServer;
use RatchetHttpHttpServer;
use RatchetWebSocketWsServer;
use ReactEventLoopFactory;

// 假设我们有一个叫 "sensor_data" 的 Redis 列表
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

class RedisWorker {
    private $clients;

    public function __construct() {
        $this->clients = new SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn) {
        $this->clients->attach($conn);
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        // 这里暂时留空,主要靠 Redis 驱动
    }

    public function onClose(ConnectionInterface $conn) {
        $this->clients->detach($conn);
    }

    public function onError(ConnectionInterface $conn, Exception $e) {
        $conn->close();
    }

    public function broadcastFromRedis() {
        // 这是一个死循环,专门负责去 Redis 里偷数据
        while (true) {
            // BLPOP 是阻塞读取,如果没有数据,线程就睡觉,不占 CPU
            // 参数格式: key1, key2
            $result = $redis->brPop('sensor_data_queue', 1);

            if ($result) {
                $data = json_decode($result[1], true);

                // 推送给所有客户端
                foreach ($this->clients as $client) {
                    if ($client->isConnected()) {
                        $client->send(json_encode($data));
                    }
                }
            }
        }
    }
}

// 启动 WebSocket 服务器
$loop = Factory::create();

$websock = new IoServer(
    new HttpServer(
        new WsServer(new RedisWorker)
    ),
    8080
);

// 启动 Redis 监听线程
// 注意:在生产环境,这通常需要 Supervisor 守护进程
$worker = new RedisWorker();
$worker->broadcastFromRedis();

echo "启动 Redis 监听线程...n";

// 让这两个进程同时跑起来
$loop->run();

通过这个改动,PHP 就变成了一个“消费者”。它不负责产生数据(那是传感器的事),它只负责把数据从 Redis 拿出来,分发给前端。哪怕前端瞬间断线了,数据也只会丢在 Redis 里,不会丢失。

第六部分:数据库报表——历史数据的存档

WebSocket 解决了“实时”的问题,但老板更在乎“历史”。老板说:“上周二那台机器怎么罢工了?给我调出来看看!”

这时候,我们就需要 MySQL 了。虽然写 MySQL 很枯燥,但它是唯一的真相之源。

我们需要一个脚本,每隔一段时间(比如5分钟),把 Redis 队列里的数据清空,存入 MySQL。

<?php
// reporter.php
// 这个脚本由系统定时任务(Crontab)每分钟执行一次

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$conn = new mysqli('localhost', 'root', 'password', 'factory_db');

// 创建表(如果不存在)
$conn->query("CREATE TABLE IF NOT EXISTS temperature_logs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    machine_id VARCHAR(50),
    value DECIMAL(10, 2),
    recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");

// 从 Redis 队列取数据并写入 MySQL
while ($result = $redis->brPop('sensor_data_queue', 0)) {
    $data = json_decode($result[1], true);

    if ($data['status'] === 'ok') {
        $stmt = $conn->prepare("INSERT INTO temperature_logs (machine_id, value) VALUES (?, ?)");
        $stmt->bind_param("sd", $data['machine_id'], $data['value']);
        $stmt->execute();
    }
}

echo "报表生成完毕n";

这个脚本很聪明。它使用 brPop(..., 0),这个 0 表示“一直等,直到有数据”。如果 Redis 里没数据,脚本就停在这里,等下一分钟 Crontab 再唤醒它。这就保证了我们不会像无头苍蝇一样每秒查询一次数据库。

第七部分:故障排查与运维——当工厂断电时

写代码的时候觉得自己是乔布斯,一旦上线,你就是救火队员。

场景一:数据没发出来。
首先,看 PHP 进程还在不在?ps aux | grep php。如果进程挂了,检查内存溢出。工业数据量大的时候,图表实例没销毁,内存蹭蹭往上涨,最后 Out of memory。记得在 JS 里写 chart.data.labels.shift()

场景二:前端一直在转圈圈。
检查 WebSocket 的端口防火墙是否开放。如果是 HTTPS 网站,WebSocket 握手协议会有点特殊,需要配置 Nginx 的 Upgrade 头。

场景三:Redis 连接不上。
这通常是网络问题或者 Redis 没启动。工业现场有时候会有电磁干扰,网线稍微一碰,数据就断了。这时候你的前端代码里要有 reconnect 机制,别傻乎乎地等 WebSocket 自己重启。

第八部分:进阶——监控自己

既然我们做的是监控系统,那我们的监控系统本身,是不是也应该被监控?

我们可以写一个脚本,去检查端口 8080 是否响应。

#!/bin/bash
# monitor.sh
while true; do
    if ! netstat -an | grep 8080 | grep LISTEN > /dev/null; then
        echo "报警!PHP WebSocket 服务挂了!正在尝试重启..."
        /usr/bin/php /path/to/worker.php > /dev/null 2>&1 &
    fi
    sleep 10
done

把这个脚本扔到后台跑。这样,只要你的系统挂了,这个脚本会立马拉它起来。

结语:全栈的真谛

回过头来看,我们用 PHP 做了什么?

  1. 前端:处理用户交互,渲染 Canvas。
  2. 后端:处理逻辑,维持连接。
  3. 中间件:Redis 做缓冲。
  4. 数据库:持久化。

这就是全栈。不要觉得 PHP 只能写简单的 CRUD。在工业自动化这个领域,PHP 的灵活性、部署的简单性,加上 Redis 和 WebSocket 的加持,足以吊打那些动不动就要装 Java 虚拟机、配置几十个 Nginx 反向代理的复杂架构。

很多老工程师喜欢说:“不要重复造轮子。” 但今天我们造的这个轮子,是为了让生产线转得更快,让数据跑得更稳。

好了,代码都在这儿了。你可以试着跑一下,看看那个红线是不是在疯狂跳动。如果跳动得让你心慌,那就说明系统正常工作了。

祝大家代码无 Bug,工厂不炸膛!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注