很荣幸能参与到这次#DAYU200体验官#的活动,感谢51CTO,感谢润和。

本文主要分享如何在DAYU200上使用NAPI打通南向和北向,实现RGB LED 彩灯控制器的过程。
演示视频如链接视频链接

1. 开发环境

此实验的主要开发环境如下:
DevEco Studio版本:DevEco Studio 3.0.0.900
OpenHarmony版本:OpenHarmony 3.1 Release
napi_generator版本:napi_generator_20220319.tar.gz
(本人电脑环境WIN11+WSL2+Ubuntu20.04)

2 南向部分

2.1 移除原来的控制

默认的彩灯是有颜色的,这个是因为在OpenHarmony 3.1 Release版本中battery_manager中有led_service会控制彩灯,不然控制上会有冲突,需要先移除,修改方式如下
在/base/powermgr/battery_manager/bundle.json中删除以下这行

 "//base/powermgr/battery_manager/charger:led_service",

2.2 NAPI 组件的实现

2.2.1 NAPI组件简介

NAPI(Native API)组件是一套对外接口基于Node.js N-API规范开发的原生模块扩展开发框架。
图 1 NAPI组件架构图
图 1 NAPI组件架构图
使用场景:
NAPI适合封装IO、CPU密集型、OS底层等能力并对外暴露JS接口,通过NAPI可以实现JS与C/C++代码互相访问。我们可以通过NAPI接口构建例如网络通信、串口访问、多媒体解码、传感器数据收集等模块。
详细介绍NAPI组件简介

2.2.2 NAPI框架生成工具简介

NAPI框架代码生成工具(napi_generator),它可以根据用户指定路径下的ts(typescript)接口文件一键生成NAPI框架代码、业务代码框架、GN文件等。在开发JS应用与NAPI间接口时,底层框架开发者无需关注Nodejs语法、C++与JS之间的数据类型转换等上层应用转换逻辑,只关注底层业务逻辑即可,专业的人做专业的事,从而可以大大提高开发效率。目前工具支持可执行文件、VS Code插件、IntelliJ插件三种入口。
工具使用说明
下载链接

目前工具还在持续开发完善中,详细信息查阅NAPI框架代码生成工具版本规划

2.2.3 NAPI接口@ohos.dayuled.d.ts的定义

在 napi_generator 下有@ohos.napitest.d.ts 可以参考
或者安装完DevEco Studio 3.0.0.900且安装SDK找到以下路径,里面有d.ts文件参考
C:\Users\xxx(你的window用户名)\AppData\Local\OpenHarmony\Sdk\ets\3.1.6.5\api
C:\Users\xxx(你的window用户名\AppData\Local\OpenHarmony\Sdk\js\3.1.6.5\api
目前napi_generator 还在持续开发中不确定哪些数据类型能支持,需要自行验证。

本案例定义@ohos.dayuled.d.ts如下

declare namespace dayuled {
  /**
   * Checks whether the screen of a device is on or off.
   *
   * @return Returns true if the screen is on; returns false otherwise.
   * @since 7
   */
  function redStatusChange(status: number): void;
  function greenStatusChange(status: number): void;
  function blueStatusChange(status: number): void;
  function ledRGBStatusChange(r: number, g: number, b: number): void;
}
export default dayuled;

前三函数分别控制RGB灯的开关,最后一个可以同时控制RGB灯的开关

2.2.4 使用 napi_generator 生成框架

napi_generator下载链接
从以上链接可以下载到

│   |   ├── generator.jar           # IntelliJ插件
│   │   |── napi_generator-linux    # Linux可执行程序 
│   │   |── napi_generator-win.exe  # Windows可执行程序    
|   |   └── napi_generator-macos    # Mac可执行程序              

我这边验证了使用Linux和Windows可执行程序可以生成框架代码。
使用方法将工具和.d.ts文件及需要import的d.ts文件也放入到待转换的d.ts文件相同的目录下(本案例中没有import其他d.ts文件)。
以下以ubuntu环境介绍,文件放置如以下

soon@SOON_NB16:~/napi/napi_generator/dayulight_nocallback$ tree
.
├── @ohos.dayuled.d.ts
└── napi_generator-linux

生成指令

./napi_generator-linux -f @ohos.dayuled.d.ts

生成成功后会有success提示,且生成文件如下

soon@SOON_NB16:~/napi/napi_generator/dayulight_nocallback$ ./napi_generator-linux -f @ohos.dayuled.d.ts
success
soon@SOON_NB16:~/napi/napi_generator/dayulight_nocallback$ tree
.
├── @ohos.dayuled.d.ts
├── BUILD.gn
├── binding.gyp
├── dayuled.cpp
├── dayuled.h
├── dayuled_middle.cpp
├── napi_generator-linux
├── test.sh
├── x_napi_tool.cpp
└── x_napi_tool.h

0 directories, 10 files
soon@SOON_NB16:~/napi/napi_generator/dayulight_nocallback$

详细说明参考NAPI框架生成工具使用说明

2.2.5 NAPI框架生成代码集成到OpenHarmony模块位置

模块目录理论上可以建立在OpenHarmony代码库的任何地方,例如在foundation目录下新建dayuled。在 dayuled 目录下,把之前用可执行文件或者插件转换出来的文件全部拷贝到该目录下。如以下

soon@SOON_NB16:~/ohos310/foundation/dayuled$ tree
.
├── @ohos.dayuled.d.ts
├── BUILD.gn
├── binding.gyp
├── dayuled.cpp
├── dayuled.h
├── dayuled_middle.cpp
├── napi_generator-linux
├── ohos.build
├── test.sh
├── x_napi_tool.cpp
└── x_napi_tool.h

0 directories, 11 files
soon@SOON_NB16:~/ohos310/foundation/dayuled$

2.2.6 编译修改点

2.2.6.1 修改build.gn文件

import("//build/ohos.gni")

ohos_shared_library("dayuled")
{
    sources = [
        "dayuled_middle.cpp",
        "dayuled.cpp",
        "x_napi_tool.cpp",
    ]
    include_dirs = [
        ".",
        "//third_party/node/src",
        "//base/hiviewdfx/hilog/interfaces/native/innerkits/include",
    ]
    deps=[
        "//foundation/ace/napi:ace_napi",
        "//base/hiviewdfx/hilog/interfaces/native/innerkits:libhilog",
    ]
    public_deps = [
        "//base/hiviewdfx/hilog/interfaces/native/innerkits:libhilog",
    ]
    remove_configs = [ "//build/config/compiler:no_rtti" ]
    cflags=[
    ]
    cflags_cc=[
        "-frtti",
    ]
    ldflags = [
    ]

    relative_install_dir = "module"
    part_name = "dayuled_interface"
    subsystem_name = "dayuled"
}

主要修改内容是添加hilog相关依赖、part_name和subsystem_name修改

2.2.6.2 修改ohos.build文件

新建一个文件ohos.build并修改如下,其中module_list选项中的"//foundation/dayuled"指的是 dayuled 目录,":dayuled"指的是上面BUILD.gn中的目标ohos_shared_library("dayuled")。

{
  "subsystem": "dayuled",
  "parts": {
    "dayuled_interface": {
      "module_list": [
        "//foundation/dayuled:dayuled"
      ],
      "test_list": []
    }
  }
}

2.2.6.3 增加子系统

在源码/build/subsystem_config.json中增加子系统选项。如下所示:

,
  "dayuled": {
    "project": "hmf/dayuled",
    "path": "foundation/dayuled",
    "name": "dayuled",
    "dir": "foundation"
  }

2.2.6.4 添加功能模块

在产品配置中添加上述子系统的功能模块,编译到产品产出文件中,例如在源码productdefine/common/products/rk3568.json中增加part选项,其中 dayuled 就是上面填的part_name,dayuled_interface 就是上面填的subsystem_name。

    ,
    "dayuled:dayuled_interface":{},

以上步骤参考NAPI框架生成代码集成到OpenHarmony的方法

2.2.6.5 修改编译错误

工具还有一些问题需要手动修改下foundation/dayuled/dayuled_middle.cpp才能编译通过

主要修改 以下两点
修改1
number_c_to_js没有用到需要注释相关内容
报错信息
number_c_to_js.png
修改方式

/*
static napi_value number_c_to_js(XNapiTool *pxt, const std::type_info &n, void *num)
{
    if (n == typeid(int32_t))
        return pxt->SwapC2JsInt32(*(int32_t *)num);
    else if (n == typeid(uint32_t))
        return pxt->SwapC2JsUint32(*(uint32_t *)num);
    else if (n == typeid(int64_t))
        return pxt->SwapC2JsInt64(*(int64_t *)num);
    else if (n == typeid(double_t))
        return pxt->SwapC2JsDouble(*(double_t *)num);
    return nullptr;
}
#define NUMBER_C_2_JS(pxt, n) \
    number_c_to_js(pxt, typeid(n), &n)
*/

修改2因为没有callback所以下相关函数vio->out要移除
报错信息
Screenshot 20220606 194027.png
修改方式

redStatusChange(vio->in0, vio->out);
greenStatusChange(vio->in0, vio->out);
blueStatusChange(vio->in0, vio->out);
ledRGBStatusChange(vio->in0, vio->in1, vio->in2, vio->out);

改为

redStatusChange(vio->in0);
greenStatusChange(vio->in0);
blueStatusChange(vio->in0);
ledRGBStatusChange(vio->in0, vio->in1, vio->in2);

2.2.7 RGB LED控制代码的实现

之前想通过调用"light_if.h"但是一直没有调通,目前是参考/base/powermgr/battery_manager/charger/led/battery_led.cpp 用echo的方式实现。

2.2.7.1 RGB LED指令开关控制方式

实际测试使用以下指令可以控制RGB LED的开关,但是绿色和蓝色是相反的(后面说明如何修改)。

开关红灯
# echo 0 > /sys/class/leds/red/brightness                                      
# echo 1 > /sys/class/leds/red/brightness 

开关蓝灯
# echo 0 > /sys/class/leds/green/brightness                                    
# echo 1 > /sys/class/leds/green/brightness

开关绿灯
# echo 1 > /sys/class/leds/blue/brightness                                     
# echo 0 > /sys/class/leds/blue/brightness 

2.2.7.2 在 dayuled.cpp中实现RGB LED 控制代码

主要控制代码如下

#include "dayuled.h"
#include "utils/log.h"

#include <fstream>
#include <memory>
#include <cstring>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
namespace dayuled {

const std::string LEDS_BASE_PATH = "/sys/class/leds";
std::vector<std::string> g_ledsNodeName;
std::string g_redLedsNode = "red";
std::string g_greenLedsNode = "green";
std::string g_blueLedsNode = "blue";
int redStatus = 0;
int greenStatus = 0;
int blueStatus = 0;

void WriteLedInfoToSys(const int redbrightness, const int greenbrightness, const int bluebrightness)
{
    HILOG_INFO("[SOON] %{public}s enter", __func__);
    FILE* file = nullptr;
    std::string redLedPath = LEDS_BASE_PATH + "/" + g_redLedsNode + "/" + "brightness";
    std::string greenLedPath = LEDS_BASE_PATH + "/" + g_greenLedsNode + "/" + "brightness";
    std::string blueLedPath = LEDS_BASE_PATH + "/" + g_blueLedsNode + "/" + "brightness";
    HILOG_INFO("[SOON] %{public}s: redLedPath is %{public}s, greenLedPath is %{public}s, blueLedPath is %{public}s", __func__,
        redLedPath.c_str(), greenLedPath.c_str(), blueLedPath.c_str());

    file = fopen(redLedPath.c_str(), "w");
    if (file == nullptr) {
        HILOG_INFO("[SOON] %{public}s: red led file open failed. redLedPath is %{public}s", __func__, redLedPath.c_str());
        return;
    }
    int ret = fprintf(file, "%d\n", redbrightness);
    if (ret < 0) {
        HILOG_INFO("[SOON] %{public}s: red led file fHILOG_INFO failed.", __func__);
    }
    ret = fclose(file);
    if (ret < 0) {
        return;
    }

    file = fopen(greenLedPath.c_str(), "w");
    if (file == nullptr) {
        HILOG_INFO("[SOON] %{public}s: green led file open failed. greenLedPath is %{public}s", __func__, greenLedPath.c_str());
        return;
    }
    ret = fprintf(file, "%d\n", greenbrightness);
    if (ret < 0) {
        HILOG_INFO("[SOON] %{public}s: green led file fHILOG_INFO failed.", __func__);
    }
    ret = fclose(file);
    if (ret < 0) {
        return;
    }

    file = fopen(blueLedPath.c_str(), "w");
    if (file == nullptr) {
        HILOG_INFO("[SOON] %{public}s: blue led file open failed.", __func__);
        return;
    }
    ret = fprintf(file, "%d\n", bluebrightness);
    if (ret < 0) {
        HILOG_INFO("[SOON] %{public}s: blue led file fHILOG_INFO failed. blueLedPath is %{public}s", __func__, blueLedPath.c_str());
    }
    ret = fclose(file);
    if (ret < 0) {
        return;
    }

    HILOG_INFO("[SOON] %{public}s exit", __func__);
    return;
}

bool redStatusChange(NUMBER_TYPE_1 &status)
{
    HILOG_INFO("[SOON] redStatusChange: enter status=%{public}d redStatus=%{public}d greenStatus=%{public}d blueStatus=%{public}d",status, redStatus, greenStatus, blueStatus);
    redStatus = status;
    WriteLedInfoToSys(redStatus, greenStatus, blueStatus);
    HILOG_INFO("[SOON] redStatusChange: end ");
    return true;
}

bool greenStatusChange(NUMBER_TYPE_2 &status)
{
    HILOG_INFO("[SOON] greenStatusChange: enter status=%{public}d redStatus=%{public}d greenStatus=%{public}d blueStatus=%{public}d",status, redStatus, greenStatus, blueStatus);
    greenStatus = status;
    WriteLedInfoToSys(redStatus, greenStatus, blueStatus);
    HILOG_INFO("[SOON] greenStatusChange: end ");
    return true;
}

bool blueStatusChange(NUMBER_TYPE_3 &status)
{
    HILOG_INFO("[SOON] blueStatusChange: enter status=%{public}d redStatus=%{public}d greenStatus=%{public}d blueStatus=%{public}d",status, redStatus, greenStatus, blueStatus);
    blueStatus = status;
    WriteLedInfoToSys(redStatus, greenStatus, blueStatus);
    HILOG_INFO("[SOON] blueStatusChange: end ");
    return true;
}

bool ledRGBStatusChange(NUMBER_TYPE_4 &r, NUMBER_TYPE_5 &g, NUMBER_TYPE_6 &b)
{
    HILOG_INFO("[SOON] ledRGBStatusChange: enter redStatus=%{public}d greenStatus=%{public}d blueStatus=%{public}d", r, g, b);
    WriteLedInfoToSys(r, g, b);
    HILOG_INFO("[SOON] ledRGBStatusChange: end ");
    return true;
}
}

2.2.7.3 brightness节点权限的修改

这一步我不确定有没有更好的方式,如果有大佬知道请留言指导,谢谢!
异常log如下,因为没有w权限导致打开文件失败

[dayuled.cpp(WriteLedInfoToSys)] [SOON] WriteLedInfoToSys: red led file open failed. redLedPath is /sys/class/leds/red/brightness

目前我的解决方式是修改base/startup/init_lite/services/etc/init.cfg中约379行后添加权限修改。

                "chown system system /sys/class/leds/red/brightness",
                "chown system system /sys/class/leds/green/brightness",
                "chown system system /sys/class/leds/blue/brightness",

这个位置下方添加

                "chmod 0666 /sys/class/leds/red/brightness",
                "chmod 0666 /sys/class/leds/green/brightness",
                "chmod 0666 /sys/class/leds/blue/brightness",

修改RGB LED brightness节点的权限为0666

2.2.7.4 修改绿色LED和蓝色LED控制控制相反问题

此处直接修改kernel/linux/patches/linux-5.10/rk3568_patch/kernel.patch中rk3568-toybrick-x10.dtsi以下部分将绿色和蓝色GPIO对调。

+++ b/linux-5.10/rk3568_patch/kernel.patch
@@ -96124,7 +96124,7 @@ index 000000000..14d80d46b
 +    gpio_leds: gpio-leds {
 +              compatible = "gpio-leds";
 +              led@1 {
-+                      gpios = <&gpio4 RK_PC2 GPIO_ACTIVE_HIGH>;
++                      gpios = <&gpio4 RK_PC5 GPIO_ACTIVE_HIGH>;
 +                      label = "blue"; // Blue LED
 +                      retain-state-suspended;
 +              };
@@ -96136,7 +96136,7 @@ index 000000000..14d80d46b
 +              };
 +
 +              led@3 {
-+                      gpios = <&gpio4 RK_PC5 GPIO_ACTIVE_HIGH>;
++                      gpios = <&gpio4 RK_PC2 GPIO_ACTIVE_HIGH>;
 +                      label = "green"; // Green LED
 +                      retain-state-suspended;
 +              };

2.2.8 编译验证

在做完以上修改后建议执行一次全编译并用烧录整包固件的方式更新,后续调试再使用增量编译方式替换so。具体说明如下

2.2.8.1 全编译验证完整烧录方式

使用hb指令如下

hb set

选择rk3568,不是DAYU

hb build -f

需要带-f参数执行全编译

编译成功后拷贝out/rk3568/packages/phone/images目录到window下烧录,具体烧录方式此处不做展开,可以看论坛其他帖子。

2.2.8.2 增量编译替换so验证方式

如果只是修改dayuled文件夹里面的内容可以用这个方式来节省开发时间。具体说明如下
因为之前使用过hb set 选了rk3568此时可以不用重新执行hb set
只需要在保存你要的修改之后执行以下指令

hb build

不带参数默认增量编译。
编译成功后会在以下路径生成libdayuled.z.so,目录如下所示

out/rk3568/dayuled/dayuled_interface/libdayuled.z.so

或者

out/rk3568/packages/phone/system/lib/module/libdayuled.z.so

这两个位置的libdayuled.z.so是一样的

将这个libdayuled.z.so拷贝到C:\Users\XXX(你的用户名)\AppData\Local\OpenHarmony\Sdk\toolchains\3.1.6.5(这个路径是DevEco Studio 3.0.0.900 SDK 安装路径)或者你放hdc_std工具的路径,
将OTG数据线与开发板相连并执行以下操作
在C:\Users\XXX(你的用户名)\AppData\Local\OpenHarmony\Sdk\toolchains\3.1.6.5输入CMD,执行以下指令即可替换so

C:\Users\soonl\AppData\Local\OpenHarmony\Sdk\toolchains\3.1.6.5>hdc_std shell
# mount -o remount,rw /
# exit

C:\Users\soonl\AppData\Local\OpenHarmony\Sdk\toolchains\3.1.6.5>hdc_std file send libdayuled.z.so /system/lib/module/
FileTransfer finish, Size:21876 time:21ms rate:1041.71kB/s

C:\Users\soonl\AppData\Local\OpenHarmony\Sdk\toolchains\3.1.6.5>

操作图片见GIF
hdc_std_file_send.gif

以上南向部分代码修改整理在附件ohos310_dayuled_napi.zip 中。

3 北向部分

要点是要将@ohos.dayuled.d.ts手动放到C:\Users\xxx(你的用户名)\AppData\Local\OpenHarmony\Sdk\ets\3.1.6.5\api路径下,这样打包的时候才不会报错。

3.1 将@ohos.dayuled.d.ts手动放到api下

在没有放@ohos.dayuled.d.ts的情况下去import dayuled from '@ohos.dayuled';会报“Cannot find module '@ohos.dayuled' or its corresponding type declarations.
”,报错截图如下
HAP_BUILD_ERROR.png
将@ohos.dayuled.d.ts手动放到C:\Users\xxx(你的用户名)\AppData\Local\OpenHarmony\Sdk\ets\3.1.6.5\api路径下就可以正常,如果找不到这个sdk路径那可能是你的DevEco Studio版本不是DevEco Studio 3.0.0.900,或者你有修改了Sdk的路径。

3.2 ets主要代码

页面只有简单的文本和四个Toggle的switch预览截图如下
preview.gif

entry/src/main/ets/MainAbility/pages/index.ets主要代码如下,entry完整代码见附件

import dayuled from '@ohos.dayuled';
@Entry
@Component
struct Index {
  @State message: string = 'RGB LED 控制'

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)

        Text(`红色 LED`).flexGrow(1).fontSize('30lpx')
        Toggle({
          type: ToggleType.Switch,
        })
          .onChange((res) => {
            console.log("[SOON] Red Toggle=" + res.toString())
            if(res == true){
              dayuled.redStatusChange(1);
            }else{
              dayuled.redStatusChange(0);
            }
            console.log("[SOON] Red End")
          })
          .padding(0)
          .width(100)
          .height(60)
          .backgroundColor(Color.Gray)
          .borderRadius(20)
          .selectedColor(Color.Red)

        Text(`绿色 LED`).flexGrow(1).fontSize('30lpx')
        Toggle({
          type: ToggleType.Switch,
        })
          .onChange((res) => {
            console.log("[SOON] Green Toggle=" + res.toString())
            if(res == true){
              dayuled.greenStatusChange(1);
            } else {
              dayuled.greenStatusChange(0);
            }
            console.log("[SOON] Green End")
          })
          .padding(0)
          .width(100)
          .height(60)
          .backgroundColor(Color.Gray)
          .borderRadius(20)
          .selectedColor(Color.Green)

        Text(`蓝色 LED`).flexGrow(1).fontSize('30lpx')
        Toggle({
          type: ToggleType.Switch,
        })
          .onChange((res) => {
            console.log("[SOON] Blue Toggle=" + res.toString())
            if(res == true){
              dayuled.blueStatusChange(1);
            } else {
              dayuled.blueStatusChange(0);
            }
            console.log("[SOON] BLUE End");
          })
          .padding(0)
          .width(100)
          .height(60)
          .backgroundColor(Color.Gray)
          .borderRadius(20)
          .selectedColor(Color.Blue)

        Text(`全部 LED`).flexGrow(1).fontSize('30lpx')
        Toggle({
          type: ToggleType.Switch,
        })
          .onChange((res) => {
            console.log("[SOON] All Toggle=" + res.toString())
            if(res == true){
              dayuled.ledRGBStatusChange(1, 1, 1);
            } else {
              dayuled.ledRGBStatusChange(0, 0, 0);
            }
            console.log("[SOON] All End");
          })
          .padding(0)
          .width(100)
          .height(60)
          .backgroundColor(Color.Gray)
          .borderRadius(20)
          .selectedColor(Color.White)
      }
      .width('100%')
    }
    .height('100%')

  }
}

3.3 签名打包HAP

在OTG连接DAYU200开发板后,使用DevEco Studio 3.0.0.900 的自动签名即可。操作步骤见动图
sign.gif
以上HAP关键代码整理如附件dayulight_hap.zip

4 HAP集成打包

直接将HAP集成到了固件中打包的主要步骤如下。

4.1 拷贝签名后的HAP到applications/standard/hap/下

从应用目录拷贝签名后的hap
即将

entry/build/default/outputs/default/entry-default-signed.hap

拷贝到源码以下路径并重命名为你想要的名称,如Dayulight.hap

applications/standard/hap/Dayulight.hap

4.2 修改applications/standard/hap/BUILD.gn

添加以下内容

ohos_prebuilt_etc("dayulight_hap") {
  source = "Dayulight.hap"
  module_install_dir = "app"
  part_name = "prebuilt_hap"
  subsystem_name = "applications"
}
group("hap") {
  ......
   else if (defined(product_name) && product_name == "rk3568") {
    ......
    deps += [ "//applications/standard/hap:dayulight_hap" ]
  }
  ......
}

修改完成后进行增量编译,重烧image(我编译的image打包如附件DayuLight_images.zip)即可看到包含了DAYU Light这个应用。

5 相关源码

如附件,且已开源至Gitee,点击DAYULIGHT跳转
地址如下:

https://gitee.com/soonliao/dayu-light

以上在DAYU200上使用NAPI打通南向和北向,实现RGB LED 彩灯控制器的分享,感谢阅读!

参考链接:
NAPI组件简介
napi_generator工具使用说明
NAPI框架生成代码集成到OpenHarmony的方法

附件链接

dayulight_hap.zip(https://ost.51cto.com/resource/2030)
ohos310_dayuled_napi.zip(https://ost.51cto.com/resource/2031
DayuLight_images.zip(https://ost.51cto.com/resource/2032

想了解更多关于开源的内容,请访问:

51CTO 开源基础软件社区

https://ost.51cto.com/#bkwz