基于Webserver的数据采集
架构:
介绍:
硬件端:设备端使用modbus slave来模拟(服务器端),通过Modbus TCP协议与Modbus 采集控制程序(客户端)进行通信
通信:(进程间通信)
对于采集的传感器数据:Modbus 采集控制程序执行modbus tcp的03功能将传感器数据从slave一侧读出,通过共享内存将数据交给网页服务器,最终在网页上显示出来
对于控制信息:网页端点击操作后,网页服务器收到相应的数据(即要实现的指令),并通过消息队列将指令传给Modbus 采集控制程序,然后Modbus 采集控制程序再通过modbus tcp的05功能实现对slave的控制
网页端:搭建一个简易的网页服务器,通过HTTP协议实现网页服务器与网页之间的请求与响应
演示:
【基于webserver工业数据采集小项目】 https://www.bilibili.com/video/BV18xMbzqEMn/?share_source=copy_web&vd_source=ca8d891b9994089253ad45652f349b9e
主要代码:
Modbus 采集控制程序:
#include <pthread.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <unistd.h>
#include <stdlib.h>
#include <modbus.h>
#include <stdio.h>
#include <errno.h>
// modbus clientmodbus_t *ctx1,*ctx2;//从mobus slave端读取传感器数据,并通过共享内存将读到的数据送到网页服务器
void *handler1(void *arg)
{int num;// 拿到keykey_t k = ftok("a.c", 'a');// 获取共享内存号int shmid;shmid = shmget(k, sizeof(uint16_t)*128, IPC_CREAT | IPC_EXCL | 0777);if (shmid < 0){if (errno == EEXIST){shmid = shmget(k, sizeof(uint16_t)*128, 0777);}else{perror("shmget err");return NULL;}}// 将共享内存映射到用户空间(拿到共享内存地址)uint16_t* data = ( uint16_t*)shmat(shmid, NULL, 0);if (data == ( uint16_t*)-1){perror("shmat err");return NULL;}while (1){// 执行03功能num = modbus_read_registers(ctx1, 0, 4, data);for (int i = 0; i < num; i++){printf("%d ", data[i]);}putchar(10);sleep(1);}
}struct msgbuf
{/*消息类型(正整数)*/long type;//必须有!!/*消息正文 自定义*/int order;
};
//网页服务器通过消息队列发来的控制命令,对modbus slave端的硬件设备进行控制
void *handler2(void *arg)
{key_t key;key= ftok("a.c",'a');if(key<0){perror("ftok err");return NULL;}//创建或打开消息队列int msgid=msgget(key,IPC_CREAT|IPC_EXCL|0777);if(msgid<0){if (errno==EEXIST){msgid=msgget(key,0777);}else{perror("msgget err");return NULL;}}/*读取消息*///定义一个结构体变量用来接收消息struct msgbuf m;while (1){msgrcv(msgid,&m,sizeof(m)-sizeof(long),1,IPC_NOWAIT);int order=m.order;if (order == 0) //LED on{modbus_write_bit(ctx2, 0, 1);}else if (order == 1) //LED off{modbus_write_bit(ctx2, 0, 0);}else if (order == 2) //buzzer on{modbus_write_bit(ctx2, 1, 1);}else //buzzer off{modbus_write_bit(ctx2, 1, 0);}}return NULL;
}int main(int argc, char const *argv[])
{// 创建modbus实例//用于采集数据ctx1 = modbus_new_tcp(argv[3], atoi(argv[1]));if (ctx1 == NULL){perror("modbus_new_tcp err");return -1;}else{printf("创建实例成功\n");}// 设置从机IDif (modbus_set_slave(ctx1, 1) < 0){perror("modbus_set_slave err");return -1;}else{printf("从机ID设置成功\n");}// 建立连接if (modbus_connect(ctx1) < 0){perror("modbus_connect err");return -1;}else{printf("连接成功\n");}//用于控制ctx2 = modbus_new_tcp(argv[3], atoi(argv[2]));if (ctx2 == NULL){perror("modbus_new_tcp err");return -1;}else{printf("创建实例成功\n");}// 设置从机IDif (modbus_set_slave(ctx2, 2) < 0){perror("modbus_set_slave err");return -1;}else{printf("从机ID设置成功\n");}// 建立连接if (modbus_connect(ctx2) < 0){perror("modbus_connect err");return -1;}else{printf("连接成功\n");}pthread_t tid1, tid2;pthread_create(&tid1, NULL, handler1, NULL);printf("输入指令来设置线圈状态(0:LED开 1:LED关 2:buzzer开 3:buzzer关)\n");pthread_create(&tid2, NULL, handler2, NULL);pthread_join(tid1,NULL); pthread_join(tid2,NULL);// 关闭套接字modbus_close(ctx1);modbus_close(ctx2);// 关闭连接modbus_free(ctx1);modbus_free(ctx2);return 0;
}
WEB服务器主要使用函数:
/***********************************************************************************
Copy right: hqyj Tech.
Author: jiaoyue
Date: 2023.07.01
Description: http请求处理
***********************************************************************************/#include <sys/types.h>
#include <sys/socket.h>
#include "custom_handle.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_login(int sock, const char *input)
{char cpy[128];strcpy(cpy, input);char reply_buf[HTML_SIZE] = {0};// strstr函数返回子串在主串中首次出现时的位置char *p = strstr(cpy, "password");char *passwd = p + strlen("password=");char *temp = passwd;while (*temp != '\"')temp++;*temp = '\0';*(p) = '\0';char *uname = strstr(cpy, "username=");uname += strlen("username=");// 创建和打开数据库sqlite3 *db;if (sqlite3_open("userDB.db", &db) != SQLITE_OK){fprintf(stderr, "open err:%s\n", sqlite3_errmsg(db));return -1;}else{printf("打开数据库成功\n");}// 创建表char *errmsg;if (sqlite3_exec(db, "create table if not exists usermsg (name string,password int)", NULL, NULL, &errmsg) != SQLITE_OK){fprintf(stderr, "create table err:%s\n", errmsg);return -1;}else{printf("打开表成功\n");}char a1[5] = "no";char a2[5] = "yes";char sql[128];sprintf(sql, "select * from usermsg where name='%s' and password='%s'", uname, passwd);printf("sql=%s\n", sql);char **result = NULL; // 用于存储查询到的结果int row = 0, column = 0; // 记录行数和列数sqlite3_get_table(db, sql, &result, &row, &column, &errmsg);if (row==0){send(sock, a1, 5, 0);printf("%s\n", a1);}else{send(sock, a2, 5, 0);printf("%s\n", a2);}return 0;
}static int handle_add(int sock, const char *input)
{int number1, number2;// input必须是"data1=1data2=6"类似的格式,注意前端过来的字符串会有双引号sscanf(input, "\"data1=%ddata2=%d\"", &number1, &number2);printf("num1 = %d\n", number1);char reply_buf[HTML_SIZE] = {0};printf("num = %d\n", number1 + number2);sprintf(reply_buf, "%d", number1 + number2);printf("resp = %s\n", reply_buf);send(sock, reply_buf, strlen(reply_buf), 0);return 0;
}/*** @brief 处理自定义请求,在这里添加进程通信* @param input* @return*/
int count = 0;
// 采集数据
int commuwithsensor(int sock, const char *input)
{// 拿到keykey_t k = ftok("/home/hq/25041/day55/a.c", 'a');// 获取共享内存号int shmid;shmid = shmget(k, sizeof(uint16_t) * 128, IPC_CREAT | IPC_EXCL | 0777);if (shmid < 0){if (errno == EEXIST){shmid = shmget(k, sizeof(uint16_t) * 128, 0777);}else{perror("shmget err");return -1;}}// 将共享内存映射到用户空间(拿到共享内存地址)uint16_t *data = (uint16_t *)shmat(shmid, NULL, 0);if (data == (uint16_t *)-1){perror("shmat err");return -1;}// 将传感器数据从共享内存中拿到,发送给网页char reply_buf[HTML_SIZE] = {0};sprintf(reply_buf, "%d %d %d %d \n", data[0], data[1], data[2], data[3]);printf("resp = %s\n", reply_buf);send(sock, reply_buf, strlen(reply_buf), 0);// 将记录插入数据库char sql[128];char buf[128];sprintf(buf, "传感器:%d 加速度_x: %d 加速度_y:%d 加速度_z:%d ", data[0], data[1], data[2], data[3]);sprintf(sql, "insert into sensordata(id, data) values(%d,'%s')", count++, buf);commuwithdb(sql);return 0;
}// 控制
int commuwithcoil(int sock, const char *input)
{int order;sscanf(input, "\"order=%d\"", &order);printf("order = %d \n", order);// 拿到key值key_t key;key = ftok("/home/hq/25041/day55/a.c", 'a');if (key < 0){perror("ftok err");return -1;}// 创建或打开消息队列int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0777);if (msgid < 0){if (errno == EEXIST){msgid = msgget(key, 0777);}else{perror("msgget err");return -1;}}/*制作消息*/// 先定义一个结构体变量struct msgbuf msg;// 变量初始化msg.type = 1;msg.order = order;/*添加消息*/msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);// 将记录插入数据库char sql[128];char data[20];if (order == 0){strcpy(data, "LED on");}else if (order == 1){strcpy(data, "LED off");}else if (order == 2){strcpy(data, "buzzer on");}else{strcpy(data, "buzzer off");}sprintf(sql, "insert into sensordata(id, data) values(%d,'%s')", count++, data);commuwithdb(sql);
}int commuwithdb(char *sql)
{// 创建和打开数据库sqlite3 *db;if (sqlite3_open("userDB.db", &db) != SQLITE_OK){fprintf(stderr, "open err:%s\n", sqlite3_errmsg(db));return -1;}else{printf("打开数据库成功\n");}// 创建表char *errmsg;if (sqlite3_exec(db, "create table if not exists sensordata (id int ,data string,time DATETIME DEFAULT CURRENT_TIMESTAMP)", NULL, NULL, &errmsg) != SQLITE_OK){fprintf(stderr, "create table err:%s\n", errmsg);return -1;}else{printf("打开表成功\n");}if (count == 0){if (sqlite3_exec(db, "delete from sensordata ", NULL, NULL, &errmsg) != SQLITE_OK){fprintf(stderr, "clear table err:%s\n", errmsg);return -1;}else{printf("清空表成功\n");}}if (sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK){fprintf(stderr, "insert err:%s\n", errmsg);return -1;}else{printf("插入成功\n");}
}int parse_and_process(int sock, const char *query_string, const char *input)
{// query_string不一定能用的到// 先处理登录操作if (strstr(input, "username=") && strstr(input, "password=")){return handle_login(sock, input);}// 处理求和请求else if (strstr(input, "data1=") && strstr(input, "data2=")){return handle_add(sock, input);}// 读取传感器数据else if (strstr(input, "light=") && strstr(input, "ax=") && strstr(input, "ay=") && strstr(input, "az=")){return commuwithsensor(sock, input);}else if (strstr(input, "order=")){return commuwithcoil(sock, input);}else // 剩下的都是json请求,这个和协议有关了{// 构建要回复的JSON数据const char *json_response = "{\"message\": \"Hello, client!\"}";// 发送HTTP响应给客户端send(sock, json_response, strlen(json_response), 0);}return 0;
}
登录页面:
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>智能系统登录</title><style>@keyframes float {0%,100% {transform: translateY(0);}50% {transform: translateY(-10px);}}@keyframes fadeIn {from {opacity: 0;transform: translateY(20px);}to {opacity: 1;transform: translateY(0);}}body {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;margin: 0;padding: 0;display: flex;justify-content: center;align-items: center;min-height: 100vh;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);color: #fff;overflow: hidden;}.login-container {width: 90%;max-width: 400px;background: rgba(255, 255, 255, 0.1);backdrop-filter: blur(12px);border-radius: 20px;padding: 40px;box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);border: 1px solid rgba(255, 255, 255, 0.1);text-align: center;animation: fadeIn 0.8s ease-out forwards;transform-origin: center;transition: transform 0.3s ease;}.login-container:hover {transform: scale(1.02);}.logo {font-size: 2.5rem;font-weight: 600;margin-bottom: 30px;background: linear-gradient(to right, #fff, #e0e0e0);-webkit-background-clip: text;background-clip: text;color: transparent;animation: float 4s ease-in-out infinite;}.input-group {margin-bottom: 25px;text-align: left;}label {display: block;margin-bottom: 8px;font-size: 0.9rem;color: rgba(255, 255, 255, 0.8);transform: translateX(5px);transition: all 0.3s ease;}input {width: 100%;padding: 12px 15px;border-radius: 8px;border: 1px solid rgba(255, 255, 255, 0.3);background: rgba(255, 255, 255, 0.1);color: white;font-size: 1rem;transition: all 0.3s ease;}input:focus {outline: none;border-color: rgba(255, 255, 255, 0.6);background: rgba(255, 255, 255, 0.2);box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);}input:focus+label {color: white;transform: translateX(0);}.btn {width: 100%;padding: 12px;border-radius: 8px;border: none;background: rgba(255, 255, 255, 0.3);color: white;font-size: 1rem;font-weight: 500;cursor: pointer;transition: all 0.3s ease;margin-top: 10px;position: relative;overflow: hidden;}.btn:hover {background: rgba(255, 255, 255, 0.4);}.btn:active {transform: scale(0.98);}.btn::after {content: '';position: absolute;top: 50%;left: 50%;width: 5px;height: 5px;background: rgba(255, 255, 255, 0.5);opacity: 0;border-radius: 100%;transform: scale(1, 1) translate(-50%, -50%);transform-origin: 50% 50%;}.btn:focus:not(:active)::after {animation: ripple 1s ease-out;}@keyframes ripple {0% {transform: scale(0, 0);opacity: 0.5;}100% {transform: scale(20, 20);opacity: 0;}}.footer {margin-top: 30px;font-size: 0.8rem;color: rgba(255, 255, 255, 0.6);animation: fadeIn 1s ease-out 0.3s both;}a {color: rgba(255, 255, 255, 0.8);text-decoration: none;transition: color 0.3s ease;position: relative;}a:hover {color: white;}a::after {content: '';position: absolute;width: 0;height: 1px;bottom: -2px;left: 0;background-color: white;transition: width 0.3s ease;}a:hover::after {width: 100%;}</style>
</head><body><div class="login-container"><div class="logo">智能系统</div><div class="input-group"><input type="text" id="username" placeholder=" " required><label for="username">用户名</label></div><div class="input-group"><input type="password" id="password" placeholder=" " required><label for="password">密码</label></div><button class="btn" id="loginBtn">登 录</button><div class="footer"><a href="#">忘记密码?</a> | <a href="#">注册账号</a></div></div><script>document.getElementById('loginBtn').addEventListener('click', function () {// 如果需要验证后再跳转,可以使用以下代码:const u = document.getElementById('username').value;const p = document.getElementById('password').value;var x = new XMLHttpRequest();x.open("post", "", true);//true:异步通知var s = "\"" + "username=" + u + "password=" + p + "\"";x.send(s);x.onreadystatechange = function () {// ==:不区分数据类型的判等// === :区分数据类型的判等if (x.readyState === 4 && x.status === 200) {var r = x.responseText;//响应正文 // console.log(encodeURIComponent(r)); if (r.slice(0,2) == 'no'){alert('用户名密码错误,请重新输入');}else {window.location.href = 'needsensor.html';}}}/*// 直接跳转window.location.href = 'needsensor.html';*/});</script>
</body></html>
主页:
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>智能设备控制面板</title><style>body {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;margin: 0;padding: 0;display: flex;justify-content: center;align-items: center;min-height: 100vh;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);color: #fff;}.container {width: 90%;max-width: 800px;background: rgba(255, 255, 255, 0.1);backdrop-filter: blur(12px);border-radius: 20px;padding: 30px;box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);border: 1px solid rgba(255, 255, 255, 0.1);}h1 {text-align: center;margin-bottom: 30px;font-weight: 600;font-size: 2.2rem;background: linear-gradient(to right, #fff, #e0e0e0);-webkit-background-clip: text;background-clip: text;color: transparent;}.section {margin-bottom: 30px;padding: 25px;border-radius: 15px;background: rgba(255, 255, 255, 0.15);backdrop-filter: blur(5px);}h2 {margin-top: 0;margin-bottom: 20px;font-weight: 500;color: #f0f0f0;}.sensor-grid {display: grid;grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));gap: 20px;}.sensor-card {background: rgba(255, 255, 255, 0.2);border-radius: 12px;padding: 20px;transition: transform 0.3s ease;}.sensor-card:hover {transform: translateY(-5px);}.sensor-name {font-size: 1rem;margin-bottom: 10px;color: rgba(255, 255, 255, 0.8);}.sensor-value {font-size: 1.8rem;font-weight: 700;margin-bottom: 5px;}.unit {font-size: 0.9rem;color: rgba(255, 255, 255, 0.6);}.controls {display: grid;grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));gap: 20px;}.control-group {background: rgba(255, 255, 255, 0.2);border-radius: 12px;padding: 20px;}.radio-option {display: flex;align-items: center;margin-bottom: 15px;}input[type="radio"] {appearance: none;width: 20px;height: 20px;border: 2px solid rgba(255, 255, 255, 0.5);border-radius: 50%;margin-right: 10px;position: relative;cursor: pointer;}input[type="radio"]:checked {background: rgba(255, 255, 255, 0.3);border-color: #fff;}input[type="radio"]:checked::after {content: '';position: absolute;width: 10px;height: 10px;background: #fff;border-radius: 50%;top: 3px;left: 3px;}label {cursor: pointer;font-size: 1rem;}</style>
</head><body><div class="container"><h1>智能设备控制面板</h1><div class="section"><h2>传感器数据</h2><div class="sensor-grid"><div class="sensor-card"><div class="sensor-name">光照强度</div><div class="sensor-value" id="light-value">0</div><div class="unit">lux</div></div><div class="sensor-card"><div class="sensor-name">加速度 X轴</div><div class="sensor-value" id="accel-x">0.00</div><div class="unit">m/s²</div></div><div class="sensor-card"><div class="sensor-name">加速度 Y轴</div><div class="sensor-value" id="accel-y">0.00</div><div class="unit">m/s²</div></div><div class="sensor-card"><div class="sensor-name">加速度 Z轴</div><div class="sensor-value" id="accel-z">9.81</div><div class="unit">m/s²</div></div></div></div><div class="section"><h2>设备控制</h2><div class="controls"><div class="control-group"><div class="radio-option"><input type="radio" id="led-on" name="led" onclick="set0()"><label for="led-on">LED灯:开启</label></div><div class="radio-option"><input type="radio" id="led-off" name="led" checked onclick="set1()"><label for="led-off">LED灯:关闭</label></div></div><div class="control-group"><div class="radio-option"><input type="radio" id="buzzer-on" name="buzzer" onclick="set2()"><label for="buzzer-on">蜂鸣器:开启</label></div><div class="radio-option"><input type="radio" id="buzzer-off" name="buzzer" onclick="set3()" checked><label for="buzzer-off">蜂鸣器:关闭</label></div></div></div></div></div><script>//每隔一秒刷新一次获取到的传感器数据function updateSensorData() {var x = new XMLHttpRequest();x.open("post", "", true);//true:异步通知x.send("\"light=1ax=2ay=3az=4\"");x.onreadystatechange = function () {// ==:不区分数据类型的判等// === :区分数据类型的判等if (x.readyState === 4 && x.status === 200) {var r = x.responseText;//响应正文var s = r.split(" ");document.getElementById('light-value').textContent = s[0];document.getElementById('accel-x').textContent = s[1]document.getElementById('accel-y').textContent = s[2]document.getElementById('accel-z').textContent = s[3]}}}setInterval(updateSensorData, 1000);updateSensorData();//控制function set0() {var x = new XMLHttpRequest();x.open("post", "", true);//true:异步通知x.send("order=0");}function set1() {var x = new XMLHttpRequest();x.open("post", "", true);//true:异步通知x.send("\"order=1\"");}function set2() {var x = new XMLHttpRequest();x.open("post", "", true);//true:异步通知x.send("\"order=2\"");}function set3() {var x = new XMLHttpRequest();x.open("post", "", true);//true:异步通知x.send("\"order=3\"");}</script></body></html>
问题:
可以再扩展一下注册页面
还有一个让我超级无语的错误o(╥﹏╥)o,
就是这个登录页面的判断,判断用户名和密码是否在数据库中,我在服务器端使用SQL语句查询数据库之后,若能找到对应的用户名密码会给网页响应"yes",否则就响应"no",然后,我明明 r 打印出来就是"no",结果 r=="no"的判断就是0,我真的好一顿捣鼓确认就是判等的问题后,我就去查了查,结果令我十分无语,用console.log(encodeURIComponent(r)); 显示了一下完整的字符串,no后面跟了一堆%00,这要能等才神奇,我就切片了一下,果然,就成功进去了。。。( ̄ー ̄)