コンテンツにスキップ

DatagramBuilder

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 |