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 として実装される.
例えば, WriteModualtionBufferは Command であり, expand の時点で単一, または, 複数の WriteModulationChunk へ分割される.
Distribution
Section titled “Distribution”pub enum Distribution { Broadcast, PerDevice }| 値 | 意味 |
|---|---|
Broadcast |
全デバイスに同一ペイロード. |
PerDevice |
デバイス毎に異なるペイロード. |
push から build まで
Section titled “push から build まで”まずは push と build だけを追う.
push_each は後述する.
DatagramBuilder の構造
Section titled “DatagramBuilder の構造”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が呼ばれると, その引数の Command の expand が呼ばれる.
Command は expand の中で, 自信を複数の Command/Operaion に展開し, 再帰的にpush する.
最終的には Operation に対する, Command のブランケット実装で push_op が呼ばれ再起が止まり, ops に Step::Op が積まれる.
例: 単一の Operation
Section titled “例: 単一の Operation”builder.push(Clear);Clear は Operation なので, ブランケット実装経由で 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>;build は Frames を新規に作って, build_into に呼び出すだけ.
build_into は ops を先頭から順に走査し, 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 部しか保持しない.
Frames の構造
Section titled “Frames の構造”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 1frames: [ { PerDevice, start: 0, len: 2 } , { Broadcast, start: 2, len: 1 } ]Frames は IntoIterator を実装しており, Frame を順に取り出して送信する.
送信側の詳細は送信処理を参照.
expand 失敗の扱い
Section titled “expand 失敗の扱い”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 の更新
Section titled “mirror の更新”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
Section titled “push_each”push_each はデバイス毎に異なるコマンドを割り当てる.
pub fn push_each<C, F>(&mut self, assign: F) -> &mut Selfwhere 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 になる.
隣接ステップの畳み込み
Section titled “隣接ステップの畳み込み”直前のステップも 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 1dev0 | opA | --- |dev1 | --- | opB |ここで, デバイスが重複していないので, 1 つのステップにまとめられる.
frame 0dev0 | 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 1dev0 | opA | opB |dev1 | --- | --- |フレームへの展開
Section titled “フレームへの展開”Step::Each は「デバイス毎に長さが異なる可能性のある op 列」を保持する.
EtherCAT は全デバイスへ同時に 1 フレームを送るため, 尺を揃える必要があり, 不足分は Nop で埋められる.
frame 0 frame 1 frame 2dev0 | opA | opB | --- | ← 不足分は Nopdev1 | opC | opD | opE |