ここでは、AVModbus を使ったアプリケーションの拡張と、変更後の検証方法をまとめます。 最初の通信確認は はじめ方 を参照してください。
Client の設定
TCP の同時要求と待機キュー
{:ok, client} =
AVModbus.Client.start_link(
transport: :tcp,
host: "192.168.1.20",
port: 502,
timeout: 1_000,
max_pending: 4,
max_queue: 64
)max_pending は送信済みで応答待ちの要求数、max_queue はまだ送っていない要求数の上限です。
Serial Client は同時に 1 件ずつ処理します。上限に達した場合は {:error, :queue_full} を返します。
待機時間もタイムアウトに含まれます。
非同期で要求を送る
case AVModbus.Client.send_request(
client,
1,
{:read_holding_registers, 0, 2},
timeout: 500,
to: self()
) do
ref when is_reference(ref) ->
receive do
{AVModbus.Client, ^ref, result} -> result
end
{:error, reason} ->
{:error, reason}
end要求の受け付けは短い同期処理ですが、応答は待ちません。 受け付けた要求の結果は reference とともに一度だけ通知します。
読み取り処理のリトライ
AVModbus.Client.read_holding_registers(
client, 1, 0, 2,
timeout: 500,
retries: 2,
backoff: {100, 1_000}
)retries: を使えるのは安全に再実行できる読み取り処理です。
書き込み・診断・カスタム要求は自動リトライしません。
Modbus 例外応答や不正な応答もリトライの対象外です。
Supervisor から起動する
children = [
{AVModbus.Client,
[transport: :tcp, host: "192.168.1.20", port: 502, name: :plc_client]}
]
Supervisor.start_link(children, strategy: :one_for_one)
AVModbus.Client.read_holding_registers(:plc_client, 1, 0, 2)Serial Transport を入れ替える
AVModbus.Serial.Transport を実装すると、既定の AtomVM UART 以外の通信路も使用できます。
Transport には open/1・read・write・close が必要です。
open/1 はインスタンスごとの設定を受け取ります。
{:ok, client} =
AVModbus.Client.start_link(
transport: :rtu,
serial: [peripheral: "UART2", speed: 38_400, tx: 4, rx: 5]
)RS485 の送受信方向を手動で切り替える場合は、送信完了待ちと切り替えを write/2 内で処理します。
UART ハンドルをアプリケーションから渡す場合は、ハンドルの終了と復旧もアプリケーションの責任です。
Server と戻り値
AVModbus.Server は RTU・ASCII・TCP で共通の起動 API を提供します。
{:ok, memory} = AVModbus.Memory.start_link()
:ok = AVModbus.Memory.put(memory, :holding_register, 10, [18, 1000, 3])
{:ok, server} =
AVModbus.Server.start_link(
handler: {AVModbus.Memory, memory},
transport: :rtu,
units: [1]
)主な戻り値は次のとおりです。
| 状況 | 戻り値 |
|---|---|
| 読み取り成功 | {:ok, value} |
| 書き込み成功 | :ok |
| Modbus 例外応答 | {:error, {:exception, reason}} |
| 要求に一致しない応答 | {:error, {:invalid_response, pdu}} |
| タイムアウト | {:error, :timeout} |
| 接続を使えない | {:error, :closed} |
| 待機キューが満杯 | {:error, :queue_full} |
Serial の Unit ID は 0..247 で、0 は書き込みの broadcast に使います。
TCP では gateway を考慮して 0..255 を使用します。
TCP Server を使う場合
{:ok, server} =
AVModbus.Server.start_link(
handler: {AVModbus.Memory, memory},
transport: :tcp,
address: {192, 168, 10, 20},
port: 502,
connections: 4,
max_buffer: 2_048,
allow: [{{192, 168, 10, 0}, 24}],
authorize: fn _role, unit_id, _request -> unit_id == 1 end
)max_buffer はブローカー内の未配送データだけを制限します。ソケットや処理中のデータを含む
メモリ全体の上限ではありません。接続終了時には実行中の handler を停止します。
handler に長い寿命が必要なら、別の Supervisor 管理プロセスへ委譲してください。
TCP の受信・終了処理については
ADR 0007
に記録しています。
対象 AtomVM は Modbus/TCP Security に必要な相互認証 TLS に対応していません。
TLS を指定すると {:error, :tls_not_supported} を返し、暗号化なしには切り替わりません。
TCP Server は信頼できるネットワーク内で使用し、address:・allow:・authorize: と
ファイアウォールで接続元と操作を制限してください。
テストする
通常の開発では次を実行します。
mise install
mix deps.get
mix format --check-formatted
mix test --cover
プロパティテストや長時間テストは必要なときだけ実行します。
FUZZ_RUNS=10000 mix test test/avmodbus/property_test.exs
SOAK_SECONDS=600 mix test --include soak test/avmodbus/soak_test.exs
AVMODBUS_PEAK_REPORT=1 mix test test/avmodbus/tcp_server_load_test.exs
最後のコマンドは TCP 受信中のメモリやメールボックスの標本化した最大値を報告します。 AtomVM 実機や OS 全体の厳密なピーク値ではありません。
外部の Modbus 実装とテストする
python3 -m venv /tmp/avmodbus-pymodbus
/tmp/avmodbus-pymodbus/bin/pip install 'pymodbus==3.15.0' pyserial
PYMODBUS_PYTHON=/tmp/avmodbus-pymodbus/bin/python mix test --include interop
libmodbus が使える環境では次も実行します。
LIBMODBUS_INTEROP=1 mix test --include libmodbus test/avmodbus/libmodbus_interop_test.exs
外部実装とのテストはホスト上の TCP・仮想シリアル接続を使用します。
実機の RS485 通信を確認する試験ではありません。
CI の interoperability job の結果も、リリース前に確認してください。
実機で検証する
XIAO ESP32-C5 の追加検証手順 に、通信速度、 ケーブルの切断・再接続、メモリ測定、リソース回復テストの手順をまとめています。