目标: 连接Bluesky和EPICS过程变量
基础
在Bluesky中,ophyd包用于连接底层控制系统(此处以EPICS为例)。其基本结构是EpicsSignal,它将单个EPICS过程变量(PV)与单个Python对象相连接。在此场景后面, 该连接在底层通过PyEpics包及EPICS通道访问协议实现。其他信息(如单位、限值、显示精度、数据类型、枚举标签等)在可用时,也会从EPICS获取。
|---------------|--------------------------------------|
| ophyd类 | 描述 |
| EpicsSignal | 与一个EPICS PV建立一个读取/写入连接, 在后台监视和更新这个对象 |
| EpicsSignalRO | EpicsSignal的只读版本 |
注意: EpicsSignal和EpicsSignalRO有超出此处描写外的很多其它配置特性. 更多细节见ophyd文档.
EpicsSignal
从ophyd导入支持开始
python
In [7]: from ophyd import EpicsSignal
有一个EPICS过程变量(PV)MotorVM:m1_able可以与之连接。该PV代表单个比特位。我们用一个名为bit1的Python对象与之连接(为了方便起见,我们设置了一个ioc前缀变量,以防有人使用的EPICS IOC具有不同的前缀)。
除了EPICS PV外,还必须添加一个name关键字,以便构造具有对象名称的Python对象。按照惯例,其名称应与等号左侧的名称保持一致。
python
In [1]: from ophyd import EpicsSignal
In [2]: ioc = "MotorVM:"
In [3]: bit1= EpicsSignal(f"{ioc}m1_able", name="bit1")
在创建连接后,我们立即测试了 Python 对象是否已与 EPICS 完全连接(通常这需要短暂时间,并非瞬时完成)。
python
In [4]: print(f"{bit1.connected = }")
bit1.connected = True
当人工在 Jupyter notebook 中进行交互时,EPICS PV 的连接通常会在移动到下一个单元格并执行它的时间内完成。但是,当这些 PV 连接由单个程序执行且需要立即使用该对象时,则可能需要等待连接完成才能继续执行后续操作。
python
In [5]: bit1.wait_for_connection()
In [6]: print(f"{bit1.connected = }")
bit1.connected = True
使用这个Python对象的get()方法打印其值:
python
In [7]: print(f"{bit1.get() = }")
bit1.get() = 0
该过程变量可能关联着其两种不同取值所对应的标签。可通过对象的 enum_strs 属性查看这些标签:
python
In [8]: print(f"{bit1.enum_strs = }")
bit1.enum_strs = ('Enable', 'Disable')
我们用数值(0或1)或用文本(Enable, Disable)设置这个对象, 并且我们可以用数值或文本显示这个对象:
python
In [9]: bit1.put(1)
In [10]: print(f"{bit1.get() = }")
bit1.get() = 1
In [11]: print(f"{bit1.get(as_string=False) = }")
bit1.get(as_string=False) = 1
In [12]: print(f"{bit1.get(as_string=True) = }")
bit1.get(as_string=True) = 'Disable'
In [13]: bit1.put("Enable")
In [14]: print(f"{bit1.get() = }")
bit1.get() = 0
In [15]: print(f"{bit1.get(as_string=False) = }")
bit1.get(as_string=False) = 0
In [16]: print(f"{bit1.get(as_string=True) = }")
bit1.get(as_string=True) = 'Enable'
通过在创建这个连接时添加string=True关键字,我们也可以使得文本表示成为默认:
python
In [17]: bit1 = EpicsSignal(f"{ioc}m1_able", name="bit1", string=True)
In [18]: bit1.get()
Out[18]: 'Enable'
read()方法(由数据采集使用)显示了从EPICS接收到的值和时间戳. 这个时间戳被Python记录并且是从1970年1月1日GMT以来的绝对秒数.
python
In [19]: bit1.read()
Out[19]: {'bit1': {'value': 'Enable', 'timestamp': 1766728047.551385}}
EpicsSignalRO
gp:UPTIME PV告诉我们IOC已经运行了多少时间. 这个PV包含我们不能更改的信息, 因为它报告了仅IOC知道的值. 用来自ophyd的EpicsSignalRO创建一个uptime对象.
python
from ophyd import EpicsSignalRO
uptime = EpicsSignalRO(f"{ioc}UPTIME", name="uptime")
uptime.wait_for_connection()
print(f"{uptime.get() = }")
Device和Component
我们可能拥有一组在某种程度上有关联的过程变量。可以将这些 Python 对象组织成一个更大的结构,称为 ophyd.Device,其中每个 EpicsSignal 对象都是一个 ophyd.Component。接下来导入相应的 ophyd 结构:
python
In [2]: from ophyd import Component, Device
构建一个结构体, 它关联这些PVs并且使用:
|------------------|----------|----------------|
| PV | 如何使用它 | 属性 |
| gp:gp:bit1 | on/off控制 | enable |
| gp:gp:float1 | 目标温度 | setpoint |
| gp:gp:float1.EGU | 温度单位 | setpoint_units |
| gp:gp:text1 | 短标签 | label |
要将这些PV组合在一起,必须创建一个自定义的Python类,该类以Device作为基类,并将每个PV定义为Component。
为了使该类更有用,我们从我们的类省略IOC前缀(即第一个gp:)。当我们为这个结构体创建Pythno对象时, 我们将使用这个IOC前缀。
python
In [5]: class MyGroup(Device):
...: setPoint = Component(EpicsSignal, "gp:float1")
...: units = Component(EpicsSignal, "gp:float1.EGU")
...: label = Component(EpicsSignal, "gp:text1")
...: enable = Component(EpicsSignal, "gp:bit1")
...:
使用这个结构体(一次性)连接所有PVs, 使用与以上EpicsSignal相同的命令类型.
python
In [11]: thing = MyGroup(prefix=ioc, name="thing")
In [12]: thing.wait_for_connection()
使用read()方法一次性显示所有值.
python
In [10]: thing.read()
Out[10]:
OrderedDict([('thing_setPoint', {'value': 0.0, 'timestamp': 631152000.0}),
('thing_units', {'value': 'Degree', 'timestamp': 631152000.0}),
('thing_label',
{'value': 'Hello World', 'timestamp': 631152000.0}),
('thing_enable', {'value': 0, 'timestamp': 631152000.0})])
设置温度单位和设置点:
python
In [13]: thing.setPoint.put(123.45)
In [14]: thing.units.put("Rankine")
In [15]: thing.read()
Out[15]:
OrderedDict([('thing_setPoint',
{'value': 123.45, 'timestamp': 1766760933.393842}),
('thing_units',
{'value': 'Rankine', 'timestamp': 1766760933.393842}),
('thing_label',
{'value': 'Hello World', 'timestamp': 631152000.0}),
('thing_enable', {'value': 0, 'timestamp': 631152000.0})])
python
In [17]: print(f"set point: {thing.setPoint.get():.2f} {thing.units.get()}")
set point: 123.45 Rankine
Device结构体可以被嵌套, 产生更复杂的结构体. 例如:
python
...: basic = Component(MyGroup, "")
...: reading = Component(EpicsSignal, "gp:float2")
...: abstract = Component(EpicsSignal, "gp:text2", string=True)
...:
In [19]: stack = Stack(ioc, name="stack")
In [20]: stack.wait_for_connection()
In [21]: stack.read()
Out[21]:
OrderedDict([('stack_basic_setPoint',
{'value': 123.45, 'timestamp': 1766760933.393842}),
('stack_basic_units',
{'value': 'Rankine', 'timestamp': 1766760933.393842}),
('stack_basic_label',
{'value': 'Hello World', 'timestamp': 631152000.0}),
('stack_basic_enable', {'value': 0, 'timestamp': 631152000.0}),
('stack_reading', {'value': 0.0, 'timestamp': 631152000.0}),
('stack_abstract',
{'value': 'Hello World', 'timestamp': 631152000.0})])
以上教程使用了一个软IOC db, 如下:
python
record(bo, "gp:gp:bit1") {
field(ZNAM, "OFF")
field(ONAM, "ON")
}
record(bo, "gp:gp:bit2") {
field(ZNAM, "OFF")
field(ONAM, "ON")
}
record(ai, "gp:gp:float1") {
field(EGU, "Degree")
}
record(ao, "gp:gp:float2") {
field(EGU, "Degree")
}
record(stringin, "gp:gp:text1") {
field(VAL, "Hello World")
}
record(stringout, "gp:gp:text2") {
field(VAL, "Hello World")
}
创建虚拟环境:
python
conda create -n XXXX python=3.11 -c conda-forge