Skip to content

Usage guide

Recommended entry points

Method Use it for
OpenAndConnectAsync Create and open the ordinary FIFO client.
ReadTypedAsync Read one typed value.
WriteTypedAsync Write one typed value.
ReadNamedAsync Read a non-atomic mixed aggregate by address strings.
PollAsync Read repeated non-atomic aggregates on a fixed interval.
ReadWordsSingleRequestAsync Read contiguous 16-bit words in one PLC request.
ReadDWordsSingleRequestAsync Read contiguous 32-bit values in one PLC request.
WriteWordsSingleRequestAsync Write contiguous 16-bit words in one PLC request.
WriteDWordsSingleRequestAsync Write contiguous 32-bit values in one PLC request.
ReadTimerCounterAsync Read timer or counter status, current value, and preset.
ReadTimerAsync Read a timer as status, current value, and preset.
ReadCounterAsync Read a counter as status, current value, and preset.
ReadCommentsAsync Decode a PLC device comment with an explicit UTF-8 or CP932 selection.
ReadCommentBytesAsync Read the exact undecoded PLC device-comment payload bytes.
ReadExpansionUnitBufferAsync Read expansion unit buffer memory.
WriteExpansionUnitBufferAsync Write expansion unit buffer memory.

Connection

using System;
using PlcComm.KvHostLink;

var options = new KvHostLinkConnectionOptions(
    Host: "192.168.250.100",
    Port: 8501,
    Transport: HostLinkTransportMode.Tcp,
    PlcProfile: "keyence:kv-8000",
    Timeout: TimeSpan.FromSeconds(3));

await using var client = await KvHostLinkClientFactory.OpenAndConnectAsync(options);
Console.WriteLine($"Connected: {client.IsOpen}");

Host, Port, Transport, and the canonical PLC profile are required. Only Timeout may be omitted; its default is 3 seconds. Explicit values must be from 1 through Int32.MaxValue milliseconds. Sub-millisecond, zero, negative, or larger timeouts are rejected. Normal Host Link command frames always end in CR. Connections are IPv4-only. IPv6 literals and bracketed IPv4 literals such as [192.168.250.100] are rejected before socket creation. Write IPv4 addresses without brackets; hostnames are resolved only to IPv4 and never fall back to IPv6.

The maintainer SendRawAsync API represents exactly one non-empty ASCII command body and accepts at most 65,506 bytes. An empty body is rejected before FIFO admission, connection-state checks, DNS, socket work, or send. The terminating CR makes the maximum complete request frame 65,507 bytes. Larger input is rejected at the same pre-transport boundary; command-specific limits that are smaller still apply.

SetTimeAsync requires an explicit DateTime whose year is 2000 through 2099. Years outside that PLC clock range are rejected before communication; the library never folds another century into a two-digit year.

Read responses are validated against the issued command. Direct-bit responses without a format accept only 0, 1, OFF, or ON. A formatted direct-bit single read returns one packed numeric token: .U, .S, and .H represent 16 bits, while .D and .L represent 32 bits. Signed .S and .L tokens may include an explicit leading +. A malformed response shape invalidates the session before another request.

The configured timeout is one absolute transaction deadline from the first send attempt through the complete response. FIFO queue waiting does not consume that deadline. A timed-out read throws HostLinkTimeoutError. If a write may already have reached the PLC, timeout, caller cancellation, close, transport failure, or an invalid response throws HostLinkOutcomeUnknownError; inspect its Reason and do not retry automatically. HostLinkClosedError, HostLinkNotConnectedError, and caller OperationCanceledException remain distinct when the outcome is known. CloseAsync and DisposeAsync promptly interrupt active I/O, reject queued work from the retired generation, and do not reconnect or retry it.

Performance notes

Choose TCP or UDP explicitly for every endpoint. TCP provides stream delivery; UDP avoids stream state but does not provide retransmission. The TCP transport disables Nagle buffering for small Host Link command frames.

One TCP request owns one non-empty response line. CR/LF-only separators are ignored, but an additional non-empty line received before the next send is a protocol error and retires the transport. A UDP open is a logical session: it resolves the IPv4 endpoint once and creates one connected socket. Fully valid exchanges reuse that socket. Timeout, cancellation, I/O, malformed response, protocol, an extra response, or a queued unowned datagram detected before send discards the socket but retains the resolved logical endpoint. The next request creates one replacement socket without DNS resolution and does not retry the failed request. Because Host Link UDP has no transaction ID, a duplicate datagram that arrives in the narrow interval after the pre-send check and before the new send cannot be distinguished from that request's response; choose TCP when strict response association is required.

Host Link TCP responses also contain no request identifier. The client rejects data already visible before a send and retires the connection whenever stale, additional, malformed, timed-out, cancelled, or failed input becomes observable. A non-conforming peer can still deliver an unrelated line after that pre-send check but before the current response, and the library cannot prove that the line belongs to another request. Opening and closing TCP for every healthy request would not add an identifier, but would repeat connection setup and teardown latency. The supported contract therefore reuses a healthy TCP stream, serializes requests, and retires it immediately when ambiguity is observable.

Reuse one connected client for repeated reads and writes. Prefer ReadWordsSingleRequestAsync, ReadDWordsSingleRequestAsync, or ReadNamedAsync over many individual ReadTypedAsync calls when its explicitly non-atomic aggregate semantics are acceptable.

Connection reuse and concurrent requests

Keep one KvHostLinkClient open for repeated reads, writes, and polling. Its built-in FIFO admits public operations in arrival order and permits only one active wire transaction. Cancelling a waiting operation sends nothing; its transaction timeout starts only when it becomes active, and the open transport generation remains usable. Recursive use of the same client from a callback is rejected with HostLinkReentrancyError. Separate client instances remain independent. Commands never open a closed logical session implicitly. TCP timeout, active-operation cancellation, EOF, protocol failure, or transport failure retires the connection; call OpenAsync explicitly before a new command when it is safe to do so. UDP exchange anomalies retain IsOpen and the resolved endpoint while discarding the affected socket; the next command creates a replacement socket. CloseAsync closes either transport completely. A failed command is never retried automatically.

Read a single value

using System;
using PlcComm.KvHostLink;

var options = new KvHostLinkConnectionOptions("192.168.250.100", 8501, HostLinkTransportMode.Tcp, "keyence:kv-8000");
await using var client = await KvHostLinkClientFactory.OpenAndConnectAsync(options);

ushort unsignedWord = (ushort)await client.ReadTypedAsync("DM0", "U");
short signedWord = (short)await client.ReadTypedAsync("DM1", "S");
uint unsignedDWord = (uint)await client.ReadTypedAsync("DM2", "D");
int signedDWord = (int)await client.ReadTypedAsync("DM4", "L");
float floatValue = (float)await client.ReadTypedAsync("DM6", "F");

Console.WriteLine($"{unsignedWord}, {signedWord}, {unsignedDWord}, {signedDWord}, {floatValue}");
Suffix Meaning Returned .NET type
U Unsigned 16-bit word ushort
S Signed 16-bit word short
D Unsigned 32-bit double word uint
L Signed 32-bit double word int
F IEEE 754 32-bit floating point float
H Hexadecimal 16-bit word text string
BIT Direct bit device bool

Write a single value

using System;
using PlcComm.KvHostLink;

var options = new KvHostLinkConnectionOptions("192.168.250.100", 8501, HostLinkTransportMode.Tcp, "keyence:kv-8000");
await using var client = await KvHostLinkClientFactory.OpenAndConnectAsync(options);

const string address = "DM100";
ushort original = (ushort)await client.ReadTypedAsync(address, "U");

try
{
    await client.WriteTypedAsync(address, "U", (ushort)42);
    ushort readback = (ushort)await client.ReadTypedAsync(address, "U");
    Console.WriteLine($"{address} readback = {readback}");
}
finally
{
    await client.WriteTypedAsync(address, "U", original);
}

This is a matched read/write/readback pattern. Keep it on a test address until you know the register is safe for your machine. Float32 (F) reads and writes are available only for the canonical ordinary one-word families DM, EM, FM, ZF, W, TM, CM, VM, D, E, and F, where the value uses two consecutive .U words. The native 32-bit Z family is not a two-word Float32 route. Direct-bit families and special-response families such as R, T, C, and AT are rejected before FIFO admission and transport in parsed, normalized, formatted, typed, named, and polling addresses. Float32 write input must also be finite and within the binary32 range; NaN, infinities, and finite values that would overflow to infinity are rejected before transport.

Semantic H reads return exactly four uppercase hexadecimal digits, such as 000F, through low-level, typed, named, monitor, and polling APIs. Raw response body APIs preserve the PLC bytes, and hexadecimal writes keep the minimal wire representation accepted by the PLC.

Monitor reads

Word monitoring accepts explicit numeric formats and one intentional bare form:

await client.RegisterMonitorWordsAsync(
[
    new KvMonitorWordTarget("R5000"),
    new KvMonitorWordTarget("DM120", ".U"),
]);

string[] values = await client.ReadMonitorWordsAsync();
Console.WriteLine($"R5000..R5015 packed = {values[0]}, DM120 = {values[1]}");

An omitted KvMonitorWordTarget.DataFormat is valid only for a direct-bit MWS target. It transmits the target exactly as written, such as MWS R5000, and the corresponding MWR field is one through five ASCII decimal digits for the unsigned packed 16-bit value beginning at that bit. Leading zeros are allowed, so R5000=OFF and R5001=ON may be represented as either 2 or 00002. Accepted numeric values span 0 through 65535. Empty or whitespace formats are invalid, and ordinary word devices still require .U, .S, .H, .D, or .L explicitly.

This packed-word rule belongs only to bare MWS/MWR targets. A bare scalar RD and MBS/MBR remain strict bit operations that accept only 0, 1, ON, or OFF. Monitor response strings retain the PLC's decimal representation; explicit .H fields alone are normalized to four uppercase hexadecimal digits. An invalid monitor response is a protocol error and retires the supplying transport. Closing and reopening a client clears monitor registration metadata, and a failed replacement MWS does not make the client reuse an older decoder plan.

Named aggregate read

using System;
using PlcComm.KvHostLink;

var options = new KvHostLinkConnectionOptions("192.168.250.100", 8501, HostLinkTransportMode.Tcp, "keyence:kv-8000");
await using var client = await KvHostLinkClientFactory.OpenAndConnectAsync(options);

string[] addresses = ["DM0:U", "DM1:S", "DM2:D", "DM4:F", "DM10.A", "DM0:COMMENT"];
var readResult = await client.ReadNamedAsync(addresses, HostLinkCommentEncoding.Utf8);

foreach (var (address, value) in readResult)
{
    Console.WriteLine($"{address} = {value}");
}

Use ReadNamedAsync when one application aggregate mixes unsigned words, signed words, double words, floats, PLC comment strings, and bit-in-word values.

ReadNamedAsync is an explicitly aggregate read. It validates and snapshots the complete plan before sending. Wire-compatible device families are handled in first-appearance order; addresses inside each family are sorted and contiguous spans are merged up to the protocol request limit. A comment, native-32-bit device, direct-bit word view, or other non-batchable entry keeps its native single request without disabling batching elsewhere. Result keys and values remain in declared input order. All sends and response decoding keep one FIFO turn, stop at the first failure, and expose no partial dictionary; pure dictionary materialization occurs after that turn. A DWord or Float value is never split across wire requests. The result is not a simultaneous PLC snapshot because internal requests may observe different scan times. For coherent data, use a single-request read or a PLC-side snapshot/handshake design.

Named keys must be semantically unique by device family, numeric address, dtype, bit index, and scalar count. Case, leading zeros, or an explicit default dtype do not make a second key distinct. Different dtype views of the same word, different bit indices, and overlapping multiword spans are valid. Result keys preserve the original input strings.

An aggregate containing :COMMENT must use the overload that supplies a HostLinkCommentEncoding. The overload without that parameter rejects the complete aggregate before sending anything. Aggregates without comments do not need an encoding selection and must use the overload without one; supplying an unused comment encoding is an argument error before transport.

Contiguous block reads

using System;
using PlcComm.KvHostLink;

var options = new KvHostLinkConnectionOptions("192.168.250.100", 8501, HostLinkTransportMode.Tcp, "keyence:kv-8000");
await using var client = await KvHostLinkClientFactory.OpenAndConnectAsync(options);

ushort[] words = await client.ReadWordsSingleRequestAsync("DM200", 8);
uint[] dwords = await client.ReadDWordsSingleRequestAsync("DM300", 4);

Console.WriteLine($"Words: {words.Length}, DWords: {dwords.Length}");

Both methods send exactly one PLC command. Word requests accept at most 1000 values and native .D Dword requests accept at most 500 values. The library does not split larger operations: application code must make each request, timing boundary, retry decision, and partial-write consequence explicit.

Bit values and explicit bit-in-word writes

using System;
using PlcComm.KvHostLink;

var options = new KvHostLinkConnectionOptions("192.168.250.100", 8501, HostLinkTransportMode.Tcp, "keyence:kv-8000");
await using var client = await KvHostLinkClientFactory.OpenAndConnectAsync(options);

var snapshot = await client.ReadNamedAsync(["DM50.A"]);

Console.WriteLine($"DM50.A = {snapshot["DM50.A"]}");

The .n notation uses hexadecimal bit indexes from 0 through F; .A means bit 10. Individual direct-bit writes accept only bool, including consecutive bit collections. Numeric 0/1 compatibility inputs are rejected before transport. Explicit low-level .U/.D operations on a direct-bit bank remain packed multi-bit representations; they are not individual bit-value inputs. Use WriteBitInWordAsync only when an explicit client-side read-modify-write is the intended policy:

await client.WriteBitInWordAsync("DM50", 10, true);

The value is a bool, the bit index is 0..15, and the target must be an ordinary 16-bit word device. The complete plan is rejected before FIFO admission if it is invalid. After activation, one absolute transaction deadline covers exactly one word read followed by one word write in the same client FIFO turn; queue wait is outside that deadline. The write is sent even when the bit already has the requested value. There is no fallback, resend, success readback, or implicit use by named writes.

This operation is not PLC-atomic. PLC logic or another connection can change the word between the read and write, and its change can be lost. Prefer PLC-side logic, a handshake, or exclusive ownership of the complete word when that risk is unacceptable. Cancellation before the write starts sends no write. Cancellation, timeout, close, transport failure, or malformed response after write transmission may have started is outcome-unknown: do not retry automatically; reopen and reconcile PLC state deliberately. A complete PLC error response is definitive and does not by itself retire a healthy connection.

Expansion-unit buffer memory uses its own explicit route-specific helper:

await client.WriteBitInExpansionUnitBufferAsync(
    unitNo: 1,
    address: 100,
    bitIndex: 3,
    value: true);

This helper fixes both requests to the same unit/address and one .U word, then sends exactly URD for one point followed by UWR for one point. The normal WriteBitInWordAsync device route and the expansion-unit route never fall back to one another. The same shared-deadline, cancellation, outcome-unknown, no-readback, and non-PLC-atomic rules apply.

Polling

using System;
using System.Threading;
using PlcComm.KvHostLink;

var options = new KvHostLinkConnectionOptions("192.168.250.100", 8501, HostLinkTransportMode.Tcp, "keyence:kv-8000");
await using var client = await KvHostLinkClientFactory.OpenAndConnectAsync(options);

string[] addresses = ["DM0:U", "DM1:S", "DM4:F"];
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var count = 0;

await foreach (var readResult in client.PollAsync(addresses, TimeSpan.FromSeconds(1), cts.Token))
{
    Console.WriteLine($"DM0:U={readResult["DM0:U"]}, DM1:S={readResult["DM1:S"]}, DM4:F={readResult["DM4:F"]}");
    if (++count >= 3)
    {
        break;
    }
}

PollAsync yields a non-atomic aggregate dictionary on each interval until cancellation or until your loop exits. It snapshots, validates, and compiles the fixed plan once when polling starts. Every cycle reuses that plan and one FIFO turn with the same input-order result, no-interleaving, and no-partial-result contract as ReadNamedAsync. The interval is a delay after a completed cycle and runs outside the FIFO turn, so other client operations may proceed during the delay. Cycles never overlap and missed time is not caught up. The interval must be greater than zero and no more than Int32.MaxValue milliseconds; invalid intervals are rejected before communication. If the address list contains :COMMENT, use the overload that adds an explicit HostLinkCommentEncoding after the interval. If it contains no comment, use the ordinary overload; an unused comment encoding is rejected before the first send.

Operational recipes

The samples include three read-only operational recipes for repeatable collection:

  • PlcComm.KvHostLink.PollingReconnectSample polls one PLC and demonstrates bounded reconnect backoff after transport loss.

  • PlcComm.KvHostLink.MultiPlcMonitorSample monitors multiple PLC endpoints at the same time. Each PLC has its own task, connection, and reconnect loop, so one offline PLC does not block healthy PLC reads.

  • PlcComm.KvHostLink.ConfigPollingSample runs periodic collection from a JSON config file and can append long-form CSV rows as timestamp,plc,tag,value.

Both samples use the same reconnect states as the polling reconnect sample: connected, lost, reconnecting, and recovered, with 1 second initial backoff, exponential delay, and a 30 second default maximum. YAML config is available only in the Python sample; the .NET sample uses JSON.

dotnet run --project samples/PlcComm.KvHostLink.MultiPlcMonitorSample -- --plc line-a=192.168.250.100,keyence:kv-8000,8501,tcp --plc line-b=192.168.250.101,keyence:kv-8000,8501,tcp --tag dm100=DM100:U
dotnet run --project samples/PlcComm.KvHostLink.ConfigPollingSample -- --config samples/PlcComm.KvHostLink.ConfigPollingSample/config_polling.example.json --dry-run

Timer/counter helpers

using System;
using PlcComm.KvHostLink;

var options = new KvHostLinkConnectionOptions("192.168.250.100", 8501, HostLinkTransportMode.Tcp, "keyence:kv-8000");
await using var client = await KvHostLinkClientFactory.OpenAndConnectAsync(options);

KvTimerCounterValue timer = await client.ReadTimerAsync("T0");
KvTimerCounterValue counter = await client.ReadCounterAsync("C0");
KvTimerCounterValue generic = await client.ReadTimerCounterAsync("T0");

Console.WriteLine($"T0 status={timer.Status}, current={timer.Current}, preset={timer.Preset}");
Console.WriteLine($"C0 status={counter.Status}, current={counter.Current}, preset={counter.Preset}");
Console.WriteLine($"Generic T0 preset={generic.Preset}");

ReadTimerCounterAsync returns Status, Current, and Preset. The response status must be exactly 0 or 1; any other numeric value is treated as an invalid response and retires the connection. ReadTimerAsync accepts timer devices, and ReadCounterAsync accepts counter devices.

The low-level formatted ReadAsync overload also returns timer/counter data as three strings. Its first string is the structural PLC status and remains exactly 0 or 1; .U, .S, .H, .D, or .L applies only to the current and preset strings. Therefore .H returns, for example, 0,000F,0010, not 0000,000F,0010. Earlier builds exposed the synthesized 0000 or 0001 in that low-level first slot. Code that consumed that erroneous representation must use 0 or 1 instead. Missing or extra tokens, invalid status, invalid current/preset values, and numeric overflow are protocol errors and retire the supplying transport.

Caution: Timer/Counter preset writes (WS/WSS) are only supported on KV-8000/7000-series PLCs. Other models return error E1.

Device comments

An RDC response does not carry an encoding identifier. Select the encoding explicitly when requesting text:

string utf8Label = await client.ReadCommentsAsync(
    "DM0",
    HostLinkCommentEncoding.Utf8);

string cp932Label = await client.ReadCommentsAsync(
    "DM1",
    HostLinkCommentEncoding.Cp932);

byte[] exactPayload = await client.ReadCommentBytesAsync("DM2");

Utf8 means strict UTF-8. Cp932 means strict Windows code page 932 / Windows-31J and is the compatibility selection for KEYENCE material that calls the encoding "Shift_JIS". CP932 accepts its mapped Windows extension pairs but rejects forbidden singleton bytes, incomplete sequences, and unassigned pairs. The library does not guess, fall back, select from the PLC profile, or replace malformed bytes. Invalid text fails with HostLinkProtocolError and retires the connection.

The text API removes only trailing ASCII 0x20 padding before decoding; tabs, full-width spaces, and embedded spaces are preserved. The raw API returns the exact response body without the Host Link CR/LF frame terminator, including any trailing ASCII padding.

Expansion unit buffer

using System;
using PlcComm.KvHostLink;

var options = new KvHostLinkConnectionOptions("192.168.250.100", 8501, HostLinkTransportMode.Tcp, "keyence:kv-8000");
await using var client = await KvHostLinkClientFactory.OpenAndConnectAsync(options);

string[] bufferWords = await client.ReadExpansionUnitBufferAsync(
    unitNo: 0,
    address: 0,
    count: 4,
    dataFormat: ".U");

Console.WriteLine($"Read {bufferWords.Length} expansion buffer values.");

Expansion unit buffer methods access module buffer memory by unit number, buffer address, count, and data format. The data format is mandatory and must be .U, .S, .D, .L, or .H. The general example is intentionally read-only. Use WriteExpansionUnitBufferAsync only with a module buffer prepared for controlled testing. Save the original values first and restore them afterward. After an outcome-unknown failure, reopen and reconcile the actual PLC state before deciding whether restoration or any retry is safe.

Low-level numeric addresses

Low-level numeric methods require a base device and a separate data format:

string[] values = await client.ReadConsecutiveAsync("DM100", 4, ".U");

This example is also intentionally read-only. Use WriteAsync only with a controlled test address and the same save, restore, and outcome-reconciliation rules described above.

Do not pass DM100.U or another suffix inside the device argument. Suffix input is rejected even when it matches the separate format. Direct bit devices are the only format-free low-level access because the device family fixes the bit unit. In high-level named syntax, DM100.D means bit 13 while DM100:D means an unsigned Dword.

Address reference table

Form Example Meaning
:U DM100:U Unsigned 16-bit view.
:S DM100:S Signed 16-bit view.
:D DM100:D Unsigned 32-bit view.
:L DM100:L Signed 32-bit view.
:F DM100:F IEEE 754 32-bit float view.
:BIT R200:BIT Direct bit device view.
:COMMENT DM100:COMMENT PLC device comment text; the aggregate call must select UTF-8 or CP932 explicitly.
.n DM100.A One bit inside a word; n is hexadecimal 0 to F.

For ReadNamedAsync and PollAsync, include the intended type. Use DM100:U instead of plain DM100 for an unsigned word. When any item is :COMMENT, use the overload with an explicit HostLinkCommentEncoding.

Runnable samples

The samples/ directory contains ready-to-run projects for the most common high-level workflows.

Project What it demonstrates
samples\PlcComm.KvHostLink.HighLevelSample\PlcComm.KvHostLink.HighLevelSample.csproj Full high-level API with read-only defaults and opt-in write/restore demonstrations.
samples\PlcComm.KvHostLink.BasicReadWriteSample\PlcComm.KvHostLink.BasicReadWriteSample.csproj Typed and block reads plus an opt-in random write/readback/restore demonstration.
samples\PlcComm.KvHostLink.NamedPollingSample\PlcComm.KvHostLink.NamedPollingSample.csproj Named polling plus opt-in bit-in-word write/restore.

All six runnable samples are read-only by default. The three write demonstrations require --allow-writes and must be used only with controlled test addresses; see samples/README.md for the exact commands.

Traffic statistics

Read client.TrafficStats for cumulative RequestCount, TxBytes, and RxBytes. For TCP, a received line counts its body plus the first CR/LF terminator; extra CR/LF separators are consumed but not counted. For UDP, the complete response datagram is counted.