低功耗蓝牙(BLE)开发——Qt

背景知识

低功耗蓝牙比经典蓝牙复杂些,需要了解一些协议的基础知识。

此部分参考博客GATT Profile 简介-CSDN博客

GATT详细介绍-CSDN博客

Introduction | Introduction to Bluetooth Low Energy | Adafruit Learning System

 蓝牙 (四) GATT profile-CSDN博客

关于低功耗蓝牙

简介

低功耗蓝牙(BLE),有时被称为“智能蓝牙”,是经典蓝牙的轻量级子集,作为蓝牙4.0核心规范的一部分引入。虽然与传统蓝牙有一些重叠,但BLE实际上有一个完全不同的血统,在被蓝牙技术联盟采用之前,它是由诺基亚作为一个内部项目开始的,名为“Wibree”。

支持的平台

支持蓝牙4.0和蓝牙低功耗(这是BT 4.0的一个子集)在大多数主要平台上可用,如下所列的版本:

  • iOS5+ (iOS7+ preferred)
  • Android 4.3+ (numerous bug fixes in 4.4+)
  • Apple OS X 10.6+
  • Windows 8 (XP, Vista and 7 only support Bluetooth 2.1)
  • GNU/Linux Vanilla BlueZ 4.93+

关于GAP

说明

 GAP(Generic Access Profile),它在用来控制设备连接和广播。外围设备发出广播,中心设备扫描接收到附近外围设备发出的广播,可选择连接。外围设备和中心设备是GAP定义的角色,一个外围设备只能连接一个中心设备,但是一个中心设备可连接多个外围设备。

设备角色及职能

GAP 给设备定义了若干角色,其中主要的两个是:外围设备(Peripheral)和中心设备(Central)。

外围设备(Peripheral):这一般就是非常小或者简单的低功耗设备,用来提供数据,并连接到一个更加相对强大的中心设备。例如小米手环。
中心设备(Central:中心设备相对比较强大,用来连接其他外围设备。例如手机等。

关于GATT

说明

GATT 的全名是 Generic Attribute Profile(通用属性协议,基本属性协议, ATT 指Attribute-属性,它定义了 两个 BLE 设备互相传输数据进行通信的方法也就是说中心设备和外围设备通过GAP创建连接后,然后通过GATT协议进行通信该方法用了两个概念, Service 和 Characteristic 。GATT 使用 ATT(Attribute Protocol)协议,ATT 协议把 Service, Characteristic以及相关的数据存储在一个简单的有索引的表中,这个索引表使用 16 位的 ID 作为表中每一项条目的索引

通信事务

GATT 通信的双方是 C/S 关系外设作为 GATT 服务端(Server),它维持了 ATT 的查找表以及 service 和 characteristic 的定义。中心设备是 GATT 客户端(Client),它向 Server 发起请求。需要注意的是,所有的通信事件,都是由客户端(也叫主设备,Master)发起,并且接收服务端(也叫从设备,Slave)的响应。

GATT 结构

GATT 事务是建立在嵌套的Profiles, Services 和 Characteristics之上的的,如下图所示:
这里写图片描述

Profile 并不是实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG 或者外设设计者预先定义的 Service 的集合。例如心率Profile(Heart Rate Profile)就是结合了 Heart Rate Service 和 Device Information Service。所有官方通过 GATT Profile 的列表可以从这里找到。

Service 是把数据分成一个个的独立逻辑项,它包含一个或者多个 Characteristic。每个 Service 有一个 UUID 唯一标识。 UUID 有 16 bit 的,或者 128 bit 的。16 bit 的 UUID 是官方通过认证的,需要花钱购买,128 bit 是自定义的,这个就可以自己随便设置。

Characteristic(特征)在 GATT 事务中的最低界别的是 Characteristic,Characteristic 是最小的逻辑数据单元,与 Service 类似,每个 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一标识。你可以免费使用 Bluetooth SIG 官方定义的标准 Characteristic,使用官方定义的,可以确保 BLE 的软件和硬件能相互理解。当然,你可以自定义 Characteristic,这样的话,就只有你自己的软件和外设能够相互理解。

Characteristic(特征)其实是个集合,包含以下子元素:

  • 特征声明(Characteristic Declaration)
  • 特征值声明(Characteristic Value Declaration)
  • 特征描述符声明(Characteristic Descriptor Declaration)

其中描述符是可选项,可能包含一个或多个描述符,也可能不包含描述符。

Characteristic(特征)是BLE 设备与外界通信的接口,当手机与BLE 设备通信,其实都是与某个具体的特征进行读写。其中读写的就是Characteristic Value(特征值)。

Attribute ​​​​​- 属性

规定数据按照一定规则存放,这个规则就是属性。

服务(Services)、特性(Characteristics)和描述符(Descriptors)都是属性类别,因此也就有了通用属性配置文件(Generic Attribute Profile)、属性表(Attribute Table)和属性协议(Attribute Protocol)等。具体是哪一个类别的属性,由“通用唯一标识符(Universally Unique Identifier,简称UUID)”来定义。

服务(Service)、特性(Characteristics)和描述符(Descriptors)也有层级之分:服务包括一项或多项特性;一项特性可能没有、拥有一个或拥有多个描述符

Atrribute规则结构:

属性句柄(2个字节) + 属性类型(2个字节) + 属性值(0-512个字节) + 属性权限

  • 属性句柄(Attribute Handle):通过他可以找到对应属性,并用于区分不同服务中的相同属性。我理解类似是数组下标。
  • 属性类型(Attribute Type):是对某个东西取一个数字代号(用uuid来代号),比如心率计,SIG就是用0x180D这个uuid来表示这个这条属性是和心率计有关SIG将uuid进行了范围规定,下面这些uuid都来标识属性类型。

      0x1800~0x26FF 用于服务类 UUID

      0x2700~0x27FF 用于标识计量单位

      0x2800~0x28FF 用于区分属性类型

      0x2900~0x29FF 用于特性描述

      0x2A00~0x7FFF 用于区分特性类型

  • 属性值(Attribute Value):属性值是一个 0~512 字节的数据,属性值是给上层应用层使用的,是用户“真正”要使用的数据,属性值可以有一下几类

        服务通用唯一识别码(UUID)  // 1800
        单位
        属性类型
        特性描述符
        特性类型

 工作流程

在了解完以上的知识后,根据需求(在安卓系统平板上开发,收发数据),我需要开发的角色是中心设备(Central),工作流程如下:

  1. 搜寻附近全部的蓝牙设备(GAP)
  2. 根据搜寻出的蓝牙设备信息,筛选出要连接的蓝牙设备进行连接(此处包括以下都是GATT)
  3. 建立连接后,去获取该蓝牙设备等services列表,根据约定好的服务uuid筛选出自己需要的服务
  4. 发现对应的服务后,根据约定好的服务下characteristic特性uuid,创建特征对象,并监听特征对象内容的变化(读Rx);或者向“写特征配置对象”写入特征生效消息(写Tx)。
  5. 关闭连接,释放资源。

BLE中心设备实现代码

  • 查找附近的BLE设备(外围设备),和经典蓝牙一样都是通过类QBluetoothDeviceDiscoveryAgent实现
m_pDevDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);connect(m_pDevDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothDevice::onDeviceDiscovered);
connect(m_pDevDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BluetoothDevice::onDiscoverFinished);connect(m_pDevDiscoveryAgent, QOverload<QBluetoothDeviceDiscoveryAgent::Error>::of(&QBluetoothDeviceDiscoveryAgent::error),this, &BluetoothDevice::onDiscoverFinished);//开始查找BLE设备
m_pDevDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
  •  筛选出要连接的蓝牙设备进行连接:通过上面的查找可以获取到需要连接的外围设备的MAC地址等信息,创建QLowEnergyController进行连接;连接成功后,查找外围设备中包含的服务。
        //因为我做的连接逻辑是只要没连接上,就不停的尝试连接,
//注意的是即使没连接上也是需要关闭连接,释放内存的。
close();//获取本地设备地址信息,创建低功耗控制器需要
QList<QBluetoothHostInfo> localdevices = QBluetoothLocalDevice::allDevices();
if(localdevices.size() == 0)
{return;
}
QBluetoothAddress localAddr = localdevices[0].address();//初始化 strAddr为需要连接的外围设备的MAC地址
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)m_BLEController = new QLowEnergyController(QBluetoothAddress(strAddr), localAddr);
#elsem_BLEController = QLowEnergyController::createCentral(QBluetoothAddress(strAddr), localAddr);
#endifconnect(m_BLEController, &QLowEnergyController::connected, m_BLEController, &QLowEnergyController::discoverServices);
connect(m_BLEController, QOverload<QLowEnergyController::Error>::of(&QLowEnergyController::error), this, &BluetoothConnection::onErrorOccurred);
connect(m_BLEController, &QLowEnergyController::serviceDiscovered, this, &BluetoothConnection::BLEC_onServiceDiscovered);//尝试连接
m_BLEController->connectToDevice();
  • 从查找出的服务中根据约定好的服务uuid筛选出自己需要的服务 。TxServerUUID和RxServerUUID分别是我定义的写服务uuid宏 和 读服务uuid宏,在使用控制器创建服务对象(createServiceObject)后,还需要查询此服务的详情(discoverDetails()),也就是此服务下的特性(Characteristics)等信息。
void BluetoothConnection::BLEC_onServiceDiscovered(const QBluetoothUuid &serviceUUID)
{if(serviceUUID == QBluetoothUuid(TxServerUUID)){//writem_BLETxService = m_BLEController->createServiceObject(serviceUUID,this);connect(m_BLETxService, &QLowEnergyService::stateChanged, this, &BluetoothConnection::BLEC_onServiceDetailDiscovered);m_BLETxService->discoverDetails();}else if(serviceUUID == QBluetoothUuid(RxServerUUID)){//notifym_BLERxService = m_BLEController->createServiceObject(serviceUUID,this);connect(m_BLERxService, &QLowEnergyService::stateChanged, this, &BluetoothConnection::BLEC_onServiceDetailDiscovered);m_BLERxService->discoverDetails();}
}
  • 服务在查找到详情后,可调用characteristics()获取本服务下的特征数组,通过uuid对比及属性判断后,得到合法的读特征和写特征

读特征需要监听数据、状态改变和错误信息(中心设备监听外围设备发来的数据);

写特征需要保存下指针,便于后续的写操作(中心设备将数据发送给外围设备),

在以上的操作执行完成后,才算连接成功了。

void BluetoothConnection::BLEC_onServiceDetailDiscovered(QLowEnergyService::ServiceState newState)
{bool deleteService = true;auto service = qobject_cast<QLowEnergyService*>(sender());if(newState == QLowEnergyService::ServiceDiscovered){deleteService = true;const QList<QLowEnergyCharacteristic> chars = service->characteristics();// delete unused serviceif(service != m_BLERxService && service != m_BLETxService){deleteService = true;}else if(service == m_BLERxService){for(auto it = chars.cbegin(); it != chars.cend(); ++it){if(!m_BLERxCharacteristicValid && it->uuid() == QBluetoothUuid(RxBLECharacteristicUUID)&& it->properties().testFlag(QLowEnergyCharacteristic::Notify)){m_BLERxCharacteristicValid = true;deleteService = false;}}}else if(service == m_BLETxService){for(auto it = chars.cbegin(); it != chars.cend(); ++it){if(!m_BLETxCharacteristicValid && it->uuid() == QBluetoothUuid(TxBLECharacteristicUUID)&& it->properties().testFlag(QLowEnergyCharacteristic::Write)){m_BLETxCharacteristicValid = true;deleteService = false;}}}if(!deleteService){if(m_BLERxCharacteristicValid && m_BLETxCharacteristicValid){// Rxconnect(m_BLERxService, QOverload<QLowEnergyService::ServiceError>::of(&QLowEnergyService::error), this, &BluetoothConnection::onErrorOccurred);connect(m_BLERxService, &QLowEnergyService::characteristicChanged, this, &BluetoothConnection::BLEC_onDataArrived);connect(m_BLERxService, &QLowEnergyService::characteristicRead, this, &BluetoothConnection::BLEC_onDataArrived);QLowEnergyDescriptor desc = m_BLERxService->characteristic(QBluetoothUuid(RxBLECharacteristicUUID)).descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);m_BLERxService->writeDescriptor(desc, QByteArray::fromHex("0100"));// Txconnect(m_BLETxService, QOverload<QLowEnergyService::ServiceError>::of(&QLowEnergyService::error), this, &BluetoothConnection::onErrorOccurred);m_BLETxCharacteristic = m_BLETxService->characteristic(QBluetoothUuid(TxBLECharacteristicUUID));onConnected();}}else{if(service == m_BLERxService) // characteristic not found{m_BLERxService = nullptr;onDisconnect();}else if(service == m_BLETxService) // characteristic not found{m_BLETxService = nullptr;onDisconnect();}service->deleteLater();}}
}

QByteArray m_buf ;void BluetoothConnection::onReadyRead()
{qDebug()<<"BluetoothConnection::onReadyRead";m_buf += m_pSocket->readAll();emit readyRead();}

qint64 BluetoothConnection::write(const char *data, qint64 len)
{if(m_BLETxService == nullptr){return 0;}m_BLETxService->writeCharacteristic(m_BLETxCharacteristic, QByteArray::fromRawData(data, len));return len;
}

 

报错后的处理

void BluetoothConnection::onErrorOccurred()
{if(sender() == m_BLEController){QLowEnergyController::Error error;error = m_BLEController->error();qDebug() << "BLE Central Controller Error:" << error << m_BLEController->errorString();qDebug() << "State:" << m_BLEController->state();if(error == QLowEnergyController::NoError);else{if(m_BLEController->state() == QLowEnergyController::ConnectingState){emit connectFailed(tr("Controller Error: ")+m_BLEController->errorString());}close();}}else if(sender() == m_BLERxService || sender() == m_BLETxService){QLowEnergyService* service = qobject_cast<QLowEnergyService*>(sender());QLowEnergyService::ServiceError error;error = service->error();// service->errorString() doesn't existqDebug() << "BLE Central Service Error:" << error;qDebug() << "State:" << service->state();if(error == QLowEnergyService::NoError);else if(error == QLowEnergyService::CharacteristicReadError || error == QLowEnergyService::CharacteristicWriteError || error == QLowEnergyService::DescriptorReadError || error == QLowEnergyService::DescriptorWriteError);else{if(service->state() == QLowEnergyService::DiscoveringServices)emit connectFailed(tr("Service Error: ")+ QString::fromUtf8(QMetaEnum::fromType<QLowEnergyService::ServiceError>().valueToKey(error)));close();}}
}
  • 关闭释放内存:
void BluetoothConnection::close()
{if(m_BLEController == nullptr)return;if(m_BLEController->state() == QLowEnergyController::UnconnectedState){return;}m_BLERxCharacteristicValid = false;m_BLETxCharacteristicValid = false;if(m_BLERxService != nullptr){QLowEnergyDescriptor desc = m_BLERxService->characteristic(QBluetoothUuid(RxBLECharacteristicUUID)).descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);m_BLERxService->writeDescriptor(desc, QByteArray::fromHex("0000"));m_BLERxService->deleteLater();m_BLERxService = nullptr;}if(m_BLETxService != nullptr){m_BLETxService->deleteLater();m_BLETxService = nullptr;}if(m_BLEController != nullptr){m_BLEController->disconnectFromDevice();m_BLEController->deleteLater();m_BLEController = nullptr;}onDisconnect();
}

在连接成功或者关闭后,向外发送信号

void BluetoothConnection::onConnected()
{emit connected();
}void BluetoothConnection::onDisconnect()
{emit disconnected();
}

 

注意事项

  • 记得关闭释放内存,即使连接失败:我这边的连接逻辑是自动连接的,只要没连接上就一直尝试连接。我之前在尝试连接前,没对上一次的连接进行关闭释放内存,导致报错:gatt status=133,这个就是因为连接个数是有限的,超额就会报错。

Android BLE 开发,GATT报错 status 133全面解析_gatt 133-CSD博客

  •  在连接成功后,外围设备不再定时发送信息供中心设备识别,也就是在连接成功后,使用QBluetoothDeviceDiscoveryAgent查找外围设备,是查找不到此外围设备的。 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/101979.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Centos中利用自带的定时器Crontab_实现mysql数据库自动备份_linux中mysql自动备份脚本---Linux运维工作笔记056

这个经常需要,怕出问题因而需要经常备份数据库,可以利用centos自带的定时器,配合脚本实现自动备份. 1.首先查看一下,这个crontab服务有没有打开: 执行:ntsysv 可以看到已经开机自启动了. 注意这个操作界面,用鼠标不行,需要用,tab按键,直接tab到确定,或取消,然后按回车回到命…

CSS 之 table 表格布局

一、简介 ​ 除了使用HTML的<table>元素外&#xff0c;我们还可以通过display: table/inline-table; 设置元素内部的布局类型为表格布局。并结合table-cell、table-row等相关CSS属性值可以实现HTML中<table>系列元素的效果&#xff0c;具有表头、表尾、行、单元格…

Docker-compos

Docker-compose 简介 Docker-Compose项目是基于Python开发的Docker官方开源项目&#xff0c;负责实现对Docker容器集群的快速编排。 Docker-Compose将所管理的容器分为三层&#xff0c;分别是 工程&#xff08;project&#xff09;&#xff0c;服务&#xff08;service&#…

HSRP热备份路由器协议的解析和配置

HSRP的解析 个人简介 HSRP hot standby router protocol 热备份路由协议&#xff08;思科私有协议&#xff09; HSRP v1 version 1 HSRP v2 version 2 虚拟一个HSRP虚拟IP地址 192.168.1.1 开启HSRP的抢占功能 通过其他参数 人为调整谁是主 谁是从 &#xff01; 查…

记录一次线上fullgc问题排查过程

某天&#xff0c;接到测试部门反馈说线上项目突然很快&#xff0c;由于当前版本代码和上一版本相比就多了一个刚上线了一个5分钟1次的跑批任务&#xff0c;先关闭次任务后观察是否卡顿&#xff0c;并检查堆内存是否使用完造成频繁gc 1.通过jmap命令查看堆内存中的对象 2.生成当…

许战海战略文库|无增长则衰亡:中小型制造企业增长困境

竞争环境不是匀速变化&#xff0c;而是加速变化。企业的衰退与进化、兴衰更迭在不断发生&#xff0c;这成为一种不可避免的现实。事实上&#xff0c;在产业链竞争中增长困境不分企业大小&#xff0c;而是一种普遍存在的问题&#xff0c;许多收入在1亿至10亿美元间的制造企业也同…

PlantUML 绘图

官网 https://plantuml.com/zh/ 示例 绘制时序图 USB 枚举过程 PlantUML 源码 startuml host <-- device : device insert host note right : step 1 host -> device : get speed, reset, speed check note right : step 2 host -> device …

Premiere Elements 2024(PR简化版)直装版

Adobe Premiere Elements 2024 是一款由Adobe Systems推出的视频编辑软件&#xff0c;它结合了易用性和专业级的功能&#xff0c;帮助用户对视频进行剪辑、特效、色彩校正等处理。 主要功能和特点&#xff1a; 导入和组织视频&#xff1a;Premiere Elements 2024允许用户快速导…

ESP8266 WiFi物联网智能插座—下位机软件实现

目录 1、软件架构 2、开发环境 3、软件功能 4、程序设计 4.1、初始化 4.2、主循环状态机 4.3、初始化模式 4.4、配置模式 4.5、运行模式 4.6、重启模式 4.7、升级模式 5、程序功能特点 5.1、日志管理 5.2、数据缓存队列 本篇博文开始讲解下位机插座节点的MCU软件…

虚拟机软件Parallels Desktop 19 mac功能介绍

Parallels Desktop 19 mac是一款虚拟机软件&#xff0c;它允许用户在Mac电脑上同时运行Windows、Linux和其他操作系统。Parallels Desktop提供了直观易用的界面&#xff0c;使用户可以轻松创建、配置和管理虚拟机。 PD19虚拟机软件具有快速启动和关闭虚拟机的能力&#xff0c;让…

Kelper.js 笔记 python交互

1 加载Kepler 地图 KeplerGl() 1.1 主要参数 height 可选 默认值&#xff1a;400 地图显示的高度 data 数据集 字典&#xff0c;键是数据集的名称 config地图配置字典 1.2 举例 from keplergl import KeplerGlmap_KeplerGl() map_ 默认的位置 1.3 添加自己的图 1.3.1 读…

ElementPlus Switch 开关基础使用

昨天开发用到开关组件 后台返回字段是 can_write 默认是0 or 1 但是Switch 组件绑定的默认值默认是 true or false 直接绑定会导致默认是关闭状态 在页面一加载 值发生变化时 会自己调用 查了文档 需要使用 active-value 和 inactive-value 来指定绑定的数据类型 …

23种经典设计模式:单例模式篇(C++)

前言&#xff1a; 博主将从此篇单例模式开始逐一分享23种经典设计模式&#xff0c;并结合C为大家展示实际应用。内容将持续更新&#xff0c;希望大家持续关注与支持。 什么是单例模式&#xff1f; 单例模式是设计模式的一种&#xff08;属于创建型模式 (Creational Pa…

网页游戏的开发流程

网页游戏的开发流程可以根据项目的规模和复杂性而有所不同&#xff0c;但通常包括以下一般步骤&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.需求分析&#xff1a; 确定游戏的概念、目标受众和核…

手写 分页

子组件&#xff1a;TimePage.vue 效果图 <template><div class"click-scroll-X"><!-- 上 --><!-- eslint-disable-next-line --><span class"left_btn" :disabled"pageNo 1" click"leftSlide"><&…

PanoFlow:学习360°用于周围时间理解的光流

1.摘要&#xff1a; 光流估计是自动驾驶和机器人系统中的一项基本任务&#xff0c;它能够在时间上解释交通场景。自动驾驶汽车显然受益于360提供的超宽视野&#xff08;FoV&#xff09;◦ 全景传感器。 然而&#xff0c;由于全景相机独特的成像过程&#xff0c;为针孔图像设计…

NSIC2050JBT3G 车规级120V 50mA ±15% 用于LED照明的线性恒流调节器(CCR) 增强汽车安全

随着汽车行业的巨大变革&#xff0c;高品质的汽车氛围灯效、仪表盘等LED指示灯效已成为汽车内饰设计中不可或缺的元素。深力科安森美LED驱动芯片系列赋能智能座舱灯效充满艺术感和科技感——NSIC2050JBT3G LED驱动芯片&#xff0c;实现对每路LED亮度和颜色进行细腻控制&#xf…

SLAM从入门到精通(launch文件学习)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 大家应该还记得我们在一开始学习ros的时候&#xff0c;如果需要启动一个节点的话&#xff0c;需要首先打开roscore&#xff0c;接着用rosrun打开对…

shiro550复现环境搭建

前言 Shiro反序列化漏洞指的是Apache Shiro安全框架中的一个潜在漏洞&#xff0c;该漏洞可能导致攻击者能够通过精心构造的恶意序列化对象来执行任意代码或进行拒绝服务&#xff08;DoS&#xff09;攻击。 这种漏洞的根源是在Shiro的RememberMe功能中&#xff0c;当用户选择“…

制造业单项冠军(国家级、广东省、深圳市)奖励政策及申报对比

制造业单项冠军的头衔含金量极高&#xff0c;是某一细分领域的“领头雁”。下面深科信对“制造业单项冠军”&#xff08;国家级、广东省级、深圳市级&#xff09;的认定标准、奖励政策进行梳理 。 2023年9月25日&#xff0c;工信部办公厅正式发布《关于开展2023年制造业单项冠军…