mcpField
M
modbus-protocol

modbus-protocol

github.com/jnzader-vault開発092 回インストール8 週間前に更新
リポジトリ

modbus-protocol について

modbus-protocol は、Modbus TCP/RTU によるセンサー通信の実装ガイドです。Rust、Go、Python を対象に、レジスター操作、PLC のアドレスマップ、データ変換、産業用途のポーリング方法を扱います。

主な機能

  1. 01Rust、Go、Python の実装ガイド
  2. 02TCP/RTU のレジスター読み書き例
  3. 03PLC アドレスマップの例
  4. 04レジスターから浮動小数点への変換例
  5. 05一括読み取りと接続再利用の指針

活用シーン

  1. 01産業用 PLC からのセンサーデータ取得
  2. 02SCADA またはゲートウェイの連携構築
  3. 03センサーデータのポーリング方法の検討

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jnzader-vault/project-starter-framework --skill modbus-protocol

インストール後、Claude Code で/modbus-protocolを入力して読み込まれたか確認します。

詳細

Modbus プロトコル

センサー通信向けの産業用 Modbus TCP/RTU 実装

使用する場面

  • PLC(Siemens、Allen-Bradley、Schneider)からのデータ読み取り
  • 産業用センサーとの通信
  • SCADA/ゲートウェイシステムの構築
  • OT(運用技術)との連携の実装

スタック

# Rust
tokio-modbus: "0.13+"

# Go
github.com/simonvetter/modbus: latest

# Python
pymodbus: "3.6+"

Modbus の基礎

レジスターの種類
種類アドレス範囲アクセス用途
コイル00001-09999R/Wデジタル出力(オン/オフ)
ディスクリート入力10001-19999Rデジタル入力(読み取り専用)
入力レジスター30001-39999Rアナログ入力(16 ビット)
保持レジスター40001-49999R/W設定とデータ
ファンクションコード
コード関数説明
01コイルの読み取り複数のデジタル出力を読み取ります
02ディスクリート入力の読み取りデジタル入力を読み取ります
03保持レジスターの読み取り設定レジスターを読み取ります
04入力レジスターの読み取りアナログ入力を読み取ります
05単一コイルの書き込み単一の出力を設定します
06単一レジスターの書き込み1 つのレジスターに書き込みます
16複数レジスターの書き込みレジスターのブロックに書き込みます

Rust の実装

クライアントのセットアップ
use tokio_modbus::prelude::*;
use tokio_modbus::client::tcp;
use std::net::SocketAddr;
use tokio::time::{timeout, Duration};

pub struct ModbusClient {
    ctx: client::Context,
    timeout_duration: Duration,
}

impl ModbusClient {
    pub async fn connect(addr: SocketAddr, slave_id: u8) -> Result<Self, ModbusError> {
        let ctx = tcp::connect_slave(addr, Slave(slave_id)).await?;
        Ok(Self { ctx, timeout_duration: Duration::from_secs(5) })
    }

    pub async fn read_holding_registers(&mut self, address: u16, count: u16) -> Result<Vec<u16>> {
        timeout(self.timeout_duration, self.ctx.read_holding_registers(address, count))
            .await
            .map_err(|_| ModbusError::Timeout)?
            .map_err(ModbusError::from)
    }

    pub async fn write_single_register(&mut self, address: u16, value: u16) -> Result<()> {
        timeout(self.timeout_duration, self.ctx.write_single_register(address, value))
            .await
            .map_err(|_| ModbusError::Timeout)?
            .map_err(ModbusError::from)
    }
}
データ型の変換
/// Two 16-bit registers to f32 (big-endian)
pub fn registers_to_f32(regs: &[u16]) -> f32 {
    let bytes = [
        (regs[0] >> 8) as u8, regs[0] as u8,
        (regs[1] >> 8) as u8, regs[1] as u8,
    ];
    f32::from_be_bytes(bytes)
}

/// Two 16-bit registers to f32 (little-endian, swapped)
pub fn registers_to_f32_le(regs: &[u16]) -> f32 {
    let bytes = [
        regs[1] as u8, (regs[1] >> 8) as u8,
        regs[0] as u8, (regs[0] >> 8) as u8,
    ];
    f32::from_le_bytes(bytes)
}

/// f32 to two 16-bit registers
pub fn f32_to_registers(value: f32) -> [u16; 2] {
    let bytes = value.to_be_bytes();
    [
        ((bytes[0] as u16) << 8) | (bytes[1] as u16),
        ((bytes[2] as u16) << 8) | (bytes[3] as u16),
    ]
}

/// Scale raw value to engineering units
pub fn scale_value(raw: u16, min_raw: u16, max_raw: u16, min_eng: f32, max_eng: f32) -> f32 {
    let range_raw = (max_raw - min_raw) as f32;
    let range_eng = max_eng - min_eng;
    let normalized = (raw - min_raw) as f32 / range_raw;
    min_eng + (normalized * range_eng)
}
センサーリーダー
#[derive(Debug, Clone)]
pub struct SensorConfig {
    pub id: String,
    pub name: String,
    pub address: u16,
    pub data_type: DataType,
    pub unit: String,
    pub scale: Option<Scale>,
}

#[derive(Debug, Clone)]
pub enum DataType {
    UInt16,
    Int16,
    Float32,
    Float32LE,
}

#[derive(Debug, Clone)]
pub struct SensorReading {
    pub sensor_id: String,
    pub value: f64,
    pub quality: u8,  // 192=Good, 128=Uncertain, 0=Bad
    pub timestamp: i64,
}

pub struct SensorReader {
    client: ModbusClient,
    sensors: Vec<SensorConfig>,
}

impl SensorReader {
    pub async fn read_sensor(&mut self, sensor: &SensorConfig) -> Result<SensorReading> {
        let count = match sensor.data_type {
            DataType::UInt16 | DataType::Int16 => 1,
            DataType::Float32 | DataType::Float32LE => 2,
        };

        let registers = self.client.read_holding_registers(sensor.address, count).await?;

        let raw_value: f64 = match sensor.data_type {
            DataType::UInt16 => registers[0] as f64,
            DataType::Int16 => (registers[0] as i16) as f64,
            DataType::Float32 => registers_to_f32(&registers) as f64,
            DataType::Float32LE => registers_to_f32_le(&registers) as f64,
        };

        let value = if let Some(scale) = &sensor.scale {
            scale_value(raw_value as u16, scale.min_raw, scale.max_raw, scale.min_eng, scale.max_eng) as f64
        } else {
            raw_value
        };

        Ok(SensorReading {
            sensor_id: sensor.id.clone(),
            value,
            quality: 192, // Good
            timestamp: chrono::Utc::now().timestamp_millis(),
        })
    }
}

Go の実装

package modbus

import (
    "encoding/binary"
    "math"
    "sync"
    "time"

    "github.com/simonvetter/modbus"
)

type Client struct {
    client *modbus.ModbusClient
    mu     sync.Mutex
}

func NewClient(address string, slaveID uint8) (*Client, error) {
    client, err := modbus.NewClient(&modbus.ClientConfiguration{
        URL:     fmt.Sprintf("tcp://%s", address),
        Speed:   19200,
        Timeout: 5 * time.Second,
    })
    if err != nil { return nil, err }

    if err := client.Open(); err != nil { return nil, err }
    client.SetUnitId(slaveID)

    return &Client{client: client}, nil
}

func (c *Client) ReadHoldingRegisters(address, count uint16) ([]uint16, error) {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.client.ReadRegisters(address, count, modbus.HOLDING_REGISTER)
}

func RegistersToFloat32(regs []uint16) float32 {
    bytes := make([]byte, 4)
    binary.BigEndian.PutUint16(bytes[0:2], regs[0])
    binary.BigEndian.PutUint16(bytes[2:4], regs[1])
    return math.Float32frombits(binary.BigEndian.Uint32(bytes))
}

Python の実装

from pymodbus.client import AsyncModbusTcpClient
from dataclasses import dataclass
import struct

@dataclass
class SensorConfig:
    id: str
    address: int
    data_type: str  # 'uint16', 'int16', 'float32'
    unit: str

class ModbusReader:
    def __init__(self, host: str, port: int = 502, slave_id: int = 1):
        self.host = host
        self.port = port
        self.slave_id = slave_id

    async def connect(self):
        self.client = AsyncModbusTcpClient(self.host, port=self.port)
        await self.client.connect()

    async def read_holding_registers(self, address: int, count: int):
        result = await self.client.read_holding_registers(address, count, slave=self.slave_id)
        return result.registers

    async def read_sensor(self, config: SensorConfig) -> float:
        count = 2 if 'float32' in config.data_type else 1
        regs = await self.read_holding_registers(config.address, count)

        if config.data_type == 'uint16':
            return regs[0]
        elif config.data_type == 'int16':
            return struct.unpack('>h', struct.pack('>H', regs[0]))[0]
        elif config.data_type == 'float32':
            bytes_data = struct.pack('>HH', regs[0], regs[1])
            return struct.unpack('>f', bytes_data)[0]

PLC アドレスマップ

Siemens S7-1200/1500
holding_registers:
  40001: DB1.DBD0   # Float, first double word in DB1
  40003: DB1.DBD4   # Float, second double word
  40005: MW100      # Word memory

coils:
  00001: Q0.0       # Output bit 0.0
  00002: Q0.1       # Output bit 0.1
Allen-Bradley ControlLogix
holding_registers:
  40001: N7:0       # Integer file
  40002: F8:0       # Float (uses 2 registers)
Schneider Modicon
holding_registers:
  40001: %MW0       # Memory word 0
  40003: %MF0       # Float (2 registers)

エラー処理

pub fn modbus_error_description(code: u8) -> &'static str {
    match code {
        0x01 => "Illegal Function",
        0x02 => "Illegal Data Address",
        0x03 => "Illegal Data Value",
        0x04 => "Slave Device Failure",
        0x05 => "Acknowledge",
        0x06 => "Slave Device Busy",
        _ => "Unknown Error",
    }
}

ベストプラクティス

バッチ読み取り
// ✅ Single read for contiguous registers
client.read_holding_registers(40001, 10).await?;

// ❌ Multiple individual reads - slow!
for addr in 40001..40011 {
    client.read_holding_registers(addr, 1).await?;
}
接続の再利用
// ✅ Reuse connection
let client = ModbusClient::connect(addr, slave_id).await?;
for _ in 0..100 {
    client.read_holding_registers(addr, count).await?;
}

// ❌ New connection per read
for _ in 0..100 {
    let client = ModbusClient::connect(addr, slave_id).await?;
    client.read_holding_registers(addr, count).await?;
}
ポーリング間隔
センサーの種類推奨間隔
振動10-50ms
温度/圧力500ms-1s
レベル/流量の合計5-10s

クイックリファレンス

タスクRust
保持レジスターの読み取りctx.read_holding_registers(addr, count).await
入力の読み取りctx.read_input_registers(addr, count).await
単一の書き込みctx.write_single_register(addr, value).await
複数の書き込みctx.write_multiple_registers(addr, &values).await
コイルの読み取りctx.read_coils(addr, count).await

品質コード(OPC UA)

コード意味
192 (0xC0)良好
128 (0x80)不確か
0 (0x00)不良

リソース

関連スキル

  • mqtt-rumqttc:MQTT へのデータ転送
  • tokio-async:非同期ポーリングのパターン
  • timescaledb:産業データの保存
  • rust-systems:Rust による総合的な連携

コメント