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

HarmonyOS ArkTS与C++数据类型转换

1. HarmonyOS ArkTS与C++数据类型转换

  本文介绍了C++与TS各自数据类型与互相之间的数据类型转换,在需要使用C++模块时可以快速上手对各种数据类型进行转换。

1.1. 概述

  HarmonyOS的主力开发语言是ArkTS,也提供了C++语言的支持,对于一些能力,比如音视频编解码等,HarmonyOS 提供的也只有C++ API,对于一些其他平台现有能力的迁移,C++也是最快捷高效的,所以对于一个HarmonyOS 开发者,掌握ArkTS与C++交互成了一项必备技能。
 &emsp每种编程语言都有自己定义的数据类型,不同编程语言之间互相调用就涉及到了数据类型的转换,ArkTS与C++的转换主要有Node-API接口提供,本文介绍ArkTS与C++互相转换的接口和最佳实践。
  做过Android JNI开发的小伙伴对Java和C++的互相调用有所了解,JNI提供了Java和C++的类型转换,与JNI不同的是NAPI中,TS调用C++的参数都封装到了一起:

static napi_value add(napi_env env, napi_callback_info info)  
{
}

  不管要传递多少个参数,都封装在napi_callback_info中,可以从napi_callback_info中获取全部参数信息:

size_t argc = 7;//参数个数
napi_value args[7] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

1.2. 创建Native C++ Module

1.2.1. 右键项目->new->module

在这里插入图片描述

1.2.2. 修改build-profile.json5配置

"externalNativeOptions": {"path": "./src/main/cpp/CMakeLists.txt","arguments": "-v -DOHOS_STL=c++_shared","abiFilters": [
//        "armeabi-v7a",
//        "x86_64","arm64-v8a"],"cppFlags": ""}

1.2.3. CMakeLists.txt 配置

# the minimum version of CMake.
cmake_minimum_required(VERSION 3.4.1)
project(MyApplication43)set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})include_directories(${NATIVERENDER_ROOT_PATH}${NATIVERENDER_ROOT_PATH}/include)add_library(application SHARED SRRtcVideoEngineNapi.cpp SRRtcRoomCallBackNapi.cpp)
target_link_libraries(application PUBLIC libace_napi.z.so)

1.3. 代码实现

1.3.1. 主调接口实现

  NAPI中缓存回调接口的变量,便于后面回调给TS

napi_value SRRtcVideoEngineNapi::setRRoomCallBack(napi_env env, napi_callback_info info) {LogE("setRRoomCallBack---一个参数:ISRoomCallBack");size_t argc = 1;napi_value args[1] = {nullptr};napi_status status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);if (status != napi_ok) {napi_throw_error(env, "", "");return nullptr;}// 缓存回调函数全局变量,回调ets用if (SRGlobalvar::napi_CallbackReference == nullptr) {LogE("setRRoomCallBack===new NapiCallBack()");SRGlobalvar::napi_CallbackReference = new NapiCallBack(); // 创建缓存函数}napi_create_reference(env, args[0], 1, &SRGlobalvar::napi_CallbackReference->roomCallBack_napi);SRGlobalvar::napi_CallbackReference->env = env;// 调用 底层sdk : RRoomControlMgr.setCallBackRResult rResult = sr_engineSdk->setRRoomCallBackRtcEngine();return SRGlobalvar::returnResult(env, rResult);
}

1.3.2. 回调接口实现

  通过缓存的env,callback对象,调用napi_call_function方法将数据传回给ts

void SRRtcRoomCallBackNapi::onRoomJoinConfirm(RResult rResult, const RRoomInfo &roomInfo) {// 处理 onRoomJoinConfirm 通知LogE("回调消息---SRRtcRoomCallBack:onRoomJoinConfirm ");// 转换N-API对象napi_value roomInfo_napi = SRGlobalvar::convertToSRRoomInfo(SRGlobalvar::napi_CallbackReference->env, roomInfo);napi_value rResult_napi = SRGlobalvar::convertToSRResult(SRGlobalvar::napi_CallbackReference->env, rResult);// 传递给TSnapi_value callback;napi_get_reference_value(SRGlobalvar::napi_CallbackReference->env,SRGlobalvar::napi_CallbackReference->roomCallBack_napi, &callback);napi_value jsMethod;napi_get_named_property(SRGlobalvar::napi_CallbackReference->env, callback, "onRoomJoinConfirm", &jsMethod);napi_value argv[] = {rResult_napi, roomInfo_napi};napi_value callbackResult = nullptr;napi_call_function(SRGlobalvar::napi_CallbackReference->env, nullptr, jsMethod, 2, argv, &callbackResult);
}

1.4. ets的接收c++传回的数据

1.4.1. index.d.ts 代码增加接口

function setRRoomCallBack(sroomCallback: ISRoomCallBack): SRReult

1.4.2. 回调接口SRoomCallBack.ets

export class SRoomCallBack implements ISRoomCallBack {onRoomJoinConfirm(rResult: SRReult, roomInfo: SRRoomInfo) {SRLog.i(TAG, `onRoomJoinConfirm==回调测试完成=rResult:${JsonUtil.jsonToString(rResult)}\n roomInfo:${JsonUtil.jsonToString(roomInfo)}`)}
}

1.4.3. 调用

import srrtcNapi from 'librtcvideo.so';setRRoomCallBackRtcEngine(callback: ISRoomCallBack) {let srResult = srrtcNapi.setRRoomCallBack(callback)SRLog.i(TAG, "setRRoomCallBackRtcEngine===srresult:" + JsonUtil.jsonToString(srResult))}

1.5. 数据转换

1.5.1. ArkTS转C++类型

  TS基本数据类型:
(1)数字类型: number
(2)字符串类型 :string
(3)布尔类型: boolean
(4)任意精度整型: bigint
(5)对象类型: object
  C++基本数据类型:
(6)整型i:nt、short、long、long long
(7)浮点型:float、double、long double
(8)字符型: char
(9)布尔型: bool

1.5.2. 基本数据类型转换

  NAPI提供了上面两种语言对应的类型的转换:
(1)转换为布尔类型:napi_get_value_bool
(2)转换为int32:napi_get_value_int32
(3)转换为int64:napi_get_value_int64
(4)转换为无符号32位:napi_get_value_uint32
(5)转换为double:napi_get_value_double
(6)bitint 64位:napi_get_value_bigint_int64
(7)bitint 无符号64位:`napi_get_value_bigint_uint64
  除了bool类型,其他基本类型就是数值类型,TS中的数值类型对应C++各种细分类型,分别调用上面不同函数即可,调用方式:

int intValue;
napi_get_value_int32(env, args[0], &intValue);

  上面代码通过napi_get_value_int32将TS中的number转换为int赋值给intValue变量。

1.5.3. 字符串类型转换

  对于字符串和object对象处理稍微复杂些,通过napi_get_value_string_utf8将js的字符串对象转换为c++的std::string对象。但是要创建std:string需要先知道TS中传来的字符串的长度,看napi_get_value_string_utf8函数说明:

napi_status napi_get_value_string_utf8(napi_env env,napi_value value,char* buf,size_t bufsize,size_t* result)

[in] env: The environment that the API is invoked under.

[in] value: napi_value representing JavaScript string.

[in] buf: Buffer to write the UTF8-encoded string into. If NULL is passed in, the length of the string in bytes and excluding the null terminator is returned in result.

[in] bufsize: Size of the destination buffer. When this value is insufficient, the returned string is truncated and null-terminated.

[out] result: Number of bytes copied into the buffer, excluding the null terminator.

Returns napi_ok if the API succeeded. If a non-string napi_value is passed in it returns napi_string_expected.

  我们需要先创建一个char类型的空间去接收转换后的字符串,创建char空间需要指定大小,可以先调用一次napi_get_value_string_utf8,buf传入空获取到传入的数据大小,然后创建对应大小buf,再次调用napi_get_value_string_utf8获取转换后的字符串:

void JsValueToString(const napi_env &env, const napi_value &value, std::string &target) {  size_t result = 0;  napi_get_value_string_utf8(env, value, nullptr, 0, &result);  std::unique_ptr<char[]> buf(new char[result+1]);  if (buf.get() == nullptr) {  return;  }  (void)memset(buf.get(), 0, result+1);  napi_get_value_string_utf8(env, value, buf.get(), result+1, &result);  target = buf.get();  
}

  上面方法就是封装了一个转换字符串对应的工具函数。

1.5.4. object类型转换

  解析object对象参数和前面参数一样,通过napi_get_cb_info转换为napi_value对象后,通过napi_get_named_property获取对象中的属性值:

//ts对象:
export class Task {  public id: number; //unique task identify  public channel?: number;   //...
}
napi_value Demo::startTask(napi_env env, napi_callback_info info){size_t argc = 1;napi_value js_cb;napi_get_cb_info(env, info, &argc, &js_cb, nullptr, nullptr);napi_value taskIdNapiValue;napi_get_named_property(env, js_cb, "id", &taskIdNapiValue);int32_t taskid;napi_get_value_int32(env, taskIdNapiValue, &taskid);napi_value channelSelectNapiValue;napi_get_named_property(env, js_cb, "channelSelect", &channelSelectNapiValue);int32_t channel_select;napi_get_value_int32(env, channelSelectNapiValue, &channel_select);//....//调用业务逻辑return nullptr;
}

1.5.5. 数组类型类型转换

  和string类似,数组类型参数需要先通过napi_get_array_length参数获取数组长度,再通过napi_get_element获取对应每个napi_value对象,通过上述基本类型转换为对应C++对象:

napi_value Demo::setArrays(napi_env env, napi_callback_info info){std::vector<std::string> backupip_list;size_t argc = 1;napi_value args[1] = {nullptr};napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);bool is_array;//判断是否为数组napi_status status = napi_is_array(env, args[0], &is_array);if (!is_array) {return nullptr;}napi_value array = args[0];uint32_t length;napi_get_array_length(env,array,&length);for(int i = 0; i < length; i++){napi_value element;napi_get_element(env, array, i, &element); // 获取返回值数组的每个元素std::string ipStr;NapiUtil::JsValueToString(env, element, ipStr);backupip_list.push_back(ipStr);}   //调用业务逻辑return nullptr;
}

1.5.6. ArrayBuffer类型转换

  如果TS向C++传输二进制流,需要用到ArrayBuffer类型数据,在C++侧通过napi_get_arraybuffer_info转换成C++字节流,接口说明:

napi_status napi_get_arraybuffer_info(napi_env env,napi_value arraybuffer,void** data,size_t* byte_length)

[in] env: The environment that the API is invoked under.

[in] arraybuffer: napi_value representing the ArrayBuffer being queried.

[out] data: The underlying data buffer of the ArrayBuffer. If byte_length is 0, this may be NULL or any other pointer value.

[out] byte_length: Length in bytes of the underlying data buffer.
Returns napi_ok if the API succeeded.
This API is used to retrieve the underlying data buffer of an ArrayBuffer and its length.

  第三个参数传入空时,只会获取字节流大小。第三个参数是指向指针的指针,我们不需要创建空间,函数内部会创建空间:

napi_value Demo::setArrayBufferData(napi_env env, napi_callback_info info){size_t argc = 1;napi_value js_cb;napi_get_cb_info(env, info, &argc, &js_cb, nullptr, nullptr);// 获取 ArrayBuffer 对象的指针和长度 void* buffer; size_t length; napi_get_arraybuffer_info(env, arrayBuffer, &buffer, &length); // 打印 ArrayBuffer 中的数据 ,也可以修改ArrayBuffer的值uint32_t* data = (uint32_t*) buffer;//调用业务逻辑return nullptr;
}

1.5.7. C++类型转ArkTS 类型

 &emsp上面介绍ArkTS转C++时看到的示例代码一般返回nullptr,如果要返回具体类型的数据,不能直接返回C++类型的变量,也需要转换成TS类型变量,NAPI提供了napi_value类型,表示TS类型数据,创建对应TS类型变量的函数:

napi_create_uint32

napi_create_int64

napi_create_double

napi_create_bigint_int64

napi_create_bigint_uint64

napi_create_bigint_words

napi_create_string_latin1

node_api_create_external_string_latin1

napi_create_string_utf16

node_api_create_external_string_utf16

napi_create_string_utf8

napi_get_boolean

1.5.8. 使用示例:

napi_value Demo::hasSon(napi_env env, napi_callback_info info){size_t argc = 1;napi_value args[1] = {nullptr};napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);int id;napi_get_value_int32(env, args[0], &id);napi_value result;bool hasSonById = _HasSon(id);//创建布尔型js对象napi_get_boolean(env, hasSonById, &result);return result;
}

相关文章:

HarmonyOS ArkTS与C++数据类型转换

1. HarmonyOS ArkTS与C数据类型转换 本文介绍了C与TS各自数据类型与互相之间的数据类型转换&#xff0c;在需要使用C模块时可以快速上手对各种数据类型进行转换。 1.1. 概述 HarmonyOS的主力开发语言是ArkTS&#xff0c;也提供了C语言的支持&#xff0c;对于一些能力&#xff…...

腾讯云或阿里云centos7安装Redis,并解决端口无法访问的问题

问题背景 最近自建的网站JeecgFlow在云环境安装redis时候&#xff0c;出现端口无法远程进行访问。 浪费好了好久时间进行排查&#xff0c; 记录一下Redis在云环境centos7环境下如何安装&#xff0c;并且远程访问。 Redis安装 //安装c 用于编译redis yum install gcc-c//在/u…...

【小问题】距离估计和频率估计的方差下界推导出距离估计的方差下界

【1】OFDM Radar Algorithms in Mobile Communication Networks pp34 文章目录 1. 频率和距离之间的关系2. 计算 d ^ \hat{d} d^ 对 n ^ \hat{n} n^ 的导数3. 将频率的方差转化为距离的方差4. 从频率的 CRB 获得 var ⁡ [ n ^ ] \operatorname{var}[\hat{n}] var[n^]5. 将 …...

Selenium爬虫技术:如何模拟鼠标悬停抓取动态内容

介绍 在当今数据驱动的世界中&#xff0c;抓取动态网页内容变得越来越重要&#xff0c;尤其是像抖音这样的社交平台&#xff0c;动态加载的评论等内容需要通过特定的方式来获取。传统的静态爬虫方法难以处理这些由JavaScript生成的动态内容&#xff0c;Selenium爬虫技术则是一…...

Z-BlogPHP显示错误Undefined array key 0 (set_error_handler)的解决办法

今天打开博客的时候&#xff0c;意外发现页面&#xff0c;打开均显示错误&#xff1a;Undefined array key 0 (set_error_handler)。 博客程序采用的是Z-BlogPHP。百度了一圈没有找到解决办法&#xff0c;在官方论坛里也没找到解决办法。 于是开始自己排查原因。我服务器采用…...

java-实例化一个List,然后添加数据的方法详解

在Java中&#xff0c;实例化一个 List 并向其中添加数据非常简单。List 是一个接口&#xff0c;因此我们通常使用它的常见实现类 ArrayList 或 LinkedList。以下是一些常见的操作方法&#xff1a; ### 1. 使用 ArrayList 实例化并添加数据 java import java.util.ArrayList; …...

【Linux系统】Ubuntu的简单操作

什么是 Ubuntu&#xff1f; Ubuntu&#xff08;乌帮图&#xff09;是一个非洲词汇&#xff0c;它的意思是“人性对待他人”或“群在故我在”。Ubuntu发行版将Ubuntu精神带到软件世界之中。 目前已有大量各种各样基于GNU/Linux的操作系统&#xff0c;例如:Debian,SuSE,Gentoo,R…...

标准日志插件项目【C/C++】

博客主页&#xff1a;花果山~程序猿-CSDN博客 文章分栏&#xff1a;项目日记_花果山~程序猿的博客-CSDN博客 关注我一起学习&#xff0c;一起进步&#xff0c;一起探索编程的无限可能吧&#xff01;让我们一起努力&#xff0c;一起成长&#xff01; 目录 一&#xff0c;项目介…...

SpingBoot原理

SpingBoot原理 在前面十多天的课程当中&#xff0c;我们学习的都是web开发的技术使用&#xff0c;都是面向应用层面的&#xff0c;我们学会了怎 么样去用。而我们今天所要学习的是web后端开发的最后一个篇章springboot原理篇&#xff0c;主要偏向于底 层原理。 我们今天的课程…...

Cout输出应用举例

Cout输出应用 在main.cpp里输入程序如下&#xff1a; #include <iostream> //使能cin(),cout(); #include <stdlib.h> //使能exit(); #include <sstream> #include <iomanip> //使能setbase(),setfill(),setw(),setprecision(),setiosflags()和res…...

java的无锁编程和锁机制

Java 的并发编程中&#xff0c;为了保证线程安全和高性能&#xff0c;采用了两种主要的同步手段&#xff1a;锁机制和无锁编程。以下是对锁机制、无锁编程、死锁及其避免的详细讲解。 一、无锁编程 无锁编程通过原子操作来避免传统锁&#xff0c;从而减少线程的上下文切换&am…...

vue实现富文本编辑器上传(粘贴)图片 + 文字

vue实现富文本编辑器上传&#xff08;粘贴&#xff09;图片 文字 1.安装插件 npm install vue-quill-editor -s2.在使用vue-quill-editor富文本的时候&#xff0c;对于图片的处理经常是将图片转换成base64&#xff0c;再上传数据库&#xff0c;但是base64不好存储。 原理&a…...

子集和全排列(深度优先遍历)问题

欢迎访问杀马特主页&#xff1a;小小杀马特主页呀&#xff01; 目录 前言&#xff1a; 例题一全排列&#xff1a; 1.题目介绍&#xff1a; 2.思路汇总&#xff1a; 3.代码解答&#xff1a; 例题二子集&#xff1a; 题目叙述&#xff1a; 解法一&#xff1a; 1.思路汇总…...

判断检测框是否在感兴趣区域(ROI)内

判断检测框是否在感兴趣区域&#xff08;ROI&#xff09;内 在计算机视觉和图像处理中&#xff0c;我们经常需要确定一个矩形检测框是否位于一个特定的感兴趣区域&#xff08;Region of Interest, ROI&#xff09;内。这个ROI可以是一个多边形&#xff0c;而检测框则是一个矩形…...

正点原子阿尔法ARM开发板-IMX6ULL(九)——关于SecureCRT连接板子上的ubuntu

文章目录 一、拨码器二、SecureCRT 一、拨码器 emmm,也是好久没学IMX6ULL了&#xff0c;也是忘了拨码器决定了主板的启动方式 一种是直接从TF卡中读取文件&#xff08;注意这里是通过imdownload软件编译好了之后&#xff0c;通过指令放入TF卡&#xff09; 一种是现在这种用串口…...

微信支付Java+uniapp微信小程序

JS&#xff1a; request.post(/vip/pay, {//这是自己写的java支付接口id: this.vipInfo.id,payWay: wechat-mini}).then((res) > {let success (res2) > {//前端的支付成功回调函数this.$refs.popup.close();// 支付成功刷新当前页面setTimeout(() > {this.doGetVipI…...

【NOIP提高组】加分二叉树

【NOIP提高组】加分二叉树 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 设一个n个节点的二叉树tree的中序遍历为&#xff08;l,2,3,…,n&#xff09;&#xff0c;其中数字1,2,3,…,n为节点编号。每个节点都有一个分数&#xff08;均为正整…...

HarmonyOS 相对布局(RelativeContainer)

1. HarmonyOS 相对布局&#xff08;RelativeContainer&#xff09; 文档中心:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-layout-development-relative-layout-V5   RelativeContainer为采用相对布局的容器&#xff0c;支持容器内部的子元素设…...

webpack5搭建react脚手架详细步骤

1. 初始化项目 首先&#xff0c;创建一个新目录并初始化项目&#xff1a; bash mkdir create-react cd create-react pnpm init --y git init 这里使用pnpm作为包管理工具&#xff0c;因为它在处理依赖和速度上表现更好。 2. 安装React和TypeScript 安装React和React-DOM…...

速盾:高防cdn怎么拦截恶意ip?

高防CDN&#xff08;Content Delivery Network&#xff09;是一种用于防御网络攻击和提供高可用性的服务。它通过分发网络流量&#xff0c;将用户的请求导向最近的服务器&#xff0c;从而提高网站的加载速度和稳定性。然而&#xff0c;不可避免地&#xff0c;有些恶意IP地址会试…...

docker详细操作--未完待续

docker介绍 docker官网: Docker&#xff1a;加速容器应用程序开发 harbor官网&#xff1a;Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台&#xff0c;用于将应用程序及其依赖项&#xff08;如库、运行时环…...

中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试

作者&#xff1a;Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位&#xff1a;中南大学地球科学与信息物理学院论文标题&#xff1a;BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接&#xff1a;https://arxiv.…...

ssc377d修改flash分区大小

1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

MODBUS TCP转CANopen 技术赋能高效协同作业

在现代工业自动化领域&#xff0c;MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步&#xff0c;这两种通讯协议也正在被逐步融合&#xff0c;形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...

Linux-07 ubuntu 的 chrome 启动不了

文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了&#xff0c;报错如下四、启动不了&#xff0c;解决如下 总结 问题原因 在应用中可以看到chrome&#xff0c;但是打不开(说明&#xff1a;原来的ubuntu系统出问题了&#xff0c;这个是备用的硬盘&a…...

UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)

UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中&#xff0c;UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化&#xf…...

IP如何挑?2025年海外专线IP如何购买?

你花了时间和预算买了IP&#xff0c;结果IP质量不佳&#xff0c;项目效率低下不说&#xff0c;还可能带来莫名的网络问题&#xff0c;是不是太闹心了&#xff1f;尤其是在面对海外专线IP时&#xff0c;到底怎么才能买到适合自己的呢&#xff1f;所以&#xff0c;挑IP绝对是个技…...

Python 实现 Web 静态服务器(HTTP 协议)

目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1&#xff09;下载安装包2&#xff09;配置环境变量3&#xff09;安装镜像4&#xff09;node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1&#xff09;使用 http-server2&#xff09;详解 …...

PHP 8.5 即将发布:管道操作符、强力调试

前不久&#xff0c;PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5&#xff01;作为 PHP 语言的又一次重要迭代&#xff0c;PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是&#xff0c;借助强大的本地开发环境 ServBay&am…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)

一、OpenBCI_GUI 项目概述 &#xff08;一&#xff09;项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台&#xff0c;其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言&#xff0c;首次接触 OpenBCI 设备时&#xff0c;往…...