Skip to content

Writing to Devices

Write operations allow setting scalars, arrays, raw bytes, alarm configuration, and device control.

Authorization Required

Writing to devices requires authentication and proper permissions. Writes without auth raise AuthenticationError.


Simple API

The quickest way to write — uses the global backend:

import pacsys

# Console class writes — just need a Kerberos ticket (kinit)
pacsys.write("Z:ACLTST", 45.0)

# Role-based writes — configure role first
pacsys.configure(role="testing")
pacsys.write("Z:ACLTST", 45.0)

# Batch
results = pacsys.write_many([
    ("Z:ACLTST", 45.0),
    ("G:AMANDA", 1.0),
])

The global DPM/HTTP backend auto-creates lazy Kerberos auth — credentials are only validated on the first write. For explicit auth, use pacsys.configure(auth=pacsys.KerberosAuth()).


Authentication Setup

DPM/HTTP - Kerberos

import pacsys

auth = pacsys.KerberosAuth()   # Default principal from `kinit`

# Specific principal (e.g., multiple tickets in cache)
auth = pacsys.KerberosAuth(name="operator@FNAL.GOV")

with pacsys.dpm(auth=auth, role="testing") as backend:
    result = backend.write("Z:ACLTST", 45.0)

auth is required. role is optional — console class writes work without it, but role-based device access requires it (e.g., role="testing").

DPM/gRPC - JWT

import pacsys

auth = pacsys.JWTAuth(token="eyJ...")   # Or set PACSYS_JWT_TOKEN env var

with pacsys.grpc(auth=auth) as backend:
    result = backend.write("Z:ACLTST", 45.0)

DMQ - Kerberos

import pacsys

auth = pacsys.KerberosAuth()

with pacsys.dmq(auth=auth) as backend:
    result = backend.write("Z:ACLTST", 45.0)

DMQ requires Kerberos for all operations (reads too), but no role is needed for writes.


Writing Values

Scalar

result = backend.write("Z:ACLTST", 45.0)

if result.success:
    print("Write successful")
else:
    print(f"Write failed: [{result.facility_code},{result.error_code}] {result.message}")

WriteResult has:

Field Description
drf The DRF that was written
success True if error_code == 0
facility_code ACNET facility code
error_code 0 = success, <0 = error
message Error message (if failed)

String

result = backend.write("Z:STRINGTEST", "hello")

Raw Bytes

Write unscaled binary data directly:

# DEC F_float representation of 45.0
result = backend.write("Z:ACLTST.SETTING.RAW", b"\x34\x43\x00\x00")

Use the .RAW field suffix to bypass scaling transformations.

Array

import numpy as np

result = backend.write("Z:ACLTS1[0:10]", np.array([1.0, 2.0, 3.0]))
result = backend.write("Z:ACLTS1[0:10]", [1.0, 2.0, 3.0])   # list also works

DPM/HTTP accepts one-dimensional arrays that are all numeric or all strings; anything else raises TypeError before I/O.


Batch Writes

Write multiple devices in a single operation:

results = backend.write_many([
    ("Z:ACLTST", 45.0),
    ("G:AMANDA", 1.0),
])

for result in results:
    if not result.success:
        print(f"Failed: {result.drf} - {result.message}")

All devices are written in the same request if possible. Results are returned in the same order as the input list.


Implicit Property Conversion

When writing, PACSys automatically converts read properties to their writable counterparts and forces the @N (never) event:

Input Wire request Why
Z:ACLTST Z:ACLTST.SETTING@N READING → SETTING
Z:ACLTST.READING Z:ACLTST.SETTING@N READING → SETTING
Z_ACLTST Z:ACLTST.SETTING@N _ qualifier = SETTING
Z:ACLTST@p,1000 Z:ACLTST.SETTING@N Event replaced with @N
Z|ACLTST Z:ACLTST.CONTROL@N | qualifier = STATUS → CONTROL
Z&ACLTST Z:ACLTST.CONTROL@N & qualifier = CONTROL
Z@ACLTST.MAX Z:ACLTST.ANALOG.MAX@N ANALOG alarm field

The @N event tells the server this is a fire-and-confirm operation, not a subscription.


Device Control (on/off/reset)

Use BasicControl enum values to send control commands:

from pacsys import BasicControl

# Turn device on/off
backend.write("Z&ACLTST", BasicControl.ON)
backend.write("Z|ACLTST", BasicControl.ON)  # STATUS → CONTROL auto-conversion
backend.write("Z|ACLTST", BasicControl.OFF)
pacsys.write("Z:ACLTST", BasicControl.ON)   # module-level: bare name routed to CONTROL

# Other control commands
backend.write("Z|ACLTST", BasicControl.RESET)
backend.write("Z|ACLTST", BasicControl.POSITIVE)
backend.write("Z|ACLTST", BasicControl.NEGATIVE)
backend.write("Z|ACLTST", BasicControl.RAMP)
backend.write("Z|ACLTST", BasicControl.DC)

Backend write() does not retarget

backend.write("Z:ACLTST", BasicControl.ON) on a bare name is a SETTING write of the enum ordinal. Use a &/| qualifier or .CONTROL. Module-level writes, acput, and Device.control() route BasicControl values for you.

Control Commands Are Sequential

Each BasicControl value is one command; e.g. turning a device on and setting its polarity takes two writes.

See Device Status for reading back status after control writes.


Alarm Configuration Writes

Individual Fields

Write a single alarm field:

# Analog alarm
backend.write("Z@ACLTST.MAX", 50.0)        # Set maximum limit
backend.write("Z@ACLTST.MIN", 40.0)        # Set minimum limit
backend.write("Z@ACLTST.ALARM_ENABLE", 1)  # Enable alarm
backend.write("Z@ACLTST.ABORT_INHIBIT", 1) # Set abort inhibit (bypass)

# Digital alarm
backend.write("Z$ACLTST.NOM", 0x0001)      # Set nominal bit pattern
backend.write("Z$ACLTST.MASK", 0x00FF)     # Set mask

Dict Shortcut

Use DPM/HTTP (pacsys.dpm()) to write multiple alarm fields by passing a dict:

# Analog alarm - set multiple fields sequentially
backend.write("Z@ACLTST", {
    "minimum": 40.0,
    "maximum": 50.0,
    "alarm_enable": True,
    "abort_inhibit": False,
})

# Digital alarm
backend.write("Z$ACLTST", {
    "nominal": 0x0001,
    "mask": 0x00FF,
    "alarm_enable": True,
})

Allowed keys for analog alarms: minimum, maximum, alarm_enable, abort_inhibit, tries_needed.

Allowed keys for digital alarms: nominal, mask, alarm_enable, abort_inhibit, tries_needed.

Unknown and read-only keys (abort, alarm_status, tries_now) raise ValueError. Boolean values are converted to 0/1 automatically.

The dict is expanded to sequential per-field writes, each a server-side read-modify-write of the shared alarm block. It is not atomic: if a later field fails, earlier changes stay applied.

gRPC returns a failed WriteResult for alarm dicts. DMQ is unverified (the reference server rejects structured alarm writes).

For read-modify-write patterns, use the alarm helpers:

from pacsys.alarm_block import AnalogAlarm

with AnalogAlarm.modify("Z:ACLTST", backend=backend) as alarm:
    alarm.maximum = 50.0
    alarm.minimum = 40.0
    alarm.bypass = False

See Alarm Helpers for the full API.


Write Verification (Device API)

Write verification is available via the Device API and works with all backends:

from pacsys import Device, Verify

dev = Device("Z:ACLTST", backend=backend)
result = dev.write(45.0, verify=Verify(tolerance=0.1))
print(result.confirmed)  # True if the write succeeded and readback matched

Backend write() methods do not accept verify or tolerance parameters.

acput --verify supports settings, basic control commands (verified through STATUS), and single alarm fields such as Z:ACLTST.ANALOG.NOM. Any other target, such as a whole alarm block or an ACNET RAW field, rejects the whole command before writing.


Error Handling

import pacsys

try:
    result = backend.write("Z:ACLTST", 45.0)
    if not result.success:
        print(f"Write rejected: {result.message}")
except pacsys.AuthenticationError as e:
    print(f"Auth failed: {e}")

Partial Failures in Batch Writes

results = backend.write_many([
    ("Z:ACLTST", 45.0),
    ("Z:NOTFND", 1.0),    # This device doesn't exist
])

# First succeeds, second fails
assert results[0].success
assert not results[1].success
print(f"Failed: {results[1].error_code}")

Common Errors

Situation Exception / Result
No auth configured AuthenticationError("not configured for authenticated")
Dict write to non-alarm DRF ValueError("Cannot write dict to READING property")
Dict write to STATUS/CONTROL ValueError pointing to BasicControl enum
Device not writable WriteResult.success == False

Backend Differences

Feature DPM/HTTP gRPC DMQ
Auth type Kerberos + role JWT Kerberos (no role)
Alarm dict write Yes (sequential, not atomic) Unsupported Unverified; reference server rejects

See Also