基于WebServer工业数据采集项目
注:实训项目,仅为记录分享自己编写项目的历程和心得,无其他用途。
一.项目实现图解
项目主要要解决的就是网页服务器WebServer和服务程序之间产生连接,为了达成连接,用到了“中间商”CGI,这是一个通用网关接口,通过这个CGI使网页服务器WebServer和服务程序之间可以相互解析,然后实现相应的要求。
二.项目实现详解
可以分成三个方面详解:一方面是服务程序,第二方面CGI通用接口中对网页输入的内容和服务程序发送的内容的处理,第三方面是网页的制作,在网页中通过一定的操作可以获得相应的数据或者改变线圈的状态。
1)服务程序
在服务程序中,用到的是多线程,线程1用来循环读取Modbus设备的数据(相当于是03操作),把数据循环读入共享内存;线程2用来改变线圈的状态(相当于05操作),在消息队列中等待接受队列中的数据。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include "modbus.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <sys/msg.h>
// 宏定义从机IP
#define IP
// 线程传参结构体
struct keyword
{
key_t key;
modbus_t *ctx;
};
// 消息队列结构体
struct msgbuf
{
long mtype;
char msg_buf[32];
};
void *fun1(void *arg)
{
struct keyword *k = (struct keyword *)arg;
key_t key = k->key;
modbus_t *ctx = k->ctx;
// 2)创建或打开共享内存 shmget
int shmid;
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
if (17 == errno)
{
shmid = shmget(key, 128, 0666);
}
else
{
perror("shmget err");
return NULL;
}
}
// 3)映射共享内存到用户空间 shmat
char *p = shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
perror("shmat err");
return NULL;
}
// 4.寄存器操作
uint16_t dest1[32] = {0};
int num1, num2;
char buf[128] = {0};
while (1)
{
// 功能码对应的函数 __03__
num1 = modbus_read_registers(ctx, 0, 4, dest1);
if (num1 < 0)
{
perror("modbus_read_registers err");
}
for (int i = 0; i < num1 / 3; i++)
{
// 把每一次的数据存到共享内存中
sprintf(buf, "X:%d Y:%d Z:%d 光线传感器:%d", dest1[i], dest1[i + 1], dest1[i + 2], dest1[i + 3]);
strcpy(p, buf);
printf("%s\n", p); // 打印一下共享内存内容看一看
sleep(1);
}
}
}
void *fun2(void *arg)
{
struct keyword *k = (struct keyword *)arg;
key_t key = k->key;
modbus_t *ctx = k->ctx;
// 2))创建或打开消息队列 msgget
int msgid;
msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
if (msgid <= 0)
{
if (17 == errno)
{
msgid = msgget(key, 0666);
}
else
{
perror("msgget err");
return NULL;
}
}
while (1)
{
// 4))读取消息:可以按照类型将消息从消息队列中度走 msgrcv
struct msgbuf val;
msgrcv(msgid, &val, sizeof(val) - sizeof(long), 1, 0);
printf("val.buf = %s\n", val.msg_buf);
int a = (int)val.msg_buf[4] - 48;
int b = (int)val.msg_buf[5] - 48;
printf("a = %d b = %d\n", a, b);
// 功能码对应的函数 __05__
modbus_write_bit(ctx, a, b);
if (0 == a)
{
if (0 == b)
printf("LED关\n");
else
printf("LED开\n");
}
else
{
if (0 == b)
printf("蜂鸣器关\n");
else
printf("蜂鸣器开\n");
}
}
}
int main(int argc, char const *argv[])
{
// 创建共享内存
// 1)创建key值 ftok
key_t key = 12138;
// key = ftok(".", 'b');
// if (key < 0)
// {
// perror("ftok err");
// return -1;
// }
// 1.创建实例
// modbus_new_tcp
modbus_t *ctx = modbus_new_tcp(IP, 502);
if (NULL == ctx)
{
perror("mod_new_tcp err ");
}
// 定义结构体变量并给结构体初始化
struct keyword kkk;
kkk.key = key;
kkk.ctx = ctx;
// 2.设置从机ID
// modbus_set_slave
int mss1 = modbus_set_slave(ctx, 1);
if (mss1 < 0)
{
perror("modbus_set_slave err");
}
// 3.连接从机
// modbus_connect
int mc1 = modbus_connect(ctx);
if (mc1 < 0)
{
perror("modbus_connect err");
}
// 创建线程1
pthread_t tid1;
if ((pthread_create(&tid1, NULL, fun1, &kkk)) != 0)
{
perror("pthread err\n");
return -1;
}
printf("pthread success\n");
// 创建线程2
pthread_t tid2;
if ((pthread_create(&tid2, NULL, fun2, &kkk)) != 0)
{
perror("pthread err\n");
return -1;
}
printf("pthread success\n");
// 等待回收线程2
pthread_join(tid2, NULL);
// 等待回收线程1
pthread_join(tid1, NULL);
// 5.关闭套接字
// modbus_close
modbus_close(ctx);
// 6.释放实例
// modbus_free
modbus_free(ctx);
return 0;
}
2)CGI通用接口
在CGI中,首先要判断要解析的功能,这里我用到了get(获取寄存器的数据)和set(改变线圈的状态)。
当在网页发送get时,CGI接收到以后,打开共享内存,获取共享内存中的数据,然后给服务器回复,回复内容按照http协议格式,最后把读取的内容追加进去,然后发送给网页。
当在网页发送set时,可以设定一定的格式,比如set=00,可以把内容直接放入消息队列,在服务程序端可以通过下标来获得需要的参数,用于改变线圈的状态。
int parse_and_process(char *input)
{
char val_buf[2048] = {0};
// 创建共享内存和消息队列
// 1)创建key值 ftok
key_t key = 12138;
if (0 == strncmp(input, "get", 3))
{
// 2)创建或打开共享内存 shmget
int shmid;
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
if (17 == errno)
{
shmid = shmget(key, 128, 0666);
}
else
{
perror("shmget err");
return -1;
}
}
// 3)映射共享内存到用户空间 shmat
char *p = shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
perror("shmat err");
return -1;
}
strcpy(val_buf, p);
//数据处理完成后,需要给服务器回复,回复内容按照http协议格式
char reply_buf[HTML_SIZE] = {0};
sprintf(reply_buf, "%sContent-Length: %ld\r\n\r\n", HTML_HEAD, strlen(val_buf));
strcat(reply_buf, val_buf);
log_console("post json_str = %s", reply_buf);
//向标准输出写内容(标准输出服务器已做重定向)
fputs(reply_buf, stdout);
// 4)撤销映射 shmdt
shmdt(p);
}
else if (0 == strncmp(input, "set", 3))
{
// 2))创建或打开消息队列 msgget
int msgid;
msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
if (msgid <= 0)
{
if (17 == errno)
{
msgid = msgget(key, 0666);
}
else
{
perror("msgget err");
return -1;
}
}
// 3))添加消息:按照类型将消息添加到已打开的消息队列末尾 msgsnd
struct msgbuf val;
val.mtype = 1;
strcpy(val.msg_buf, input);
log_console("msg_buf = %s\n", val.msg_buf);
msgsnd(msgid, &val, sizeof(val) - sizeof(long), 0);
strcpy(val_buf, input);
// 判断并给网页回复消息(可不写)
if ('0' == val_buf[4])
{
if ('1' == val_buf[5])
{
strcpy(val_buf, "LED开");
}
else
{
strcpy(val_buf, "LED关");
}
}
else
{
if ('1' == val_buf[5])
{
strcpy(val_buf, "蜂鸣器开");
}
else
{
strcpy(val_buf, "蜂鸣器关");
}
}
//数据处理完成后,需要给服务器回复,回复内容按照http协议格式
char reply_buf[HTML_SIZE] = {0};
sprintf(reply_buf, "%sContent-Length: %ld\r\n\r\n", HTML_HEAD, strlen(val_buf));
strcat(reply_buf, val_buf);
log_console("post json_str = %s", reply_buf);
//向标准输出写内容(标准输出服务器已做重定向)
fputs(reply_buf, stdout);
}
3)网页的制作
制作要求:
1.一个文本框,可以用来接收读取到的寄存器的数据,在网页中输出出来。
2.一个按钮,通过点击按钮,实时刷新文本框中的数据。
3.四个可单选的开关,控制线圈的状态。
4.一个文本框,用来接收通过开关改变线圈后,显示线圈状态。
通过下方代码<body></body>内的内容可以完成页面的制作要求:
通过下方代码<body></body>内input中的onclick链接子函数,然后通过子函数执行相应的操作。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="js/xhr.js"></script>
<script>
function get_info() {
// 获取标签为result的标签,赋值给v
var v = document.getElementsByName("result");
// v[0].value = "hello"; // 测试一下
// 向服务器请求数据
// 参数url:地址
// data:请求数据正文
// callback:回调函数,当服务器有数据回复时可以在回调函数中处理
XHR.post('/cgi-bin/web.cgi', "get", function (x, info) {
// console.log(info); // info表示服务器回复的数据
v[0].value = info;
})
}
function func1(obj) {
// 获取标签为bite1的标签,赋值给v
var v = document.getElementsByName("result");
// 向服务器请求数据
// 参数url:地址
// data:请求数据正文
// callback:回调函数,当服务器有数据回复时可以在回调函数中处理
if ('open' == obj) {
XHR.post('/cgi-bin/web.cgi', "set=01", function (x, info) {
console.log(info); // info表示服务器回复的数据
v[1].value = info;
})
}
else {
XHR.post('/cgi-bin/web.cgi', "set=00", function (x, info) {
console.log(info); // info表示服务器回复的数据
v[1].value = info;
})
}
}
function func2(obj) {
// 获取标签为bite1的标签,赋值给v
var v = document.getElementsByName("result");
// 向服务器请求数据
// 参数url:地址
// data:请求数据正文
// callback:回调函数,当服务器有数据回复时可以在回调函数中处理
if ('open' == obj) {
XHR.post('/cgi-bin/web.cgi', "set=11", function (x, info) {
console.log(info); // info表示服务器回复的数据
v[1].value = info;
})
}
else {
XHR.post('/cgi-bin/web.cgi', "set=10", function (x, info) {
console.log(info); // info表示服务器回复的数据
v[1].value = info;
})
}
}
</script>
</head>
<body>
<h1>基于WebServer的工业数据采集项目</h1>
<br>
数据:
<input type="text" name="result" style="width: 300px" value="">
<input type="button" name="command" value="get" onclick="get_info()">
<br>
<br>
LED灯:
开:<input type="radio" name="bit1" id="open" onclick="func1(id)">
关:<input type="radio" name="bit1" id="close" onclick="func1(id)">
<br>
蜂鸣器:
开:<input type="radio" name="bit2" id="open" onclick="func2(id)">
关:<input type="radio" name="bit2" id="close" onclick="func2(id)">
<br>
结果:
<input type="text" name="result" value="">
</body>
</html>
三.结果展示
1.点击get按钮获取实时数据
2.通过点击LED开的开关打开LED灯
3.通过点击蜂鸣器开的开关打开蜂鸣器
4.通过点击LED关的开关关闭LED灯
需要注意的问题:
1.key值的确定,一定要认真的检查key值,确保在共享内存或者消息队列的key是相对应的,不然无法进行进程间通信。
2.网页端给CGI发送的数据是一个字符串,比如字符串set=00,所以在改变线圈的状态时,需要考虑在传参时对字符串中的数据处理。
3.网页制作的过程中,要注意LED灯和蜂鸣器的开和关两个开关的name相对应,不然就会产生可以同时选择的错误。
2401_82437998: 解释好清晰!
宁远133: 解释清晰,感谢大佬
HelloKQing: 有用的贴纸,测试下来FM24V02A也能直接使用
CSDN-Ada助手: 恭喜你写了第三篇博客!标题“基于WebServer工业数据采集项目”听起来很有实际应用价值。你的持续创作精神值得赞赏。在下一篇博客中,或许你可以分享一些关于如何优化数据采集过程或者介绍一些常见的工业数据采集项目案例。谦虚地说,我相信你的经验和见解将会给读者带来更多的启发和帮助。期待你的下一篇博客! CSDN 正在通过评论红包奖励优秀博客,请看红包流:https://bbs.csdn.net/?type=4&header=0&utm_source=csdn_ai_ada_blog_reply3
CSDN-Ada助手: 恭喜你写了第四篇博客!标题听起来很有趣,我对基于TCP协议多进程通信和数据库的电子词典很感兴趣。你的创作内容一直都很有深度和技术性,让我从中受益匪浅。希望你能继续保持创作的热情和努力,为我们带来更多有关技术和应用的精彩博客。接下来,我建议你可以探索一些实际案例,分享一些电子词典在现实生活中的应用场景,这样读者能更好地理解它的价值和实用性。期待你的下一篇作品! CSDN 会根据你创作的前四篇博客的质量,给予优秀的博主博客红包奖励。请关注 https://bbs.csdn.net/forums/csdnnews?typeId=116148&utm_source=csdn_ai_ada_blog_reply4 看奖励名单。