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 |
- 設定後はメニューを閉じる
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 ★メインプログラム
- atmarktechno_RS485_model
- Cactusphere_100
- HLApp
各ファイルの変更内容については下記で説明します。
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 のヘッダファイルとソースファイルを変更するか、もしくは対応する実装を追加する必要があります。