封装和使用 Arduino C++ 类库


封装和使用 Arduino C++ 类库

阅读本文档之前,请先确认您已了解C++ 节点且了解如何处理自定义类型。当然,需要也了解 Arduino 和 C++ 的基础知识。

我们知道,Arduino 已经是一个世界上最庞大的开源硬件生态,大多数我们需要使用的硬件都已经有对应的 Arduino 封装。如果我们能够拿来主义基于这些已存在的库壮大 Ticos Studio 的力量,显然会是一桩美事。

本文档用一个简单示例来说明 Arduino C++ 库的使用:一个连接了一盏 LED 灯和一个 NFC 感应器的 Arduino 开发板,当我们将一个 NFC 标签靠近开发板时,开发板连接的 LED 应该能够亮起来。

加入我们当前并没有现成可用的模组节点对应到所连接的 RFID/RFC 扫描器 PN532,然后想弄一个我们自己的版本。接下来我们将基于一个线程的 Arduino C++ 库:Adafruit-PN532open in new window 来进行封装。

为了达成此目的,我们需要进行如下工作:

  1. 定义一个新的自定义类型 pn532-device。这个节点描述了 RFID/NFC 模组以及是怎么连接到 Arduino 开发板上的。每一个这样的节点对应到一个物理设备,因此如果项目中用到多个这类设备,就需要加入多个此节点;

  2. 声明对第三方 C++ 库的依赖,这样 IDE 知道从哪里去下载对应的代码库,从而节省了开发者手动下载和配置的时间;

  3. 为这个自定义类型增加功能节点,以对应到相应的第三方 C++ 库所提供的函数和方法;

  4. 创建一个简化使用且能满足大多数场景的辅助节点,以读取一个 NFC 标签;

  5. 创建一系列的示例工作流,演示如何正确的使用上述创建的新节点;

创建设备节点并引入依赖库

我们可以先看一眼相应的 Adafruit 库里的构造函数:

class Adafruit_PN532{
    public:
        // Software SPI
        Adafruit_PN532(uint8_t clk, uint8_t miso, uint8_t mosi, uint8_t ss);
        // Hardware I2C
        Adafruit_PN532(uint8_t irq, uint8_t reset);
        // Hardware SPI
        Adafruit_PN532(uint8_t ss);
        // …
};

我们选择其中的第二个构造函数,也就是面向 I2C 连接方式:

  1. 创建一个名为 pn532-device 的新工作流。工作流的名称将成为自定义类型的名称,接下去我们将会传到功能节点。需要注意的是,一般对应到某个具体硬件设备的节点都应该被命名为 xxx-device

  2. 放置 not-implemented-in-ticos 节点以支持编写 C++ 代码;

  3. 放置一个 output-self 节点,且将其标签修改为 DEV。这样我们就定义了一个自定义类型。之后我们可以在项目浏览创各种发现 input-pn532-deviceoutput-pn532-device 端口;

  4. 放置一个 input-port 端口节点,且修改其标签为 IRQ。这个端口用于告知 Arduino 开发板该模组已经检测到了一个 NFC 标签。我们目前不需要用到构造函数里的第二个参数 reset,它只有在需要手动重启这个模组的时候才会用到;

接下来我们双击 not-implemented-in-ticos 节点以打开 C++ 代码编辑界面,并输入如下代码:

#pragma TICOS require "https://github.com/adafruit/Adafruit-PN532"

提示

目前还只支持从 GitHub 的主分支下载。如果你需要从一个库的其他分支下载代码,可以考虑先 fork 这个库。

这一行代码告知系统这个节点依赖于一个外部库。在第一次编译时,IDE 会检查是否本地已经存在这个库,如果还没有则会弹出一个提示框。开发者需要点击确认按钮以下载。

提示

下载完成后并不会自动继续编译过程,开发者需要重新启动编译。

接下来我们可以开始基于这个库来开发我们的节点了:

// Tell Ticos where it could download the library and its dependencies:
#pragma TICOS require "https://github.com/adafruit/Adafruit-PN532"
#pragma TICOS require "https://github.com/adafruit/Adafruit_BusIO"

// Include C++ library:
#include <Adafruit_PN532.h>

node {
    meta {
        // Define our custom type as a pointer on the class instance.
        using Type = Adafruit_PN532*;
    }

    // Keep Adafruit_PN532 object in state.
    // Instead of the `reset` port, specify `NOT_A_PORT`, since it is not needed
    Adafruit_PN532 nfc = Adafruit_PN532(constant_input_IRQ, NOT_A_PORT)

    void evaluate(Context ctx) {
        // It should be evaluated only once on the first (setup) transaction
        if (!isSettingUp())
            return;

        emitValue<output_DEV>(ctx, &nfc);
    }
}

在以上代码中,我们引入了这个外部库,并创建了一个类型为 Adafruit_PN532 的对象 nfc

接下来我们需要创建对应的功能节点。

功能节点

为了能够使用刚刚创建出来的对象,我们需要在节点中封装该对象的相应方法。我们传入了刚刚创建的这个自定义类型,因此每一个对应这个库的功能节点都包含了一个类型为 pn532-device 的输入。

这些方法的执行可能会带来一些副作用(因为在操作硬件)而且是异步的,也就是我们的程序并不会等待这些副作用发生完毕后再继续执行。因此对于这些功能节点我们需要添加两个 pulse 输入:一个用于执行相应的功能,另一个用于通知功能执行完毕(无论结果是成功还是失败)。

对于我们当前的场景,我们创建了两个功能节点:

  1. init:用于初始化硬件模组;

  2. pair-tag:用于检测 NFC 标签并读取其 UID;

提示

功能节点的命名应该是一个动词,比如 pair-taginitwrite-line 等。

创建 init 节点

接下来我们创建一个用于初始化 NFC 扫描器的 init 节点。

node-init

相应的 C++ 代码:

node {
    void evaluate(Context ctx) {
        // The node responds only if there is an input pulse
        if (!isInputDirty<input_INIT>(ctx))
            return;

        // Get a pointer to the `Adafruit_PN532` class instance
        auto nfc = getValue<input_DEV>(ctx);

        // Initialize RFID/NFC module
        nfc->begin();

        uint32_t versiondata = nfc->getFirmwareVersion();
        if (!versiondata) {
            // If the module did not respond with its version,
            // it's a connection error or something wrong with the module
            raiseError(ctx); // Initialization error
            return;
        }

        // Set the max number of retry attempts to read from a card
        // This prevents us from waiting forever for a card, which is
        // the default behavior of the PN532.
        nfc->setPassiveActivationRetries(1);

        // Configure the board to read an RFID/NFC tags
        nfc->SAMConfig();

        // Pulse that module initialized successfully
        emitValue<output_OK>(ctx, 1);
    }
}

现在我们可以初始化硬件模组了。

存储和比较 UID

根据卡和 UID 的类型不同,UID 的长度会是从 4 到 10 个字节之间(按标准 ISO14443A,RFID 的 UID 类型有:单尺寸、双尺寸、随机、三重尺寸等,参见 UID 类型open in new window)。这里我们将这个值封装为一个自定义数据类型 nfc-uid

node-nfc-uid

可以看到这里我们有 7 个类型为 byte 的输入端口,因此我们可以手动设置一个 NFC 标签的 UID。这样比较方便比较两个 UID。

该节点的 C++ 实现:

node {
    meta {
        // Declare custom type as a struct
        // in which we will store an array of bytes
        struct Type {
            uint8_t items[7];
        };
    }

    void evaluate(Context ctx) {
        Type uid;
        // Put each value from input terminal into the array of bytes
        uid.items[0] = (uint8_t)getValue<input_IN1>(ctx);
        uid.items[1] = (uint8_t)getValue<input_IN2>(ctx);
        uid.items[2] = (uint8_t)getValue<input_IN3>(ctx);
        uid.items[3] = (uint8_t)getValue<input_IN4>(ctx);
        uid.items[4] = (uint8_t)getValue<input_IN5>(ctx);
        uid.items[5] = (uint8_t)getValue<input_IN6>(ctx);
        uid.items[6] = (uint8_t)getValue<input_IN7>(ctx);

        emitValue<output_OUT>(ctx, uid);
    }
}

因为我们需要比较两个 UID 是否相等,这里我们再创建一个比较节点 equal(nfc-uid)。如果之前看过文档泛型和特化,就知道这是一个泛型节点 ticos/core/equal 针对类型 nfc-uid 的特化节点。

node-equal-nfc-uid

在实现中,我们直接使用标准库函数 memcmp

node {
    void evaluate(Context ctx) {
        auto uidA = getValue<input_IN1>(ctx);
        auto uidB = getValue<input_IN2>(ctx);

        // Function `memcmp` compares data by two pointers
        // and returns `0` if they are equal
        bool eq = memcmp(uidA.items, uidB.items, sizeof(uidA.items)) == 0;

        emitValue<output_OUT>(ctx, eq);
    }
}

接下来我们先试试看上面的工作是否可以正确运行。新建一个工作流,命名为 example-uid-equals

flow-example-uid-equals

运行一下可以得到,我们可以创建、存储和比较 UID 了。接下来该开始从 NFC 标签中读取 UID 了。

创建 pair-tag 节点

节点构造:

node-pair-tag

实现 C++ 代码:

node {
    void evaluate(Context ctx) {
        if (!isInputDirty<input_READ>(ctx))
            return;

        auto nfc = getValue<input_DEV>(ctx);

        // Create a variable of a custom type
        // by getting the type from output terminal
        typeof_UID uid;
        // Create a variable to store length of the UID
        uint8_t uidLength;

        // Fill UID with zeroes
        memset(uid.items, 0, sizeof(uid.items));
        // Detect the tag and read the UID
        bool res = nfc->readPassiveTargetID(
            PN532_MIFARE_ISO14443A,
            uid.items,
            &uidLength
        );

        if (res) {
            emitValue<output_UID>(ctx, uid);
            emitValue<output_OK>(ctx, 1);
        } else {
            emitValue<output_NA>(ctx, 1);
        }
    }

}

有了这个节点我们就可以开始读取 NFC 标签的 UID 了。

提示

作为一个功能完备的库,事实上 Adafruit 包含了一些其他的功能,比如从 NFC 标签读写除了 UID 之外的其他数据。如果我们希望自己封装的这些节点可以造福其他开发者,那么最好能够把最核心的这些能力都一起实现了。

简化使用的辅助节点

这种节点有点类似于我们在编程中遇到的语法糖,事实上没有增加任何额外功能,但是可以降低使用门槛。

为了让开发者无需自行组合使用我们上面提供的若干个节点来仅仅读取一下 UID,我们接下来构建一个名为 nfc-scanner 的辅助节点:

node-nfc-scanner

这个节点做了如下事情:

  1. 在设备启动时初始化 RFID/NFC 模组;

  2. 在模组完成初始化后,flip-flop 会被设置为 True 来使 gate 节点处于放行状态;

  3. 使用 gate 节点来避免在模组初始化完成之前就开始检测 NFC 标签;

接下来我们要为这个节点构建一个示例工作流 example-nfc-scanner

flow-example-nfc-scanner

提示

作为最佳实践,我们推荐为每一个新建的节点都在同一个节点库里创建一个示例工作流。

任何强调动手能力的开发者都明白,对于任何新功能来说,直接可以运行的例子价值有多么的巨大。

教程小结

在这篇文档中,我们需要记住的是如下几点:

  1. 为了能够使用第三方库,需要在代码开头添加一行 pragma 指令,类似于:#pragma TICOS require "https://github.com/some/library"

  2. 在使用的位置需要先用头文件引入这个库:#include <SomeLib.h>

  3. 在封装功能节点时命名应该用动词,比如 pair-taginit 之类;

  4. 尽可能创建一些简单易用的辅助节点,降低使用者的心智负担;

接下来,大家就可以开始封装 Arduino C++ 库,并往 Ticos Gallery 贡献自己的模组节点open in new window了。

期待大家的共同参与来一起完善 Ticos 开发者生态!

上次编辑于: 2022/12/17 07:45:59
Loading...