Changelog¶
Changelog¶
All notable changes to anymodbus are documented here.
The format follows Keep a Changelog, and the project adheres to Semantic Versioning.
Unreleased¶
0.3.0 - 2026-09-28¶
Hardening driven by the device libraries built on anymodbus (fujilib,
servomexlib, watlowlib): each item below replaces a workaround those
libraries had to carry. The public API is backwards-compatible; the behaviour
changes are listed under Changed and Fixed.
Added¶
- Late-reply window.
TimingConfig.late_reply_window(default 0, off). After an attempt whose outcome is uncertain — a timeout, a cancellation, a checksum or framing error, or a reply that does not answer the request — the bus sends nothing until the window has passed, reading and discarding whatever arrives, and waits for the line to be quiet. Without it, a reply that arrives after its request timed out or was cancelled can be taken as the answer to the next request.estimate_late_reply_window(baudrate=..., max_turnaround=..., max_reply_bytes=256, latency=0.0)sizes it. - Transaction observers.
Bus.add_transaction_observer(callback)(andBus(on_transaction=...), and the syncBus.add_transaction_observer) reports every attempt as aTransactionInfo: slave address, function code,request_id,attempt/max_attempts,will_retry, aTransactionOutcome, the error,started_at/sent_at/ended_aton the AnyIO clock andsent_at_ns/ended_at_nsontime.monotonic_ns(), and the late bytes discarded before it.sent_atis taken after the inter-frame gap, the send and the drain, so readings can be timestamped with when the request actually went out. Retries and recovered errors can be counted from the reports without turning off the bus's own retries. TransportError(ConnectionLostError,OSError): an OS-level failure of the stream other than a disconnect or a close.- Server-side building blocks, for test slaves and simulators:
- Request decoders in
anymodbus.pdu(decode_read_holding_registers_request,decode_write_multiple_registers_request, ... one per implemented FC) returning frozen dataclasses (ReadRequest,WriteSingleCoilRequest,WriteSingleRegisterRequest,WriteMultipleCoilsRequest,WriteMultipleRegistersRequest), and response encoders (encode_read_holding_registers_response, ...,encode_exception_response). read_request_adu(stream, *, inter_char_idle)on both framers (and theFramerprotocol): reads one request frame for any address, framed by request length, with the checksum verified.anymodbus.testing.MockServer: severalMockSlaves on one line, routed by address.client_server_pair(*slaves, ...)wires one to a bus.MockSlave.handle(request_pdu)as the public extension point, the publicServerException(code)to answer with an exception response,MockSlave.send_response(stream, pdu), andMockSlave.response_count.QuantityLimits: per-request quantity caps on aMockSlave(andclient_slave_pair(limits=...)), e.g. a device limited to 64 registers.anymodbus.stream.SupportsDrain/SupportsResetInputBuffer: the optional stream methods the bus uses, as runtime-checkable protocols.
Changed¶
- Replies are checked against the request. A register read whose reply
carries a different number of registers, a coil / discrete-input read whose
byte count does not fit the requested count, and a write (FC 0x05, 0x06,
0x0F, 0x10) whose echoed address, value or quantity differs from the
request now raise
UnexpectedResponseError. Before, a well-formed reply of the wrong length was returned as a shorter or longer tuple, and write echoes were not compared.decode_read_holding_registers_response/decode_read_input_registers_responseaccept a keyword-onlyexpected_countfor the same check. - The check runs inside each attempt, so a reply that fails it is retried, reported to observers, and opens a late-reply window like any other bad reply.
- Default
RetryPolicy.retry_onis now{ProtocolError, FrameTimeoutError}(was{ChecksumError, FrameTimeoutError}). Reads are now also retried on a malformed frame (FrameError) and on a reply that does not answer the request (UnexpectedResponseError); writes are still not retried by default (retry_idempotent_only=True). As in 0.2.0, only set membership changed for subclasses:ChecksumError in cfg.retries.retry_onis nowFalse, though checksum errors are still retried. - Bad arguments raise
ConfigurationErrorinstead of a bareValueErrorthroughout the PDU codec, both ADU encoders, the register decoders and encoders, the broadcast path andMockSlave.ConfigurationErroris aValueError, so existingexcept ValueErrorhandlers still catch it. - The bus uses any stream's
drain()andreset_input_buffer(), not only ananyserial.SerialPort's, when they are async methods. A wrapper around a serial port keeps drain-after-send and the input reset by forwarding them. MockSlaveanswers a request with a function code it does not implement with exception code 0x01 (ILLEGAL FUNCTION), as a device should, instead of stopping its serve loop.MockSlave.serveruns throughMockServer.
Fixed¶
- Port failures are
ModbusErrors. AnOSErrorfrom the stream that is not a disconnect or a close (for example ananyserial.SerialErrorfor an unmapped errno or Windows error code, or a failingdrain) now raisesTransportError, which is still anOSError, instead of escaping raw. The input reset before a request now runs inside the transaction's error handling, so its failures are translated too, and the next request still gets its inter-frame gap. - A reply whose function code the client never sends is line damage, not
"unsupported". A reply carrying FC 0x07, 0x0B, 0x0C, 0x11, 0x14, 0x15,
0x18 or 0x2B raised
ModbusUnsupportedFunctionError, which is not retried and reads as "the device lacks this function" (on a write, as "the write was refused"). It is now read to the idle gap and checked: a damaged frame (06 → 07, 03 → 0B, ... are one bit apart) raisesCRCError, and a checksum-valid one raisesUnexpectedResponseError; reads retry either.ModbusUnsupportedFunctionErrorremains for the send side. - Documentation:
FaultPlan.corrupt_crc_after_n/drop_response_after_nfire once, at the response with that 0-based index;client_slave_pairuses real serial ports, so the baud rate, drain and input reset all apply.
0.2.1 - 2026-09-28¶
Fixed¶
- Inter-frame idle gap after a failed transaction.
inter_frame_idleis now measured from the end of the previous transaction however it ended. Before, it was measured from the end only after a normal reply. After an exception response, a CRC/LRC or framing error, an unexpected response, a timeout or a cancellation, it was measured from when the previous request was sent. A reply slower than the gap (as most real devices' are) had already used it up, so the next request went out straight after the error. A cancelled broadcast now also leaves the gap in place. The first request after an error may now wait up tointer_frame_idlelonger than before. Found byfujilib.
0.2.0 - 2026-05-30¶
Adds Modbus-ASCII framing, FC08 diagnostic loopback, FC04 input-register typed
reads, and RS485 cold-start ergonomics — the capabilities requested by
servomexlib for its [modbus-ascii] / AUTO milestone. The public API is
additive and backwards-compatible; the internal framing layer was restructured
behind a narrow Framer strategy seam so RTU / ASCII / (future) TCP share one
response interpreter.
Added¶
- Modbus-ASCII framing.
Bus(stream, framing=Framing.ASCII)selects the:…hex…LRC…CRLF wire format, sharing the existing PDU/register codec unchanged. NewFramingenum (RTU/ASCII) and a thinopen_modbus_ascii(path, *, baudrate, parity, data_bits=8)convenience opener (data_bits=7for the classic 7E1 wire). Sync mirrors included. - Pure LRC primitives at
anymodbus.lrc:lrc8,lrc8_bytes,verify_lrc— mirroringanymodbus.crc(submodule-level, not top-level). - FC08 diagnostic loopback.
Slave.diagnostic_loopback(data=b"\x00\x00")(FC 0x08 sub-function 0x0000, Return Query Data) — a side-effect-free liveness / round-trip probe. PDU codec inanymodbus.pdu; sync mirror. - FC04 input registers in the typed reads.
Slave.read_float/read_int32/read_string(and sync mirrors) acceptsource=RegisterSource.INPUT(FC04) — defaults toHOLDING(FC03). TimingConfig.startup_settle— a one-shot idle wait applied once before the first transaction, to absorb USB-RS485 adapter warm-up.ChecksumErrorbase exception (parent ofCRCErrorand the newLRCError).MockSlaveparity: answers FC08 sub-0 (echo) and can speak ASCII (MockSlave(framing=...),client_slave_pair(framing=...)), so one register bank backs both an RTU and an ASCII bus in hardware-free CI.
Changed¶
- Default
RetryPolicy.retry_onwidened from{CRCError, FrameTimeoutError}to{ChecksumError, FrameTimeoutError}, so ASCIILRCErrorretries by default. RTU behaviour is unchanged (CRCError ⊂ ChecksumError; retry usesisinstance). Note: a direct membership checkCRCError in cfg.retries.retry_onis nowFalseby default even thoughCRCErroris still retried — only the set membership changed, not the behaviour. - FC 0x08 reclassified from "known-unsupported" (raised
ModbusUnsupportedFunctionError) to supported (sub-0 only).CRCErrorreparented fromProtocolErrortoChecksumError(still aProtocolErrortransitively —except CRCError/except ProtocolErrorunaffected). - Internal: the RTU framer was split into
RtuFramer.read_adu(reads one raw frame) plus a sharedinterpret_response_pdu; the module-levelread_response_aduis preserved as a compatibility wrapper.
No breaking changes to documented public API.
0.1.1 - 2026-04-26¶
First post-release polish, driven by the REVIEW.md notes against v0.1.0 ahead of the first downstream consumers (watlowlib + planned alicatlib / sartoriuslib). The package is still alpha; backwards-compatibility shims were not kept.
Added¶
RegisterTypeenum (int16/uint16/int32/uint32/int64/uint64/float32/float64/string) and a type-dispatcheddecode(words, *, type=RegisterType.X, ...)/encode(value, *, type=RegisterType.X, ...)pair inanymodbus.decoders. Recommended entry point for downstream device libraries that load a register schema from configuration.register_count_for(type)helper for schema callers.- New per-type helpers:
decode_int16/encode_int16,decode_int64/encode_int64,decode_float64/encode_float64. decode_string/encode_stringgained abyte_orderkeyword for devices that store strings byte-swapped within each register.encode_stringnow accepts eitherregister_countorbyte_count— the latter is convenient for spec'd-in-bytes fields whose length isn't a multiple of two.Slave.read_string/Slave.write_string(and their sync mirrors) accept eitherregister_countorbyte_count, plusbyte_order.FCshort alias forFunctionCodeat the top-level package surface, fulfilling the long-standing docstring promise.
Changed¶
TimingConfig.inter_char_timeoutrenamed toTimingConfig.inter_char_idleto match the spec term used throughout DESIGN.md. The framer's keyword argument is renamed to match.ModbusUnknownExceptionErroris now a subclass ofModbusExceptionResponse, so callers wanting "any slave-returned exception" canexcept ModbusExceptionResponseand be done. The raw byte is exposed onexception_code(was:code).code_to_exceptionnow returnsModbusExceptionResponseinstead ofModbusError.
Removed¶
BusBusyErrorfrom DESIGN.md — it was documented but never implemented. The bus lock serializes transactions; the right resource-busy signal already comes fromanyio.BusyResourceErroron the underlying stream.
0.1.0 - 2026-04-25¶
Initial release. See DESIGN.md for the full plan.
Added¶
- Repository skeleton:
pyproject.toml,Makefile, CI/docs/publish workflows, pre-commit config, src/tests/docs layout. - Foundational pure-Python pieces:
anymodbus._types(enums + small dataclasses),anymodbus.exceptions(fullModbusErrortree +code_to_exceptiontranslator),anymodbus.crc(CRC-16/Modbus, 256-entry table),anymodbus.config(frozen dataclasses with validation). WordOrderandByteOrdernamed enums for 32-bit value layout, with defaults matching the Modbus Application Protocol spec example (high-word-first, big-endian within word).Bus(half-duplex single-master client) and per-slaveSlavehandle, withopen_modbus_rtu(...)convenience opener overanyserial.- Length-aware RTU
Framerwith timing-gap fallback for unknown function codes and tx-side 3.5-character pre-tx idle gap. - PDU encode/decode for the v0.1 function code set (FC 1–6, 15, 16) and
decodersfor 32-bit float / int values across two registers. Capabilitymodel (SUPPORTED/UNSUPPORTED/UNKNOWN) populated from probe results.- Sync wrapper (
anymodbus.sync) using the same shared-portal pattern asanyserial.sync. anymodbus.testingwithMockSlave,FaultPlan, andclient_slave_pair()for hardware-free integration tests.