基于WebServer的工业数据采集项目

一、项目名称

基于WebServer的工业数据采集项目

二、实现功能

本项目主要是通过网页来实现对传感器(光线传感器,加速度传感器x,y,z)数据的采集,以及对硬件设备(LED灯、Buzzer蜂鸣器)的模拟控制。

三、项目原理图

思路解析:思考如何才能实现网页控制Modbus设备,对于网页需要通过HTTP协议与webserver进行交互,对于采集控制程序与Modbus设备间需要通过Modbus TCP协议,而WebServer与Modbus采集控制之间的通信选择了共享内存和消息队列。

    1.网页与WebServer的交互

          采用HTTP协议,网页作为HTTP的客户端,用户通过url主动向webserver发送请求,webserver根据接收到的请求,处理完后,向客户端发送响应信息。

    2.Modbus采集控制程序与Modbus设备

           对于硬件设备的模拟,我们使用ModbusSlave软件,用于仿真Modbus从机,接收主机的命令包,回送数据包。

           在modbus采集控制程序与modbus设备之间,我们采用modbusTCP协议来实现,通过创建Modbus实例,和从机(slave)建立连接,通过创建线程函数循环采集传感器的数据。通过03功能码读保持寄存器来实现对传感器数据的采集,05功能码写单个线圈状态来实现对硬件设备(LED灯、蜂鸣器)的模拟控制。

    3.Modbus采集控制程序与WebServer服务器间的通信

          采集控制程序与webserver服务器间的通信,首先数据的获取使用了共享内存,可以进行多进程间的数据交互。将循环采集的数据放入共享内存中。对硬件设备的控制,使用了消息队列,按消息类型进行消息的添加与读取,来提高指令的准确性。

    四、具体实现流程:

    1.网页端发出modbus_get指令

           通过http协议传输给webserver,webserver通过handler_get函数,创建打开,映射共享内存,读取modbus采集控制程序存入共享内存的数据,并通过webserver的send返回给网页。

    2.网页端发出modbus_set指令

         webserver接收到控制命令后,通过handler_set函数创建消息队列,将其添加到消息队列,modbus采集控制程序,进行消息的读取,将读取到的指令进行判断,做出相应的控制操作。

    3.modbus采集控制程序

         利用handler_data线程函数,通过 modbus_read_registers 读取 Modbus 设备寄存器的数据(光线、加速度 XYZ),并存入共享内存中。利用handler_Control线程,将读取到的消息,判断后通过modbus_write_bit 向 Modbus 设备写入控制指令(如开灯、关蜂鸣器)。

    五、项目实现界面

    网页端界面:

    slave端界面及设置:

    运行效果:

    六、项目模块代码

    moudbus.c(Modbus采集控制程序)

    #include <stdio.h>
    #include "modbus.h"
    #include <bits/types.h>
    #include <pthread.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <errno.h>
    #include <sys/msg.h>
    
    modbus_t *ctx;
    struct msgbuf
    {
        long type; // 第一个成员必须是long类型变量,表示消息的类型
        int num1;
        int num2;
    };
    // 循环采集数据
    void *handler_data(void *arg)
    {
        // 创建共享内存->Modbus采集数据程序与Webserver进行数据交互
        // 创建key值
        key_t key = ftok(".", 'a');
        if (key < 0)
        {
            perror("key err\n");
            return NULL;
        }
        printf("ftok ok! key:%#x\n", key);
    
        // 创建或打开共享内存
        int shmid = shmget(key, 64, IPC_CREAT | IPC_EXCL | 0666);
        // 判断是否已存在->已存在无需创建,直接给权限即可
        if (shmid < 0) // 判错
        {
            if (errno == EEXIST)
            {
                shmid = shmget(key, 128, 0666);
            }
            else
            {
                perror("shmget err\n");
                return NULL;
            }
        }
        printf("shmget creat ok! shmid:%d\n", shmid);
    
        // 映射共享内存->将指定的共享内存,映射到进程的地址空间,用于访问
        char *p = shmat(shmid, NULL, 0); 
        if (p == (char *)-1)
        {
            perror("shmat err\n");
            return NULL;
        }
    
        // 使用
        uint16_t dest[4] = {0}; // 4:光线传感器及加速度传感器xyz
        while (1)
        {
            int red = modbus_read_registers(ctx, 0, 4, dest);
            // sprintf将格式化的数据写入指定的字符数组(字符串)
            sprintf(p, "光线传感器:%d 加速度传感器 x:%d y:%d z:%d", dest[0],dest[1],dest[2],dest[3]); 
            printf("%s\n",p);
            sleep(1);
        }
        return NULL;
    }
    void *handler_Control(void *arg)
    {
         // 创建key值
        key_t key;
        key = ftok("./main.c", 'a');
        if (key < 0)
        {
            perror("ftok err");
        }
        // 创建消息队列
        int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
        if (msgid < 0)
        {
            if (errno == EEXIST)
                msgid = msgget(key, 0666);
            else
            {
                perror("msgget err");
            }
        }
    
        int a, b;
        struct msgbuf msg;
        while (1)
        {
            //读取消息
            msgrcv(msgid, &msg, sizeof(struct msgbuf) - sizeof(long), 1, 0);//接收消息队列中第一条消息
    
            // 从终端读取命令
            //printf("输入控制命令(0 1: 开灯  0 0: 关灯  1 1: 开蜂鸣器   1 0: 关蜂鸣器):\n");
    
            a = msg.num1;
            b = msg.num2;
            // 检查 cmd1 和 cmd2 的值,并打印相应的状态
    
            if (a == 0 && b == 1)
            {
                modbus_write_bit(ctx, 0, 1);
                printf("modbus_set=0 1 LED on\n");
                putchar(10);
            }
            else if (a == 0 && b == 0)
            {
                modbus_write_bit(ctx, 0, 0);
                printf("modbus_set=0 0 LED off\n");
                putchar(10);
            }
            else if (a == 1 && b == 1)
            {
                modbus_write_bit(ctx, 1, 1);
                printf("modbus_set=1 1 Buzzer on\n");
                putchar(10);
            }
            else if (a == 1 && b == 0)
            {
                modbus_write_bit(ctx, 1, 0);
                printf("modbus_set=1 0 Buzzer off\n");
                putchar(10);
            }
            else
            {
                printf("输入错误,请重新输入\n");
                putchar(10);
            }
        }
        return NULL;
    }
    
    int main(int argc, char const *argv[])
    {
        pthread_t tid1;
        pthread_t tid2;
        if (argc != 3)
        {
            printf("please input %s port、ip!", argv[0]);
            return -1;
        }
        // 1.创建Modbus实例
        ctx = modbus_new_tcp(argv[2], atoi(argv[1]));
        // 2.设置从机id
        modbus_set_slave(ctx, 1);
        // 3.和从机(slave)建立连接
        if (modbus_connect(ctx) < 0)
        {
            perror("connect err");
            return -1;
        }
        // 创建循环采集数据线程
        if (pthread_create(&tid1, NULL, handler_data, NULL) != 0)
        {
            perror("pthread err");
            return -1;
        }
        // 输入指令控制硬件设备
        if (pthread_create(&tid2, NULL, handler_Control, NULL) != 0)
        {
            perror("pthread err");
            return -1;
        }
        pthread_join(tid1, NULL);
        pthread_join(tid2, NULL);
        // 关闭套接字
        modbus_close(ctx);
        // 释放Modbus实例
        modbus_free(ctx);
        return 0;
    }
    

    customer_handler.c(含handle_get函数及handle_set函数)

    #include <sys/types.h>
    #include <sys/socket.h>
    #include "custom_handle.h"
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <errno.h>
    #include <sys/msg.h>
    
    #define KB 1024
    #define HTML_SIZE (64 * KB)
    
    // 普通的文本回复需要增加html头部
    #define HTML_HEAD "Content-Type: text/html\r\n" \
                      "Connection: close\r\n"
    
    static int handle_get(int sock, const char *input)
    {
        key_t key = ftok(".", 'a');
        if (key < 0)
        {
            perror("key err\n");
            return -1;
        }
        printf("ftok ok! key:%#x\n", key);
    
        // 创建或打开共享内存
        int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
        // 判断是否已存在->已存在无需创建,直接给权限即可
        if (shmid < 0) // 判错
        {
            if (errno == EEXIST)
            {
                shmid = shmget(key, 64, 0666);
            }
            else
            {
                perror("shmget err\n");
                return -1;
            }
        }
        printf("shmget creat ok! shmid:%d\n", shmid);
    
        // 映射共享内存->将指定的共享内存,映射到进程的地址空间,用于访问
        char *p = shmat(shmid, NULL, SHM_RDONLY); // 对该共享内存只读
        if (p == (char *)-1)
        {
            perror("shmat err\n");
            return -1;
        }
        // 使用
        char val_buf[HTML_SIZE] = {0};
        strcpy(val_buf, p);
        send(sock, val_buf, sizeof(val_buf), 0);
    }
    static int handle_set(int sock, const char *input)
    {
        struct msgbuf
        {
            long type; // 第一个成员必须是long类型变量,表示消息的类型
            int num1;
            int num2;
        };
        // 创建key值
        key_t key;
        key = ftok("./main.c", 'a');
        if (key < 0)
        {
            perror("ftok err");
        }
        // 创建消息队列
        int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
        if (msgid < 0)
        {
            if (errno == EEXIST)
                msgid = msgget(key, 0666);
            else
            {
                perror("msgget err");
            }
        }
        int num1, num2;
        sscanf(input, "modbus_set=%d %d", &num1, &num2); // 放到了input数组中,本身正文内容包含 字符双引号
    
        printf("num1 = %d\n", num1);
        printf("num2 = %d\n", num2);
        
        // 添加消息队列
        struct msgbuf msg = {1, num1, num2};
        msgsnd(msgid, &msg, sizeof(struct msgbuf) - sizeof(long), 0);
        
        //向客户端发送确认消息
        char str[64] = "send ok";
        send(sock, str, sizeof(str), 0);
        return 0;
    }
    /**
     * @brief 处理自定义请求,在这里添加进程通信
     * @param input
     * @return
     */
    int parse_and_process(int sock, const char *query_string, const char *input)
    {
       
        // input是post的数据
        /*strstr找到第二个字符串在第一个数组里的起始地址,返回一个地址  
        在这里只需要判断在不在数组里即可*/
    
        // 获取传感器数据
        else if (strstr(input, "modbus_get"))
        {
            return handle_get(sock, input);
        }
        //控制硬件设备
        else if (strstr(input, "modbus_set="))
        {
            return handle_set(sock, input);
        }
        else // 剩下的都是json请求,这个和协议有关了
        {
            // 构建要回复的JSON数据
            const char *json_response = "{\"message\": \"Hello, client!\"}";
    
            // 发送HTTP响应给客户端
            send(sock, json_response, strlen(json_response), 0);
        }
    
        return 0;
    }
    

    HTML界面

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <style>
            /* 添加全屏渐变背景 */
            body {
                margin: 0;
                padding: 20px;
                background: linear-gradient(135deg, #83a4d4 0%, #b6fbff 100%);
                min-height: 100vh;
            }
    
            /* 内容区域半透明效果 */
            div {
                background-color: rgba(255, 255, 255, 0.9) !important;
                padding: 20px;
                border-radius: 10px;
                box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
                margin-bottom: 20px;
            }
        </style>
        
        <script>
            function get_info() {
                // v是个数组,获取标签名为usrname的标签,符合的都放进去了,赋值给v
                var v = document.getElementsByName("usrname");//通过什么方式来获取标签
                var xhr = new XMLHttpRequest();//创建一个对象(通过网络通信需要有一个套接字才能实现)
                var url = "";//网页已经打开了,get请求已经获取了 
                xhr.open("post", url, true);//设置属性填充到对象中,例如:请求,true:允许异步通知
                xhr.send("modbus_get");//自定义:双方判断内容  发送请求正文
                xhr.onreadystatechange = function ()//每次触发调用此函数,进行判断
                {
                    //状态变化:readyState0:请求还未初始化,1:和服务器建立连接2:请求被接收3:接收完正在处理4:处理完成
                    if (xhr.readyState == 4 && xhr.status == 200)//请求成功完成并返回
                    {
                        v[0].value = xhr.responseText;//请求返回的 正文数据
                    }
                };
            }
            function set_info(obj) {
                var xhr = new XMLHttpRequest();
                var url = "";
                if (obj == 'set=0 1') {
                    xhr.open("post", url, true);
                    xhr.send("modbus_set=0 1");
                    console.log("LED on");
                } else if (obj == 'set=0 0') {
                    xhr.open("post", url, true);
                    xhr.send("modbus_set=0 0");
                    console.log("LED off");
                }
                else if (obj == 'set=1 1') {
                    xhr.open("post", url, true);
                    xhr.send("modbus_set=1 1");
                    console.log("Buzzer on");
                }
                else if (obj == 'set=1 0') {
                    xhr.open("post", url, true);
                    xhr.send("modbus_set=1 0");
                    console.log("Buzzer off");
                }
            }
        </script>
    </head>
    
    <body>
        <!-- 使用内联样式:标签内加style属性,设置text-align: center让文字居中 -->
        <!-- div块标签 -->
        </style>
        <div style="color:rgb(3, 80, 197);background-color: bisque;">
            <h1 style="text-align: center">基于WebServer的工业数据采集项目</h1>
        </div>
        <div style="background-color: rgb(248, 235, 216);">
            <h2>项目实现功能:传感器数据采集与硬件模拟控制</h2>
            <h3>一、传感器数据采集</h3>
            <br>
            光照强度及加速度x,y,z:<input type="text" name="usrname" size="30">
            <input type="button" name="flash" value="获取数据" onclick="get_info()">
            <br><br>
            <!-- 加速度x,y,z:<input type="text" name="usrname" value="">
            <br><br> -->
            <h3>二、硬件模拟控制</h3>
            LED灯:
            on:<input type="radio" name="led" id="set=0 1" onclick="set_info(id)">
            off:<input type="radio" name="led" id="set=0 0" checked="checked" onclick="set_info(id)">
            <br><br>
            Buzzer:
            on:<input type="radio" name="buzzer" id="set=1 1" onclick="set_info(id)">
            off: <input type="radio" name="buzzer" id="set=1 0" checked="checked" onclick="set_info(id)">
            <br><br>
        </div>
    
    </body>
    
    </html>

    补充信息:

    slave使用

    1.先设置。两个窗口的配置,从机地址选择1,功能码选择03和01

     2.后连接,slave端网络配置应用主机的IP地址,端口号选择502


    HTTP协议

    HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于Web Browser(浏览器)到Web Server(服务器)进行数据交互的传输协议。

    HTTP是应用层协议

    HTTP是一个基于TCP通信协议传输来传递数据(HTML 文件, 图片文件, 查询结果等)

    HTTP协议工作于B/S架构上,浏览器作为HTTP客户端通过URL主动向HTTP服务端即WEB服务器发送所有请求,Web服务器根据接收到的请求后,向客户端发送响应信息。

    HTTP默认端口号为80,但也可以改为8080或者其他端口

    Modbus

    Modbus通信协议具有多个变种,其中有支持串口,以太网多个版本,其中最著名的是Modbus RTU、Modbus ASCII和Modbus TCP三种

    分类

    1)  Modbus RTU:

    运行在串口上的协议,采用二进制表现形式以及紧凑型数据结构,通信效率高,应用广泛

    2)  Modbus ASCII:

    运行在串口上的协议,采用ASCII码传输并且利用特殊字符作为其字节的开始与结束标识,其传输效率要远远低于Modbus RTU协议,一般只有在通信数据量较小的情况下才考虑使用Modbus ASCII通信协议

    3)  Modbus TCP:

    运行在以太网上的协议

    Modbus TCP协议

     采用主从问答式通信
     Modbus TCP是应用层协议,基于传输层的TCP协议实现
     Modbus TCP端口号是502
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值