架构:

介绍:
硬件端:设备端使用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 采集控制程序:
cpp
#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 client
modbus_t *ctx1,*ctx2;
//从mobus slave端读取传感器数据,并通过共享内存将读到的数据送到网页服务器
void *handler1(void *arg)
{
int num;
// 拿到key
key_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");
}
// 设置从机ID
if (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");
}
// 设置从机ID
if (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服务器主要使用函数:
cpp
/***********************************************************************************
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)
{
// 拿到key
key_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;
}
登录页面:
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>
@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>
主页:
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,这要能等才神奇,我就切片了一下,果然,就成功进去了。。。( ̄ー ̄)