Documentation
¶
Overview ¶
Package ads is a pure-Go client for the Beckhoff TwinCAT ADS protocol.
Two layers, chosen at construction. Client (this file) is a thin RPC layer: one TCP connection, raw AMS framing, no cache or reconnect -- once it drops, build a new one. Session (session.go) wraps it with the symbol cache, persistent notifications, auto-reconnect and an FSM.
Session does NOT embed *Client: raw methods live on *Client, cache-aware ones on *Session. See docs/archive/specs/09-fsm-design.md.
Index ¶
- Constants
- Variables
- func AddRemoteRoute(remoteHost string, localNetID [6]byte, routeName string, computerName string, ...) error
- func AddRemoteRouteWithLogger(logger *slog.Logger, remoteHost string, localNetID [6]byte, routeName string, ...) error
- func EncodeAMSHeader(h AMSHeader) []byte
- func ParseNetID(source string) (result [6]byte, err error)
- func ReadBit(data []byte, bitIndex int) bool
- func SetDefaultLogger(logger *slog.Logger)
- func WriteBit(data []byte, bitIndex int, value bool)
- type ADSDataType
- type ADSState
- type AMSAddress
- type AMSEndpoint
- type AMSError
- type AMSHeader
- type BackoffConfig
- type BatchError
- type BatchItemError
- type Client
- func (c *Client) AcceptPeerConn(conn net.Conn)
- func (c *Client) AddDeviceNotification(ctx context.Context, group uint32, offset uint32, length uint32, ...) (handle uint32, err error)
- func (c *Client) ClearProcessInputs(ctx context.Context) error
- func (c *Client) ClearProcessOutputs(ctx context.Context) error
- func (c *Client) Close() error
- func (c *Client) DeleteDeviceNotification(ctx context.Context, handle uint32) error
- func (c *Client) DownloadDataTypes(ctx context.Context, length uint32) ([]byte, error)
- func (c *Client) DownloadInChunks(ctx context.Context, group uint32, totalLength uint32, chunkSize uint32, ...) ([]byte, error)
- func (c *Client) DownloadSymbolList(ctx context.Context, length uint32) ([]byte, error)
- func (c *Client) GetHandleByName(ctx context.Context, symbolName string) (uint32, error)
- func (c *Client) GetSymbolInfoByName(ctx context.Context, symbolName string) (*symbol, error)
- func (c *Client) GetSymbolUploadInfo(ctx context.Context) (SymbolUploadInfo, error)
- func (c *Client) GetSymbolVersion(ctx context.Context) (uint8, error)
- func (c *Client) Read(ctx context.Context, group uint32, offset uint32, length uint32) (data []byte, err error)
- func (c *Client) ReadDeviceInfo(ctx context.Context) (response DeviceInfo, err error)
- func (c *Client) ReadProcessInput(ctx context.Context, byteOffset, length uint32) ([]byte, error)
- func (c *Client) ReadProcessInputBit(ctx context.Context, byteOffset uint32, bitIndex uint8) (bool, error)
- func (c *Client) ReadProcessInputSize(ctx context.Context) (uint32, error)
- func (c *Client) ReadProcessOutput(ctx context.Context, byteOffset, length uint32) ([]byte, error)
- func (c *Client) ReadState(ctx context.Context) (response States, err error)
- func (c *Client) ReadStateOnPort(ctx context.Context, port Port) (States, error)
- func (c *Client) ReleaseHandle(ctx context.Context, handle uint32) error
- func (c *Client) SetNotificationHandler(fn NotificationHandler)
- func (c *Client) SetOnDrop(fn func())
- func (c *Client) SumAddDeviceNotification(ctx context.Context, requests []SumNotificationRequest) ([]SumNotificationResult, error)
- func (c *Client) SumDeleteDeviceNotification(ctx context.Context, handles []uint32) ([]ReturnCode, error)
- func (c *Client) SumRead(ctx context.Context, requests []SumReadRequest) ([]SumReadResult, error)
- func (c *Client) SumWrite(ctx context.Context, requests []SumWriteRequest) ([]SumWriteResult, error)
- func (c *Client) Write(ctx context.Context, group uint32, offset uint32, data []byte) error
- func (c *Client) WriteProcessOutput(ctx context.Context, byteOffset uint32, data []byte) error
- func (c *Client) WriteProcessOutputBit(ctx context.Context, byteOffset uint32, bitIndex uint8, value bool) error
- func (c *Client) WriteRead(ctx context.Context, group uint32, offset uint32, readLength uint32, ...) (data []byte, err error)
- type ClientOption
- type CommandID
- type DeviceInfo
- type Group
- type HeartbeatRecovery
- type NotificationConfig
- type NotificationHandler
- type NotificationSample
- type NotificationStream
- type Offset
- type Port
- type Reason
- type RemoteIdentity
- type ReturnCode
- type Session
- func (sess *Session) AddRoute(ctx context.Context, routeName, username, password string) error
- func (sess *Session) AddSymbolNotification(ctx context.Context, symbolName string, maxDelay time.Duration, ...) (uint32, error)
- func (sess *Session) AddSymbolNotifications(ctx context.Context, configs []NotificationConfig, ch chan *Update) ([]SumNotificationResult, error)
- func (sess *Session) BrowseSymbols(path string) ([]SymbolBrowseEntry, error)
- func (sess *Session) CheckSymbolVersion(ctx context.Context) (changed bool, err error)
- func (sess *Session) Close() error
- func (sess *Session) Connect(ctx context.Context) (retErr error)
- func (sess *Session) DeleteDeviceNotification(ctx context.Context, handle uint32) error
- func (sess *Session) GetSymbol(ctx context.Context, symbolName string) (SymbolView, error)
- func (sess *Session) IsClosed() bool
- func (sess *Session) IsDisconnected() bool
- func (sess *Session) ListSymbols() (map[string]SymbolView, error)
- func (sess *Session) LoadDataTypes(ctx context.Context, cfg SlowDiscoveryConfig) error
- func (sess *Session) LoadSymbolList(ctx context.Context, cfg SlowDiscoveryConfig) error
- func (sess *Session) LoadSymbols(ctx context.Context) error
- func (sess *Session) LoadSymbolsSlow(ctx context.Context, cfg SlowDiscoveryConfig) error
- func (sess *Session) ReadFromSymbol(ctx context.Context, symbolName string) (string, error)
- func (sess *Session) ReadMultipleSymbols(ctx context.Context, names []string) (map[string]string, error)
- func (sess *Session) Reconnect(ctx context.Context) error
- func (sess *Session) RefreshSymbols(ctx context.Context) error
- func (sess *Session) RuntimeState(ctx context.Context) (ADSState, error)
- func (sess *Session) SumDeleteDeviceNotification(ctx context.Context, handles []uint32) ([]ReturnCode, error)
- func (sess *Session) WriteMultipleSymbols(ctx context.Context, values map[string]string) (map[string]ReturnCode, error)
- func (sess *Session) WriteToSymbol(ctx context.Context, symbolName string, value string) error
- type SessionOption
- func WithAmsPeerListen(port int) SessionOption
- func WithAutoReconnect(enabled bool) SessionOption
- func WithBackoff(cfg BackoffConfig) SessionOption
- func WithForceRouteRegistration() SessionOption
- func WithHeartbeatRecovery(mode HeartbeatRecovery) SessionOption
- func WithHostIP(ip string) SessionOption
- func WithLocalAMS(local AMSAddress) SessionOption
- func WithLocalBindIP(ip string) SessionOption
- func WithLocalMode() SessionOption
- func WithLogger(logger *slog.Logger) SessionOption
- func WithMaxReconnectAttempts(n int) SessionOption
- func WithMaxSymbolVersionReloadAttempts(n int) SessionOption
- func WithNotificationHeartbeat(interval time.Duration, missed int) SessionOption
- func WithNotificationSilenceTimeout(d time.Duration) SessionOption
- func WithOnDisconnect(fn func()) SessionOption
- func WithOnReconnect(fn func()) SessionOption
- func WithOnSymbolVersionChanged(fn func(reason Reason)) SessionOption
- func WithRequestTimeout(d time.Duration) SessionOption
- func WithRoute(routeName, username, password string) SessionOption
- func WithRouteActivationTimeout(d time.Duration) SessionOption
- func WithRuntimeStateWatch(d time.Duration) SessionOption
- func WithSkipRouteRegistration() SessionOption
- func WithStrictReconnect(maxAttempts int) SessionOption
- func WithSymbolVersionReloadWindow(d time.Duration) SessionOption
- func WithSymbolVersionStrategy(s SymbolVersionStrategy) SessionOption
- func WithTargetCheck(c TargetCheck) SessionOption
- func WithoutAmsPeerFallback() SessionOption
- func WithoutNotificationHeartbeat() SessionOption
- func WithoutRuntimeStateWatch() SessionOption
- type SessionState
- type SlowDiscoveryConfig
- type StaleInfo
- type StampHeader
- type States
- type SumNotificationRequest
- type SumNotificationResult
- type SumReadRequest
- type SumReadResult
- type SumWriteRequest
- type SumWriteResult
- type SymbolBrowseEntry
- type SymbolFlag
- type SymbolUploadDataType
- type SymbolUploadInfo
- type SymbolVersionStrategy
- type SymbolView
- type TargetCheck
- type TransMode
- type Update
Constants ¶
const AMSHeaderSize = 32
AMSHeaderSize is the on-wire size of an AMSHeader in bytes.
const LevelTrace = slog.Level(-8)
LevelTrace is a custom slog level for trace-level logging, below slog.LevelDebug (-4). Matches zerolog's Trace level semantics.
const MaxAMSPayloadSize = 32 * 1024 * 1024 // 32 MiB
MaxAMSPayloadSize is a sanity bound on the Length field of a parsed AMSHeader. The Beckhoff ADS protocol does not define a hard ceiling but real-world frames stay under ~16MB (SumRead with thousands of items is the typical upper bound). 32MB is a defensive cap that rejects corrupt or hostile frames before they cause downstream slice-bounds panics in the documented "b[AMSHeaderSize : AMSHeaderSize+h.Length]" pattern.
const ReturnCodeErrorOffset = 0x0700
ReturnCodeErrorOffset General ADS errors begin at this offset
Variables ¶
var ( // ErrBatchSymbolUnresolved: the symbol could not be resolved to an address, // so the item was never put on the wire. ErrBatchSymbolUnresolved = errors.New("symbol could not be resolved for batch") // ErrBatchValueUnserializable: the value could not be encoded for this // symbol's data type, so the write was never put on the wire. A type change // under an online change produces this. ErrBatchValueUnserializable = errors.New("value could not be serialized for batch write") // ErrBatchValueUnparsable: the PLC answered but the bytes did not decode // against the cached type — the shape an undetected online change takes. ErrBatchValueUnparsable = errors.New("batch value could not be parsed") // ErrBatchSymbolVanished: the cache entry disappeared mid-roundtrip // (loadSymbols swapped the cache), so the PLC's answer has nowhere to land. ErrBatchSymbolVanished = errors.New("symbol removed from cache during batch") // ErrBatchNoResult: the PLC's response carried fewer items than the request, // so this item has no verdict at all. ErrBatchNoResult = errors.New("no result returned for symbol") )
Reasons the library itself, rather than the PLC, produced no value for an item of a batch read or write. They appear in BatchItemError.Skipped, wrapped around the underlying cause where there is one, so both errors.Is(err, ErrBatchSymbolUnresolved) and errors.Is(err, theUnderlyingCause) hold.
var ( // ErrNotificationDuplicate — the symbol already has an active notification. ErrNotificationDuplicate = errors.New("symbol already subscribed") // ErrNotificationChannelMismatch — all notifications on one connection must // share a single updateReceiver channel. ErrNotificationChannelMismatch = errors.New("all notifications on a connection must use the same updateReceiver channel") // ErrNotificationSymbolVanished — the symbol left the cache between resolve // and commit (online change, or a concurrent LoadSymbols). ErrNotificationSymbolVanished = errors.New("symbol removed from cache during subscribe") // ErrNotificationStrandedByReload — the symbol cache was reloaded while the // batch was in flight, so this entry's registration no longer exists. // Retryable: the caller (or resubscribeNotifications) should re-subscribe. ErrNotificationStrandedByReload = errors.New("symbol cache reloaded during batch subscribe") // ErrNotificationTransportFailure — the batch could not be sent. ErrNotificationTransportFailure = errors.New("batch transport failure") )
Reasons a batch subscribe refused to commit an entry, reported via SumNotificationResult.Skipped. They are sentinels because the right response differs per reason and callers should branch with errors.Is rather than on message text: a stranded or transport-failed entry is worth retrying, a duplicate or channel mismatch is a caller bug that retrying cannot fix.
Each is wrapped with the symbol name, so errors.Is matches while the message still says which symbol.
var ErrDisconnected = errors.New("connection is disconnected")
ErrDisconnected indicates the underlying TCP connection is not available — either Close() has been called or a reconnect has failed. Callers should use errors.Is(err, ErrDisconnected) to detect this case.
var ErrEstablishedDropped = errors.New("established connection dropped by the PLC or the network")
ErrEstablishedDropped reports a connection that had been carrying AMS frames and was then dropped by the PLC or something on the path. The route demonstrably existed, so route tables are the wrong place to look; eviction by another client on this host IP, a runtime restart, or the network path are the candidates.
var ErrRouteNotServed = errors.New("connection dropped before the PLC served any frame")
ErrRouteNotServed reports a connection that was dropped without ever carrying an AMS frame: the addressing or the route is the thing to look at. Distinct from ErrEstablishedDropped so a consumer can branch without matching strings.
var ErrRouterUnresponsive = errors.New("the PLC's AMS router answered nothing; it is briefly unreachable, not misconfigured")
ErrRouterUnresponsive reports a PLC whose AMS router answered nothing, on TCP or UDP 48899, for long enough that the route could not be checked. Nothing to fix: retry. Measured at ~8s after a client restart on a TC3 4024.
var ErrRuntimeNotRunning = errors.New("ads: PLC runtime is not in RUN")
ErrRuntimeNotRunning is returned by symbol and subscription calls when the system service reports the runtime is not in RUN. A refusal, not a retry: in CONFIG the runtime port does not exist, so the call cannot succeed and attempting it only yields a misleading AMS "port not found". The session keeps polling.
var ErrTransportClosed = errors.New("ads: client transport closed")
ErrTransportClosed is returned by every Client method after the underlying TCP transport has been closed (Close called, drop detected, dial failed). Callers reconstruct a new *Client to re-establish.
Functions ¶
func AddRemoteRoute ¶
func AddRemoteRoute(remoteHost string, localNetID [6]byte, routeName string, computerName string, username string, password string) error
AddRemoteRoute registers a route on the PLC over UDP 48899, telling it how to reach this client's NetID. remoteHost may carry the router's UDP port ("10.0.0.5:6499") when the PLC is behind NAT; computerName is the address the PLC should dial back.
Security: Beckhoff's protocol sends credentials in cleartext and offers no encrypted alternative. Trusted networks only.
func AddRemoteRouteWithLogger ¶
func AddRemoteRouteWithLogger(logger *slog.Logger, remoteHost string, localNetID [6]byte, routeName string, computerName string, username string, password string) error
AddRemoteRouteWithLogger is like AddRemoteRoute but accepts an explicit logger.
func EncodeAMSHeader ¶ added in v2.2.0
EncodeAMSHeader serializes an AMSHeader to its 32-byte wire form. No length prefix is added; callers wrap with the 6-byte AMS-over-TCP header (1 unknown + 1 system + 4 length) when sending over TCP.
func ParseNetID ¶ added in v2.2.0
ParseNetID converts a dotted-notation NetID string (e.g. "192.168.1.1.1.1") to a 6-byte array. Returns an error if the string is malformed (wrong number of parts or non-numeric values).
func ReadBit ¶ added in v2.1.0
ReadBit extracts a single bit from a byte slice. bitIndex 0 is the least significant bit of the first byte. Returns false if bitIndex is out of range.
func SetDefaultLogger ¶
SetDefaultLogger sets the package-level logger used by standalone functions like AddRemoteRoute. Call this before creating any connections if you want package-level functions to use a custom logger.
Types ¶
type ADSDataType ¶ added in v2.2.0
type ADSDataType uint32
ADST_ data type IDs from the ADS protocol (ADSDATATYPEID enum). The PLC sends these numeric codes in symbolEntry.DataType to identify the base type of a variable. Works on both TwinCAT 2 and TwinCAT 3. Source: Beckhoff TC2_Utilities ADSDATATYPEID ADSDataType identifies a primitive IEC 61131-3 data type by the numeric code Beckhoff publishes in symbolEntry.DataType. Works on both TwinCAT 2 and TwinCAT 3. The companion ADSTBigType code is the catch-all for composite types (structs, enums, type aliases, arrays).
const ( ADSTVoid ADSDataType = 0 ADSTInt16 ADSDataType = 2 // INT ADSTInt32 ADSDataType = 3 // DINT ADSTReal32 ADSDataType = 4 // REAL ADSTReal64 ADSDataType = 5 // LREAL ADSTInt8 ADSDataType = 16 // SINT ADSTUint8 ADSDataType = 17 // USINT/BYTE ADSTUint16 ADSDataType = 18 // UINT/WORD ADSTUint32 ADSDataType = 19 // UDINT/DWORD ADSTInt64 ADSDataType = 20 // LINT ADSTUint64 ADSDataType = 21 // ULINT/LWORD ADSTString ADSDataType = 30 // STRING ADSTWString ADSDataType = 31 // WSTRING ADSTBool ADSDataType = 33 // BOOL // ADSTBigType is the PLC's catch-all for composite/user-defined types: // structs, enums, type aliases, arrays. PLC sends this when the leaf does // not have a primitive ADST_ code (e.g. TC2 always reports enums this way; // TC3 sometimes reports the underlying primitive but falls back to BIGTYPE // for opaque types). Structs are caught earlier by the parse path's // Children branch, so a symbol reaching inferBaseType with BIGTYPE + a // 1/2/4/8 byte size is enum-like — safe to interpret as signed integer. ADSTBigType ADSDataType = 65 )
type ADSState ¶ added in v2.1.0
type ADSState uint16
ADSState is the run-time state of an ADS device (typically the PLC runtime), as reported by Client.ReadState.
const ( ADSStateInvalid ADSState = 0 ADSStateIdle ADSState = 1 ADSStateReset ADSState = 2 ADSStateInit ADSState = 3 ADSStateStart ADSState = 4 ADSStateRun ADSState = 5 ADSStateStop ADSState = 6 ADSStateSaveCfg ADSState = 7 ADSStateLoadCfg ADSState = 8 ADSStatePowerFailure ADSState = 9 ADSStatePowerGood ADSState = 10 ADSStateError ADSState = 11 ADSStateShutdown ADSState = 12 ADSStateSuspend ADSState = 13 ADSStateResume ADSState = 14 ADSStateConfig ADSState = 15 // System Is In Config Mode ADSStateReconfig ADSState = 16 // System Should Restart In Config Mode ADSStateMaxStates ADSState = 255 )
type AMSAddress ¶ added in v2.1.0
AMSAddress identifies an ADS endpoint by 6-byte NetID and 16-bit port.
Wire layout is those 8 bytes contiguously, which encoding/binary reproduces from declaration order. DO NOT reorder or insert fields, or change either type, without updating AMSHeader and every binary.Read/Write site.
func NewAMSAddress ¶ added in v2.2.0
func NewAMSAddress(netID string, port uint16) (AMSAddress, error)
NewAMSAddress parses a dotted-notation NetID string plus a uint16 port into an AMSAddress. The typical TwinCAT 3 PLC runtime listens on port 851 (PortR0PlcTc3).
func (AMSAddress) Equal ¶ added in v2.2.0
func (a AMSAddress) Equal(other AMSAddress) bool
Equal reports whether a and other have the same NetID and Port.
func (AMSAddress) NetIDString ¶ added in v2.2.0
func (a AMSAddress) NetIDString() string
NetIDString returns the AMS NetID in dotted notation (e.g. "192.168.1.1.1.1"). Use String for the full "NetID:Port" form.
func (AMSAddress) String ¶ added in v2.2.0
func (a AMSAddress) String() string
String returns "NetID:Port" in dotted notation, e.g. "1.2.3.4.1.1:851".
type AMSEndpoint ¶ added in v2.2.0
type AMSEndpoint struct {
// IP is the host or address of the PLC (or of the NAT that forwards to it).
IP string
// Port is the TCP port carrying AMS. Defaults to 48898, TwinCAT's own.
Port int
// AMS is the target AMS address. A zero NetID and/or Port is resolved from
// the device — see NewSession.
AMS AMSAddress
// RouterPort is the AMS router's UDP port (default 48899), used to register a
// route and identify the device. Set it behind NAT, where the forwarded UDP
// port differs from the TCP one and cannot be derived from Port. Only the two
// UDP calls use it; notifications arrive on the TCP connection we opened.
RouterPort int
}
AMSEndpoint identifies a remote ADS endpoint at the TCP and AMS layers. IP+Port locate the TwinCAT runtime over TCP (port 48898 by default); AMS is the target AMSAddress carried in every ADS request header.
type AMSError ¶ added in v2.3.0
type AMSError struct {
Code ReturnCode
}
AMSError is a rejection from the AMS router, not a verdict about any ADS item: the request never reached a service that could answer. A system in CONFIG answers 0x06 for every request to a runtime port. Branch with errors.Is against the ReturnCode constants, or read Code off the typed value.
func (AMSError) Is ¶ added in v2.3.0
Is makes errors.Is(err, ReturnCodeX) match the router's code without making a router rejection indistinguishable from an ADS device verdict.
There is deliberately no Unwrap: errors.As traverses it, and every abort guard in this package asks errors.As(err, &ReturnCode) to mean "the device answered about my item". Unwrapping to Code would put the router back inside that answer — which is the bug this type exists to close, so do not add one.
type AMSHeader ¶ added in v2.2.0
type AMSHeader struct {
Target AMSAddress
Source AMSAddress
Command CommandID
State uint16
Length uint32
ErrorCode uint32
InvokeID uint32
}
AMSHeader is the 32-byte ADS frame header (per Beckhoff AMS/ADS spec). Wire layout (little-endian):
offset 0 Target NetID(6) + Port(2) = AMSAddress offset 8 Source NetID(6) + Port(2) = AMSAddress offset 16 CommandID (2) offset 18 State flags (2) offset 20 Data length (4) — payload bytes following this header offset 24 ErrorCode (4) offset 28 InvokeID (4)
Exported so router and proxy implementations can parse, mutate and re-encode frames without touching the Client RPC plumbing.
func ParseAMSHeader ¶ added in v2.2.0
ParseAMSHeader decodes a 32-byte AMS header from the front of b. The caller is responsible for stripping any preceding 6-byte AMS-over-TCP length prefix. Returns an error if b is shorter than AMSHeaderSize or if the decoded payload Length field exceeds MaxAMSPayloadSize (the latter catches corrupt/hostile frames that would otherwise panic the caller's "b[AMSHeaderSize : AMSHeaderSize+h.Length]" extraction). The payload (Length bytes) follows directly after the header in b; callers extract it as b[AMSHeaderSize : AMSHeaderSize+int(h.Length)].
type BackoffConfig ¶ added in v2.1.0
type BackoffConfig struct {
InitialInterval time.Duration // delay for first N attempts (default: 1s)
InitialAttempts int // how many attempts at initial interval (default: 3)
MidInterval time.Duration // delay for mid-tier attempts (default: 5s)
MidAttempts int // how many attempts at mid interval (default: 3)
SlowInterval time.Duration // delay for slow-tier attempts (default: 15s)
SlowAttempts int // how many attempts at slow interval (default: 4)
MaxInterval time.Duration // cap after all tiers exhausted (default: 30s)
}
BackoffConfig controls reconnect timing behavior. Reconnection uses stepped intervals: fast retries first (for network blips), then progressively slower intervals to avoid overwhelming the PLC. Backoff resets on each successful reconnect.
func DefaultBackoffConfig ¶ added in v2.1.0
func DefaultBackoffConfig() BackoffConfig
DefaultBackoffConfig returns the default reconnect backoff configuration.
MaxInterval also caps the cross-cycle flap cooldown (see flapResetWindow), which is what governs a device that resets the connection on a timer. The 30s default protects the PLC's socket table at the cost of stream continuity during such an episode; lower it if the samples matter more than the sockets.
func (BackoffConfig) Validate ¶ added in v2.2.0
func (c BackoffConfig) Validate() error
Validate reports configuration problems that would produce pathological reconnect behavior: zero-or-negative intervals (zero-delay retry storms that exhaust ephemeral ports), negative attempt counts (skipped tiers surfacing as silent fast-fail), or a MaxInterval below the previous tier (caps that defeat the slow-tier ramp).
type BatchError ¶ added in v2.3.0
type BatchError struct {
// Op is "read" or "write".
Op string
// Requested is how many symbols the caller asked for.
Requested int
// Succeeded is how many produced a valid result.
Succeeded int
// Items holds one entry per failed symbol, never empty.
Items []BatchItemError
}
BatchError reports the items of a batch read or write that produced no value, while the successful ones stay in the call's map. It is the only error from these calls that leaves the map usable -- any other means the transport failed and no outcome is known. Recover it with errors.As. Batch size does not change the contract: one absent symbol alone or among forty reads the same.
func (*BatchError) Error ¶ added in v2.3.0
func (e *BatchError) Error() string
Error names the failed symbols and their reasons.
func (*BatchError) Unwrap ¶ added in v2.3.0
func (e *BatchError) Unwrap() []error
Unwrap exposes the library-side skip reasons so errors.Is can match the ErrBatch* sentinels through the BatchError. Per-item PLC codes are not unwrapped: a ReturnCode reached through errors.As would be indistinguishable from a call-level failure, which is the ambiguity this type exists to remove.
type BatchItemError ¶ added in v2.3.0
type BatchItemError struct {
// Symbol is the symbol name as the caller passed it.
Symbol string
// Error is the PLC's per-item return code. Meaningful only when Skipped is nil.
Error ReturnCode
// Skipped is non-nil when the library produced no value for this item.
Skipped error
}
BatchItemError is the per-item failure of a batch read or write, carrying the same three-state contract as SumNotificationResult so a device verdict is distinguishable from a library-side drop: Skipped != nil means the library never sent it or could not use the answer and Error is meaningless, otherwise Error is the PLC's own return code. Only failures appear.
func (BatchItemError) String ¶ added in v2.3.0
func (i BatchItemError) String() string
String renders one item as "MAIN.a (code 0x710)" or "MAIN.a (skipped: ...)".
type Client ¶ added in v2.1.0
type Client struct {
// contains filtered or unexported fields
}
Client is the Beckhoff-equivalent thin RPC layer. One TCP connection, raw AMS framing, request multiplexing via InvokeID, listen + transmit + recv worker goroutines. No cache, no FSM, no reconnect, no callbacks.
Lifetime states are exactly two: alive (Dial succeeded, Close not yet called, transport not yet observed as dropped) and closed. Once closed, every public method returns ErrTransportClosed.
func Dial ¶ added in v2.1.0
func Dial( ip string, port int, target, source AMSAddress, requestTimeout time.Duration, opts ...ClientOption, ) (*Client, error)
Dial opens one TCP connection to ip:port, configures TCP keepalive, and spawns the listen / transmit / recvWorker goroutines. Returns a usable Client. See docs/archive/specs/09-fsm-design.md "Layer 2: Client (raw RPC)".
func (*Client) AcceptPeerConn ¶ added in v2.3.0
AcceptPeerConn adopts a connection the PLC opened TO US, reading its frames into the same response mux. Some devices treat a registered route as a peer router: they process our requests on our connection but answer over one they open back to us. Frames carry their own invokeID, so responses match regardless of which socket they arrive on.
func (*Client) AddDeviceNotification ¶ added in v2.1.0
func (c *Client) AddDeviceNotification( ctx context.Context, group uint32, offset uint32, length uint32, transmissionMode TransMode, maxDelay time.Duration, cycleTime time.Duration, ) (handle uint32, err error)
AddDeviceNotification registers a device notification with the PLC and returns the PLC-assigned handle. Raw RPC: no Session-side persistence. Callers wanting auto-resubscribe-on-reconnect use Session.AddSymbolNotification.
func (*Client) ClearProcessInputs ¶ added in v2.1.0
EXPERIMENTAL: ClearProcessInputs writes all input process image bytes to zero.
func (*Client) ClearProcessOutputs ¶ added in v2.1.0
EXPERIMENTAL: ClearProcessOutputs writes all output process image bytes to zero.
func (*Client) Close ¶ added in v2.1.0
Close cancels worker goroutines, closes the TCP connection, and waits for workers to exit. Idempotent: subsequent calls are no-ops returning nil. Sets tx.disconnected so any subsequent RPC method returns ErrTransportClosed immediately.
func (*Client) DeleteDeviceNotification ¶ added in v2.1.0
DeleteDeviceNotification deletes a device notification by handle. Raw RPC: returns the wire-level success/error. Callers that maintain activeNotifications must clean up themselves (Session does this in its wrapper Session.DeleteDeviceNotification below).
func (*Client) DownloadDataTypes ¶ added in v2.1.0
DownloadDataTypes downloads the raw datatype-table bytes (group 0xF00E). Caller decodes via parseUploadSymbolInfoDataTypes. This is a raw RPC and bypasses the symbol cache; the caller is responsible for decoding the response.
func (*Client) DownloadInChunks ¶ added in v2.1.0
func (c *Client) DownloadInChunks(ctx context.Context, group uint32, totalLength uint32, chunkSize uint32, delay time.Duration) ([]byte, error)
downloadInChunks reads a large blob in chunkSize pieces via the Read command's offset, with an optional inter-chunk delay, so the loaders do not overwhelm the PLC's real-time loop. Returns immediately when chunking is already known to be unsupported, so the caller takes the single-request fallback.
func (*Client) DownloadSymbolList ¶ added in v2.1.0
DownloadSymbolList downloads the raw symbol-table bytes (group 0xF00B). Caller decodes per parseUploadSymbolInfoSymbols. This is a raw RPC and bypasses the symbol cache; the caller is responsible for decoding the response.
func (*Client) GetHandleByName ¶ added in v2.1.0
GetHandleByName resolves a symbol name to its PLC-side handle. Wire RPC; no cache. The handle is valid until the TCP transport drops or until the caller releases it via ReleaseHandle.
func (*Client) GetSymbolInfoByName ¶ added in v2.1.0
getSymbolInfoByName queries a single symbol's metadata from the PLC using GroupSymbolInfoByNameEx (0xF009). GetSymbolInfoByName resolves a single symbol on the PLC and returns a populated symbol with Group, Offset, Length, DataType, etc. Does NOT populate Children (struct / array children require full discovery via LoadSymbols / LoadSymbolList + LoadDataTypes). This is a raw RPC and bypasses the symbol cache; the caller is responsible for decoding the response.
func (*Client) GetSymbolUploadInfo ¶ added in v2.1.0
func (c *Client) GetSymbolUploadInfo(ctx context.Context) (SymbolUploadInfo, error)
GetSymbolUploadInfo reads the symbol-table size header from the PLC. Tries extended info (0xF00F, 24 bytes) first; falls back to basic info (0xF00C, 16 bytes) for older PLCs. Used by LoadSymbols / LoadSymbolList / LoadDataTypes to size the subsequent download. This is a raw RPC and bypasses the symbol cache; the caller is responsible for decoding the response.
func (*Client) GetSymbolVersion ¶ added in v2.1.0
GetSymbolVersion reads the current PLC symbol version (single byte). Increments on online-change or download. This is a raw RPC and bypasses the symbol cache; the caller is responsible for decoding the response.
func (*Client) Read ¶ added in v2.1.0
func (c *Client) Read(ctx context.Context, group uint32, offset uint32, length uint32) (data []byte, err error)
Read issues ADS Read (cmd 2) against the given index group/offset.
func (*Client) ReadDeviceInfo ¶ added in v2.1.0
func (c *Client) ReadDeviceInfo(ctx context.Context) (response DeviceInfo, err error)
ReadDeviceInfo issues ADS ReadDeviceInfo (cmd 1).
func (*Client) ReadProcessInput ¶ added in v2.1.0
EXPERIMENTAL: ReadProcessInput reads bytes from the input process image at the given byte offset.
func (*Client) ReadProcessInputBit ¶ added in v2.1.0
func (c *Client) ReadProcessInputBit(ctx context.Context, byteOffset uint32, bitIndex uint8) (bool, error)
EXPERIMENTAL: ReadProcessInputBit reads a single bit from the input process image. bitIndex must be 0-7 (bit within a single byte).
func (*Client) ReadProcessInputSize ¶ added in v2.1.0
EXPERIMENTAL: ReadProcessInputSize returns the size of the input process image in bytes.
func (*Client) ReadProcessOutput ¶ added in v2.1.0
EXPERIMENTAL: ReadProcessOutput reads bytes from the output process image at the given byte offset.
func (*Client) ReadState ¶ added in v2.1.0
ReadState issues ADS ReadState (cmd 4) and returns the PLC's ADS+device state.
func (*Client) ReadStateOnPort ¶ added in v2.3.0
ReadStateOnPort reads the ADS state of another port on the same device. The system service (PortSystemService) is the one that matters: it answers while the system is in CONFIG, when the runtime ports do not exist at all.
func (*Client) ReleaseHandle ¶ added in v2.1.0
ReleaseHandle releases a symbol handle previously acquired via GetHandleByName. Wraps Write to GroupSymbolReleaseHandle so the Beckhoff-equivalent surface includes a symmetric release primitive.
func (*Client) SetNotificationHandler ¶ added in v2.1.0
func (c *Client) SetNotificationHandler(fn NotificationHandler)
SetNotificationHandler installs (or replaces) the callback for inbound DeviceNotification packets. nil disables dispatch (packets dropped after a Debug log entry). Concurrent-safe; the recvWorker reads under RLock.
func (*Client) SetOnDrop ¶ added in v2.1.0
func (c *Client) SetOnDrop(fn func())
SetOnDrop registers a callback fired on unexpected transport drop. Prefer WithOnDrop at construction time.
func (*Client) SumAddDeviceNotification ¶ added in v2.1.0
func (c *Client) SumAddDeviceNotification(ctx context.Context, requests []SumNotificationRequest) ([]SumNotificationResult, error)
SumAddDeviceNotification adds multiple device notifications in a single ADS round-trip using GroupSumupAddDeviceNotification (0xF085). Falls back to individual AddDeviceNotification calls on older PLCs.
func (*Client) SumDeleteDeviceNotification ¶ added in v2.1.0
func (c *Client) SumDeleteDeviceNotification(ctx context.Context, handles []uint32) ([]ReturnCode, error)
SumDeleteDeviceNotification deletes multiple device notifications by handle in a single ADS round-trip using GroupSumupDeleteDeviceNotification (0xF086). Falls back to individual DeleteDeviceNotification calls on older PLCs.
Returns the per-handle ReturnCode slice from the PLC. Persistent activeNotifications cleanup is the caller's responsibility; Session wraps this with its notifications.lock cleanup in Session.SumDeleteDeviceNotification.
func (*Client) SumRead ¶ added in v2.1.0
func (c *Client) SumRead(ctx context.Context, requests []SumReadRequest) ([]SumReadResult, error)
SumRead performs a batch read of multiple index group/offset/length combinations in a single ADS round-trip.
Tries SumReadEx2 (0xF084; TC3 and TC2 2.11), then SumReadEx (0xF083), then individual reads. Both sum variants share one response format, and the detected level is cached until reconnect. Deliberately not on executeSumCommand: the two-command-ID fallback does not fit its single-spec contract.
func (*Client) SumWrite ¶ added in v2.1.0
func (c *Client) SumWrite(ctx context.Context, requests []SumWriteRequest) ([]SumWriteResult, error)
SumWrite performs a batch write using GroupSumupWrite (0xF081). This writes multiple index group/offset combinations in a single ADS round-trip. If the sum command fails (e.g. on older PLCs), it falls back to individual writes.
SumWrite is intentionally NOT migrated to executeSumCommand. The request payload has a header section (Group+Offset+Length per item) followed by a concatenated data section (variable-size Data bytes per item). The generic helper's fixed itemWriteSize cannot represent this. Custom orchestration is required.
func (*Client) Write ¶ added in v2.1.0
Write issues ADS Write (cmd 3) to the PLC at the given index group/offset.
func (*Client) WriteProcessOutput ¶ added in v2.1.0
EXPERIMENTAL: WriteProcessOutput writes bytes to the output process image at the given byte offset.
func (*Client) WriteProcessOutputBit ¶ added in v2.1.0
func (c *Client) WriteProcessOutputBit(ctx context.Context, byteOffset uint32, bitIndex uint8, value bool) error
EXPERIMENTAL: WriteProcessOutputBit writes a single bit to the output process image. bitIndex must be 0-7 (bit within a single byte).
type ClientOption ¶ added in v2.1.0
type ClientOption func(*Client)
ClientOption configures optional construction parameters for Dial.
func WithClientLogger ¶ added in v2.1.0
func WithClientLogger(logger *slog.Logger) ClientOption
WithClientLogger sets the slog.Logger for a Client. Nil is ignored.
func WithClientRequestTimeout ¶ added in v2.1.0
func WithClientRequestTimeout(d time.Duration) ClientOption
WithClientRequestTimeout overrides the per-request and dial timeout. Values <= 0 are ignored (the default of 5s applies).
func WithNotificationHandler ¶ added in v2.1.0
func WithNotificationHandler(fn NotificationHandler) ClientOption
WithNotificationHandler installs a callback for inbound DeviceNotification packets. Session installs its own handler internally; raw Client consumers (CLI, web ADS browser) install their own to receive notifications, or leave nil to drop them silently.
func WithOnDrop ¶ added in v2.1.0
func WithOnDrop(fn func()) ClientOption
WithOnDrop registers a callback fired on unexpected transport drop. See SetOnDrop for the runtime equivalent.
type CommandID ¶
type CommandID uint16
CommandID is the ADS command opcode carried in the AMS header (see Beckhoff ADS specification for the wire format).
type DeviceInfo ¶
DeviceInfo is the PLC's self-reported identity (returned by ReadDeviceInfo). DeviceName is a 16-byte null-padded ASCII field; trim at the first null byte for display.
type Group ¶
type Group uint32
Group is an ADS index group identifier. The high-byte hex values address reserved system tables (symbol cache, process image, device data) per Beckhoff's published index-group reservations.
const ( GroupSymbolTab Group = 0xf000 GroupSymbolName Group = 0xf001 GroupSymbolValue Group = 0xf002 GroupSymbolHandleByName Group = 0xF003 GroupSymbolValueByName Group = 0xF004 GroupSymbolValueByHandle Group = 0xF005 GroupSymbolReleaseHandle Group = 0xF006 GroupSymbolInfoByName Group = 0xF007 GroupSymbolVersion Group = 0xF008 GroupSymbolInfoByNameEx Group = 0xF009 GroupSymbolDownload Group = 0xF00A GroupSymbolUpload Group = 0xF00B GroupSymbolUploadInfo Group = 0xF00C GroupSymbolDownload2 Group = 0xF00D GroupSymbolDataTypeUpload Group = 0xF00E GroupSymbolUploadInfo2 Group = 0xF00F GroupSymbolNotification Group = 0xF010 // notification of named handle GroupSumupRead Group = 0xF080 GroupSumupWrite Group = 0xF081 GroupSumupReadWrite Group = 0xF082 GroupSumupReadEx Group = 0xF083 GroupSumupReadEx2 Group = 0xF084 GroupSumupAddDeviceNotification Group = 0xF085 GroupSumupDeleteDeviceNotification Group = 0xF086 GroupIoImageRwib Group = 0xF020 // read/write input byte(s) GroupIoImageRwix Group = 0xF021 // read/write input bit GroupIoImageRisize Group = 0xF025 // read input size (in byte) GroupIoImageRwob Group = 0xF030 // read/write output byte(s) GroupIoImageRwox Group = 0xF031 // read/write output bit GroupIoImageCleari Group = 0xF040 // write inputs to null GroupIoImageClearo Group = 0xF050 // write outputs to null GroupIoImageRwiob Group = 0xF060 // read input and write output byte(s) GroupDeviceData Group = 0xF100 // state, name, etc... )
type HeartbeatRecovery ¶ added in v2.3.1
type HeartbeatRecovery int
Heartbeat: proving the caller's subscriptions are alive without asking the PLC. A runtime restart kills subscriptions while leaving the connection, symbol version and ADS state unchanged, so one cyclic subscription of our own turns silence into proof. See notificationManager.heartbeatHandle.
HeartbeatRecovery selects what happens when it goes silent. Recovery is not free -- a delete and an add per handle, 82 requests on a 41-symbol session -- but Immediate stays the default as the behaviour that has been in the field.
const ( // HeartbeatRecoveryImmediate re-subscribes as soon as the silence window is // exceeded. The default. HeartbeatRecoveryImmediate HeartbeatRecovery = iota + 1 // HeartbeatRecoveryConfirm requires two consecutive silent windows before // re-subscribing, trading time-to-notice for not firing the burst at a device // that was merely late. HeartbeatRecoveryConfirm // HeartbeatRecoveryObserve never re-subscribes. The silence is reported (log // plus the WithOnSymbolVersionChanged callback) and left to the consumer, for // a caller that would rather rebuild the session itself than have handles // churned underneath it. HeartbeatRecoveryObserve )
func (HeartbeatRecovery) String ¶ added in v2.3.1
func (h HeartbeatRecovery) String() string
type NotificationConfig ¶
type NotificationConfig struct {
SymbolName string
MaxDelay time.Duration
CycleTime time.Duration
TransmissionMode TransMode
}
NotificationConfig holds configuration for a symbol notification, used for batch add and reconnect re-subscribe. MaxDelay and CycleTime use time.Duration for consistency with SumNotificationRequest and the rest of the standard library.
type NotificationHandler ¶ added in v2.2.0
NotificationHandler is the callback the recvWorker invokes when it decodes a DeviceNotification packet. Session installs its handleNotification here; raw Client consumers (web ADS browser, CLI tools, AMS routers) install their own. ctx is the Client's internal worker context — observe Done() to abort long-running handler work on Close. handle, timestamp, content are the parsed notification fields (handle = PLC-assigned ID, timestamp = Windows FILETIME, content = raw payload bytes).
type NotificationSample ¶
NotificationSample is the per-handle prefix inside a stamp record: Handle identifies the subscription that produced the value; Size is the byte length of the value bytes that follow.
type NotificationStream ¶
NotificationStream is the outer header of an ADS DeviceNotification payload: Length is the total payload length in bytes, Stamps is the number of StampHeader records that follow.
type Offset ¶
type Offset uint32
Offset is the index-offset half of an ADS group/offset address; the interpretation depends on the index group (symbol-relative byte offset, process-image bit/byte index, device-data field, ...).
type Port ¶
type Port uint32
Port is a well-known AMS port number for a TwinCAT service. PortR0PlcTc3 (851) is the typical PLC runtime port for TwinCAT 3.
const ( PortLogger Port = 100 PortR0Rtime Port = 200 PortR0Trace Port = (PortR0Rtime + 90) PortR0Io Port = 300 PortR0Sps Port = 400 PortR0Nc Port = 500 PortR0Isg Port = 550 PortR0Pcs Port = 600 PortR0Plc Port = 801 PortR0PlcRts1 Port = 801 PortR0PlcRts2 Port = 811 PortR0PlcRts3 Port = 821 PortR0PlcRts4 Port = 831 PortR0PlcTc3 Port = 851 // PortSystemService is the TwinCAT system service. Unlike the runtime ports it // stays up while the system is in CONFIG — measured on TC3.1.4024: every request // to 851 came back with AMS ErrorCode 6 (target port not found), while 10000 // answered ADSState=15 (CONFIG). So it is the only place to ask what state the // system is actually in. PortSystemService Port = 10000 )
type Reason ¶ added in v2.2.0
type Reason string
Reason is the enumerated cause of a stale-cache detection or one-shot Update.Stale notification. Values are STABLE — safe for switch/case comparison by callers. New reasons may be added in future versions; callers SHOULD have a default branch.
const ( ReasonSymbolVersionInvalid Reason = "symbol-version-invalid" ReasonSymbolNotFound Reason = "symbol-not-found" ReasonInvalidOffset Reason = "invalid-offset" ReasonSymbolNotActive Reason = "symbol-not-active" ReasonNotifyHandleInvalid Reason = "notify-handle-invalid" ReasonInvalidSize Reason = "invalid-size" ReasonReloadCapExhausted Reason = "reload-cap-exhausted" ReasonReloadInProgress Reason = "reload-in-progress" // ReasonHeartbeatSilent is delivered when the internal heartbeat has gone // silent and the session is NOT re-subscribing because the caller chose // WithHeartbeatRecovery(HeartbeatRecoveryObserve). It is the only signal that // mode produces, and it means this session's subscriptions are dead until the // consumer rebuilds it. ReasonHeartbeatSilent Reason = "heartbeat-silent" )
type RemoteIdentity ¶ added in v2.3.0
type RemoteIdentity struct {
// AMS is the router's own address. The port is the router's (10000), NOT a
// PLC runtime port — see RuntimePort.
AMS AMSAddress
// HostName is the device's computer name, e.g. "CX-4285CB". Empty if the
// device did not report one.
HostName string
// Major, Minor and Build are the reported TwinCAT version, e.g. 3, 1, 4024.
// Zero if the device did not report a version.
Major uint8
Minor uint8
Build uint16
// Tags is every tag exactly as received, keyed by Beckhoff tag id,
// including the ones this package does not interpret. Present so callers
// can inspect platform details (tag 4 embeds "TC/RTOS" on that platform)
// without this package pretending to decode a layout it has not verified.
Tags map[uint16][]byte
}
RemoteIdentity is what an AMS router reports about ITSELF, from IdentifyRemote.
Read AMS carefully: it identifies the router answering at that IP, not the PLC behind it. On a CX the two are the same device; on an engineering PC or gateway the PLC you want is an entry in its route table. Nothing in the response distinguishes the cases, and NetIDs are not derived from the IP.
func IdentifyRemote ¶ added in v2.3.0
func IdentifyRemote(ctx context.Context, host string) (RemoteIdentity, error)
IdentifyRemote asks the device at host for its own NetID and system details. host may carry a port for NAT forwarding; bare hosts use the protocol's UDP port. Read-only, honouring ctx's deadline if sooner than the default 3s. The answer describes the ROUTER, which is the PLC only when they are one device.
func IdentifyRemoteWithLogger ¶ added in v2.3.0
func IdentifyRemoteWithLogger(ctx context.Context, logger *slog.Logger, host string) (RemoteIdentity, error)
IdentifyRemoteWithLogger is IdentifyRemote with an explicit logger.
func (RemoteIdentity) RuntimePort ¶ added in v2.3.0
func (id RemoteIdentity) RuntimePort() uint16
RuntimePort returns the conventional AMS port of the FIRST PLC runtime for the reported TwinCAT major version: PortR0PlcRts1 (801) on TwinCAT 2, PortR0PlcTc3 (851) otherwise.
This is a convention, not a discovered value — the identify service reports the router's own port and never a runtime port. A TwinCAT 2 project with several runtimes uses 811, 821, 831; a TwinCAT 3 one uses 852, 853, … and a caller targeting any of those must say so explicitly.
func (RemoteIdentity) Version ¶ added in v2.3.0
func (id RemoteIdentity) Version() string
Version renders the reported TwinCAT version, e.g. "3.1.4024".
type ReturnCode ¶
type ReturnCode uint32
ReturnCode is an ADS return code reported by the PLC in command responses and per-item sum-command results. The Error method on ReturnCode satisfies the error interface so callers can use errors.As to extract the code from wrapped errors.
const ( ReturnCodeNoErrors ReturnCode = 0x00 // Global error codes (0x01 - 0x1E) ReturnCodeGlobalInternalError ReturnCode = 0x01 ReturnCodeGlobalNoRtime ReturnCode = 0x02 ReturnCodeGlobalAllocLockedMemError ReturnCode = 0x03 ReturnCodeGlobalInsertMailboxError ReturnCode = 0x04 ReturnCodeGlobalWrongReceiveHmsg ReturnCode = 0x05 ReturnCodeGlobalTargetPortNotFound ReturnCode = 0x06 ReturnCodeGlobalTargetNotFound ReturnCode = 0x07 ReturnCodeGlobalUnknownCommandID ReturnCode = 0x08 ReturnCodeGlobalBadTaskID ReturnCode = 0x09 ReturnCodeGlobalNoIO ReturnCode = 0x0A ReturnCodeGlobalUnknownAdsCommand ReturnCode = 0x0B ReturnCodeGlobalWin32Error ReturnCode = 0x0C ReturnCodeGlobalPortNotConnected ReturnCode = 0x0D ReturnCodeGlobalInvalidAdsLength ReturnCode = 0x0E ReturnCodeGlobalInvalidAmsNetID ReturnCode = 0x0F ReturnCodeGlobalLowInstallLevel ReturnCode = 0x10 ReturnCodeGlobalNoDebugAvailable ReturnCode = 0x11 ReturnCodeGlobalPortDisabled ReturnCode = 0x12 ReturnCodeGlobalPortAlreadyConn ReturnCode = 0x13 ReturnCodeGlobalAdsSyncW32Error ReturnCode = 0x14 ReturnCodeGlobalAdsSyncTimeout ReturnCode = 0x15 ReturnCodeGlobalAdsSyncAmsError ReturnCode = 0x16 ReturnCodeGlobalAdsSyncNoIndexMap ReturnCode = 0x17 ReturnCodeGlobalInvalidAdsPort ReturnCode = 0x18 ReturnCodeGlobalNoMemory ReturnCode = 0x19 ReturnCodeGlobalTcpSendError ReturnCode = 0x1A ReturnCodeGlobalHostUnreachable ReturnCode = 0x1B ReturnCodeGlobalInvalidAmsFragment ReturnCode = 0x1C ReturnCodeGlobalTlsSendError ReturnCode = 0x1D ReturnCodeGlobalAccessDenied ReturnCode = 0x1E // Router error codes (0x500 - 0x50D) ReturnCodeRouterNoLockedMemory ReturnCode = 0x500 ReturnCodeRouterResizeMemory ReturnCode = 0x501 ReturnCodeRouterMailboxFull ReturnCode = 0x502 ReturnCodeRouterDebugBoxFull ReturnCode = 0x503 ReturnCodeRouterUnknownPortType ReturnCode = 0x504 ReturnCodeRouterNotInitialized ReturnCode = 0x505 ReturnCodeRouterPortAlreadyInUse ReturnCode = 0x506 ReturnCodeRouterNotRegistered ReturnCode = 0x507 ReturnCodeRouterNoMoreQueues ReturnCode = 0x508 ReturnCodeRouterInvalidPort ReturnCode = 0x509 ReturnCodeRouterNotActivated ReturnCode = 0x50A ReturnCodeRouterFragmentBoxFull ReturnCode = 0x50B ReturnCodeRouterFragmentTimeout ReturnCode = 0x50C ReturnCodeRouterToBeRemoved ReturnCode = 0x50D // General ADS error codes (0x700 - 0x739) ReturnCodeDeviceError ReturnCode = (0x00 + ReturnCodeErrorOffset) ReturnCodeDeviceServiceNotSupported ReturnCode = (0x01 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidGroup ReturnCode = (0x02 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidOffset ReturnCode = (0x03 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidAccess ReturnCode = (0x04 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidSize ReturnCode = (0x05 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidData ReturnCode = (0x06 + ReturnCodeErrorOffset) ReturnCodeDeviceNotReady ReturnCode = (0x07 + ReturnCodeErrorOffset) ReturnCodeDeviceBusy ReturnCode = (0x08 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidContext ReturnCode = (0x09 + ReturnCodeErrorOffset) ReturnCodeDeviceNoMemory ReturnCode = (0x0A + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidParam ReturnCode = (0x0B + ReturnCodeErrorOffset) ReturnCodeDeviceNotFound ReturnCode = (0x0C + ReturnCodeErrorOffset) ReturnCodeDeviceSyntax ReturnCode = (0x0D + ReturnCodeErrorOffset) ReturnCodeDeviceIncompatible ReturnCode = (0x0E + ReturnCodeErrorOffset) ReturnCodeDeviceExists ReturnCode = (0x0F + ReturnCodeErrorOffset) ReturnCodeDeviceSymbolNoFound ReturnCode = (0x10 + ReturnCodeErrorOffset) ReturnCodeDeviceSymbolVersionInvalid ReturnCode = (0x11 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidState ReturnCode = (0x12 + ReturnCodeErrorOffset) ReturnCodeDeviceTransModeNotSupported ReturnCode = (0x13 + ReturnCodeErrorOffset) ReturnCodeDeviceNotifyHandleInvalid ReturnCode = (0x14 + ReturnCodeErrorOffset) ReturnCodeDeviceClientUnknown ReturnCode = (0x15 + ReturnCodeErrorOffset) ReturnCodeDeviceNoMoreHandles ReturnCode = (0x16 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidWatchSize ReturnCode = (0x17 + ReturnCodeErrorOffset) ReturnCodeDeviceNotInitialized ReturnCode = (0x18 + ReturnCodeErrorOffset) ReturnCodeDeviceTimeout ReturnCode = (0x19 + ReturnCodeErrorOffset) ReturnCodeDeviceNoInterface ReturnCode = (0x1A + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidInterface ReturnCode = (0x1B + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidClsID ReturnCode = (0x1C + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidObjID ReturnCode = (0x1D + ReturnCodeErrorOffset) ReturnCodeDevicePending ReturnCode = (0x1E + ReturnCodeErrorOffset) ReturnCodeDeviceAborted ReturnCode = (0x1F + ReturnCodeErrorOffset) ReturnCodeDeviceWarning ReturnCode = (0x20 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidArrayIndex ReturnCode = (0x21 + ReturnCodeErrorOffset) ReturnCodeDeviceSymbolNotActive ReturnCode = (0x22 + ReturnCodeErrorOffset) ReturnCodeDeviceAccessDenied ReturnCode = (0x23 + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseNotFound ReturnCode = (0x24 + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseExpired ReturnCode = (0x25 + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseExceeded ReturnCode = (0x26 + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseInvalid ReturnCode = (0x27 + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseSystemID ReturnCode = (0x28 + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseNoTimeLimit ReturnCode = (0x29 + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseFutureIssue ReturnCode = (0x2A + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseTimeToLong ReturnCode = (0x2B + ReturnCodeErrorOffset) ReturnCodeDeviceException ReturnCode = (0x2C + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseDuplicated ReturnCode = (0x2D + ReturnCodeErrorOffset) ReturnCodeDeviceSignatureInvalid ReturnCode = (0x2E + ReturnCodeErrorOffset) ReturnCodeDeviceCertificateInvalid ReturnCode = (0x2F + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseOemNotFound ReturnCode = (0x30 + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseRestricted ReturnCode = (0x31 + ReturnCodeErrorOffset) ReturnCodeDeviceLicenseDemoDenied ReturnCode = (0x32 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidFuncID ReturnCode = (0x33 + ReturnCodeErrorOffset) ReturnCodeDeviceOutOfRange ReturnCode = (0x34 + ReturnCodeErrorOffset) ReturnCodeDeviceInvalidAlignment ReturnCode = (0x35 + ReturnCodeErrorOffset) ReturnCodeDeviceLicensePlatform ReturnCode = (0x36 + ReturnCodeErrorOffset) ReturnCodeDeviceForwardPL ReturnCode = (0x37 + ReturnCodeErrorOffset) ReturnCodeDeviceForwardDL ReturnCode = (0x38 + ReturnCodeErrorOffset) ReturnCodeDeviceForwardRT ReturnCode = (0x39 + ReturnCodeErrorOffset) // Client error codes (0x740 - 0x756) ReturnCodeClientError ReturnCode = (0x40 + ReturnCodeErrorOffset) ReturnCodeClientInvalidParameter ReturnCode = (0x41 + ReturnCodeErrorOffset) ReturnCodeClientListEmpty ReturnCode = (0x42 + ReturnCodeErrorOffset) ReturnCodeClientVarUsed ReturnCode = (0x43 + ReturnCodeErrorOffset) ReturnCodeClientDuplicateInvokeID ReturnCode = (0x44 + ReturnCodeErrorOffset) ReturnCodeClientSyncTimeout ReturnCode = (0x45 + ReturnCodeErrorOffset) ReturnCodeClientW32Error ReturnCode = (0x46 + ReturnCodeErrorOffset) ReturnCodeClientTimeoutInvalid ReturnCode = (0x47 + ReturnCodeErrorOffset) ReturnCodeClientPortNotOpen ReturnCode = (0x48 + ReturnCodeErrorOffset) ReturnCodeClientNoAMSAddress ReturnCode = (0x49 + ReturnCodeErrorOffset) ReturnCodeClientSyncInternal ReturnCode = (0x50 + ReturnCodeErrorOffset) ReturnCodeClientAddHash ReturnCode = (0x51 + ReturnCodeErrorOffset) ReturnCodeClientRemoveHash ReturnCode = (0x52 + ReturnCodeErrorOffset) ReturnCodeClientNoMoreSymbols ReturnCode = (0x53 + ReturnCodeErrorOffset) ReturnCodeClientSyncResponseInvalid ReturnCode = (0x54 + ReturnCodeErrorOffset) ReturnCodeClientSyncPortLocked ReturnCode = (0x55 + ReturnCodeErrorOffset) ReturnCodeClientRequestCancelled ReturnCode = (0x56 + ReturnCodeErrorOffset) // RTime error codes (0x1000 - 0x101A) ReturnCodeRTimeInternal ReturnCode = 0x1000 ReturnCodeRTimeBadTimerPeriods ReturnCode = 0x1001 ReturnCodeRTimeInvalidTaskPtr ReturnCode = 0x1002 ReturnCodeRTimeInvalidStackPtr ReturnCode = 0x1003 ReturnCodeRTimePrioExists ReturnCode = 0x1004 ReturnCodeRTimeNoMoreTcb ReturnCode = 0x1005 ReturnCodeRTimeNoMoreSemas ReturnCode = 0x1006 ReturnCodeRTimeNoMoreQueues ReturnCode = 0x1007 ReturnCodeRTimeExtIrqAlreadyDef ReturnCode = 0x100D ReturnCodeRTimeExtIrqNotDef ReturnCode = 0x100E ReturnCodeRTimeExtIrqInstallFailed ReturnCode = 0x100F ReturnCodeRTimeIrqlNotLessOrEqual ReturnCode = 0x1010 ReturnCodeRTimeVmxNotSupported ReturnCode = 0x1017 ReturnCodeRTimeVmxDisabled ReturnCode = 0x1018 ReturnCodeRTimeVmxControlsMissing ReturnCode = 0x1019 ReturnCodeRTimeVmxEnableFails ReturnCode = 0x101A // TCP/Winsock error codes ReturnCodeWsaeTimedOut ReturnCode = 0x274C ReturnCodeWsaeConnRefused ReturnCode = 0x274D ReturnCodeWsaeHostDown ReturnCode = 0x2751 )
func (ReturnCode) Error ¶
func (rc ReturnCode) Error() string
Error implements the error interface for ReturnCode, allowing it to be used directly as an error.
func (ReturnCode) String ¶
func (rc ReturnCode) String() string
String returns a human-readable description of the ADS return code. For unknown codes, it returns the hex value.
type Session ¶ added in v2.1.0
type Session struct {
// contains filtered or unexported fields
}
Session is the managed wrapper around one ADS *Client: symbol cache, persistent notifications, lifecycle FSM, auto-reconnect and route state. It does NOT embed *Client -- raw RPCs live on a *Client from Dial.
func NewSession ¶ added in v2.1.0
func NewSession(ctx context.Context, remote AMSEndpoint, opts ...SessionOption) (sess *Session, err error)
NewSession creates a session targeting remote. No I/O until Connect, except to resolve an incomplete remote.AMS over UDP -- the port then follows the TC major version, so several runtimes means setting it explicitly. Local NetID and AMS port default so sessions cannot collide on the PLC's handle table.
func (*Session) AddRoute ¶ added in v2.1.0
AddRoute registers a route on the remote PLC using this connection's settings. It uses callbackIP (from WithHostIP) if set, otherwise derives the callback address from the source AMS NetID (first 4 bytes = IP).
func (*Session) AddSymbolNotification ¶ added in v2.1.0
func (sess *Session) AddSymbolNotification(ctx context.Context, symbolName string, maxDelay time.Duration, cycleTime time.Duration, transMode TransMode, updateReceiver chan *Update) (uint32, error)
AddSymbolNotification subscribes a single symbol. All notifications on one connection share the same channel, and a duplicate symbol is rejected; the stored channel is reused to re-subscribe after a reconnect. Prefer AddSymbolNotifications for more than one.
The caller MUST NOT close updateReceiver while any notification is active -- a recover guards against it, but samples are silently dropped. Delete them or Close first.
func (*Session) AddSymbolNotifications ¶ added in v2.1.0
func (sess *Session) AddSymbolNotifications(ctx context.Context, configs []NotificationConfig, ch chan *Update) ([]SumNotificationResult, error)
AddSymbolNotifications subscribes several symbols in one round-trip, returning results parallel to configs; a non-nil error means the batch never went at all. Partial outcomes are normal. Skipped != nil means the library did not commit it (match the ErrNotification* sentinels), otherwise Error is the PLC's verdict and NoErrors means Handle is valid. Do not close ch while notifications are active.
func (*Session) BrowseSymbols ¶ added in v2.1.0
func (sess *Session) BrowseSymbols(path string) ([]SymbolBrowseEntry, error)
BrowseSymbols returns browsable entries at the given path in the symbol hierarchy. If path is empty, returns root-level groupings (first path segments). If path is specified, returns children of that symbol or prefix. Requires LoadSymbolList() or LoadSymbols() to have been called first.
func (*Session) CheckSymbolVersion ¶ added in v2.1.0
CheckSymbolVersion compares the current PLC symbol version against the stored version. Returns true if the version has changed.
func (*Session) Close ¶ added in v2.1.0
Close releases PLC-side notifications and symbol handles, cancels the session context, closes the socket and waits for the workers. Idempotent: the teardown runs once and a repeat call only re-waits finished workers. Implements io.Closer.
func (*Session) Connect ¶ added in v2.1.0
Connect dials the PLC and transitions to Connected. May bind a listening socket on the AMS port unasked, since some devices answer only on a connection they open to us. A failed Connect rolls back and stays usable for a retry. Not safe for concurrent use on one Session.
func (*Session) DeleteDeviceNotification ¶ added in v2.1.0
DeleteDeviceNotification on Session wraps the raw Client RPC with notifications.lock cleanup: removes the entry from activeNotifications, drops the cached notificationConfig, and clears notificationChannel when the last subscription dies. Callers that want raw delete behavior use the Client method directly.
func (*Session) GetSymbol ¶ added in v2.1.0
GetSymbol returns a read-only SymbolView for the named symbol. Resolves on-demand if the symbol is not in the cache (single-symbol lookup against the PLC).
func (*Session) IsClosed ¶ added in v2.1.0
IsClosed reports whether the session has reached the terminal Closed state. A closed session cannot be reused; construct a new one via NewSession. Distinct from IsDisconnected (transient transport loss).
func (*Session) IsDisconnected ¶ added in v2.1.0
IsDisconnected returns whether the connection is currently in a disconnected state.
func (*Session) ListSymbols ¶ added in v2.1.0
func (sess *Session) ListSymbols() (map[string]SymbolView, error)
ListSymbols returns the full symbol table. Requires LoadSymbols() or LoadSymbolsSlow() to have been called first. Returns an error if full discovery has not been performed.
ListSymbols returns read-only SymbolViews for every symbol discovered via LoadSymbols/LoadSymbolsSlow. Keys are PLC-cased FullNames.
func (*Session) LoadDataTypes ¶ added in v2.1.0
func (sess *Session) LoadDataTypes(ctx context.Context, cfg SlowDiscoveryConfig) error
LoadDataTypes downloads only the datatype table (0xF00E) from the PLC in chunks. After calling this along with LoadSymbolList(), struct/array children can be browsed and expanded via BrowseSymbols().
func (*Session) LoadSymbolList ¶ added in v2.1.0
func (sess *Session) LoadSymbolList(ctx context.Context, cfg SlowDiscoveryConfig) error
LoadSymbolList downloads only the symbol table (0xF00B) from the PLC in chunks. This is the smaller of the two tables and enables browsing top-level symbol names. After calling this, BrowseSymbols() can list root symbols and navigate by prefix. To also expand struct/array children, call LoadDataTypes() afterwards.
func (*Session) LoadSymbols ¶ added in v2.1.0
LoadSymbols performs full symbol and datatype discovery from the PLC. After calling this, ListSymbols() returns all symbols, and struct/array children are available. Write operations with type aliases also work. This downloads the entire symbol and datatype tables in single requests, which may cause real-time jitter on the PLC. For large programs, consider LoadSymbolsSlow() instead.
func (*Session) LoadSymbolsSlow ¶ added in v2.1.0
func (sess *Session) LoadSymbolsSlow(ctx context.Context, cfg SlowDiscoveryConfig) error
LoadSymbolsSlow downloads the full symbol table in chunks with delays between each chunk, to minimize disruption to the PLC's real-time task. If the PLC does not support offset-based chunked reads, it falls back to downloading each table in a single request with a delay between them.
func (*Session) ReadFromSymbol ¶ added in v2.1.0
ReadFromSymbol reads a PLC symbol by name and returns its stringified value (handle resolved on-demand and cached).
func (*Session) ReadMultipleSymbols ¶ added in v2.1.0
func (sess *Session) ReadMultipleSymbols(ctx context.Context, names []string) (map[string]string, error)
ReadMultipleSymbols reads several symbols in one round-trip, returning a map of name to parsed value for those that succeeded. Failures are named in a *BatchError (errors.As), and the map stays usable -- one absent symbol in forty leaves thirty-nine present. Any other error means the transport failed and no outcome is known. Reading none returns nil, nil.
func (*Session) Reconnect ¶ added in v2.1.0
Reconnect re-establishes the transport, reloads symbols and re-subscribes. Cancelling ctx CLOSES the session, as exhausting the attempt limit does -- not a pause: Reconnecting has no exit to Disconnected, so a session left there would never retry and IsClosed() could not see it.
func (*Session) RefreshSymbols ¶ added in v2.1.0
RefreshSymbols reloads the symbol table if the symbol version has changed. It releases old handles, reloads symbol/datatype tables, and re-acquires handles for active symbols.
func (*Session) RuntimeState ¶ added in v2.3.0
RuntimeState reads the device's ADS state from the system service port. This is the SYSTEM's state, not the runtime port's: ADSStateConfig means no runtime port is serving. Answers while the runtime is unavailable, which is the point.
func (*Session) SumDeleteDeviceNotification ¶ added in v2.1.0
func (sess *Session) SumDeleteDeviceNotification(ctx context.Context, handles []uint32) ([]ReturnCode, error)
SumDeleteDeviceNotification wraps the raw RPC with activeNotifications cleanup, returning the per-handle codes. On a partial result the codes processed so far are still flushed and both the partial slice and the error are returned, so in-memory state matches what the PLC saw rather than leaving phantom entries for the next reconnect to re-clean.
func (*Session) WriteMultipleSymbols ¶ added in v2.1.0
func (sess *Session) WriteMultipleSymbols(ctx context.Context, values map[string]string) (map[string]ReturnCode, error)
WriteMultipleSymbols writes several symbols in one round-trip, returning a map of name to per-symbol code for those that reached the PLC.
Do not read success from the map alone: NoErrors is 0, so a symbol that was never written is indistinguishable from one that succeeded. Failures are named in a *BatchError (errors.As), which says whether the PLC rejected it or the library never sent it; any other error means the transport failed and no outcome is known. Writing none returns nil, nil.
type SessionOption ¶ added in v2.1.0
type SessionOption func(*Session)
SessionOption configures optional parameters for NewSession.
func WithAmsPeerListen ¶ added in v2.3.0
func WithAmsPeerListen(port int) SessionOption
WithAmsPeerListen makes the session listen for a connection the PLC opens back to us and use it for responses. Needed for devices that treat a registered route as a peer router: they process our requests on our connection but answer over one they open to us. Without it such a device looks like it times out on everything, because the responses reach a socket nobody is listening on.
port is normally 48898, parameterised for tests and hosts already running a TwinCAT router. Off by default, since binding is not a client library's business unless asked; binding failures surface from Connect rather than silently.
func WithAutoReconnect ¶ added in v2.1.0
func WithAutoReconnect(enabled bool) SessionOption
WithAutoReconnect controls whether the connection automatically reconnects when the TCP connection drops. Default is true. When disabled, triggerReconnect sets the transport-down flag but does not launch a reconnect goroutine. Pending and subsequent RPCs return ErrDisconnected. The caller must call Reconnect() manually to re-establish the connection.
func WithBackoff ¶ added in v2.1.0
func WithBackoff(cfg BackoffConfig) SessionOption
WithBackoff sets the reconnect backoff configuration. Invalid configs are rejected at option-application time: a Warn is logged and the default is kept. Callers wanting hard validation can call cfg.Validate() before passing.
func WithForceRouteRegistration ¶ added in v2.1.0
func WithForceRouteRegistration() SessionOption
WithForceRouteRegistration skips probing and registers on every Connect and Reconnect, for environments where routes are not persistent. The cost is one UDP registration per attempt on a flapping link, against a router this library has seen go mute under duplicate entries -- freshness traded for route-table safety. Without it a session registers once, plus one healing registration per cooldown.
func WithHeartbeatRecovery ¶ added in v2.3.1
func WithHeartbeatRecovery(mode HeartbeatRecovery) SessionOption
WithHeartbeatRecovery selects what happens when the heartbeat goes silent. The default, Immediate, re-subscribes at once -- a delete plus an add per handle, 82 requests on a 41-symbol session, against a device that may have merely stalled. Confirm waits for a second silent window, doubling the time to notice but halving needless churn. Observe never re-subscribes and reports it instead. An unrecognised value keeps the default: a typo must not turn recovery off.
func WithHostIP ¶ added in v2.1.0
func WithHostIP(ip string) SessionOption
WithHostIP sets the IP address the PLC should use to reach this client. Required in Docker/VPN/NAT scenarios where the local TCP socket address differs from the externally routable IP. When set, AddRoute uses this IP as the callback address instead of deriving it from the AMS NetID.
func WithLocalAMS ¶ added in v2.2.0
func WithLocalAMS(local AMSAddress) SessionOption
WithLocalAMS sets the local (source) AMSAddress in outgoing ADS headers. NetID defaults to the local TCP source IP, Port to a random dynamic-range value. The AMS port is a logical id in the header, not the TCP source or destination port.
HAZARD with route registration: TC3 keys its table by ADDRESS, and two entries for one address take the router out of service for every client until it is cleared by hand. Safe: a NetID matching the address, or WithSkipRouteRegistration. Avoid two sessions from one host under different ones.
func WithLocalBindIP ¶ added in v2.2.0
func WithLocalBindIP(ip string) SessionOption
WithLocalBindIP forces the outbound TCP source IP; unset lets the OS pick. Used on hosts with IP aliases so each Session looks like a separate host and gets its own TCP slot -- TwinCAT allows one per source IP regardless of NetID (Beckhoff #49/#72). The alias must exist before Connect. An invalid IP is rejected at option time with a Warn, leaving OS-default routing.
func WithLocalMode ¶ added in v2.2.0
func WithLocalMode() SessionOption
WithLocalMode targets the in-process TwinCAT runtime at 127.0.0.1, used when the application runs on the same machine as the PLC runtime. Sets the local-mode flag that Connect uses to short-circuit the route probe and force the loopback target NetID 127.0.0.1.1.1.
func WithLogger ¶
func WithLogger(logger *slog.Logger) SessionOption
WithLogger sets the logger for the Session and the underlying Client. If not provided, slog.Default() is used.
func WithMaxReconnectAttempts ¶ added in v2.1.0
func WithMaxReconnectAttempts(n int) SessionOption
WithMaxReconnectAttempts limits total TCP reconnection attempts before giving up. Default is 0 (infinite retries). When the limit is reached, the reconnect goroutine returns an error and the connection stays in disconnected state.
func WithMaxSymbolVersionReloadAttempts ¶ added in v2.1.0
func WithMaxSymbolVersionReloadAttempts(n int) SessionOption
WithMaxSymbolVersionReloadAttempts caps reload attempts under SymbolVersionAutoReload within a sliding window. Default: 3. n<1 is rejected (logged Warn, default kept).
Note: unlike WithMaxReconnectAttempts (where n=0 means infinite), reload attempts are intentionally bounded — runaway reload loops would hammer the PLC under recurring online-change conditions.
func WithNotificationHeartbeat ¶ added in v2.3.0
func WithNotificationHeartbeat(interval time.Duration, missed int) SessionOption
WithNotificationHeartbeat tunes the internal cyclic notification that detects subscriptions dying silently. A subscription can stop delivering with nothing observable happening, and an on-change subscription may be silent legitimately, so only a cyclic beat's absence is conclusive.
interval is the cycle time, missed how many beats may be lost before re-subscribing. Defaults 2s and 5; missed < 2 is raised to 2. See WithNotificationSilenceTimeout, WithHeartbeatRecovery and WithRuntimeStateWatch.
func WithNotificationSilenceTimeout ¶ added in v2.3.1
func WithNotificationSilenceTimeout(d time.Duration) SessionOption
WithNotificationSilenceTimeout says how long subscriptions may be silent before the session calls them dead, in wall-clock time -- the same decision as WithNotificationHeartbeat's missed argument, in the unit an operator thinks in. missed is derived from it at construction (rounded up, floored at 2), and whichever of the two options is applied later wins.
func WithOnDisconnect ¶ added in v2.1.0
func WithOnDisconnect(fn func()) SessionOption
WithOnDisconnect registers a callback invoked when a disconnect is detected. The callback runs in a separate goroutine and must not block.
func WithOnReconnect ¶ added in v2.1.0
func WithOnReconnect(fn func()) SessionOption
WithOnReconnect registers a callback invoked after a successful reconnect. The callback runs in a separate goroutine and must not block.
func WithOnSymbolVersionChanged ¶ added in v2.1.0
func WithOnSymbolVersionChanged(fn func(reason Reason)) SessionOption
WithOnSymbolVersionChanged registers a callback fired once per stale-cache detection. The reason argument is one of the documented enumerated values on Update.Reason (symbol-version-invalid, invalid-size, ...). Callback runs in its own goroutine — do NOT block.
Under SymbolVersionIgnore strategy this callback is the only signal for symbol-removed events: the dead handle's user channel goes silent (no terminal Update). Surviving sibling handles still receive a one-shot Stale=true Update; only the removed symbol's channel is mute.
func WithRequestTimeout ¶ added in v2.1.0
func WithRequestTimeout(d time.Duration) SessionOption
WithRequestTimeout overrides the per-request timeout for ADS commands and initial-dial timeout. Defaults to the value passed as requestTimeout in NewSession (or 5s if that was zero). Useful for slow PLCs or networks where a single command may legitimately take longer than the default.
Note: this option is also used as the net.DialTimeout for initial Connect and reconnect dial. A single value covers both ADS request and TCP dial semantics — split if you need different deadlines.
func WithRoute ¶ added in v2.1.0
func WithRoute(routeName, username, password string) SessionOption
WithRoute registers an AMS route during Connect, over UDP 48899 once the source NetID is derived and before any ADS command. Connect and Reconnect probe first and register only if that fails; WithForceRouteRegistration always registers.
Security: Beckhoff's protocol sends credentials in cleartext and offers no encrypted alternative. Trusted networks only.
func WithRouteActivationTimeout ¶ added in v2.3.0
func WithRouteActivationTimeout(d time.Duration) SessionOption
WithRouteActivationTimeout caps how long Connect waits for a freshly registered route to actually be served: the router acks the registration before the entry is live, and until then requests are dropped with no reply. Default 10s covers every PLC observed. Values <= 0 are ignored, and a deadline on Connect's context also bounds the wait, so this is only needed to wait longer.
func WithRuntimeStateWatch ¶ added in v2.3.1
func WithRuntimeStateWatch(d time.Duration) SessionOption
WithRuntimeStateWatch sets how often the session polls the system service for the runtime state. That reading is what lets symbol and subscription calls refuse with "the runtime is not running" instead of failing obscurely, and lets a session starting in CONFIG come up and wait. Default 5s, independent of the heartbeat interval.
func WithSkipRouteRegistration ¶ added in v2.2.0
func WithSkipRouteRegistration() SessionOption
WithSkipRouteRegistration disables route registration, for routes managed externally -- pre-registered on the PLC, or owned by a local AmsRouterDaemon. Equivalent to omitting WithRoute but explicit, so a caller can keep WithRoute for documentation and override here. Bypasses probe and AddRoute, so no UDP at all.
func WithStrictReconnect ¶ added in v2.1.0
func WithStrictReconnect(maxAttempts int) SessionOption
WithStrictReconnect makes reconnection fail if any previously-resolved on-demand symbol is no longer available on the PLC (e.g., after an online change). By default, missing symbols are skipped gracefully during reconnect. maxAttempts controls how many reconnect attempts are allowed before giving up:
- 0 = fail immediately on first missing symbol
- N > 0 = retry up to N times, then return error (connection closes)
func WithSymbolVersionReloadWindow ¶ added in v2.1.0
func WithSymbolVersionReloadWindow(d time.Duration) SessionOption
WithSymbolVersionReloadWindow sets the sliding window for reload-attempt counting. Default: 60s. d<=0 is rejected (logged Warn, default kept).
func WithSymbolVersionStrategy ¶ added in v2.1.0
func WithSymbolVersionStrategy(s SymbolVersionStrategy) SessionOption
WithSymbolVersionStrategy selects the online-change handling strategy. Default: SymbolVersionAutoReload (zero-value, applies if option not set).
Values outside the SymbolVersionAutoReload/Close/Ignore enumeration are rejected at option-application time: a warning is logged and the strategy falls back to AutoReload. The strategy controls what handleStaleDetection does when a stale-cache return code (0x711, 0x705, 0x710, 0x704, 0x703, 0x702) is observed.
func WithTargetCheck ¶ added in v2.3.0
func WithTargetCheck(c TargetCheck) SessionOption
WithTargetCheck sets what happens when the target NetID disagrees with what the device reports. Default TargetCheckWarn. Runs in Connect for one UDP round-trip, and only for a caller-supplied target -- an incomplete one is resolved from the device, which is authoritative. A device that does not answer identify is never a mismatch in any mode: a firewalled UDP port says nothing about the address.
func WithoutAmsPeerFallback ¶ added in v2.3.0
func WithoutAmsPeerFallback() SessionOption
WithoutAmsPeerFallback disables the automatic peer-listener fallback, which otherwise binds the AMS port when a Connect proves the PLC answers nothing at all (see WithAmsPeerListen). The fallback only binds for a session that would be dead anyway. Use this where a TwinCAT router already owns the port, or where a client process may not listen.
func WithoutNotificationHeartbeat ¶ added in v2.3.0
func WithoutNotificationHeartbeat() SessionOption
WithoutNotificationHeartbeat disables the heartbeat described in WithNotificationHeartbeat.
The cost it saves: one notification handle in the PLC's table per session, and one small cyclic sample per interval. The cost it accepts: a runtime restart or CONFIG toggle leaves this session's subscriptions dead permanently, with no error and nothing in the session's state to show it — the consumer has to notice the absence of data and rebuild the session itself.
func WithoutRuntimeStateWatch ¶ added in v2.3.1
func WithoutRuntimeStateWatch() SessionOption
WithoutRuntimeStateWatch turns the runtime-state poll off, saving one small request per interval. In exchange the gates fall back to permitting, so a PLC in CONFIG fails the old obscure way instead of saying the runtime is not running, and a session starting in CONFIG will not notice the return to RUN. Connect still does one synchronous read.
type SessionState ¶ added in v2.1.0
type SessionState uint32
SessionState is the explicit FSM state for a Session. See docs/archive/specs/09-fsm-design.md for the full state diagram, event taxonomy, and transition table.
The FSM is the source of truth for closed and reconnecting concerns. The transport-down flag (disconnected on *transport) is a finer-grained signal than the FSM state — it flips false the instant dialAndStart reattaches a live socket, even while the FSM is still Reconnecting.
const ( SessionStateConstructed SessionState = iota SessionStateConnecting SessionStateConnected SessionStateReloading SessionStateDisconnected SessionStateReconnecting SessionStateClosed )
func (SessionState) String ¶ added in v2.1.0
func (s SessionState) String() string
type SlowDiscoveryConfig ¶
type SlowDiscoveryConfig struct {
// ChunkSize is the number of bytes to download per request.
// Default: 4096 bytes.
ChunkSize uint32
// ChunkDelay is the delay between chunk requests, giving the PLC
// time to handle its real-time tasks. Default: 100ms.
ChunkDelay time.Duration
}
SlowDiscoveryConfig configures chunked symbol table download.
type StaleInfo ¶ added in v2.2.0
type StaleInfo struct {
Reason Reason
}
StaleInfo describes why a one-shot Stale Update was delivered. Non-nil iff the corresponding Update's value MAY be from a pre-online-change cache state. Reason is one of the documented Reason* constants.
type StampHeader ¶
StampHeader prefixes the samples that share a single PLC sample timestamp. Timestamp is in Windows FILETIME ticks (100 ns since 1601-01-01 UTC). Samples is the count of NotificationSample records that follow.
type SumNotificationRequest ¶
type SumNotificationRequest struct {
Group uint32
Offset uint32
Length uint32
TransmissionMode TransMode
MaxDelay time.Duration
CycleTime time.Duration
}
SumNotificationRequest represents a single notification add request within a batch.
type SumNotificationResult ¶ added in v2.1.0
type SumNotificationResult struct {
Handle uint32
Error ReturnCode // PLC-side return code; valid only when Skipped == nil
Skipped error // non-nil if library skipped this entry; Error/Handle not meaningful
}
SumNotificationResult is a per-item result from SumAddDeviceNotification. Either Skipped is non-nil (library refused to send this entry — duplicate, resolution failure, transport-aborted batch) or Skipped is nil and Error carries the PLC-side return code. Handle is valid only when Skipped == nil AND Error == ReturnCodeNoErrors.
type SumReadRequest ¶
SumReadRequest represents a single read request within a sum/batch read.
type SumReadResult ¶
type SumReadResult struct {
Error ReturnCode
Data []byte
}
SumReadResult represents the result of a single read within a sum/batch read.
type SumWriteRequest ¶
SumWriteRequest represents a single write request within a sum/batch write.
type SumWriteResult ¶
type SumWriteResult struct {
Error ReturnCode
}
SumWriteResult represents the result of a single write within a sum/batch write.
type SymbolBrowseEntry ¶
type SymbolBrowseEntry struct {
Name string // short name (e.g., "motor")
FullName string // full path (e.g., "MAIN.motor")
DataType string // type name (e.g., "ST_Motor", "INT")
Size uint32
HasChildren bool // true if struct/array (requires LoadDataTypes to expand)
Comment string
}
SymbolBrowseEntry represents a browsable symbol or child.
type SymbolFlag ¶ added in v2.1.0
type SymbolFlag uint32
SymbolFlag represents bits in the symbol flags field returned by INFOBYNAMEEX (0xF009) and the bulk symbol upload. These flags control how extended symbol info is parsed and which notification modes are available.
const ( // SymbolFlagPersistent indicates the symbol value survives PLC restarts. SymbolFlagPersistent SymbolFlag = 0x0001 // SymbolFlagBitValue indicates the symbol is a single bit within a byte. SymbolFlagBitValue SymbolFlag = 0x0002 // SymbolFlagReferenceTo indicates the symbol is a reference/pointer. SymbolFlagReferenceTo SymbolFlag = 0x0004 // SymbolFlagTypeGuid indicates a 16-byte type GUID follows after the name/type/comment strings. SymbolFlagTypeGuid SymbolFlag = 0x0008 // SymbolFlagTComObj indicates the symbol is a TcCOM object. SymbolFlagTComObj SymbolFlag = 0x0010 // SymbolFlagReadOnly indicates the symbol is read-only. SymbolFlagReadOnly SymbolFlag = 0x0020 // SymbolFlagContextMask extracts the PLC task index from bits 8-11: non-zero // binds the variable to a task and enables the InContext modes, zero means they // are rejected (0x070B on TC3) or silently ignored (TC2). Only variables local // to a PROGRAM POU in a multi-task project are non-zero. SymbolFlagContextMask SymbolFlag = 0x0F00 // SymbolFlagAttributes indicates attribute key-value pairs follow after the type GUID. SymbolFlagAttributes SymbolFlag = 0x1000 // SymbolFlagExtendedFlags indicates additional extended flags are present. SymbolFlagExtendedFlags SymbolFlag = 0x8000 )
func (SymbolFlag) ContextMask ¶ added in v2.1.0
func (f SymbolFlag) ContextMask() uint8
ContextMask extracts the PLC task context index from symbol flags (bits 8-11). Returns 0 if the symbol is not bound to a specific task.
func (SymbolFlag) Has ¶ added in v2.1.0
func (f SymbolFlag) Has(flag SymbolFlag) bool
Has returns true if the flag set contains the given flag(s).
type SymbolUploadDataType ¶
type SymbolUploadDataType struct {
DatatypeEntry datatypeEntry
Name string
DataType string
Comment string
Children map[string]*SymbolUploadDataType
}
type SymbolUploadInfo ¶
type SymbolVersionStrategy ¶ added in v2.1.0
type SymbolVersionStrategy uint8
SymbolVersionStrategy selects online-change handling behavior (R-SES-011).
const ( // SymbolVersionAutoReload is the default. On detection of an online // change, the Session re-runs symbol discovery and resubscribes // notifications. Bounded by WithMaxSymbolVersionReloadAttempts within // a sliding window (default 3 attempts / 60s). SymbolVersionAutoReload SymbolVersionStrategy = iota // SymbolVersionClose terminates the Session on detection. The // OnDisconnect callback fires and Session.Close() is invoked. The // caller decides reconnect timing. SymbolVersionClose // SymbolVersionIgnore surfaces the PLC error verbatim and flags the next sample // on surviving handles Stale, once. Removed symbols go SILENT instead -- no // terminal Update for a dead handle, so use WithOnSymbolVersionChanged to see // removals. SymbolVersionIgnore )
func (SymbolVersionStrategy) String ¶ added in v2.1.0
func (s SymbolVersionStrategy) String() string
String returns the human-readable name of the strategy.
type SymbolView ¶ added in v2.1.0
type SymbolView struct {
Name string
FullName string
DataType string
Comment string
Handle uint32
Group uint32
Offset uint32
Length uint32
BaseType ADSDataType // protocol ADST_ code; see adsTypeToString
Flags SymbolFlag
ContextMask uint8 // PLC task context (bits 8-11 of Flags); 0 = no task binding
Parsed bool // true if Value has been parsed at least once at snapshot time
IsRoot bool // true if this symbol has no parent (top-level program/global var)
Value string
// contains filtered or unexported fields
}
SymbolView is a read-only snapshot of a symbol's metadata and cached value, captured under cache.lock at creation. It does not track later updates -- call GetSymbol again or subscribe for fresh data.
The snapshot model buys lock-free field access and internal consistency (Parsed and Value captured together), at the cost of going stale after a concurrent reload. IsValid() is false for the zero value. Children() and ChildrenWalk() release the lock before calling the iterator, so any Session method is safe inside a walk.
func (SymbolView) BaseTypeName ¶ added in v2.1.0
func (v SymbolView) BaseTypeName() string
BaseTypeName returns the IEC 61131-3 primitive underlying this symbol, for consumers that need the storage layout without tracking user-defined names. Resolved from the protocol ADST_ code, else the datatype table, else inferred from size -- inference is limited to 1- and 2-byte widths, since 4 and 8 are REAL/LREAL ambiguous.
Returns "" when none resolve -- no symbol has "" legitimately, so callers branch on it -- and warns once per symbol naming the remedy. Never fetches the table itself: a getter with no ctx and no error has no business doing I/O.
func (SymbolView) Children ¶ added in v2.1.0
func (v SymbolView) Children() map[string]SymbolView
Children returns SymbolViews for struct/array members captured at view creation time. Returns nil for scalars and for symbols whose subtree has not been populated by full discovery. The map is freshly allocated per call; callers can mutate it without affecting library state.
Each child is a freshly-snapshotted SymbolView (lookups walk the cache under cache.lock once per call, then release). Caller code in a walk loop is free to call any Session method - no lock held.
func (SymbolView) ChildrenWalk ¶ added in v2.1.0
func (v SymbolView) ChildrenWalk(fn func(SymbolView) bool)
ChildrenWalk visits every symbol in the subtree rooted at this view in depth-first order. Snapshots the entire subtree under cache.lock once, releases the lock, then invokes fn for each entry. fn is free to call any Session method (Value field reads are lock-free, GetSymbol etc. take their own locks - no deadlock risk).
Walk terminates early if fn returns false.
func (SymbolView) GetJSON ¶ added in v2.2.0
func (v SymbolView) GetJSON() string
GetJSON serializes the cached value: nested JSON walked from the children subtree for composites, a single literal for primitives. Returns "" for a detached view or a symbol no longer in the cache. Takes cache.lock briefly; safe from any goroutine.
func (SymbolView) IsValid ¶ added in v2.1.0
func (v SymbolView) IsValid() bool
IsValid reports whether the view is backed by a live connection. Zero-value SymbolView returns false; views obtained from GetSymbol/ListSymbols return true (until the connection is closed).
type TargetCheck ¶ added in v2.3.0
type TargetCheck int
TargetCheck selects what NewSession does when the device at the target IP reports a different NetID than the caller supplied. Set with WithTargetCheck.
const ( // TargetCheckWarn logs a warning and continues. Default, because a mismatch // is not proof of a mistake: pointed at a router, the NetID you want // legitimately belongs to a device behind it. TargetCheckWarn TargetCheck = iota + 1 // TargetCheckError refuses to construct the session. Right for a deployment // that talks straight to its PLCs, where a mismatch is always a // misconfiguration and failing at startup beats a session that connects and // then answers nothing. TargetCheckError // TargetCheckOff skips the check, and with it the one UDP round-trip it // costs. Also the way to stay silent on a host that is deliberately // addressed through a router. TargetCheckOff )
type TransMode ¶
type TransMode uint32
TransMode is the transmission mode for an ADS device notification — how the PLC decides when to deliver a sample to the subscriber.
const ( TransModeNoTransmission TransMode = 0 TransModeClientCycle TransMode = 1 TransModeClientOnChange TransMode = 2 TransModeServerCycle TransMode = 3 TransModeServerOnChange TransMode = 4 // The "InContext" variants run the notification check inside the PLC task cycle // rather than a separate ADS thread, for more deterministic timing. They need a // non-zero ContextMask (the owning task), which only variables local to a // PROGRAM POU in a multi-task project have -- GVLs and single-task projects are // always 0, and TC3 then rejects with 0x070B while TC2 silently never fires. // The AddSymbolNotification paths fall back automatically when it is 0. TransModeServerCycle2 TransMode = 5 // CyclicInContext TransModeServerOnChange2 TransMode = 6 // OnChangeInContext TransModeClient1Request TransMode = 10 )
type Update ¶
Update is delivered to the user channel for each PLC notification sample. Stale is non-nil only on the first sample following a stale-cache detection (R-NOT-017 one-shot); the field is nil for normal samples. Couples the "this sample may be stale" signal with the reason in a single check — callers do `if u.Stale != nil { /* handle stale */ }`.