Skip to content
This is the development version of the documentation. It may change before the next release. See 0.6.x for the latest release.

DatagramBuilder

This content is not available in your language yet.

DatagramBuilder は, ユーザーが積んだコマンドを EtherCAT の 1 サイクルで送れる単位 (フレーム) の列へ変換するコンポーネント. 実装は crates/autd3-rs/src/datagram/ にある.

コマンドからフレームへの変換
内容
ユーザー向け Command<'a> 複数のOperationを束ねたもの. expand で自身を Operation の列へ展開.
中間 Operation 1 フレーム分の処理に対応. encode でペイロードに内容書き出す.
出力 Frames 実際に送出する Datagram の連結バッファとフレーム境界.
pub trait Command<'a> {
fn expand(self, builder: &mut DatagramBuilder<'a>);
fn boxed(self) -> BoxedCommand<'a> where Self: Sized + 'a;
}
pub trait Operation {
fn distribution(&self) -> Distribution;
fn encode(&self, device: &Device, out: &mut [u8; PAYLOAD_BYTES]) -> Result<Cmd, Error>;
fn reflect(&self, device: usize, state: &mut FirmwareState) -> Result<(), Error>;
}

impl<'a, O: Operation + 'a> Command<'a> for O のブランケット実装があるため, 単一の Operation はそのまま Command として扱える.

Operation は 1 フレームに対応する. ペイロード長を超えるデータを送る可能性があるものは, Operation ではなく Command として実装される. 例えば, WriteModualtionBufferCommand であり, expand の時点で単一, または, 複数の WriteModulationChunk へ分割される.

pub enum Distribution { Broadcast, PerDevice }
意味
Broadcast 全デバイスに同一ペイロード.
PerDevice デバイス毎に異なるペイロード.

まずは pushbuild だけを追う. push_each は後述する.

enum Step<'a> {
Op(Box<dyn Operation + 'a>),
Each { devices: EachOps<'a> }, // push_each 用. 後述
}
pub struct DatagramBuilder<'a> {
geometry: Arc<Geometry>,
ops: Vec<Step<'a>>,
invalid: Option<PayloadError>,
mirror: Option<MirrorHandle>,
}
フィールド 役割
ops 積まれたステップの列.
invalid expand 中に検出した不正 (最初の 1 件のみ)
mirror クライアント側ファームウェア状態のミラー (後述)
pub fn push<C: Command<'a>>(&mut self, cmd: C) -> &mut Self {
cmd.expand(self);
self
}
pub(crate) fn push_op<O: Operation + 'a>(&mut self, op: O) -> &mut Self {
self.ops.push(Step::Op(Box::new(op)));
self
}
impl<'a, O: Operation + 'a> Command<'a> for O {
fn expand(self, builder: &mut DatagramBuilder<'a>) {
builder.push_op(self);
}
}

pushが呼ばれると, その引数の Commandexpand が呼ばれる. Commandexpand の中で, 自信を複数の Command/Operaion に展開し, 再帰的にpush する. 最終的には Operation に対する, Command のブランケット実装で push_op が呼ばれ再起が止まり, opsStep::Op が積まれる.

builder.push(Clear);

ClearOperation なので, ブランケット実装経由で push_op が 1 回呼ばれる.

ops = [ Step::Op(Clear) ]

例: 複数の Operation へ展開されるコマンド

Section titled “例: 複数の Operation へ展開されるコマンド”

Modulation は変調データのサイズによって展開先が変わる.

impl<'a> Command<'a> for Modulation<'a> {
fn expand(self, builder: &mut DatagramBuilder<'a>) {
let size = self.data.len();
if WriteModulationFused::fits_single_frame(size) {
builder.push(WriteModulationFused { .. });
return;
}
builder
.push(WriteModulationBuffer { bank: self.bank, offset: 0, data: self.data })
.push(ConfigModulation { .. })
.push(ChangeModulationBank { .. });
}
}

1 フレームに収まるなら, データと設定を 1 つにまとめた WriteModulationFused へ展開する. 収まらないなら「バッファ書き込み → 設定 → バンク切り替え」の 3 段に分ける.

さらに WriteModulationBuffer 自身も Command であり, WriteModulationChunk へ分割される. したがって, 1 フレームに収まらないサイズの Modulation を 1 つ push すると, ops は次のようになる.

ops = [ Step::Op(WriteModulationChunk { offset: 0 })
, Step::Op(WriteModulationChunk { offset: MOD_WRITE_MAX_DATA_LEN })
, ...
, Step::Op(ConfigModulation)
, Step::Op(ChangeModulationBank) ]
pub fn build(&self) -> Result<Frames, Error>;
pub fn build_into(&self, out: &mut Frames) -> Result<(), Error>;

buildFrames を新規に作って, build_into に呼び出すだけ.

build_intoops を先頭から順に走査し, Frames へ書き出す.

for step in &self.ops {
match step {
Step::Op(op) => out.push_op(op.as_ref(), &self.geometry)?,
Step::Each { devices } => { /* 後述 */ }
}
}

Step::Op 1 つがフレーム 1 つに対応する. Distribution によって, そのフレームが持つ Datagram の数が変わる.

let encode_devices = match op.distribution() {
Distribution::Broadcast => 1,
Distribution::PerDevice => geometry.num_devices(),
};
let start = self.payloads.len();
for device in 0..encode_devices {
let mut payload = [0u8; PAYLOAD_BYTES];
let cmd = op.encode(&geometry[device], &mut payload)?;
self.payloads.push(Datagram { cmd, payload });
}
self.frames.push(FrameDesc { dist, start, len: encode_devices });

Broadcast では encode がデバイス 0 に対して 1 回しか呼ばれず, ペイロードも 1 部しか保持しない.

pub struct Datagram {
pub cmd: Cmd,
pub payload: [u8; PAYLOAD_BYTES]
}
struct FrameDesc {
dist: Distribution,
start: usize,
len: usize
}
pub struct Frames {
payloads: Vec<Datagram>, // 全フレームのペイロードを連結
frames: Vec<FrameDesc>, // 各フレームの (distribution, 開始位置, 長さ)
}
pub struct Frame<'a> {
dist: Distribution,
datagrams: &'a [Datagram]
}

フレームごとに Vec を持たず, 1 本の連結バッファと境界記述に分けている.

デバイス 2 台で Pattern (PerDevice) と ConfigModulation (Broadcast) を積んだ場合は次のようになる.

payloads: [ P(dev0), P(dev1), C ]
└──── frame 0 ───┘ └ frame 1
frames: [ { PerDevice, start: 0, len: 2 }
, { Broadcast, start: 2, len: 1 } ]

FramesIntoIterator を実装しており, Frame を順に取り出して送信する. 送信側の詳細は送信処理を参照.

expand の戻り値は () であり, 展開の時点ではエラーを返せない. 代わりに builder が invalid を保持し, build の時点でエラーとして返す.

pub(crate) fn reject(&mut self, e: PayloadError) -> &mut Self {
self.invalid.get_or_insert(e); // 最初の 1 件だけを保持
self
}

たとえば WriteModulationBuffer は空データを reject で弾く.

if self.data.is_empty() {
builder.reject(PayloadError::ModulationDataEmpty);
return;
}

mirror はクライアント側で保持するファームウェア状態のシミュレーションであり, 送信前の検証に使う.

例えば, Silencer は, 不正な設定がファームウェア側で弾かれるが, この mirror を使うことで, 送信前に不正を検出できる.

build_into は mirror を直接更新せず, 複製に対して reflect を適用し, 全ステップが成功した後に書き戻す.

let mut work = /* mirror の複製 */;
for step in &self.ops {
out.push_op(op.as_ref(), &self.geometry)?; // ここで失敗しても
for (device, state) in work.iter_mut().enumerate() {
op.reflect(device, state)?; // work は捨てられるだけ
}
}
**guard = Mirror::Synced(work); // 全部成功したら書き戻す

push_each はデバイス毎に異なるコマンドを割り当てる.

pub fn push_each<C, F>(&mut self, assign: F) -> &mut Self
where
C: Command<'a>,
F: FnMut(&Device) -> Option<C>;

積まれるのは Step::Each であり, デバイス毎の op 列を保持する.

Each { devices: Vec<Vec<Box<dyn Operation + 'a>>> }

assign が返したコマンドは使い捨ての子 DatagramBuilder で展開し, take_ops() で op 列を取り出す. None を返したデバイスは空の Vec になる.

直前のステップも Step::Each であり, かつ担当デバイスが 1 つも重複しない場合, 新しいステップを積まずに既存のステップへ埋め込む.

builder
.push_each(|d| (d.idx() == 0).then_some(opA))
.push_each(|d| (d.idx() == 1).then_some(opB));

例えば上記の場合, push_each の直後は以下のようになる.

frame 0 frame 1
dev0 | opA | --- |
dev1 | --- | opB |

ここで, デバイスが重複していないので, 1 つのステップにまとめられる.

frame 0
dev0 | opA |
dev1 | opB |

下記のようにデバイスが]重複する場合は畳み込まず, 空いている部分は Nop で埋める.

builder
.push_each(|d| (d.idx() == 0).then_some(opA))
.push_each(|d| (d.idx() == 0).then_some(opB));
frame 0 frame 1
dev0 | opA | opB |
dev1 | --- | --- |

Step::Each は「デバイス毎に長さが異なる可能性のある op 列」を保持する. EtherCAT は全デバイスへ同時に 1 フレームを送るため, 尺を揃える必要があり, 不足分は Nop で埋められる.

frame 0 frame 1 frame 2
dev0 | opA | opB | --- | ← 不足分は Nop
dev1 | opC | opD | opE |