基于 i.MX6ULL-Linux的智能家居项目(Qt)

基于 i.MX6ULL-Linux的智能家居项目(Qt)

H_Haozi Lv2

A 前情提要

这里是基于我之前构建的linux系统和写的驱动为基础的,如果只是为了学习QT,大家当然也可以根据正点原子手册里的教程使用出厂系统来编写驱动,会更加简单

这里主要使用到的内容是:

  • 嵌入式QT的编译运行环境

  • 支持QT的根文件系统

  • 触摸屏驱动(gt911)

  • 提供好读写接口的 LED 蜂鸣器 ADC 驱动

  • 一个云服务器(主要来挂载html网页和py后端程序,如果再本地部署当然也可以)

这里先贴一个最终效果图:

这里默认除了QT界面和前后端代码以外的驱动及环境已经OK了,并且有相关基础~
完整的代码会贴在最后部分……

B QT相关

B.1 图形化配置基本控件

这里的布局相对简单,所以直接使用QT的图形化ui进行布局:
大致的样子如下: 有三个button,其它都是label标签,然后分别对每个标签进行命名,方便后面进行操作

B.2 控件逻辑相关

在这个界面,主要是要实现这三个按钮和一个更新温度的逻辑: 开关LED 开关蜂鸣器 连接/断开网络 更新温度

这里先在头文件的添加4个槽函数相关flag变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*部分代码*/
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
bool led1_flag;
bool buzzer_flag;
bool tcp_flag;

private slots:
bool Set_LED1(bool flag); //设置LED
bool Set_Buzzer(bool flag); //设置蜂鸣器
bool Temperature_Update(void); //更新温度
bool Set_Tcp_Server(bool flag); //连接或断开服务器连接

这四个函数就可以完成整体的逻辑,这里使用 Lambda 表达式来实现槽函数,下面给出一个控制LED的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MainWindow::led1_flag = false; //默认关闭
//当按钮按下,执行Set_LED1() 并且切换按钮的文字,如果设置失败,提示error
connect(ui->pushbutton_led1, &QPushButton::clicked, this, [this]()
{
bool newState = !MainWindow::led1_flag;
if (Set_LED1(newState))
{
MainWindow::led1_flag = newState;
ui->pushbutton_led1->setText(newState ? "关闭LED1" : "打开LED1");
} else
{
QMessageBox::critical(this, QStringLiteral("错误"), QStringLiteral("设置 LED1 失败"));
}
});

/*......*/

同理蜂鸣器和TCP的连接和断开也可以这么实现

然后是 更新温度 相关,因为温度并不是按键获取,而是ADC内部获取,所以这里选择使用定时器,3s自动获取一次ADC值然后更新出来(因为这是一个模拟项目,所以我们只获取转化为电压,而不是转化为温度)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*......*/
timer = new QTimer(this); //注意在头文件定义一个 QTimer timer;
/*......*/
timer->start(3000); //启动定时器3000ms后触发信号

connect(timer,&QTimer::timeout,this,[=](){
if ( !Temperature_Update() )
{
ui->label_temp->setText("获取失败");
}
else
{
//这是后面网络相关的,即如果tcp已经连接,就将更新的温度值发送给服务器处理
if(MainWindow::tcp_flag == true)
{
QString tempData = ui->label_temp->text();
QByteArray out = tempData.toUtf8();
tcpSocket->write(out);
tcpSocket->flush();
}
}
});

最后将一个槽函数空实现(return true),运行调试就可以得到一个基础的界面

B.3 读写imx6u外设相关

这里来实现控制LED,蜂鸣器等,Linux一切皆文件,所以控制这些外设也是通过读写文件来完成,这里不涉及LED等的驱动编写,主要介绍如何读写

首先还是以LED为例子,下面是部分LED驱动的部分代码(使用的字符设备驱动,如果是出厂系统使用的是led子系统,读写路径不同):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*......*/

//这里是驱动给外界提供的写接口,我们可以通过C库函数的write来写文件
static ssize_t gpioled_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos)
{
struct gpio_dev *dev = (struct gpio_dev *)filp->private_data;
int retvalue;
unsigned char databuf[1];
retvalue = copy_from_user(databuf,buf,count);
if(retvalue < 0)
{
printk("kernel write failed!\r\n");
return -EFAULT;
}
//判断开灯关灯
led_switch(databuf[0]);
return 0;
}

/*......*/

//自动创建设备节点,然后我们就会在linux下的 /dev/GPIOLED_NAME 的位置操作文件来控制外设
gpioled.class = class_create(THIS_MODULE,GPIOLED_NAME);
if(IS_ERR(gpioled.class))
{
ret = PTR_ERR(gpioled.class);
goto fail_class;
}
gpioled.device = device_create(gpioled.class,NULL,gpioled.devid,NULL,GPIOLED_NAME);
if(IS_ERR(gpioled.device))
{
ret = PTR_ERR(gpioled.device);
goto fail_device;
}

/*......*/

void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LED_ON)
{
gpio_set_value(gpioled.led_gpio,0); //低电平点亮
}
else if(sta == LED_OFF)
{
gpio_set_value(gpioled.led_gpio,1);
}
}

/*......*/

通过驱动提供的接口,我们就可以在linux下对 /dev/GPIOLED_NAME 文件写入一个 LED_ON ,那么灯就会打开,反之输入 LED_OFF 就会关闭

注意GPIOLED_NAME等都是宏,需要你自己在代码中定义设置

所以在QT下我们使用QFile来读写文件,其实是非常简单的,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool MainWindow::Set_LED1(bool flag)
{
bool ret = true;
QFile file("/dev/user_gpio_led");

// 以写入方式打开设备文件
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qDebug() << "无法打开LED设备文件";
return false;
}

// 根据flag值写入对应的控制信号
QTextStream out(&file);
if (flag) {
out << "1"; // 打开LED
} else {
out << "0"; // 关闭LED
}

file.close();
return ret;
}

同理蜂鸣器也是这样写的,就不列出来了,最后是ADC模块,首先这里使用的ADC的驱动是linux内核自带的,IIO子系统相关,我只是配置了设备树,然后加载相关驱动就可以使用

/sys/bus/iio/devices 里面会有一个设备 iio:device0,进入该目录,会看到很多文件,比如in_voltage0_raw in_voltage1_raw in_voltage_scale等,其中in_voltage1_raw 是ADC1 通道 1 原始值文件,in_voltage_scale是ADC1 比例文件(分辨率),单位为 mV。实际电压值(mV)=in_voltage1_raw,通过用cat读取这两个文件的值,然后相乘,就可以得到ADC读取到的电压值,同理在QT里面也是如此读
但这里涉及到了一个小问题,就是读取该文件之后,其实当前的路径只是一个链接,如果直接使用,可能会失效,所以这里可以使用pwd来查看一下实际的路径,比如我这里的路径就是:/sys/devices/platform/soc/2100000.aips-bus/2198000.adc/iio:device0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
bool MainWindow::Temperature_Update(void)
{
bool ret = true;

// 使用完整的设备路径
QString basePath = "/sys/devices/platform/soc/2100000.aips-bus/2198000.adc/iio:device0";
QFile rawFile(basePath + "/in_voltage1_raw");
QFile scaleFile(basePath + "/in_voltage_scale");

// 读取原始电压值
if (!rawFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "无法打开原始电压文件:" << rawFile.errorString();
return false;
}
QString rawStr = QTextStream(&rawFile).readLine().trimmed();
rawFile.close();

// 读取电压缩放比例
if (!scaleFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "无法打开电压缩放文件:" << scaleFile.errorString();
return false;
}
QString scaleStr = QTextStream(&scaleFile).readLine().trimmed();
scaleFile.close();

// 转换为数值
bool ok1, ok2;
double rawValue = rawStr.toDouble(&ok1);
double scaleValue = scaleStr.toDouble(&ok2);

if (!ok1 || !ok2) {
qDebug() << "数据转换错误: raw=" << rawStr << "scale=" << scaleStr;
return false;
}

// 计算实际电压值(单位:毫伏 mV)
double voltage_mV = rawValue * scaleValue;
double voltage_V = voltage_mV / 1000.0; // 转换为伏特 V

qDebug() << "Debug Info - Raw:" << rawValue << "Scale:" << scaleValue << "Voltage:" << voltage_mV << "mV";

//保留更多小数位方便观察
QString temperatureText = QString(" %1 V").arg(voltage_V, 0, 'f', 6);
ui->label_temp->setText(temperatureText);

return ret;
}

读取文件后简单计算就可以获得对应的电压值了,通过以上内容,基本可以实现在触摸屏上来控制LED和蜂鸣器,以及显示温度,这里就可以进行第二步调试,如果有问题需要检查led等驱动和qt的读写文件是不是有问题,下面就是物联网现相关的代码了……

B.4 物联网相关

在实现上面的基础控制之后,就可以来添加物联网功能了,首先这里假设服务器已经搭建完毕,我们只用负责去连接,这里选择TCP连接,也是使用QT的QTcpSocket等互联网相关库,注意在pro文件中添加QT += core gui network

这里还是主要实现几个函数: 连接/断开服务器获取本地IP,以及处理TCP接收到的指令
首先先看一下如何获取本地IP,这个ip地址要在后面连接的时候使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
QString MainWindow::getLocalHostIP(void)
{
QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();
QString localIP;

// 遍历所有IP地址,优先选择IPv4且非回环地址
for (int i = 0; i < ipAddressesList.size(); ++i)
{
QHostAddress ipAddr = ipAddressesList.at(i);
// 筛选条件:IPv4协议、不是本地回环地址、不是空地址
if (ipAddr.protocol() == QAbstractSocket::IPv4Protocol &&
ipAddr != QHostAddress::Null &&
ipAddr != QHostAddress::LocalHost &&
!ipAddr.toString().startsWith("127.0.") &&
!ipAddr.toString().startsWith("169."))
{ // 排除APIPA地址
localIP = ipAddr.toString();
break;
}
}
// 如果没有找到合适的IP,使用本地回环地址作为备选
if (localIP.isEmpty())
{
localIP = "127.0.0.1";
qDebug() << "Warning: Using loopback address, no valid IPv4 address found";
}

qDebug() << "Local IP Address:" << localIP;
return localIP;
}

然后就是连接远程服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
bool MainWindow::Set_Tcp_Server(bool flag)
{
bool ret = false;
if(flag)
{
// 连接服务器操作
if(tcpSocket->state() == QAbstractSocket::ConnectedState) {
ui->label_tcp_state->setText("已连接");
qDebug() << "TCP: Already connected";
return true;
}
if(tcpSocket->state() == QAbstractSocket::ConnectingState) {
ui->label_tcp_state->setText("正在连接中,请稍候...");
qDebug() << "TCP: Connection in progress";
return false;
}

// 获取本地IP并显示
QString localIP = getLocalHostIP();
qDebug() << "Local IP:" << localIP;

QString serverIP = "xxx.xxx.xxx.xxx"; // 自己的服务器IP
quint16 serverPort = "xxx"; // 自己的服务器端口

// 连接信号槽
connect(tcpSocket, &QTcpSocket::connected, this, [this]() {
qDebug() << "TCP: Connected to server";
ui->label_tcp_state->setText("已连接");
ui->pushbutton_tcp->setText("断开远程服务器");
});

connect(tcpSocket, &QTcpSocket::disconnected, this, [this]() {
qDebug() << "TCP: Disconnected from server";
ui->label_tcp_state->setText("未连接");
ui->pushbutton_tcp->setText("连接远程服务器");
MainWindow::tcp_flag = false;
});

// 开始连接
ui->label_tcp_state->setText("正在连接...");
tcpSocket->connectToHost(serverIP, serverPort);

// 等待连接完成(最多等待5秒)
if(tcpSocket->waitForConnected(5000)) {
ret = true;
qDebug() << "TCP: Connection established successfully";
} else {
qDebug() << "TCP: Connection failed -" << tcpSocket->errorString();
ui->label_tcp_state->setText("连接失败: " + tcpSocket->errorString());
ret = false;
}

} else {
// 断开连接操作
if(tcpSocket->state() == QAbstractSocket::ConnectedState) {
tcpSocket->disconnectFromHost();
if(tcpSocket->state() == QAbstractSocket::UnconnectedState ||
tcpSocket->waitForDisconnected(3000)) {
ret = true;
ui->label_tcp_state->setText("未连接");
qDebug() << "TCP: Disconnected successfully";
}
} else {
// 如果当前没有连接,也返回成功
ret = true;
ui->label_tcp_state->setText("未连接");
}

// 断开所有信号连接,防止重连的时候无法接收数据
//tcpSocket->disconnect();
}

return ret;
}

这里主要断开连接的时候不要使用tcpSocket->disconnect()来断开所有连接,这样会导致接收TCP数据的槽函数被断开,如果imx6u断开重连后就无法接收数据,这里的接收函数如下,数据包是自己定义的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// 连接readyRead信号来处理接收到的数据
connect(tcpSocket, &QTcpSocket::readyRead, this, [this]() {
// 读取所有可用数据并追加到缓冲区
QByteArray newData = tcpSocket->readAll();
if (!newData.isEmpty()) {
tcpBuf.append(newData);
}

// 每个完整包为 3 字节: [cmd_group, cmd_id, 0xFF]
// LED 开/关: [0x01, 0x01/0x02, 0xFF]
// BEEP 开/关: [0x02, 0x01/0x02, 0xFF]
// GET_TEMP: [0x03, 0x01, 0xFF]

while (tcpBuf.size() >= 3) {
// 检查第三字节是否为 0xFF(包尾标志)
if (static_cast<unsigned char>(tcpBuf[2]) != 0xFF) {
// 如果第三字节不是 0xFF,说明可能出现错位或噪声
// 丢弃第一个字节,继续尝试重同步
tcpBuf.remove(0, 1);
continue;
}

// 现在前 3 字节看起来像一包,取出进行处理
unsigned char b0 = static_cast<unsigned char>(tcpBuf[0]);
unsigned char b1 = static_cast<unsigned char>(tcpBuf[1]);

// 移除已处理的三个字节
tcpBuf.remove(0, 3);

// 根据命令处理
if (b0 == 0x01) {
// LED 控制
if (b1 == 0x01) {
// 打开 LED1
if (Set_LED1(true)) {
MainWindow::led1_flag = true;
ui->pushbutton_led1->setText(QStringLiteral("关闭LED1"));
qDebug() << "LED1 turned on via 3-byte packet";
} else {
qDebug() << "Failed to turn on LED1 via TCP packet";
}
} else if (b1 == 0x02) {
// 关闭 LED1
if (Set_LED1(false)) {
MainWindow::led1_flag = false;
ui->pushbutton_led1->setText(QStringLiteral("打开LED1"));
qDebug() << "LED1 turned off via 3-byte packet";
} else {
qDebug() << "Failed to turn off LED1 via TCP packet";
}
} else {
qDebug() << "Unknown LED command b1=" << b1;
}
}
else if (b0 == 0x02) {
// 蜂鸣器控制
if (b1 == 0x01) {
if (Set_Buzzer(true)) {
MainWindow::buzzer_flag = true;
ui->pushbutton_buzzer->setText(QStringLiteral("关闭蜂鸣器"));
qDebug() << "Buzzer turned on via 3-byte packet";
} else {
qDebug() << "Failed to turn on buzzer via TCP packet";
}
} else if (b1 == 0x02) {
if (Set_Buzzer(false)) {
MainWindow::buzzer_flag = false;
ui->pushbutton_buzzer->setText(QStringLiteral("打开蜂鸣器"));
qDebug() << "Buzzer turned off via 3-byte packet";
} else {
qDebug() << "Failed to turn off buzzer via TCP packet";
}
} else {
qDebug() << "Unknown BEEP command b1=" << b1;
}
}
else if (b0 == 0x03 && b1 == 0x01) {
// GET_TEMP 请求:刷新温度并把数据发送回服务器
if (Temperature_Update()) {
qDebug() << "Temperature updated via TCP request";
QString tempData = ui->label_temp->text(); // 例如 "25.3"
QByteArray out = tempData.toUtf8();
tcpSocket->write(out);
tcpSocket->flush();
} else {
ui->label_temp->setText(QStringLiteral("获取失败"));
QByteArray out = QStringLiteral("获取失败").toUtf8();
tcpSocket->write(out);
tcpSocket->flush();
}
}
else {
qDebug() << "Unknown 3-byte packet: b0=" << b0 << " b1=" << b1;
// 可选择应答错误或直接忽略
}
}
});

通过解析得到的数据包然后调用相关函数就可以实现接收服务器数据,并控制相关外设或者发送数据给服务器,这样就实现了一个简易的物联网通信,当然实际项目中,大家可以使用MQTT物联网会更加方便安全

通过上面这些函数,QT的逻辑方面基本都结束了,下面的B5 B6主要是一些额外添加自选的部分

B.5 添加显示时间

添加显示时间很简单,只用调用QT的QDateTime库即可,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
connect(timer,&QTimer::timeout,this,[=](){
time_cnt++;
if(time_cnt>=3)
{
time_cnt = 0;
if ( !Temperature_Update() )
{
ui->label_temp->setText("获取失败");
}
else
{
if(MainWindow::tcp_flag == true)
{
QString tempData = ui->label_temp->text();
QByteArray out = tempData.toUtf8();
tcpSocket->write(out);
tcpSocket->flush();
}
}
}
//update timer
QDateTime currentDateTime = QDateTime::currentDateTime();
QString timeString = currentDateTime.toString("yyyy-MM-dd HH:mm:ss");
ui->label_time->setText(timeString);

});
timer->start(1000);

通过更改定时器为1s,每过1s通过 QDateTime::currentDateTime()获取当前时间,并转化为想要的格式显示到屏幕上即可

B.6 界面简单美化

这里主要使用样式表设置了一下按钮背景,字体大小,还有整体背景颜色,大家可以根据自己的喜欢来设置
举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
this->setStyleSheet(
"QMainWindow {"
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #2c3e50, stop:1 #3498db);"
"}"
"QPushButton {"
" background-color: #3498db;"
" border: none;"
" border-radius: 5px;"
" color: white;"
" padding: 8px 15px;"
"}"
"QPushButton:hover {"
" background-color: #2980b9;"
"}"
"QPushButton:pressed {"
" background-color: #21618c;"
"}"
);

ui->label_title->setStyleSheet(
"QLabel {"
" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #e74c3c, stop:1 #c0392b);"
" color: white;"
" border-radius: 10px;"
" padding: 10px;"
" font-weight: bold;"
"}"
);

C 网页相关

因为这里主要是嵌入式相关,前后端涉及的不多,所以这里使用的都是非常简单的代码,如果有前后端基础的可以自己编写更好用且更美观的界面~

C.1 前端

前端使用静态页面(HTML/CSS/原始javaScript),通过浏览器的WebSocket与后端通信,通过sessionStorage做一个示例登录(不安全)

  • WebSocket 是全双工、事件驱动的协议;浏览器与后端可以任意一端主动推送数据

C.2 后端

后端使用 Python 3 + asyncio,使用第三方库 websockets 提供 WebSocket 服务,同时用 asyncio(异步) 的 TCP server 接收 imx6ull的数据并转发给网页客户端,或网页转发给imx6ull命令

这两个部分只是物联网示例,代码逻辑都非常简单,大家也可以直接用AI跑出来,具体代码贴也是贴在最后面,下面是前端的页面:

D imx6u驱动相关

这里主要给出链接,大家可以自己查看或者查找,有问题可以留言

E 相关问题

  • 在imx6ull连接再断开,再连接之后,无法响应服务器发送的消息

检查Set_Tcp_Server()函数中,不要调用tcpSocket->disconnect(),这样会导致槽函数也关闭

  • QT无法正确控制 led或者蜂鸣器等

检查Set_LED1()等函数,检查写文件写入的字符是什么类型,需要与驱动里面的类型对上,比如可以同时规定成字符’1’

  • QT无法正确读取ADC的值

检查QT读取ADC的打开文件函数的路径,路径尽量写成绝对地址,不要使用链接,否则可能会有问题

  • 在开发板上启动的时间不对,但是确实是有网络的

检查rtc的时间,因为没有写网络更新函数,所以时间需要更新一次
hwclock -r 查看当前时间,如果不对可以使用date -s "2025-10-14 15:30:00"写入当前时间,然后hwclock -w进行保存

F 完整代码

如果有整体文件需求,可以留言或者发邮箱给我

下面是具体代码:

mainwindow.h: 点击查看更多
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QPushButton>
#include <QTimerEvent>
#include <QFile>
#include <QFont>
#include <QTimer>
#include <QNetworkInterface>
#include <QDebug>
#include <QTcpServer>
#include <QTcpSocket>
#include <QString>
#include <QAbstractAnimation>
#include <QErrorMessage>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();

QTimer *timer;
bool led1_flag;
bool buzzer_flag;
bool tcp_flag;
QTcpSocket *tcpSocket;
QByteArray tcpBuf;

QString getLocalHostIP();
int time_cnt;

private:
Ui::MainWindow *ui;

private slots:
bool Set_LED1(bool flag);
bool Set_Buzzer(bool flag);
bool Temperature_Update(void);
bool Set_Tcp_Server(bool flag);



};
#endif // MAINWINDOW_H
mainwindow.c: 点击查看更多

注意修改成自己的tcp和websocket端口号,以及修改服务器地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QDateTime>
#include <QTimer>

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

QFont qfont;
qfont.setPointSize(40);
ui->label_title->setFont(qfont);

qfont.setPointSize(16);
ui->label_temp->setFont(qfont);
ui->label_tcp_state->setFont(qfont);
ui->label_temp_unuser->setFont(qfont);
ui->pushbutton_tcp->setFont(qfont);
ui->pushbutton_led1->setFont(qfont);
ui->pushbutton_buzzer->setFont(qfont);

ui->label_time->setFont(qfont);
ui->label_time_unuser->setFont(qfont);

time_cnt = 0;



timer = new QTimer(this);
tcpSocket = new QTcpSocket();

MainWindow::tcp_flag = false;
MainWindow::led1_flag = false;
MainWindow::buzzer_flag = false;




this->setStyleSheet(
"QMainWindow {"
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #2c3e50, stop:1 #3498db);"
"}"
// "QLabel {"
// " background-color: rgba(255, 255, 255, 150);" // 半透明白色背景
// " border-radius: 8px;"
// " padding: 5px;"
// " color: #2c3e50;"
// "}"
"QPushButton {"
" background-color: #3498db;"
" border: none;"
" border-radius: 5px;"
" color: white;"
" padding: 8px 15px;"
"}"
"QPushButton:hover {"
" background-color: #2980b9;"
"}"
"QPushButton:pressed {"
" background-color: #21618c;"
"}"
);

ui->label_title->setStyleSheet(
"QLabel {"
" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #e74c3c, stop:1 #c0392b);"
" color: white;"
" border-radius: 10px;"
" padding: 10px;"
" font-weight: bold;"
"}"
);



connect(ui->pushbutton_led1, &QPushButton::clicked, this, [this]() {
bool newState = !MainWindow::led1_flag;
if (Set_LED1(newState)) {
MainWindow::led1_flag = newState;
ui->pushbutton_led1->setText(newState ? "关闭LED1" : "打开LED1");
} else {
QMessageBox::critical(this, QStringLiteral("错误"), QStringLiteral("设置 LED1 失败"));
}
});

connect(ui->pushbutton_buzzer, &QPushButton::clicked, this, [this]() {
bool newState = !MainWindow::buzzer_flag;
if (Set_Buzzer(newState)) {
MainWindow::buzzer_flag = newState;
ui->pushbutton_buzzer->setText(newState ? "关闭蜂鸣器" : "打开蜂鸣器");
} else {
QMessageBox::critical(this, QStringLiteral("错误"), QStringLiteral("设置蜂鸣器失败"));
}
});

connect(ui->pushbutton_tcp, &QPushButton::clicked, this, [this]() {
bool newState = !MainWindow::tcp_flag;
if (Set_Tcp_Server(newState)) {
MainWindow::tcp_flag = newState;
ui->pushbutton_tcp->setText(newState ? "断开远程服务器" : "连接远程服务器");
} else {
QMessageBox::critical(this, QStringLiteral("错误"), QStringLiteral("连接/断开远程服务器失败"));
// 更新 UI 状态以反映实际连接状态
ui->pushbutton_tcp->setText(MainWindow::tcp_flag ? "断开远程服务器" : "连接远程服务器");
}
});

connect(timer,&QTimer::timeout,this,[=](){
time_cnt++;
if(time_cnt>=3)
{
time_cnt = 0;
if ( !Temperature_Update() )
{
ui->label_temp->setText("获取失败");
}
else
{
if(MainWindow::tcp_flag == true)
{
QString tempData = ui->label_temp->text(); // 例如 "25.3"
QByteArray out = tempData.toUtf8();
tcpSocket->write(out);
tcpSocket->flush();
}
}
}
//update timer
QDateTime currentDateTime = QDateTime::currentDateTime();
QString timeString = currentDateTime.toString("yyyy-MM-dd HH:mm:ss");
ui->label_time->setText(timeString);

});
timer->start(1000);

// 连接readyRead信号来处理接收到的数据
connect(tcpSocket, &QTcpSocket::readyRead, this, [this]() {
// 读取所有可用数据并追加到缓冲区
QByteArray newData = tcpSocket->readAll();
if (!newData.isEmpty()) {
tcpBuf.append(newData);
}

// 每个完整包为 3 字节: [cmd_group, cmd_id, 0xFF]
// LED 开/关: [0x01, 0x01/0x02, 0xFF]
// BEEP 开/关: [0x02, 0x01/0x02, 0xFF]
// GET_TEMP: [0x03, 0x01, 0xFF]

while (tcpBuf.size() >= 3) {
// 检查第三字节是否为 0xFF(包尾标志)
if (static_cast<unsigned char>(tcpBuf[2]) != 0xFF) {
// 如果第三字节不是 0xFF,说明可能出现错位或噪声
// 丢弃第一个字节,继续尝试重同步
tcpBuf.remove(0, 1);
continue;
}

// 现在前 3 字节看起来像一包,取出进行处理
unsigned char b0 = static_cast<unsigned char>(tcpBuf[0]);
unsigned char b1 = static_cast<unsigned char>(tcpBuf[1]);

// 移除已处理的三个字节
tcpBuf.remove(0, 3);

// 根据命令处理
if (b0 == 0x01) {
// LED 控制
if (b1 == 0x01) {
// 打开 LED1
if (Set_LED1(true)) {
MainWindow::led1_flag = true;
ui->pushbutton_led1->setText(QStringLiteral("关闭LED1"));
qDebug() << "LED1 turned on via 3-byte packet";
} else {
qDebug() << "Failed to turn on LED1 via TCP packet";
}
} else if (b1 == 0x02) {
// 关闭 LED1
if (Set_LED1(false)) {
MainWindow::led1_flag = false;
ui->pushbutton_led1->setText(QStringLiteral("打开LED1"));
qDebug() << "LED1 turned off via 3-byte packet";
} else {
qDebug() << "Failed to turn off LED1 via TCP packet";
}
} else {
qDebug() << "Unknown LED command b1=" << b1;
}
}
else if (b0 == 0x02) {
// 蜂鸣器控制
if (b1 == 0x01) {
if (Set_Buzzer(true)) {
MainWindow::buzzer_flag = true;
ui->pushbutton_buzzer->setText(QStringLiteral("关闭蜂鸣器"));
qDebug() << "Buzzer turned on via 3-byte packet";
} else {
qDebug() << "Failed to turn on buzzer via TCP packet";
}
} else if (b1 == 0x02) {
if (Set_Buzzer(false)) {
MainWindow::buzzer_flag = false;
ui->pushbutton_buzzer->setText(QStringLiteral("打开蜂鸣器"));
qDebug() << "Buzzer turned off via 3-byte packet";
} else {
qDebug() << "Failed to turn off buzzer via TCP packet";
}
} else {
qDebug() << "Unknown BEEP command b1=" << b1;
}
}
else if (b0 == 0x03 && b1 == 0x01) {
// GET_TEMP 请求:刷新温度并把数据发送回服务器(保持你原来的行为)
if (Temperature_Update()) {
qDebug() << "Temperature updated via TCP request";
QString tempData = ui->label_temp->text(); // 例如 "25.3"
QByteArray out = tempData.toUtf8();
tcpSocket->write(out);
tcpSocket->flush();
} else {
ui->label_temp->setText(QStringLiteral("获取失败"));
QByteArray out = QStringLiteral("获取失败").toUtf8();
tcpSocket->write(out);
tcpSocket->flush();
}
}
else {
qDebug() << "Unknown 3-byte packet: b0=" << b0 << " b1=" << b1;
// 可选择应答错误或直接忽略
}
} // end while
});

}

MainWindow::~MainWindow()
{
delete ui;
}



bool MainWindow::Set_LED1(bool flag)
{
bool ret = true;
QFile file("/dev/user_gpio_led");

// 以写入方式打开设备文件
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qDebug() << "无法打开LED设备文件";
return false;
}

// 根据flag值写入对应的控制信号
QTextStream out(&file);
if (flag) {
out << "1"; // 打开LED
} else {
out << "0"; // 关闭LED
}

file.close();
return ret;
}

bool MainWindow::Set_Buzzer(bool flag)
{
bool ret = true;
QFile file("/dev/user_gpio_buzzer");

// 以写入方式打开设备文件
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qDebug() << "无法打开蜂鸣器设备文件:" << file.errorString();
return false;
}

// 根据flag值写入对应的控制信号
QTextStream out(&file);
if (flag) {
out << "1"; // 打开蜂鸣器
qDebug() << "蜂鸣器已打开";
} else {
out << "0"; // 关闭蜂鸣器
qDebug() << "蜂鸣器已关闭";
}

file.close();
return ret;
}

bool MainWindow::Set_Tcp_Server(bool flag)
{
bool ret = false;

if(flag) {
// 连接服务器操作
if(tcpSocket->state() == QAbstractSocket::ConnectedState) {
ui->label_tcp_state->setText("已连接");
qDebug() << "TCP: Already connected";
return true;
}
if(tcpSocket->state() == QAbstractSocket::ConnectingState) {
ui->label_tcp_state->setText("正在连接中,请稍候...");
qDebug() << "TCP: Connection in progress";
return false;
}

// 获取本地IP并显示
QString localIP = getLocalHostIP();
qDebug() << "Local IP:" << localIP;

QString serverIP = "xxx.xxx.xxx.xxx"; // 服务器IP
quint16 serverPort = 555; // 服务器端口

// 连接信号槽
connect(tcpSocket, &QTcpSocket::connected, this, [this]() {
qDebug() << "TCP: Connected to server";
ui->label_tcp_state->setText("已连接");
ui->pushbutton_tcp->setText("断开远程服务器");
});

connect(tcpSocket, &QTcpSocket::disconnected, this, [this]() {
qDebug() << "TCP: Disconnected from server";
ui->label_tcp_state->setText("未连接");
ui->pushbutton_tcp->setText("连接远程服务器");
MainWindow::tcp_flag = false;
});

// 开始连接
ui->label_tcp_state->setText("正在连接...");
tcpSocket->connectToHost(serverIP, serverPort);

// 等待连接完成(最多等待5秒)
if(tcpSocket->waitForConnected(5000)) {
ret = true;
qDebug() << "TCP: Connection established successfully";
} else {
qDebug() << "TCP: Connection failed -" << tcpSocket->errorString();
ui->label_tcp_state->setText("连接失败: " + tcpSocket->errorString());
ret = false;
}

} else {
// 断开连接操作
if(tcpSocket->state() == QAbstractSocket::ConnectedState) {
tcpSocket->disconnectFromHost();
if(tcpSocket->state() == QAbstractSocket::UnconnectedState ||
tcpSocket->waitForDisconnected(3000)) {
ret = true;
ui->label_tcp_state->setText("未连接");
qDebug() << "TCP: Disconnected successfully";
}
} else {
// 如果当前没有连接,也返回成功
ret = true;
ui->label_tcp_state->setText("未连接");
}

// 断开所有信号连接,防止重连的时候无法接收数据
//tcpSocket->disconnect();
}

return ret;
}

bool MainWindow::Temperature_Update(void)
{
bool ret = true;

// 使用完整的设备路径
QString basePath = "/sys/devices/platform/soc/2100000.aips-bus/2198000.adc/iio:device0";
QFile rawFile(basePath + "/in_voltage1_raw");
QFile scaleFile(basePath + "/in_voltage_scale");

// 读取原始电压值
if (!rawFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "无法打开原始电压文件:" << rawFile.errorString();
return false;
}
QString rawStr = QTextStream(&rawFile).readLine().trimmed();
rawFile.close();

// 读取电压缩放比例
if (!scaleFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "无法打开电压缩放文件:" << scaleFile.errorString();
return false;
}
QString scaleStr = QTextStream(&scaleFile).readLine().trimmed();
scaleFile.close();

// 转换为数值
bool ok1, ok2;
double rawValue = rawStr.toDouble(&ok1);
double scaleValue = scaleStr.toDouble(&ok2);

if (!ok1 || !ok2) {
qDebug() << "数据转换错误: raw=" << rawStr << "scale=" << scaleStr;
return false;
}

// 计算实际电压值(单位:毫伏 mV)
double voltage_mV = rawValue * scaleValue;
double voltage_V = voltage_mV / 1000.0; // 转换为伏特 V

qDebug() << "Debug Info - Raw:" << rawValue << "Scale:" << scaleValue << "Voltage:" << voltage_mV << "mV";

//保留更多小数位以便观察变化
QString temperatureText = QString(" %1 V").arg(voltage_V, 0, 'f', 6);
ui->label_temp->setText(temperatureText);

return ret;
}

QString MainWindow::getLocalHostIP(void)
{
QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();
QString localIP;

// 遍历所有IP地址,优先选择IPv4且非回环地址
for (int i = 0; i < ipAddressesList.size(); ++i) {
QHostAddress ipAddr = ipAddressesList.at(i);
// 筛选条件:IPv4协议、不是本地回环地址、不是空地址
if (ipAddr.protocol() == QAbstractSocket::IPv4Protocol &&
ipAddr != QHostAddress::Null &&
ipAddr != QHostAddress::LocalHost &&
!ipAddr.toString().startsWith("127.0.") &&
!ipAddr.toString().startsWith("169.")) { // 排除APIPA地址
localIP = ipAddr.toString();
break;
}
}

// 如果没有找到合适的IP,使用本地回环地址作为备选
if (localIP.isEmpty()) {
localIP = "127.0.0.1";
qDebug() << "Warning: Using loopback address, no valid IPv4 address found";
}

qDebug() << "Local IP Address:" << localIP;
return localIP;
}
qianduan.html: 点击查看更多

注意修改成自己的tcp和websocket端口号,以及修改服务器地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>IMX6ULL远程控制</title>
<style>
body { font-family: sans-serif; }
.status { padding: 10px; margin: 5px; background-color: #f0f0f0; }
button { padding: 10px; margin: 5px; }
#loginBox { max-width: 320px; margin: 80px auto; padding: 20px; border: 1px solid #ccc; border-radius: 6px; background:#fff; }
#loginBox input { width: 100%; padding: 8px; margin: 6px 0; box-sizing: border-box; }
#mainUI { display: none; padding: 16px; }
</style>
</head>
<body>
<!-- 登录界面 -->
<div id="loginBox">
<h2>登录</h2>
<label>用户名</label>
<input id="username" type="text" placeholder="用户名">
<label>密码</label>
<input id="password" type="password" placeholder="密码">
<div style="text-align:right;">
<button onclick="doLogin()">登录</button>
</div>
</div>

<!-- 主界面 -->
<div id="mainUI">
<h1>IMX6ULL智能家居控制台</h1>
<div id="connectionStatus" class="status">未连接</div>

<button onclick="sendCommand('LED1_ON')">打开LED</button>
<button onclick="sendCommand('LED1_OFF')">关闭LED</button>
<button onclick="sendCommand('GET_TEMP')">获取温度</button>
<button onclick="sendCommand('BEEP_ON')">打开蜂鸣器</button>
<button onclick="sendCommand('BEEP_OFF')">关闭蜂鸣器</button>
<button onclick="logout()" style="float:right;">登出</button>

<div id="sensorData" style="margin-top:12px;"></div>
</div>

<script>
const serverIP = 'xx.xxx.xxx.xxx';
const wsPort = 555;
let websocket = null;
let deviceConnected = false;

function markControlButtons() {
const btns = document.querySelectorAll('#mainUI button');
btns.forEach(b => {
if (b.getAttribute('onclick') && b.getAttribute('onclick').includes('sendCommand')) {
b.dataset.control = '1';
}
});
}

function setDeviceConnected(connected) {
deviceConnected = connected;
const status = document.getElementById('connectionStatus');
if (connected) {
status.innerHTML = "设备已连接";
status.style.backgroundColor = '#90EE90';
} else {
status.innerHTML = "设备未连接";
status.style.backgroundColor = '#FFB6C1';
}
document.querySelectorAll('#mainUI button[data-control="1"]').forEach(b => {
b.disabled = !connected;
});
}

function showMainUI() {
document.getElementById('loginBox').style.display = 'none';
document.getElementById('mainUI').style.display = 'block';
markControlButtons();
setDeviceConnected(false); // 等后端通知 DEVICE_CONNECTED 再启用
connect();
}

function doLogin() {
const user = document.getElementById('username').value;
const pass = document.getElementById('password').value;
if (user === 'xxxxxxx' && pass === 'xxxxxxx') {
sessionStorage.setItem('loggedIn', '1');
showMainUI();
} else {
alert('用户名或密码错误');
}
}

function logout() {
sessionStorage.removeItem('loggedIn');
location.reload();
}

// 辅助:把 hex 字符串转换为 UTF-8 字符串并返回
function hexToString(hex) {
// 移除可能的空格或 0x 前缀
hex = hex.replace(/\s+/g, '').replace(/0x/g,'')
if (hex.length === 0) return ''
// 若奇数位,前面补 0
if (hex.length % 2 !== 0) {
hex = '0' + hex;
}
try {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
// 使用 TextDecoder 解码为字符串(不可解的字节会被替代字符替换)
const dec = new TextDecoder('utf-8');
return dec.decode(bytes);
} catch (e) {
// 任何异常直接返回原 hex(但你说不要 hex,仍尽量返回空串)
console.warn('hexToString decode failed', e);
return '';
}
}

function connect() {
websocket = new WebSocket(`ws://${serverIP}:${wsPort}`);

websocket.onopen = function () {
document.getElementById('connectionStatus').innerHTML = "已连接到服务器(等待设备状态)";
document.getElementById('connectionStatus').style.backgroundColor = '#FFFF99';
};

websocket.onmessage = function (event) {
const msg = event.data;
// 后端发的是文本控制消息:
// DEVICE_CONNECTED / DEVICE_DISCONNECTED / DATA:hex / TEMP:val / ERROR:...
if (typeof msg === 'string') {
if (msg === "DEVICE_CONNECTED") {
setDeviceConnected(true);
return;
}
if (msg === "DEVICE_DISCONNECTED") {
setDeviceConnected(false);
return;
}
if (msg.startsWith("TEMP:")) {
// TEMP:0.98 直接显示电压值字符串
const val = msg.slice(5);
document.getElementById('sensorData').innerHTML = `电压: ${val} V`;
return;
}
if (msg.startsWith("DATA:")) {
// DATA:hexstring —— 尝试把 hex 解码为字符串并直接显示
const hex = msg.slice(5);
const text = hexToString(hex);
// 若解码后为空(不可解),显示“收到不可读数据”
document.getElementById('sensorData').innerHTML = text !== '' ? `收到设备字符串:${text}` : `收到不可读二进制数据(无法解码为文本)`;
return;
}
if (msg.startsWith("ERROR:")) {
alert(msg);
return;
}
// 其它文本消息直接显示
document.getElementById('sensorData').innerHTML = `收到: ${msg}`;
} else {
// 若收到了二进制(后端目前不发二进制),直接把二进制用 TextDecoder 解码并显示
try {
const reader = new FileReader();
reader.onload = function () {
const arrayBuffer = reader.result;
const bytes = new Uint8Array(arrayBuffer);
const dec = new TextDecoder('utf-8');
const s = dec.decode(bytes);
document.getElementById('sensorData').innerHTML = `收到设备字符串(二进制帧):${s}`;
};
reader.readAsArrayBuffer(event.data);
} catch (e) {
document.getElementById('sensorData').innerHTML = `收到二进制消息(len=${event.data.byteLength})`;
}
}
};

websocket.onclose = function () {
document.getElementById('connectionStatus').innerHTML = "连接已断开(与服务器)";
document.getElementById('connectionStatus').style.backgroundColor = '#FFB6C1';
setDeviceConnected(false);
};

websocket.onerror = function (e) {
console.error('WebSocket error', e);
};
}

// sendCommand 支持二进制发送(浏览器)或文本回退
function sendBinaryArray(arr) {
if (websocket && websocket.readyState === WebSocket.OPEN) {
try {
websocket.send(new Uint8Array(arr));
return true;
} catch (e) {
console.warn("Binary send failed, will try textual fallback", e);
return false;
}
} else {
alert("未连接到服务器!");
return false;
}
}

function sendCommand(cmd) {
if (!deviceConnected) {
alert("设备未连接,无法发送命令");
return;
}

// 映射命令到三字节包
const map = {
"LED1_ON": [0x01, 0x01, 0xFF],
"LED1_OFF": [0x01, 0x02, 0xFF],
"BEEP_ON": [0x02, 0x01, 0xFF],
"BEEP_OFF": [0x02, 0x02, 0xFF],
"GET_TEMP": [0x03, 0x01, 0xFF],
};

const payload = map[cmd];
if (!payload) {
alert("未知命令");
return;
}

// 先尝试二进制发送(浏览器支持),否则回退为字符串命令
const ok = sendBinaryArray(payload);
if (!ok) {
websocket.send(cmd);
}
}

window.onload = function () {
if (sessionStorage.getItem('loggedIn') === '1') {
showMainUI();
} else {
document.getElementById('username').focus();
}
};
</script>
</body>
</html>
houduan.py: 点击查看更多

注意修改成自己的tcp端口号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import asyncio
import websockets
import re

# 存储当前设备的 StreamWriter(假设只有一台 imx6ull)
device_writer = None

# 存储当前连接的网页客户端(websocket 实例)
web_clients = set()

# 广播文本消息给所有网页客户端(非阻塞)
def broadcast_text(msg: str):
for ws in list(web_clients):
# 创建任务异步发送,发送失败时移除该客户端
asyncio.create_task(_safe_send(ws, msg))

async def _safe_send(ws, msg: str):
try:
await ws.send(msg)
except Exception:
try:
await ws.close()
except Exception:
pass
web_clients.discard(ws)

async def handle_imx6ull(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
"""
处理 imx6ull 的 TCP 连接。
- 把收到的原始字节尝试按 UTF-8 解码;如果是数字/浮点文本(例如 "0.123"),当作 TEMP 广播;
否则以 hex 广播为 DATA。
- 当设备连接/断开时广播 DEVICE_CONNECTED / DEVICE_DISCONNECTED
"""
global device_writer
peer = writer.get_extra_info('peername')
print(f"[TCP] imx6ull connected from {peer}")

# 如果已有旧的 writer,先关闭它(简单处理)
if device_writer is not None and device_writer is not writer:
try:
device_writer.close()
await device_writer.wait_closed()
except Exception:
pass

device_writer = writer
broadcast_text("DEVICE_CONNECTED")

try:
while True:
data = await reader.read(1024)
if not data:
print("[TCP] imx6ull closed connection")
break

# 先尝试把 bytes 解码为 utf-8 文本(去掉首尾空白)
decoded = None
try:
decoded = data.decode('utf-8', errors='strict').strip()
except Exception:
decoded = None

# 如果 decode 成功并且看起来像数字或浮点(比如 "0.123"、"3.30"、"1"),当作 TEMP 广播
if decoded:
# 简单的匹配:整数字或浮点(可带小数点)
if re.fullmatch(r'\d+(\.\d+)?', decoded):
# 前端直接显示字符串(例如 "0.98")
broadcast_text("TEMP:" + decoded)
print(f"[TCP] recv TEMP {decoded} from device")
continue

# 否则按 hex 广播(便于调试二进制)
hexstr = data.hex()
broadcast_text("DATA:" + hexstr)
print(f"[TCP] recv hex {hexstr} from device")
except Exception as e:
print(f"[TCP] device error: {e}")
finally:
# 清理
if device_writer is writer:
device_writer = None
broadcast_text("DEVICE_DISCONNECTED")
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
print("[TCP] imx6ull cleanup done")

async def handle_web_client(ws, path):
"""
处理网页的 websocket 连接。
- 接收二进制消息或文本命令并转发到 device_writer(TCP)
- 初次连接时通知当前设备状态
"""
global device_writer
print("[WS] web client connected")
web_clients.add(ws)

# 发送当前设备状态到该客户端
try:
if device_writer is not None:
await ws.send("DEVICE_CONNECTED")
else:
await ws.send("DEVICE_DISCONNECTED")
except Exception:
pass

try:
async for msg in ws:
# msg 可能是 str(文本帧)或 bytes(二进制帧)
if isinstance(msg, bytes):
# 直接把二进制数据原样转发给设备
if device_writer is None:
await _safe_send(ws, "ERROR: device not connected")
continue

transport = getattr(device_writer, "transport", None)
if transport is None or transport.is_closing():
# 设备看起来已断开,清理并通知所有网页
device_writer = None
broadcast_text("DEVICE_DISCONNECTED")
await _safe_send(ws, "ERROR: device disconnected")
continue

try:
device_writer.write(msg) # 原样写入
await device_writer.drain()
except Exception as e:
print(f"[WS->TCP] send error: {e}")
device_writer = None
broadcast_text("DEVICE_DISCONNECTED")
await _safe_send(ws, "ERROR: failed to send to device")
else:
# 兼容旧文本命令(如果前端仍发送字符串),这里简单映射为指定的三字节包
cmd = str(msg)
mapping = {
"LED1_ON": bytes([0x01, 0x01, 0xFF]),
"LED1_OFF": bytes([0x01, 0x02, 0xFF]),
"BEEP_ON": bytes([0x02, 0x01, 0xFF]),
"BEEP_OFF": bytes([0x02, 0x02, 0xFF]),
"GET_TEMP": bytes([0x03, 0x01, 0xFF]),
}
payload = mapping.get(cmd)
if payload:
if device_writer is None:
await _safe_send(ws, "ERROR: device not connected")
continue
try:
device_writer.write(payload)
await device_writer.drain()
except Exception as e:
print(f"[WS->TCP] send error: {e}")
device_writer = None
broadcast_text("DEVICE_DISCONNECTED")
await _safe_send(ws, "ERROR: failed to send to device")
else:
# 未知文本,直接回显或忽略
await _safe_send(ws, "ERROR: unknown command")
except websockets.exceptions.ConnectionClosed:
pass
except Exception as e:
print(f"[WS] client error: {e}")
finally:
web_clients.discard(ws)
print("[WS] web client disconnected")

async def main():
# 启动 TCP server(imx6ull) 444是tcp端口号,记得改成自己的端口号,555是websocket端口号
tcp_server = await asyncio.start_server(handle_imx6ull, "0.0.0.0", 444)
print("[MAIN] TCP server listening on 0.0.0.0:444")

# 启动 WebSocket server(网页)
ws_server = await websockets.serve(handle_web_client, "0.0.0.0", 555)
print("[MAIN] WebSocket server listening on 0.0.0.0:555")

await asyncio.gather(tcp_server.serve_forever(), ws_server.wait_closed())

if __name__ == "__main__":
asyncio.run(main())

参考文献

  1. 【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81

留言

有问题请指出,你可以选择以下方式:

  1. 在下方评论区留言
  2. 邮箱留言
  • Title: 基于 i.MX6ULL-Linux的智能家居项目(Qt)
  • Author: H_Haozi
  • Created at : 2025-10-12 15:52:45
  • Updated at : 2025-10-14 20:34:59
  • Link: https://redefine.ohevan.com/2025/10/12/embedded_imx6ull_smart_home_qt_tcp/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments