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 ボタンを長押しして電源を入れます。以下の手順でシリアル通信設定を行います。
- メニューボタンを押す
- ▼キーを押して[各種設定]に合わせて[決定]
- ▼キーを押して[シリアルポート設定]に合わせて[決定]
- ▼キーを押して[ポート4設定]に合わせて[決定]
- 設定項目と設定値 を以下の通りに設定する
設定項目 | 設定値 |
---|---|
用途設定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 のヘッダファイルとソースファイルを変更するか、もしくは対応する実装を追加する必要があります。