目录
涉及内容
Modbus协议
Modbus起源
1. 起源
2. 分类
3. 优势
4. 应用场景
5. Modbus TCP协议特点
Modbus TCP通信协议
报文头
寄存器
功能码
Modbus库
三方库的使用
库的使用
函数接口
创建modbus实例
设置从机ID
和从机(slave)建立连接
释放Modbus实例
关闭套接字
读取线圈状态
读取离散输入寄存器状态
读取保持寄存器的值
读取输入寄存器的值
写线圈寄存器 (单个和多个)
写保持寄存器(单个和多个)
编程流程
工具使用
modbusSlave/Poll
网络调试助手
Wirshark使用
WebServer服务器
Web Server的分类
Lighttpd服务器
服务器安装配置(在虚拟机内)
配置文件修改
修改配置文件
运行测试
Postman使用
作用
注意事项
测试使用
html
开发环境
安装库 open in browser
基于WebServer的工业数据采集项目
项目框架
CGI
CGGI简介
CGI特点
常见的环境变量
CGI工作原理
源码分析
源码使用
http&html
http协议
http简介
http特点
http协议格式
html语法
html简介
html标签
标签格式
标签分类
常用标签
代码如下
ModbusTCP端服务程序
CGI代码
makefile
main.c
req_handle.h
req_handle.c
log_console.h
log_console.c
custom_handle.h
custom_handle.c
html网页部分代码
Modbus协议、http协议、html协议
Webserver:网页服务器(postman)
工具:wireshark,Modbus Slave,Modbus Poll,Postman
Modbus由Modicon公司于1979年开发,是一种工业现场总线协议标准。Modbus通信协议具有多个变种,其中有支持串口,以太网多个版本,其中最著名的是Modbus RTU、Modbus ASCII和Modbus TCP三种。其中Modbus TCP是在施耐德收购Modicon后1997年发布的。
免费、简单、容易使用
Modbus协议是现在国内工业领域应用最多的协议,不只PLC设备,各种终端设备都会用到Modbus协议,比如水控机、水表、电表、工业秤、各种采集数据等。
Modbus TCP协议包含三部分:报文头、功能码、数据
Modbus TCP协议最大数据帧长度为260字节。
Modbus TCP协议的报文头共有7字节
Modbus TCP协议包含四种寄存器,分别是线圈、离散量输入、保持寄存器、输入寄存器。
1. 离散量输入和线圈是位寄存器(每个寄存器占一字节),工业上主要用于控制IO设备。
线圈寄存器:可以类比为开关量,每一个bit都对应一个信号的开关状态。所以一个byte就可以同时控制8路的信号。比如控制外部8路IO的高低,线圈寄存器既可以支持读操作也可以支持写操作,写操作也分为写的单个线圈寄存器和写多个线圈寄存器。
线圈寄存器的功能码:0x01(读线圈) 0x05(写单个线圈) 0x0f(写多个线圈)
离散输入寄存器:相当于线圈寄存器的只读模式,每个bit表示一个开关量,离散输入寄存器的开关量只能读取输入的开关信号,不能进行写操作。比如:读取外部按键是按下还是松开。
离散寄存器的功能码:0x02(读线圈寄存器)
2. 输入寄存器和保持寄存器是字寄存器(每个寄存器数据占2个字节),工业上主要用于存储工业设备的值。
保持寄存器:该寄存器的单位是byte,并且每个寄存器都占2字节,能够放具体的数据值,保持寄存器支持可读可写操作,并且写操作分为写单个寄存器和写多个寄存器。例如:设置时间(年月日),既可以写入也可以读出。
保持寄存器的功能码:0x03(读保持寄存器) 0x06(写单个寄存器) 0x10(写多个寄存器)
输入寄存器:相当于保持寄存器的只读模式,一个寄存器也是占两个字节。例如:通过读取输入寄存器获取的AD采集值。
输入寄存器的功能码:0x04(读保持寄存器)
根据四种不同功能的寄存器设置了8中功能码。
练习:
读传感器数据,读一个寄存器数据,写出主从数据收发协议
主机给从机发送:
| 事务处理标识符 | 协议类型 | 字节长度 | 从机ID | 功能码 | 起始地址 | 寄存器个数 |
例: 0x0000 0000 0006 01 03 0000 0000 0001
从寄给主机回复:
| 事务处理标识符 | 协议类型 | 字节长度 | 从机ID | 功能码 | 数据长度 | 数据 |
例: 0x0000 0000 0005 01 03 02 0002 0102
练习:
写出控制IO设备开关的协议数据,操作一个线圈
主机给从机发送:
| --------------MBAP报文头-------------- | 功能码 | 起始地址 | 断通标志 |
例:0x0000 0x0000 0x0006 0x11 0x05 0x000b 0xff00
从机给主机回复:
| --------------MBAP报文头-------------- | 功能码 | 起始地址 | 断通标志 |
例:0x0000 0x0000 0x0006 0x11 0x05 0x000b 0xff00
1. 在linux中解压压缩包
将库压缩包复制到linux下,进行解压
执行命令:tar -xvf libmodbus-3.1.7.tar.gz
2. 进入源码目录,创建文件夹(存放头文件、库文件)
先执行命令:cd libmodbus-3.1.7
再执行命令:mkdir install
3. 执行脚本configure,进行安装配置(指定安装目录)
执行命令:configure
再执行命令:./configure --prefix=$PWD/install
4. 执行make和make install,执行完成后在install里面会生成对应的头文件、库文件文件夹
先执行命令:make//编译
再执行命令:make install//安装
要想编译方便,可以将头文件和库文件放到系统路径下,后期编译时,可以直接gcc xx.c -lmodbus
先执行命令:sudo cp install/include/modbus/*.h /usr/include
再执行命令:sudo cp install/lib/* -r /lib -d
头文件默认搜索路径:/usr/include 、/usr/local/include
库文件默认搜索路径:/lib、/usr/lib
格式:modbus_t* modbus_new_tcp(const char *ip, int port)
功能:以TCP方式创建Modbus实例,并初始化
参数:
ip:ip地址
port:端口号
返回值:
成功:Modbus实例
失败:NULL
格式:int modbus_set_slave(modbus_t *ctx, int slave)
功能:设置从机ID
参数:
ctx:Modbus实例
slave:从机ID
返回值:
成功:0
失败:-1
格式:int modbus_connect(modbus_t *ctx)
功能:和从机(slave)建立连接
参数:
ctx:Modbus实例
返回值:
成功:0
失败:-1
格式:void modbus_free(modbus_t *ctx)
功能:释放Modbus实例
参数:ctx:Modbus实例
格式:void modbus_close(modbus_t *ctx)
功能:关闭套接字
参数:ctx:Modbus实例
格式:int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest)
功能:读取线圈状态,可读取多个连续线圈的状态(对应功能码为0x01)
参数:
ctx:Modbus实例
addr:寄存器起始地址
nb:寄存器个数
dest:得到的状态值
格式:int modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest)
功能:读取离散输入寄存器状态,可读取多个连续输入的状态(对应功能码为0x02)
参数:
ctx:Modbus实例
addr:寄存器起始地址
nb:寄存器个数
dest:得到的状态值
返回值:
成功:返回nb的值
格式:int modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest)
功能:读取保持寄存器的值,可读取多个连续保持寄存器的值(对应功能码为0x03)
参数:
ctx:Modbus实例
addr:寄存器起始地址
nb:寄存器个数
dest:得到的寄存器的值
返回值:
成功:读到寄存器的个数
失败:-1
格式:int modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest)
功能:读输入寄存器的值,可读取多个连续输入寄存器的值(对应功能码为0x04)
参数:
ctx:Modbus实例
addr:寄存器起始地址
nb:寄存器个数
dest:得到的寄存器的值
返回值:
成功:读到寄存器的个数
失败:-1
格式:int modbus_write_bit(modbus_t *ctx, int addr, int status);
功能:写入单个线圈的状态(对应功能码为0x05)
参数:
ctx:Modbus实例
addr:线圈地址
status:线圈状态
返回值:成功:?
失败:-1
格式:int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src);
功能:写入多个连续线圈的状态(对应功能码为15)
参数:
ctx:Modbus实例
addr:线圈地址
nb:线圈个数
src:多个线圈状态
返回值:
成功:?
失败:-1
格式:int modbus_write_register(modbus_t *ctx, int addr, int value);
功能:写入单个寄存器(对应功能码为0x06)
参数:
ctx:Modbus实例
addr:寄存器地址
value:寄存器的值
返回值:
成功:?
失败:-1
格式:int modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *src);
功能:写入多个连续寄存器(对应功能码为16)
参数:
ctx:Modbus实例
addr:寄存器地址
nb:寄存器的个数
src:多个寄存器的值
返回值:
成功:?
失败:-1
注意:
追源码操作:ctrl + 鼠标单击
返回:alt + 键盘向左方向的键
只有工作区顶层目录下有解压的库文件夹可以追到
编程的时候要加头文件:#include "modbus.h"
编译代码的时候需要链接库:-l modbus
练习:
编程实现采集传感器数据和控制硬件设备(传感器和硬件通过slave模拟)
传感器两个:光线传感器、加速度传感器(x\y\z)
硬件设备两个:led灯、蜂鸣器
要求:
- 多任务编程:多线程
- 循环1s采集一次数据,并将数据打印到终端
- 同时从终端输入指令控制硬件设备
0 1:led灯打开
0 0:led灯关闭
1 1:蜂鸣器打开
1 0:蜂鸣器关闭
/*思路*/
//采集数据
void * handler_data(void * arg)
{
modbus_t *ctx=(modbus_t *)arg;
循环采集数据,并打印到终端,睡一秒
}
//控制设备
void* handler_ctrl(void * arg)
{
循环从终端输入,写线圈,睡一秒
int dev,op;
scanf("%d %d",&dev,&op);
(ctx,dev,op)
}
int main()
{
//1.创建实例
//2.设置从机ID
//3.链接
//4.创建线程
//5.阻塞
//6.关闭套接字
//7.释放实例
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "modbus.h"
#define ERR_MSG(msg) \
do \
{ \
fprintf(stderr, "__%s__ __%s__ __%d__", __FILE__, __func__, __LINE__); \
perror(msg); \
} while (0)
#define IP "192.168.50.28"
#define PORT 502
void *Coil(void *arg)
{
modbus_t *ctx = modbus_new_tcp(IP, PORT);
int slave = 1;
modbus_set_slave(ctx, slave);
modbus_connect(ctx);
int addr, status;
while (1)
{
scanf("%d %d", &addr, &status);
if (modbus_write_bit(ctx, addr, status) < 0)
{
ERR_MSG("modbus_write_bit err");
break;
}
if (addr == 0 && status == 0)
printf("led关闭\n");
else if (addr == 0 && status == 1)
printf("led打开\n");
else if (addr == 1 && status == 0)
printf("蜂鸣器关闭\n");
else if (addr == 1 && status == 1)
printf("蜂鸣器打开\n");
else
{
printf("error\n");
}
}
exit(0);
}
void *ReadRegister(void *arg)
{
//读寄存器
modbus_t *ctx = modbus_new_tcp(IP, PORT);
int slave = 1;
modbus_set_slave(ctx, slave);
modbus_connect(ctx);
uint16_t dest[128];
while (1)
{
if (modbus_read_registers(ctx, 0, 4, dest) < 0)
{
ERR_MSG("modbus_read_registers err");
break;
}
for (int i = 0; i < 4; i++)
{
printf("%#.2x ", dest[i]);
}
printf("\n");
sleep(3);
}
exit(0);
}
int main(int argc, char const *argv[])
{
//多线程
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, Coil, NULL))
{
ERR_MSG("tid1 err");
return -1;
}
printf("Coil create success\n");
if (pthread_create(&tid2, NULL, ReadRegister, NULL))
{
ERR_MSG("tid2 err");
return -1;
}
printf("ReadRegister create success\n");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
注意事项:
Modbus TCP是应用层协议,是基于TCP协议进行传输数据的
Modbus TCP协议格式:报文头 + 功能码 + 数据
8种功能码,常用的有:0x01,0x03,0x06,0x15
主机给从机发的消息一般是12个字节
modbus库一般在 /lib 或 /usr/include 路径下
先设置
后连接(连接时注意先开启slave端(slave相当于服务器端),再开启poll端(poll相当于客户端))
查询主机ip:win + r 然后输入cmd,调出命令提示符界面,输入命令ipconfig即可查询主机ip
安装使用wireshark时注意要把杀毒软件和防火墙关闭。
捕获器的选择:
过滤条件:
练习:
在虚拟机写程序实现poll端功能,编写客户端实现和slave通信。
要求:
实现对slave单个线圈的控制
实现读保持寄存器(03功能码)
分别对以上两个功能封装函数,其中读保持寄存器函数参数可以传递寄存器起始地址、寄存器个数和从机ID
#include
#include // atoi
#include
#include
#include
#include
#include
#include
int sockfd; //定义文件描述符
void set_slave_id(uint8_t *p, int id) //设置从机id
{
*(p + 6) = id;
}
//读保持寄存器 (发送数据首地址, 功能码, 寄存器地址,寄存器数量,存放接受数据首地址)
void read_registers(uint8_t *p, int function, int addr, int nb, uint8_t *dest)
{
int i;
*(p + 5) = 6; //后面字节数
*(p + 7) = (char)function; //功能码
*(p + 8) = addr >> 8; //寄存器高字节地址
*(p + 9) = addr & 0xff; //寄存器低字节地址
*(p + 10) = nb >> 8; //寄存器数量高位
*(p + 11) = nb & 0xff; //寄存器数量低位
send(sockfd, p,12,0);//注意这里不能sizeof(p),p为指针
recv(sockfd, dest,64,0);//注意这里不能sizeof(dest),dest为指针
}
void write_coil(uint8_t *p, int function, int addr, int nb, uint8_t *dest)
{
int i = 0;
*(p + 5) = 6; //后面字节数
*(p + 7) = (char)function; //功能码
*(p + 8) = addr >> 8; //线圈高位地址
*(p + 9) = addr & 0xff; //线圈低位地址
if (nb == 1)
*(p + 10) = 0xff;
else if (nb == 0)
*(p + 10) = 0x00;
*(p + 11) = 0x00;
send(sockfd, p, 12, 0);
recv(sockfd, dest, 64, 0);
}
int main(int argc, char const *argv[])
{
struct sockaddr_in s;
uint8_t data[12] = {0};
uint8_t dest[64] = {0};
int i;
//1.socket创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
socklen_t len = sizeof(struct sockaddr_in);
//2.填充结构体
s.sin_family = AF_INET; //协议族
s.sin_port = htons(atoi(argv[2])); // htons:小端转大端 atoi:将数字字符串转换为数值
s.sin_addr.s_addr = inet_addr(argv[1]); //字符串转点分十进制
//3.connect请求连接
if (connect(sockfd, (struct sockaddr *)&s, len) < 0)
{
perror("connect err");
return -1;
}
//4.设置从机ID
set_slave_id(data, 1);
printf("从机id\n");
//5.循环发送
while (1)
{
printf("开始读\n");
read_registers(data, 0x03, 0, 2, dest);
printf("recv data:");
for (i = 0; i < dest[8]; i++)
printf("%#x ", dest[9 + i]);
printf("\n");
sleep(1);
write_coil(data,0x05,0,1,dest);//线圈置一
printf("线圈置位后:\n");
printf("%#x %#x \n",dest[10],dest[11]);
sleep(1);
}
//5.关闭套接字
close(sockfd);
return 0;
}
web Server中午呢名叫网页服务器或Web服务器,Web服务器也称为WWW(World Wide Web)服务器,其主要功能就是提供网上信息浏览服务。
web Server通常分为Kangle、Nginx、Apache等。在嵌入式中常见的轻量级服务器有Lighttpd、Shttpd、Thttpd、Boa、Mini_httpd、Appweb、Goahead。
Lighttpd是一个开源的轻量级嵌入式web Server,是一个提供专门针对高性能网站,安全、快速、兼容性好并且较为灵活的Web Server环境,具有非常低的内存开销,cpu占用率低、效能好以及丰富的模块等特点。
1. 解压
执行命令 tar -xvf lighttpd-1.4.54.tar.gz
2. 进入源代码,创建文件夹web
先执行命令 cd lighttpd-1.4.54
再执行命令 mkdir web
3. 执行configure脚本文件
执行命令 ./configure --prefix=$PWD/web
4. 执行Makefile文件
先执行命令 make
再执行命令 make install
1. 在web文件夹下创建文件夹(config、log、run、www)
先执行命令 cd web
再执行命令 mkdir config log run www
2. 将源目录lighttpd-1.4.54下web文件夹移动到某个路径下
执行命令 mv lighttpd-1.4.54/web ~/hq/home/demo
3. 将源码目录lighttpd-1.4.54/doc/config下的conf.d lighttpd.conf modules.conf复制到~/hq/home/demo/web/config中
执行命令 cp conf.d lighttpd.conf modules.conf ~//hq/home/demo/web/config -r
4. 修改log文件夹权限,并在log目录下创建error.log文件修改权限
先执行命令 chmod 777 log
再执行命令 touch log/error.log
最后执行命令 chmod 777 log/error.log
5. 在www目录下创建htdocs文件夹存放网页文件
执行命令 mkdir www/htdocs
1. 执行命令 vi ~/work/web/config/lighttpd.conf
##
var.home_dir = "/home/hq/work/web" #lighttpd操作的主目录
var.log_root = home_dir + "/log" #日志文件目录(程序执行中出现的错误信息)
var.server_root = home_dir + "/www" #存放html、cgi代码目录
var.state_dir = home_dir + "/run" #存放pid文件服务运行起来后自动创建
var.conf_dir = home_dir + "/config" #存放配置文件
##
var.vhosts_dir = home_dir + "/vhosts"
##
var.cache_dir = home_dir + "/cache"
##
var.socket_dir = home_dir + "/sockets"
##
server.port = 80 #端口号为80
##
server.use-ipv6 = "disable" #设置为禁用
##
#server.bind = "localhost" #默认即可
##
server.username = "hq" #修改为当前用户,nobody为任何人都可以访问
#server.groupname = "nobody" #将其注释即可
##
server.document-root = server_root + "/htdocs" #存放html网页的文件
##
server.pid-file = state_dir + "/lighttpd.pid"
##
server.errorlog = log_root + "/error.log" #错误日志文件
2. 执行命令 vi ~/work/web/config/modules.conf
include "conf.d/cgi.conf" 将此行注释打开(149)
3. 执行命令 vi ~/work/web/config/conf.d/cgi.conf
$HTTP["url"] =~ "^/cgi-bin" {
cgi.assign = ( "" => "" )
} 将这三行注释打开28-30行
1. 运行
先执行命令 cd ~/work/web
再执行命令 sudo sbin/lighttpd -f config/lighttpd.conf -m lib/
注意:结束进程命令是 pkill lighttpd
2. 测试
将 index.html 文件放到www/htdocs目录下
打开浏览器,在地址栏输入服务器的IP地址(虚拟机IP地址)即可看到index的主页
容易出现的问题:
403:找一下index.html有没有出错或index.html位置有没有放错
404:连接问题,防火墙管、家有没有退或虚拟机有没有网,服务器有没有开
模拟浏览器,实现Modbus Slave端数据采集和硬件设备控制。
先确保服务器打开
先执行结束进程命令:pkill lighttpd
再执行打开命令:sudo sbin/lighttpd -f config/lighttpd.conf -m lib/
按照 上述设置完成postman后,将lighttpd服务器开启,当点击发送时,会在小终端上显示调试信息,同时在postman中也会看到回复的数据。
在某路径下先新建文件夹,打开VScode打开文件夹,新建文件,文件命名为index.html
库安装完成后,在编写文本位置右击 -> open in other browser -> 选择合适的浏览器即可在网页显示html标签内容。
早期的Web服务器只能响应浏览器发来的http静态资源的请求,并将存储在服务器中的静态资源返回给浏览器。随着Web技术的发展,逐渐出现了动态技术,但是Web服务器并不能够直接运行动态脚本,为了解决Web服务器与外部应用程序之间数据互通,于是出现了CGI通用网关接口。简单理解,可以认为CGI是Web服务器和运行其上的应用程序进行“交流”的一种约定。CGI(Common Gateway Interface)通用网关接口,是外部扩展应用程序与 Web 服务器交互的一个标准接口。
CGI是Web服务器和一个独立的进程之间的协议,通过环境变量以及标准输入、标准输出和服务器进行数据交互。
REQUEST_URI:访问此页面需要的URL,比如:“/index.html”
REQUEST_METHOD:获取客户端请求数据的方式:POST或GET
CONTENT_LENGTH:获取用户数据的长度
CONTENT_TYPE:网页中存在的 Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件
当浏览器向Web服务器发送动态数据请求时,Web服务器主进程就会fork()创建出一个新的进程来启动CGI程序,也就是将动态脚本交给CGI程序来处理。当CGI程序启动后会去解析动态脚本然后将结果返回给Web服务器,最后由Web服务器将结果返回给客户端,之前fork()出来的进程也随之关闭。这样每次用户请求动态脚本时Web服务器都要重新fork()创建一个新进程去启动CGI程序,由CGI程序来处理动态脚本,处理完后进程随之关闭。对于一个CGI程序,主要的工作是从环境变量和标准输入中读取数据,然后处理数据,最后向标准输出中输出数据(在这服务器将标准输入和标准输出做了重定向)。
在main.c程序中,主函数内的handle_requst() 函数获取网页发给服务器的数据,请求头(环境变量)和消息正文(标准输入)的信息,再调用parse_and_process() 函数,在该函数中根据正文判断网页需要执行什么操作(读传感器数据或控制硬件设备状态),根据请求完成采集数据或控制硬件设备,最终给网页回复(标准输出)数据(遵循http协议)。
1. 首先将cgi_demo复制到虚拟机web目录下
2. 用uxterm命令打开简化版终端(小终端),用who am i命令查看当前小终端的文件,根据自己的小终端文件修改log_consloe.h里面的内容。
3. 在www/htdocs下创建cgi-bin文件夹,在cgi源码目录(cgi_demo)执行make,会在cgi-bin路径下生成web.cgi
HTTP协议(Hyper Transfer Protocol)是超文本传输协议,是用于web Browser(浏览器)到web Server(服务器)进行数据交互的传输协议,是基于TCP通信协议传输来传送数据(HTML文件,图片文件,查询结果等)的应用层协议,该协议工作于B/S架构上,浏览器作为HTTP客户端通过URL主动向HTTP服务端也就是Web服务器发送请求,Web服务器接收到请求后,向客户端发送响应信息,其中HTTP协议的默认端口号是80,但是可以手动改端口号。
1. 客户端请求消息格式
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行、请求头部、空行和请求数据四个部分。一般格式如下:
请求行:
请求行是由请求方法字段、url字段、http协议本字段三个部分组成。请求行定义了本次请求的方式。
格式如下:GET/example.html HTTP/1.1(CRLF)
请求头:
也被称作消息报头,请求头是有一些键值对组成,每行一对,关键字和值用英文冒号“ : ”分隔。
Accept:作用:描述客户端希望接收的 响应body 数据类型;示例:Accept:text/html
Accept-Charset:作用:浏览器可以接受的字符编码集;示例:Accept-Charset:utf-8
Accept-Language:作用:浏览器可接受的语言;示例:Accept-Language:en
Connection:作用:表示是否需要持久连接,注意HTTP1.1默认进行持久连接;示例:Connection:close
Content-Length:作用:请求的内容长度:示例:Content-Length:348
Content-Type:作用:描述客户端发送的 body 数据类型
2. 服务器响应消息格式
HTTP响应也由四个部分组成,分别是:状态行、消息报头空行和响应正文。
状态行由HTTP协议版本号、状态码以及对状态码的文本描述。例如:HTTP/1.1 200 OK(CRLF)。(200表示请求已经成功)。
HTML(Hyper Text Markup Language)是超文本标记语言,用来描述网页的一种语言,所谓超文本就是可以加入图片、动画、声音、多媒体等内容,还可以从一个文件跳转到另一文件,与其他主机的文件连接。HTML不是一种编程语言而是一种标记语言。
Web浏览器的作用是读取HTML文档,并且以网页的形式显示,但是浏览器不会显示HTML标签,而是使用标签来解释页面的内容。
1. h1-h6标题标签
格式为:
h1是一级标签,依次后推,h6是六级标签
2. p段落标签
一个段落中会根据浏览器窗口的大小自动换行
格式为:
文本内容
3. br换行标签(强制换行)
是一个块级元素,可以把文档分割为独立的、不同的部分,可以在div标签中嵌套标签
格式为:
例如:
News headline 1
some text. some text. some text...
注意:div标签可以设置class或id,通过选择器设置属性,则内部成员具有相同属性。
4. Input表单标签
表示输入的意思,是单标签
格式为:
属性有多种:
其中type属性的text和radio较为重要。
当type为text时,表示的是文本输入框。
用法为:
当type为radio时,表示的是单选框
用法为:
其中,name为控件名称,同一组单选框设置相同的名字,一组内只能选一个
value值必须要填写,是当点击时会提交的数据
onclick:点击时会执行双引号中的处理函数
checked:默认选中,同一组中只选中一个即可
5. label标签
label标签为input元素定义标注(标签),其作用是用于绑定一个表单元素,当点击label标签的时候,被绑定的表单元素就会获得输入焦点。
例如:
注:这里for要跟input中的id一致
项目要求:
编程实现采集传感器数据和控制硬件设备(传感器和硬件通过slave模拟)
传感器两个:光线传感器、加速度传感器(x\y\z)
硬件设备两个:led灯、蜂鸣器
要求:
- 多任务编程:多线程
- 循环1s采集一次数据,并将数据打印到网页
- 同时从网页输入指令控制硬件设备
0 1:led灯打开
0 0:led灯关闭
1 1:蜂鸣器打开
1 0:蜂鸣器关闭
注意事项:
1. 存在共享内存和消息队列数据接收发送出问题时
解决方案:
1. 在代码中打印语句,确保两个进程用的是同一个id
2. 由于程序是强制结束,下次再运行代码时,将已存在的消息队列删除
查看和删除共享内存、消息队列
ipcs -m :查看共享内存
ipcrm -m shmid:删除共享内存
ipcs -q:查看消息队列
ipcrm -q semid:删除消息队列
2. key值的创建路径指定或目录下的某个新建文件
3. 多使用打印语句,学会通过uxterm转到小终端查看打印信息,方便定位错误位置
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "modbus.h"
#define ERR_MSG(msg) \
do \
{ \
fprintf(stderr, "__%s__ __%s__ __%d__", __FILE__, __func__, __LINE__); \
perror(msg); \
} while (0)
#define IP "192.168.50.192"//连接windows端ip地址,ipconfig
#define PORT 502
#define filepath "/home/demo/web/cji_demo/main.c"
//消息队列结构体
struct msgbuf
{
long mtype;
char buf[128];
} msg;
int SharedMemory(modbus_t *ctx, int shmid)
{
uint16_t dest[128];
char *p = shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
perror("shmat error");
return -1;
}
printf("shmat success\n");
if (modbus_read_registers(ctx, 0, 4, dest) < 0)
{
ERR_MSG("modbus_read_registers err");
return -1;
}
for (int i = 0; i < 4; i++)
{
printf("%#.2x ", dest[i]);//在终端打印数据部分
}
sprintf(p, "%#.2x %#.2x %#.2x %#.2x", dest[0], dest[1], dest[2], dest[3]); //将数据写入共享内存
printf("%s\n", p);
printf("\n");
}
void *handler1(void *arg)
{
//创建modbus实例
modbus_t *ctx = modbus_new_tcp(IP, PORT);
int slave = 1;
modbus_set_slave(ctx, slave);
if (modbus_connect(ctx) < 0)
{
perror("connect error");
return -1;
}
printf("connect success\n");
//uint16_t dest[128];
//创建共享内存
key_t key;
key = ftok("/home/hq/app.c", 'a');
int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
if (17 == errno)
{
shmid = shmget(key, 128, 0666);//已经打开
}
else
{
perror("shmget error");
return -1;
}
}
printf("key:%d shmid:%d\n", key, shmid);
printf("shmget success\n");
while (1)
{
// char *p = shmat(shmid, NULL, 0);
// if (p == (char *)-1)
// {
// perror("shmat error");
// return -1;
// }
// printf("shmat success\n");
// if (modbus_read_registers(ctx, 0, 4, dest) < 0)
// {
// ERR_MSG("modbus_read_registers err");
// break;
// }
// for (int i = 0; i < 4; i++)
// {
// printf("%#.2x ", dest[i]);
// }
// sprintf(p, "dest:%#.2x %#.2x %#.2x %#.2x", dest[0], dest[1], dest[2], dest[3]); //写入共享内存
// printf("%s\n", p);
// printf("\n");
// sleep(3);
//对共享内存进行操作
SharedMemory(ctx, shmid);
}
pthread_exit(0);
modbus_close(ctx);
modbus_free(ctx);
}
void *handler2(void *arg)
{
//消息队列
//连接modbus
modbus_t *ctx = modbus_new_tcp(IP, PORT);
int slave = 1;
modbus_set_slave(ctx, slave);
modbus_connect(ctx);
uint16_t dest[128];
//创建共享内存
key_t key;
key = ftok("/home/hq/app.c", 'b');
int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
if (msgid < 0)
{
if (17 == errno)
{
msgid = msgget(key, 0666);
}
else
{
perror("msgget error");
return -1;
}
}
// struct msgbuf msg;
while (1)
{
//循环写入控制器的内容
msgrcv(msgid, &msg, sizeof(msg) - sizeof(long), 1, 0);
printf("msgbuf:%s\n", msg.buf);
if (msg.buf[4] == '0' && msg.buf[6] == '0')
{
printf("led关闭\n");
//将控制器状态写入到消息队列
modbus_write_bit(ctx, 0, 0);
}
else if (msg.buf[4] == '0' && msg.buf[6] == '1')
{
printf("led打开\n");
modbus_write_bit(ctx, 0, 1);
}
else if (msg.buf[4] == '1' && msg.buf[6] == '0')
{
printf("蜂鸣器关闭\n");
modbus_write_bit(ctx, 1, 0);
}
else if (msg.buf[4] == '1' && msg.buf[6] == '1')
{
printf("蜂鸣器打开\n");
modbus_write_bit(ctx, 1, 1);
}
}
msgctl(msgid, IPC_RMID, NULL);
modbus_close(ctx);//关闭ctx
modbus_free(ctx);//释放ctx
pthread_exit(0);//退出线程
}
int main(int argc, char const *argv[])
{
//创建多线程
pthread_t pid1, pid2;
if (pthread_create(&pid1, NULL, handler1, NULL))
{
perror("pid1 error");
return -1;
}
printf("pid1 success\n");
if (pthread_create(&pid2, NULL, handler2, NULL))
{
perror("pid2 error");
return -1;
}
printf("pid2 success\n");
pthread_join(pid1, NULL);//回收线程
pthread_join(pid2, NULL);
return 0;
}
#指定编译器
CC = gcc
#指定最终目标名字
OBJ = web.cgi
#指定所有的中间.o文件
OBJS := main.o log_console.o req_handle.o custom_handle.o
#指定cgi的拷贝路径
CGI_DIR = ../www/htdocs/cgi-bin
all:$(OBJS)
$(CC) -o $(OBJ) $^
cp $(OBJ) $(CGI_DIR)
# .a.o.d .b.o.d
dep_files := $(foreach f,$(OBJS),.$(f).d)
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)
include $(dep_files)
endif
%.o : %.c
$(CC) -Wp,-MD,[email protected] -c -o $@ $<
clean:
rm -rf .*.o.d *.o $(OBJ)
#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);
return 0;
}
#ifndef REQ_HANDLE_H
#define REQ_HANDLE_H
int handle_request(int argc, char *argv[]);
#endif // REQ_HANDLE_H
#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)//相当于strcmp,比较两个字符串的值
{
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_content中了
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;
}
#ifndef LOG_CONSOLE_H
#define LOG_CONSOLE_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CONFIG_ARCH_X86
#ifdef CONFIG_ARCH_X86
#define LOG_CONSOLE "/dev/pts/20"//改成对应的小终端的文件名
#else
#define LOG_CONSOLE "/dev/tty1"
#endif
int log_console_init();
int log_console(const char *format, ...);
#endif // LOG_CONSOLE_H
/***********************************************************************************
Copy right: Coffee Tech.
Author: jiaoyue
Date: 2022-03-23
Description: console模块
***********************************************************************************/
#include
#include
#include
#include
#include
#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, ...)//相当于重写printf,写到小终端而不是网页
{
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));
}
#ifndef CUSTOM_HANDLE_H
#define CUSTOM_HANDLE_H
int parse_and_process(char *input);
#endif // REQ_HANDLE_H
#include "req_handle.h"
#include "log_console.h"
#define KB 1024
#define HTML_SIZE (64 * KB)
#define filepath "/home/demo/web/cji_demo/main.c"
//普通的文本回复需要增加html头部
#define HTML_HEAD "Content-Type: text/html\r\n" \
"Connection: close\r\n"
/**
* @brief 处理自定义请求,在这里添加进程通信
* @param input
* @return
*/
//消息队列结构体
struct msgbuf
{
long mtype;//消息类型
char buf[128];//接收到的数据
};
int parse_and_process(char *input)
{
char val_buf[2048] = {0}; //获取的回应数据部分
//strcpy(val_buf, input);
//这里可以根据接收的数据请求进行处理
//共享内存,判断是set还是get,然后做出相应的回应
//读:共享内存读内容,然后原文返回
//创建key值
key_t key;
//从共享内存读数据
if (strcmp(input, "get") == 0)
{
key = ftok("/home/hq/app.c", 'a');
if (key < 0)
{
log_console("1111");
perror("ftok error");
return -1;
}
log_console("ftok success\n");
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 error");
return -1;
}
}
log_console("key:%d shmid:%d",key,shmid);
log_console("shmget success\n");
char *p = shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
perror("shmat error");
return -1;
}
log_console("shmat success\n");
//将数据放到val_buf里面以便后面组合成响应数据
strcpy(val_buf, p);
log_console("p:%s\n", p);
log_console("val_buf:%s\n", val_buf);
}
//写:用消息队列写
else
{
log_console("write\n");
key = ftok("/home/hq/app.c", 'b');
if(key<0)
{
log_console("key err\n");
perror("ftok err");
return -1;
}
int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
if (msgid <= 0)
{
if (17 == errno)
{
msgid=msgget(key, 0666);
}
else
{
log_console("msgget\n");
perror("msgget error");
return -1;
}
}
log_console("msgget success\n");
log_console("key:%d msgid:%d\n",key,msgid);
struct msgbuf msg;
msg.mtype = 1; //消息类型全部设为1
strcpy(msg.buf, input);
msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);
strcpy(val_buf, msg.buf);
log_console("val_buf:%s\n", val_buf);
}
//数据处理完成后,需要给服务器回复,回复内容按照http协议格式
char reply_buf[HTML_SIZE] = {0};
//reply_buf先获取请求头
sprintf(reply_buf, "%sContent-Length: %ld\r\n\r\n", HTML_HEAD, strlen(val_buf)); //\r\n组合起来是回车换行
//将正文部分接到请求头后面
strcat(reply_buf, val_buf);
log_console("post json_str = %s", reply_buf);
//向标准输出写内容(标准输出服务器已做重定向)
fputs(reply_buf, stdout);
return 0;
}
Document
基于WebServer的工业数据采集项目
------信息采集------
光线传感器
光线传感器:
加速度传感器
加速度传感器x:
加速度传感器y:
加速度传感器z:
Modbus设备
led灯状态:
开
关
蜂鸣器状态:
开
关