Howto

CactusphereRS485モデルを用いたRS485シリアル通信アプリケーションの作成方法

Cactusphere RS485 モデルのファームフェアは通常 ModbusRTU 通信となっているため、そのほかの通信仕様を利用したい場合は既存アプリケーションを変更する必要があります。本 HowTo ではCactusphere RS485 モデルを用いた RS485 シリアル通信のため、既存アプリケーションの変更方法について説明します。
本 HowTo の構成は以下です。
1.システム構成図
2.対象機器
・2.1 KS-C8000 のシリアル通信設定
・2.2 KS-C8000 と Cactusphere RS485モデルとの接続
・2.3 KS-C8000 データフォーマット
3.既存アプリケーションの変更
・3.1 変更・新規作成ファイル一覧
・3.2 メインプログラムの変更
・3.3 KS-C8000 からの送信データ格納・取得関数の実装
・3.4 KS-C8000 からの送信データ格納関数の呼び出し
・3.5 ModbusDataFetchScheduler の変更
・3.6 app_manifest.json の変更
4.動作確認
・4.1 Azure IoT Central Application 接続用アプリケーションの作成方法
・4.2 Azure IoT Central Application の Setting 設定
5.応用編

1.システム構成図

今回作成するシステムの構成図を以下に示します。

KS-C8000 と Cactusphere RS485 モデルとの接続についての詳細は次節で説明します。

2.対象機器

クボタ計装指示計KS-C8000 を対象にします。

KS-C8000 は RS485 シリアル通信で現在の計量値を定期的に送信します。今回は「表示量」について取得し、計量値が安定したときだけ、Azure IoT Central Application へテレメトリ送信を行うプログラムを作成します。

2.1 KS-C8000 のシリアル通信設定

KS-C8000 の ON/OFF ボタンを長押しして電源を入れます。以下の手順でシリアル通信設定を行います。

  1. メニューボタンを押す
  2. ▼キーを押して[各種設定]に合わせて[決定]
  3. ▼キーを押して[シリアルポート設定]に合わせて[決定]
  4. ▼キーを押して[ポート4設定]に合わせて[決定]
  5. 設定項目と設定値 を以下の通りに設定する
設定項目 設定値
用途設定4 ホスト
機種設定4 なし
ボーレート4 9600bps
データフレーム4 8N1
送信制御4 なし
出力制御4 常時
出力モード4 連続
出力間隔加算4 なし
出力フォーマット4 表示量
ターミネータ4 CR+LF
  1. 設定後はメニューを閉じる

2.2 KS-C8000 と Cactusphere RS485モデルとの接続

今回の接続端子は以下です。

KS-C8000 Cactusphere RS485モデル
D2- Data-
D2+ Data+
GND GND_ISO

上記の通り配線します。配線中は KS-C8000、Cactusphere RS485モデルともに電源をオフにしてください。まず、KS-C8000 の背面を開けて端子を確認します。
D2-、 D2+、 GND となっている端子に配線します。下記の写真では D2- と D2+ の間に 120Ωの終端抵抗をいれています。

次に Cactusphere RS485モデルの配線を行います。Cactusphere RS485モデルは半二重設定にします。GND_ISO、 Data-、 Data+ となっている端子に配線します。半二重設定やポートの詳細についてはマニュアルをご確認ください。

これで、KS-C8000 と Cactusphere RS485モデルの配線については終了です。

2.3 KS-C8000 データフォーマット

アプリケーション修正前に KS-C8000 から送信されるデータフォーマットについて説明します。データフォーマットは2種類ありますが、今回は「表示量」のデータフォーマットを対象にします。以下は送信されるデータ例です。

S100G+ 128.5kg
U000G---------kg
U100G+ 10.0kg
U100G+ 47.5kg
U100G+ 79.0kg
U100G+ 554.0kg
H100G+ 554.0kg
H100G+ 554.0kg
S100G+ 554.0kg
S100G+ 554.0kg

dump した一つ分のデータだと以下のようなバイト列になります。

5302 3031 4730 202b 2020 3031 2e38 6b30 0367 0a0d

こちらのデータフォーマットについてポイントは以下です。

byte 説明
1 スタートコード 0x02
2 計量状態 H(0x48) :ホールド中、S(0x53) :質量が安定、U(0x55) :質量が非安定
7 質量値符号 +(0x02b)、-(0x2d)
8-15 表示量 例: 108.0(0x2020203130382e30)
16-17 質量単位 kg(0x6b67)
18 終了コード 0x03
19-20 改行記号 CR+LF(0x0d0a)

改行コードが CR+LF の場合に 20byte のデータになります。今回は計量状態が 'S' になったときの計量値をテレメトリ送信するようにアプリケーションを変更します。

3.既存アプリケーションの変更

RS485 シリアル通信を行うために既存アプリケーションを変更していきます。
今回は「Cactusphere 150 RS485モデル v210604」の 「Source code(zip)」 を変更するため、こちらをダウンロードします。デフォルトでは ModbusRTU を使用するようになっていますが、こちらを UART を用いた接続へ変更します。
変更方法についてはこちらのサンプルアプリケーションを参考にしています。そのため今回は HLApp のみで動作する構造になります。

3.1 変更・新規作成ファイル一覧

以下が今回変更するファイル、また新規作成するファイルの一覧です。変更するファイルが★、新規作成するファイルが☆になっています。

  • Firmware
    • HLApp
      • Cactusphere_100
        • atmarktechno_RS485_model
          • app_manifest.json★
        • RS485
          • ks_c8000Parser.c☆
          • ks_c8000Parser.h☆
          • ModbusDataFetchScheduler.c★
        • main.c ★メインプログラム

各ファイルの変更内容については下記で説明します。

3.2 メインプログラムの変更

main.c ファイルに UART 通信個所を追加します。上部に include 文、変数、関数宣言を追加します。

#include <applibs/uart.h>
static int uartFd = -1;
EventRegistration* uartEventReg = NULL;
static void UartEventHandler(EventLoop* el, int fd, EventLoop_IoEvents events, void* context);
static void CreateUartEvent();

次にさきほど関数宣言した関数を実装します。UartEventHandler() が UART 通信を行う関数になります。
★の個所は後ほど書き換えます。

static void UartEventHandler(EventLoop* el, int fd, EventLoop_IoEvents events, void* context)
{
    const size_t receiveBufferSize = 256;
    uint8_t receiveBuffer[receiveBufferSize + 1]; // allow extra byte for string termination
    ssize_t bytesRead;

    // Read incoming UART data. It is expected behavior that messages may be received in multiple
    // partial chunks.
    bytesRead = read(uartFd, receiveBuffer, receiveBufferSize);
    if (bytesRead == -1) {
        Log_Debug("ERROR: Could not read UART: %s (%d).\n", strerror(errno), errno);
        return;
    }

    if (bytesRead > 0) {
        //  ★ ここに受信したデータを格納する呼び出し追加
    }
}

CreateUartEvent() は UART の初期化部分をまとめた関数になります。UART_Config の設定値については今回は対象が決まっているため、KS-C8000 用の設定にします。EventLoop_RegisterIo() の引数に、先ほど実装した UartEventHandler() を渡しています。

static void CreateUartEvent() {
    UART_Config uartConfig;
    UART_InitConfig(&uartConfig);

    // KS-C8000用
    uartConfig.baudRate = 9600;
    uartConfig.parity = UART_Parity_None;
    uartConfig.stopBits = UART_StopBits_One;
    uartConfig.flowControl = UART_FlowControl_None;

  // すでに open 済の uartFd がある場合は close する
    if (uartFd > 0) {
        close(uartFd);
        uartFd = -1;
    }
  // open 済の uartFd がない場合に UART_Open() を実行
    if (uartFd == -1) {
        uartFd = UART_Open(MT3620_ISU3_UART, &uartConfig);
    }
  // open に失敗した場合
    if (uartFd == -1) {
        Log_Debug("ERROR: Could not open UART: %s (%d).\n", strerror(errno), errno);
        return;
    }
    else { // open に成功した場合 eventloop に登録する
        uartEventReg = EventLoop_RegisterIo(eventLoop, uartFd, EventLoop_Input, UartEventHandler, NULL);
    }
    if (uartEventReg == NULL) {
        return;
    }
}

既存の ClosePeripheralsAndHandlers() に追加した変数の close を追加します。

static void ClosePeripheralsAndHandlers(void){
    DisposeEventLoopTimer(azureTimer);
    DisposeEventLoopTimer(watchdogLoopTimer);
    DisposeEventLoopTimer(ledEventLoopTimer);
    SysEvent_UnregisterForEventNotifications(updateEventReg);
 // ここから追加
    EventLoop_UnregisterIo(eventLoop, uartEventReg);
    close(uartFd);
 // ここまで
    EventLoop_Close(eventLoop);
}

既存の TwinCallback() に関数呼び出しを追加します。

static void TwinCallback(DEVICE_TWIN_UPDATE_STATE updateState, const unsigned char *payload,
                         size_t payloadSize, void *userContextCallback)
{
~~~~~~~~~~省略~~~~~~~~~~~
    switch (err)
    {
    case NO_ERROR:
    case ILLEGAL_PROPERTY:
        DataFetchScheduler_Init(
            mTelemetrySchedulerArr[MODBUS_RTU],
            ModbusFetchConfig_GetFetchItemPtrs(ModbusConfigMgr_GetModbusFetchConfig()));
        CreateUartEvent(); // ここを追加
        if (err == NO_ERROR) {
            sphereStatus.isPropertySettingValid = true;
            ChangeLedStatus(LED_ON);
        } else { // ILLEGAL_PROPERTY
            // do not set ct_error and exitCode,
            // as it won't hang with this error.
            Log_Debug("ERROR: Receive illegal property.\n");
            sphereStatus.isPropertySettingValid = false;
            ChangeLedStatus(LED_BLINK);
            cactusphere_error_notify(err);
        }
        break;
~~~~~~~~~~省略~~~~~~~~~~~
}

3.3 KS-C8000 からの送信データ格納・取得関数の実装

KS-C8000 からの送信データ格納・取得関数を実装します。今回は RS485 フォルダにks_c8000Parser という名前のヘッダファイルとソースファイルを作成します。以下はヘッダファイルの内容です。ks_c8000Parser_AddData() を UartEventHandler() の★の個所から呼び出すことになります。

#ifndef _KS_C8000_PARSER_H_
#define _KS_C8000_PARSER_H_

extern void ks_c8000Parser_AddData(unsigned char* recvData, int recvBytes);//格納
extern double ks_c8000Parser_GetData(unsigned char* msg, int length);//測定値[kg]取得。少数点以下の値もあるため double 型を返す

#endif  // _KS_C8000_PARSER_H_

次はソースファイルの内容です。データ格納用の関数を実装します。

#include "ks_c8000Parser.h" 
#define MAX_BUF_SIZE 256

static unsigned char dataBuf1[MAX_BUF_SIZE];
static unsigned char dataBuf2[MAX_BUF_SIZE];
static int dataBytes = 0;
static bool use1Flag = true; //今使用しているのが dataBuf1 か否か

ks_c8000Parser_AddData(unsigned char* recvData, int recvBytes) {
    unsigned char* curs;

    if (use1Flag) {
        curs = dataBuf1;
    }
    else {
        curs = dataBuf2;
    }

    if (recvBytes >= 256) {
        recvBytes -= dataBytes;
    }

    strncpy(curs + dataBytes, recvData, (size_t)recvBytes);
    dataBytes += recvBytes;

    if (dataBytes > 90) {
        *(curs + dataBytes) = 0;
        //        Log_Debug("UART received %d bytes: '%s'.\n", dataBytes, curs); // 確認用
        dataBytes = 0;
        use1Flag = !use1Flag;
    }
}

次にデータ取得関数を実装します。

double
ks_c8000Parser_GetData(unsigned char* msg, int length) {
    double val = 0.0;
    unsigned char tmp[MAX_BUF_SIZE];

    if (use1Flag) {
        strncpy(tmp, dataBuf2, MAX_BUF_SIZE);
    }
    else {
        strncpy(tmp, dataBuf1, MAX_BUF_SIZE);
    }

    val = ks_c8000Parser_Parse(tmp, (int)strlen(tmp));
    return val;
}

また、内部関数としてデータをパースする関数を実装します。

static bool stableFlag = false;

static double
ks_c8000Parser_Parse(unsigned char* msg, int length)
{
    unsigned char* ptr = msg;
    double value = 0.0;
    int i = 0;

    // length is short
    if (length < 18) {
        return value;
    }

    // different start code
    if (*ptr != 0x02) {
        for (i = 0; i < length; i++) {
            if (*ptr == 0x02) { // スタートコードを見つける
                break;
            }
            ptr++;
        }
    }

    // value is not stable || data < 0
    if (*(ptr + 1) != 'S' || *(ptr + 6) == '-') {
        stableFlag = false;
        return value;
    }

    if (stableFlag) {
        return value;
    }

    ptr += 7;

    value += AsciiToInt(*ptr++) * 100000;
    value += AsciiToInt(*ptr++) * 10000;
    value += AsciiToInt(*ptr++) * 1000;
    value += AsciiToInt(*ptr++) * 100;
    value += AsciiToInt(*ptr++) * 10;
    value += AsciiToInt(*ptr++);
    ptr++; // '.' 
    value += AsciiToInt(*(ptr++)) / 10.0;

    stableFlag = true;
    return value;
}

数値は Ascii コードで届くため int に変換する関数を実装します。

static
int AsciiToInt(unsigned char a) {
    int value = 0;

    if (a >= '0' && a <= '9') {
        value = a - '0';
    }
    else {
        value = 0;
    }
    return value;
}

3.4 KS-C8000 からの送信データ格納関数の呼び出し

UartEventHandler() の★の個所を ks_c8000Parser_AddData() で置き換えます。

#include "ks_c8000Parser.h" 

static void UartEventHandler(EventLoop* el, int fd, EventLoop_IoEvents events, void* context)
{
~~~~~~~~~~省略~~~~~~~~~~~
    if (bytesRead > 0) {
        ks_c8000Parser_AddData(receiveBuffer, bytesRead); // 置き換え
    }
}

3.5 ModbusDataFetchScheduler の変更

現在の ModbusDataFetchScheduler_DoSchedule() は ModbusRTU 用の実装になっています。そのため、上記で実装した ks_c8000Parser_GetData() を呼び出す実装に変更します。

#include "ks_c8000Parser.h" 

static void
ModbusDataFetchScheduler_DoSchedule(DataFetchSchedulerBase* me)
{
    ModbusDataFetchScheduler* self = (ModbusDataFetchScheduler*)me;
    vector    devIDs;

    devIDs = ModbusFetchTargets_GetDevIDs(self->mFetchTargets);
    if (!vector_is_empty(devIDs)) {
        unsigned long* devIDCurs = (unsigned long*)vector_get_data(devIDs);

        for (int i = 0, n = vector_size(devIDs); i < n; i++) {
            unsigned long    devID = *devIDCurs++;
            vector    fetchItems = ModbusFetchTargets_GetFetchItems(
                self->mFetchTargets, devID);
            const ModbusFetchItem** fiCurs =
                (const ModbusFetchItem**)vector_get_data(fetchItems);
            for (int j = 0, m = vector_size(fetchItems); j < m; ++j) {
                const ModbusFetchItem* item = *fiCurs++;
                double fVal = 0;
                unsigned char msg[90];

                fVal = ks_c8000Parser_GetData(msg, strlen(msg)); ★データを取得する

                if (fVal > 0) {
                    StringBuf_AppendByPrintf(me->mStringBuf, "%f", fVal);

                    TelemetryItems_Add(me->mTelemetryItems,
                        item->telemetryName, StringBuf_GetStr(me->mStringBuf));
                    StringBuf_Clear(me->mStringBuf);
                }
            }
        }
    }
}

次に ModbusDataFetchScheduler_New() に DE, RE_N の設定を追加します。既存アプリケーションでは DE, RE_N の設定は RTApp で行っており、今回は HLApp のみの実装に変更するため追加が必要です。

#include <applibs/gpio.h>
#include <hw/mt3620.h>
int DEFd = -1;
int RE_NFd = -1;

DataFetchScheduler*
ModbusDataFetchScheduler_New(void)
    ModbusDataFetchScheduler* newObj =
        (ModbusDataFetchScheduler*)malloc(sizeof(ModbusDataFetchScheduler));
    DataFetchSchedulerBase* super;

    if (NULL != newObj) {
        super = &newObj->Super;
        if (NULL == DataFetchScheduler_InitOnNew(
            super, ModbusFetchTimerCallback, MODBUS_RTU)) {
            goto err;
        }
        newObj->mFetchTargets = ModbusFetchTargets_New();
        if (NULL == newObj->mFetchTargets) {
            goto err_delete_super;
        }
    }
 // ここから追加
    DEFd = GPIO_OpenAsOutput(MT3620_GPIO21, GPIO_OutputMode_PushPull, GPIO_Value_Low);
    if (DEFd < 0) {
        goto err_delete_super;
    }
    RE_NFd = GPIO_OpenAsOutput(MT3620_GPIO23, GPIO_OutputMode_PushPull, GPIO_Value_Low);
    if (RE_NFd < 0) {
        goto err_delete_super;
    }
 // ここまで
    super->DoDestroy = ModbusDataFetchScheduler_DoDestroy;
//	super->DoInit    = ModbusDataFetchScheduler_DoInit;  // don't override
    super->ClearFetchTargets = ModbusDataFetchScheduler_ClearFetchTargets;
    super->DoSchedule        = ModbusDataFetchScheduler_DoSchedule;

    return super;
err_delete_super:
    DataFetchScheduler_Destroy(super);
err:
    free(newObj);
    return NULL;
}

3.6 app_manifest.json の変更

上記で DE, RE_N の設定を追加しましたが、HLApp で使用するためには app_manifest.json を変更する必要があります。
"Gpio" を以下のように変更し、"Uart" を追加します。

    "Gpio": [ "$MT3620_GPIO8", "$MT3620_GPIO21", "$MT3620_GPIO23" ],
    "Uart": [ "$MT3620_ISU3_UART" ],

また、後述するように動作確認のためにここでは Azure IoT Central Application への接続情報を記載する必要があります。"Gpio" と "Uart" のみ変更し その他はデフォルトのままでビルドすると app_manifest.json の Validateに失敗し Visual Studio の [出力] ウインドウに「ビルドが失敗しました」と出力されます。

4.動作確認

動作確認のために、Azure IoT Central Application に接続します。app_manifest.json には Azure IoT Central Application 接続用の設定を加えてください。この変更を加えた状態でビルドが通ればアプリケーションの変更は成功しています。
下記は Azure IoT Central Application に接続して設定した場合の確認方法です。

4.1 Azure IoT Central Application 接続用アプリケーションの作成方法

アプリケーションの作成方法についてはCactusphere ソフトウェアマニュアルを確認して、アプリケーションを動かしてください。

4.2 Azure IoT Central Application の Setting 設定

Cactupshere RS485モデルではデバイステンプレートが提供されています。Setting に以下の値を入れて保存してください。この設定はデバイスアプリケーションがテレメトリ送信する値の項目名を解釈して、画面に表示させるために必要です。

  • ModbusDevConfig
    { "ModbusDevConfig": { "01" : { "baudrate": 9600 } } }
  • ModbusTelemetryConfig
    { "ModbusTelemetryConfig": { "Data1" : { "devID" : "01", "registerAddr" : "0000", "registerCount" : "2", "funcCode" : "03", "interval" : "2", "asFloat" : true} } }

    ビルド済みの HLApp を Cactusphere RS485モデル に流し込みます。正しく動いた場合はAzure IoT Central Application に計量値が送信されます。下記は Azure IoT Central Application の Overview で計量値を確認した画面です。

上記で設定した 'Data1' という項目名で計量値が表示されています。

5.応用編

上記で Azure IoT Central Application の Setting を設定しましたが、現在 main.c での UART の設定は KS-C8000 用に設定されています。もし、この設定をAzure IoT Central Application から変更したい場合は LibModbus のヘッダファイルとソースファイルを変更するか、もしくは対応する実装を追加する必要があります。