API#
The top level pandablocks module contains a number of packages that can be used from code:
pandablocks.commands
: The control commands that can be sent to a PandApandablocks.responses
: The control and data responses that will be receivedpandablocks.connections
: Control and data connections that implements the parsing logicpandablocks.asyncio
: An asyncio client that uses the control and data connectionspandablocks.blocking
: A blocking client that uses the control and data connectionspandablocks.hdf
: Some helpers for efficiently writing data responses to diskpandablocks.utils
: General utility methods for use with pandablocks
Commands#
There is a Command
subclass for every sort of command that can be sent to
the ControlConnection
of a PandA. Many common actions can be accomplished
with a simple Get
or Put
, but some convenience commands like
GetBlockInfo
, GetFieldInfo
, etc. are provided that parse output into
specific classes.
- class pandablocks.commands.Command[source]#
Abstract baseclass for all ControlConnection commands to be inherited from
- exception pandablocks.commands.CommandException[source]#
Raised if a
Command
receives a mal-formed response
- class pandablocks.commands.Raw(inp: List[str])[source]#
Send a raw command
- Parameters:
inp – The input lines to send
For example:
Raw(["PCAP.ACTIVE?"]) -> ["OK =1"] Raw(["SEQ1.TABLE>", "1", "1", "0", "0", ""]) -> ["OK"] Raw(["SEQ1.TABLE?"]) -> ["!1", "!1", "!0", "!0", "."])
- class pandablocks.commands.Get(field: str)[source]#
Get the value of a field or star command.
If the form of the expected return is known, consider using
GetLine
orGetMultiline
instead.- Parameters:
field – The field, attribute, or star command to get
For example:
Get("PCAP.ACTIVE") -> "1" Get("SEQ1.TABLE") -> ["1048576", "0", "1000", "1000"] Get("*IDN") -> "PandA 1.1..."
- class pandablocks.commands.GetLine(field: str)[source]#
Get the value of a field or star command, when the result is expected to be a single line.
- Parameters:
field – The field, attribute, or star command to get
For example:
GetLine("PCAP.ACTIVE") -> "1" GetLine("*IDN") -> "PandA 1.1..."
- class pandablocks.commands.GetMultiline(field: str)[source]#
Get the value of a field or star command, when the result is expected to be a multiline response.
- Parameters:
field – The field, attribute, or star command to get
For example:
GetMultiline("SEQ1.TABLE") -> ["1048576", "0", "1000", "1000"] GetMultiline("*METADATA.*") -> ["LABEL_FILTER1", "APPNAME", ...]
- class pandablocks.commands.Put(field: str, value: str | List[str] = '')[source]#
Put the value of a field.
- Parameters:
field – The field, attribute, or star command to put
value – The value, either string or list of strings or empty, to put
For example:
Put("PCAP.TRIG", "PULSE1.OUT") Put("SEQ1.TABLE", ["1048576", "0", "1000", "1000"]) Put("SFP3_SYNC_IN1.SYNC_RESET")
- class pandablocks.commands.Append(field: str, value: List[str])[source]#
Append the value of a table field.
- Parameters:
field – The field, attribute, or star command to append
value – The value, list of strings, to append
For example:
Append("SEQ1.TABLE", ["1048576", "0", "1000", "1000"])
- class pandablocks.commands.Disarm[source]#
Disarm PCAP, stopping acquisition by sending
*PCAP.DISARM=
- class pandablocks.commands.GetBlockInfo(skip_description: bool = False)[source]#
Get the name, number, and description of each block type in a dictionary, alphabetically ordered
- Parameters:
skip_description – If
True
, prevents retrieving the description for each Block. This will reduce network calls.
For example:
GetBlockInfo() -> { "LUT": BlockInfo(number=8, description="Lookup table"), "PCAP": BlockInfo(number=1, description="Position capture control"), ... }
- class pandablocks.commands.GetFieldInfo(block: str, extended_metadata: bool = True)[source]#
Get the fields of a block, returning a
FieldInfo
(or appropriate subclass) for each one, ordered to match the definition order in the PandA- Parameters:
For example:
GetFieldInfo("LUT") -> { "INPA": BitMuxFieldInfo(type='bit_mux', subtype=None, description='Input A', max_delay=5 label=['TTLIN1.VAL', 'TTLIN2.VAL', ...]), ...}
- class pandablocks.commands.GetPcapBitsLabels[source]#
Get the labels for the bit fields in PCAP.
For example:
GetPcapBitsLabels() -> {"BITS0" : ["TTLIN1.VAL", "TTLIN2.VAL", ...], ...}
- class pandablocks.commands.ChangeGroup(value, names=None, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]#
Which group of values to ask for
*CHANGES
on: https://pandablocks-server.readthedocs.io/en/latest/commands.html#system-commands- ALL = ''#
All the groups below
- CONFIG = '.CONFIG'#
Configuration settings
- BITS = '.BITS'#
Bits on the system bus
- POSN = '.POSN'#
Positions
- READ = '.READ'#
Polled read values
- ATTR = '.ATTR'#
Attributes (included capture enable flags)
- TABLE = '.TABLE'#
Table changes
- METADATA = '.METADATA'#
Table changes
- class pandablocks.commands.GetChanges(group: ChangeGroup = ChangeGroup.ALL, get_multiline: bool = False)[source]#
Get a
Changes
object showing which fields have changed since the last time this was called- Parameters:
group – Restrict to a particular
ChangeGroup
get_multiline – If
True
, return values of multiline fields in themultiline_values
attribute. Note that this will invoke additional network requests. IfFalse
these fields will instead be returned in theno_value
attribute. Default value isFalse
.
For example:
GetChanges() -> Changes( value={"PCAP.TRIG": "PULSE1.OUT"}, no_value=["SEQ1.TABLE"], in_error=["BAD.ENUM"], multiline_values={} ) GetChanges(ChangeGroup.ALL, True) -> Changes( values={"PCAP.TRIG": "PULSE1.OUT"}, no_value=[], in_error=["BAD.ENUM"], multiline_values={"SEQ1.TABLE" : ["1", "2", "3",...]} )
- class pandablocks.commands.GetState[source]#
Get the state of all the fields in a PandA that should be saved as a list of raw lines that could be sent with
SetState
.NOTE:
GetState
may behave unexpectedly ifGetChanges
has previously been called using the same client. The caller should use separate clients to avoid potential issues.For example:
GetState() -> [ "SEQ1.TABLE<B" "234fds0SDklnmnr" "" "PCAP.TRIG=PULSE1.OUT", ]
Responses#
Classes used in responses from both the ControlConnection
and
DataConnection
of a PandA live in this package.
- class pandablocks.responses.BlockInfo(number: int = 0, description: str | None = None)[source]#
Block number and description as exposed by the TCP server
- class pandablocks.responses.FieldInfo(type: str, subtype: str | None, description: str | None)[source]#
Field type, subtype, description and labels as exposed by TCP server: https://pandablocks-server.readthedocs.io/en/latest/fields.html#field-types
Note that many fields will use a more specialised subclass of FieldInfo for their additional attributes.
- labels#
A list of the valid values for the field when there is a defined list of valid values, e.g. those with sub-type “enum”
- class pandablocks.responses.UintFieldInfo(type: str, subtype: str | None, description: str | None, max_val: int)[source]#
Extended
FieldInfo
for fields with type “param”,”read”, or “write” and subtype “uint
- class pandablocks.responses.ScalarFieldInfo(type: str, subtype: str | None, description: str | None, units: str, scale: float, offset: float)[source]#
Extended
FieldInfo
for fields with type “param”,”read”, or “write” and subtype “scalar
- class pandablocks.responses.TimeFieldInfo(type: str, subtype: str | None, description: str | None, units_labels: List[str], min_val: float)[source]#
Extended
FieldInfo
for fields with type “time
- class pandablocks.responses.SubtypeTimeFieldInfo(type: str, subtype: str | None, description: str | None, units_labels: List[str])[source]#
Extended
FieldInfo
for fields with type “param”,”read”, or “write” and subtype “time
- class pandablocks.responses.EnumFieldInfo(type: str, subtype: str | None, description: str | None, labels: List[str])[source]#
Extended
FieldInfo
for fields with type “param”,”read”, or “write” and subtype “enum
- class pandablocks.responses.BitOutFieldInfo(type: str, subtype: str | None, description: str | None, capture_word: str, offset: int)[source]#
Extended
FieldInfo
for fields with type “bit_out
- class pandablocks.responses.BitMuxFieldInfo(type: str, subtype: str | None, description: str | None, max_delay: int, labels: List[str])[source]#
Extended
FieldInfo
for fields with type “bit_mux
- class pandablocks.responses.PosMuxFieldInfo(type: str, subtype: str | None, description: str | None, labels: List[str])[source]#
Extended
FieldInfo
for fields with type “pos_mux
- class pandablocks.responses.TableFieldDetails(subtype: str, bit_low: int, bit_high: int, description: str | None = None, labels: List[str] | None = None)[source]#
Info for each field in a table
- class pandablocks.responses.TableFieldInfo(type: str, subtype: str | None, description: str | None, max_length: int, fields: Dict[str, TableFieldDetails], row_words: int)[source]#
Extended
FieldInfo
for fields with type “table
- class pandablocks.responses.PosOutFieldInfo(type: str, subtype: str | None, description: str | None, capture_labels: List[str])[source]#
Extended
FieldInfo
for fields with type “pos_out
- class pandablocks.responses.ExtOutFieldInfo(type: str, subtype: str | None, description: str | None, capture_labels: List[str])[source]#
Extended
FieldInfo
for fields with type “ext_out” and subtypes “timestamp” or “samples
- class pandablocks.responses.ExtOutBitsFieldInfo(type: str, subtype: str | None, description: str | None, capture_labels: List[str], bits: List[str])[source]#
Extended
ExtOutFieldInfo
for fields with type “ext_out” and subtype “bits
- class pandablocks.responses.Changes(values: Dict[str, str], no_value: List[str], in_error: List[str], multiline_values: Dict[str, List[str]])[source]#
The changes returned from a
*CHANGES
command
- class pandablocks.responses.EndReason(value, names=None, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]#
The reason that a PCAP acquisition completed
- OK = 'Ok'#
Experiment completed by falling edge of
PCAP.ENABLE`
- EARLY_DISCONNECT = 'Early disconnect'#
Client disconnect detected
- DATA_OVERRUN = 'Data overrun'#
Client not taking data quickly or network congestion, internal buffer overflow. In raw unscaled mode (i.e., no server-side scaling), the most recent
FrameData
is likely corrupted.
- FRAMING_ERROR = 'Framing error'#
Triggers too fast for configured data capture
- DRIVER_DATA_OVERRUN = 'Driver data overrun'#
Probable CPU overload on PandA, should not occur
- DMA_DATA_ERROR = 'DMA data error'#
Data capture too fast for memory bandwidth
- UNKNOWN_EXCEPTION = 'Unknown exception'#
An unknown exception occurred during HDF5 file processing
- START_DATA_MISMATCH = 'Start Data mismatched'#
StartData packets did not match when trying to continue printing to a file
- MANUALLY_STOPPED = 'Manually stopped'#
Experiment manually completed by
DATA:CAPTURE
- DISARMED = 'Disarmed'#
Experiment manually completed by
*PCAP.DISARM=
command
- class pandablocks.responses.FieldCapture(name: str, type: dtype, capture: str, scale: float = 1.0, offset: float = 0.0, units: str = '')[source]#
Information about a field that is being captured
- type#
Numpy data type of the field as transmitted
- Type:
- class pandablocks.responses.Data[source]#
Baseclass for all responses yielded by a
DataConnection
- class pandablocks.responses.ReadyData[source]#
Yielded once when the connection is established and ready to take data
- class pandablocks.responses.StartData(fields: List[FieldCapture], missed: int, process: str, format: str, sample_bytes: int)[source]#
Yielded when a new PCAP acquisition starts.
- fields#
Information about each captured field as a
FieldCapture
object- Type:
- class pandablocks.responses.FrameData(data: ndarray)[source]#
Yielded when a new data frame is flushed.
- data#
A numpy
Structured Array
- Type:
Data is structured into complete columns. Each column name is
<name>.<capture>
from the correspondingFieldInfo
. Data can be accessed with these column names. For example:# Table view with 2 captured fields >>> import numpy >>> data = numpy.array([(0, 10), ... (1, 11), ... (2, 12)], ... dtype=[('COUNTER1.OUT.Value', '<f8'), ('COUNTER2.OUT.Value', '<f8')]) >>> fdata = FrameData(data) >>> fdata.data[0] # Row view (0., 10.) >>> fdata.column_names # Column names ('COUNTER1.OUT.Value', 'COUNTER2.OUT.Value') >>> fdata.data['COUNTER1.OUT.Value'] # Column view array([0., 1., 2.])
Connections#
Sans-IO connections for both the Control and Data ports of PandA TCP server.
- exception pandablocks.connections.NeedMoreData[source]#
Raised if the
Buffer
isn’t full enough to return the requested bytes
- exception pandablocks.connections.NoContextAvailable[source]#
Raised if there were no contexts available for this connection. This may result from calling
ControlConnection.receive_bytes()
without callingControlConnection.send()
, or if there were unmatched sends/receives
- class pandablocks.connections.Buffer[source]#
Byte storage that provides line reader and bytes reader interfaces. For example:
buf = Buffer() buf += bytes_from_server line = buf.read_line() # raises NeedMoreData if no line for line in buf: pass bytes = buf.read_bytes(50) # raises NeedMoreData if not enough bytes
- read_bytes(num: int) bytearray [source]#
Read and pop num bytes from the beginning of the buffer, raising
NeedMoreData
if the buffer isn’t full enough to do so
- peek_bytes(num: int) bytearray [source]#
Read but do not pop num bytes from the beginning of the buffer, raising
NeedMoreData
if the buffer isn’t full enough to do so
- read_line()[source]#
Read and pop a newline terminated line (without terminator) from the beginning of the buffer, raising
NeedMoreData
if the buffer isn’t full enough to do so
- class pandablocks.connections.ControlConnection[source]#
Sans-IO connection to control port of PandA TCP server, supporting a Command based interface. For example:
cc = ControlConnection() # Connection says what bytes should be sent to execute command to_send = cc.send(command) socket.sendall(to_send) while True: # Repeatedly process bytes from the PandA received = socket.recv() # Sending any subsequent bytes to be sent back to the PandA to_send = cc.receive_bytes(received) socket.sendall(to_send) # And processing the produced responses for command, response in cc.responses() do_something_with(response)
- receive_bytes(received: bytes) bytes [source]#
Tell the connection that you have received some bytes off the network. Parse these into high level responses which are yielded back by
responses
. Return any bytes to send back
- class pandablocks.connections.DataConnection[source]#
Sans-IO connection to data port of PandA TCP server, supporting an flushable iterator interface. For example:
dc = DataConnection() # Single connection string to send to_send = dc.connect() socket.sendall(to_send) while True: # Repeatedly process bytes from the PandA looking for data received = socket.recv() for data in dc.receive_bytes(received): do_something_with(data)
Asyncio Client#
This is an asyncio
wrapper to the ControlConnection
and DataConnection
objects, allowing async calls to send(command)
and iterate over
data()
.
- class pandablocks.asyncio.AsyncioClient(host: str)[source]#
Asyncio implementation of a PandABlocks client. For example:
async with AsyncioClient("hostname-or-ip") as client: # Control port is now connected resp1, resp2 = await asyncio.gather(client.send(cmd1), client.send(cmd2)) resp3 = await client.send(cmd3) async for data in client.data(): handle(data) # Control and data ports are now disconnected
- is_connected()[source]#
True if there is a currently active connection. NOTE: This does not indicate if the remote end is still connected.
- async send(command: Command[T], timeout: float | None = None) T [source]#
Send a command to control port of the PandA, returning its response.
- Parameters:
command – The
Command
to send
- async data(scaled: bool = True, flush_period: float | None = None, frame_timeout: float | None = None) AsyncGenerator[Data, None] [source]#
Connect to data port and yield data frames
- Parameters:
scaled – Whether to scale and average data frames, reduces throughput
flush_period – How often to flush partial data frames, None is on every chunk of data from the server
frame_timeout – If no data is received for this amount of time, raise
asyncio.TimeoutError
Blocking Client#
This is a blocking wrapper to the ControlConnection
and DataConnection
objects, allowing blocking calls to send(commands)
and iterate over
data()
.
- class pandablocks.blocking.BlockingClient(host: str)[source]#
Blocking implementation of a PandABlocks client. For example:
with BlockingClient("hostname-or-ip") as client: # Control port is now connected resp1, resp2 = client.send([cmd1, cmd2]) resp3 = client.send(cmd3) for data in client.data(): handle(data) # Control and data ports are now disconnected
- send(commands: Command[T], timeout: int | None = None) T [source]#
- send(commands: Iterable[Command], timeout: int | None = None) List
Send a command to control port of the PandA, returning its response.
- Parameters:
commands – If single
Command
, return its response. If a list of commands return a list of reponsestimeout – If no reponse in this time, raise
socket.timeout
HDF Writing#
This package contains components needed to write PCAP data to and HDF file
in the most efficient way. The oneshot write_hdf_files
is exposed in the
commandline interface. It assembles a short Pipeline
of:
The FrameProcessor and HDFWriter run in their own threads as most of the heavy lifting is done by numpy and h5py, so running in their own threads gives multi-CPU benefits without hitting the limit of the GIL.
- class pandablocks.hdf.Pipeline[source]#
Helper class that runs a pipeline consumer process in its own thread
- class pandablocks.hdf.HDFWriter(file_names: Iterator[str])[source]#
Write an HDF file per data collection. Each field will be written in a 1D dataset
/<field.name>.<field.capture>
.- Parameters:
file_names – Iterator of file names. Must be full file paths. Will be called once per file created.
- class pandablocks.hdf.FrameProcessor[source]#
Scale field data according to the information in the StartData
- exception pandablocks.hdf.HDFDataOverrunException[source]#
Raised if
DATA_OVERRUN
occurs while receiving data for HDF file
- pandablocks.hdf.create_pipeline(*elements: Pipeline) List[Pipeline] [source]#
Create a pipeline of elements, wiring them and starting them before returning them
- pandablocks.hdf.create_default_pipeline(file_names: Iterator[str], *additional_downstream_pipelines: Pipeline) List[Pipeline] [source]#
Create the default processing pipeline consisting of one
FrameProcessor
and oneHDFWriter
. Seecreate_pipeline
for more details.- Parameters:
file_names – Iterator of file names. Must be full file paths. Will be called once per file created. As required by
HDFWriter
.additional_downstream_pipelines – Any number of additional pipelines to add downstream.
- pandablocks.hdf.stop_pipeline(pipeline: List[Pipeline])[source]#
Stop and join each element of the pipeline
- async pandablocks.hdf.write_hdf_files(client: AsyncioClient, file_names: Iterator[str], num: int = 1, arm: bool = False, flush_period: float = 1)[source]#
Connect to host PandA data port, and write num acquisitions to HDF file according to scheme
- Parameters:
client – The
AsyncioClient
to use for communicationsfile_names – Iterator of file names. Must be full file paths. Will be called once per file created.
num – The number of acquisitions to store in separate files. 0 = Infinite capture
arm – Whether to arm PCAP at the start, and after each successful acquisition
- Raises:
HDFDataOverrunException – if there is a data overrun.
Utilities#
This package contains general methods for working with pandablocks.