当前位置: 首页 > article >正文

Qt u盘自动升级软件

Qt u盘自动升级软件

  • Chapter1 Qt u盘自动升级软件
    • u盘自动升级软件思路:
    • step1. 获取U盘 判断U盘名字是否正确, 升级文件是否存在。
    • step2. 升级
    • step3. 升级界面
  • Chapter2 Qt 嵌入式设备应用程序,通过U盘升级的一种思路
  • Chapter3 在开发板上运行的QT应用程序,如何拷贝文件到U盘
  • Chapter4 嵌入式Qt,U盘升级程序
    • 前言
    • 一、实现过程
      • 1.检测U盘中的更新程序
      • 2. 数字验证
      • 3.升级程序
      • 4.主函数
    • 二、总结


Chapter1 Qt u盘自动升级软件

原文链接

u盘自动升级软件思路:

1.检测U盘是否存在

2.检测升级文件是否存在,升级文件版本是否比当前软件版本新

3.软件升级

step1. 获取U盘 判断U盘名字是否正确, 升级文件是否存在。

//获取U盘  判断U盘名字是否正确, 升级文件是否存在。
void MainWindow::getUDisk()
{
    QMap<QString,QString> namePath;
    foreach (const QStorageInfo &storage, QStorageInfo::mountedVolumes())
    {
        if (storage.isValid() && storage.isReady())
        {
            UDiskPath = storage.rootPath();
            namePath.insert(storage.displayName(),storage.rootPath());
        }
    }
    QString path = namePath.value("LADYBUG");
    QFile file(path+"/"+app::updateFile);
    if(file.exists())
    {
        //存在 弹窗是否升级
    }
    else
    {
        //不存在
    }
}

step2. 升级

    OperateThread *thread = new OperateThread;
    thread->setUpgradeFile(strSrcFile);
    ShowProgressDialog dialog(this);
 
    connect(thread, SIGNAL(emitFinish(int)), &dialog, SLOT(onThreadFinished(int)));
 
    thread->start();
 
    dialog.exec();

step3. 升级界面

class ShowProgressDialog : public QDialog
{
    Q_OBJECT
public:
    explicit ShowProgressDialog(QWidget *parent = 0);
    ~ShowProgressDialog();
 
    void setTitleText(const QString &strText);
    void setMessageText(const QString &strMsg);
 
public slots:
    void on_btnOk_clicked();
    void onThreadFinished(int nExitCode);
 
private:
    QLabel          *m_labelTitle;
    QLabel          *m_labelMsg;
    QPushButton     *m_btnOk;
    QLabel          *m_labelWait;
    QMovie          *m_pMovie;
};
 
ShowProgressDialog::ShowProgressDialog(QWidget *parent) : QDialog(parent)
{
    setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Dialog);
    setAttribute(Qt::WA_X11DoNotAcceptFocus);
    setFocusPolicy(Qt::NoFocus);
    //更改背景色
    QPalette palette = this->palette();
    QPixmap pix(":/res/dialog_bg.png");
    palette.setBrush(QPalette::Background,QBrush(pix));
    setAutoFillBackground(true);
    this->setPalette(palette);
 
    resize(410, 260);
    setMinimumSize(QSize(410, 260));
    setMaximumSize(QSize(410, 260));
 
    m_labelTitle = new QLabel(this);
    m_labelMsg = new QLabel(this);
    m_btnOk = new QPushButton(this);
    m_labelWait = new QLabel(this);
    m_pMovie = new QMovie(":/res/loading.gif");
 
    m_labelTitle->setGeometry(QRect(10, 1, 220, 30));
    m_labelMsg->setGeometry(QRect(30, 60, 350, 60));
    m_labelMsg->setWordWrap(true);
    m_btnOk->setGeometry(QRect(320, 180, 58, 58));
    m_labelWait->setGeometry(QRect(30, 120, 322, 18));
 
    m_labelWait->setMovie(m_pMovie);
    m_pMovie->start();
 
    m_labelTitle->setStyleSheet("font: bold 17px; color: white;");
    m_labelMsg->setStyleSheet("font: bold 17px ; color: black; ");
 
    m_btnOk->setStyleSheet("QPushButton{background-image: url(:/res/dialog_ok.png);border: 0px;}"
                           "QPushButton:pressed{background-image: url(:/res/dialog_ok_p.png);border: 0px;}");
 
    connect(m_btnOk , SIGNAL(clicked()) , this , SLOT(on_btnOk_clicked()));
 
    setTitleText("升级");
    setMessageText("正在升级,请勿插拔U盘!");
    m_btnOk->hide();
}
 
ShowProgressDialog::~ShowProgressDialog()
{
    delete m_labelTitle;
    delete m_labelMsg;
    delete m_btnOk;
}
 
void ShowProgressDialog::setTitleText(const QString &strText)
{
    m_labelTitle->setText(strText);
}
 
void ShowProgressDialog::setMessageText(const QString &strMsg)
{
    m_labelMsg->setText(strMsg);
}
 
void ShowProgressDialog::on_btnOk_clicked()
{
    QDialog::accept();
}
 
void ShowProgressDialog::onThreadFinished(int nExitCode)
{
    qDebug() << "nExitCode" << nExitCode;
    if (nExitCode == 0)
    {
        setMessageText("升级成功,请重新上电。");
        m_btnOk->show();
        m_pMovie->stop();
    }
    else
    {
        setMessageText("升级出错!请断电以恢复!");
        m_btnOk->show();
        m_pMovie->stop();
    }
}

注意:线程,继承自QThread,完成复制工作。使用QProcess来完成Linux下的解压和删除的工作。关于Qt的多线程,可以去查找一下其他的教程,这里就不做过多的解释。

class OperateThread : public QThread
{
    Q_OBJECT
public:
    explicit OperateThread(QObject *parent = 0);
 
public:
    void setUpgradeFile(QString strUpgradeFile)
    {
        m_UpgradeFile = strUpgradeFile;
    }
 
protected:
    void run();
 
signals:
    void emitFinish(int);
 
public slots:
 
private:
    void OprUpgradeUp();
 
private:
    QString  m_UpgradeFile;
};
 
OperateThread::OperateThread(QObject *parent) : QThread(parent)
{
 
}
 
void OperateThread::run()
{
    //很复杂的数据处理
    OprUpgradeUp();
}
 
void OperateThread::OprUpgradeUp()
{
#define DEST_DIR    "../"
 
    QString destDir = QString("%1/update.tar.gz").arg(DEST_DIR);
 
    if( QFile::copy(m_UpgradeFile, destDir) )
    {
        qDebug()<<"-------成功-----------";
    }
    else
    {
        qDebug()<<"-------出错-----------";
        //升级失败
        emit emitFinish(1);
        return;
    }
 
#ifdef Q_OS_LINUX
    QString exe = QString("tar -zxvf %1 -C %2").arg(destDir).arg(DEST_DIR);
    QProcess::execute(exe);
 
    //升级成功,自动同步磁盘并删除ARM上的升级包
    exe = QString("sync");
    QProcess::execute(exe);
    exe = QString("rm -rf %1").arg(destDir);
    QProcess::execute(exe);
#endif
    //升级成功
    emit emitFinish(0);
}

Chapter2 Qt 嵌入式设备应用程序,通过U盘升级的一种思路

原文链接:https://blog.csdn.net/qq1113231395/article/details/81867153

最近在做一个通过U盘升级的功能,程序是运行在ARM Linux Qt平台上的。这个应该是很多嵌入式设备必备的一个功能了,所以把这部分的实现抽出来,做成一个例子供需要的人参考。这只是U盘升级的一种思路,如果有更好的方法,也可以提供相应的意见。

源码下载:softwareupgrade.tar.gz

升级文件的格式是通过tar压缩后的文件以gz结尾的, 可以通过tar命令生成相应的升级文件如update.tar.gz:

tar -czvf update.tar.gz demo

主要思路就是,点击相应的功能后从U盘中选中需要升级的文件update.tar.gz,之后开启一个线程和一个提示正在升级的对话框,以免让用户觉得假死。在线程中完成的事情是:将选中的升级文件复制到应用程序的目录中,然后将update.tar.gz 解压覆盖原来的程序。最后将update.tar.gz删除。发送一个线程结束的信号。

下面就介绍一下实现过程。

MainWindow很简单,只有一个按钮。 点击按钮开启线程,显示提示框,绑定线程结束后的信号。提示框根据信号的参数值来确定升级是否成功。

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}
 
MainWindow::~MainWindow()
{
    delete ui;
}
 
void MainWindow::on_pushButton_clicked()
{
    QString strSrcFile = QFileDialog::getOpenFileName(this, "选择升级文件", ".", "tar (*.gz)");
    qDebug() << "strSrcFile" << strSrcFile;
 
    OperateThread *thread = new OperateThread;
    thread->setUpgradeFile(strSrcFile);
    ShowProgressDialog dialog(this);
 
    connect(thread, SIGNAL(emitFinish(int)), &dialog, SLOT(onThreadFinished(int)));
 
    thread->start();
 
    dialog.exec();
}

运行的效果图:
在这里插入图片描述

提示框: void onThreadFinished(int nExitCode) 线程结束后更新提示框。里面还使用到了一些qss,来设置背景。

class ShowProgressDialog : public QDialog
{
    Q_OBJECT
public:
    explicit ShowProgressDialog(QWidget *parent = 0);
    ~ShowProgressDialog();
 
    void setTitleText(const QString &strText);
    void setMessageText(const QString &strMsg);
 
public slots:
    void on_btnOk_clicked();
    void onThreadFinished(int nExitCode);
 
private:
    QLabel          *m_labelTitle;
    QLabel          *m_labelMsg;
    QPushButton     *m_btnOk;
    QLabel          *m_labelWait;
    QMovie          *m_pMovie;
};
 
ShowProgressDialog::ShowProgressDialog(QWidget *parent) : QDialog(parent)
{
    setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Dialog);
    setAttribute(Qt::WA_X11DoNotAcceptFocus);
    setFocusPolicy(Qt::NoFocus);
    //更改背景色
    QPalette palette = this->palette();
    QPixmap pix(":/res/dialog_bg.png");
    palette.setBrush(QPalette::Background,QBrush(pix));
    setAutoFillBackground(true);
    this->setPalette(palette);
 
    resize(410, 260);
    setMinimumSize(QSize(410, 260));
    setMaximumSize(QSize(410, 260));
 
    m_labelTitle = new QLabel(this);
    m_labelMsg = new QLabel(this);
    m_btnOk = new QPushButton(this);
    m_labelWait = new QLabel(this);
    m_pMovie = new QMovie(":/res/loading.gif");
 
    m_labelTitle->setGeometry(QRect(10, 1, 220, 30));
    m_labelMsg->setGeometry(QRect(30, 60, 350, 60));
    m_labelMsg->setWordWrap(true);
    m_btnOk->setGeometry(QRect(320, 180, 58, 58));
    m_labelWait->setGeometry(QRect(30, 120, 322, 18));
 
    m_labelWait->setMovie(m_pMovie);
    m_pMovie->start();
 
    m_labelTitle->setStyleSheet("font: bold 17px; color: white;");
    m_labelMsg->setStyleSheet("font: bold 17px ; color: black; ");
 
    m_btnOk->setStyleSheet("QPushButton{background-image: url(:/res/dialog_ok.png);border: 0px;}"
                           "QPushButton:pressed{background-image: url(:/res/dialog_ok_p.png);border: 0px;}");
 
    connect(m_btnOk , SIGNAL(clicked()) , this , SLOT(on_btnOk_clicked()));
 
    setTitleText("升级");
    setMessageText("正在升级,请勿插拔U盘!");
    m_btnOk->hide();
}
 
ShowProgressDialog::~ShowProgressDialog()
{
    delete m_labelTitle;
    delete m_labelMsg;
    delete m_btnOk;
}
 
void ShowProgressDialog::setTitleText(const QString &strText)
{
    m_labelTitle->setText(strText);
}
 
void ShowProgressDialog::setMessageText(const QString &strMsg)
{
    m_labelMsg->setText(strMsg);
}
 
void ShowProgressDialog::on_btnOk_clicked()
{
    QDialog::accept();
}
 
void ShowProgressDialog::onThreadFinished(int nExitCode)
{
    qDebug() << "nExitCode" << nExitCode;
    if (nExitCode == 0)
    {
        setMessageText("升级成功,请重新上电。");
        m_btnOk->show();
        m_pMovie->stop();
    }
    else
    {
        setMessageText("升级出错!请断电以恢复!");
        m_btnOk->show();
        m_pMovie->stop();
    }
}

线程,继承自QThread,完成复制工作。使用QProcess来完成Linux下的解压和删除的工作。关于Qt的多线程,可以去查找一下其他的教程,这里就不做过多的解释。

class OperateThread : public QThread
{
    Q_OBJECT
public:
    explicit OperateThread(QObject *parent = 0);
 
public:
    void setUpgradeFile(QString strUpgradeFile)
    {
        m_UpgradeFile = strUpgradeFile;
    }
 
protected:
    void run();
 
signals:
    void emitFinish(int);
 
public slots:
 
private:
    void OprUpgradeUp();
 
private:
    QString  m_UpgradeFile;
};
 
OperateThread::OperateThread(QObject *parent) : QThread(parent)
{
 
}
 
void OperateThread::run()
{
    //很复杂的数据处理
    OprUpgradeUp();
}
 
void OperateThread::OprUpgradeUp()
{
#define DEST_DIR    "../"
 
    QString destDir = QString("%1/update.tar.gz").arg(DEST_DIR);
 
    if( QFile::copy(m_UpgradeFile, destDir) )
    {
        qDebug()<<"-------成功-----------";
    }
    else
    {
        qDebug()<<"-------出错-----------";
        //升级失败
        emit emitFinish(1);
        return;
    }
 
#ifdef Q_OS_LINUX
    QString exe = QString("tar -zxvf %1 -C %2").arg(destDir).arg(DEST_DIR);
    QProcess::execute(exe);
 
    //升级成功,自动同步磁盘并删除ARM上的升级包
    exe = QString("sync");
    QProcess::execute(exe);
    exe = QString("rm -rf %1").arg(destDir);
    QProcess::execute(exe);
#endif
    //升级成功
    emit emitFinish(0);
}

Chapter3 在开发板上运行的QT应用程序,如何拷贝文件到U盘

原文链接:https://blog.csdn.net/pang_fighting/article/details/139114780

笔者最近一直被这个问题所困惑,好在今天已经解决,之前找了很多资料,试了其他家的使用QProcess类来实现QT应用下的命令行实现,但很遗憾还是不行,找到新的解决方法如下:

system("cp -r /home/root/test_data /run/media/sda1");
system("sync");
system("umount /run/media/sda1");

解释一下,这需要你的开发板移植的系统能够插入U盘直接挂载,否则需要先检测U盘插入再挂载U盘,才可以使用,笔者用的正点原子157开发板,使用的是他们的官方系统,这个系统是支持插入U盘直接挂载的,挂载目录是“/run/media/sda1”。

U盘挂载完成之后,使用C函数system来执行命令行拷贝文件,拷贝完成之后执行sync命令,最后直接取消挂载拔出U盘即可!

Chapter4 嵌入式Qt,U盘升级程序

原文链接:https://blog.csdn.net/sjd753/article/details/135284869

前言

近期在完成一个U盘升级功能,对于Arm嵌入式设备来说应该算是必备的了。

参考链接:

https://blog.csdn.net/newnewman80/article/details/8766657
https://blog.csdn.net/newnewman80/article/details/8766657

一、实现过程

1.检测U盘中的更新程序

结合netlink捕获USB的热拔插,我们需要去检测U盘的挂载点,通常情况下都是挂载在/media下。

bool checkConsoleAppExists() 
{
    DIR *dir = opendir("/run/media"); // 打开 /run/media 目录 具体的挂载点根据设备决定
    if (dir) 
    {
        struct dirent *entry;
        while ((entry = readdir(dir)) != NULL) 
        {
            if (strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) 
            {
                devpath = "/run/media/" + std::string(entry->d_name);
                path = "/run/media/" + std::string(entry->d_name) + "/updateFileName";
 
                std::string signatureFilePath = devpath +"/sigFileName.sig"; 
                std::string publicKeyPath = devpath + "/key.pub" ;
 
                if (access(path.c_str(), F_OK) != -1) 
                {
                    //检测到更新程序,可以做一些校验,我这里的话是验证数字签名,根据具体情况决定
                    return checkUpdate(signatureFilePath,publicKeyPath,path);
                }
            }
        }
        closedir(dir);
    }
    return false;
}

2. 数字验证

/*检测是否需要更新,是否为正确的更新程序*/
bool checkUpdate(const std::string& signatureFile, const std::string& publicKeyFile, const std::string& dataFile) {
    // 打开更新文件
    // 这里是想要去检测版本号,去判断是否需要升级的,后面由于使用了数字签名就没有实现这部分。
    FILE *updateFile = fopen(dataFile.c_str(), "rb");
    if (!updateFile) {
        perror("Error opening update file\n");
        return false;
    }
    // 验证数字签名
    return verifySignature(signatureFile,publicKeyFile,dataFile);
}
// 函数用于验证数字签名是否有效
bool verifySignature(const std::string& signatureFile, const std::string& publicKeyFile, const std::string& dataFile) 
{
    // 打开公钥文件以读取公钥信息
    FILE* fp = fopen(publicKeyFile.c_str(), "r");
    if (!fp) {
        std::cerr << "Error opening public key file." << std::endl;
        return false;
    }
 
    // 从公钥文件中读取 RSA 公钥信息
    RSA* rsa = PEM_read_RSA_PUBKEY(fp, NULL, NULL, NULL);
    fclose(fp);
 
    // 检查是否成功读取公钥信息
    if (!rsa) {
        std::cerr << "Error reading public key." << std::endl;
        return false;
    }
 
    // 创建一个用于存储 RSA 公钥的 EVP_PKEY 对象
    EVP_PKEY* evpKey = EVP_PKEY_new();
 
    // 将 RSA 公钥赋值给 EVP_PKEY 对象
    if (!EVP_PKEY_assign_RSA(evpKey, rsa)) {
        std::cerr << "Error assigning RSA key." << std::endl;
        RSA_free(rsa);
        return false;
    }
 
    // 创建一个用于消息摘要计算的 EVP_MD_CTX 对象
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    if (!ctx) {
        std::cerr << "Error creating context." << std::endl;
        RSA_free(rsa);
        return false;
    }
 
    // 打开数据文件以进行签名验证
    std::ifstream fileStream(dataFile, std::ios::binary | std::ios::ate);
    if (!fileStream.is_open()) {
        std::cerr << "Error opening data file." << std::endl;
        EVP_MD_CTX_free(ctx);
        RSA_free(rsa);
        return false;
    }
 
    // 读取数据文件的大小和内容
    std::streamsize fileSize = fileStream.tellg();
    fileStream.seekg(0, std::ios::beg);
    std::vector<unsigned char> fileData(fileSize);
    if (!fileStream.read(reinterpret_cast<char*>(fileData.data()), fileSize)) {
        std::cerr << "Error reading data file." << std::endl;
        fileStream.close();
        EVP_MD_CTX_free(ctx);
        RSA_free(rsa);
        return false;
    }
    fileStream.close();
 
    // 打开签名文件以进行签名验证
    std::ifstream signatureFileStream(signatureFile, std::ios::binary);
    if (!signatureFileStream.is_open()) {
        std::cerr << "Error opening signature file." << std::endl;
        EVP_MD_CTX_free(ctx);
        RSA_free(rsa);
        return false;
    }
 
    // 读取签名文件的大小和内容
    signatureFileStream.seekg(0, std::ios::end);
    size_t signatureFileSize = signatureFileStream.tellg();
    signatureFileStream.seekg(0, std::ios::beg);
 
    std::vector<unsigned char> signatureData(signatureFileSize);
    if (!signatureFileStream.read(reinterpret_cast<char*>(signatureData.data()), signatureFileSize)) {
        std::cerr << "Error reading signature file." << std::endl;
        signatureFileStream.close();
        EVP_MD_CTX_free(ctx);
        RSA_free(rsa);
        return false;
    }
    signatureFileStream.close();
 
    // 更新消息摘要的内容,计算数据文件的哈希值
    if (!EVP_VerifyUpdate(ctx, fileData.data(), fileSize)) {
        std::cerr << "Error updating verification." << std::endl;
        EVP_MD_CTX_free(ctx);
        RSA_free(rsa);
        return false;
    }
 
    // 验证签名的有效性
    int result = EVP_VerifyFinal(ctx, signatureData.data(), signatureFileSize, evpKey);
    EVP_MD_CTX_free(ctx);
    RSA_free(rsa);
 
    // 根据验证结果返回相应的布尔值
    if (result != 1) {
        std::cerr << "Signature verification failed." << std::endl;
        return false;
    }
    std::cout << "Signature verification successful." << std::endl;
    return true;
}

3.升级程序

升级程序的话,实际上就是一个拷贝的过程。将设备上的备份,U盘中的更新程序拷贝出来。

/* 更新程序 */
bool copyFile(const char *sourcePath, const char *destinationPath) {
    
    //system("mv updateFileName  updateFileName.back"); //这里是一个备份,后续可以进行一个升级失败,备份还原。
 
    FILE *sourceFile = fopen(sourcePath, "rb");
    FILE *destinationFile = fopen(destinationPath, "wb");
 
    if (sourceFile == NULL) {
        printf("Error opening sourceFile files.\n");
        return false;
    }
    if(destinationFile == NULL){
        printf("Error opening  destinationFile files.\n");
        return false;
    }
    char buffer[1024];
    size_t bytesRead;
    while ((bytesRead = fread(buffer, 1, sizeof(buffer), sourceFile)) > 0) {
        printf("start copy file........................\n");
        fwrite(buffer, 1, bytesRead, destinationFile);
    }
 
    /*更简单一点就是用system命令,直接拷贝
      不过有一点需要注意,程序在运行的时候,有时候会拷贝失败。
      可以先用system命令删除或者备份,然后再进行拷贝,再用system给个权限。
    */
 
    fclose(sourceFile);
    fclose(destinationFile);
    return true;
}
 

4.主函数

int main(void)
{
    struct sockaddr_nl client;
    struct timeval tv;
    int CppLive, rcvlen, ret;
    fd_set fds;
    int buffersize = 1024;
    CppLive = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);
    memset(&client, 0, sizeof(client));
    client.nl_family = AF_NETLINK;
    client.nl_pid = getpid();
    client.nl_groups = 1; /* receive broadcast message*/
    setsockopt(CppLive, SOL_SOCKET, SO_RCVBUF, &buffersize, sizeof(buffersize));
    bind(CppLive, (struct sockaddr*)&client, sizeof(client));
    while (1) {
        char buf[UEVENT_BUFFER_SIZE] = { 0 };
        FD_ZERO(&fds);
        FD_SET(CppLive, &fds);
        tv.tv_sec = 0;
        tv.tv_usec = 100 * 1000;
        ret = select(CppLive + 1, &fds, NULL, NULL, &tv);
        if(ret < 0)
            continue;
        if(!(ret > 0 && FD_ISSET(CppLive, &fds)))
            continue;
        /* receive data */
        rcvlen = recv(CppLive, &buf, sizeof(buf), 0);
        if (rcvlen > 0) { 
            printf("%s\n", buf);
            //检测更新程序
            //升级程序
            //重启....
        }
    }
    close(CppLive);
    return 0;
}

二、总结

以上就是今天要讲的内容,本文仅仅简单介绍了结合netlink的u盘升级,后期可以通过Qt实现U盘升级,实现一个程序,通过界面可以判断更新状态。


http://www.kler.cn/a/525623.html

相关文章:

  • [JavaWeb]搜索表单区域
  • 图论——最小生成树的扩展应用
  • 计算机毕业设计Python+CNN卷积神经网络高考推荐系统 高考分数线预测 高考爬虫 协同过滤推荐算法 Vue.js Django Hadoop 大数据毕设
  • PostgreSQL 数据备份与恢复:掌握 pg_dump 和 pg_restore 的最佳实践
  • Qt Ribbon使用实例
  • CAG技术:提升LLM响应速度与质量
  • 【愚公系列】《循序渐进Vue.js 3.x前端开发实践》036-案例:实现支持搜索和筛选的用户列表
  • 【某大厂一面】JDK1.8中对HashMap数据结构进行了哪些优化
  • 手撕Diffusion系列 - 第十一期 - lora微调 - 基于Stable Diffusion(代码)
  • Kafka常见问题之 org.apache.kafka.common.errors.RecordTooLargeException
  • 《DeepSeek 网页/API 性能异常(DeepSeek Web/API Degraded Performance):网络安全日志》
  • MIMIC IV数据库中mimiciv_hosp的transfers表的careunit分析
  • Java CAS操作
  • Windows平台最新视频号内容下载工具(MP4格式一键解析)
  • Vue.js 路由守卫:前置和后置守卫
  • 安卓(android)读取手机通讯录【Android移动开发基础案例教程(第2版)黑马程序员】
  • 一文大白话讲清楚webpack进阶——9——ModuleFederation实战
  • YOLO11/ultralytics:环境搭建
  • 菜鸟之路Day11-12一一集合进阶(四)
  • Effective Python:(10)
  • 电路研究9.2.5——合宙Air780EP中GPS 相关命令使用方法研究
  • 数字图像处理:实验六
  • 【RocketMQ 存储】- 一文总结 RocketMQ 的存储结构-基础
  • 基于SpringBoot的租房管理系统(含论文)
  • ICANN 关闭 WHOIS Port 43
  • SSM开发(八) MyBatis解决方法重载