一、项目名称
基于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:
运行在以太网上的协议