ここでは、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 の追加検証手順 に、通信速度、 ケーブルの切断・再接続、メモリ測定、リソース回復テストの手順をまとめています。

測定値は リソース計測、 リリース前の確認は チェックリスト を参照してください。