Skip to content

Gotchas

Symptom: D0000 is rejected

Root cause Fix
Basic area families require a program prefix. D, M, S, N, R, P, K, V, T, C, L, X, and Y must be written as P1-*, P2-*, or P3-*. Use P1-D0000, P2-D0000, or P3-D0000 for the intended program area.
from toyopuc import ToyopucDeviceClient


def main() -> None:
    with ToyopucDeviceClient(
        "192.168.250.100", 1025, transport="tcp", plc_profile="toyopuc:plus:extended"
    ) as client:
        print(client.read_one("P1-D0000"))


if __name__ == "__main__":
    main()

Symptom: multi-address access hides splitting

Root cause Fix
A contiguous or multi-device operation can require multiple protocol requests, incompatible protocol groups, or a PC10 block boundary crossing. Read aggregates validate the complete plan and split only when necessary while holding one FIFO turn. The result is ordered and all-or-error, but not an atomic PLC snapshot. Writes still reject every multi-request plan before communication.

Symptom: a write value is rejected instead of truncated

This is intentional. Semantic bit writes accept only actual bool values; byte writes accept integers in 0..255; word writes accept integers in 0..65535; and dword writes accept integers in 0..4294967295. Boolean values are not word or dword integers. Fractional values and numeric strings are never converted. Raw frame and payload builders are the wire layer and therefore use validated integer 0/1 bit fields rather than semantic Boolean values. All direct, relay, single, aggregate, and explicit bit-in-word semantic writes perform this validation before entering the sync FIFO or async lock. An invalid value therefore does not wait behind active communication and sends no request.

Symptom: a bit-in-word update races with another writer

write_bit_in_word and relay_write_bit_in_word always issue one word read followed by one word write under one local FIFO turn and one absolute deadline. They are not PLC-atomic, so PLC logic or another connection can update the word between requests. Use PLC-side coordination when the whole word is shared. A cancellation or failure after the write may have started is outcome-unknown; retire and reconnect the transport and reconcile PLC state before retrying.

Symptom: an IPv6 PLC endpoint is rejected

Computerlink connections are IPv4-only. TCP and UDP accept an IPv4 literal or a hostname that resolves to IPv4. An IPv6 literal, including an IPv4-mapped IPv6 literal such as ::ffff:192.0.2.1, raises ValueError before a socket is created. For a hostname with multiple results, the library uses the first IPv4 result in resolver order; a hostname with no IPv4 result fails without falling back to IPv6.

Symptom: not every resolved or retried connection phase gets a full timeout

This is intentional. One absolute connection deadline covers IPv4 DNS, first-IPv4 selection, socket creation, UDP bind/connect, TCP configuration, and client adoption. Pre-send retries use only the time remaining to that same deadline. An IPv4 literal bypasses DNS. If an operating-system resolver or socket call finishes after timeout or async cancellation, its result is not adopted and any late socket is closed.

Symptom: a fixed-port UDP client cannot reconnect after a timeout

Connection timeouts, retry delays, and polling intervals have a common inclusive maximum of 2,147,483.647 seconds (2,147,483,647 milliseconds, about 24.86 days). Timeouts and polling intervals must be greater than zero; retry delay may be zero. Invalid values raise ValueError before communication or waiting starts.

A connected UDP socket accepts datagrams only from its configured PLC endpoint. However, Computerlink has no request serial that can distinguish a late response from a later request to the same endpoint. After a request may have been sent and a fixed-local-port UDP session times out or fails, that client instance is terminal. Create a new client only after the network can no longer contain the late response; prefer local_port=0 unless a fixed source port is required.

When a state-changing request may already have reached the PLC, Python raises ToyopucOperationOutcomeUnknownError. Reconcile PLC state before retrying.

Malformed command-specific data is handled inside the same post-send lifecycle. Reads raise ToyopucProtocolError; state-changing calls raise ToyopucOperationOutcomeUnknownError with malformed-response reason and the protocol error as their cause. The affected transport is retired, and a fixed-local-port UDP client is tainted. Validation failures found before send do not retire the transport.

Cancellation does not prove that a write was unapplied. Async clients cancel their native socket wait and retire that transport; when a state-changing request may already have been sent, they raise ToyopucOperationOutcomeUnknownError instead of implying non-application.

Symptom: a multi-address read_named result changes between entries

Root cause Fix
read_named preserves declaration order and holds one client FIFO turn, but protocol limits can require multiple PLC reads. The PLC may update data between those requests. Use a PLC-side consistency marker when a cross-request atomic snapshot is required. Use a single-request helper only when one wire request is itself the requirement.
import asyncio

from toyopuc import ToyopucConnectionOptions, open_and_connect, read_named


async def main() -> None:
    options = ToyopucConnectionOptions(
        host="192.168.250.100",
        port=1025,
        transport="tcp",
        plc_profile="toyopuc:plus:extended",
    )
    async with await open_and_connect(options) as client:
        snapshot = await read_named(client, ["P1-D0000:U", "P1-D0001:U"])
        print(snapshot["P1-D0000:U"], snapshot["P1-D0001:U"])


asyncio.run(main())

Symptom: a request was not retried after a disconnect

Only a connection failure proven to happen before any send attempt is eligible for automatic retry. Once a read or write may have been sent, the client retires the transport and does not resend it. This avoids applying a response from a different request or repeating a state change whose outcome is unknown.

Symptom: P1-D0100.D reads a bit instead of a dword

Root cause Fix
A dot means bit-in-word access. .D is hexadecimal bit 13. Use a colon for typed views: P1-D0100:D.
import asyncio

from toyopuc import ToyopucConnectionOptions, open_and_connect, read_typed


async def main() -> None:
    options = ToyopucConnectionOptions(
        host="192.168.250.100",
        port=1025,
        transport="tcp",
        plc_profile="toyopuc:plus:extended",
    )
    async with await open_and_connect(options) as client:
        print(await read_typed(client, "P1-D0100", "D"))


asyncio.run(main())

Symptom: FR values revert after power cycle

Root cause Fix
write_fr(...) updates only the FR work area. It never commits flash. Call commit_fr() separately with the first word of exactly one block only when persistence is intended.
An FR word is an unsigned 16-bit value. Pass an integer in 0..65535; Boolean, fractional, string, negative, and overflowing values are rejected before communication.

FR writes are intentionally available only through write_fr / relay_write_fr. Generic, aggregate, typed dword/float, and bit-in-word write APIs reject an FR address before opening or using the transport. Migrate an intentional FR write to the explicit FR work-area API so its separate commit lifecycle remains visible.

from toyopuc import ToyopucDeviceClient


def main() -> None:
    with ToyopucDeviceClient(
        "192.168.250.100", 1025, transport="tcp", plc_profile="toyopuc:pc10g:pc10"
    ) as client:
        # Use only a test FR address. commit_fr persists the staged value
        # to flash and does not restore the previous value automatically.
        client.write_fr("FR000000", 0x1234)
        client.commit_fr("FR000000")


if __name__ == "__main__":
    main()

Symptom: non-canonical profile string fails immediately

Root cause Fix
The library accepts only exact canonical profile strings from source. Aliases and blank values are rejected. Copy the exact string from profiles.
from toyopuc import ToyopucPlcProfiles


def main() -> None:
    profile = ToyopucPlcProfiles.from_name("toyopuc:plus:extended")
    print(profile.name)


if __name__ == "__main__":
    main()

Symptom: relay access does not find the route automatically

Root cause Fix
Relay hops are not probed automatically because automatic routing can hide configuration mistakes. Pass the exact relay hop string to the relay helper you call.

Relay text is decimal-only. Use P10-L11:N20 for component notation or 171:32 for a direct link/station pair. Hexadecimal prefixes, hexadecimal suffixes, and A-F digits are rejected; format_relay_hop() also returns decimal text.

from toyopuc import ToyopucDeviceClient


def main() -> None:
    with ToyopucDeviceClient(
        "192.168.250.100", 1025, transport="tcp", plc_profile="toyopuc:nano-10gx:compatible"
    ) as client:
        hops = "P1-L2:N2"
        print(client.relay_read_words(hops, "P1-D0000", count=1))


if __name__ == "__main__":
    main()

Symptom: P1-M0010W is mistaken for a type suffix

Root cause Fix
W, H, and L after a bit-area address are packed-word or byte notation, not :D or :F type suffixes. Use P1-M0010W for a packed 16-bit view, and use P1-D0100:D for typed dword reads.
from toyopuc import ToyopucDeviceClient


def main() -> None:
    with ToyopucDeviceClient(
        "192.168.250.100", 1025, transport="tcp", plc_profile="toyopuc:plus:extended"
    ) as client:
        packed = client.read_one("P1-M0010W")
        dword = client.read_dword("P1-D0100")
        print(packed, dword)


if __name__ == "__main__":
    main()