bluesky - Plans

计划 是Bluesky中描述实验流程的概念。计划可以是任何可迭代对象(如列表、元组、自定义可迭代类等),但最常见通过Python生成器实现。有关技术细节的讨论,请参阅消息协议章节。

我们提供了多种预置计划。就像熟食店菜单上的三明治一样,您可以直接使用这些预置计划,也可以利用下方Stub计划部分列出的基础组件自行组合构建。

注意: 在以下示例中,我们将假设您已有一个名为 RE 的运行引擎实例。如果您是在已部署Bluesky的实验设施中工作的用户,该实例可能已为您预先配置。请参阅本教程的此章节以确认是否已拥有运行引擎,或在需要时了解如何快速创建。

预置计划

在此汇总表格下方,我们将按类别分项详解这些计划,并提供配图示例。

概要

|--------------------|--------------------------------------|
| count | 从探测器获取一个或多个读数 |
| scan | 沿单个多电机轨迹进行扫描。 |
| rel_scan | 相对于当前位置沿单个多电机轨迹进行扫描。 |
| list_scan | 同步步进扫描一个或多个变量(内积扫描法)。 |
| rel_list_scan | 相对当前位置步进扫描一个变量 |
| list_grid_scan | 网格扫描, 每个电机在一个独立轨迹上. |
| rel_list_grid_scan | 网格扫描,, 每个电机相对于当前位置在一个独立轨迹上. |
| log_scan | 以对数间隔的步长扫描一个变量。 |
| rel_log_scan | 相对于当前位置以对数间隔的步长扫描一个变量。 |
| grid_scan | 网格扫描, 每个电机在一个独立轨迹上. |
| rel_grid_scan | 相对于当前位置的网格扫描 |
| scan_nd | 在任意N维轨迹上扫描 |
| spiral | 螺旋扫描,以 (x_start, y_start) 为中心。 |
| spiral_fermat | 绝对费马螺旋扫描,以 (x_start, y_start) 为中心。 |
| spiral_square | 绝对方形螺旋扫描,以 (x_center, y_center) 为中心。 |
| rel_spiral | 相对螺旋扫描 |
| rel_spiral_fermat | 相对费马螺旋扫描 |
| rel_spiral_square | 相对方形螺旋扫描,以 (x_center, y_center) 为中心。 |
| adaptive_scan | 以自适应调整的步长扫描一个变量。 |
| rel_adaptive_scan | 以自适应调整的步长相对扫描一个变量。 |
| tune_centroid | 计划:通过扫描将电机调整至信号分布的质心位置。 |
| tweak | 通过交互式提示移动电机并读取探测器数据的扫描计划。 |
| ramp_plan | 斜坡扫描一个或多个定位器并同步采集数据。 |
| fly | 执行一个或多个'飞扫器'的飞扫。 |

时间序列("count")

示例:

python 复制代码
from bluesky import RunEngine
from bluesky.callbacks.best_effort import BestEffortCallback
from ophyd.sim import det
from bluesky.plans import count

bc = BestEffortCallback()
RE = RunEngine({})
RE.subscribe(bc)

# 单次读取探测器'det'
RE(count([det]))

# 5次连续读取
RE(count([det], num=5))

# 5次连续读取,两次之间延迟1秒
RE(count([det], num=5, delay=1))

# 5次连续读取,两次之间分别延迟1,2,3,4秒
RE(count([det], num=5, delay=[1,2,3,4]))

# 不间断读取, 直到中断(例如, 用Ctrl+C)
RE(count([det], num=None))
python 复制代码
from ophyd.sim import noisy_det

# 为更有趣的绘图使用'noisy_det'示例探测器
RE(count([noisy_det], num=5, delay=1))

为什么count() 函数没有 exposure_time 参数?

现代CCD探测器用 acquire_time(采集时间)、acquire_period(采集周期)、num_exposures(曝光次数)参数化曝光时间, 而scalers用使用 preset_time(预设时间)或 auto_count_time(自动计数时间)参数化曝光时间. 没有适用于所有探测器的"曝光时间".

当同时计数多个探测器时(如 count([det1, det2, det3])),一般情况下, 使用者需要为每个探测提供单独的曝光时间, 此举变得冗长.

保持计划的通用性,将设备特定的配置留给设备类本身管理。

对于交互使用:

python 复制代码
# 仅用作示例. 你的探测器可能有不同名称或者数目的曝光相关参数--这正是关键所在。
det.exposure_time.set(3)
det.acquire_period.set(3.5)

从计划:

python 复制代码
# 仅用作示例. 你的探测器可能有不同名称或者数目的曝光相关参数--这正是关键所在。
 yield from bluesky.plan_stubs.mv(
     det.exposure_time, 3,
     det.acquire_period, 3.5)

另一种解决方案是编写一个自定义计划,该计划封装count()并设置曝光时间。这个自定义计划可以编码那些Bluesky框架本身无法知晓的具体细节。

python 复制代码
def count_with_time(detectors, num, delay, exposure_time, *, md=None):
    # 假设所有探测器都有一个完全指定其曝光的名为'exposure_time'的曝光时间组件.
    for detector in detectors:
        yield from bluesky.plan_stubs.mv(detector.exposure_time, exposure_time)
    yield from bluesky.plans.count(detectors, num, delay, md=md)

在一个维度上扫描

此"维度"可以是物理电机位置、温度参数或伪轴。对于扫描计划而言,它们的处理方式都是相同的。例如:

python 复制代码
from ophyd.sim import det, motor
from bluesky.plans import scan , rel_scan, list_scan

# 将电机从1扫描到5,对'det'进行5次等间距读数采集。
RE(scan([det], motor, 1, 5, 5))


# 将电机相对于它的当前位置从1扫描到5,对'det'进行5次等间距读数采集。
RE(rel_scan([det], motor, 1, 5, 5))

# 通过一个用户指定位置的列表扫描电机
RE(list_scan([det], motor, [1,1,2,3,5,8]))

注意: 为何扫描函数没有 delay 参数?

您可能注意到 count() 函数有 delay 参数,但所有扫描函数均未提供此参数。这是有意为之的设计。

通常希望在扫描中添加延迟的常见原因是为了:等待电机稳定到位或者让温度控制器达到平衡状态

更好的解决方案是将延迟配置在相应的设备上,这样无论使用哪种扫描计划,都会自动为该特定设备添加适当的延迟。

python 复制代码
motor.settle_time = 1
temperature_controller.settle_time = 10

在多数情况下,将延迟配置在设备层面比每次调用扫描时都手动输入delay参数更为便捷和可靠。您只需设置一次,即可持续生效。

这正是Bluesky在设计扫描函数时有意省略delay参数的原因------旨在引导用户采用一种比他们最初可能想到的方法更合适的方案。当然,对于确实需要使用delay参数的特殊情况,您完全可以通过编写自定义计划来实现。以下是利用per_step钩子的一种实现方式。

python 复制代码
import bluesky.plans
import bluesky.plan_stubs

def scan_with_delay(*args, delay=0, **kwargs):
    # 接收所有正常的'scan'参数,添上一个可选的delay.

    def one_nd_step_with_delay(detectors, step, pos_cache):
        # 这是添加了一个sleep的bluesky.plan_stubs.one_nd_step副本
        motors = step.keys()
        yield from bluesky.plan_stubs.move_per_step(step, pos_cache)
        yield from bluesky.plan_stubs.sleep(delay)
        yield from bluesky.plan_stubs.trigger_and_read(list(detectors) + list(motors))

    kwargs.setdefault('per_step', one_nd_step_with_delay)
    yield from bluesky.plans.scan(*args, **kwargs)

多维扫描

关于多电机协同运动的常见情况(例如沿对角线协同移动X轴和Y轴电机,或在网格中移动),请参见教程中的同时扫描多个电机章节。下文重现了其中的关键示例,更多详细解释仍请参阅所链接的章节。

python 复制代码
from ophyd.sim import det, motor1, motor2, motor3
from bluesky.plans import scan, grid_scan, list_scan, list_grid_scan
# 电机1从-1.5到1.5同时电机2从-0.1到0.1都扫描11个点
RE(scan([det], motor1,-1.5,1.5,motor2,-0.1,0.1,11))
python 复制代码
# 电机1和电机2一起通过5个点的轨迹
RE(list_scan([noisy_det], motor1,[1,1,3,5,8], motor2,[25,16,9,4,1]))
python 复制代码
# 扫描3*2个网格
RE(grid_scan([noisy_det], motor1,-1.5,1.5,3, motor3,-200,200,2,False))
python 复制代码
# 按照给定的特定位置进行任意间距的网格扫描。
RE(list_grid_scan([noisy_det], motor1,[1,2,3],motor2,[4,5,6]))

所有上述计划都建立在更通用的scan_nd()计划基础之上,该计划可用于处理更特殊的扫描场景。

引入一些术语:我们将scan()式的协同运动称为轨迹的 "内积扫描" ,而将grid_scan()式的运动称为轨迹的 "外积扫描" 。更一般的情况,即在 "外积扫描" 中协同移动某些电机的同时,对另一电机(或多个电机)进行 "内积扫描" ,可以通过cycler对象来处理。请注意当我们对cycler对象进行加法或乘法运算时发生的情况。

python 复制代码
from cycler import cycler

from ophyd.sim import motor1, motor2,  motor3

traj1 = cycler(motor1, [1,2,3])
traj2 = cycler(motor2, [10,20,30])
traj3 = cycler(motor3, [100,200,300])

我们已分别展示了内积扫描和外积扫描。真正的功能出现在我们将它们组合使用时,如下所示。这里,motor1 和 motor2 在一个网格中协同运动,并与 motor3 进行扫描。

螺旋轨迹

我们提供沿螺旋轨迹进行扫描的二维扫描方法。

一种简单螺旋轨迹扫描:

python 复制代码
from bluesky.simulators import plot_raster_path
from ophyd.sim import motor1, motor2, det
from bluesky.plans import spiral

plan = spiral([det], motor1, motor2, x_start=0.0, y_start=0.0, x_range=1.,
              y_range=1.0, dr=0.1, nth=10)
plot_raster_path(plan, 'motor1', 'motor2', probe_size=.01)

费马螺旋:

python 复制代码
from bluesky.simulators import plot_raster_path
from ophyd.sim import motor1, motor2, det
from bluesky.plans import spiral_fermat

plan = spiral_fermat([det], motor1, motor2, x_start=0.0, y_start=0.0,
                     x_range=2.0, y_range=2.0, dr=0.1, factor=2.0, tilt=0.0)
plot_raster_path(plan, 'motor1', 'motor2', probe_size=.01, lw=0.1)

方形螺旋:

python 复制代码
from bluesky.simulators import plot_raster_path
from ophyd.sim import motor1, motor2, det
from bluesky.plans import spiral_square

plan = spiral_square([det], motor1, motor2, x_center=0.0, y_center=0.0,
                     x_range=1.0, y_range=1.0, x_num=11, y_num=11)
plot_raster_path(plan, 'motor1', 'motor2', probe_size=.01)

自适应扫描

这是一种一维扫描技术,通过自适应调整步长来实现:在变化平缓的区域快速移动,而在变化显著的区域通过计算局部斜率、以目标相邻点间y值变化量为基准来集中采集读数。

这是自适应计划逻辑功能的一个基本示例。

python 复制代码
from bluesky.plans import adaptive_scan
from ophyd.sim import motor, det

RE(adaptive_scan([det], 'det', motor,
                 start=-15,
                 stop=10,
                 min_step=0.01,
                 max_step=5,
                 target_delta=.05,
                 backstep=True))

从左到右观察扫描过程:在平坦区域,扫描会增大步幅快速跨越。初次扫描时,它直接越过了峰值。由于这次大幅跳跃检测到显著变化,算法随即折返,并在峰值区域进行更密集的采样。随着峰值区域变化趋于平缓,扫描再次拉大步幅继续前进。

Stubs计划

这些正是前文提及的"可重组基础单元",即用于构建上述预置计划的组成部分。关于如何实际运用这些组件编写自定义计划,请参阅教程中的编写自定义计划章节,以获取实践指南。

以下是与硬件交互的基础计划:

|-----------|--------------------------------------|
| abs_set | 设置一个值 |
| rel_set | 相对于当前值设置一个值 |
| mv | 移动一个或多个设备到设定点 |
| mvr | 移动一个或多个设备到相对设定点 |
| trigger | 触发并采集 |
| read | 进行一次读数并将其添加到当前的读数组中。 |
| rd | 读取一个单值的非触发对象。 |
| stage | "设就"设备(即准备设备使其进入可用状态,或称"预置"或"就绪")。 |
| unstage | "解除就绪"设备(即将其置于待机状态,或称"撤防"或"解除工作状态")。 |
| configure | 更改设备配置并且发出一个更新的事件描述符文档 |
| stop | 停止设备 |

用于异步采集的计划:

|-----------|------------------------|
| monitor | 用于新值并且发出事件文档的异步监视器 |
| unmonitor | 停止监视 |
| kickoff | 触发一个飞扫设备 |
| complete | 在你准备好时,告诉一个飞扫器, '停止采集' |
| collect | 采集由飞扫设备缓存的数据并且发出文档 |

控制运行引擎的计划:

|-------------------|---------------------------------------|
| open_run | 标记一个新'运行'起始 |
| close_run | 标记当前'运行'结束 |
| create | 将后续的读数归集到一个新的事件文档中。 |
| save | 关闭一个读数包并且发出一个完成的事件文档 |
| drop | 丢弃一个读数包,且不发出已完成的事件文档。 |
| pause | 暂停并且等待用户继续 |
| deferred_pause | 暂停在下个检查点 |
| checkpoint | 若执行被中断,则回退至此标记点。 |
| clear_checkpoint | 标识其为不安全的恢复点。 |
| sleep | 告诉运行引擎睡眠, 同时异步执行其它运行 |
| input_plan | 提示用户文本输入 |
| subscribe | 订阅发出的文档流 |
| unsubscribe | 删除一个订阅 |
| install_suspender | 在执行计划时安装一个挂起器。 |
| remove_suspender | 在执行计划时删除一个挂起器。 |
| wait | 等待一个组中所有状态都报告结束 |
| wait_for | 底层操作: 等待一系列 asyncio.Future 对象设置(完成) |
| null | 生成一个空操作消息。 |

上述各项的常用组合:

|-------------------------------------------------|------------------------------|
| trigger_and_read(devices[,name]) | 触发并读取探测器列表,并将所有读数打包至一个事件文档中。 |
| one_1d_step(detectors, motor, step[,...]) | 1维步进扫描的内层循环 |
| one_nd_step(detectors, step, pos_cache[,...]) | N维步进扫描的内层循环 |
| one_shot(detectors[,tabke_reading]) | 一个计数的内层循环 |
| move_per_step(step, pos_cache) | 不带任何读数的N维步进扫描的内层循环 |

特殊工具:

|--------------------------------------------------|-------------------------------|
| repeat(plan[,num,delay]) | 按指定次数重复执行某个计划,每次重复之间设有延迟与检查点。 |
| repeater(n,gen_func,*args, **kwargs) | 生成 n 个来自 gen_func 的链式消息副本。 |
| caching_repeater(n,plan) | 在一个计划中生成n个链式消息副本 |
| broadcast_msg(command, objs, *args, **kwargs) | 生成一条消息的多个副本,并将其应用于设备列表。 |

计划预处理器

补充数据

计划预处理器可在运行时动态修改计划内容。一个常见的用途是,在每个运行(run)的开始和结束时,自动对一组设备采集"基线"读数。通过补充数据(SupplementalData) 机制,可以方便地将此功能应用于运行引擎执行的所有计划。

python 复制代码
class bluesky.preprocessors.SupplementalData(*, baseline=None, monitors=None, flyers=None)

一个用于补充测量的可配置预处理器.这是一个计划预处理器. 它插入消息到计划以实现:

  • 在其 baseline 属性中列出的设备,会在每个运行的开始和结束时自动进行 "基线"读数。
  • 在其 monitors 属性中列出的信号,会在每个运行期间进行异步更新监视。
  • 在其 flyers 属性中列出的"可飞扫"设备,会在每个运行开始时启动,并在运行结束时收集它们的数据。

内部, 它使用这些计划预处理器:

  • baseline_wrapper()
  • monitor_during_wrapper()
  • fly_during_wrapper()

参数:

  • baseline:list 在每次运行开始和结束要被读取的设备.
  • monitor:list 会在每个运行期间进行异步更新监视的设备。
  • flyers: list 在每次运行前要被触发并且运行结束时采集的"可飞行"设备.

示例

创建一个SupplementalData的实例并且应用它于运行引擎.

python 复制代码
sd = SupplementalData(baseline=[some_motor, some_detector]),
                      monitors=[some_signal],
                      flyers=[some_flyer])
RE = RunEngine({})
RE.preprocessors.append(sd)

现在由RE执行的所有计划将被修改为在每次运行前后添加基线读数, (在运行期间)添加监视, 以及(在每次运行前触发并且之后采集)飞行器.

交互地检查或更新列表:

python 复制代码
from bluesky.preprocessors import SupplementalData
sd = SupplementalData(baseline=[motor])

sd.baseline
python 复制代码
sd.baseline.append(det)
sd.baseline
python 复制代码
sd.baseline.remove(det)
sd.baseline

每个属性(baseline, monitors, flyers)时一个普通地Python列表, 支持所有标准的列表方法,诸如:

python 复制代码
sd.baseline.clear()

传给SupplementalData的参数是可选的, 所有列表默认是空的. 入以上显示,它们可以被交互地填充:

python 复制代码
sd = SupplementalData()
RE = RunEngine({})
RE.preprocessors.append(sd)
sd.baseline.append(some_detector)

我们已在运行引擎上安装了一个 "预处理器"。预处理器能够修改计划,以某种方式补充或更改其指令。从现在起,每次我们执行 RE(some_plan()) 时,运行引擎都会静默地将 some_plan() 转换为 sd(some_plan())。其中,sd(即预处理器)可以插入一些额外的指令。可以这样理解指令的流动路径:指令从 some_plan 流出,经过 sd 处理,最后到达 REsd 预处理器在指令传递过程中有机会检查流经的指令,并在它们被运行引擎处理之前,根据需要进行修改。

预处理器包装器和装饰器

预处理器可以对计划进行任意修改,甚至可以设计得相当巧妙。例如,relative_set_wrapper() 预处理器会将所有位置指令重写为相对于初始位置的相对位移。

python 复制代码
def rel_scan(detectors, motor, start, stop, num):
    absolute = scan(detectors, motor, start, stop, num)
    relative = relative_set_wrapper(absolute, [motor])
    yield from relative

这是一个微妙却异常强大的特性。

relative_set_wrapper() 这样的包装器作用于生成器实例,例如 scan(...) 返回的实例。同时,也存在对应的装饰器函数(如 relative_set_decorator),它们作用于生成器函数本身,例如 scan() 函数。

python 复制代码
#  使用装饰器修改一个生成器函数
def rel_scan(detectors, motor, start, stop, num):

    @relative_set_decorator([motor])  
    def inner_relative_scan():
        yield from scan(detectors, motor, start, stop, num)

    yield from inner_relative_scan()

inner_relative_scan 只是一个内部变量名,那么我们为何选择这样一个冗长的名称?为什么不直接命名为 f?当然,使用 f 在功能上完全可行。但采用一个描述性的名称可以使调试过程更加容易。在处理复杂、深度嵌套的调用栈时,清晰的内部变量名能为我们提供宝贵的线索,帮助我们快速定位问题根源。

注意: 装饰器语法------即 @ 符号------是一种将函数传递给另一个函数的简洁方式。

python 复制代码
@g
def f(...):
    pass

f(...)

# 等价于
g(f)(...)

内建预处理器

以下每个名为 <某名称>_wrapper 的函数都作用于一个生成器实例。而对应的名为 <某名称>_decorator 的函数则作用于生成器函数本身。

|---------------------------|-----------------------------------------|
| baseline_decorator | 在open_run后记录所有设备基线的预处理器 |
| baseline_wrapper | 在open_run后记录所有设备基线的预处理器 |
| contingency_wrapper | try...except..else..finally帮助程序 |
| finalize_decorator | try..finally帮助程序 |
| finalize_wrapper | try..finally帮助程序 |
| fly_during_decorator | 触发并且采集运行时"飞行器"(异步采集)对象 |
| fly_during_wrapper | 触发并且采集运行时"飞行器"(异步采集)对象 |
| inject_md_decorator | 向运行注入更多元数据 |
| inject_md_wrapper | 向运行注入更多元数据 |
| lazily_stage_decorator | 这是一个插入'stage'消息和添加'unstage'的预处理器 |
| lazily_stage_wrapper | 这是一个插入'stage'消息和添加'unstage'的预处理器 |
| monitor_during_decorator | 在运行中监视(异步读取)设备 |
| monitor_during_wrapper | 在运行中监视(异步读取)设备 |
| relative_set_decorator | 解析设备'设置'消息为相对于初始位置 |
| relative_set_wrapper | 解析设备'设置'消息为相对于初始位置 |
| reset_positions_decorator | 结束时移动可移动设备到它们的初始位置 |
| reset_positions_wrapper | 结束时移动可移动设备到它们的初始位置 |
| run_decorator | 封装在 open_runclose_run 消息中。 |
| run_wrapper | 封装在 open_runclose_run 消息中。 |
| stage_decorator | 'Stage'设备(即,准备使用它们, 'arm'它们)并且接着unstage |
| stage_wrapper | 'Stage'设备(即,准备使用它们, 'arm'它们)并且接着unstage |
| stubs_decorator | 订阅对文档流的回调,最后,取消订阅 |
| stubs_wrapper | 订阅对文档流的回调,最后,取消订阅 |
| suspend_decorator | 对运行引擎安装挂起器, 并且在结束时移除它们 |
| suspend_wrapper | 对运行引擎安装挂起器, 并且在结束时移除它们 |

自定义预处理器

使用msg_mutator()(用于就地更改消息)和plan_mutator()(用于插入消息到计划或者移除消息)实现预处理器.

学习此的最简单方式是通过示例, 在plans模块的源代码中学习内建处理器的实现.

用per_step自定义步进扫描

一维与多维扫描计划均采用三部分结构:(1) 准备阶段,(2) 在每一点执行的多步执行循环,以及 (3) 清理阶段。

我们提供了一个用于自定义第(2)步的钩子。这使得您能够基于现有计划编写其变体,而无需从零开始。

对于一维计划, 默认内层循环是:

python 复制代码
from bluesky.plan_stubs import checkpoint, abs_set, trigger_and_read

def one_1d_step(detectors, motor, step):
    """
    1维步进扫描的内层循环
    这是1维计划中"per_step"的默认函数
    """
    yield from checkpoint()
    yield from abs_set(motor, step, wait=True)
    return (yield from trigger_and_read(list(detectors) + [motor]))

某些用户定义的有相同签名的函数custom_step可以在其位置上使用.

python 复制代码
scan([det], motor, 1, 5, 5, per_step=custom_step)

为了方便, 这可以被封装到一个新计划的定义中:

python 复制代码
def custom_scan(detectors, motor, start, stop, step, *, md=None):
    yield from scan([det], motor, start, stop, step, md=md
                    per_step=custom_step)

对于多维计划, 默认的内层循环是:

python 复制代码
from bluesky.utils import short_uid
from bluesky.plan_stubs import checkpoint, abs_set, wait, trigger_and_read

def one_nd_step(detectors, step, pos_cache):
    """
    一个N维步进扫描的内层循环
    这是ND计划中'per_step'的默认函数

      参数:
    ----------
    detectors : 可迭代, 要读取的设备
        
    step : dict, 映射电机到在这个step中的位置
     
    pos_cache : dict, 映射电机到末尾设置位置
    """
    def move():
        yield from checkpoint()
        grp = short_uid('set')
        for motor, pos in step.items():
            if pos == pos_cache[motor]:
                # 这个位置不移动这个电机
                continue
            yield from abs_set(motor, pos, group=grp)
            pos_cache[motor] = pos
        yield from wait(group=grp)

    motors = step.keys()
    yield from move()
    yield from trigger_and_read(list(detectors) + list(motors))

同样, 一个相同签名的自定义函数可以被传递给任何多维扫描计划的per_step参数.

异步计划:"飞行扫描"和"监视"

关于这些术语的上下文说明及一些示例计划,请参阅 异步采集 部分,该章节末尾附近提供了相关示例。

这些是定义自定义计划和计划预处理器的有用工具.

|----------------|-------------------------------|
| pchain | 像itertools.chain但使用yield from |
| msg_mutator | 一种可在计划中修改或删除单个消息的简单预处理器。 |
| plan_mutator | 通过更改或插入消息来实现动态修改计划内容, |
| single_gen | 把单个消息变成计划 |
| make_decorator | 把一个生成器实例包装器变成一个生成器函数装饰器 |

相关推荐
yuyuyuliang0023 天前
Bluesky基础C: 用电机和Scaler步进扫描
epics
yuyuyuliang0024 天前
连接Bluesky和EPICS
epics
yuyuyuliang001 个月前
pydm教程2
epics
BF06241 个月前
EPICS ARCHIVER APPLIANCE
编辑器·epics
翟天保Steven6 个月前
Ubuntu-安装Epics教程
linux·ubuntu·epics
yuyuyuliang007 个月前
EPICS IP模块
epics
yuyuyuliang001 年前
Qt5中使用EPICS通道访问读写EPICS PV
linux·开发语言·qt·epics
woshigaowei51461 年前
自定义EPICS在LabVIEW中的测试
labview·epics
woshigaowei51461 年前
LabVIEW中EPICS客户端/服务端的测试
labview·epics