High-Level Control Layer (xdm1000_ctrl.py)
xdm1000_ctrl.py provides a high-level control class for operating the XDM1000 / XDM1241 as measurement instruments. It does not simply expose the low-level SCPI API.
The current control layer requires the instrument state to be configured explicitly before calling meas() or meas_continue(). The ctrl layer caches the measurement mode, range, internal measurement speed, and continuous-measurement settings, and reflects them in shared state on the worker-process side.
Separation of Responsibilities
SCPI Layer
xdm1000_scpi.py is a low-level SCPI wrapper.
Its main responsibilities are:
Open serial.Serial
Write/query SCPI commands
Query *IDN?
Provide thin wrappers for FUNCtion / RANGE / RATE / MEAS1?, etc.
ctrl Layer
xdm1000_ctrl.py provides the user-facing API.
Its main responsibilities are:
Map measurement modes to ctrl API names
Maintain mode, range, auto-ranging, and internal measurement speed
Provide separate entry points for single and continuous measurements
Maintain continuous-measurement settings
Manage RPC to the worker in process-separated mode
Store measurement results in the result store
Worker Layer
When use_process=True, xdm1000_worker.py holds the SCPI instance in a child process.
Its main responsibilities are:
Receive commands from the parent via the command queue
Own the SCPI object
Reflect instrument status in shared state
Store single- and continuous-measurement results in the result store
Main Public APIs
The main public APIs of the current Xdm1000Ctrl are:
close()
disconnect()
get_state()
is_running()
idn()
set_mode(mode)
set_range(value=None, index=None, range_value=None)
set_auto_range(enabled)
set_integration_speed(speed)
set_continue_interval(interval)
set_continue_duration(duration)
set_continue_count(count)
meas(wait=None)
meas_continue(wait=None)
get_result(clear=False)
wait_until_done(poll_interval=0.2)
The following are legacy APIs and are not present in the current Xdm1000Ctrl:
stop_job()
get_continuous_result(clear=False)
get_statistics_result(clear=False)
set_meas_mode(kind, range_value="AUTO", rate="F")
meas_one_shot(kind=None, range_value=None, rate=None, settle=0.0)
meas_voltage_dc(range_value=None, rate=None, settle=0.0)
meas_current_dc(range_value=None, rate=None, settle=0.0)
start_continuous_meas(kind=None, duration=0.0, range_value=None, rate=None, interval=0.0, settle=0.0)
start_statistics_mode(kind=None, duration=0.0, range_value=None, rate=None, settle=0.0)
remote(), local(), query(), and write() are not exposed. They belong to the SCPI layer.
use_process=True
With use_process=True, the parent process receives user API calls, while the child worker process handles SCPI communication.
Xdm1000Ctrl
├── Parent Process
│ ├── command_q
│ ├── response_q
│ ├── Manager().dict() state
│ ├── Manager().dict() result_store
│ ├── Event stop_event
│ └── Lock rpc_lock
└── Worker Process
└── _Xdm1000Worker
└── Xdm1000Scpi
└── serial.Serial
Only the worker process owns the serial port. The parent process does not execute SCPI operations directly.
The parent-side meas_continue() sends a start request to the worker and returns after receiving {"job_id": "..."}. The measurement loop continues in the worker, and results are retrieved using get_result().
use_process=False
With use_process=False, Xdm1000Ctrl creates its own Xdm1000Scpi instance. SCPI commands execute directly in the same process.
Although the API name is still meas_continue(), the call runs synchronously and blocks the caller. After completion, it returns {"job_id": "..."}.
Initialization
Xdm1000Ctrl(
port=None,
timeout=2.0,
baudrate=115200,
logger=None,
log_level=None,
config=None,
use_process=True,
)
When config is supplied, its PORT, TIMEOUT, and BAUDRATE values take precedence. A missing final port raises ValueError.
With use_process=True, initialization creates mp.Manager(), command and response queues, a stop event, an RPC lock, and a worker process.
With use_process=False, initialization creates Xdm1000Scpi and calls remote().
state
get_state() returns the current state as a dictionary.
With use_process=True, process_alive is added to the shared dictionary. With use_process=False, process_alive=False is added to the direct-mode state.
The principal keys are:
worker_alive
process_alive
port
baudrate
running
job_id
job_type
started_at
finished_at
error
traceback
message
args
mode
range_value
auto_range
integration_speed
continue_interval
continue_duration
continue_count
mode, range_value, auto_range, integration_speed, continue_interval, continue_duration, and continue_count are configuration values cached by the ctrl layer.
Restrictions While Running
The following APIs cannot be called while a measurement is running:
idn()
set_mode()
set_range()
set_auto_range()
set_integration_speed()
set_continue_interval()
set_continue_duration()
set_continue_count()
meas()
meas_continue()
Calling them while running raises DeviceBusyError.
The following APIs remain available while running:
get_state()
is_running()
get_result()
wait_until_done()
The current implementation does not provide stop_job(). Continuous measurement stops when continue_duration or continue_count is reached.
Measurement Modes
The ctrl API mode names map to SCPI FUNCtion parameters as follows:
voltage_dc -> VOLT
voltage_ac -> VOLT AC
current_dc -> CURR
current_ac -> CURR AC
resistance -> RES
fresistance -> FRES
frequency -> FREQ
period -> PER
capacitance -> CAP
temperature_rtd -> TEMP
diode -> DIOD
continuity -> CONT
An unsupported mode raises ValueError.
Manual Range
set_range(value=None, index=None, range_value=None) selects a manual range for the current measurement mode.
value specifies a physical quantity. A numeric value uses the base unit of the current mode: V for voltage, A for current, Ohm for resistance, and F for capacitance.
dmm.set_mode("voltage_dc")
dmm.set_range(value=5.0)
dmm.set_range(value="500 mV")
dmm.set_range(value="500mV")
index directly selects a discrete SCPI RANGE index.
dmm.set_range(index=3)
Do not specify value, index, and range_value together. range_value is a legacy-compatible alias for value.
Internal Measurement Speed
set_integration_speed(speed) sets the SCPI RATE.
Accepted values are:
F
M
S
Lowercase input is converted to uppercase. Other values raise ValueError.
integration_speed is the instrument’s internal measurement speed, not the sample interval of a continuous measurement. Use set_continue_interval() to set that interval.
Single Measurement
meas(wait=None) reads MEAS1? once using the current settings.
set_mode() must be called before meas().
The result can also be retrieved with get_result().
{
"type": "single",
"mode": "voltage_dc",
"range_value": 10,
"auto_range": False,
"integration_speed": "F",
"value": 1.234,
"host_time_start": 1710000000.0,
"host_time_end": 1710000000.02,
"read_elapsed": 0.02,
}
Continuous Measurement
meas_continue(wait=None) performs continuous measurement using the current settings.
At least one of set_continue_duration() and set_continue_count() must be configured before meas_continue(). If both values are None, it raises ValueError.
The interval argument of set_continue_interval(interval) must be nonnegative. With zero, samples are read as quickly as possible.
The duration argument of set_continue_duration(duration) must be None or positive.
The count argument of set_continue_count(count) must be None or a positive integer.
With use_process=True, a successful start returns {"job_id": ...} and the result is stored in result_store["result"].
A continuous-measurement result has the following form:
{
"type": "continuous",
"job_id": "...",
"ok": True,
"mode": "voltage_dc",
"range_value": 10,
"auto_range": False,
"integration_speed": "F",
"continue_interval": 0.1,
"continue_duration": 10.0,
"continue_count": None,
"stats": {
"count": 100,
"minimum": 1.23,
"maximum": 1.25,
"average": 1.24,
"stdev_population": 0.01
},
"started_at": 1710000000.0,
"finished_at": 1710000010.0,
"samples": [
{
"index": 0,
"value": 1.234,
"host_time_start": 1710000000.0,
"host_time_end": 1710000000.02,
"read_elapsed": 0.02,
"t_rel_start": 0.0,
"t_rel_end": 0.02
}
]
}
In direct mode (use_process=False), the continuous result does not include stats; it includes count instead.
Retrieving Results
get_result(clear=False) returns the most recent single- or continuous-measurement result.
With clear=True, the stored result is deleted after retrieval.
get_continuous_result() and get_statistics_result() are not current APIs. Use get_result() for all results.
Transparent FastAPI
The transparent APIs exposed by server_fastapi/spec.py are:
get_state
is_running
idn
set_mode
set_range
set_auto_range
set_integration_speed
set_continue_interval
set_continue_duration
set_continue_count
meas
meas_continue
get_result
wait_until_done
All use object_name="xdm1000" and HTTP method post.