最近了解WEB大屏显示。一般像嵌入式这类的,MQTT协议会走的多一些,走订阅和发布的策略,网上走了一圈之后,目前有几个实现方案。
这里对比一下几个物联网协议,相对而言MQTT更合适物联网,其它几个协议不是干这个的,不过我推荐一下DDS,这玩意还挺好用的。
(ps:最近了解到一个团队的实现方案是tcp。。。什么魔鬼设计,想的啥呢)
大屏实现方案
1.买物联网网关,附带WEB大屏服务,这里就不打广告了,自己找一圈,蛮多有支持的。
优势是,有些基础服务是免费的,硬件是现成的,有详细的指导文件。
缺点是,高级一些的服务是要收费的,不合适大批量生产,毕竟从底层硬件到前端程序都是人家的,要找到契合自己的网关也麻烦,比如我遇到的几个,都不支持MQTT协议接入。还有就是,数据都得去人家服务器,多少有点膈应。
2.买云服务器,比如阿里云的可视化数据服务,我整了个15天试用。
优点是,数据都是自己的。
缺点是,要买一堆的配套服务,比如协议转换,服务器啥的,死贵死贵的。
3.技术牛皮,那就自己搭建服务器,然后写前端后台嵌入式,我能搞,但这玩意费劲,要时间,而且中间肯定有那么两个环节自己得摸索一段时间。
优点,啥都是自己的,可定制化程度高,省钱,只要有个服务器就行。
缺点,费时费力。
4.找个低代码平台,看了一下,网上这种公司有,只是要掏钱
优点,省时省力,不花多少钱。
缺点,难找合适的。
推荐方案
对比一圈之后,还是选择了低代码平台,仔细想想,我是干嵌入式的,后台前端这些,对我来说是行业外的东西,可以去了解,但不需要深入,关键省钱就行。
私有部署 - 使用教程-免费低代码数据可视化平台-触达云屏 (topthink.com)
上面的方案个人觉得蛮合适,哔哩哔哩有教程,作者有个群,现在对大家都有技术支持,可以部署到局域网,重点是对个人免费,我觉得不花钱就可以打败一切了,这些都是他的资料。
服务器搭建
这部分其实是抄官方资料了,自己可以去看,我就抄linux搭建部分
全新安装说明 | 免费低代码数据可视化平台-触达云屏 (chudayun.com)
1,基础环境安装
安装以下基础环境(参考各官网)
Nginx
Mysql 5.7+
2,创建/导入数据库
初始化数据库:创建数据库 chudy_data_visual,执行 chudy_visual_[版本号] /doc 目录下的 初始化SQL文件
3,创建安装目录
分别执行以下三行命令,创建安装目录
cd /
mkdir app
mkdir app/java
4,部署安装
上传以下文件夹到服务器 /app 目录下
chudy_visual_[版本号]
filesystem
chudy_designer
上传 jdk-8u221-linux-x64.tar.gz文件 到服务器 /app/java目录下,解压。
cd /app/java
tar -zxvf jdk-8u221-linux-x64.tar.gz
修改数据库连接配置文件:
/app/chudy_visual_[版本号] /mgr/config 目录下 application-dev.properties
修改 数据库端口、名称、帐号、密码 等信息。
5,配置nginx
上传 chudy_designer 目录下的 chudy_visual.conf 文件到 服务器 nginx的 conf.d 目录下(一般在 /etc/nginx/conf.d/)
重载Nginx配置使配置生效。
防火墙中配置,放行端口 18088
6,启动应用
cd /app/chudy_visual_[版本号]/mgr/
./mgr.sh start
tail -f visual_cms.out
mgr.sh 支持启动、停止、重启、查看状态
./mgr.sh start | stop | restart | status
7,访问系统
http://IP:18088/
初始帐号 sadmin 密码 111111
设备端实现
目前测试,我是找了个温湿度传感器在折腾,实现方案是
传感器->C#上位机->MQTT协议->云屏
这里放出C#的代码(ps:反正是测试玩的,不重要)
代码
using System;
using System.Text;
using System.Windows.Forms;
using System.IO.Ports;
using System.Net;
using System.Net.Sockets;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Protocol;
using System.Threading.Tasks;
using System.Threading;
namespace 开发快上位机
{
public partial class Form1 : Form
{
public static IMqttClient _mqttClient;
byte[] uart_data = new byte[100];
byte uart_addr = 0;
byte uart_start = 0;
byte uart_time = 0;
float wendu, shidu;
public Form1()
{
InitializeComponent();
System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
}
private void Form1_Load(object sender, EventArgs e)
{
button8.Enabled = false;
serialPort1.DataReceived += new SerialDataReceivedEventHandler(port_DataReceived);//必须手动添加事件处理程序
}
private void port_DataReceived(object sender, SerialDataReceivedEventArgs e)//串口数据接收事件
{
try
{
uart_start = 1;
uart_time = 0;
int ilen = serialPort1.BytesToRead;
byte[] bytes = new byte[ilen];
serialPort1.Read(bytes, 0, ilen);
for (int i = 0; i < ilen; i++)
{
uart_data[uart_addr] = bytes[i];
uart_addr++;
}
/* if (!radioButton6.Checked)//如果接收模式为字符模式
{
int ilen = serialPort1.BytesToRead;
byte[] bytes = new byte[ilen];
serialPort1.Read(bytes, 0, ilen);
//string str = System.Text.Encoding.Default.GetString(bytes); //xx="中文";
//textBox1.AppendText(str);//添加内容
}
else
{ //如果接收模式为数值接收
byte data;
data = (byte)serialPort1.ReadByte();//此处需要强制类型转换,将(int)类型数据转换为(byte类型数据,不必考虑是否会丢失数据
//string str = Convert.ToString(data, 16).ToUpper();//转换为大写十六进制字符串
//textBox1.AppendText("0x" + (str.Length == 1 ? "0" + str : str) + " ");//空位补“0”
}
*/
}
catch
{
textBox1.AppendText("串口数据接收出错,请检查!\r\n");
}
}
private void button1_Click(object sender, EventArgs e)
{
if (button1.Text == "串口连接")
{
try
{
serialPort1.PortName = comboBox1.Text;
serialPort1.BaudRate = Convert.ToInt32(comboBox2.Text);
serialPort1.Open();
button1.Text = "断开连接";
button2.Enabled = false;
panel2.Enabled = false;
comboBox1.Enabled = false;
comboBox2.Enabled = false;
comboBox3.Enabled = false;
comboBox4.Enabled = false;
textBox1.AppendText("串口已连接\r\n");
}
catch
{
if (serialPort1.IsOpen)
serialPort1.Close();
button1.Text = "串口连接";
button2.Enabled = true;
comboBox1.Enabled = true;
panel2.Enabled = true;
comboBox2.Enabled = true;
comboBox3.Enabled = true;
comboBox4.Enabled = true;
textBox1.AppendText("请检查串口连接\r\n");
}
}
else if (button1.Text == "断开连接")
{
try
{
serialPort1.Close();
button1.Text = "串口连接";
button2.Enabled = true;
comboBox1.Enabled = true;
comboBox2.Enabled = true;
comboBox3.Enabled = true;
panel2.Enabled = true;
comboBox4.Enabled = true;
textBox1.AppendText("串口已断开\r\n");
}
catch { }
}
}
private void SearchAndAddSerialToComboBox(SerialPort MyPort, ComboBox MyBox)
{ //将可用端口号添加到ComboBox
string Buffer; //缓存
string[] MyString = new string[Convert.ToInt32(comboBox3.Text)]; //最多容纳20个,太多会影响调试效率
MyBox.Items.Clear(); //清空ComboBox内容
for (int i = 1; i < Convert.ToInt32(comboBox3.Text); i++) //循环
{
try
{ //核心原理是依靠try和catch完成遍历
progressBar1.Value = i * (100 / Convert.ToInt32(comboBox3.Text));
Buffer = "COM" + i.ToString();
MyPort.PortName = Buffer;
MyPort.Open(); //如果失败,后面的代码不会执行
MyString[i - 1] = Buffer;
MyBox.Items.Add(Buffer); //打开成功,添加至下俩列表
comboBox1.Text = Buffer.ToString();
MyPort.Close(); //关闭
}
catch { }
}
}
private void button2_Click(object sender, EventArgs e)
{
textBox1.AppendText("开始自动配置串口\r\n");//出错提示
textBox1.AppendText("串口扫描\r\n");//出错提示
SearchAndAddSerialToComboBox(serialPort1, comboBox1); //扫描并讲课用串口添加至下拉列表
textBox1.AppendText("端口扫描完毕\r\n");//出错提示
textBox1.AppendText("正在配置波特率\r\n");//出错提示
comboBox2.Text = comboBox4.Text;
serialPort1.BaudRate = Convert.ToInt32(comboBox2.Text);
progressBar1.Value = 0;
textBox1.AppendText("自动配置完成\r\n");//出错提示
button1_Click(sender, e);
}
private void button3_Click(object sender, EventArgs e)
{
textBox1.Clear();
}
private void button5_Click(object sender, EventArgs e)
{
textBox3.Clear();
}
private void button4_Click(object sender, EventArgs e)
{
System.IO.File.WriteAllText(@"C:\Users\Administrator\Desktop\发送区数据.txt", textBox1.Text);
System.IO.File.WriteAllText(@"C:\Users\Administrator\Desktop\接收区数据.txt", textBox3.Text);
textBox1.AppendText("数据保存完成!\r\n");
}
void uart_send(object sender, EventArgs e,string data)
{
byte[] Data = new byte[1];//作用同上集
if (serialPort1.IsOpen)//判断串口是否打开,如果打开执行下一步操作
{
try
{
if (data != "")
{
if (!radioButton6.Checked)//如果发送模式是字符模式
{
try
{//实现串口发送汉字
Encoding gb = System.Text.Encoding.GetEncoding("gb2312");
byte[] bytes = gb.GetBytes(data);
serialPort1.Write(bytes, 0, bytes.Length);
}
catch
{
textBox1.AppendText("串口数据写入错误\r\n");//出错提示
serialPort1.Close();
button1_Click(sender, e);
}
}
else
{
for (int i = 0; i < (data.Length - data.Length % 2) / 2; i++)//取余3运算作用是防止用户输入的字符为奇数个
{
Data[0] = Convert.ToByte(data.Substring(i * 2, 2), 16);
serialPort1.Write(Data, 0, 1);//循环发送(如果输入字符为0A0BB,则只发送0A,0B)
}
if (data.Length % 2 != 0)//剩下一位单独处理
{
Data[0] = Convert.ToByte(data.Substring(data.Length - 1, 1), 16);//单独发送B(0B)
serialPort1.Write(Data, 0, 1);//发送
}
}
}
}
catch
{
textBox1.AppendText("串口数据写入错误\r\n");//出错提示
}
}
}
private void button6_Click(object sender, EventArgs e)
{
uart_send(sender,e, textBox3.Text);
}
private void button9_Click(object sender, EventArgs e)
{
var optionsBuilder = new MqttClientOptionsBuilder()
.WithTcpServer(textBox2.Text, Convert.ToInt16(textBox4.Text)) // 要访问的mqtt服务端的 ip 和 端口号
.WithCredentials("admin", "123456") // 要访问的mqtt服务端的用户名和密码
.WithClientId("testclient02") // 设置客户端id
.WithCleanSession()
.WithTls(new MqttClientOptionsBuilderTlsParameters
{
UseTls = false // 是否使用 tls加密
});
var clientOptions = optionsBuilder.Build();
_mqttClient = new MqttFactory().CreateMqttClient();
_mqttClient.ConnectedAsync += _mqttClient_ConnectedAsync; // 客户端连接成功事件
_mqttClient.DisconnectedAsync += _mqttClient_DisconnectedAsync; // 客户端连接关闭事件
_mqttClient.ApplicationMessageReceivedAsync += _mqttClient_ApplicationMessageReceivedAsync; // 收到消息事件
_mqttClient.ConnectAsync(clientOptions);
}
/// <summary>
/// 客户端连接关闭事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task _mqttClient_DisconnectedAsync(MqttClientDisconnectedEventArgs arg)
{
button8.Enabled = false;
button9.Text = "连接";
textBox6.AppendText($"客户端已断开与服务端的连接……\n");
return Task.CompletedTask;
}
/// <summary>
/// 客户端连接成功事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task _mqttClient_ConnectedAsync(MqttClientConnectedEventArgs arg)
{
button8.Enabled = Enabled;
button9.Text = "断开";
textBox6.AppendText($"客户端已连接服务端……\n");
// 订阅消息主题
// MqttQualityOfServiceLevel: (QoS): 0 最多一次,接收者不确认收到消息,并且消息不被发送者存储和重新发送提供与底层 TCP 协议相同的保证。
// 1: 保证一条消息至少有一次会传递给接收方。发送方存储消息,直到它从接收方收到确认收到消息的数据包。一条消息可以多次发送或传递。
// 2: 保证每条消息仅由预期的收件人接收一次。级别2是最安全和最慢的服务质量级别,保证由发送方和接收方之间的至少两个请求/响应(四次握手)。
_mqttClient.SubscribeAsync("sub", MqttQualityOfServiceLevel.AtLeastOnce);
return Task.CompletedTask;
}
/// <summary>
/// 收到消息事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task _mqttClient_ApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg)
{
textBox6.AppendText($"Topic主题=【{arg.ApplicationMessage.Topic}】 消息={Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}");
string str = Encoding.UTF8.GetString(arg.ApplicationMessage.Payload);
if (str.Contains("on") || str.Contains("off"))
{
textBox6.AppendText("\r\n控制继电器\r\n");//出错提示
byte[] Data = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xc4, 0x0b };//作用同上集
if (serialPort1.IsOpen)//判断串口是否打开,如果打开执行下一步操作
{
try
{
serialPort1.Write(Data, 0, 8);//循环发送(如果输入字符为0A0BB,则只发送0A,0B)
}
catch
{
textBox1.AppendText("串口数据写入错误\r\n");//出错提示
}
}
}
//textBox6.AppendText($"ApplicationMessageReceivedAsync:客户端ID=【{arg.ClientId}】接收到消息。 Topic主题=【{arg.ApplicationMessage.Topic}】 消息=【{Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}】 qos等级=【{arg.ApplicationMessage.QualityOfServiceLevel}】");
return Task.CompletedTask;
}
public void Publish(string topic,string data)
{
var message = new MqttApplicationMessage
{
Topic = topic,
Payload = Encoding.Default.GetBytes(data),
QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce,
Retain = true // 服务端是否保留消息。true为保留,如果有新的订阅者连接,就会立马收到该消息。
};
_mqttClient.PublishAsync(message);
}
private void button8_Click(object sender, EventArgs e)
{
if(textBox8.Text != "")
Publish(textBox7.Text,textBox8.Text);
}
private void button7_Click(object sender, EventArgs e)
{
textBox6.Text = "";
textBox8.Text = "";
}
private void button10_Click(object sender, EventArgs e)
{
textBox6.AppendText("\r\n读取温度\r\n");//出错提示
byte[] Data = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xc4, 0x0b };//作用同上集
if (serialPort1.IsOpen)//判断串口是否打开,如果打开执行下一步操作
{
try
{
serialPort1.Write(Data, 0, 8);//循环发送(如果输入字符为0A0BB,则只发送0A,0B)
}
catch
{
textBox1.AppendText("串口数据写入错误\r\n");//出错提示
}
}
}
string zhongliang1 = "{\"value\":";
string zhongliang2 = ",\"unit\": \"Kg\",\"min\": 0,\"max\": 100,\"label\": \"11\"}";
string wenshidu1 = "[{\"extObj\": {},\"value\": \"";
string wenshidu2 = "\",\"prefixText\": \"\",\"suffixText\": \"\",\"descText\": \"\",\"backgroundColor\": \"\",\"icon\": \"\",\"color\": \"\"}]";
private void button11_Click(object sender, EventArgs e)
{
string data;
data = wenshidu1 + textBox9.Text + wenshidu2;
Publish("wendu", data);
}
private void button12_Click(object sender, EventArgs e)
{
string data;
data = wenshidu1 + textBox10.Text + wenshidu2;
Publish("shidu", data);
}
private void button13_Click(object sender, EventArgs e)
{
string data;
data = zhongliang1 + textBox11.Text + zhongliang2;
Publish("zhongliang", data);
}
private void timer1_Tick(object sender, EventArgs e)
{
if (uart_start == 1)
{
uart_time++;
if (uart_time >= 100)
{
textBox1.AppendText(uart_addr.ToString());//空位补“0”
for (int i = 0; i < uart_addr; i++)
{
string str = Convert.ToString(uart_data[i], 16).ToUpper();//转换为大写十六进制字符串
textBox1.AppendText("0x" + (str.Length == 1 ? "0" + str : str) + " ");//空位补“0”
}
if (uart_data[0] == 0x01 && uart_data[1] == 0x03 && uart_data[2] == 0x04)
{
shidu = uart_data[3] << 8 | uart_data[4];
wendu = uart_data[5] << 8 | uart_data[6];
shidu = shidu / 10;
wendu = wendu / 10;
string str1,str2;
textBox9.Text = wendu.ToString();
textBox10.Text = shidu.ToString();
button11_Click(sender, e);
button12_Click(sender, e);
}
uart_start = 0;
uart_time = 0;
uart_addr = 0;
}
}
}
}
}
测试效果
简化操作,直接搞了几个按键去发
当然实际项目不会这么干,实际项目的构成应该是
单片机接传感器,以MODBUS/无线等方式提供数据访问接口
linux网关实现数据转发,比如NXP的IMX6,上面实现MQTT客户端
云服务器做MQTT服务器,实现数据转发
WEB云屏/微信小程序/桌面应用等订阅MQTT数据
上面是我的微信和QQ群,欢迎新朋友的加入。