基于Modbus实现的工业采集数据项目(包含led的开关)

简介: 1.首先应该实现一个基于Modbus实现对一个设备的数据采集以及控制,我这边是通过Modbus_tcp实现的。2.因为要实现采集控制程序和网页服务器的通信我这边采用的是共享内粗你和消息队列,传输存储的数据就用共享内存,操作开关就用消息队列3.写网页服务器和一个网页,需要让其两个可以正常通信,就是让网页服务器上的东西可以发到网页上4.然后将采集的数据通过共享内存去分享给网页服务器,让网页服务器传给网页即可,控制开关同理,反过来。

 目录

思路图:

个人思路:

技术简单讲解:

Modbus-TCP

Modbus 从站-主站架构

遇到的问题:

Modbus_tcp端

网页服务器 (因为太多我分了两个去写)

html.c


思路图:

image.gif 编辑

个人思路:

1.首先应该实现一个基于Modbus实现对一个设备的数据采集以及控制,我这边是通过Modbus_tcp实现的。

2.因为要实现采集控制程序和网页服务器的通信我这边采用的是共享内粗你和消息队列,传输存储的数据就用共享内存,操作开关就用消息队列

3.写网页服务器和一个网页,需要让其两个可以正常通信,就是让网页服务器上的东西可以发到网页上

4.然后将采集的数据通过共享内存去分享给网页服务器,让网页服务器传给网页即可,控制开关同理,反过来。

技术简单讲解:

Modbus-TCP 和 Modbus 协议中的从站-主站架构是理解和实施工业自动化通信的关键概念。

Modbus-TCP

Modbus-TCP 是 Modbus 协议的一个变种,它将传统的 Modbus 协议帧直接封装在 TCP/IP 协议的数据部分,从而允许 Modbus 在以太网等网络环境中进行通信。与基于串行链路的传统 Modbus 相比,Modbus-TCP 提供了更高的数据传输速率、更远的传输距离以及更好的网络兼容性。它保留了 Modbus 的简单性和普遍性,同时利用了 TCP/IP 网络的基础设施,使得设备之间的通信更加灵活和高效。

Modbus 从站-主站架构

无论是 Modbus RTU、ASCII 还是 Modbus-TCP,Modbus 协议都遵循一种主-从(或称客户端-服务器)的通信模型:

  • 主站(Master):在 Modbus 网络中,主站通常是发起通信请求的一方,它可以请求从站的数据(读取寄存器或线圈状态)或向从站写入数据(设置寄存器或改变线圈状态)。一个 Modbus 网络中可以有一个或多个主站,但同一时刻只有一个主站能够主动发起通信。
  • 从站(Slave):从站是响应主站请求的设备,它们监听网络上的指令,并根据接收到的命令执行相应的操作,如返回存储在内部寄存器中的数据或执行某些动作。一个 Modbus 网络可以有多个从站,每个从站都有唯一的地址用于识别。

在 Modbus-TCP 中,这种架构依然保持不变,只是通信介质和封装方式变成了以太网和TCP/IP。主站通过TCP连接向特定IP地址和端口(通常是502端口)发送Modbus请求,从站则在该端口监听并响应这些请求。由于TCP/IP的面向连接特性,Modbus-TCP通信相比传统串行Modbus提供了更好的可靠性和错误处理能力。

遇到的问题:

1.在网页服务器给网页发信息的格式必须得是字符串类型的,否则不会显示在网页上,我还以为我共享内存里没数据-.-。

2.网页传过来的字符串都是会多带一个双引号,所以如果判断的时候式判断的字符串的话就要注意了,你判断的是字符串,他发来的是字符字符串。

3.消息队列的一个特点,没仔细去看,如果不给消息队列的类型赋值一个数的话是无法添加消息的。

Modbus_tcp端

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <modbus.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
struct shm *p;// 定义映射结构体指针
int msgid;// 定义消息队列ID
//共享内存结构体
struct shm
{
    uint16_t buf[128];
};
//消息队列结构体
struct mes
{
    long type;
    char buf[32];
} mes_cli;
// 读取寄存器数据函数
void *tcp_read(void *arg)
{
    modbus_t *tcp = (void *)arg;
    while (1)
    {
        sleep(1);
        int modbusfd = modbus_read_registers(tcp, 0, 5, p->buf);//直接存放在共享内存内
        if (modbusfd < 0)
        {
            perror("modbusfd error\n");
            return NULL;
        }
    }
}
void *tcp_write(void *arg)
{
    modbus_t *tcp = (void *)arg;
    while (1)
    {
        //读取消息队列的消息
        msgrcv(msgid, &mes_cli, sizeof(mes_cli) - sizeof(long), 0, 0);
        //对比执行相应的函数
        if (!strcmp(mes_cli.buf, "0 1"))
        {
            int bitfd = modbus_write_bit(tcp, 0, 1);
            if (bitfd < 0)
            {
                perror("bitfd error\n");
                return NULL;
            }
        }
        if (!strcmp(mes_cli.buf, "0 0"))
        {
            int bitfd = modbus_write_bit(tcp, 0, 0);
            if (bitfd < 0)
            {
                perror("bitfd error\n");
                return NULL;
            }
        }
        if (!strcmp(mes_cli.buf, "1 1"))
        {
            int bitfd = modbus_write_bit(tcp, 1, 1);
            if (bitfd < 0)
            {
                perror("bitfd error\n");
                return NULL;
            }
        }
        if (!strcmp(mes_cli.buf, "1 0"))
        {
            int bitfd = modbus_write_bit(tcp, 1, 0);
            if (bitfd < 0)
            {
                perror("bitfd error\n");
                return NULL;
            }
        }
    }
}
int main(int argc, char const *argv[])
{
    //1.创建实例
    modbus_t *tcp = modbus_new_tcp("192.168.3.4", 502);
    if (NULL == tcp)
    {
        perror("tcp error\n");
        return -1;
    }
    //2.设计从机ID
    if (modbus_set_slave(tcp, 1) < 0)
    {
        perror("set id error\n");
        return -1;
    }
    //3.建立链接
    if (modbus_connect(tcp) < 0)
    {
        perror("connect error\n");
        return -1;
    }
    //创建key值
    key_t key = ftok(".", 'a');
    if (key < 0)
    {
        perror("ftok error\n");
        return -1;
    }
    //创建或者打开共享内存
    int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0)
    {
        if (errno == 17)
        {
            shmid = shmget(key, 128, 0666);
        }
        else
        {
            perror("shmget error\n");
            return -1;
        }
    }
    printf("shmid : %d\n", shmid);
    // 创建或者打开消息队列
    msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msgid <= 0)
    {
        if (errno == 17)
        {
            msgid = msgget(key, 0666);
        }
        else
        {
            perror("msgid error\n");
            return -1;
        }
    }
    // 映射
    p = shmat(shmid, NULL, 0);
    if (p == (void *)-1)
    {
        perror("shmat error\n");
        return -1;
    }
    //4.建立线程
    pthread_t cli_read, cli_write;
    if (pthread_create(&cli_read, NULL, tcp_read, tcp) != 0)
    {
        perror("read creat error\n");
        return -1;
    }
    pthread_detach(cli_read);
    if (pthread_create(&cli_write, NULL, tcp_write, tcp) != 0)
    {
        perror("write creat error\n");
        return -1;
    }
    pthread_detach(cli_write);
    while (1)
        ;
    return 0;
}

image.gif

网页服务器 (因为太多我分了两个去写)

#include "thttpd.h"
#include <sys/types.h>
#include <sys/wait.h>
#include <modbus.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
static void *msg_request(void *arg)
{
  //这里客户端描述符通过参数传进来了
  int sock = (int)arg;
  // 一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。
  //但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。
  pthread_detach(pthread_self());
  //handler_msg作为所有的请求处理入口
  return (void *)handler_msg(sock);
}
int main(int argc, char *argv[])
{
  //如果不传递端口,那么使用默认端口80
  int port = 80;
  if (argc > 1)
  {
    port = atoi(argv[1]);
  }
  //初始化服务器
  int lis_sock = init_server(port);
  while (1)
  {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int sock = accept(lis_sock, (struct sockaddr *)&peer, &len);
    if (sock < 0)
    {
      perror("accept failed");
      continue;
    }
    //每次接收一个链接后,会自动创建一个线程,这实际上就是线程服务器模型的应用
    pthread_t tid;
    if (pthread_create(&tid, NULL, msg_request, (void *)sock) > 0)
    {
      perror("pthread_create failed");
      close(sock);
    }
  }
  return 0;
}

image.gif

#include "thttpd.h"
#include "custom_handle.h"
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
int init_server(int _port) //创建监听套接字
{
  int sock=socket(AF_INET,SOCK_STREAM,0);
  if(sock<0)
  {
    perror("socket failed");
    exit(2);
  }
  //设置地址重用
  int opt=1;                     
  setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
  struct sockaddr_in local;
  local.sin_family=AF_INET;
  local.sin_port=htons(_port);
  local.sin_addr.s_addr=INADDR_ANY;
  
  if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
  {
    perror("bind failed");
    exit(3);
  }
  if(listen(sock,5)<0)
  {
    perror("listen failed");
    exit(4);
  }
  return sock;
}
static int get_line(int sock,char* buf)   //按行读取请求报头
{
  char ch='\0';
  int i=0;
  ssize_t ret=0;
  while(i<SIZE && ch!='\n')
  {
    ret=recv(sock,&ch,1,0);
    if(ret>0&&ch=='\r')
    {
      ssize_t s=recv(sock,&ch,1,MSG_PEEK);
      if(s>0&&ch=='\n')
      {
        recv(sock,&ch,1,0);
      }
      else
      {
        ch='\n';
      }
    }
    buf[i++]=ch;
  }
  buf[i]='\0';
  return i;
}
static void clear_header(int sock)    //清空消息报头
{
  char buf[SIZE];
  int ret=0;
  do
  {
    ret=get_line(sock,buf);
  }while(ret!=1&&(strcmp(buf,"\n")!=0));
}
static void show_404(int sock)      //404错误处理
{
  clear_header(sock);
  char* msg="HTTP/1.0 404 Not Found\r\n";
  send(sock,msg,strlen(msg),0);         //发送状态行
  send(sock,"\r\n",strlen("\r\n"),0);      //发送空行
  struct stat st;
  stat("wwwroot/404.html",&st);
  int fd=open("wwwroot/404.html",O_RDONLY);
  sendfile(sock,fd,NULL,st.st_size);
  close(fd);
}
void echo_error(int sock,int err_code)    //错误处理
{
  switch(err_code)
  {
  case 403:
    break;
  case 404:
    show_404(sock);
    break;
  case 405:
    break;
  case 500:
    break;
  default:
    break;
  }
}
static int echo_www(int sock,const char * path,size_t s)  //处理非CGI的请求
{
  int fd=open(path,O_RDONLY);
  if(fd<0)
  {
    echo_error(sock,403);
    return 7;
  }
  char* msg="HTTP/1.0 200 OK\r\n";      // 代表请求成功的状态
  send(sock,msg,strlen(msg),0);         //发送状态行
  send(sock,"\r\n",strlen("\r\n"),0);      //发送空行
  
  //sendfile方法可以直接把文件发送到网络对端
  if(sendfile(sock,fd,NULL,s)<0)
  {
    echo_error(sock,500);
    return 8; 
  }
  close(fd);
  return 0;
}
static int handle_request(int sock,const char* method,\
    const char* path,const char* query_string)
{
  char line[SIZE];
  int ret=0;
  int content_len=-1;
  if(strcasecmp(method,"GET")==0)
  {
    //清空消息报头
    clear_header(sock);
    //!! 添加到一个字符传'
    
  }
  else
  {
    //获取post方法的参数大小
    do
    {
      ret=get_line(sock,line);
      if(strncasecmp(line,"content-length",14)==0)  //post的消息体记录正文长度的字段
      {
        content_len=atoi(line+16);  //求出正文的长度
      }
    }while(ret!=1&&(strcmp(line,"\n")!=0));
  }
  printf("method = %s\n", method);
  printf("query_string = %s\n", query_string);
  printf("content_len = %d\n", content_len);
  char req_buf[4096] = {0};
  //如果是POST方法,那么肯定携带请求数据,那么需要把数据解析出来
  if(strcasecmp(method,"POST")==0)
  {
    int len = recv(sock, req_buf, content_len, 0);
    printf("len = %d\n", len);
    printf("req_buf = %s\n", req_buf);
  }
  //先发送状态码
  char* msg="HTTP/1.1 200 OK\r\n\r\n";
  send(sock,msg,strlen(msg),0);
  //请求交给自定义代码来处理,这是业务逻辑
  parse_and_process(sock, query_string, req_buf);
  return 0;
}
int handler_msg(int sock)       //浏览器请求处理函数
{
  char del_buf[SIZE] = {};
  //通常recv()函数的最后一个参数为0,代表从缓冲区取走数据
  //而当为MSG_PEEK时代表只是查看数据,而不取走数据。
  recv(sock,del_buf,SIZE,MSG_PEEK);
#if 1 //初学者强烈建议打开这个开关,看看tcp实际请求的协议格式
  puts("---------------------------------------");
  printf("recv:%s\n",del_buf);
  puts("---------------------------------------");
#endif
  //接下来method方法判断之前的代码,可以不用重点关注
  //知道是处理字符串,把需要的信息过滤出来即可
  char buf[SIZE];
  int count=get_line(sock,buf);
  int ret=0;
  char method[32];
  char url[SIZE];
  char *query_string=NULL;
  int i=0;
  int j=0;
  int need_handle=0;
  //获取请求方法和请求路径
  while(j<count)
  {
    if(isspace(buf[j]))
    {
      break;
    }
    method[i]=buf[j]; 
    i++;
    j++;
  }
  method[i]='\0';
  while(isspace(buf[j])&&j<SIZE)      //过滤空格
  {
    j++;
  }
  //这里开始就开始判断发过来的请求是GET还是POST了
  if(strcasecmp(method,"POST")&&strcasecmp(method,"GET"))
  {
    printf("method failed\n");  //如果都不是,那么提示一下
    echo_error(sock,405);
    ret=5;
    goto end;
  }
  if(strcasecmp(method,"POST")==0)
  {
    need_handle=1;
  } 
  
  i=0;
  while(j<count)
  {
    if(isspace(buf[j]))
    {
      break;
    }
    if(buf[j]=='?')
    {
      //将资源路径(和附带数据,如果有的话)保存再url中,并且query_string指向附带数据
      query_string=&url[i];
      query_string++;
      url[i]='\0';
    }
    else{
      url[i]=buf[j];
    }
    j++;
    i++;
  }
  url[i]='\0';
  printf("query_string = %s\n", query_string);
  //浏览器通过http://192.168.8.208:8080/?test=1234这种形式请求
  //是携带参数的意思,那么就需要额外处理了
  if(strcasecmp(method,"GET")==0&&query_string!=NULL)
  {
    need_handle=1;
  }
  //我们把请求资源的路径固定为wwwroot/下的资源,这个自己可以改
  char path[SIZE];
  sprintf(path,"wwwroot%s",url);       
  
  printf("path = %s\n", path);
  
  //如果请求地址没有携带任何资源,那么默认返回index.html
  if(path[strlen(path)-1]=='/')              //判断浏览器请求的是不是目录
  {
    strcat(path,"index.html");       //如果请求的是目录,则就把该目录下的首页返回回去
  }
  //如果请求的资源不存在,就要返回传说中的404页面了
  struct stat st;   
  if(stat(path,&st)<0)            //获取客户端请求的资源的相关属性
  {
    printf("can't find file\n");
    echo_error(sock,404);
    ret=6;
    goto end;
  }
  //到这里基本就能确定是否需要自己的程序来处理后续请求了
  printf("need progress handle:%d\n",need_handle);
  //如果是POST请求或者带参数的GET请求,就需要我们自己来处理了
  //这些是业务逻辑,所以需要我们自己写代码来决定怎么处理
  if(need_handle)
  {
    ret=handle_request(sock,method,path,query_string);
  }
  else
  {
    clear_header(sock);
    //如果是GET方法,而且没有参数,则直接返回资源 
    ret=echo_www(sock,path,st.st_size);  
  }
  
end:
  close(sock);
  return ret;
}

image.gif

html.c

<!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>
    <style>
        button {
            background-color: dodgerblue;
            color: white;
            width: 300px;
            height: 30px;
            border: 0;
            font-size: 16px;
            border-radius: 30px;
            margin-top: 10px;
        }
        .content {
            height: 400px;
            width: 800px;
            background-color: silver;
            text-align: center;
            border: 2px solid;
            margin: auto;
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
        }
        input {
            line-height: 25%;
        }
    </style>
    <script>
        function get_info() {
            // 获取name为username1的标签,赋值给v
            var v = document.getElementsByName("username1");
            var xhr = new XMLHttpRequest();
            var url = "";
            xhr.open("post", url, true);
            // 如果状态正确的话
            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    var response = xhr.responseText;
                    // split是根据空格来来传给v1这个数组
                    var v1 = response.split(' ');
                    v[0].value = v1[0];//+赋值
                    v[1].value = v1[1];
                    v[2].value = v1[2];
                    v[3].value = v1[3];
                    v[4].value = v1[4];
                }
            };
            xhr.send("modbus_get=");//发送内容
        }
        // 传参ID 
        function set_into(obj) {
            var v = document.getElementsByName("username1");
            var xhr = new XMLHttpRequest();
            var url = "";
            xhr.open("post", url, true);
            //判断ID是哪个函数的
            if (obj == "0 1") {
                xhr.send("\"modbus_set=0 1\"");
            }
            else if (obj == "0 0") {
                xhr.send("\"modbus_set=0 0\"");
            }
            else if (obj == "1 1") {
                xhr.send("\"modbus_set=1 1\"");
            }
            else if (obj == "1 0") {
                xhr.send("\"modbus_set=1 0\"");
            }
        }
    </script>
</head>
<body>
    <div class="content">
        <h3>信息采集<h3>
                光照强度<input type="text" id="data1" placeholder="data1" name="username1"><br />
                加速度1<input type="text" id="data2" placeholder="data2" name="username1"><br />
                加速度2<input type="text" id="data3" placeholder="data3" name="username1"><br />
                加速度3<input type="text" id="data4" placeholder="data4" name="username1"><br />
                加速度4<input type="text" id="data5" placeholder="data5" name="username1"><br />
                <button name="flash" onclick="get_info()">控制器开关</button>
                <h3>机器开关<h3>
                        led灯:<input type="radio" id="0 1" name="led" onclick="set_into(id)"> ON
                        <input type="radio" id="0 0" name="led" onclick="set_into(id)"> OFF
                        <br />
                        <br />
                        蜂鸣器:<input type="radio" id="1 1" name="feng" onclick="set_into(id)"> ON
                        <input type="radio" id="1 0" name="feng" onclick="set_into(id)"> OFF
    </div>
    <body>
</html>

image.gif


相关文章
工程监测无线中继采集仪的参数读写MODBUS协议
无线中继采集仪支持基于地址的 MODBUS 协议、自定义的 AAB/B 协议以及字符串指令集协议,使用这些通讯协议可对寄存器(参数)进行访问。
工程监测无线中继采集仪的参数读写MODBUS协议
|
物联网
工程监测无线中继采集仪的寄存器(参数)汇总详解
无线中继采集发送仪有很多参数(寄存器),对于一些简单的应用,用户无需关心这些参数,使用默认参数值即可。仅列出较为常用的参数,当需要配置设备完成复杂、特殊的应用时,请查看“无线中继采集发送仪寄存器汇总说明” 。
工程监测无线中继采集仪的寄存器(参数)汇总详解
|
存储 传感器 物联网
工程监测无线中继采集仪的常用功能与设置
设备地址设置 在同一区域内,不得有重复地址的 无线中继采集发送仪,故此需要使用参数配置工具为每一台无线中继采集发送仪 修改为不同的地址。
工程监测无线中继采集仪的常用功能与设置
|
传感器 存储
手持VH501TC多功能混合传感器信号采集读数仪使用方法
电池仓位于设备背面下半部分, 仅当使用 5 号电池供电时需要操作电池仓,锂电池供电的设备无需操作电池仓。默认情况下,电池仓盖处于锁定状态无法直接打开,在需要安装或者更换电池时,应将水平拨动开关推至解锁侧,在电池安装完成后必须将开关推至锁定侧。注意:在安装电池时必须按照仓内+/-符号对应电池的正/负极,错误的安装极性会永久性损坏设备。
手持VH501TC多功能混合传感器信号采集读数仪使用方法
LabVIEW控制Arduino采集光敏电阻数值(基础篇—14)
利用光敏电阻和LIAT中的光敏函数库,通过Arduino Uno控制板的模拟端口采集与光敏电阻串联电阻的分压值上传给LabVIEW软件,并除以光照系数以获得光照值,实现一个光强计的功能。
|
存储 传感器
多功能手持VH501TC混合信号采集仪如何处理监测数据
在实时数据显示窗口, 长按【存储】按键即可保存当前显示的传感数据,当听到蜂鸣器提示后表示存储完成,同时屏幕底部的已保存数量值自动加 1。
多功能手持VH501TC混合信号采集仪如何处理监测数据
|
传感器 物联网
工程监测无线中继采集发送仪 指示灯功能说明及接口定义
工程监测NLM5无线中继采集发送仪 指示灯功能说明及接口定义
|
传感器 存储 物联网
手持VH501TC多功能混合信号采集仪接口说明
传感器接口 传感器接口须使用设备专门配备的测线,一端为 DB9 或者航空插头,另一端为用颜色区分的多个鳄鱼夹,线(鳄鱼夹)颜色和功能定义详见前述“设备组成和接口定义” 。
手持VH501TC多功能混合信号采集仪接口说明
树莓派控制继电器开关样例源码
树莓派控制继电器开关样例源码
115 0
树莓派控制继电器开关样例源码
|
传感器 移动开发 数据格式
振弦采集模块主动上传测量数据( UART)
默认情况下 VMXXX 模块总是以从机身份与主机完成数据交互, 在这种主从结构中, VMXXX 从不主动上传数据, 可通过修改自动上传寄存器( ATSD_SEL)来实现模块主动输出测量数据功能,ATSD_SEL 寄存器的每 1 位对应了一种数据类型
振弦采集模块主动上传测量数据( UART)

热门文章

最新文章

下一篇
DataWorks