借用设备¶
机械臂、相机、夹爪这类设备需要排他访问:同一时刻只能有一个使用者向它发指令。roplat 不把设备当成在节点之间传递的消息,而是在每次调用时把它的可变借用交给使用者,调用结束借用就结束。
完整程序¶
先回零,再伺服 30 个周期。第一次从 0.3 出发,正常完成;第二次从 5.0 出发,超出回零范围,整次运行以错误结束。
use roplat::rhythm::IntoRhythm;
use roplat::{Lifecycle, Node, RoplatError, RoplatResult};
const DT: f64 = 0.1;
/// 一个单轴模拟机械臂。真实项目里它是驱动库提供的设备句柄。
struct Arm {
name: &'static str,
position: f64,
commands: usize,
}
impl Arm {
fn new(name: &'static str, position: f64) -> Self {
Self {
name,
position,
commands: 0,
}
}
fn command(&mut self, velocity: f64) {
self.position += velocity * DT;
self.commands += 1;
}
}
impl Lifecycle for Arm {
type Error = RoplatError;
async fn on_init(&mut self) -> RoplatResult<()> {
println!("connect {}", self.name);
Ok(())
}
async fn on_shutdown(&mut self) -> RoplatResult<()> {
println!("disconnect {}", self.name);
Ok(())
}
}
/// 设备拒绝执行的原因。
#[derive(Debug)]
enum Fault {
OutOfRange(f64),
}
impl From<Fault> for RoplatError {
fn from(fault: Fault) -> Self {
match fault {
Fault::OutOfRange(position) => {
RoplatError::NodeProcessing(format!("起始位置 {position} 超出回零范围"))
}
}
}
}
/// 回零:只在安全范围内执行。
struct Home {
limit: f64,
}
impl Lifecycle for Home {
type Error = RoplatError;
}
impl Node for Home {
type Resources = (Arm,);
type Input = ();
type Output = Result<(), Fault>;
async fn process<'call>(
&'call mut self,
(): (),
(arm,): (&'call mut Arm,),
) -> Result<(), Fault> {
if arm.position.abs() > self.limit {
return Err(Fault::OutOfRange(arm.position));
}
arm.position = 0.0;
Ok(())
}
}
/// 每个周期向目标发一次速度指令,输出剩余误差。
struct Servo {
gain: f64,
limit: f64,
}
impl Lifecycle for Servo {
type Error = RoplatError;
}
impl Node for Servo {
type Resources = (Arm,);
type Input = f64;
type Output = f64;
async fn process<'call>(&'call mut self, target: f64, (arm,): (&'call mut Arm,)) -> f64 {
let error = target - arm.position;
arm.command((self.gain * error).clamp(-self.limit, self.limit));
target - arm.position
}
}
/// 一次运行之后留给调用方的结果。
#[derive(Debug)]
struct Snapshot {
position: f64,
commands: usize,
}
#[roplat::system]
async fn home_then_move(start: f64, target: f64) -> RoplatResult<Snapshot> {
// 机械臂由本层创建:本层负责连接与断开。
let mut arm = Arm::new("left", start);
let mut home = Home { limit: 2.0 };
let mut ticks = (0..30).into_rhythm();
let mut servo = Servo {
gain: 3.0,
limit: 1.0,
};
// 先回零:home 在 process 期间借用 arm;失败时整次运行以错误结束。
&mut arm >> home.resources.0;
home?;
// 两个使用 arm 的地方必须有依赖:回零之后才开始伺服。
home >> ticks.trigger;
// 再伺服 30 个周期:每个周期 servo 借用一次 arm。
ticks
>> |_tick| {
&mut arm >> servo.resources.0;
&target >> servo;
};
// 尾段:arm 仍是本体,读取状态作为结果。
Ok(Snapshot {
position: arm.position,
commands: arm.commands,
})
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
match home_then_move(0.3, 1.0).await {
Ok(snapshot) => println!(
"reached {:.3} after {} commands",
snapshot.position, snapshot.commands
),
Err(error) => println!("failed: {error}"),
}
match home_then_move(5.0, 1.0).await {
Ok(snapshot) => println!("unexpected: {snapshot:?}"),
Err(error) => println!("failed: {error}"),
}
}
输出:
connect left
disconnect left
reached 1.000 after 30 commands
connect left
disconnect left
failed: 节点处理错误: 起始位置 5 超出回零范围
声明要借用的设备¶
节点在 type Resources 里列出要借用的设备类型,process 的第三个参数就是这些设备的可变引用:
type Resources |
process 收到 |
|---|---|
() |
() |
(Arm,) |
(&mut Arm,) |
(Arm, Camera) |
(&mut Arm, &mut Camera) |
借用只在这一次 process 期间有效。节律也可以声明 Resources,那时借用覆盖整次 drive(见多层节律)。
在图里借出¶
&mut arm >> home.resources.0;
home?;
home >> ticks.trigger;
ticks >> |_tick| {
&mut arm >> servo.resources.0;
&target >> servo;
};
&mut arm >> home.resources.0把arm借给home的第 0 个资源槽。借多个设备时依次连.resources.0、.resources.1,编号从 0 连续。- 设备不出现在 Input 或 Output 里。
home的输入是(),输出是业务结果Result<(), Fault>。 - 内层周期域里的
servo也借用arm。在外层看来,整个ticks域算作arm的一个使用者。
同一设备的使用者必须有先后¶
home 和 ticks 都使用 arm。roplat 只按图依赖给它们排序,语句的书写顺序不算数。两者之间没有依赖时,宏会报错:
error: resource `arm` is used by `home` and `ticks` with no data or `.trigger` dependency between them;
uses of one resource are ordered only by graph dependencies, so add one, e.g. `home >> ticks.trigger;`
home >> ticks.trigger 只建立"在 home 之后"的依赖,不传递任何数据。如果两个使用者之间本来就有数据边,就不需要再补 .trigger。
业务错误与设备的去向¶
home 的输出是普通数据。home? 把其中的 Err 提升为本次运行的错误:图停止推进,ticks 不会开始,System 返回 Err。第二次调用的输出说明了两件事:
- 错误发生后,
arm仍然被正确断开(disconnect left)。借用在调用结束时就已结束,失败不会让设备"丢在"某个节点里。 - 不需要任何"归还"写法:没有返回边,也不需要把设备放进输出带回来。
0.5.0 的已知问题
在 0.5.0 中,把 ? 和 trigger 边写在同一句里,例如 home? >> ticks.trigger;,不会传播错误,后续步骤照常运行。请像上面那样分成 home?; 和 home >> ticks.trigger; 两句。? 后面接数据边(home? >> next;)不受影响。
谁负责连接和断开¶
arm 在 System 头段创建,所以由这个 System 在开始时调用 on_init(连接)、结束时调用 on_shutdown(断开),出错和停止时也一样。如果 arm 是函数参数,System 只借用它,连接和断开由调用方负责。
尾段里 arm 仍是设备本体,可以直接读取字段。本层创建的设备要留到关闭钩子执行,所以尾段返回的是状态快照,而不是把 arm 本身移出去;参数传进来的设备则可以原样返回给调用方。
要点¶
- 设备是资源,不是消息:
&mut arm >> user.resources.N,用完即还。 - 同一设备的任意两个使用者之间必须有数据边或
.trigger边。 - 不同设备的使用者之间没有这个要求,可以并发。
下一篇:分支、条件与重试。