一、整体架构
这是我学习lighttpd和cgi用来练手的项目,就是简单的完成在网页上可以完成对两个保持寄存器的读取,和写两个线圈。主要完成部分如下图:
网页页面如下图:
二、具体内容
1.服务程序<--ModbusRTU-->Modbus slave
总体设计使用了两个进程,
子进程用来每隔1s通过共享内存向cgi传递温湿度数据;
父进程使用信号灯集接收cgi写线圈的命令。
这里的Modbus slave我使用了虚拟串口;
具体程序如下:
其中,
unsigned short GetCRC16(unsigned char *ptr, unsigned char len)函数是用来取得CRC校验码;
void uart_init(int fd)函数是用来对串口进行初始化,设置一些串口属性。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "Crc_Calc.h"
#include "head.h"
#include <string.h>
#include <sys/msg.h>
struct msgbuf
{
long mtype;
char mtext[128];
};
int main(int argc, char const *argv[])
{
char val_buf[2048] = {0};
struct msgbuf msg;
struct msgbuf msg1;
//1.ftok函数获取一个关键key值
key_t key_recv = ftok("/home/hq/work/web/cgi_demo/a.txt", 'A');
if (key_recv < 0)
{
perror("ftok err\n");
return -1;
}
//2.创建一个共享内存或打开(key)-实际的物理内存空间
int shmid = shmget(key_recv, 128, IPC_CREAT | IPC_EXCL | 0666);
{
if (shmid < 0)
{
if (errno == EEXIST)
{
shmid = shmget(key_recv, 128, 0666);
}
else
{
perror("shmget err");
return -1;
}
}
}
//3.建立共享内存和虚拟地址的映射
char *sp = (char *)shmat(shmid, NULL, 0);
if (sp == (char *)-1)
{
perror("shmat err\n");
return -1;
}
key_t key_send = ftok("/home/hq/work/web/cgi_demo/b.txt", 'B');
if (key_send < 0)
{
perror("ftok err\n");
return -1;
}
int msgid = msgget(key_send, IPC_CREAT | IPC_EXCL | 0666);
if (msgid < 0)
{
if (errno == EEXIST)
{
msgid = msgget(key_send, 0666);
}
else
{
perror("msgget err\n");
return -1;
}
}
int fd = open("/dev/ttyS1", O_RDWR);
if (fd < 0)
{
perror("open err");
return -1;
}
uart_init(fd);
u_int8_t send_buf[32] = {
0x01,
0x03,
0x00,
0x00,
0x00,
0x02};
uint16_t crc = GetCRC16(send_buf, 0x06);
uint8_t crc_high = crc >> 8;
uint8_t crc_low = crc & 0xff;
send_buf[6] = crc_high;
send_buf[7] = crc_low;
u_int8_t recv_buf[32] = {};
uint8_t send_sig[32] = {0x01,
0x0F,
0x00,
0x00,
0x00,
0x02,
0x01};
u_int8_t recv_sig[32] = {};
uint16_t dest[2] = {};
pid_t pid = fork();
if (pid < 0)
{
perror("fork err\n");
return -1;
}
else if (pid == 0)
{
while (1)
{
if (write(fd, send_buf, 8) < 0)
{
perror("write err");
return -1;
}
if (read(fd, recv_buf, 9) < 0)
{
perror("read err");
return -1;
}
for (int i = 0, j = 0; i < recv_buf[2]; i += 2, j++)
{
dest[j] = recv_buf[3 + i] << 8 | recv_buf[4 + i];
}
sprintf(val_buf, "%d %d\n", dest[0], dest[1]);
strcpy(sp, val_buf);
printf("%s", val_buf);
sleep(1);
}
}
else
{
int LED = 0, BEEP = 0;
while (1)
{
if (msgrcv(msgid, &msg, sizeof(msg), 0, 0) < 0)
{
perror("msgrcv err\n");
}
printf("type:%c text:%s\n", msg.mtype, msg.mtext);
if (!strcmp(msg.mtext, "LED 0"))
{
printf("LED 0");
if (BEEP == 0)
send_sig[7] = 0x00;
else
send_sig[7] = 0x02;
LED = 0;
}
else if (!strcmp(msg.mtext, "LED 1"))
{
printf("LED 1");
if (BEEP == 0)
send_sig[7] = 0x01;
else
send_sig[7] = 0x03;
LED = 1;
}
else if (!strcmp(msg.mtext, "BEEP 0"))
{
printf("BEEP0");
if (LED == 0)
send_sig[7] = 0x00;
else
send_sig[7] = 0x01;
BEEP = 0;
}
else if (!strcmp(msg.mtext, "BEEP 1"))
{
printf("BEEP1");
if (LED == 0)
send_sig[7] = 0x02;
else
send_sig[7] = 0x03;
BEEP = 1;
}
else
{
continue;
}
uint16_t crs = GetCRC16(send_sig, 0x08);
uint8_t crs_high = crs >> 8;
uint8_t crs_low = crs & 0xff;
send_sig[8] = crs_high;
send_sig[9] = crs_low;
if (write(fd, send_sig, 10) < 0)
{
perror("write err");
return -1;
}
if (read(fd, recv_sig, 8) < 0)
{
perror("read err");
return -1;
}
if (LED == 0)
printf("LED关闭 ");
else
printf("LED打开");
if (BEEP == 0)
printf("BEEP关闭 ");
else
printf("BEEP打开");
}
}
close(fd);
//4.取消映射
shmdt(sp);
//5.销毁共享内存
shmctl(shmid, IPC_RMID, NULL);
//删除消息队列
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
2.服务器<--http-->网页(html)
这是网页html:
<!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>工业数据采集系统</title>
<script src="js/xhr.js"></script>
<script>
function get_tem(obj) {
XHR.post('/cgi-bin/web.cgi', obj, function (x, info) {
str = info.split(' ');
var v=document.getElementById("tem");
v.value=str[0];
});
}
function get_hum(obj) {
XHR.post('/cgi-bin/web.cgi', obj, function (x, info) {
str = info.split(' ');
var v=document.getElementById("hum");
v.value=str[1];
});
}
function set(obj) {
XHR.post('/cgi-bin/web.cgi', obj, function (x, info) {
});
}
</script>
</head>
<body>
<h1 style="color: chocolate">工业数据采集系统</h1>
<div style="color:darkturquoise">
温度:
<input type="text" name="温度" id="tem">
<input type="button" name="温度" value="获取温度" id="get" onclick="get_tem(id)">
<br>
湿度:
<input type="text" name="湿度" id="hum">
<input type="button" name="湿度" value="获取湿度" id="get" onclick="get_hum(id)">
<br>
LED灯
<br>
开
<input type="radio" name="LED" value="开" id="LED 1" onclick="set(id)">
关
<input type="radio" name="LED" value="关" id="LED 0" onclick="set(id)">
<br>
蜂鸣器
<br>
开
<input type="radio" name="BEEP" value="开" id="BEEP 1" onclick="set(id)">
关
<input type="radio" name="BEEP" value="关" id="BEEP 0" onclick="set(id)">
</div>
</body>
</html>
3.cgi程序
使用共享内存接收数据,信号灯集下发从webServer获得到的写线圈的命令。
(1)main.c
#include "req_handle.h"
#include "log_console.h"
int main(int argc, char *argv[])
{
//先初始化log,标准输出已被重定向到网络
int ret = log_console_init();
if (ret < 0)
{
perror("open console err");
system("echo open log err > err.log");
exit(-1);
}
//argc和argv web server会自动传给cgi程序
handle_request(argc, argv);
//如果有led命令,通过共享内存给子进程。。。去custom_handle.c中去写。
return 0;
}
(2) custom_handle.c
#include "req_handle.h"
#include "log_console.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#define KB 1024
#define HTML_SIZE (64 * KB)
//普通的文本回复需要增加html头部
#define HTML_HEAD "Content-Type: text/html\r\n" \
"Connection: close\r\n"
struct msgbuf
{
long mtype;
char mtext[128];
};
int parse_and_process(char *input)
{
//1.ftok函数获取一个关键key值
key_t key_recv = ftok("/home/hq/work/web/cgi_demo/a.txt", 'A');
log_console("%d\n", key_recv);
if (key_recv < 0)
{
perror("ftok err\n");
return -1;
}
//2.创建一个共享内存或打开(key)-实际的物理内存空间
int shmid = shmget(key_recv, 128, IPC_CREAT | IPC_EXCL | 0666);
{
if (shmid < 0)
{
if (errno == EEXIST)
{
shmid = shmget(key_recv, 128, 0666);
}
else
{
perror("shmget err");
return -1;
}
}
}
//3.建立共享内存和虚拟地址的映射
char *sp = (char *)shmat(shmid, NULL, 0);
if (sp == (char *)-1)
{
perror("shmat err\n");
return -1;
}
key_t key_send = ftok("/home/hq/work/web/cgi_demo/b.txt", 'B');
log_console("%d\n", key_send);
if (key_send < 0)
{
perror("ftok err\n");
return -1;
}
int msgid = msgget(key_send, IPC_CREAT | IPC_EXCL | 0666);
if (msgid < 0)
{
if (errno == EEXIST)
{
msgid = msgget(key_send, 0666);
}
else
{
perror("msgget err\n");
return -1;
}
}
char val_buf[2048] = {0};
char val_put[2048] = {0};
char reply_buf[HTML_SIZE] = {0};
strcpy(val_buf, sp);
strcpy(val_put, input);
if (!strncmp(val_put, "LED", 3))
{
struct msgbuf msg;
msg.mtype='L';
strcpy(msg.mtext,val_put);
msgsnd(msgid, &msg, sizeof(msg), 0);
log_console("has been sent:%s\n", msg.mtext);
}
else if (!strncmp(val_put, "BEEP", 4))
{
struct msgbuf msg1;
msg1.mtype='B';
strcpy(msg1.mtext,val_put);
msgsnd(msgid, &msg1, sizeof(msg1), 0);
log_console("has been sent:%s\n", msg1.mtext);
}
else if(!strncmp(val_put,"get",3))
{
struct msgbuf msg2;
msg2.mtype='G';
strcpy(msg2.mtext,val_put);
msgsnd(msgid, &msg2, sizeof(msg2), 0);
log_console("has been sent:%s\n", msg2.mtext);
}
else
{
log_console("send err\n");
return -1;
}
if(!strncmp(val_put,"LED 0",5))
{
strcat(val_buf, "小灯已关闭\n");
}else if(!strncmp(val_put,"LED 1",5))
{
strcat(val_buf, "小灯已打开\n");
}else if(!strncmp(val_put,"BEEP 0",6))
{
strcat(val_buf, "蜂鸣器已关闭\n");
}else if(!strncmp(val_put,"BEEP 1",6))
{
strcat(val_buf, "蜂鸣器已打开\n");
}
//数据处理完成后,需要给服务器回复,回复内容按照http协议格式
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);
return 0;
}
(3) log_console.c
/***********************************************************************************
Copy right: Coffee Tech.
Author: jiaoyue
Date: 2022-03-23
Description: console模块
***********************************************************************************/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdarg.h>
#include "log_console.h"
static int console_fd = -1;
/**
* @brief 初始化log_console
* @return 0 -1
*/
int log_console_init()
{
console_fd = open(LOG_CONSOLE, O_WRONLY);
if (console_fd < 0)
{
printf("open %s error\n", LOG_CONSOLE);
return -1;
}
return 0;
}
static ssize_t log_console_write(const void *buf, size_t count)
{
int ret = write(console_fd, buf, count);
if(ret < 0)
{
perror("write err");
}
}
#define MAX_LOG_BUF_LEN (20*1024) //打印内容不能超过这个
int log_console(const char *format, ...)
{
char str_tmp[MAX_LOG_BUF_LEN];
va_list arg;
int len;
va_start(arg, format);
len = vsnprintf(str_tmp, MAX_LOG_BUF_LEN, format, arg);
va_end(arg);
str_tmp[len] = '\0';
return log_console_write(str_tmp, strlen(str_tmp));
}
(4)req_handle.c
#include "req_handle.h"
#include "log_console.h"
#include "custom_handle.h"
/**
* @brief 处理请求
* @param argc
* @param argv
* @return
*/
int handle_request(int argc, char *argv[])
{
int ret;
if (argc <= 0)
{
log_console("argc error\n");
return -1;
}
//获取访问此页面所需的URI。例如,“/index.html”。
char *uri = getenv("REQUEST_URI");
//获取前端请求方式
char *request_method = getenv("REQUEST_METHOD");
log_console("uri = %s\n", uri);
log_console("req = %s\n", request_method);
if(NULL == request_method)
{
log_console("error to get request_method\n");
exit(-1);
}
//get方法我们不处理,交由服务器自己处理
if (strcasecmp(request_method, "POST") != 0)
{
log_console("only handle post\n");
return 0;
}
//获取用户数据长度
char *len_tmp = getenv("CONTENT_LENGTH"); //get CONTENT_LENGTH from env
int content_length;
if (len_tmp != NULL)
{
content_length = atoi(len_tmp);
if (content_length <= 0)
{
return -1;
}
}
else
{
log_console("request_content length error");
return -1;
}
char *content_type = getenv("CONTENT_TYPE");
//打印下CONTENT_TYPE看看,我们只处理普通请求
log_console("content_type=%s\n", content_type);
//普通请求处理
char *request_content = malloc(content_length + 1);
if (!request_content)
{
return -1;
}
int len = 0;
//从标准输入中读取数据到content中
//使用循环的目的防止数据一次没有读完
while (len < content_length)
{
ret = fread(request_content + len, 1, content_length - len, stdin);
if (ret < 0)
{
free(request_content);
return -1;
}
else if (ret > 0)
{
len += ret;
}
else
{
break;
}
}
if (len != content_length)
{
log_console("fread len != content_length");
free(request_content);
return -1;
}
//此时所有的请求内容都存到request_conten中了
request_content[len] = '\0';
log_console("notice request_content = %s\n", request_content);
ret = parse_and_process(request_content);
if(ret < 0)
{
log_console("error to parse\n");
}
free(request_content);
return ret;
}
三、体会
在写这个项目时有点忘记之前的内容了,还犯了比如在共享内存传递数据时使用指针直接等于字符串首地址的错误(sp = val_buf),这样就只传递了一个字符,后来改成strcpy(sp,val_buf)就可以了。还有一点,对于出错会设置errno的最好都要使用perror进行输出错误,这次我偷懒漏了几个perror,后来好长时间才发现错误,这是因为我之前设置的共享内存在两个进程中大小不一样导致的,运行一次后发现不对虽然改回来了,但我一直使用Ctrl+C进行停止,之前创建的共享内存没有销毁。后来通过加入perror发现是共享内存“传入的数据太长”,这才明白过来,浪费了很多时间。
希望我的经验能给读者一点启发。