Funpack3-5 基于Teensy 4.1的简易远程灯控装置
该项目使用了Teensy 4.1,实现了基于上位机的远程灯控的设计,它的主要功能为:可以在客户端控制LED的开关、闪烁。
标签
Funpack活动
QT
Teensy 4.1
obrulviser
更新2025-01-13
6

Funpack3-5

——板卡二 任务一:基于Teensy 4.1的远程灯控

一、项目介绍

本项目依托于Funpack3-5活动,实现一基于TCP/IP协议通信的远程灯控装置。本项目采用上位机与受控段配合的运行模式。其中上位机采用QT开发,实现了全平台上位机。受控端为Teensy 4.1,使用Arduino IDE基于Arduino框架开发。


项目内容:

1、开发一具有TCP/IP通信功能的全平台上位机。

2、开发一套具有TCP/IP通信功能的板卡控制程序。


应用场景

本项目为一远程灯控系统的简易demo,通过与上位机的配合,可以实现远程灯光的远程控制。在现实生活中,往小,可以应用到智能台灯;往大,可以应用到LED照明系统的控制。总之,本项目为基于TCP/IP协议通信的远程灯具提供了一种方案及实现。


二、总体架构

本项目建立在局域网通信的基础上,使用TCP/IP协议,以Teensy 4.1为服务器,使用Qt开发Windows、Linux、Android客户端,是一套上位机-受控端配合的嵌入式系统方案。

三、Teensy 4.1软件

Teensy 4.1支持Arduino框架或者CircuitPython开发,支持Arduino IDE、PltaformIO及VS studio等多种开发环境。相互之间并无优劣之分,出于作者习惯,使用ArduinoIDE开发。

3.1 环境安装

笔者采用ArduinoIDE 2.3.1版本,通过在开发板管理器添加链接即可支持。

https://www.pjrc.com/teensy/package_teensy_index.json

笔者安装时多次出现网络问题,可以进入链接的网页,下载无法安装的包,放入ArduinoIDE报错的缓存文件夹内即可。

3.2 软件架构

未命名文件(2).png

Teensy 4.1程序流程图如图所示,系统上电后,首先初始化串口,之后是LED,最后是网络通信相关。然后,启动服务器。至此,系统的初始化部分完成,进入主循环。主循环会循环监听是否有新的客户端接入,接收客户端的指令,执行相应的动作,清理僵尸连接。

3.3 软件实现

3.3.1 系统初始化函数

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);


  Serial.begin(115200);
  while (!Serial && millis() < 4000) {
    // Wait for Serial
  }
  printf("Starting...\r\n");


  // Unlike the Arduino API (which you can still use), QNEthernet uses
  // the Teensy's internal MAC address by default, so we can retrieve
  // it here
  uint8_t mac[6];
  Ethernet.macAddress(mac);  // This is informative; it retrieves, not sets
  printf("MAC = %02x:%02x:%02x:%02x:%02x:%02x\r\n",
         mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);


  // Add listeners
  // It's important to add these before doing anything with Ethernet
  // so no events are missed.


  // Listen for link changes
  Ethernet.onLinkState([](bool state) {
    printf("[Ethernet] Link %s\r\n", state ? "ON" : "OFF");
  });


  // Listen for address changes
  Ethernet.onAddressChanged([]() {
    IPAddress ip = Ethernet.localIP();
    bool hasIP = (ip != INADDR_NONE);
    if (hasIP) {
      printf("[Ethernet] Address changed:\r\n");


      printf("    Local IP = %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
      ip = Ethernet.subnetMask();
      printf("    Subnet   = %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
      ip = Ethernet.gatewayIP();
      printf("    Gateway  = %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
      ip = Ethernet.dnsServerIP();
      if (ip != INADDR_NONE) {  // May happen with static IP
        printf("    DNS      = %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
      }
    } else {
      printf("[Ethernet] Address changed: No IP address\r\n");
    }
  });


  if (initEthernet()) {
    // Start the server
    printf("Starting server on port %u...", kServerPort);
    server.begin();
    printf("%s\r\n", (server) ? "Done." : "FAILED!");
  }
}

这部分代码是系统初始化部分,刨除打印调试信息部分,主要是串口、LED及网络的初始化,并启动了TCP服务器。

3.3.2 以太网初始化

bool initEthernet() {
  // DHCP
  if (staticIP == INADDR_NONE) {
    printf("Starting Ethernet with DHCP...\r\n");
    if (!Ethernet.begin()) {
      printf("Failed to start Ethernet\r\n");
      return false;
    }
    // We can choose not to wait and rely on the listener to tell us
    // when an address has been assigned
    if (kDHCPTimeout > 0) {
      printf("Waiting for IP address...\r\n");
      if (!Ethernet.waitForLocalIP(kDHCPTimeout)) {
        printf("No IP address yet\r\n");
        // We may still get an address later, after the timeout,
        // so continue instead of returning
      }
    }
  } else {
    // Static IP
    printf("Starting Ethernet with static IP...\r\n");
    if (!Ethernet.begin(staticIP, subnetMask, gateway)) {
      printf("Failed to start Ethernet\r\n");
      return false;
    }
    // When setting a static IP, the address is changed immediately,
    // but the link may not be up; optionally wait for the link here
    if (kLinkTimeout > 0) {
      printf("Waiting for link...\r\n");
      if (!Ethernet.waitForLink(kLinkTimeout)) {
        printf("No link yet\r\n");
        // We may still see a link later, after the timeout, so
        // continue instead of returning
      }
    }
  }
  return true;
}

以上这部分主要是初始化系统的以太网,首先根据是否是静态IP,决定等待DHCP分配IP还是静态IP连接路由器,启动连接,超时未连接或连接成功都会打印调试信息。

3.3.3 接收数据处理程序

void processClientData(ClientState &state) {
  // Loop over available data until an empty line or no more data
  // Note that if emptyLine starts as false then this will ignore any
  // initial blank line.
  while (true) {
    int avail = state.client.available();
    if (avail <= 0) {
      return;
    }


    state.lastRead = millis();
    int c = state.client.read();
    state.client.flush();


    printf("%c", c);
    if (c == '1')
      digitalWrite(LED_BUILTIN, HIGH);
    else
      digitalWrite(LED_BUILTIN, LOW);
    // printf("1\n");


    if (c == '\n') {
      if (state.emptyLine) {
        break;
      }


      // Start a new empty line
      state.emptyLine = true;
    } else if (c != '\r') {
      // Ignore carriage returns because CRLF is a likely pattern in
      // an HTTP request
      state.emptyLine = false;
    }
  }


  IPAddress ip = state.client.remoteIP();
  printf("Sending to client: %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
  state.client.writeFully("HTTP/1.1 200 OK\r\n"
                          "Connection: close\r\n"
                          "Content-Type: text/plain\r\n"
                          "\r\n"
                          "Hello, Client!\r\n");
  state.client.flush();


  // Half close the connection, per
  // [Tear-down](https://datatracker.ietf.org/doc/html/rfc7230#section-6.6)
  state.client.closeOutput();
  state.closedTime = millis();
  state.outputClosed = true;
}

这部分是接收数据处理程序,根据接收字符解析控制命令,然后执行。

四、上位机软件

对于跨平台的网络通信客户端来说,有多种选择,诸如QT、Electron、Tarui及Flutter等等,他们有着不同的开发特点与优势。本项目采用QT作为上位机框架,其安装包体积较小,占用内存较少,更能适应资源较少的设备,因而拥有更强的适用范围。

4.1 开发环境

即使限定QT开发,也有多种开发环境。虽然QTCreator存在诸多缺点,本项目仍旧使用QTCreator开发,因其作为QT原生IDE,拥有其他IDE所不具备的开箱即用的方便。

对于本项目,已经验证了基于QT 6.8 版本,基于qmake,使用MSVC2019编译。

安装QT时注意安装网络通信相关的模块。

4.2 软件架构

QT主要利用信号与槽这一机制完成操作。以点灯为例。如果连接成功,发送点灯信号后立刻关闭连接,会导致点灯信号不能被稳定接收,因此设计如下流程。当“点灯”按键按下,按键对应的槽函数启动,发起网络连接。连接成功信号发出后,弹出连接成功指示框,程序运行槽函数,向受控端发送点灯信号,启动定时器。定时器超时后,槽函数终止网络连接。本次操作结束。


4.3 软件实现

QT框架下程序架构与常规C/C++并不相同,存在主函数与多个窗口函数等,此处不再赘述,请读者自行了解。

4.3.1 主函数实现

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.setStyle(QStyleFactory::create("fusion"));
MainWindow w;
w.show();
return a.exec();
}

界面实现较为简单,首先生成一个应用,设置显示风格,显示。主程序就结束了

4.3.2 主窗口初始化程序

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
onSocket=new QTcpSocket(this);
offSocket=new QTcpSocket(this);
tim1=new QTimer(this);
tim1->stop();
tim1->setTimerType(Qt::PreciseTimer);
tim1->setInterval(50);
tim1->setSingleShot(true);
QObject::connect(tim1,&QTimer::timeout,this,[&](){
onSocket->disconnectFromHost();
;
});

tim2=new QTimer(this);
tim2->stop();
tim2->setTimerType(Qt::PreciseTimer);
tim2->setInterval(50);
tim2->setSingleShot(true);
QObject::connect(tim2,&QTimer::timeout,this,[&](){
offSocket->disconnectFromHost();
;
});

QObject::connect(offSocket,&QTcpSocket::connected,this,[&](){
offSocket->write("0");
// mSocket->write("0\n");
tim2->start();
});

QObject::connect(onSocket,&QTcpSocket::connected,this,[&](){
onSocket->write("1");
// mSocket->write("1\n");
tim1->start();
// mSocket->disconnectFromHost();
});
}

主窗口的初始化主要是设置UI界面,以及各定时器、信号与槽的连接等等。首先创建UI控件、生成网络通信变量。然后创建两个定时器,并通过虚函数链接定时器超时信号与处理函数。最后,通过虚函数的方式连接网络连接的信号与处理函数。

4.3.3 主界面按键槽函数

void MainWindow::on_pushButton_clicked()
{
QString ip=ui->lineEdit->text();
quint16 port=ui->spinBox->value();
offSocket->connectToHost(ip,port);
if(!offSocket->waitForConnected(40))
{
QMessageBox::warning(this, QObject::tr("连接受控端"), QObject::tr("连接失败"));
}
else {
QMessageBox::information(this, QObject::tr("连接受控端"), QObject::tr("成功"));
}
}


void MainWindow::on_pushButton_2_clicked()
{
QString ip=ui->lineEdit->text();
quint16 port=ui->spinBox->value();
onSocket->connectToHost(ip,port);
if(!onSocket->waitForConnected(40))
{
QMessageBox::warning(this, QObject::tr("连接受控端"), QObject::tr("连接失败"));
}
else {
QMessageBox::information(this, QObject::tr("连接受控端"), QObject::tr("成功"));
}
}

这部分是主界面两个按键对应的槽函数,分别对应“关灯”和“开灯”。两个函数大体相同,首先读取受控端IP和端口,然后发起连接,等待40ms,弹出连接成功或失败弹窗。

4.3.4 主界面关闭程序

MainWindow::~MainWindow()
{
onSocket->disconnectFromHost();
offSocket->disconnectFromHost();
delete ui;
}

这部分是主界面关闭时执行的程序,主要是为防止过多的连接导致受控端不能正常运行,退出前断开可能存在的连接,然后关闭界面。



五、最终效果

具体效果请参考视频,此处放出上位机截图。

image.png

六、展望

本项目在嵌入式代码安全相关有一定缺陷,未考虑与商业化相关的代码加密、固化等操作。如果优化本项目,可以考虑利用Teensy官方设计的安全相关功能,提高产品的保密性能。

由于本项目的网络通信存在未知缺陷,导致TCP连接会不定期断联,因而本项目采取每次操作都先连接服务器,操作完成后再断开的方式,避免TCP断联导致系统操作无反应等恶性问题。但是,终归不够完美。如果能排查到网络通信的未知问题,就可以避免这种运行方式。



附件下载
ChatServer.ino
untitled (2).zip
untitled (2).z01
untitled (2).z02
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号