开篇
距离上一次发文已经过了一周了(除去笔者与同学去飞腾相关竞赛的那期),那么现在笔者的工作主要是逐步开始实现申请理由中所立的项目-基于飞腾派实现智能座舱系统,在开始工作之前我们首先需要完善系统环境的配置与实现一个基础的交互Demo,本文实现如下:
1.补全Qt环境
2.Web服务配置
3.Weston桌面环境配置
4.基础交互实现
这边也是直接给出本次所实现的效果演示:
废话不多说,直接进入第一部分:
1.补全Qt环境
对于配置Qt环境首先需要确认系统上的选型,在上一篇文章笔者已经提到在官方提供的系统镜像中Ubuntu_xfce已经将QtBase配置完整了,而对于PhytiumOS及OpenKylin而言,前者是仅配置了基础环境而无QtBase,后者则是由于其资源占用太高而不适合作为嵌入式实现,因此本次笔者建议使用Ubuntu_xfce实现,当然,负责任的笔者仍然会给出在非Ubuntu上的必需库补全的指令:
安装Qt5基础库:sudo apt-get install qt5-default
安装QtCreator IDE:sudo apt-get install qtcreator
安装qmake编译工具:sudo apt-get install qt5-qmake
安装cmake编译工具:sudo apt install cmake
安装g++/gcc编译器:sudo apt install g++ gcc
上面就是Qt的基础安装指令,倘若有其他的必需库缺失就得自行在百度/谷歌寻求答案了,包括Ubuntu在内,目前通过apt及其它包管理器的方式所安装的Qt只是基础能力,诸如Multimedia WebEngine这些是不自带的,因此还得我们手动补全,具体指令如下:
安装Multimedia库:
sudo apt-get install qtmultimedia5-dev
(由于笔者已经安装完了,截图只是用于演示)
sudo apt-get install libqt5multimedia5-plugins
安装WebEngine库:
sudo apt-getinstall libqt5webenginewidgets5
先安装这两个库,这两个库分别用于Qt的影音内容处理和用于在Qt中创建WebEngineView(基于Chromium)嵌入网页内容;OpenCV及libtorch之后需要用到的时候在安装
2.Web服务配置
本次笔者的项目还需要用到Node.js/TypeScript环境,具体原因文章后面会提到,先来讲讲如何安装
首先为了方便后续进行编译和调试,先来装一个Visual Studio Code:Arm64版下载链接
下载下来默认是deb安装包,可以使用dpkg命令或是直接双击通过包管理器安装即可
接着是安装Node.js:sudo apt install nodejs
但是,这里要说但是了,有些列文虎克或许很熟悉Nodejs,这里所安装的Node.js是10.19.0的版本
这个版本是相对滞后的,在编译一些相对较新的SDK时就会发生各类错误,例如
这个问题就是由于Node.js版本太低不支持flatMap而产生的错误,要解决也很简单:
首先,先安装一个npm:sudo apt-get install npm
,这个软件包与PIP之于Python的地位相同
紧接着我们使用这个npm命令安装一个n模块,这个模块是用于管理Node.js版本的一个工具:sudo npm install -g n
然后我们再利用这个工具安装Node.js的最新LTS版本:sudo n stable
执行完以上的命令之后咱们再使用node -v(注意,Python中V是大写在node中v是小写)命令
这里显示是20.10.0即可,有些人可能还是会显示10版本,此时你需要做的是重新打开一遍终端再检查就是最新版本了
3.Weston桌面环境配置
Weston是什么,要讲这个得先讲Wayland和X11的关系:
X11是一个非常老旧的渲染协议,从1987年发布了第一个版本X11R1至今36年了,在设计的该协议的时候受限于当时PC的性能相对较弱,这样设计可以减轻用户终端的计算负担将更多的任务放在服务器上,因此X11的最本质特征就是所有渲染相关都在服务端(指X11协议的具体实现)进行
而Wayland则不同,当客户端(指运行在用户终端上的软件)有渲染需求的时候,所有的工作都在客户端执行,这减少了客户端与服务端的频繁通讯,也因此Wayland可以实现更小的性能代价,例如在X11中实现高刷新率容易出现屏幕撕裂,而Wayland则不再出现这种问题
而Weston就是基于Wayland协议实现的最小DE/Compositor,它可以作为我们嵌入式的基底进行开发
安装weston:sudo apt-get install weston
通常一个TTY只能运行一个Compositor,因此我们需要进入另一个TTY之中,同时按crtl alt 以及F1-7(自己选),Ubuntu_xfce默认的xfce是在F5中
接着在终端中输入weston并回车,即可进入weston环境中,环境相当干净仅有一个Shell应用
此时我们Weston的环境就算装好了,之后的配置就是通过修改Weston.ini配置文件来将它多余不需要的功能例如顶部Dock栏这些禁用掉,详细可以参考Weston.ini配置文件等教程进行修改
4.基础交互实现
终于可以开始我们的软件实现了,系统的配置往往是最令人费神的,至少在我玩懂LFS之前还是软件上的实现更令人着迷
首先是功能的设计:
1.最起码实现一个可以问答的交互式AI,否则实现之后它就是一个没有价值的Demo
2.最起码有个时间显示,平时不用的时候可以当一个时钟摆件看看日期看看时间
3.最起码要有个AI形象,不然光是放那也没啥意思,看着不养眼光看时间去了
先实现上面三个需求,后续在考虑增减功能:
交互式AI实现
我们直接采用最简单粗暴的方式准备下面四个组件:
1.QListWidget:用于实现一个基础的聊天框
2.QTextEdit:用于用户输入提问内容
3.QPushButton:用于控制文本发送
4.OpenAI API:用于对接GPT问答模型
关于第四个使用OpenAI的API其实原本计划是使用笔者原来在校科研组时期开发的动态服务模型的(这是一个旨在将多个本地模型服务与商用模型接口通过插件式开发接口高效的抽象成一个对外服务API,具备高效迁移和服务高鲁棒性的特点),但是由于笔者的服务器挂在家庭宽带下最近受到了爆破端口,安全狗已经将服务器与外网封锁了,需要待笔者回家重启设备之后才能使用,因此现在先用OpenAI进行实现
那么模块的样式设计直接从简,使用Widget实现:
(问答模块设计)
核心代码实现,函数命名和注释都有规范写好,就不具体介绍了,相信各位都看得懂:
void MainWindow::onSendBoxTextChanged()
{
QString userInput = sendBox->toPlainText().trimmed();
sendButton->setEnabled(!userInput.isEmpty());
}
void MainWindow::onSendButtonClicked()
{
sendButton->setEnabled(false);
QString userInput = sendBox->toPlainText().trimmed();
if (userInput.isEmpty()) {
QMessageBox::warning(this, "Warning", "Please enter a message before sending.");
return;
}
addMessage(userInput, QNChatMessage::User_Me);
QNetworkRequest request(QUrl("https://api.openai-proxy.com/v1/chat/completions"));
request.setRawHeader("Authorization", "Bearer 这里填入密钥");
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject message;
message["role"] = "user";
message["content"] = userInput;
QJsonArray messages;
messages.append(message);
QJsonObject requestData;
requestData["model"] = "gpt-3.5-turbo";
requestData["messages"] = messages;
requestData["temperature"] = 0.7;
QNetworkReply *reply = networkManager->post(request, QJsonDocument(requestData).toJson());
connect(reply, &QNetworkReply::finished, this, [=]() {
if (reply->error() == QNetworkReply::NoError) {
processApiResponse(reply->readAll());
} else {
QMessageBox::warning(this, "Error", "Failed to connect to OpenAI API.");
}
reply->deleteLater();
});
sendBox->clear();
}
void MainWindow::addMessage(const QString &text, QNChatMessage::User_Type userType)
{
QNChatMessage *chatMessage = new QNChatMessage;
chatMessage->setText(text, "123456789", QSize(300, 50), userType);
QSize messageSize = chatMessage->fontRect(text);
QListWidgetItem *item = new QListWidgetItem(chatBox);
item->setSizeHint(messageSize);
chatBox->addItem(item);
chatBox->setItemWidget(item, chatMessage);
}
void MainWindow::processApiResponse(const QString &response)
{
qDebug() << "API Response:" << response;
QJsonDocument jsonResponse = QJsonDocument::fromJson(response.toUtf8());
QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("choices") && jsonObject["choices"].isArray()) {
QJsonArray choicesArray = jsonObject["choices"].toArray();
if (!choicesArray.isEmpty()) {
QJsonObject firstChoice = choicesArray.at(0).toObject();
if (firstChoice.contains("message") && firstChoice["message"].isObject()) {
QJsonObject messageObject = firstChoice["message"].toObject();
if (messageObject.contains("content") && messageObject["content"].isString()) {
QString generatedText = messageObject["content"].toString();
addMessage(generatedText, QNChatMessage::User_She);
return;
}
}
}
}
QMessageBox::warning(this, "Error", "Invalid API response format.");
sendButton->setEnabled(true);
}
那么光是显示内容也不好看,这边引用了别的大佬开源的气泡代码,非常好看非常nice,这里尊重作者,不再分发开源代码,各位可移步阅读引用代码链接
实现效果:
时间模块实现
1.显示日期年月日
2.显示星期
3.显示时间,本人喜欢12小时计时
效果图:
核心代码:
void MainWindow::updateDateTime()
{
QDateTime currentDateTime = QDateTime::currentDateTime();
QString dateText = "新历:" + currentDateTime.toString("yyyy年MM月dd日");
ui->DateLabel->setText(dateText);
QString timeText = currentDateTime.toString("AP hh:mm:ss");
ui->TimeLabel->setText(timeText);
QString weekText = currentDateTime.toString("dddd");
static const QMap<QString, QString> weekMap = {
{"Monday", "星期一"},
{"Tuesday", "星期二"},
{"Wednesday", "星期三"},
{"Thursday", "星期四"},
{"Friday", "星期五"},
{"Saturday", "星期六"},
{"Sunday", "星期日"}
};
if (weekMap.contains(weekText)) {
weekText = weekMap.value(weekText);
}
ui->WeekLabel->setText(weekText);
}
void MainWindow::startTimer()
{
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &MainWindow::updateDateTime);
timer->start(1000);
}
AI形象
那么笔者选择实现AI形象的方式就是通过Live2D进行,3D的性能消耗大不说,形象实现也更麻烦昂贵,因此2D小人足矣
当然这个部分要比上面的实现要繁琐很多,原本笔者的实现是通过Live2D的C++ SDK进行实现的,早期的笔者的相关应用:
上面就是通过OpenGLWidget回调Live2D C++ SDK的方式在Qt中实现了Live2D的显示,那么为什么笔者不复用原有代码的实现呢?
原因很简单,Live2D SDK闭源,且C++不提供AArch64支持
那么笔者前面提到安装的WebEngine就派上用场了,经常查资料看博客的小伙伴应该经常会在私人博客上看到一些二次元小人,比如这样:
那么我们都知道,JS是跨平台部署语言,不管你是IA64 还是AArch64,亦或是Mips,甚至LoongArch都有对应的支持,于是乎,使用JS的SDK就是我们最佳的方案了,虽然笔者没有做过任何TS/JS相关的开发,但是道理是相通的,我们只要将它们给予的SDK编译并修改Demo以适配我们的需求即可
第一步:下载SDK并通过VSCode打开:
下载SDK
注意,其有不可再分发原则
第二步:将SDK中的背景与设置相关的可视化内容删除:
1.lappdefine.ts:
2.lappview.ts:
将画像の初期化を行う。 之后的所有内容替换成如下代码:
public initializeSprite(): void {
if (this._programId == null) {
this._programId = LAppDelegate.getInstance().createShader();
}
}
public onTouchesBegan(pointX: number, pointY: number): void {
this._touchManager.touchesBegan(pointX, pointY);
}
public onTouchesMoved(pointX: number, pointY: number): void {
const viewX: number = this.transformViewX(this._touchManager.getX());
const viewY: number = this.transformViewY(this._touchManager.getY());
this._touchManager.touchesMoved(pointX, pointY);
const live2DManager: LAppLive2DManager = LAppLive2DManager.getInstance();
live2DManager.onDrag(viewX, viewY);
}
public onTouchesEnded(pointX: number, pointY: number): void {
const live2DManager: LAppLive2DManager = LAppLive2DManager.getInstance();
live2DManager.onDrag(0.0, 0.0);
{
const x: number = this._deviceToScreen.transformX(
this._touchManager.getX()
);
const y: number = this._deviceToScreen.transformY(
this._touchManager.getY()
);
if (LAppDefine.DebugTouchLogEnable) {
LAppPal.printMessage(`[APP]touchesEnded x: ${x} y: ${y}`);
}
live2DManager.onTap(x, y);
}
}
public transformViewX(deviceX: number): number {
const screenX: number = this._deviceToScreen.transformX(deviceX);
return this._viewMatrix.invertTransformX(screenX);
}
public transformViewY(deviceY: number): number {
const screenY: number = this._deviceToScreen.transformY(deviceY);
return this._viewMatrix.invertTransformY(screenY);
}
public transformScreenX(deviceX: number): number {
return this._deviceToScreen.transformX(deviceX);
}
public transformScreenY(deviceY: number): number {
return this._deviceToScreen.transformY(deviceY);
}
_touchManager: TouchManager;
_deviceToScreen: CubismMatrix44;
_viewMatrix: CubismViewMatrix;
_programId: WebGLProgram;
_changeModel: boolean;
_isClick: boolean;
}
第三步:将背景设置为透明通道:
修改lappdelegate.ts文件:
1.将66行:
修改为:
gl = canvas.getContext('webgl', { alpha: true }) || canvas.getContext('experimental-webgl', { alpha: true })
2.将160行:
修改为
gl.clearColor(0.0, 0.0, 0.0, 0.0);
修改的部分就结束了,接着是编译bundle.js文件了,注意,这个编译必须在飞腾派上进行,笔者仅是为了方便撰文方才用Windows演示,否则从Windows迁移到Linux有概率报错,同时编译过程中如果非缺失库导致中断,请尝试再次编译即可通过:
VSCode安装插件:EditorConfig for VS Code
用VSCode打开SDK根目录:
而后执行如下:
按住ctrl + shift + P,在选项栏里找到Tasks:Run Task
点击之后选择npm:install -Samples/TypeScript/Demo
运行完成之后:
按住ctrl + shift + B,在选项栏里找到npm:build -Samples/TypeScript/Demo
此时运行完了之后按官方教程会建议你通过VSCode进行服务器启动,但是笔者不建议使用,一来官方示例使用的是Chrome,而通常发行版配置的是Firefox,再次安装Chromium会把问题复杂化,改配置为Firefox也不符合我们的需求,因此咱们直接安装http-server命令使用
打开终端安装:sudo npm install http-server -g
安装完成之后我们还要配置服务文件夹
创建一个新的文件夹或者直接用bundle生成所在的dist文件夹也行
将Resources文件夹,index.html,live2dcubismcore.js文件从SDK文件夹里面找到并复制过来:
修改index.html文件中指向的文件路径,如下:
而后,从终端cd进入或直接在文件夹中调用终端输入http-server即可启动服务端
然后我们就该进入Qt中创建WebEngineView并设置为透明背景了
核心代码:
void MainWindow::loadWebPage()
{
QString url = "http://127.0.0.1:8080";
webEngineView->setStyleSheet("background-color: transparent;");
webEngineView->setAttribute(Qt::WA_TranslucentBackground);
webEngineView->setAttribute(Qt::WA_OpaquePaintEvent, false);
webEngineView->setAttribute(Qt::WA_NoSystemBackground, true);
webEngineView->setUrl(QUrl(url));
webEngineView->page()->setBackgroundColor(Qt::transparent);
webEngineView->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
}
然后这就是最终成品了,还支持触摸非常不错: