modbus-swift

1.0.2

Complete Modbus protocol implementation in pure Swift with SwiftNIO. Swift Modbus client: TCP, TLS, UDP, Serial RTU/ASCII. All 19 function codes.
3a4oT/modbus-swift

What's New

1.0.2

2025-12-15T14:07:22Z

Full Changelog: 1.0.1...1.0.2

modbus-swift

Production-ready Modbus implementation in pure Swift, built on SwiftNIO.

Swift 6.2+ Platforms SPM Compatible License

Features

  • Pure Swift — No C dependencies
  • Complete Modbus Protocol — All 19 function codes (CANopen excluded), TCP/TLS/UDP (SwiftNIO), Serial RTU/ASCII (POSIX)
  • Swift 6.2 — Typed throws, Span<UInt8> parsing, Mutex request serialization
  • Protocol Compliant — Validated against pymodbus reference server
  • Observability — swift-log, swift-metrics, ServiceLifecycle integration

Modules

Module Description
ModbusCore Zero-dependency PDU builders/parsers and CRC-16
ModbusKit SwiftNIO-based TCP, TLS, UDP clients
ModbusSerial POSIX termios-based Serial RTU and ASCII clients

Transports

Transport Client Port Use Case
TCP ModbusTCPClient 502 Standard industrial networks
TLS ModbusTLSClient 802 Secure connections (TLS 1.2+)
UDP ModbusUDPClient 502 Connectionless, broadcast
Serial RTU ModbusRTUClient — RS-485/RS-232 binary mode
Serial ASCII ModbusASCIIClient — RS-485/RS-232 ASCII mode

Installation

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/3a4oT/modbus-swift.git", from: "1.0.0")
]

Then add to your target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "ModbusKit", package: "modbus-swift"),
        // For Serial RTU/ASCII:
        .product(name: "ModbusSerial", package: "modbus-swift"),
        // For PDU builders/parsers only (zero dependencies):
        .product(name: "ModbusCore", package: "modbus-swift"),
    ]
)

Quick Start

Scoped Client (CLI / Scripts / Tests)

Auto-closes connection when scope exits. Best for one-off operations:

import ModbusKit

// TCP
let registers = try await withModbusTCPClient(host: "192.168.1.100") { client in
    try await client.readHoldingRegisters(address: 0, count: 10, unitId: 1).registers
}

// TLS
let registers = try await withModbusTLSClient(host: "secure.example.com") { client in
    try await client.readHoldingRegisters(address: 0, count: 10, unitId: 1).registers
}

// UDP
let registers = try await withModbusUDPClient(host: "192.168.1.100") { client in
    try await client.readHoldingRegisters(address: 0, count: 10, unitId: 1).registers
}

Long-Lived Client (Services / Daemons)

For persistent connections with logging, metrics, and ServiceLifecycle integration:

import Logging
import Metrics
import ModbusKit
import ServiceLifecycle

let logger = Logger(label: "modbus")
let metrics = ModbusMetrics()

let config = ModbusClientConfiguration(
    host: "192.168.1.100",
    port: 502,
    timeout: .seconds(5),
    retries: 3,
    reconnectionStrategy: .exponentialBackoff(
        initialDelay: .seconds(1),
        maxDelay: .seconds(30)
    )
)

let client = ModbusTCPClient(
    configuration: config,
    logger: logger,
    metrics: metrics
)

try await client.connect()
let response = try await client.readHoldingRegisters(address: 0, count: 10, unitId: 1)
print(response.registers)

// Graceful shutdown with ServiceLifecycle
let group = ServiceGroup(
    services: [client],
    gracefulShutdownSignals: [.sigterm, .sigint],
    logger: logger
)
try await group.run()

Serial RTU

import ModbusSerial

let client = ModbusRTUClient(
    port: "/dev/ttyUSB0",
    baudRate: .b9600,
    parity: .none
)
try await client.connect()
let response = try await client.readHoldingRegisters(address: 0, count: 10, unitId: 1)
await client.close()

Serial RTU with Error Recovery

For USB-to-serial adapters that may disconnect, enable automatic reconnection:

import ModbusSerial

let config = RTUClientConfiguration(
    serialConfiguration: SerialConfiguration(
        port: "/dev/ttyUSB0",
        baudRate: .b9600,
        parity: .none,
        stopBits: .one,
        dataBits: .eight,
        timeout: .seconds(1)
    ),
    retries: 3,
    errorRecovery: .exponentialBackoff(
        initialDelay: .milliseconds(100),
        maxDelay: .seconds(30)
    ),
    handleLocalEcho: true  // For RS-485 half-duplex adapters
)

let client = ModbusRTUClient(path: "/dev/ttyUSB0", configuration: config)
try await client.connect()
let response = try await client.readHoldingRegisters(address: 0, count: 10, unitId: 1)
await client.close()

Error Recovery Modes:

Mode Description
.disabled No auto-reconnect (default)
.link(delay:) Reconnect after fixed delay (libmodbus style)
.exponentialBackoff(initialDelay:maxDelay:) Reconnect with increasing delays

handleLocalEcho: Some RS-485 half-duplex adapters echo transmitted bytes back. Enable this to strip echoed request from response. Symptoms: CRC errors with response containing your request bytes.

Serial ASCII

ASCII mode uses hex-encoded frames with LRC checksum. Useful for devices that require human-readable communication or have noisy serial lines (better error detection per character).

import ModbusSerial

let client = ModbusASCIIClient(
    port: "/dev/ttyUSB0",
    baudRate: .b9600,
    parity: .even,      // ASCII default per spec
    dataBits: .seven    // ASCII default per spec
)
try await client.connect()
let response = try await client.readHoldingRegisters(address: 0, count: 10, unitId: 1)
await client.close()

Supported Function Codes

Code Function TCP TLS UDP RTU ASCII
0x01 Read Coils ✓ ✓ ✓ ✓ ✓
0x02 Read Discrete Inputs ✓ ✓ ✓ ✓ ✓
0x03 Read Holding Registers ✓ ✓ ✓ ✓ ✓
0x04 Read Input Registers ✓ ✓ ✓ ✓ ✓
0x05 Write Single Coil ✓ ✓ ✓ ✓ ✓
0x06 Write Single Register ✓ ✓ ✓ ✓ ✓
0x07 Read Exception Status — — — ✓ —
0x08 Diagnostics — — — ✓ —
0x0B Get Comm Event Counter — — — ✓ —
0x0C Get Comm Event Log — — — ✓ —
0x0F Write Multiple Coils ✓ ✓ ✓ ✓ ✓
0x10 Write Multiple Registers ✓ ✓ ✓ ✓ ✓
0x11 Report Server ID — — — ✓ —
0x14 Read File Record ✓ ✓ ✓ — —
0x15 Write File Record ✓ ✓ ✓ — —
0x16 Mask Write Register ✓ ✓ ✓ ✓ ✓
0x17 Read/Write Multiple Registers ✓ ✓ ✓ ✓ —
0x18 Read FIFO Queue ✓ ✓ ✓ ✓ —
0x2B/0x0E Device Identification ✓ ✓ ✓ ✓ —

Notes:

  • FC 0x07, 0x08, 0x0B, 0x0C, 0x11 are Serial Line only per Modbus specification
  • FC 0x2B/0x0D (CANopen General Reference) is not implemented — requires proprietary CiA 309-2 spec

Advanced: Transaction ID Pipelining

For 99% of use cases, use serial mode (default). Pipelining is an advanced feature for high-throughput scenarios with devices that explicitly support it.

TCP/TLS clients support Transaction ID pipelining per Modbus TCP spec Section 4.2:

import ModbusKit

let config = ModbusClientConfiguration(
    host: "192.168.1.100",
    pipelining: .enabled  // maxInFlight: 4
)
let client = ModbusTCPClient(configuration: config)
try await client.connect()

// Concurrent requests
async let r1 = client.readHoldingRegisters(address: 0, count: 10, unitId: 1)
async let r2 = client.readHoldingRegisters(address: 100, count: 10, unitId: 1)
let (result1, result2) = try await (r1, r2)

await client.close()

Caution: Many industrial devices only support 1 outstanding request per connection. Test thoroughly with your specific hardware before enabling in production.

Requirements

  • Swift 6.2+
  • macOS 26+, iOS 26+, or Linux (Ubuntu 24.04+)

Documentation

Testing

See Testing Guide for detailed instructions.

Development

Setup

# Install SwiftFormat
brew install swiftformat

# Install pre-commit hook (runs SwiftFormat on staged files)
./Scripts/install-hooks.sh

Code Style

This project uses SwiftFormat with configuration in .swiftformat.

# Format all files
swiftformat .

# Check without modifying
swiftformat . --lint

References

License

Apache License 2.0. See LICENSE for details.

Description

  • Swift Tools 6.2.0
View More Packages from this Author

Dependencies

Last updated: Sun Sep 27 2026 05:31:36 GMT-0900 (Hawaii-Aleutian Daylight Time)