简介:面向粤嵌GEC6818开发板学习者的综合性实践资源,将相册、音频、视频三类多媒体模块整合于一体,并通过蓝牙远程控制所有功能,覆盖了嵌入式系统开发、蓝牙协议通信、多媒体解码与图形界面交互等多个知识面。压缩包共24个文件,以C语言源码(9个.c)与头文件(3个.h)为主体,另有12个txt说明文档记录操作步骤、功能注解与调试思路,全部内容仅31KB,体量轻、易消化,适合快速通读代码逻辑。目前已有3719人学习下载。资源内含完整可编译源代码与可执行文件,从显示颜色、触摸切换图片、特效切换,到播放指定音乐、开始暂停控制、视频播放器代码等分阶段小任务一应俱全,最终作品与各阶段任务整合关系清晰,可作为课程设计或毕设复现的现成参照,也能帮助初学者理解蓝牙控制架构、多模块协同和嵌入式软件迭代管理思路。
1. GEC6818 多媒体三件套:蓝牙才是真正的总开关
GEC6818 这种八核 Cortex-A53 的教学板,跑一个 Qt 相册、音频、视频三件套并不稀奇,真正的分水岭在“蓝牙控制所有模块”这半句话上。我见到的九成小任务最终都倒在同一个地方:三个界面各玩各的,演示时只能用手去戳触摸屏,蓝牙指令要么配对不上、要么发过去石沉大海。这个项目的本质不是多媒体开发,而是把三个播放器收编到同一条蓝牙串口命令线上——谁先把协议理顺,谁就能把课设小任务直接升级成最终作品。如果你手上的板子带 LCD、要交相册/音频/视频三件套,还想用手机统一调度,这篇内容给的是能在 GEC6818 上直接落地的一套流程:Qt 工程骨架、播放器进程选型、自定义指令协议、后台守护进程,以及最容易翻车的几个调试点。适合已经能独立编译板级 Qt 程序、第一次碰蓝牙联动的人。
2. 把三件套骨架立起来:Qt 工程结构与帧缓冲下的显示细节
2.1 为什么我用三个独立 QWidget 而不是三个 Tab 页
很多人写多媒体模块第一步就打开 QTabWidget 拖三个页面,但 GEC6818 这种跑 framebuffer 的板子和 PC 上的窗口环境完全是两回事。演示最终作品时,你要的是沉浸式全屏播放,而不是屏幕上顶着一排可以误触的 Tab 栏。蓝牙指令来了,切换的是各模块内部的状态机,不是 UI 组件,所以 QTabWidget 属于给自己找麻烦。
我一般会建三个独立 Widget,一个主窗口用 QStackedWidget 承载,这样相册、音频、视频各自维护自己的播放状态,互不干扰。qmake 工程文件长这样:
TARGET = media_demo TEMPLATE = app QT += core gui CONFIG += c++11 SOURCES += main.cpp \ mainwindow.cpp \ albumwidget.cpp \ audiowidget.cpp \ videowidget.cpp HEADERS += mainwindow.h \ albumwidget.h \ audiowidget.h \ videowidget.h target.path = /opt/media_demo INSTALLS += target这里有一个参数值得说清楚:QT += widgets在 Qt5 下必须显式声明,但如果你的板级镜像还是 Qt4.8,去掉 widgets 直接写QT += core gui即可。target.path我习惯放到 /opt 下而不是根目录,因为教学板的根文件系统经常是只读的,放 /opt 避免每次断电后文件被还原,也方便后面做开机自启脚本指向。
主窗口的代码很直接:
MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { m_stack = new QStackedWidget(this); m_album = new AlbumWidget; m_audio = new AudioWidget; m_video = new VideoWidget; m_stack->addWidget(m_album); m_stack->addWidget(m_audio); m_stack->addWidget(m_video); m_stack->setCurrentWidget(m_album); // framebuffer 下没有窗口管理器,全屏状态就是占满整个 LCD setWindowState(Qt::WindowFullScreen); }注意setWindowState(Qt::WindowFullScreen)这行在 X11 环境下可能是个幌子,但在 GEC6818 的 framebuffer 环境下它决定布局是否占满整块屏。启动时先显示相册页,避免开机直接黑屏让评委以为没跑起来。
2.2 相册模块:目录扫描、格式过滤与缩略图预生成
相册模块第一个坑是图片格式。板级 Qt 的 imageformats 插件未必齐全,JPEG 和 PNG 基本没问题,但 GIF 动图、WebP 这类不要指望。扫描目录时用后缀过滤是第一道关口,再用 QImageReader 做二次校验,防止把改名成 .jpg 的其他文件也塞进列表:
// 扫描 /mnt/sdcard/album 下的图片,按文件名排序 QStringList filters; filters << "*.jpg" << "*.jpeg" << "*.png" << "*.bmp"; QDir dir("/mnt/sdcard/album"); QFileInfoList files = dir.entryInfoList(filters, QDir::Files, QDir::Name); m_imageList.clear(); for (const QFileInfo &f : files) { // 二次校验:QImageReader 能解码才算数 if (QImageReader::canRead(f.absoluteFilePath())) m_imageList << f.absoluteFilePath(); }QDir::Name排序很重要,它保证幻灯片播放时按文件名顺序走,而不是文件系统里看起来混乱的 inode 顺序。QImageReader::canRead()是容易被忽略的一层:有些图片后缀是对的但文件损坏或编码不标准,直接加载会在 UI 线程里抛异常甚至卡死。
缩略图方面,6818 没有 GPU 加速,framebuffer 下的软渲染非常吃 CPU,如果每张图都加载原图再 QImage::scaled,滑动列表能卡到怀疑人生。我一般用 setScaledSize 在解码阶段就限制尺寸:
// 生成 320x240 缩略图,避免加载原图再缩放 QImageReader reader(path); reader.setAutoTransform(true); // 按 EXIF 方向自动旋转 QSize imageSize = reader.size(); if (imageSize.width() > 320 || imageSize.height() > 240) { reader.setScaledSize(imageSize.scaled(320, 240, Qt::KeepAspectRatio)); } QPixmap thumb = QPixmap::fromImage(reader.read()); QListWidgetItem *item = new QListWidgetItem(QIcon(thumb), QFileInfo(path).fileName()); m_listWidget->addItem(item);这里的核心是setScaledSize:它让解码器只解出一张缩小后的图,而不是先吃进整张 4000x3000 原图再缩放。我用 320x240 做列表缩略图,既清晰又不拖累滑动帧率。真到了全屏看大图,再单独开一条后台线程去加载,这个细节在第 5 章的避坑清单里还会提到。
2.3 main 函数里的帧缓冲初始化:跑不起来先查环境变量
很多模块写完了上板却黑屏,问题不在代码,而在启动参数。Qt4 在 framebuffer 下需要 QWS 环境变量,Qt5 则需要 eglfs 或 linuxfb 插件,这两者经常被搞混。我建议把 Qt 程序启动包成一个脚本,环境变量全部固定写在里面,不要依赖 shell 手动导出:
#!/bin/sh # GEC6818 多媒体程序启动脚本 export QWS_DISPLAY=LinuxFb:/dev/fb0 export QWS_MOUSE_PROTO=tslib:/dev/input/event1 export TSLIB_TSDEVICE=/dev/input/event1 export TSLIB_CALIBFILE=/etc/pointercal cd /opt/media_demo ./media_demo -qws-qws是 Qt4 特有的参数,告诉 Qt 自己直接写 framebuffer 而不是去找 X 服务。如果你手上的镜像已经是 Qt5,把-qws去掉,并改成export QT_QPA_PLATFORM=linuxfb:/dev/fb0。很多从别人那里拷贝的工程在 PC 上跑得欢,上板黑屏,十有八九就是这一行环境变量没配对。触摸设备节点也不是固定的 event1,具体是哪个要拿cat /proc/bus/input/devices去确认,我在第 5 章会专门讲这个。
3. 音频与视频播放:为什么我选进程外播放器而不是 Qt 自带解码
3.1 音频模块:alsa 通道初始化与 mplayer 的 slave 模式
GEC6818 这类教学板的 Qt 镜像,解码能力是很“骨感”的。Qt4 时代用 phonon,后端插件不全,MP3 放出来经常是“黑匣子”不出声;Qt5 的 QMediaPlayer 依赖 gstreamer 插件组,镜像里往往缺 mad 或者 mpg123,结果同样是无声。所以常见做法、也是我长期用的方案:直接外挂 mplayer 进程,Qt 只管下发命令和接收事件。这样不管你拿到的是 Qt4 还是 Qt5 镜像,播放能力都只取决于 mplayer 这一个二进制。
先解决无声问题。板载 Codec 的 ALSA 通道默认可能是静音的,代码写得再好也没用,先在板子上跑一遍:
# 列出板载声卡的全部控制项 amixer -c 0 scontrols # 逐个打开常见输出通道,音量拉满再降到合适位置 amixer -c 0 sset 'Speaker' 100% amixer -c 0 sset 'Headphone' 100% amixer -c 0 sset 'PCM' 80% amixer -c 0 sset 'Master' 80%不同板载 Codec 的通道命名不一样,有的叫 DAC,有的叫 Playback。amixer scontrols列出来的名字为准,不要照搬网上的配置,我踩过一次把 Speaker 写成 Speaker L/R 导致始终静音的坑。接音箱时看 Speaker,插耳机时看 Headphone,PCM 是总线通道一般都要打开。
Qt 端拉起 mplayer 播放音乐的代码:
// 用 QProcess 拉起 mplayer,全程用 stdin 下发控制命令 m_proc = new QProcess(this); m_proc->start("mplayer", QStringList() << "-slave" // slave 模式:mplayer 从 stdin 读命令 << "-quiet" // 减少日志,避免 stdout 干扰 << "-vo" << "null" // 纯音频,不需要视频输出 << "-ao" << "alsa" << "/mnt/sdcard/music/demo.mp3");逻辑说明:-slave是音频控制的核心,它让 mplayer 启动后不进入交互界面,而是持续从标准输入读取命令。所以播放中想暂停、调音量都不用杀掉进程重新拉起:
// 播放过程中下发的控制指令 m_proc->write("pause\n"); // 暂停/恢复 m_proc->write("volume 50 1\n"); // 音量调到 50%,第二个参数 1 表示立即生效 m_proc->write("seek 10 0\n"); // 快进 10 秒参数说明:volume 50 1里的 0 到 100 对应音量百分比,最后的1表示立即生效。如果省略,mplayer 可能用一个渐变过程,蓝牙控制时会有可感知的延迟。seek 10 0的三段含义是“偏移量 10 秒 + 模式 0 表示相对当前时间”,想做进度条快进快退就靠它。
3.2 视频模块:直接写 framebuffer 与控制权的交接
视频播放比音频麻烦一点,因为视频画面和 Qt 界面都要用 framebuffer,处理不好就是画面互相覆盖。这里我踩过的坑是:用 Qt 的 QVideoWidget 在 framebuffer 环境下播放视频,解码是软解,还要把每一帧从解码器搬进 QWidget,CPU 直接被打满。最终我仍然选择 mplayer,只不过视频输出走 fbdev:
// 启动视频时先隐藏 Qt 主窗口,把帧缓冲让给 mplayer this->hide(); QStringList args; args << "-slave" << "-quiet" << "-vo" << "fbdev:/dev/fb0" // 直接写帧缓冲设备 << "-vf" << "scale=1024:600" // 强制缩放到 LCD 分辨率 << "-zoom" << "-framedrop" // 允许丢帧保持音画同步 << "-ao" << "alsa" << videoPath; m_videoProc = new QProcess(this); m_videoProc->start("mplayer", args);参数说明:-vo fbdev:/dev/fb0让 mplayer 绕过 X/Qt 直接写显存,这是 6818 上最稳的视频显示路径。-vf scale=1024:600这里的数值必须换成你板子 LCD 的真实分辨率,否则视频原始尺寸和屏幕不一致时会出现黑边或者只有中间一块画面。-framedrop在软解吃紧时自动丢帧,优先保证音频连续——一个视频卡顿但声音正常,比音画全乱更容易让人接受。
播放结束时必须把界面还回来。mplayer 进程结束后 Qt 的窗口还处于隐藏状态,很多人第一次写完代码就卡在这:视频放完屏幕一片黑,再点触摸屏没任何反应,只能重启。
// 视频进程退出后:回收资源,恢复 Qt 窗体 connect(m_videoProc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this]() { m_videoProc->close(); delete m_videoProc; m_videoProc = nullptr; this->show(); // 重新显示 Qt 主窗口 m_stack->setCurrentWidget(m_album); });很多人会忽略delete m_videoProc这一步。QProcess 对象不删,mplayer 子进程就算退出了,Qt 这边仍然持有它的相关句柄,第二次播放时可能因为端口或资源占用直接起不来。播放结束后把当前页切回相册,也符合最终作品演示时“视频放完自动回主界面”的流畅预期。
4. 蓝牙控制所有模块:协议设计、消息路由与后台化
4.1 先定指令协议再写代码:给相册、音频、视频各留一个命令空间
蓝牙控制所有模块,最容易翻车的不是蓝牙本身,而是协议没定清楚。我见过有人直接用“1 表示下一张,2 表示播放”这种裸数字做命令,结果视频模块加一个快进功能,整个协议就得推翻重来。正确做法是把命令设计成“模块前缀 + 动作 + 冒号分隔的参数”,三个模块共享一个解析入口。
| 目标模块 | 指令示例 | 实际动作 |
|---|---|---|
| 相册 | ALBUM:NEXT | 查看下一张照片 |
| 相册 | ALBUM:SLIDE | 开始/停止幻灯片播放 |
| 音频 | MUSIC:PLAY | 播放音乐 |
| 音频 | MUSIC:VOL:60 | 音量调到 60% |
| 视频 | VIDEO:PLAY:movie.mp4 | 播放指定视频文件 |
| 视频 | VIDEO:STOP | 停止视频播放 |
为什么用文本协议而不是二进制?因为蓝牙调试时你永远是拿手机端的“蓝牙串口”App 先发指令,文本一眼能看出发的是什么,二进制数据还得准备一套 hex 转换工具。而且文本协议天然能被人读懂,演示现场出问题时能立刻对着手机屏幕判断是协议问题还是播放器问题。指令结尾统一用回车换行,解析端统一做trim(),这样兼容手机 App 发\r或\n的差异。
Qt 端的统一分发入口就是一个大 dispatch 函数,三个模块各自的 handler 方法接收去掉前缀后的剩余部分:
// 所有指令先到这里,再按模块前缀分流 void MediaController::dispatch(const QString &cmd) { if (cmd.startsWith("ALBUM:")) { m_album->handleCommand(cmd.mid(6)); } else if (cmd.startsWith("MUSIC:")) { m_audio->handleCommand(cmd.mid(6)); } else if (cmd.startsWith("VIDEO:")) { m_video->handleCommand(cmd.mid(6)); } else { qWarning() << "unknown command:" << cmd; } }cmd.mid(6)从前缀长度 6 开始截取,ALBUM:NEXT会变成NEXT传给相册模块。这样后续想加亮度调节、音量增量这些新指令,每个模块自己扩展即可,不需要动总线的分发逻辑。整个项目的“蓝牙控制所有模块”最终落点就是这一个函数。
4.2 GEC6818 蓝牙串口侧:把手机 App 变成无线遥控器
板子本身往往不带蓝牙,需要插一个 USB 蓝牙适配器。常见做法是用 BlueZ 协议栈的经典蓝牙 SPP,让板子模拟一个串口服务,手机上的蓝牙串口 App 连上来之后就是收发数据这么简单。先把基础服务配起来:
# 打开蓝牙控制器并设置为可被发现 hciconfig hci0 up hciconfig hci0 piscan # 注册一个 SerialPort 服务,手机蓝牙串口 App 才能搜到 sdptool add SP # 绑定到 /dev/rfcomm0,之后的蓝牙数据流就是一个普通串口 rfcomm bind 0 /dev/rfcomm0参数说明:piscan是“page scan + inquiry scan”,不做这一步手机搜索不到板子。sdptool add SP注册的是蓝牙协议栈里的 SerialPort 服务 UUID,很多手机 App 扫描时只列出带SPP标识的设备,不注册它即使配对成功也连不上。rfcomm bind 0 /dev/rfcomm0的意思是建立 rfcomm 通道 0 与设备节点的映射,之后板子程序打开/dev/rfcomm0就等同于操作蓝牙串口。如果新式镜像里hciconfig已废弃,就用bluetoothctl的power on与discoverable on,但 SPP 绑定那步还是得靠 sdptool 和 rfcomm 这两个老命令。
蓝牙串口在 Linux 下就是一个普通 tty 设备,所以后台路由守护进程可以用最经典的 C 语言写。这个 daemon 只干两件事:读 rfcomm0 的命令行,再通过 Unix domain socket 广播给三个 Qt 模块:
// bt_daemon.c 核心片段:读蓝牙串口并转发 int fd = open("/dev/rfcomm0", O_RDWR | O_NOCTTY); if (fd < 0) { perror("open rfcomm0"); exit(1); } struct termios tty; memset(&tty, 0, sizeof(tty)); cfsetospeed(&tty, B115200); cfsetispeed(&tty, B115200); tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8; // 8 数据位 tty.c_cflag |= (CLOCAL | CREAD); // 忽略调制解调器控制线 tcsetattr(fd, TCSANOW, &tty); char buf[256]; while (fgets(buf, sizeof(buf) - 1, fd)) { strip_crlf(buf); broadcast_to_clients(buf); // 向 /tmp/bt_cmd.sock 发送给各 Qt 模块 }参数说明:蓝牙虚拟串口的波特率实际没多大意义,但必须和手机 App 里选的一致,安卓侧常见可选项是 9600 和 115200,这里直接用 115200。O_NOCTTY是关键参数,不设它的话蓝牙串口会变成守护进程的控制终端,进程收到 Ctrl+C 这类信号时可能被意外杀掉。CLOCAL | CREAD表示忽略 modem 控制线并启用接收,避免没有 CTS/RTS 信号时读不到数据。fgets按行读天然匹配前面设计的文本协议,一条指令一行,解析端不用自己拼缓冲区。
4.3 Qt 端怎么接消息:QLocalSocket 按行读取与断线重连
daemon 和 Qt 之间我选 Unix domain socket 而不是 TCP,原因很简单:本机通信延迟更低,而且不需要处理网络权限和防火墙问题。Qt 端用 QLocalSocket 连上/tmp/bt_cmd.sock即可:
// 连接蓝牙命令守护进程 m_sock = new QLocalSocket(this); m_sock->connectToServer("/tmp/bt_cmd.sock"); connect(m_sock, &QLocalSocket::readyRead, this, [this]() { // 必须按行循环读,防止一条指令被拆到两个数据块里 while (m_sock->canReadLine()) { QString cmd = QString::fromUtf8(m_sock->readLine()).trimmed(); dispatch(cmd); } });readyRead 信号是数据块触发而不是行触发,这是新手最容易踩的坑:一条ALBUM:NEXT\r\n可能被内核拆成ALBUM:NE和XT\r\n两块到达,如果直接读一个固定长度或者只 readLine 一次,就会拿到半条指令然后 dispatch 报“未知命令”。while (canReadLine())循环能把缓冲里的完整行全部取出来,拆包问题从根上就没了。
还有一层要做:如果 daemon 比 Qt 程序晚启动,QLocalSocket 的连接会失败,此时不做重连,蓝牙指令就永远进不来。我习惯用 QTimer 做 3 秒一次的重试:
m_reconnectTimer = new QTimer(this); m_reconnectTimer->setInterval(3000); connect(m_reconnectTimer, &QTimer::timeout, this, [this]() { if (m_sock->state() != QLocalSocket::ConnectedState) { m_sock->abort(); m_sock->connectToServer("/tmp/bt_cmd.sock"); } }); m_reconnectTimer->start();只监听ConnectedState而不监听 Connecting 状态,避免 daemon 启动慢时 Qt 程序反复报错。实际演示时我一般先启动 daemon 再启动 Qt,但把这个重连逻辑留着,现场就算蓝牙适配器掉线重插,整套系统也能自己恢复,比现场跑去找命令重来体面得多。
5. GEC6818 蓝牙与多媒体验收最容易翻车的 5 个坑
5.1 蓝牙硬件和串口的坑:连不上、收不到、时好时坏
坑一:USB 蓝牙插上后/dev/rfcomm0不出现,反而多了一个/dev/ttyACM0。
现象是板子上插了蓝牙适配器,ls /dev能看到 ttyACM0 却找不到 rfcomm0,用cat /dev/ttyACM0还会吐一堆乱码。原因是很多 USB 蓝牙适配器同时带一个 AT 命令用的 modem 接口,内核枚举时默认把它识别成了 USB 串口。解决方法是先确认 hci0 存在:hciconfig hci0 up能过说明蓝牙控制器工作正常,然后用sdptool add SP注册 SPP 服务后rfcomm bind 0 /dev/rfcomm0强制建立映射。如果提示Device or resource busy,先把占用 ttyACM0 的进程找出来杀掉,或者直接拔插一次让内核重新枚举。
坑二:手机能连上板子,但在蓝牙串口 App 里发指令石沉大海。
现象是 App 显示已连接,发任何指令板子都没反应,Qt 界面毫无变化。原因一大半出在服务注册和参数不匹配上:板子没有跑sdptool add SP,手机 App 连上的其实是一个没有 SPP 服务的空通道;或者手机 App 串口参数与 daemon 里设置的不一致。解决方法是先绕过 daemon 做一次裸串口测试:在板子上跑rfcomm listen 0 1,再用手机 App 连接发送随意字符,如果终端里能看到字符,说明链路是通的,问题在 daemon 或协议解析;如果看不到,就回头查 sdptool 注册和波特率设置。这个过程 5 分钟能定位,别一上来就怀疑 Qt 代码,蓝牙链路的玄学十有八九是没分层排查。
5.2 多媒体播放与 Qt 环境的坑:黑屏、无声、触摸漂移
坑三:视频播放完美,但声音没有,或者有声音却从耳机口出不来。
现象是 mplayer 播放视频有画面没声音,音频模块也一样,ALSA 通道检查过 Master 没问题,仍然无声。原因在板载 Codec 的输出路由是多个通道叠加:光调 Master 不够,Speaker、Headphone、PCM 这些通道只要有一个是静音,输出就是零。解决方法是回到第一章第 3.1 那套命令,把amixer scontrols列出的通道挨个设一遍 100%,注意有些镜像默认把输出路由到耳机口,外接音箱要确认Speaker通道不是 mute 状态。这个坑和 Qt 代码毫无关系,属于上板第一天就该做掉的板级配置,很多人卡了两天才发现是被 ALSA 默认状态坑了。
坑四:Qt 界面启动黑屏或触摸完全无响应,重启也没用。
现象是编译好的 Qt 程序上板执行,屏幕一片黑,或者鼠标指针不动、触摸点了没反应。原因是 QWS 环境变量没设置,以及 tslib 的设备节点和校准文件不对。这里没有后悔药可吃,必须在启动脚本里固定几行:export QWS_DISPLAY=LinuxFb:/dev/fb0告诉 Qt 写哪个显示设备,export QWS_MOUSE_PROTO=tslib:/dev/input/eventX告诉 Qt 触摸设备在哪。这个 eventX 不要照抄别人的脚本,用cat /proc/bus/input/devices找名字里带 touch 的那一行,把它对应的 event 编号填进去。校准用ts_calibrate生成/etc/pointercal,一次校准终身受用,但换了触摸屏或者升级内核后必须重新校准,否则点 A 跑 B。
坑五:相册翻大图时界面卡死几秒,蓝牙指令也排队响应。
现象是列表滑动流畅,但点开一张大图要等两三秒,期间整个界面无响应,蓝牙指令全部积压。原因是主线程里直接QImageReader(path).read()然后 scaled 到全屏尺寸,软解一张大图把 UI 线程堵死了。解决方法是全屏大图用后台线程加载,加载完通过信号或事件队列回主线程更新图片控件:
// 后台线程加载大图,主线程保持可响应 m_loading = true; QtConcurrent::run([this, path]() { QImageReader reader(path); reader.setAutoTransform(true); QSize imgSize = reader.size(); // 限制到屏幕分辨率即可,再大就是浪费内存和解码时间 reader.setScaledSize(imgSize.scaled(1024, 600, Qt::KeepAspectRatio)); QImage img = reader.read(); QMetaObject::invokeMethod(this, [this, img]() { m_imageLabel->setPixmap(QPixmap::fromImage(img)); m_loading = false; }, Qt::QueuedConnection); });Qt::QueuedConnection保证回调在主线程执行,避免直接在后台线程操作 UI 控件导致崩溃。如果板级镜像没有 QtConcurrent,改用 QThread 加信号槽也是同样的效果。这个坑不填,蓝牙连上也没用——指令发过来,界面卡在解码里,体验跟死机没区别。
6. 把整套方案后台化:从演示 Demo 变成开机自启的成品
做到这一步,模块功能、蓝牙协议、消息路由都通了,但最终作品验收的标准往往往上是“断电重开,手机一键操作”,而不是打开电脑连串口敲命令。所以最后一步是把蓝牙 daemon 和 Qt 程序都做成开机自动拉起,把整套东西变成一个不依赖调试终端的产品。
蓝牙 daemon 那边,如果板子跑的是 systemd 镜像,直接写一个 service:
[Unit] Description=GEC6818 Bluetooth Command Daemon After=bluetooth.service Requires=bluetooth.service [Service] ExecStart=/usr/bin/bt_daemon -p /dev/rfcomm0 -s /tmp/bt_cmd.sock Restart=on-failure RestartSec=3 [Install] WantedBy=multi-user.targetRestart=on-failure是重点。USB 蓝牙适配器上电时机不确定,daemon 可能在 rfcomm0 还没建立时启动失败,有了自动重启机制,适配器就绪后 3 秒内它会自己恢复。没有 systemd 的老镜像就把启动命令追加到 rc.local 之类的位置,但守护进程写法和这里一致。
Qt 那侧倒是从第 2.3 的启动脚本里再加两件小事:把/dev/rfcomm0的权限放宽,很多板子上 rfcomm0 默认只有 root 能读写,Qt 以普通用户运行就会一直“打开设备失败”,这不是代码问题而是权限问题;再把启动脚本放进开机自启,这样你什么都不用做,板子通电就是一台“蓝牙遥控多媒体终端”。
一套最终作品做到这里,剩下的都是细节打磨。以我带这批板子调过的经验,最后有条习惯想分享给你:音频、视频的 mplayer 参数,每次改动后先回到命令行单独验证一遍,确认没问题再写进 Qt 代码;蓝牙指令先在手机串口 App 上手动发一遍,看到 daemon 有日志输出后再去测界面联动。这套流程看着土,却是替我省掉最多无意义调试时间的做法。希望帮到你。
本文还有配套的精品资源,点击获取