├── .cargo └── config.toml ├── .circleci └── config.yml ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature.md │ └── issue.md └── workflows │ └── matrix-bot.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── Justfile ├── NOTES.md ├── README.md ├── README.tpl ├── SPECNOTES.md ├── benches ├── frame.rs └── pdu_loop.rs ├── doc └── windows-tuning.md ├── dumps ├── Term 5 (EL2004).bin ├── eeprom │ ├── akd.hex │ ├── akd_null_strings.hex │ ├── ek1100.hex │ ├── el2262.bin │ ├── el2828.hex │ ├── el2889.hex │ └── hbm_clipx_eeprom_dump.bin ├── ek1100-eeprom-dump.bin ├── el2004-eeprom-dump.bin ├── issue-286-empty-eeprom-example.bin ├── rs-detect-slaves-ek1100-el1004.pcapng ├── rs-detect-slaves-ek1100-only.pcapng ├── rs-example-sii-el2004.pcapng ├── soem-dual-lan9252.pcapng ├── soem-sdinfo-2-el2004.txt ├── soem-sdinfo-2-lan9252.txt ├── soem-sdinfo-akd.pcapng ├── soem-sdinfo-ek1100-2-lan9252.txt ├── soem-sdinfo-ek1100-el1004.pcapng ├── soem-sdinfo-ek1100-only.pcapng ├── soem-sdinfo-el7-sdo.txt ├── soem-sdinfo-no-slaves.pcapng ├── soem-single-lan9252.pcapng ├── twincat-akd-nc-mode.pcapng ├── twincat-run-ek1100-el1004-2.pcapng ├── twincat-run-ek1100-el1004.pcapng ├── twincat-scan-boxes.pcapng ├── twincat-scan-devices-2.pcapng └── twincat-scan-devices.pcapng ├── ethercrab-logo-docsrs.svg ├── ethercrab-logo-solid-white.svg ├── ethercrab-logo.svg ├── ethercrab-wire-derive ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── README.tpl ├── benches │ ├── derive-enum.rs │ ├── derive-large.rs │ └── derive-struct.rs ├── src │ ├── generate_enum.rs │ ├── generate_struct.rs │ ├── help.rs │ ├── lib.rs │ ├── parse_enum.rs │ └── parse_struct.rs └── ui │ ├── enum-isize.rs │ ├── enum-isize.stderr │ ├── enum-usize.rs │ ├── enum-usize.stderr │ ├── union.rs │ └── union.stderr ├── ethercrab-wire ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── README.tpl ├── src │ ├── error.rs │ ├── impls.rs │ └── lib.rs └── tests │ ├── heapless.rs │ ├── irl.rs │ ├── pack.rs │ ├── signed-enums.rs │ ├── std.rs │ └── unpack.rs ├── examples ├── akd.rs.disabled ├── c5-e.rs.disabled ├── dc-lan9252-groups.rs ├── dc.rs ├── discover.rs ├── dump-eeprom.rs ├── ec400.rs.disabled ├── ek1100.rs ├── embassy-stm32 │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── rust-toolchain.toml │ └── src │ │ └── main.rs ├── io-uring.rs ├── multiple-groups.rs ├── performance.rs ├── release.rs ├── smol-io-uring.rs ├── windows.rs └── xdp.rs ├── release.toml ├── repo-stats.sh ├── rust-toolchain.toml ├── src ├── al_control.rs ├── al_status_code.rs ├── base_data_types.rs ├── coe │ ├── abort_code.rs │ ├── mod.rs │ └── services.rs ├── command │ ├── mod.rs │ ├── reads.rs │ └── writes.rs ├── dc.rs ├── dl_status.rs ├── eeprom │ ├── device_provider.rs │ ├── file_provider.rs │ ├── mod.rs │ └── types.rs ├── error.rs ├── ethernet.rs ├── fmmu.rs ├── fmt.rs ├── generate.rs ├── lib.rs ├── mailbox.rs ├── maindevice.rs ├── maindevice_config.rs ├── pdi.rs ├── pdu_loop │ ├── frame_element │ │ ├── created_frame.rs │ │ ├── frame_box.rs │ │ ├── mod.rs │ │ ├── received_frame.rs │ │ ├── receiving_frame.rs │ │ └── sendable_frame.rs │ ├── frame_header.rs │ ├── mod.rs │ ├── pdu_flags.rs │ ├── pdu_header.rs │ ├── pdu_rx.rs │ ├── pdu_tx.rs │ └── storage.rs ├── register.rs ├── std │ ├── io_uring.rs │ ├── mod.rs │ ├── unix │ │ ├── bpf.rs │ │ ├── linux.rs │ │ └── mod.rs │ ├── windows.rs │ └── xdp.rs ├── subdevice │ ├── configuration.rs │ ├── dc.rs │ ├── eeprom.rs │ ├── mod.rs │ ├── pdi.rs │ ├── ports.rs │ └── types.rs ├── subdevice_group │ ├── group_id.rs │ ├── handle.rs │ ├── mod.rs │ └── tx_rx_response.rs ├── subdevice_state.rs ├── sync_manager_channel.rs ├── timer_factory.rs └── vendors.rs └── tests ├── README.md ├── init-must-be-send.rs ├── replay-dc.pcapng ├── replay-dc.rs ├── replay-ek1100-alias-address.pcapng ├── replay-ek1100-alias-address.rs ├── replay-ek1100-el2828-el2889.pcapng ├── replay-ek1100-el2828-el2889.rs ├── replay-ek1914-el3004-configure.pcapng ├── replay-ek1914-el3004-configure.rs ├── replay-ek1914-el3004-mailbox.pcapng ├── replay-ek1914-el3004-mailbox.rs ├── replay-ek1914-no-complete-access.pcapng ├── replay-ek1914-no-complete-access.rs ├── replay-ek1914-segmented-upload.pcapng ├── replay-ek1914-segmented-upload.rs ├── replay-issue-255.pcapng ├── replay-issue-255.rs └── util.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Raspberry Pi 4 64 bit cross compile 2 | # 3 | # On apt-based systems, run `apt install gcc-aarch64-linux-gnu` to install deps 4 | [target.aarch64-unknown-linux-gnu] 5 | linker = "aarch64-linux-gnu-gcc" 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | CHANGELOG.md merge=union 2 | NOTES.md merge=union 3 | SPECNOTES.md merge=union 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [jamwaffles] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: jamwaffles 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: A feature that EtherCrab should/could support 4 | title: "" 5 | labels: "feature" 6 | assignees: "" 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: A request for help, a problem or a bug 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Hi! Thanks for opening an issue. Please provide some specific details to make it easier to debug any 11 | problems. 12 | 13 | ## Environment 14 | 15 | - Version of ethercrab in use or git hash: [version here] 16 | - Operating system kind and version: [kind] [version] 17 | - EtherCAT devices in use: [list here] 18 | 19 | ## Uploads 20 | 21 | (these may need to be uploaded as a single ZIP) 22 | 23 | - A Wireshark capture of a single run of the buggy program 24 | - The log output of the program run with `RUST_LOG=debug` 25 | 26 | ## Description of the problem/feature request/other 27 | 28 | [description here] 29 | 30 | ## Code used to produce the bug 31 | 32 | ```rust 33 | // Failing test case demonstrating the issue here 34 | ``` 35 | -------------------------------------------------------------------------------- /.github/workflows/matrix-bot.yml: -------------------------------------------------------------------------------- 1 | name: Matrix bot 2 | on: 3 | pull_request_target: 4 | types: [opened, closed] 5 | 6 | jobs: 7 | new-pr: 8 | if: github.event.action == 'opened' && github.repository == 'ethercrab-rs/ethercrab' 9 | runs-on: ubuntu-latest 10 | continue-on-error: true 11 | steps: 12 | - name: send message 13 | uses: s3krit/matrix-message-action@v0.0.3 14 | with: 15 | room_id: ${{ secrets.MATRIX_ROOM_ID }} 16 | access_token: ${{ secrets.MATRIX_ACCESS_TOKEN }} 17 | message: 18 | "New PR: [${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url 19 | }})" 20 | server: "matrix.org" 21 | 22 | merged-pr: 23 | if: 24 | github.event.action == 'closed' && github.event.pull_request.merged == true && 25 | github.repository == 'ethercrab-rs/ethercrab' 26 | runs-on: ubuntu-latest 27 | continue-on-error: true 28 | steps: 29 | - name: send message 30 | uses: s3krit/matrix-message-action@v0.0.3 31 | with: 32 | room_id: ${{ secrets.MATRIX_ROOM_ID }} 33 | access_token: ${{ secrets.MATRIX_ACCESS_TOKEN }} 34 | message: 35 | "PR merged: [${{ github.event.pull_request.title }}](${{ 36 | github.event.pull_request.html_url }})" 37 | server: "matrix.org" 38 | 39 | abandoned-pr: 40 | if: 41 | github.event.action == 'closed' && github.event.pull_request.merged == false && 42 | github.repository == 'ethercrab-rs/ethercrab' 43 | runs-on: ubuntu-latest 44 | continue-on-error: true 45 | steps: 46 | - name: send message 47 | uses: s3krit/matrix-message-action@v0.0.3 48 | with: 49 | room_id: ${{ secrets.MATRIX_ROOM_ID }} 50 | access_token: ${{ secrets.MATRIX_ACCESS_TOKEN }} 51 | message: 52 | "PR closed without merging: [${{ github.event.pull_request.title }}](${{ 53 | github.event.pull_request.html_url }})" 54 | server: "matrix.org" 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | 15 | /target 16 | /Cargo.lock 17 | /examples/*.pcapng 18 | /npcap-sdk-* 19 | .vscode/ 20 | # Debug output from examples 21 | *.csv 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethercrab" 3 | categories = ["science::robotics", "no-std", "network-programming"] 4 | version = "0.6.0" 5 | edition = "2024" 6 | license = "MIT OR Apache-2.0" 7 | readme = "README.md" 8 | repository = "https://github.com/ethercrab-rs/ethercrab" 9 | documentation = "https://docs.rs/ethercrab" 10 | description = "A pure Rust EtherCAT MainDevice supporting std and no_std environments" 11 | keywords = ["ethercat", "beckhoff", "automation", "fieldbus", "robotics"] 12 | exclude = ["dumps", "doc", "NOTES.md", "SPECNOTES.md"] 13 | resolver = "2" 14 | rust-version = "1.85" 15 | 16 | [workspace] 17 | members = ["ethercrab-wire", "ethercrab-wire-derive"] 18 | 19 | [package.metadata.docs.rs] 20 | default-target = "x86_64-unknown-linux-gnu" 21 | targets = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-gnu"] 22 | 23 | [dependencies] 24 | async-io = { version = "2.0.0", optional = true } 25 | atomic-waker = "1.1.2" 26 | atomic_enum = "0.3.0" 27 | bitflags = "2.9.0" 28 | defmt = { version = "0.3.5", optional = true } 29 | embassy-time = "0.4.0" 30 | embedded-io-async = { version = "0.6.0", default-features = false } 31 | futures-lite = { version = "2.0.0", default-features = false } 32 | heapless = "0.8.0" 33 | log = { version = "0.4.27", optional = true, default-features = false } 34 | sealed = "0.6.0" 35 | serde = { version = "1.0.190", features = ["derive"], optional = true } 36 | smlang = "0.8.0" 37 | ethercrab-wire = { version = "0.2.0", path = "./ethercrab-wire" } 38 | spin = { version = "0.10.0", default-features = false, features = ["rwlock"] } 39 | crc = { version = "3.2.1", default-features = false } 40 | 41 | [target.'cfg(target_os = "windows")'.dependencies] 42 | pnet_datalink = { version = "0.35.0", features = ["std"], optional = true } 43 | pcap = "2.2.0" 44 | 45 | [target.'cfg(unix)'.dependencies] 46 | libc = "0.2.149" 47 | nix = { version = "0.29.0", features = ["net"] } 48 | 49 | [target.'cfg(target_os = "linux")'.dependencies] 50 | io-uring = "0.7.4" 51 | smallvec = { version = "1.13.1", features = ["const_generics"] } 52 | slab = "0.4.9" 53 | timerfd = "1.5.0" 54 | xsk-rs = { version = "0.7.0", optional = true } 55 | 56 | [dev-dependencies] 57 | arbitrary = { version = "1.3.1", features = ["derive"] } 58 | criterion = { version = "0.5.1", default-features = false, features = [ 59 | "cargo_bench_support", 60 | ] } 61 | env_logger = "0.11.6" 62 | heckcheck = "2.0.1" 63 | pcap-file = "2.0.0" 64 | pretty_assertions = "1.4.0" 65 | smol = "2.0.0" 66 | tokio = { version = "1.33.0", features = [ 67 | "rt-multi-thread", 68 | "macros", 69 | "sync", 70 | "time", 71 | ] } 72 | thread-priority = "1.2.0" 73 | ta = "0.5.0" 74 | cassette = "0.3.0" 75 | csv = "1.3.0" 76 | serde = { version = "1.0.190", default-features = false, features = ["derive"] } 77 | signal-hook = "0.3.17" 78 | core_affinity = "0.8.1" 79 | spin_sleep = "1.2.1" 80 | quanta = "0.12.3" 81 | savefile = { version = "0.18.5", default-features = false } 82 | savefile-derive = { version = "0.18.5", default-features = false } 83 | simple_logger = { version = "5.0.0", default-features = false } 84 | smoltcp = { version = "0.12.0", default-features = false, features = [ 85 | "medium-ethernet", 86 | "proto-ipv4", 87 | "socket-raw", 88 | ] } 89 | 90 | [features] 91 | default = ["std"] 92 | defmt = [ 93 | "dep:defmt", 94 | "embedded-io-async/defmt-03", 95 | "heapless/defmt-03", 96 | "ethercrab-wire/defmt-03", 97 | ] 98 | log = ["dep:log"] 99 | std = [ 100 | "dep:pnet_datalink", 101 | "dep:async-io", 102 | "log", 103 | "futures-lite/std", 104 | "embedded-io-async/std", 105 | "ethercrab-wire/std", 106 | "spin/std", 107 | ] 108 | xdp = ["dep:xsk-rs"] 109 | serde = ["dep:serde", "bitflags/serde"] 110 | 111 | # [[example]] 112 | # name = "akd" 113 | # required-features = ["std"] 114 | 115 | [[example]] 116 | name = "xdp" 117 | required-features = ["std", "xdp"] 118 | 119 | [[example]] 120 | name = "dc" 121 | required-features = ["std"] 122 | 123 | # [[example]] 124 | # name = "ec400" 125 | # required-features = ["std"] 126 | 127 | [[example]] 128 | name = "ek1100" 129 | required-features = ["std"] 130 | 131 | [[example]] 132 | name = "multiple-groups" 133 | required-features = ["std"] 134 | 135 | [[bench]] 136 | name = "pdu_loop" 137 | harness = false 138 | 139 | [[bench]] 140 | name = "frame" 141 | harness = false 142 | 143 | [profile.profiling] 144 | inherits = "release" 145 | debug = true 146 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | linux-example example *args: 2 | cargo build --example {{example}} && \ 3 | sudo setcap cap_net_raw=pe ./target/debug/examples/{{example}} && \ 4 | ./target/debug/examples/{{example}} {{args}} 5 | 6 | linux-example-release example *args: 7 | cargo build --example {{example}} --release && \ 8 | sudo setcap cap_net_raw=pe ./target/release/examples/{{example}} && \ 9 | ./target/release/examples/{{example}} {{args}} 10 | 11 | linux-test *args: 12 | #!/usr/bin/env bash 13 | 14 | set -e 15 | 16 | OUT=$(cargo test --no-run 2>&1 | tee /dev/tty | grep -oE '\(target/.+\)' | sed 's/[)(]//g') 17 | # BINS=$(echo $OUT) 18 | 19 | mapfile -t BINS < <( echo "$OUT" ) 20 | 21 | for BIN in "${BINS[@]}" 22 | do 23 | echo " Setcap for test binary $BIN" 24 | sudo setcap cap_net_raw=pe $BIN 25 | done 26 | 27 | # We've now setcap'd everything so we should be able to run this again without perm issues 28 | cargo test {{args}} 29 | 30 | miri *args: 31 | MIRIFLAGS="-Zmiri-symbolic-alignment-check -Zmiri-isolation-error=warn -Zdeduplicate-diagnostics=yes" cargo +nightly-2025-03-29 miri test --target aarch64-unknown-linux-gnu {{args}} 32 | 33 | miri-nextest *args: 34 | MIRIFLAGS="-Zmiri-symbolic-alignment-check -Zmiri-isolation-error=warn -Zdeduplicate-diagnostics=yes" cargo +nightly-2025-03-29 miri nextest run --target aarch64-unknown-linux-gnu {{args}} 35 | 36 | _generate-readme path: 37 | cargo readme --project-root "{{path}}" --template README.tpl --output README.md 38 | # Remove unprocessed doc links 39 | sed -i 's/\[\(`[^`]*`\)] /\1 /g' README.md 40 | 41 | _check-readme path: (_generate-readme path) 42 | git diff --quiet --exit-code "{{path}}/README.md" 43 | 44 | check-readmes: (_check-readme ".") (_check-readme "./ethercrab-wire") (_check-readme "./ethercrab-wire-derive") 45 | 46 | generate-readmes: (_generate-readme ".") (_generate-readme "./ethercrab-wire") (_generate-readme "./ethercrab-wire-derive") 47 | 48 | dump-eeprom *args: 49 | cargo build --example dump-eeprom --features "std" --release && \ 50 | sudo setcap cap_net_raw=pe ./target/release/examples/dump-eeprom && \ 51 | ./target/release/examples/dump-eeprom {{args}} 52 | 53 | test-replay test_file *args: 54 | cargo test {{ replace(test_file, '-', '_') }} 55 | 56 | capture-replay test_name interface *args: 57 | #!/usr/bin/env bash 58 | 59 | set -euo pipefail 60 | 61 | # Kill child tshark on failure 62 | trap 'killall tshark' EXIT 63 | 64 | test_file=$(echo "{{test_name}}" | tr '_' '-') 65 | 66 | if [ ! -f "tests/${test_file}.rs" ]; then 67 | echo "Test file tests/${test_file}.rs does not exist" 68 | 69 | exit 1 70 | fi 71 | 72 | cargo build --tests --release 73 | sudo echo 74 | fd . --type executable ./target/debug/deps -x sudo setcap cap_net_raw=pe 75 | fd . --type executable ./target/release -x sudo setcap cap_net_raw=pe 76 | 77 | tshark -Q -w "tests/${test_file}.pcapng" --interface {{interface}} -f 'ether proto 0x88a4' & 78 | 79 | # Wait for tshark to start 80 | sleep 1 81 | 82 | test_name=$(echo "${test_file}" | tr '-' '_') 83 | 84 | # Set env var to put test in capture mode 85 | INTERFACE="{{interface}}" cargo test "${test_name}" --release -- {{args}} 86 | 87 | # Let tshark finish up 88 | sleep 1 89 | 90 | killall tshark 91 | 92 | capture-all-replays interface *args: 93 | #!/usr/bin/env bash 94 | 95 | set -euo pipefail 96 | 97 | for file in `ls tests/replay-*.rs`; do 98 | test_name=$(basename "${file%.*}" | tr '-' '_') 99 | 100 | echo "Capturing $test_name, test is:" 101 | echo "" 102 | grep '//!' $file 103 | echo "" 104 | 105 | read -p "Prepare hardware then press any key to continue." ::new(); 10 | 11 | let (_tx, _rx, pdu_loop) = storage.try_split().unwrap(); 12 | 13 | let maindevice = MainDevice::new(pdu_loop, Timeouts::default(), MainDeviceConfig::default()); 14 | 15 | c.bench_function("frame push pdu", |b| { 16 | b.iter(|| { 17 | let mut f = pin!(Command::fpwr(0x5678, 0x1234).send_receive_slice(&maindevice, &DATA)); 18 | 19 | cassette::block_on(poll_fn(|ctx| { 20 | let _ = f.as_mut().poll(ctx); 21 | 22 | Poll::Ready(()) 23 | })); 24 | }) 25 | }); 26 | } 27 | 28 | criterion_group!(frame, push_pdu); 29 | criterion_main!(frame); 30 | -------------------------------------------------------------------------------- /benches/pdu_loop.rs: -------------------------------------------------------------------------------- 1 | use core::future::poll_fn; 2 | use core::task::Poll; 3 | use criterion::{Bencher, Criterion, Throughput, criterion_group, criterion_main}; 4 | use ethercrab::{Command, MainDevice, MainDeviceConfig, PduStorage, Timeouts}; 5 | use std::{pin::pin, time::Duration}; 6 | 7 | const DATA: [u8; 8] = [0x11u8, 0x22, 0x33, 0x44, 0xaa, 0xbb, 0xcc, 0xdd]; 8 | 9 | fn do_bench(b: &mut Bencher) { 10 | const FRAME_OVERHEAD: usize = 28; 11 | 12 | // 1 frame, up to 128 bytes payload 13 | let storage = PduStorage::<1, { PduStorage::element_size(128) }>::new(); 14 | 15 | let (mut tx, mut rx, pdu_loop) = storage.try_split().unwrap(); 16 | 17 | let maindevice = MainDevice::new( 18 | pdu_loop, 19 | Timeouts { 20 | pdu: Duration::from_millis(1000), 21 | ..Timeouts::default() 22 | }, 23 | MainDeviceConfig::default(), 24 | ); 25 | 26 | let mut written_packet = [0u8; { FRAME_OVERHEAD + DATA.len() }]; 27 | 28 | b.iter(|| { 29 | // --- Prepare frame 30 | 31 | let mut frame_fut = 32 | pin!(Command::fpwr(0x5678, 0x1234).send_receive::<()>(&maindevice, &DATA)); 33 | 34 | // Poll future once to register it with sender 35 | cassette::block_on(poll_fn(|ctx| { 36 | let _ = frame_fut.as_mut().poll(ctx); 37 | 38 | Poll::Ready(()) 39 | })); 40 | 41 | let frame = tx.next_sendable_frame().expect("Next frame"); 42 | 43 | frame 44 | .send_blocking(|bytes| { 45 | written_packet.copy_from_slice(bytes); 46 | 47 | Ok(bytes.len()) 48 | }) 49 | .expect("TX"); 50 | 51 | // --- Receive frame 52 | 53 | // Turn master sent MAC into receiving MAC 54 | written_packet[6] = 0x12; 55 | // Bump working counter so we don't error out 56 | written_packet[written_packet.len() - 2] = 1; 57 | 58 | rx.receive_frame(&written_packet).expect("RX"); 59 | 60 | let _ = cassette::block_on(frame_fut); 61 | }) 62 | } 63 | 64 | pub fn tx_rx(c: &mut Criterion) { 65 | let mut group = c.benchmark_group("pdu_loop"); 66 | 67 | group.throughput(Throughput::Elements(1)); 68 | 69 | group.bench_function("elements", do_bench); 70 | 71 | let overhead = { 72 | // Ethernet header 73 | (6 + 6 + 2) + 74 | // EtherCAT header 75 | 2 + 76 | // PDU header 77 | 10 + 78 | // Working counter 79 | 2 80 | }; 81 | 82 | group.throughput(Throughput::Bytes(DATA.len() as u64 + overhead)); 83 | 84 | group.bench_function("ethernet frame bytes", do_bench); 85 | 86 | group.finish(); 87 | } 88 | 89 | criterion_group!(pdu_loop, tx_rx); 90 | criterion_main!(pdu_loop); 91 | -------------------------------------------------------------------------------- /dumps/Term 5 (EL2004).bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/Term 5 (EL2004).bin -------------------------------------------------------------------------------- /dumps/eeprom/akd.hex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/eeprom/akd.hex -------------------------------------------------------------------------------- /dumps/eeprom/akd_null_strings.hex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/eeprom/akd_null_strings.hex -------------------------------------------------------------------------------- /dumps/eeprom/ek1100.hex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/eeprom/ek1100.hex -------------------------------------------------------------------------------- /dumps/eeprom/el2262.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/eeprom/el2262.bin -------------------------------------------------------------------------------- /dumps/eeprom/el2828.hex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/eeprom/el2828.hex -------------------------------------------------------------------------------- /dumps/eeprom/el2889.hex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/eeprom/el2889.hex -------------------------------------------------------------------------------- /dumps/eeprom/hbm_clipx_eeprom_dump.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/eeprom/hbm_clipx_eeprom_dump.bin -------------------------------------------------------------------------------- /dumps/ek1100-eeprom-dump.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/ek1100-eeprom-dump.bin -------------------------------------------------------------------------------- /dumps/el2004-eeprom-dump.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/el2004-eeprom-dump.bin -------------------------------------------------------------------------------- /dumps/issue-286-empty-eeprom-example.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/issue-286-empty-eeprom-example.bin -------------------------------------------------------------------------------- /dumps/rs-detect-slaves-ek1100-el1004.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/rs-detect-slaves-ek1100-el1004.pcapng -------------------------------------------------------------------------------- /dumps/rs-detect-slaves-ek1100-only.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/rs-detect-slaves-ek1100-only.pcapng -------------------------------------------------------------------------------- /dumps/rs-example-sii-el2004.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/rs-example-sii-el2004.pcapng -------------------------------------------------------------------------------- /dumps/soem-dual-lan9252.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/soem-dual-lan9252.pcapng -------------------------------------------------------------------------------- /dumps/soem-sdinfo-2-el2004.txt: -------------------------------------------------------------------------------- 1 | PS C:\Users\jamwa\Repositories\SOEM\build\test\win32\slaveinfo> .\slaveinfo.exe '\Device\NPF_{DCEDC919-0A20-47A2-9788-FC57D0169EDB}' 2 | SOEM (Simple Open EtherCAT Master) 3 | Slaveinfo 4 | Starting slaveinfo 5 | ec_init on \Device\NPF_{DCEDC919-0A20-47A2-9788-FC57D0169EDB} succeeded. 6 | 3 slaves found and configured. 7 | Calculated workcounter 4 8 | 9 | Slave:1 10 | Name:EK1100 11 | Output size: 0bits 12 | Input size: 0bits 13 | State: 4 14 | Delay: 0[ns] 15 | Has DC: 1 16 | DCParentport:0 17 | Activeports:1.1.0.0 18 | Configured address: 1001 19 | Man: 00000002 ID: 044c2c52 Rev: 00120000 20 | FMMUfunc 0:0 1:0 2:0 3:0 21 | MBX length wr: 0 rd: 0 MBX protocols : 00 22 | CoE details: 00 FoE details: 00 EoE details: 00 SoE details: 00 23 | Ebus current: -2000[mA] 24 | only LRD/LWR:0 25 | 26 | Slave:2 27 | Name:EL2004 28 | Output size: 4bits 29 | Input size: 0bits 30 | State: 4 31 | Delay: 144[ns] 32 | Has DC: 1 33 | DCParentport:1 34 | Activeports:1.1.0.0 35 | Configured address: 1002 36 | Man: 00000002 ID: 07d43052 Rev: 00100000 37 | SM0 A:0f00 L: 1 F:00090044 Type:3 38 | FMMU0 Ls:00000000 Ll: 1 Lsb:0 Leb:3 Ps:0f00 Psb:0 Ty:02 Act:01 39 | FMMUfunc 0:1 1:0 2:0 3:0 40 | MBX length wr: 0 rd: 0 MBX protocols : 00 41 | CoE details: 00 FoE details: 00 EoE details: 00 SoE details: 00 42 | Ebus current: 100[mA] 43 | only LRD/LWR:0 44 | 45 | Slave:3 46 | Name:EL2004 47 | Output size: 4bits 48 | Input size: 0bits 49 | State: 4 50 | Delay: 299[ns] 51 | Has DC: 1 52 | DCParentport:1 53 | Activeports:1.0.0.0 54 | Configured address: 1003 55 | Man: 00000002 ID: 07d43052 Rev: 00100000 56 | SM0 A:0f00 L: 1 F:00090044 Type:3 57 | FMMU0 Ls:00000000 Ll: 1 Lsb:4 Leb:7 Ps:0f00 Psb:0 Ty:02 Act:01 58 | FMMUfunc 0:1 1:0 2:0 3:0 59 | MBX length wr: 0 rd: 0 MBX protocols : 00 60 | CoE details: 00 FoE details: 00 EoE details: 00 SoE details: 00 61 | Ebus current: 100[mA] 62 | only LRD/LWR:0 63 | End slaveinfo, close socket 64 | End program 65 | -------------------------------------------------------------------------------- /dumps/soem-sdinfo-2-lan9252.txt: -------------------------------------------------------------------------------- 1 | PS C:\Users\jamwa\Repositories\SOEM\build\test\win32\slaveinfo> .\slaveinfo.exe "\\Device\\NPF_{DCEDC919-0A20-47A2-9788-FC57D0169EDB}" 2 | SOEM (Simple Open EtherCAT Master) 3 | Slaveinfo 4 | Starting slaveinfo 5 | ec_init on \\Device\\NPF_{DCEDC919-0A20-47A2-9788-FC57D0169EDB} succeeded. 6 | 2 slaves found and configured. 7 | Calculated workcounter 6 8 | 9 | Slave:1 10 | Name:LAN9252-EVB-HBI 11 | Output size: 16bits 12 | Input size: 48bits 13 | State: 4 14 | Delay: 0[ns] 15 | Has DC: 1 16 | DCParentport:0 17 | Activeports:1.1.0.0 18 | Configured address: 1001 19 | Man: 00000009 ID: 00009252 Rev: 00000001 20 | SM0 A:1000 L: 128 F:00010026 Type:1 21 | SM1 A:1080 L: 128 F:00010022 Type:2 22 | SM2 A:1100 L: 2 F:00010064 Type:3 23 | SM3 A:1400 L: 6 F:00010020 Type:4 24 | FMMU0 Ls:00000000 Ll: 2 Lsb:0 Leb:7 Ps:1100 Psb:0 Ty:02 Act:01 25 | FMMU1 Ls:00000004 Ll: 6 Lsb:0 Leb:7 Ps:1400 Psb:0 Ty:01 Act:01 26 | FMMUfunc 0:1 1:2 2:3 3:0 27 | MBX length wr: 128 rd: 128 MBX protocols : 04 28 | CoE details: 23 FoE details: 00 EoE details: 00 SoE details: 00 29 | Ebus current: 0[mA] 30 | only LRD/LWR:0 31 | 32 | Slave:2 33 | Name:LAN9252-EVB-HBI 34 | Output size: 16bits 35 | Input size: 48bits 36 | State: 4 37 | Delay: 740[ns] 38 | Has DC: 1 39 | DCParentport:1 40 | Activeports:1.0.0.0 41 | Configured address: 1002 42 | Man: 00000009 ID: 00009252 Rev: 00000001 43 | SM0 A:1000 L: 128 F:00010026 Type:1 44 | SM1 A:1080 L: 128 F:00010022 Type:2 45 | SM2 A:1100 L: 2 F:00010064 Type:3 46 | SM3 A:1400 L: 6 F:00010020 Type:4 47 | FMMU0 Ls:00000002 Ll: 2 Lsb:0 Leb:7 Ps:1100 Psb:0 Ty:02 Act:01 48 | FMMU1 Ls:0000000a Ll: 6 Lsb:0 Leb:7 Ps:1400 Psb:0 Ty:01 Act:01 49 | FMMUfunc 0:1 1:2 2:3 3:0 50 | MBX length wr: 128 rd: 128 MBX protocols : 04 51 | CoE details: 23 FoE details: 00 EoE details: 00 SoE details: 00 52 | Ebus current: 0[mA] 53 | only LRD/LWR:0 54 | End slaveinfo, close socket 55 | End program 56 | -------------------------------------------------------------------------------- /dumps/soem-sdinfo-akd.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/soem-sdinfo-akd.pcapng -------------------------------------------------------------------------------- /dumps/soem-sdinfo-ek1100-2-lan9252.txt: -------------------------------------------------------------------------------- 1 | Times (-258803446, -258802556, -258799926, 1819436374) - offset 2012517446 2 | Times (-257028436, -257027836, 0, 1819436374) - offset 2010742436 3 | Times (-255816056, -255815746, 0, 1819436374) - offset 2009530056 4 | Times (-253687066, 1717989224, 0, 1819436374) - offset -255816056 5 | Times (-1452872046, -1452870606, 0, 0) - offset -1088381250 6 | Times (-1279221694, 1413563250, 0, 0) - offset -1262031602 7 | 6 slaves found and configured. 8 | Calculated workcounter 11 9 | 10 | Slave:1 11 | Name:EK1100 12 | Output size: 0bits 13 | Input size: 0bits 14 | State: 4 15 | Delay: 0[ns] 16 | Has DC: 1 17 | DCParentport:0 18 | Activeports:1.1.1.0 19 | Configured address: 1001 20 | Man: 00000002 ID: 044c2c52 Rev: 00120000 21 | FMMUfunc 0:0 1:0 2:0 3:0 22 | MBX length wr: 0 rd: 0 MBX protocols : 00 23 | CoE details: 00 FoE details: 00 EoE details: 00 SoE details: 00 24 | Ebus current: -2000[mA] 25 | only LRD/LWR:0 26 | 27 | Slave:2 28 | Name:EL2004 29 | Output size: 4bits 30 | Input size: 0bits 31 | State: 4 32 | Delay: 145[ns] 33 | Has DC: 1 34 | DCParentport:1 35 | Activeports:1.1.0.0 36 | Configured address: 1002 37 | Man: 00000002 ID: 07d43052 Rev: 00100000 38 | SM0 A:0f00 L: 1 F:00090044 Type:3 39 | FMMU0 Ls:00000000 Ll: 1 Lsb:0 Leb:3 Ps:0f00 Psb:0 Ty:02 Act:01 40 | FMMUfunc 0:1 1:0 2:0 3:0 41 | MBX length wr: 0 rd: 0 MBX protocols : 00 42 | CoE details: 00 FoE details: 00 EoE details: 00 SoE details: 00 43 | Ebus current: 100[mA] 44 | only LRD/LWR:0 45 | 46 | Slave:3 47 | Name:EL2004 48 | Output size: 4bits 49 | Input size: 0bits 50 | State: 4 51 | Delay: 290[ns] 52 | Has DC: 1 53 | DCParentport:1 54 | Activeports:1.1.0.0 55 | Configured address: 1003 56 | Man: 00000002 ID: 07d43052 Rev: 00100000 57 | SM0 A:0f00 L: 1 F:00090044 Type:3 58 | FMMU0 Ls:00000000 Ll: 1 Lsb:4 Leb:7 Ps:0f00 Psb:0 Ty:02 Act:01 59 | FMMUfunc 0:1 1:0 2:0 3:0 60 | MBX length wr: 0 rd: 0 MBX protocols : 00 61 | CoE details: 00 FoE details: 00 EoE details: 00 SoE details: 00 62 | Ebus current: 100[mA] 63 | only LRD/LWR:0 64 | 65 | Slave:4 66 | Name:EL1004 67 | Output size: 0bits 68 | Input size: 4bits 69 | State: 4 70 | Delay: 445[ns] 71 | Has DC: 1 72 | DCParentport:1 73 | Activeports:1.0.0.0 74 | Configured address: 1004 75 | Man: 00000002 ID: 03ec3052 Rev: 00130000 76 | SM0 A:1000 L: 1 F:00010000 Type:4 77 | FMMU0 Ls:00000005 Ll: 1 Lsb:0 Leb:3 Ps:1000 Psb:0 Ty:01 Act:01 78 | FMMUfunc 0:2 1:0 2:0 3:0 79 | MBX length wr: 0 rd: 0 MBX protocols : 00 80 | CoE details: 00 FoE details: 00 EoE details: 00 SoE details: 00 81 | Ebus current: 90[mA] 82 | only LRD/LWR:0 83 | 84 | Slave:5 85 | Name:LAN9252-EVB-HBI 86 | Output size: 16bits 87 | Input size: 48bits 88 | State: 4 89 | Delay: 1485[ns] 90 | Has DC: 1 91 | DCParentport:2 92 | Activeports:1.1.0.0 93 | Configured address: 1005 94 | Man: 00000009 ID: 00009252 Rev: 00000001 95 | SM0 A:1000 L: 128 F:00010026 Type:1 96 | SM1 A:1080 L: 128 F:00010022 Type:2 97 | SM2 A:1100 L: 2 F:00010064 Type:3 98 | SM3 A:1400 L: 6 F:00010020 Type:4 99 | FMMU0 Ls:00000001 Ll: 2 Lsb:0 Leb:7 Ps:1100 Psb:0 Ty:02 Act:01 100 | FMMU1 Ls:00000006 Ll: 6 Lsb:0 Leb:7 Ps:1400 Psb:0 Ty:01 Act:01 101 | FMMUfunc 0:1 1:2 2:3 3:0 102 | MBX length wr: 128 rd: 128 MBX protocols : 04 103 | CoE details: 23 FoE details: 00 EoE details: 00 SoE details: 00 104 | Ebus current: 0[mA] 105 | only LRD/LWR:0 106 | 107 | Slave:6 108 | Name:LAN9252-EVB-HBI 109 | Output size: 16bits 110 | Input size: 48bits 111 | State: 4 112 | Delay: 2205[ns] 113 | Has DC: 1 114 | DCParentport:1 115 | Activeports:1.0.0.0 116 | Configured address: 1006 117 | Man: 00000009 ID: 00009252 Rev: 00000001 118 | SM0 A:1000 L: 128 F:00010026 Type:1 119 | SM1 A:1080 L: 128 F:00010022 Type:2 120 | SM2 A:1100 L: 2 F:00010064 Type:3 121 | SM3 A:1400 L: 6 F:00010020 Type:4 122 | FMMU0 Ls:00000003 Ll: 2 Lsb:0 Leb:7 Ps:1100 Psb:0 Ty:02 Act:01 123 | FMMU1 Ls:0000000c Ll: 6 Lsb:0 Leb:7 Ps:1400 Psb:0 Ty:01 Act:01 124 | FMMUfunc 0:1 1:2 2:3 3:0 125 | MBX length wr: 128 rd: 128 MBX protocols : 04 126 | CoE details: 23 FoE details: 00 EoE details: 00 SoE details: 00 127 | Ebus current: 0[mA] 128 | only LRD/LWR:0 129 | End slaveinfo, close socket 130 | End program 131 | -------------------------------------------------------------------------------- /dumps/soem-sdinfo-ek1100-el1004.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/soem-sdinfo-ek1100-el1004.pcapng -------------------------------------------------------------------------------- /dumps/soem-sdinfo-ek1100-only.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/soem-sdinfo-ek1100-only.pcapng -------------------------------------------------------------------------------- /dumps/soem-sdinfo-el7-sdo.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/soem-sdinfo-el7-sdo.txt -------------------------------------------------------------------------------- /dumps/soem-sdinfo-no-slaves.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/soem-sdinfo-no-slaves.pcapng -------------------------------------------------------------------------------- /dumps/soem-single-lan9252.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/soem-single-lan9252.pcapng -------------------------------------------------------------------------------- /dumps/twincat-akd-nc-mode.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/twincat-akd-nc-mode.pcapng -------------------------------------------------------------------------------- /dumps/twincat-run-ek1100-el1004-2.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/twincat-run-ek1100-el1004-2.pcapng -------------------------------------------------------------------------------- /dumps/twincat-run-ek1100-el1004.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/twincat-run-ek1100-el1004.pcapng -------------------------------------------------------------------------------- /dumps/twincat-scan-boxes.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/twincat-scan-boxes.pcapng -------------------------------------------------------------------------------- /dumps/twincat-scan-devices-2.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/twincat-scan-devices-2.pcapng -------------------------------------------------------------------------------- /dumps/twincat-scan-devices.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/dumps/twincat-scan-devices.pcapng -------------------------------------------------------------------------------- /ethercrab-wire-derive/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Derives for `ethercrab`. 4 | 5 | 6 | 7 | ## [Unreleased] - ReleaseDate 8 | 9 | ### Changed 10 | 11 | - **(breaking)** [#230](https://github.com/ethercrab-rs/ethercrab/pull/230) Increase MSRV from 1.77 12 | to 1.79. 13 | 14 | ## [0.2.0] - 2024-07-28 15 | 16 | ## [0.1.4] - 2024-03-31 17 | 18 | ### Fixed 19 | 20 | - [#207](https://github.com/ethercrab-rs/ethercrab/pull/207) Generate `EtherCrabWireSized` for 21 | write-only enums. 22 | 23 | ## [0.1.3] - 2024-03-27 24 | 25 | ## [0.1.2] - 2024-02-03 26 | 27 | ### Changed 28 | 29 | - [#160](https://github.com/ethercrab-rs/ethercrab/pull/160) Packing buffers are now zeroed before 30 | being written into. 31 | 32 | ### Added 33 | 34 | - [#159](https://github.com/ethercrab-rs/ethercrab/pull/159) Support `i*` enum discriminants. Also 35 | adds support for `u64`. `usize` and `isize` are explicitly unsupported as they can change size on 36 | different targets. 37 | 38 | ## [0.1.1] - 2024-01-11 39 | 40 | ## [0.1.0] - 2024-01-11 41 | 42 | ### Added 43 | 44 | - Initial release 45 | 46 | 47 | 48 | [unreleased]: https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-derive-v0.2.0...HEAD 49 | [0.2.0]: 50 | https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-derive-v0.1.4...ethercrab-wire-derive-v0.2.0 51 | [0.1.4]: 52 | https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-derive-v0.1.3...ethercrab-wire-derive-v0.1.4 53 | [0.1.3]: 54 | https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-derive-v0.1.2...ethercrab-wire-derive-v0.1.3 55 | [0.1.2]: 56 | https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-derive-v0.1.1...ethercrab-wire-derive-v0.1.2 57 | [0.1.1]: 58 | https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-derive-v0.1.0...ethercrab-wire-derive-v0.1.1 59 | [0.1.0]: https://github.com/ethercrab-rs/ethercrab/compare/HEAD...ethercrab-wire-derive-v0.1.0 60 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethercrab-wire-derive" 3 | version = "0.2.0" 4 | edition = "2021" 5 | categories = ["science::robotics", "no-std", "network-programming"] 6 | license = "MIT OR Apache-2.0" 7 | readme = "README.md" 8 | repository = "https://github.com/ethercrab-rs/ethercrab" 9 | documentation = "https://docs.rs/ethercrab-derive" 10 | description = "Derive macros for EtherCrab" 11 | resolver = "2" 12 | keywords = ["ethercat", "ethercrab", "beckhoff", "automation", "fieldbus"] 13 | rust-version = "1.79" 14 | 15 | [lib] 16 | proc-macro = true 17 | # Explicitly written here to make cargo-readme happy 18 | path = "src/lib.rs" 19 | 20 | [dependencies] 21 | criterion = { version = "0.5.1", default-features = false } 22 | proc-macro2 = "1.0.73" 23 | quote = "1.0.34" 24 | syn = { version = "2.0.44", features = ["full"] } 25 | 26 | [dev-dependencies] 27 | trybuild = "1.0.86" 28 | ethercrab-wire = { path = "../ethercrab-wire" } 29 | syn = { version = "2.0.44", features = ["full", "extra-traits"] } 30 | 31 | [[bench]] 32 | name = "derive-struct" 33 | harness = false 34 | 35 | [[bench]] 36 | name = "derive-enum" 37 | harness = false 38 | 39 | [[bench]] 40 | name = "derive-large" 41 | harness = false 42 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://circleci.com/gh/ethercrab-rs/ethercrab/tree/main.svg?style=shield)](https://circleci.com/gh/ethercrab-rs/ethercrab/tree/main) 2 | [![Crates.io](https://img.shields.io/crates/v/ethercrab-wire-derive.svg)](https://crates.io/crates/ethercrab-wire-derive) 3 | [![Docs.rs](https://docs.rs/ethercrab-wire-derive/badge.svg)](https://docs.rs/ethercrab-wire-derive) 4 | 5 | Derive attributes for [`ethercrab-wire`]. 6 | 7 | ## Experimental 8 | 9 | This crate is in its early stages and may contain bugs or publish breaking changes at any time. 10 | It is in use by [`ethercrab`] and is well exercised there, but please use with caution in your 11 | own code. 12 | 13 | These derives support both structs with bit- and (multi)byte-sized fields for structs, as well 14 | as enums with optional catch-all variant. 15 | 16 | ## Supported attributes 17 | 18 | ### Structs 19 | 20 | - `#[wire(bits = N)]` OR `#[wire(bytes = N)]` 21 | 22 | The size of this struct when packed on the wire. These attributes may not be present at the 23 | same time. 24 | 25 | ### Struct fields 26 | 27 | - `#[wire(bits = N)]` OR `#[wire(bytes = N)]` 28 | 29 | How many bytes this field consumes on the wire. These attributes may not be present at the 30 | same time. 31 | 32 | - `#[wire(pre_skip = N)]` OR `#[wire(pre_skip_bytes = N)]` 33 | 34 | Skip one or more whole bytes before or after this field in the packed representation. 35 | 36 | - `#[wire(post_skip = N)]` OR `#[wire(post_skip_bytes = N)]` 37 | 38 | How many bits or bytes to skip in the raw data **after** this field. 39 | 40 | These attributes are only applicable to fields that are less than 8 bits wide. 41 | 42 | ### Enums 43 | 44 | Enums must have a `#[repr()]` attribute, as well as implement the `Copy` trait. 45 | 46 | ### Enum discriminants 47 | 48 | Enum discriminants may not contain fields. 49 | 50 | - `#[wire(alternatives = [])]` 51 | 52 | A discriminant with this attribute will be parsed successfully if either its direct value or 53 | any of the listed alternatives are found in the input data. 54 | 55 | The discriminant value is used when packing _to_ the wire. 56 | 57 | - `#[wire(catch_all)]` 58 | 59 | Apply this once to a discriminant with a single unnamed field the same type as the enum's 60 | `#[repr()]` to catch any unrecognised values. 61 | 62 | ## Examples 63 | 64 | ### A struct with both bit fields and multi-byte fields. 65 | 66 | ```rust 67 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 68 | #[wire(bytes = 4)] 69 | struct Mixed { 70 | #[wire(bits = 1)] 71 | one_bit: u8, 72 | #[wire(bits = 2)] 73 | two_bits: u8, 74 | 75 | // Fields that are 8 bits or larger must be byte aligned, so we skip the two remaining bits 76 | // of the previous byte with `post_skip`. 77 | #[wire(bits = 3, post_skip = 2)] 78 | three_bits: u8, 79 | 80 | /// Whole `u8` 81 | #[wire(bytes = 1)] 82 | one_byte: u8, 83 | 84 | /// Whole `u16` 85 | #[wire(bytes = 2)] 86 | one_word: u16, 87 | } 88 | ``` 89 | 90 | ### Enum with catch all discriminant and alternatives 91 | 92 | ```rust 93 | #[derive(Copy, Clone, ethercrab_wire::EtherCrabWireReadWrite)] 94 | #[repr(u8)] 95 | enum OneByte { 96 | Foo = 0x01, 97 | #[wire(alternatives = [ 3, 4, 5, 6 ])] 98 | Bar = 0x02, 99 | Baz = 0x07, 100 | Quux = 0xab, 101 | #[wire(catch_all)] 102 | Unknown(u8), 103 | } 104 | 105 | // Normal discriminant 106 | assert_eq!(OneByte::unpack_from_slice(&[0x07]), Ok(OneByte::Baz)); 107 | 108 | // Alternative value for `Bar` 109 | assert_eq!(OneByte::unpack_from_slice(&[0x05]), Ok(OneByte::Bar)); 110 | 111 | // Catch all 112 | assert_eq!(OneByte::unpack_from_slice(&[0xaa]), Ok(OneByte::Unknown(0xaa))); 113 | ``` 114 | 115 | ## Struct field alignment 116 | 117 | Struct fields of 1 byte or more MUST be byte-aligned. For example, the following struct will be 118 | rejected due to `bar` being 5 bits "early": 119 | 120 | ```rust,compile_fail 121 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 122 | #[wire(bytes = 2)] 123 | struct Broken { 124 | #[wire(bits = 3)] 125 | foo: u8, 126 | 127 | // There are 5 bits here unaccounted for 128 | 129 | #[wire(bytes = 1)] 130 | bar: u8, 131 | } 132 | ``` 133 | 134 | This can easily be fixed by using the `pre_skip` or `post_skip` attributes to realign the next 135 | field to 8 bits (or skip whole bytes of the input data): 136 | 137 | ```rust 138 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 139 | #[wire(bytes = 2)] 140 | struct Fixed { 141 | #[wire(bits = 3, post_skip = 5)] 142 | foo: u8, 143 | #[wire(bytes = 1)] 144 | bar: u8, 145 | } 146 | ``` 147 | 148 | A field in the middle of a byte can be written as such, maintaining 8 bit alignment: 149 | 150 | ```rust 151 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 152 | #[wire(bytes = 1)] 153 | struct Middle { 154 | #[wire(pre_skip = 2, bits = 3, post_skip = 3)] 155 | foo: u8, 156 | } 157 | ``` 158 | 159 | [`ethercrab`]: https://docs.rs/ethercrab 160 | [`ethercrab-wire`]: https://docs.rs/ethercrab-wire 161 | 162 | ## License 163 | 164 | Licensed under either of 165 | 166 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 167 | http://www.apache.org/licenses/LICENSE-2.0) 168 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 169 | 170 | at your option. 171 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/README.tpl: -------------------------------------------------------------------------------- 1 | [![Build Status](https://circleci.com/gh/ethercrab-rs/ethercrab/tree/main.svg?style=shield)](https://circleci.com/gh/ethercrab-rs/ethercrab/tree/main) 2 | [![Crates.io](https://img.shields.io/crates/v/ethercrab-wire-derive.svg)](https://crates.io/crates/ethercrab-wire-derive) 3 | [![Docs.rs](https://docs.rs/ethercrab-wire-derive/badge.svg)](https://docs.rs/ethercrab-wire-derive) 4 | 5 | {{readme}} 6 | 7 | ## License 8 | 9 | Licensed under either of 10 | 11 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 12 | http://www.apache.org/licenses/LICENSE-2.0) 13 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 14 | 15 | at your option. 16 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/benches/derive-enum.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireWrite, EtherCrabWireWriteSized}; 3 | 4 | pub fn fallible(c: &mut Criterion) { 5 | #[derive(Copy, Clone, ethercrab_wire::EtherCrabWireReadWrite)] 6 | #[repr(u8)] 7 | enum OneByte { 8 | Foo = 0x01, 9 | Bar = 0x02, 10 | Baz = 0x03, 11 | Quux = 0xab, 12 | } 13 | 14 | let input_data = [0xab]; 15 | 16 | c.bench_function("enum 1 byte unpack", |b| { 17 | b.iter(|| OneByte::unpack_from_slice(black_box(&input_data))) 18 | }); 19 | 20 | let instance = OneByte::unpack_from_slice(&input_data).unwrap(); 21 | 22 | c.bench_function("enum 1 byte pack array", |b| { 23 | b.iter(|| black_box(instance.pack())) 24 | }); 25 | 26 | c.bench_function("enum 1 byte pack slice unchecked", |b| { 27 | b.iter(|| { 28 | let mut buf = [0u8; 16]; 29 | 30 | instance.pack_to_slice_unchecked(black_box(&mut buf)); 31 | }) 32 | }); 33 | 34 | c.bench_function("enum 1 byte pack slice checked", |b| { 35 | b.iter(|| { 36 | let mut buf = [0u8; 16]; 37 | 38 | let _ = instance.pack_to_slice(black_box(&mut buf)); 39 | }) 40 | }); 41 | } 42 | 43 | pub fn infallible(c: &mut Criterion) { 44 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 45 | #[repr(u8)] 46 | enum OneByte { 47 | Foo = 0x01, 48 | Bar = 0x02, 49 | Baz = 0x03, 50 | Quux = 0xab, 51 | #[wire(catch_all)] 52 | Unknown(u8), 53 | } 54 | 55 | let input_data = [0xff]; 56 | 57 | c.bench_function("enum 1 byte unknown unpack", |b| { 58 | b.iter(|| OneByte::unpack_from_slice(black_box(&input_data))) 59 | }); 60 | 61 | let instance = OneByte::unpack_from_slice(&input_data).unwrap(); 62 | 63 | c.bench_function("enum 1 byte unknown pack array", |b| { 64 | b.iter(|| black_box(instance.pack())) 65 | }); 66 | 67 | c.bench_function("enum 1 byte unknown pack slice unchecked", |b| { 68 | b.iter(|| { 69 | let mut buf = [0u8; 16]; 70 | 71 | instance.pack_to_slice_unchecked(black_box(&mut buf)); 72 | }) 73 | }); 74 | 75 | c.bench_function("enum 1 byte unknown pack slice checked", |b| { 76 | b.iter(|| { 77 | let mut buf = [0u8; 16]; 78 | 79 | let _ = instance.pack_to_slice(black_box(&mut buf)); 80 | }) 81 | }); 82 | } 83 | 84 | criterion_group!(enums, fallible, infallible); 85 | criterion_main!(enums); 86 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/benches/derive-large.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireWrite, EtherCrabWireWriteSized}; 3 | 4 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 5 | #[wire(bytes = 16)] 6 | struct NormalTypes { 7 | #[wire(bytes = 4)] 8 | foo: u32, 9 | 10 | #[wire(bytes = 2)] 11 | bar: u16, 12 | 13 | #[wire(bytes = 2)] 14 | baz: u16, 15 | 16 | #[wire(bytes = 8)] 17 | huge: u64, 18 | } 19 | 20 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 21 | #[wire(bytes = 5)] 22 | struct Mixed { 23 | #[wire(bits = 1)] 24 | a: u8, 25 | #[wire(bits = 2)] 26 | b: u8, 27 | #[wire(bits = 3)] 28 | c: u8, 29 | #[wire(bits = 2)] 30 | d: u8, 31 | 32 | /// Whole u8 33 | #[wire(bytes = 1)] 34 | e: u8, 35 | 36 | #[wire(bits = 3)] 37 | f: u8, 38 | #[wire(bits = 3)] 39 | g: u8, 40 | #[wire(bits = 2)] 41 | h: u8, 42 | 43 | /// Whole u16 44 | #[wire(bytes = 2)] 45 | i: u16, 46 | } 47 | 48 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 49 | #[wire(bytes = 2)] 50 | struct Packed { 51 | #[wire(bits = 1)] 52 | a: u8, 53 | #[wire(bits = 2)] 54 | b: u8, 55 | #[wire(bits = 3)] 56 | c: u8, 57 | #[wire(bits = 2)] 58 | d: u8, 59 | #[wire(bits = 3)] 60 | f: u8, 61 | #[wire(bits = 3)] 62 | g: u8, 63 | #[wire(bits = 2)] 64 | h: u8, 65 | } 66 | 67 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 68 | #[repr(u8)] 69 | enum OneByte { 70 | Foo = 0x01, 71 | Bar = 0x02, 72 | Baz = 0x03, 73 | Quux = 0xab, 74 | #[wire(catch_all)] 75 | Unknown(u8), 76 | } 77 | 78 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 79 | #[wire(bytes = 12)] 80 | struct Inner { 81 | #[wire(bytes = 2)] 82 | foo: Packed, 83 | #[wire(bytes = 4)] 84 | bar: u32, 85 | #[wire(bytes = 5)] 86 | baz: Mixed, 87 | #[wire(bytes = 1)] 88 | quux: OneByte, 89 | } 90 | 91 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 92 | #[wire(bytes = 36)] 93 | struct LotsOfNesting { 94 | #[wire(bytes = 2)] 95 | something: u16, 96 | #[wire(bytes = 12)] 97 | inner: Inner, 98 | #[wire(bytes = 16)] 99 | normal: NormalTypes, 100 | #[wire(bytes = 5)] 101 | mixed: Mixed, 102 | #[wire(bytes = 1)] 103 | one_byte: OneByte, 104 | } 105 | 106 | pub fn nested(c: &mut Criterion) { 107 | let input_data = [ 108 | 0x47, 0x0e, 0xb2, 0x9d, 0x66, 0x83, 0x21, 0xf3, 0xae, 0xa4, 0x38, 0x89, 0xaf, 0xae, 0xf7, 109 | 0x19, 0x44, 0x8c, 0x7d, 0x80, 0xc2, 0x36, 0xd0, 0xb3, 0x40, 0x35, 0x76, 0x9c, 0xf3, 0xe3, 110 | 0x7d, 0x70, 0x4f, 0x11, 0xa5, 0x60, 111 | ]; 112 | 113 | c.bench_function("nested struct unpack", |b| { 114 | b.iter(|| LotsOfNesting::unpack_from_slice(black_box(&input_data))) 115 | }); 116 | 117 | let instance = LotsOfNesting::unpack_from_slice(&input_data).unwrap(); 118 | 119 | c.bench_function("nested struct pack array", |b| { 120 | b.iter(|| black_box(instance.pack())) 121 | }); 122 | 123 | c.bench_function("nested struct pack slice unchecked", |b| { 124 | b.iter(|| { 125 | let mut buf = [0u8; 36]; 126 | 127 | instance.pack_to_slice_unchecked(black_box(&mut buf)); 128 | }) 129 | }); 130 | 131 | c.bench_function("nested struct pack slice checked", |b| { 132 | b.iter(|| { 133 | let mut buf = [0u8; 36]; 134 | 135 | let _ = instance.pack_to_slice(black_box(&mut buf)); 136 | }) 137 | }); 138 | } 139 | 140 | criterion_group!(large, nested); 141 | criterion_main!(large); 142 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/benches/derive-struct.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireWrite, EtherCrabWireWriteSized}; 3 | 4 | pub fn normal_types(c: &mut Criterion) { 5 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 6 | #[wire(bytes = 16)] 7 | struct NormalTypes { 8 | #[wire(bytes = 4)] 9 | foo: u32, 10 | 11 | #[wire(bytes = 2)] 12 | bar: u16, 13 | 14 | #[wire(bytes = 2)] 15 | baz: u16, 16 | 17 | #[wire(bytes = 8)] 18 | huge: u64, 19 | } 20 | 21 | let input_data = [ 22 | 0xbf, 0x7e, 0xc0, 0x07, 0xab, 0xa1, 0xc2, 0x22, 0x45, 0x23, 0xaa, 0x68, 0x47, 0xbf, 0xff, 23 | 0xea, 24 | ]; 25 | 26 | c.bench_function("u* struct unpack", |b| { 27 | b.iter(|| NormalTypes::unpack_from_slice(black_box(&input_data))) 28 | }); 29 | 30 | let instance = NormalTypes::unpack_from_slice(&input_data).unwrap(); 31 | 32 | c.bench_function("u* struct pack array", |b| { 33 | b.iter(|| black_box(instance.pack())) 34 | }); 35 | 36 | c.bench_function("u* struct pack slice unchecked", |b| { 37 | b.iter(|| { 38 | let mut buf = [0u8; 16]; 39 | 40 | instance.pack_to_slice_unchecked(black_box(&mut buf)); 41 | }) 42 | }); 43 | 44 | c.bench_function("u* struct pack slice checked", |b| { 45 | b.iter(|| { 46 | let mut buf = [0u8; 16]; 47 | 48 | let _ = instance.pack_to_slice(black_box(&mut buf)); 49 | }) 50 | }); 51 | } 52 | 53 | pub fn packed_and_normal(c: &mut Criterion) { 54 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 55 | #[wire(bytes = 5)] 56 | struct Mixed { 57 | #[wire(bits = 1)] 58 | a: u8, 59 | #[wire(bits = 2)] 60 | b: u8, 61 | #[wire(bits = 3)] 62 | c: u8, 63 | #[wire(bits = 2)] 64 | d: u8, 65 | 66 | /// Whole u8 67 | #[wire(bytes = 1)] 68 | e: u8, 69 | 70 | #[wire(bits = 3)] 71 | f: u8, 72 | #[wire(bits = 3)] 73 | g: u8, 74 | #[wire(bits = 2)] 75 | h: u8, 76 | 77 | /// Whole u16 78 | #[wire(bytes = 2)] 79 | i: u16, 80 | } 81 | 82 | let input_data = [0xc7, 0x0c, 0xfe, 0xc6, 0x50]; 83 | 84 | c.bench_function("mixed struct unpack", |b| { 85 | b.iter(|| Mixed::unpack_from_slice(black_box(&input_data))) 86 | }); 87 | 88 | let instance = Mixed::unpack_from_slice(&input_data).unwrap(); 89 | 90 | c.bench_function("mixed struct pack array", |b| { 91 | b.iter(|| black_box(instance.pack())) 92 | }); 93 | 94 | c.bench_function("mixed struct pack slice unchecked", |b| { 95 | b.iter(|| { 96 | let mut buf = [0u8; 5]; 97 | 98 | instance.pack_to_slice_unchecked(black_box(&mut buf)); 99 | }) 100 | }); 101 | 102 | c.bench_function("mixed struct pack slice checked", |b| { 103 | b.iter(|| { 104 | let mut buf = [0u8; 5]; 105 | 106 | let _ = instance.pack_to_slice(black_box(&mut buf)); 107 | }) 108 | }); 109 | } 110 | 111 | pub fn packed(c: &mut Criterion) { 112 | #[derive(ethercrab_wire::EtherCrabWireReadWrite)] 113 | #[wire(bytes = 2)] 114 | struct Packed { 115 | #[wire(bits = 1)] 116 | a: u8, 117 | #[wire(bits = 2)] 118 | b: u8, 119 | #[wire(bits = 3)] 120 | c: u8, 121 | #[wire(bits = 2)] 122 | d: u8, 123 | #[wire(bits = 3)] 124 | f: u8, 125 | #[wire(bits = 3)] 126 | g: u8, 127 | #[wire(bits = 2)] 128 | h: u8, 129 | } 130 | 131 | let input_data = [0xc7, 0x0c, 0xfe, 0xc6, 0x50]; 132 | 133 | c.bench_function("packed struct unpack", |b| { 134 | b.iter(|| Packed::unpack_from_slice(black_box(&input_data))) 135 | }); 136 | 137 | let instance = Packed::unpack_from_slice(&input_data).unwrap(); 138 | 139 | c.bench_function("packed struct pack array", |b| { 140 | b.iter(|| black_box(instance.pack())) 141 | }); 142 | 143 | c.bench_function("packed struct pack slice unchecked", |b| { 144 | b.iter(|| { 145 | let mut buf = [0u8; 5]; 146 | 147 | instance.pack_to_slice_unchecked(black_box(&mut buf)); 148 | }) 149 | }); 150 | 151 | c.bench_function("packed struct pack slice checked", |b| { 152 | b.iter(|| { 153 | let mut buf = [0u8; 5]; 154 | 155 | let _ = instance.pack_to_slice(black_box(&mut buf)); 156 | }) 157 | }); 158 | } 159 | 160 | criterion_group!(structs, normal_types, packed_and_normal, packed); 161 | criterion_main!(structs); 162 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/src/parse_enum.rs: -------------------------------------------------------------------------------- 1 | use crate::help::{ 2 | all_valid_attrs, attr_exists, enum_repr_ty, variant_alternatives, variant_is_default, 3 | }; 4 | use syn::{DataEnum, DeriveInput, Expr, ExprLit, ExprUnary, Ident, Lit, UnOp}; 5 | 6 | #[derive(Clone)] 7 | pub struct EnumMeta { 8 | /// Width in bits on the wire. 9 | // pub width: usize, 10 | pub repr_type: Ident, 11 | 12 | pub variants: Vec, 13 | 14 | pub catch_all: Option, 15 | pub default_variant: Option, 16 | } 17 | 18 | #[derive(Clone)] 19 | pub struct VariantMeta { 20 | pub name: Ident, 21 | pub discriminant: i128, 22 | pub catch_all: bool, 23 | #[allow(unused)] 24 | pub default: bool, 25 | #[allow(unused)] 26 | pub alternatives: Vec, 27 | } 28 | 29 | pub fn parse_enum( 30 | e: DataEnum, 31 | DeriveInput { attrs, ident, .. }: DeriveInput, 32 | ) -> syn::Result { 33 | // let width = bit_width_attr(&attrs)?; 34 | 35 | all_valid_attrs(&attrs, &["bits", "bytes"])?; 36 | 37 | let repr = enum_repr_ty(&attrs, &ident)?; 38 | 39 | if ["isize", "usize"].iter().any(|bad| repr == bad) { 40 | return Err(syn::Error::new( 41 | repr.span(), 42 | "usize and isize may not be used as enum repr as these types can change size based on target platform. Use an i* or u* type instead.".to_string(), 43 | )); 44 | } 45 | 46 | // --- Variants 47 | 48 | let mut discriminant_accum = 0; 49 | let mut variants = Vec::new(); 50 | let mut catch_all = None; 51 | let mut default_variant = None; 52 | 53 | for variant in e.variants { 54 | all_valid_attrs(&variant.attrs, &["alternatives", "catch_all"])?; 55 | 56 | let ident = variant.ident; 57 | 58 | let variant_discriminant = match variant.discriminant { 59 | Some(( 60 | _, 61 | Expr::Lit(ExprLit { 62 | lit: Lit::Int(discr), 63 | .. 64 | }), 65 | )) => { 66 | // Parse to i128 to fit any possible value we could encounter 67 | discr.base10_parse::()? 68 | } 69 | Some(( 70 | _, 71 | Expr::Unary(ExprUnary { 72 | expr, 73 | op: UnOp::Neg(_), 74 | .. 75 | }), 76 | )) => { 77 | match *expr { 78 | Expr::Lit(ExprLit { 79 | lit: Lit::Int(discr), 80 | .. 81 | }) => { 82 | discr 83 | // Parse to i128 to fit any possible value we could encounter 84 | .base10_parse::() 85 | // Negate value because we matched on `UnOp::Neg` above. 86 | .map(|value| -value)? 87 | } 88 | _ => return Err(syn::Error::new(repr.span(), "Invalid discriminant format")), 89 | } 90 | } 91 | None => discriminant_accum + 1, 92 | _ => return Err(syn::Error::new(repr.span(), "Invalid discriminant format")), 93 | }; 94 | 95 | let is_default = variant_is_default(&variant.attrs); 96 | let is_catch_all = attr_exists(&variant.attrs, "catch_all"); 97 | 98 | let alternatives = variant_alternatives(&variant.attrs)?; 99 | 100 | if is_catch_all && !alternatives.is_empty() { 101 | return Err(syn::Error::new( 102 | ident.span(), 103 | "Catch all cannot have alternatives", 104 | )); 105 | } 106 | 107 | let record = VariantMeta { 108 | name: ident.clone(), 109 | discriminant: variant_discriminant, 110 | catch_all: is_catch_all, 111 | alternatives: alternatives.clone(), 112 | default: is_default, 113 | }; 114 | 115 | if is_catch_all { 116 | let old = catch_all.replace(record.clone()); 117 | 118 | if old.is_some() { 119 | return Err(syn::Error::new( 120 | ident.span(), 121 | "Only one catch all variant is allowed", 122 | )); 123 | } 124 | } 125 | 126 | if is_default { 127 | let old = default_variant.replace(record.clone()); 128 | 129 | if old.is_some() { 130 | return Err(syn::Error::new( 131 | ident.span(), 132 | "Only one default variant is allowed", 133 | )); 134 | } 135 | } 136 | 137 | discriminant_accum = variant_discriminant; 138 | 139 | variants.push(record.clone()); 140 | 141 | for alternative in alternatives { 142 | let alt = VariantMeta { 143 | name: ident.clone(), 144 | discriminant: alternative, 145 | alternatives: Vec::new(), 146 | default: false, 147 | catch_all: false, 148 | }; 149 | 150 | variants.push(alt); 151 | 152 | discriminant_accum = alternative; 153 | } 154 | } 155 | 156 | Ok(EnumMeta { 157 | // width, 158 | repr_type: repr, 159 | variants, 160 | catch_all, 161 | default_variant, 162 | }) 163 | } 164 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/src/parse_struct.rs: -------------------------------------------------------------------------------- 1 | use crate::help::{all_valid_attrs, attr_exists, bit_width_attr, usize_attr}; 2 | use std::ops::Range; 3 | use syn::{DataStruct, DeriveInput, Fields, FieldsNamed, Ident, Type, Visibility}; 4 | 5 | #[derive(Clone)] 6 | pub struct StructMeta { 7 | /// Width in bits on the wire. 8 | pub width_bits: usize, 9 | 10 | pub fields: Vec, 11 | } 12 | 13 | #[derive(Clone)] 14 | pub struct FieldMeta { 15 | #[allow(unused)] 16 | pub vis: Visibility, 17 | pub name: Ident, 18 | pub ty: Type, 19 | // Will be None for arrays 20 | pub ty_name: Option, 21 | #[allow(unused)] 22 | pub bit_start: usize, 23 | #[allow(unused)] 24 | pub bit_end: usize, 25 | #[allow(unused)] 26 | pub byte_start: usize, 27 | #[allow(unused)] 28 | pub byte_end: usize, 29 | /// Offset of the starting bit in the starting byte. 30 | pub bit_offset: usize, 31 | 32 | pub bits: Range, 33 | pub bytes: Range, 34 | 35 | #[allow(unused)] 36 | pub pre_skip: Option, 37 | #[allow(unused)] 38 | pub post_skip: Option, 39 | 40 | pub skip: bool, 41 | } 42 | 43 | pub fn parse_struct( 44 | s: DataStruct, 45 | DeriveInput { attrs, ident, .. }: DeriveInput, 46 | ) -> syn::Result { 47 | // --- Struct attributes 48 | 49 | all_valid_attrs(&attrs, &["bits", "bytes"])?; 50 | 51 | let width = bit_width_attr(&attrs)?; 52 | 53 | let Some(width) = width else { 54 | return Err(syn::Error::new( 55 | ident.span(), 56 | "Struct total bit width is required, e.g. #[wire(bits = 32)]", 57 | )); 58 | }; 59 | 60 | // --- Fields 61 | 62 | let Fields::Named(FieldsNamed { named: fields, .. }) = s.fields else { 63 | return Err(syn::Error::new( 64 | ident.span(), 65 | "Only structs with named fields can be derived.", 66 | )); 67 | }; 68 | 69 | let mut total_field_width = 0; 70 | 71 | let mut field_meta = Vec::new(); 72 | 73 | for field in fields { 74 | all_valid_attrs( 75 | &field.attrs, 76 | &[ 77 | "bits", 78 | "bytes", 79 | "skip", 80 | "pre_skip", 81 | "pre_skip_bytes", 82 | "post_skip", 83 | "post_skip_bytes", 84 | ], 85 | )?; 86 | 87 | // Unwrap: this is a named-field struct so the field will always have a name. 88 | let field_name = field.ident.unwrap(); 89 | let field_width = bit_width_attr(&field.attrs)?; 90 | 91 | // Whether to ignore this field when sending AND receiving 92 | let skip = attr_exists(&field.attrs, "skip"); 93 | 94 | let pre_skip = usize_attr(&field.attrs, "pre_skip")? 95 | .or(usize_attr(&field.attrs, "pre_skip_bytes")?.map(|bytes| bytes * 8)) 96 | .filter(|_| !skip); 97 | 98 | let post_skip = usize_attr(&field.attrs, "post_skip")? 99 | .or(usize_attr(&field.attrs, "post_skip_bytes")?.map(|bytes| bytes * 8)) 100 | .filter(|_| !skip); 101 | 102 | if let Some(skip) = pre_skip { 103 | total_field_width += skip; 104 | } 105 | 106 | let bit_start = total_field_width; 107 | let bit_end = field_width.map_or(total_field_width, |w| total_field_width + w); 108 | let byte_start = bit_start / 8; 109 | let byte_end = bit_end.div_ceil(8); 110 | let bytes = byte_start..byte_end; 111 | let bit_offset = bit_start % 8; 112 | let bits = bit_start..bit_end; 113 | 114 | let ty_name = match field.ty.clone() { 115 | Type::Path(path) => path.path.get_ident().cloned(), 116 | _ => None, 117 | }; 118 | 119 | let meta = FieldMeta { 120 | name: field_name, 121 | vis: field.vis, 122 | ty: field.ty, 123 | ty_name, 124 | 125 | bits, 126 | bytes, 127 | 128 | bit_start, 129 | bit_end, 130 | byte_start, 131 | byte_end, 132 | 133 | bit_offset, 134 | 135 | pre_skip, 136 | post_skip, 137 | 138 | skip, 139 | }; 140 | 141 | // Validation if we're not skipping this field 142 | if !skip { 143 | let Some(field_width) = field_width else { 144 | return Err(syn::Error::new( 145 | meta.name.span(), 146 | "Field must have a width attribute, e.g. #[wire(bits = 4)]", 147 | )); 148 | }; 149 | 150 | if meta.bytes.len() > 1 && (bit_offset > 0 || field_width % 8 > 0) { 151 | return Err(syn::Error::new( 152 | meta.name.span(), 153 | format!("Multibyte fields must be byte-aligned at start and end. Current bit position {}", total_field_width), 154 | )); 155 | } 156 | 157 | if meta.bits.len() < 8 && meta.bytes.len() > 1 { 158 | return Err(syn::Error::new( 159 | meta.name.span(), 160 | "Fields smaller than 8 bits may not cross byte boundaries", 161 | )); 162 | } 163 | 164 | total_field_width += field_width; 165 | } 166 | 167 | if let Some(skip) = post_skip { 168 | total_field_width += skip; 169 | } 170 | 171 | field_meta.push(meta); 172 | } 173 | 174 | if total_field_width != width { 175 | return Err(syn::Error::new( 176 | ident.span(), 177 | format!( 178 | "Total field width is {}, expected {} from struct definition", 179 | total_field_width, width 180 | ), 181 | )); 182 | } 183 | 184 | Ok(StructMeta { 185 | width_bits: width, 186 | fields: field_meta, 187 | }) 188 | } 189 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/ui/enum-isize.rs: -------------------------------------------------------------------------------- 1 | #[derive(ethercrab_wire::EtherCrabWireWrite)] 2 | #[repr(isize)] 3 | enum NoIsize { 4 | Foo, 5 | Bar, 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/ui/enum-isize.stderr: -------------------------------------------------------------------------------- 1 | error: usize and isize may not be used as enum repr as these types can change size based on target platform. Use an i* or u* type instead. 2 | --> ui/enum-isize.rs:2:8 3 | | 4 | 2 | #[repr(isize)] 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/ui/enum-usize.rs: -------------------------------------------------------------------------------- 1 | #[derive(ethercrab_wire::EtherCrabWireWrite)] 2 | #[repr(usize)] 3 | enum NoUsize { 4 | Foo, 5 | Bar, 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/ui/enum-usize.stderr: -------------------------------------------------------------------------------- 1 | error: usize and isize may not be used as enum repr as these types can change size based on target platform. Use an i* or u* type instead. 2 | --> ui/enum-usize.rs:2:8 3 | | 4 | 2 | #[repr(usize)] 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/ui/union.rs: -------------------------------------------------------------------------------- 1 | #[derive(ethercrab_wire::EtherCrabWireWrite)] 2 | union Whatever { 3 | i: u32, 4 | f: f32, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /ethercrab-wire-derive/ui/union.stderr: -------------------------------------------------------------------------------- 1 | error: Unions are not supported 2 | --> ui/union.rs:2:7 3 | | 4 | 2 | union Whatever { 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /ethercrab-wire/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Traits used for converting optionally packed values to/from raw data as represented in the EtherCAT 4 | specification. 5 | 6 | Primarily used by `ethercrab`. 7 | 8 | 9 | 10 | ## [Unreleased] - ReleaseDate 11 | 12 | ### Changed 13 | 14 | - **(breaking)** [#230](https://github.com/ethercrab-rs/ethercrab/pull/230) Increase MSRV from 1.77 15 | to 1.79. 16 | 17 | ## [0.2.0] - 2024-07-28 18 | 19 | ### Changed 20 | 21 | - **(breaking)** [#218](https://github.com/ethercrab-rs/ethercrab/pull/218) Removed `expected` and 22 | `got` fields from `WireError::{Read,Write}BufferTooShort`. 23 | - **(breaking)** [#218](https://github.com/ethercrab-rs/ethercrab/pull/218) Increase MSRV from 1.75 24 | to 1.77. 25 | 26 | ## [0.1.4] - 2024-03-31 27 | 28 | ## [0.1.3] - 2024-03-27 29 | 30 | ### Added 31 | 32 | - [#183](https://github.com/ethercrab-rs/ethercrab/pull/183) Add support for encoding/decoding 33 | tuples up to 16 items long. 34 | 35 | ## [0.1.2] - 2024-02-03 36 | 37 | ### Changed 38 | 39 | - [#160](https://github.com/ethercrab-rs/ethercrab/pull/160) Packing buffers are now zeroed before 40 | being written into. 41 | 42 | ## [0.1.1] - 2024-01-11 43 | 44 | ## [0.1.0] - 2024-01-11 45 | 46 | ### Added 47 | 48 | - Initial release 49 | 50 | 51 | 52 | [unreleased]: https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-v0.2.0...HEAD 53 | [0.2.0]: 54 | https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-v0.1.4...ethercrab-wire-v0.2.0 55 | [0.1.4]: 56 | https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-v0.1.3...ethercrab-wire-v0.1.4 57 | [0.1.3]: 58 | https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-v0.1.2...ethercrab-wire-v0.1.3 59 | [0.1.2]: 60 | https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-v0.1.1...ethercrab-wire-v0.1.2 61 | [0.1.1]: 62 | https://github.com/ethercrab-rs/ethercrab/compare/ethercrab-wire-v0.1.0...ethercrab-wire-v0.1.1 63 | [0.1.0]: https://github.com/ethercrab-rs/ethercrab/compare/HEAD...ethercrab-wire-v0.1.0 64 | -------------------------------------------------------------------------------- /ethercrab-wire/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethercrab-wire" 3 | version = "0.2.0" 4 | edition = "2021" 5 | categories = ["science::robotics", "no-std", "network-programming"] 6 | license = "MIT OR Apache-2.0" 7 | readme = "README.md" 8 | repository = "https://github.com/ethercrab-rs/ethercrab" 9 | documentation = "https://docs.rs/ethercrab-wire" 10 | description = "On-the-wire tools for the EtherCrab crate" 11 | keywords = ["ethercat", "ethercrab", "beckhoff", "automation", "fieldbus"] 12 | resolver = "2" 13 | rust-version = "1.79" 14 | 15 | [dependencies] 16 | defmt = { version = "0.3.5", optional = true } 17 | ethercrab-wire-derive = { version = "0.2.0", path = "../ethercrab-wire-derive" } 18 | heapless = { version = "0.8.0", default-features = false } 19 | 20 | [features] 21 | std = [] 22 | defmt-03 = ["dep:defmt", "heapless/defmt-03"] 23 | -------------------------------------------------------------------------------- /ethercrab-wire/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://circleci.com/gh/ethercrab-rs/ethercrab/tree/main.svg?style=shield)](https://circleci.com/gh/ethercrab-rs/ethercrab/tree/main) 2 | [![Crates.io](https://img.shields.io/crates/v/ethercrab-wire.svg)](https://crates.io/crates/ethercrab-wire) 3 | [![Docs.rs](https://docs.rs/ethercrab-wire/badge.svg)](https://docs.rs/ethercrab-wire) 4 | 5 | Traits used to pack/unpack structs and enums from EtherCAT packets on the wire. 6 | 7 | This crate is designed for use with [`ethercrab`](https://docs.rs/ethercrab) but can be 8 | used standalone too. 9 | 10 | While these traits can be implemented by hand as normal, it is recommended to derive them using 11 | [`ethercrab-wire-derive`](https://docs.rs/ethercrab-wire-derive) where possible. 12 | 13 | ## Experimental 14 | 15 | This crate is in its early stages and may contain bugs or publish breaking changes at any time. 16 | It is in use by [`ethercrab`](https://docs.rs/ethercrab) and is well exercised there, 17 | but please use with caution in your own code. 18 | 19 | ## License 20 | 21 | Licensed under either of 22 | 23 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 24 | http://www.apache.org/licenses/LICENSE-2.0) 25 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 26 | 27 | at your option. 28 | -------------------------------------------------------------------------------- /ethercrab-wire/README.tpl: -------------------------------------------------------------------------------- 1 | [![Build Status](https://circleci.com/gh/ethercrab-rs/ethercrab/tree/main.svg?style=shield)](https://circleci.com/gh/ethercrab-rs/ethercrab/tree/main) 2 | [![Crates.io](https://img.shields.io/crates/v/ethercrab-wire.svg)](https://crates.io/crates/ethercrab-wire) 3 | [![Docs.rs](https://docs.rs/ethercrab-wire/badge.svg)](https://docs.rs/ethercrab-wire) 4 | 5 | {{readme}} 6 | 7 | ## License 8 | 9 | Licensed under either of 10 | 11 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 12 | http://www.apache.org/licenses/LICENSE-2.0) 13 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 14 | 15 | at your option. 16 | -------------------------------------------------------------------------------- /ethercrab-wire/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Encode/decode error. 2 | 3 | /// Wire encode/decode errors. 4 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 5 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 6 | pub enum WireError { 7 | /// The buffer to extract a type from is too short to do so. 8 | ReadBufferTooShort, 9 | /// The buffer to write the packed data into is too short. 10 | WriteBufferTooShort, 11 | /// Invalid enum or struct value. 12 | /// 13 | /// If this comes from an enum, consider adding a variant with `#[wire(catch_all)]` or 14 | /// `#[wire(alternatives = [])]`. 15 | InvalidValue, 16 | /// Failed to create an array of the correct length. 17 | ArrayLength, 18 | /// Valid UTF8 input data is required to decode to a string. 19 | InvalidUtf8, 20 | } 21 | 22 | #[cfg(feature = "std")] 23 | impl std::error::Error for WireError {} 24 | 25 | impl core::fmt::Display for WireError { 26 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 27 | match self { 28 | WireError::ReadBufferTooShort => { 29 | write!(f, "Read buffer too short to extract type from") 30 | } 31 | WireError::WriteBufferTooShort => { 32 | write!(f, "Write buffer too short to pack type into") 33 | } 34 | WireError::InvalidValue => f.write_str("Invalid decoded value"), 35 | WireError::ArrayLength => f.write_str("Incorrect array length"), 36 | WireError::InvalidUtf8 => f.write_str("Invalid UTF8"), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ethercrab-wire/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Traits used to pack/unpack structs and enums from EtherCAT packets on the wire. 2 | //! 3 | //! This crate is designed for use with [`ethercrab`](https://docs.rs/ethercrab) but can be 4 | //! used standalone too. 5 | //! 6 | //! While these traits can be implemented by hand as normal, it is recommended to derive them using 7 | //! [`ethercrab-wire-derive`](https://docs.rs/ethercrab-wire-derive) where possible. 8 | //! 9 | //! # Experimental 10 | //! 11 | //! This crate is in its early stages and may contain bugs or publish breaking changes at any time. 12 | //! It is in use by [`ethercrab`](https://docs.rs/ethercrab) and is well exercised there, 13 | //! but please use with caution in your own code. 14 | 15 | #![cfg_attr(not(feature = "std"), no_std)] 16 | #![deny(missing_docs)] 17 | #![deny(missing_copy_implementations)] 18 | #![deny(trivial_casts)] 19 | #![deny(trivial_numeric_casts)] 20 | #![deny(unused_import_braces)] 21 | #![deny(unused_qualifications)] 22 | #![deny(rustdoc::broken_intra_doc_links)] 23 | #![deny(rustdoc::private_intra_doc_links)] 24 | 25 | mod error; 26 | mod impls; 27 | 28 | pub use error::WireError; 29 | pub use ethercrab_wire_derive::{EtherCrabWireRead, EtherCrabWireReadWrite, EtherCrabWireWrite}; 30 | 31 | /// A type to be received from the wire, according to EtherCAT spec rules (packed bits, little 32 | /// endian). 33 | /// 34 | /// This trait is [derivable](https://docs.rs/ethercrab-wire-derive). 35 | pub trait EtherCrabWireRead: Sized { 36 | /// Unpack this type from the beginning of the given buffer. 37 | fn unpack_from_slice(buf: &[u8]) -> Result; 38 | } 39 | 40 | /// A type to be sent/received on the wire, according to EtherCAT spec rules (packed bits, little 41 | /// endian). 42 | /// 43 | /// This trait is [derivable](https://docs.rs/ethercrab-wire-derive). 44 | pub trait EtherCrabWireWrite { 45 | /// Pack the type and write it into the beginning of `buf`. 46 | /// 47 | /// The default implementation of this method will return an error if the buffer is not long 48 | /// enough. 49 | fn pack_to_slice<'buf>(&self, buf: &'buf mut [u8]) -> Result<&'buf [u8], WireError> { 50 | buf.get(0..self.packed_len()) 51 | .ok_or(WireError::WriteBufferTooShort)?; 52 | 53 | Ok(self.pack_to_slice_unchecked(buf)) 54 | } 55 | 56 | /// Pack the type and write it into the beginning of `buf`. 57 | /// 58 | /// # Panics 59 | /// 60 | /// This method must panic if `buf` is too short to hold the packed data. 61 | fn pack_to_slice_unchecked<'buf>(&self, buf: &'buf mut [u8]) -> &'buf [u8]; 62 | 63 | /// Get the length in bytes of this item when packed. 64 | fn packed_len(&self) -> usize; 65 | } 66 | 67 | /// A type that can be both written to the wire and read back from it. 68 | /// 69 | /// This trait is [derivable](https://docs.rs/ethercrab-wire-derive). 70 | pub trait EtherCrabWireReadWrite: EtherCrabWireRead + EtherCrabWireWrite {} 71 | 72 | impl EtherCrabWireReadWrite for T where T: EtherCrabWireRead + EtherCrabWireWrite {} 73 | 74 | /// Implemented for types with a known size at compile time. 75 | /// 76 | /// This trait is implemented automatically if [`EtherCrabWireRead`], [`EtherCrabWireWrite`] or 77 | /// [`EtherCrabWireReadWrite`] is [derived](https://docs.rs/ethercrab-wire-derive). 78 | pub trait EtherCrabWireSized { 79 | /// Packed size in bytes. 80 | const PACKED_LEN: usize; 81 | 82 | /// Used to define an array of the correct length. This type should be an array `[u8; N]` where 83 | /// `N` is a fixed value or const generic as per the type this trait is implemented on. 84 | type Buffer: AsRef<[u8]> + AsMut<[u8]>; 85 | 86 | /// Create a buffer sized to contain the packed representation of this item. 87 | fn buffer() -> Self::Buffer; 88 | } 89 | 90 | /// Implemented for writeable types with a known size at compile time. 91 | /// 92 | /// This trait is implemented automatically if [`EtherCrabWireWrite`] or [`EtherCrabWireReadWrite`] 93 | /// is [derived](https://docs.rs/ethercrab-wire-derive). 94 | pub trait EtherCrabWireWriteSized: EtherCrabWireSized { 95 | /// Pack this item to a fixed sized array. 96 | fn pack(&self) -> Self::Buffer; 97 | } 98 | 99 | /// A readable type that has a size known at compile time. 100 | /// 101 | /// This trait is [derivable](https://docs.rs/ethercrab-wire-derive). 102 | pub trait EtherCrabWireReadSized: EtherCrabWireRead + EtherCrabWireSized {} 103 | 104 | impl EtherCrabWireReadSized for T where T: EtherCrabWireRead + EtherCrabWireSized {} 105 | -------------------------------------------------------------------------------- /ethercrab-wire/tests/heapless.rs: -------------------------------------------------------------------------------- 1 | use ethercrab_wire::EtherCrabWireRead; 2 | 3 | #[test] 4 | fn heapless_str() { 5 | let input = "Hello world".as_bytes(); 6 | 7 | assert_eq!( 8 | heapless::String::<32>::unpack_from_slice(input), 9 | Ok("Hello world".try_into().unwrap()) 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /ethercrab-wire/tests/pack.rs: -------------------------------------------------------------------------------- 1 | use ethercrab_wire::{EtherCrabWireReadWrite, EtherCrabWireWrite}; 2 | 3 | #[test] 4 | fn one_bit() { 5 | #[derive(Debug, EtherCrabWireReadWrite)] 6 | #[wire(bits = 1)] 7 | struct Bit { 8 | #[wire(bits = 1)] 9 | foo: u8, 10 | } 11 | } 12 | 13 | #[test] 14 | fn one_byte() { 15 | #[derive(Debug, EtherCrabWireReadWrite)] 16 | #[wire(bits = 8)] 17 | struct Byte { 18 | #[wire(bits = 8)] 19 | foo: u8, 20 | } 21 | } 22 | 23 | #[test] 24 | fn basic_enum_byte() { 25 | #[derive(Debug, Copy, Clone, EtherCrabWireReadWrite)] 26 | #[repr(u8)] 27 | #[wire(bits = 8)] 28 | enum Check { 29 | Foo = 0x01, 30 | Bar = 0x02, 31 | Baz = 0xaa, 32 | Quux = 0xef, 33 | } 34 | 35 | // TODO 36 | } 37 | 38 | #[test] 39 | fn basic_struct() { 40 | #[derive(Debug, EtherCrabWireReadWrite)] 41 | #[wire(bits = 56)] 42 | struct Check { 43 | #[wire(bits = 8)] 44 | foo: u8, 45 | #[wire(bits = 16)] 46 | bar: u16, 47 | #[wire(bits = 32)] 48 | baz: u32, 49 | } 50 | 51 | let check = Check { 52 | foo: 0xaa, 53 | bar: 0xbbcc, 54 | baz: 0x33445566, 55 | }; 56 | 57 | let mut buf = [0u8; 16]; 58 | 59 | let out = check.pack_to_slice(&mut buf).unwrap(); 60 | 61 | let expected = [ 62 | 0xaau8, // u8 63 | 0xcc, 0xbb, // u16 64 | 0x66, 0x55, 0x44, 0x33, // u32 65 | ]; 66 | 67 | assert_eq!(out, &expected); 68 | } 69 | 70 | #[test] 71 | fn bools() { 72 | #[derive(Debug, EtherCrabWireReadWrite)] 73 | #[wire(bits = 3)] 74 | struct Check { 75 | #[wire(bits = 1)] 76 | foo: bool, 77 | #[wire(bits = 1)] 78 | bar: bool, 79 | #[wire(bits = 1)] 80 | baz: bool, 81 | } 82 | 83 | let check = Check { 84 | foo: true, 85 | bar: false, 86 | baz: true, 87 | }; 88 | 89 | let mut buf = [0u8; 16]; 90 | 91 | let out = check.pack_to_slice(&mut buf).unwrap(); 92 | 93 | let expected = [0b101]; 94 | 95 | assert_eq!(out, &expected); 96 | } 97 | 98 | #[test] 99 | fn pack_struct_single_byte() { 100 | #[derive(Debug, EtherCrabWireReadWrite)] 101 | #[wire(bits = 8)] 102 | struct Check { 103 | #[wire(bits = 2)] 104 | foo: u8, 105 | #[wire(bits = 3)] 106 | bar: u8, 107 | #[wire(bits = 3)] 108 | baz: u8, 109 | } 110 | 111 | let check = Check { 112 | foo: 0b11, 113 | bar: 0b101, 114 | baz: 0x010, 115 | }; 116 | 117 | let mut buf = [0u8; 8]; 118 | 119 | let out = check.pack_to_slice(&mut buf).unwrap(); 120 | 121 | let expected = [0b11 | (0b101 << 2) | (0x010 << 5)]; 122 | 123 | assert_eq!(out, &expected); 124 | 125 | assert_eq!(buf, [expected[0], 0, 0, 0, 0, 0, 0, 0]); 126 | } 127 | 128 | #[test] 129 | fn pack_struct_nested_enum() { 130 | #[derive(Debug, Copy, Clone, EtherCrabWireReadWrite)] 131 | #[repr(u8)] 132 | enum Nested { 133 | Foo = 0x01, 134 | Bar = 0x02, 135 | Baz = 0x03, 136 | Quux = 0x04, 137 | } 138 | 139 | #[derive(Debug, EtherCrabWireReadWrite)] 140 | #[wire(bits = 8)] 141 | struct Check { 142 | #[wire(bits = 2)] 143 | foo: u8, 144 | #[wire(bits = 3)] 145 | bar: Nested, 146 | #[wire(bits = 3)] 147 | baz: u8, 148 | } 149 | 150 | let check = Check { 151 | foo: 0b11, 152 | bar: Nested::Baz, 153 | baz: 0x010, 154 | }; 155 | 156 | let expected = [0b11u8 | (0x03 << 2) | (0x010 << 5)]; 157 | 158 | let mut buf = [0u8; 16]; 159 | 160 | let out = check.pack_to_slice(&mut buf).unwrap(); 161 | 162 | assert_eq!(out, &expected, "{:#010b} {:#010b}", out[0], expected[0]); 163 | } 164 | 165 | #[test] 166 | fn nested_structs() { 167 | #[derive(Debug, EtherCrabWireReadWrite)] 168 | #[wire(bits = 80)] 169 | struct Check { 170 | #[wire(bits = 32)] 171 | foo: u32, 172 | #[wire(bits = 24)] 173 | control: Inner, 174 | #[wire(bits = 24)] 175 | status: Inner, 176 | } 177 | 178 | #[derive(Debug, Copy, Clone, EtherCrabWireReadWrite)] 179 | #[wire(bits = 24)] 180 | struct Inner { 181 | #[wire(bits = 1)] 182 | yes: bool, 183 | #[wire(bits = 1)] 184 | no: bool, 185 | #[wire(pre_skip = 6, bits = 16)] 186 | stuff: u16, 187 | } 188 | 189 | let check = Check { 190 | foo: 0x44556677, 191 | control: Inner { 192 | yes: true, 193 | no: false, 194 | stuff: 0xaabb, 195 | }, 196 | status: Inner { 197 | yes: false, 198 | no: true, 199 | stuff: 0xccdd, 200 | }, 201 | }; 202 | 203 | let expected = [ 204 | 0x77u8, 0x66u8, 0x55u8, 0x44u8, // u32 205 | 0b01, 0xbb, 0xaa, // Control 206 | 0b10, 0xdd, 0xcc, // Status 207 | ]; 208 | 209 | let mut buf = [0u8; 16]; 210 | 211 | let out = check.pack_to_slice(&mut buf).unwrap(); 212 | 213 | assert_eq!(out, &expected); 214 | } 215 | 216 | // // If I don't need this I won't implement it because it makes things a bunch more complex. 217 | // #[test] 218 | // fn u16_across_bytes() { 219 | // #[derive(Debug, EtherCrabWireReadWrite)] 220 | // #[wire(bits = 24)] 221 | // struct Check { 222 | // #[wire(bits = 3)] 223 | // foo: u8, 224 | // #[wire(bits = 16)] 225 | // bar: u16, 226 | // #[wire(bits = 5)] 227 | // baz: u8, 228 | // } 229 | 230 | // let check = Check { 231 | // foo: 0b00, 232 | // bar: u16::MAX, 233 | // baz: 0x00000, 234 | // }; 235 | // } 236 | -------------------------------------------------------------------------------- /ethercrab-wire/tests/signed-enums.rs: -------------------------------------------------------------------------------- 1 | use ethercrab_wire::{ 2 | EtherCrabWireRead, EtherCrabWireReadWrite, EtherCrabWireWrite, EtherCrabWireWriteSized, 3 | }; 4 | 5 | #[test] 6 | fn signed_byte_enum() { 7 | #[derive(Debug, Copy, Clone, EtherCrabWireReadWrite)] 8 | #[repr(i8)] 9 | enum SignedByte { 10 | Foo = -10, 11 | Bar, 12 | Baz, 13 | } 14 | 15 | #[allow(unused)] 16 | #[repr(i8)] 17 | enum NotDerived { 18 | Foo = -10, 19 | Bar, 20 | Baz, 21 | } 22 | 23 | assert_eq!(NotDerived::Bar as i8, -9); 24 | assert_eq!(SignedByte::Bar.pack(), [-9i8 as u8]); 25 | // Just sanity checking my self here 26 | assert_eq!(SignedByte::Bar.pack(), [247u8]); 27 | } 28 | 29 | #[test] 30 | fn signed_enum_i32() { 31 | #[derive(Debug, PartialEq, Copy, Clone, EtherCrabWireReadWrite)] 32 | #[repr(i32)] 33 | enum BigBoy { 34 | Foo = 0x00bbccdd, 35 | Bar = -2_147_483_648, 36 | Baz = -1073741824, 37 | } 38 | 39 | assert_eq!(BigBoy::unpack_from_slice(&[0, 0, 0, 192]), Ok(BigBoy::Baz)); 40 | assert_eq!( 41 | BigBoy::unpack_from_slice(&[0xdd, 0xcc, 0xbb, 0x00]), 42 | Ok(BigBoy::Foo) 43 | ); 44 | } 45 | 46 | #[test] 47 | fn cia402_control_word() { 48 | #[derive(ethercrab_wire::EtherCrabWireReadWrite, Debug, Eq, PartialEq)] 49 | #[wire(bytes = 2)] 50 | pub struct ControlWord { 51 | /// bit 0, switch on 52 | #[wire(bits = 1)] 53 | pub so: bool, 54 | 55 | /// bit 1, enable voltage 56 | #[wire(bits = 1)] 57 | pub ev: bool, 58 | 59 | /// bit 2, quick stop 60 | #[wire(bits = 1)] 61 | pub qs: bool, 62 | 63 | /// bit 3, enable operation 64 | #[wire(bits = 1)] 65 | pub eo: bool, 66 | 67 | /// bit 4, operation mode specific 68 | #[wire(bits = 1)] 69 | pub oms_1: bool, 70 | 71 | /// bit 5, operation mode specific 72 | #[wire(bits = 1)] 73 | pub oms_2: bool, 74 | 75 | /// bit 6, operation mode specific 76 | #[wire(bits = 1)] 77 | pub oms_3: bool, 78 | 79 | /// bit 7, fault reset 80 | #[wire(bits = 1)] 81 | pub fr: bool, 82 | 83 | /// bit 8, immediate stop 84 | #[wire(bits = 1)] 85 | pub halt: bool, 86 | 87 | /// bit 9, immediate stop 88 | #[wire(bits = 1, post_skip = 6)] 89 | pub oms_4: bool, 90 | } 91 | 92 | let mut cw = ControlWord { 93 | so: true, 94 | ev: true, 95 | qs: true, 96 | eo: true, 97 | oms_1: true, 98 | oms_2: false, 99 | oms_3: false, 100 | fr: true, 101 | halt: false, 102 | oms_4: false, 103 | }; 104 | 105 | let mut buf = cw.pack(); 106 | 107 | assert_eq!(buf, [0b1001_1111, 0b0000_0000]); 108 | 109 | // Change some flags, so when we pack to the buffer again we can make sure they're updated 110 | // properly. 111 | cw.so = false; 112 | cw.ev = true; 113 | cw.qs = true; 114 | cw.fr = false; 115 | 116 | cw.pack_to_slice(&mut buf).unwrap(); 117 | 118 | let cw2 = ControlWord::unpack_from_slice(&buf).unwrap(); 119 | 120 | assert_eq!(cw, cw2); 121 | } 122 | 123 | #[test] 124 | #[allow(unused)] 125 | fn sized() { 126 | #[derive(ethercrab_wire::EtherCrabWireRead)] 127 | #[wire(bytes = 9)] 128 | struct DriveState { 129 | #[wire(bytes = 4)] 130 | actual_position: u32, 131 | #[wire(bytes = 4)] 132 | actual_velocity: u32, 133 | #[wire(bits = 4)] 134 | status_word: u8, 135 | #[wire(bits = 1)] 136 | di0: bool, 137 | #[wire(bits = 1)] 138 | di1: bool, 139 | #[wire(bits = 1)] 140 | di2: bool, 141 | #[wire(bits = 1)] 142 | di3: bool, 143 | } 144 | 145 | #[derive(Copy, Clone, ethercrab_wire::EtherCrabWireWrite)] 146 | #[wire(bytes = 1)] 147 | #[repr(u8)] 148 | enum ControlState { 149 | Init = 0x01, 150 | Conf = 0x04, 151 | Op = 0xaa, 152 | } 153 | 154 | #[derive(ethercrab_wire::EtherCrabWireWrite)] 155 | #[wire(bytes = 5)] 156 | struct DriveControl { 157 | #[wire(bytes = 4)] 158 | target_position: u32, 159 | #[wire(bytes = 1)] 160 | control_state: ControlState, 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /ethercrab-wire/tests/std.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "std")] 2 | #[test] 3 | fn std_string() { 4 | use ethercrab_wire::EtherCrabWireRead; 5 | 6 | let input = "Hello world".as_bytes(); 7 | 8 | assert_eq!( 9 | String::unpack_from_slice(input), 10 | Ok("Hello world".to_string()) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /ethercrab-wire/tests/unpack.rs: -------------------------------------------------------------------------------- 1 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireReadWrite, EtherCrabWireWrite}; 2 | 3 | #[test] 4 | fn basic_struct() { 5 | #[derive(Debug, EtherCrabWireReadWrite, PartialEq)] 6 | #[wire(bits = 56)] 7 | struct Check { 8 | #[wire(bits = 8)] 9 | foo: u8, 10 | #[wire(bits = 16)] 11 | bar: u16, 12 | #[wire(bits = 32)] 13 | baz: u32, 14 | } 15 | 16 | let expected = Check { 17 | foo: 0xaa, 18 | bar: 0xbbcc, 19 | baz: 0x33445566, 20 | }; 21 | 22 | let buf = [ 23 | 0xaau8, // u8 24 | 0xcc, 0xbb, // u16 25 | 0x66, 0x55, 0x44, 0x33, // u32 26 | ]; 27 | 28 | let out = Check::unpack_from_slice(&buf).unwrap(); 29 | 30 | assert_eq!(out, expected); 31 | } 32 | 33 | #[test] 34 | fn unpack_struct_nested_enum() { 35 | #[derive(Debug, Copy, Clone, EtherCrabWireReadWrite, PartialEq)] 36 | #[repr(u8)] 37 | enum Nested { 38 | Foo = 0x01, 39 | Bar = 0x02, 40 | Baz = 0x03, 41 | Quux = 0x04, 42 | } 43 | 44 | #[derive(Debug, EtherCrabWireReadWrite, PartialEq)] 45 | #[wire(bits = 8)] 46 | struct Check { 47 | #[wire(bits = 2)] 48 | foo: u8, 49 | #[wire(bits = 3)] 50 | bar: Nested, 51 | #[wire(bits = 3)] 52 | baz: u8, 53 | } 54 | 55 | let expected = Check { 56 | foo: 0b11, 57 | bar: Nested::Baz, 58 | // 0x010 is too big to fit in 3 bits, so it will be zero on deserialise 59 | baz: 0, 60 | }; 61 | 62 | let buf = [0b11 | (0x03 << 2) | (0x010 << 5)]; 63 | 64 | let out = Check::unpack_from_slice(&buf); 65 | 66 | assert_eq!(out, Ok(expected)); 67 | } 68 | 69 | #[test] 70 | fn nested_structs() { 71 | #[derive(Debug, EtherCrabWireReadWrite)] 72 | #[wire(bits = 80)] 73 | struct Check { 74 | #[wire(bits = 32)] 75 | foo: u32, 76 | #[wire(bits = 24)] 77 | control: Inner, 78 | #[wire(bits = 24)] 79 | status: Inner, 80 | } 81 | 82 | #[derive(Debug, Copy, Clone, EtherCrabWireReadWrite)] 83 | #[wire(bits = 24)] 84 | struct Inner { 85 | #[wire(bits = 1)] 86 | yes: bool, 87 | #[wire(bits = 1)] 88 | no: bool, 89 | #[wire(pre_skip = 6, bits = 16)] 90 | stuff: u16, 91 | } 92 | 93 | let check = Check { 94 | foo: 0x44556677, 95 | control: Inner { 96 | yes: true, 97 | no: false, 98 | stuff: 0xaabb, 99 | }, 100 | status: Inner { 101 | yes: false, 102 | no: true, 103 | stuff: 0xccdd, 104 | }, 105 | }; 106 | 107 | let expected = [ 108 | 0x77u8, 0x66u8, 0x55u8, 0x44u8, // u32 109 | 0b01, 0xbb, 0xaa, // Control 110 | 0b10, 0xdd, 0xcc, // Status 111 | ]; 112 | 113 | let mut buf = [0u8; 16]; 114 | 115 | let out = check.pack_to_slice(&mut buf).unwrap(); 116 | 117 | assert_eq!(out, &expected); 118 | } 119 | -------------------------------------------------------------------------------- /examples/discover.rs: -------------------------------------------------------------------------------- 1 | //! Discover devices connected to the network. 2 | 3 | use env_logger::Env; 4 | use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, Timeouts, std::ethercat_now}; 5 | use std::{str::FromStr, sync::Arc}; 6 | 7 | /// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1. 8 | const MAX_SUBDEVICES: usize = 128; 9 | /// Maximum PDU data payload size - set this to the max PDI size or higher. 10 | const MAX_PDU_DATA: usize = PduStorage::element_size(1100); 11 | /// Maximum number of EtherCAT frames that can be in flight at any one time. 12 | const MAX_FRAMES: usize = 16; 13 | /// Maximum total PDI length. 14 | const PDI_LEN: usize = 64; 15 | 16 | static PDU_STORAGE: PduStorage = PduStorage::new(); 17 | 18 | fn main() { 19 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 20 | 21 | let interface = std::env::args() 22 | .nth(1) 23 | .expect("Provide network interface as first argument."); 24 | 25 | log::info!("Discovering EtherCAT devices on {}...", interface); 26 | 27 | let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 28 | 29 | let maindevice = Arc::new(MainDevice::new( 30 | pdu_loop, 31 | Timeouts::default(), 32 | MainDeviceConfig { 33 | dc_static_sync_iterations: 0, 34 | ..MainDeviceConfig::default() 35 | }, 36 | )); 37 | 38 | smol::block_on(async { 39 | #[cfg(target_os = "windows")] 40 | std::thread::spawn(move || { 41 | ethercrab::std::tx_rx_task_blocking( 42 | &interface, 43 | tx, 44 | rx, 45 | ethercrab::std::TxRxTaskConfig { spinloop: false }, 46 | ) 47 | .expect("TX/RX task") 48 | }); 49 | #[cfg(not(target_os = "windows"))] 50 | smol::spawn(ethercrab::std::tx_rx_task(&interface, tx, rx).expect("spawn TX/RX task")) 51 | .detach(); 52 | 53 | let group = maindevice 54 | .init_single_group::(ethercat_now) 55 | .await 56 | .expect("Init"); 57 | 58 | log::info!("Discovered {} SubDevices", group.len()); 59 | 60 | for subdevice in group.iter(&maindevice) { 61 | log::info!( 62 | "--> SubDevice {:#06x} name {}, description {}, {}", 63 | subdevice.configured_address(), 64 | subdevice.name(), 65 | subdevice 66 | .description() 67 | .await 68 | .expect("Failed to read description") 69 | .unwrap_or(heapless::String::from_str("[no description]").unwrap()), 70 | subdevice.identity() 71 | ); 72 | } 73 | }); 74 | 75 | log::info!("Done."); 76 | } 77 | -------------------------------------------------------------------------------- /examples/dump-eeprom.rs: -------------------------------------------------------------------------------- 1 | //! Dump the EEPROM of a given sub device to stdout. 2 | 3 | use env_logger::Env; 4 | use ethercrab::{ 5 | MainDevice, MainDeviceConfig, PduStorage, Timeouts, error::Error, std::ethercat_now, 6 | }; 7 | use std::io::Write; 8 | 9 | /// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1. 10 | const MAX_SUBDEVICES: usize = 16; 11 | /// Maximum PDU data payload size - set this to the max PDI size or higher. 12 | const MAX_PDU_DATA: usize = PduStorage::element_size(1100); 13 | /// Maximum number of EtherCAT frames that can be in flight at any one time. 14 | const MAX_FRAMES: usize = 16; 15 | /// Maximum total PDI length. 16 | const PDI_LEN: usize = 64; 17 | 18 | static PDU_STORAGE: PduStorage = PduStorage::new(); 19 | 20 | #[tokio::main] 21 | async fn main() -> Result<(), Error> { 22 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 23 | 24 | let interface = std::env::args() 25 | .nth(1) 26 | .expect("Provide network interface as first argument."); 27 | 28 | let index: u16 = std::env::args() 29 | .nth(2) 30 | .expect("Provide device index (starting from zero) as second argument.") 31 | .parse() 32 | .expect("Invalid index: must be a number"); 33 | 34 | log::info!( 35 | "Starting EEPROM dump tool, interface {}, device index {}", 36 | interface, 37 | index 38 | ); 39 | 40 | let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 41 | 42 | let maindevice = MainDevice::new( 43 | pdu_loop, 44 | Timeouts::default(), 45 | MainDeviceConfig { 46 | dc_static_sync_iterations: 0, 47 | ..MainDeviceConfig::default() 48 | }, 49 | ); 50 | 51 | #[cfg(target_os = "windows")] 52 | std::thread::spawn(move || { 53 | ethercrab::std::tx_rx_task_blocking( 54 | &interface, 55 | tx, 56 | rx, 57 | ethercrab::std::TxRxTaskConfig { spinloop: false }, 58 | ) 59 | .expect("TX/RX task") 60 | }); 61 | #[cfg(not(target_os = "windows"))] 62 | tokio::spawn(ethercrab::std::tx_rx_task(&interface, tx, rx).expect("spawn TX/RX task")); 63 | 64 | let group = maindevice 65 | .init_single_group::(ethercat_now) 66 | .await 67 | .expect("Init"); 68 | 69 | log::info!("Discovered {} SubDevices", group.len()); 70 | 71 | for subdevice in group.iter(&maindevice) { 72 | log::info!( 73 | "--> SubDevices {:#06x} {} {}", 74 | subdevice.configured_address(), 75 | subdevice.name(), 76 | subdevice.identity() 77 | ); 78 | } 79 | 80 | let subdevice = group 81 | .subdevice(&maindevice, usize::from(index)) 82 | .expect("Could not find device for given index"); 83 | 84 | log::info!( 85 | "Dumping EEPROM for device index {}: {:#06x} {} {} {}...", 86 | index, 87 | subdevice.configured_address(), 88 | subdevice.name(), 89 | subdevice 90 | .description() 91 | .await? 92 | .map(|d| d.as_str().to_string()) 93 | .unwrap_or(String::new()), 94 | subdevice.identity() 95 | ); 96 | 97 | let eeprom_len = subdevice 98 | .eeprom_size(&maindevice) 99 | .await 100 | .expect("Could not read EEPROM len"); 101 | 102 | log::info!("--> Device EEPROM is {} bytes long", eeprom_len); 103 | 104 | let mut buf = vec![0u8; eeprom_len]; 105 | 106 | // Read entire EEPROM into buffer 107 | subdevice.eeprom_read_raw(&maindevice, 0, &mut buf).await?; 108 | 109 | std::io::stdout().write_all(&buf[..]).expect("Stdout write"); 110 | 111 | log::info!("Done, wrote {} bytes to stdout", buf.len()); 112 | 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /examples/ek1100.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrate setting outputs using a Beckhoff EK1100/EK1501 and modules. 2 | //! 3 | //! Run with e.g. 4 | //! 5 | //! Linux 6 | //! 7 | //! ```bash 8 | //! RUST_LOG=debug cargo run --example ek1100 --release -- eth0 9 | //! ``` 10 | //! 11 | //! Windows 12 | //! 13 | //! ```ps 14 | //! $env:RUST_LOG="debug" ; cargo run --example ek1100 --release -- '\Device\NPF_{FF0ACEE6-E8CD-48D5-A399-619CD2340465}' 15 | //! ``` 16 | 17 | use env_logger::Env; 18 | use ethercrab::{ 19 | MainDevice, MainDeviceConfig, PduStorage, Timeouts, error::Error, std::ethercat_now, 20 | }; 21 | use std::{ 22 | sync::{ 23 | Arc, 24 | atomic::{AtomicBool, Ordering}, 25 | }, 26 | time::Duration, 27 | }; 28 | use tokio::time::MissedTickBehavior; 29 | 30 | /// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1. 31 | const MAX_SUBDEVICES: usize = 16; 32 | /// Maximum PDU data payload size - set this to the max PDI size or higher. 33 | const MAX_PDU_DATA: usize = PduStorage::element_size(1100); 34 | /// Maximum number of EtherCAT frames that can be in flight at any one time. 35 | const MAX_FRAMES: usize = 16; 36 | /// Maximum total PDI length. 37 | const PDI_LEN: usize = 64; 38 | 39 | static PDU_STORAGE: PduStorage = PduStorage::new(); 40 | 41 | #[tokio::main] 42 | async fn main() -> Result<(), Error> { 43 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 44 | 45 | let interface = std::env::args() 46 | .nth(1) 47 | .expect("Provide network interface as first argument."); 48 | 49 | log::info!("Starting EK1100/EK1501 demo..."); 50 | log::info!( 51 | "Ensure an EK1100 or EK1501 is the first SubDevice, with any number of modules connected after" 52 | ); 53 | log::info!("Run with RUST_LOG=ethercrab=debug or =trace for debug information"); 54 | 55 | let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 56 | 57 | let maindevice = Arc::new(MainDevice::new( 58 | pdu_loop, 59 | Timeouts { 60 | wait_loop_delay: Duration::from_millis(2), 61 | mailbox_response: Duration::from_millis(1000), 62 | ..Default::default() 63 | }, 64 | MainDeviceConfig::default(), 65 | )); 66 | 67 | #[cfg(target_os = "windows")] 68 | std::thread::spawn(move || { 69 | ethercrab::std::tx_rx_task_blocking( 70 | &interface, 71 | tx, 72 | rx, 73 | ethercrab::std::TxRxTaskConfig { spinloop: false }, 74 | ) 75 | .expect("TX/RX task") 76 | }); 77 | #[cfg(not(target_os = "windows"))] 78 | tokio::spawn(ethercrab::std::tx_rx_task(&interface, tx, rx).expect("spawn TX/RX task")); 79 | 80 | let group = maindevice 81 | .init_single_group::(ethercat_now) 82 | .await 83 | .expect("Init"); 84 | 85 | log::info!("Discovered {} SubDevices", group.len()); 86 | 87 | for subdevice in group.iter(&maindevice) { 88 | if subdevice.name() == "EL3004" { 89 | log::info!("Found EL3004. Configuring..."); 90 | 91 | subdevice.sdo_write(0x1c12, 0, 0u8).await?; 92 | 93 | subdevice 94 | .sdo_write_array(0x1c13, &[0x1a00u16, 0x1a02, 0x1a04, 0x1a06]) 95 | .await?; 96 | 97 | // The `sdo_write_array` call above is equivalent to the following 98 | // subdevice.sdo_write(0x1c13, 0, 0u8).await?; 99 | // subdevice.sdo_write(0x1c13, 1, 0x1a00u16).await?; 100 | // subdevice.sdo_write(0x1c13, 2, 0x1a02u16).await?; 101 | // subdevice.sdo_write(0x1c13, 3, 0x1a04u16).await?; 102 | // subdevice.sdo_write(0x1c13, 4, 0x1a06u16).await?; 103 | // subdevice.sdo_write(0x1c13, 0, 4u8).await?; 104 | } 105 | } 106 | 107 | let group = group.into_op(&maindevice).await.expect("PRE-OP -> OP"); 108 | 109 | for subdevice in group.iter(&maindevice) { 110 | let io = subdevice.io_raw(); 111 | 112 | log::info!( 113 | "-> SubDevice {:#06x} {} inputs: {} bytes, outputs: {} bytes", 114 | subdevice.configured_address(), 115 | subdevice.name(), 116 | io.inputs().len(), 117 | io.outputs().len() 118 | ); 119 | } 120 | 121 | let mut tick_interval = tokio::time::interval(Duration::from_millis(5)); 122 | tick_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); 123 | 124 | let shutdown = Arc::new(AtomicBool::new(false)); 125 | signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&shutdown)) 126 | .expect("Register hook"); 127 | 128 | loop { 129 | // Graceful shutdown on Ctrl + C 130 | if shutdown.load(Ordering::Relaxed) { 131 | log::info!("Shutting down..."); 132 | 133 | break; 134 | } 135 | 136 | group.tx_rx(&maindevice).await.expect("TX/RX"); 137 | 138 | // Increment every output byte for every SubDevice by one 139 | for subdevice in group.iter(&maindevice) { 140 | let mut o = subdevice.outputs_raw_mut(); 141 | 142 | for byte in o.iter_mut() { 143 | *byte = byte.wrapping_add(1); 144 | } 145 | } 146 | 147 | tick_interval.tick().await; 148 | } 149 | 150 | let group = group 151 | .into_safe_op(&maindevice) 152 | .await 153 | .expect("OP -> SAFE-OP"); 154 | 155 | log::info!("OP -> SAFE-OP"); 156 | 157 | let group = group 158 | .into_pre_op(&maindevice) 159 | .await 160 | .expect("SAFE-OP -> PRE-OP"); 161 | 162 | log::info!("SAFE-OP -> PRE-OP"); 163 | 164 | let _group = group.into_init(&maindevice).await.expect("PRE-OP -> INIT"); 165 | 166 | log::info!("PRE-OP -> INIT, shutdown complete"); 167 | 168 | Ok(()) 169 | } 170 | -------------------------------------------------------------------------------- /examples/embassy-stm32/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 2 | runner = "probe-rs run --chip STM32F429ZITx" 3 | 4 | [build] 5 | target = "thumbv7em-none-eabi" 6 | 7 | [env] 8 | DEFMT_LOG = "debug" 9 | -------------------------------------------------------------------------------- /examples/embassy-stm32/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethercrab-stm32-embassy" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [workspace] 7 | 8 | [dependencies] 9 | ethercrab = { path = "../..", default-features = false, features = ["defmt"] } 10 | 11 | defmt = "0.3.10" 12 | defmt-rtt = "0.4.2" 13 | cortex-m = { version = "0.7.6", features = [ 14 | "inline-asm", 15 | "critical-section-single-core", 16 | ] } 17 | cortex-m-rt = "0.7.5" 18 | embedded-hal = "1.0.0" 19 | embedded-io = "0.6.0" 20 | panic-probe = { version = "0.3", features = ["print-defmt"] } 21 | static_cell = { version = "2.0.0" } 22 | smoltcp = { version = "0.12.0", features = ["defmt"], default-features = false } 23 | embassy-executor = { version = "0.7.0", features = [ 24 | "arch-cortex-m", 25 | "executor-thread", 26 | "executor-interrupt", 27 | "defmt", 28 | "arch-cortex-m", 29 | "task-arena-size-81920", 30 | ] } 31 | embassy-time = { version = "0.4.0", features = [ 32 | "defmt", 33 | "defmt-timestamp-uptime", 34 | "tick-hz-1_000_000", 35 | ] } 36 | embassy-stm32 = { version = "0.2.0", features = [ 37 | "defmt", 38 | "stm32f429zi", 39 | "memory-x", 40 | "time-driver-any", 41 | ] } 42 | embassy-net = { version = "0.6.0", features = [ 43 | "defmt", 44 | "tcp", 45 | "dhcpv4", 46 | "medium-ethernet", 47 | ] } 48 | embassy-net-driver = { version = "0.2.0", features = ["defmt"] } 49 | 50 | [profile.release] 51 | debug = 2 52 | opt-level = "z" 53 | lto = true 54 | codegen-units = 1 55 | -------------------------------------------------------------------------------- /examples/embassy-stm32/README.md: -------------------------------------------------------------------------------- 1 | # EtherCrab on STM32 `no_std` with embassy 2 | 3 | 4 | 5 | This targets the STM32F429 Nucleo dev board. Follow the Embassy setup guide, then 6 | `cargo run --release` should do. 7 | -------------------------------------------------------------------------------- /examples/embassy-stm32/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg-bins=--nmagic"); 3 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 4 | println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/embassy-stm32/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["llvm-tools"] 4 | targets = ["thumbv7em-none-eabi"] 5 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-replacements = [ 2 | { file = "CHANGELOG.md", search = "[Uu]nreleased", replace = "{{version}}" }, 3 | { file = "CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}" }, 4 | { file = "CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}" }, 5 | { file = "CHANGELOG.md", search = "", replace = "\n\n## [Unreleased] - ReleaseDate" }, 6 | { file = "CHANGELOG.md", search = "", replace = "\n[unreleased]: https://github.com/ethercrab-rs/ethercrab/compare/{{tag_name}}...HEAD" }, 7 | ] 8 | pre-release-commit-message = "Release" 9 | tag-message = "Release {{crate_name}} v{{version}}" 10 | allow-branch = ["main", "*-backports"] 11 | tag-prefix = "{{crate_name}}-" 12 | -------------------------------------------------------------------------------- /repo-stats.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo -e "commit,date,loc_total,loc_code,loc_comments,text,bss,dec,bin" > target/sizes.csv 4 | 5 | trap 'echo -e "\nCancelled. Restoring repo."; git checkout --quiet --force main; exit' INT 6 | 7 | for commit in $(git rev-list main) 8 | do 9 | date=$(git show -s --format=%ci $commit) 10 | 11 | echo "Commit ${commit} at ${date}" 12 | 13 | git checkout $commit --quiet 14 | 15 | if [[ -f "examples/embassy-stm32/Cargo.toml" ]]; then 16 | pushd examples/embassy-stm32 > /dev/null 17 | 18 | out=$(cargo size --release --quiet | tail -n1) 19 | text=$(echo $out | awk '{print $1}') 20 | bss=$(echo $out | awk '{print $3}') 21 | dec=$(echo $out | awk '{print $4}') 22 | 23 | cargo objcopy --release --quiet -- -O binary target/size.bin 24 | out=$(wc -c target/size.bin) 25 | bin=$(echo $out | awk '{print $1}') 26 | 27 | popd > /dev/null 28 | fi 29 | 30 | code_stats=$(tokei --type Rust | tail -n2 | head -n1 | awk '{print $3","$4","$5}') 31 | 32 | echo -n "--> " 33 | echo -e "$commit,$date,$code_stats,$text,$bss,$dec,$bin" | tee -a target/sizes.csv 34 | done 35 | 36 | echo "Done" 37 | 38 | git checkout main --quiet 39 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85" 3 | profile = "default" 4 | targets = ["x86_64-unknown-linux-gnu"] 5 | -------------------------------------------------------------------------------- /src/al_control.rs: -------------------------------------------------------------------------------- 1 | use crate::subdevice_state::SubDeviceState; 2 | 3 | /// The AL control/status word for an individual SubDevice. 4 | /// 5 | /// Defined in ETG1000.6 Table 9 - AL Control Description. 6 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | #[wire(bytes = 2)] 9 | pub struct AlControl { 10 | /// AL status. 11 | #[wire(bits = 4)] 12 | pub state: SubDeviceState, 13 | /// Error flag. 14 | #[wire(bits = 1)] 15 | pub error: bool, 16 | /// ID request flag. 17 | #[wire(bits = 1, post_skip = 10)] 18 | pub id_request: bool, 19 | } 20 | 21 | impl AlControl { 22 | pub fn new(state: SubDeviceState) -> Self { 23 | Self { 24 | state, 25 | error: false, 26 | id_request: false, 27 | } 28 | } 29 | 30 | pub fn reset() -> Self { 31 | Self { 32 | state: SubDeviceState::Init, 33 | // Acknowledge error 34 | error: true, 35 | ..Default::default() 36 | } 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use super::*; 43 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireWriteSized}; 44 | 45 | #[test] 46 | fn al_control() { 47 | let value = AlControl { 48 | state: SubDeviceState::SafeOp, 49 | error: true, 50 | id_request: false, 51 | }; 52 | 53 | let packed = value.pack(); 54 | 55 | assert_eq!(packed, [0x04 | 0x10, 0x00]); 56 | } 57 | 58 | #[test] 59 | fn unpack() { 60 | let value = AlControl { 61 | state: SubDeviceState::SafeOp, 62 | error: true, 63 | id_request: false, 64 | }; 65 | 66 | let parsed = AlControl::unpack_from_slice(&[0x04 | 0x10, 0x00]).unwrap(); 67 | 68 | assert_eq!(value, parsed); 69 | } 70 | 71 | #[test] 72 | fn unpack_short() { 73 | let parsed = AlControl::unpack_from_slice(&[0x04 | 0x10]); 74 | 75 | assert!(parsed.is_err()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/base_data_types.rs: -------------------------------------------------------------------------------- 1 | /// Base data types, defined in ETG1020 Table 98 (etc) or ETG1000.6 Table 64. 2 | /// 3 | /// Many more data types are defined, however this enum only lists the primitive types. Other types, 4 | /// e.g. ETG1020 Table 100 should be defined elsewhere. 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq, ethercrab_wire::EtherCrabWireRead)] 6 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 7 | #[repr(u8)] 8 | pub enum PrimitiveDataType { 9 | Unknown = 0x00, 10 | 11 | /// Boolean, bit, on or off. 12 | Bool = 0x01, 13 | /// A single byte. 14 | Byte = 0x1E, 15 | /// A two byte (16 bit) word. 16 | Word = 0x1F, 17 | /// A 4 byte (32 bit) double word. 18 | DWord = 0x20, 19 | 20 | // --- 21 | // Bit String 22 | // --- 23 | /// A single bit. 24 | Bit1 = 0x30, 25 | /// 2 invidual bits. 26 | Bit2 = 0x31, 27 | /// 3 invidual bits. 28 | Bit3 = 0x32, 29 | /// 4 invidual bits. 30 | Bit4 = 0x33, 31 | /// 5 invidual bits. 32 | Bit5 = 0x34, 33 | /// 6 invidual bits. 34 | Bit6 = 0x35, 35 | /// 7 invidual bits. 36 | Bit7 = 0x36, 37 | /// 8 invidual bits. 38 | Bit8 = 0x37, 39 | /// 9 invidual bits. 40 | Bit9 = 0x38, 41 | /// 10 invidual bits. 42 | Bit10 = 0x39, 43 | /// 11 invidual bits. 44 | Bit11 = 0x3A, 45 | /// 12 invidual bits. 46 | Bit12 = 0x3B, 47 | /// 13 invidual bits. 48 | Bit13 = 0x3C, 49 | /// 14 invidual bits. 50 | Bit14 = 0x3D, 51 | /// 15 invidual bits. 52 | Bit15 = 0x3E, 53 | /// 16 invidual bits. 54 | Bit16 = 0x3F, 55 | /// 8 individual Bits 56 | BitArr8 = 0x2D, 57 | /// 16 individual Bits 58 | BitArr16 = 0x2E, 59 | /// 32 individual Bits 60 | BitArr32 = 0x2F, 61 | 62 | // --- 63 | // Signed Integer 64 | // --- 65 | /// SINT 8 Short Integer -128 to 127 66 | I8 = 0x02, 67 | /// INT 16 Integer -32 768 to 32 767 68 | I16 = 0x03, 69 | /// INT24 24 -223 to 223-1 70 | I24 = 0x10, 71 | /// DINT 32 Double Integer -231 to 231-1 72 | I32 = 0x04, 73 | /// INT40 40 74 | I40 = 0x12, 75 | /// INT48 48 76 | I48 = 0x13, 77 | /// INT56 56 78 | I56 = 0x14, 79 | /// LINT 64 Long Integer -263 to 263-1 80 | I64 = 0x15, 81 | 82 | // --- 83 | // Unsigned Integer 84 | // --- 85 | /// USINT 8 Unsigned Short Integer 0 to 255 86 | U8 = 0x05, 87 | /// UINT 16 Unsigned Integer / Word 0 to 65 535 88 | U16 = 0x06, 89 | /// UINT24 24 90 | U24 = 0x16, 91 | /// UDINT 32 Unsigned Double Integer 0 to 232-1 92 | U32 = 0x07, 93 | /// UINT40 40 94 | U40 = 0x18, 95 | /// UINT48 48 96 | U48 = 0x19, 97 | /// UINT56 56 98 | U56 = 0x1A, 99 | /// ULINT 64 Unsigned Long Integer 0 to 264-1 100 | U64 = 0x1B, 101 | 102 | // --- 103 | // Floating point 104 | // --- 105 | /// REAL 32 Floating point 106 | F32 = 0x08, 107 | /// LREAL 64 Long float 108 | F64 = 0x11, 109 | } 110 | -------------------------------------------------------------------------------- /src/coe/mod.rs: -------------------------------------------------------------------------------- 1 | use ethercrab_wire::EtherCrabWireReadSized; 2 | 3 | pub mod abort_code; 4 | pub mod services; 5 | 6 | /// Defined in ETG1000.6 Table 29 – CoE elements 7 | #[derive(Clone, Copy, Debug, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)] 8 | #[cfg_attr(test, derive(arbitrary::Arbitrary))] 9 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 10 | #[repr(u8)] 11 | pub enum CoeService { 12 | /// Emergency 13 | Emergency = 0x01, 14 | /// SDO Request 15 | SdoRequest = 0x02, 16 | /// SDO Response 17 | SdoResponse = 0x03, 18 | /// TxPDO 19 | TxPdo = 0x04, 20 | /// RxPDO 21 | RxPdo = 0x05, 22 | /// TxPDO remote request 23 | TxPdoRemoteRequest = 0x06, 24 | /// RxPDO remote request 25 | RxPdoRemoteRequest = 0x07, 26 | /// SDO Information 27 | SdoInformation = 0x08, 28 | } 29 | 30 | /// The field near the bottom of SDO definition tables called "Command specifier". 31 | /// 32 | /// See e.g. ETG1000.6 Section 5.6.2.6.2 Table 39 – Upload SDO Segment Response. 33 | #[derive(Clone, Copy, Debug, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)] 34 | #[wire(bits = 3)] 35 | #[repr(u8)] 36 | pub enum CoeCommand { 37 | Download = 0x01, 38 | Upload = 0x02, 39 | Abort = 0x04, 40 | UploadSegment = 0x03, 41 | } 42 | 43 | /// Defined in ETG1000.6 Section 5.6.2.1.1 44 | #[derive(Clone, Copy, Debug, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)] 45 | #[wire(bytes = 4)] 46 | pub struct InitSdoHeader { 47 | #[wire(bits = 1)] 48 | pub size_indicator: bool, 49 | #[wire(bits = 1)] 50 | pub expedited_transfer: bool, 51 | #[wire(bits = 2)] 52 | pub size: u8, 53 | #[wire(bits = 1)] 54 | pub complete_access: bool, 55 | #[wire(bits = 3)] 56 | pub command: CoeCommand, 57 | #[wire(bytes = 2)] 58 | pub index: u16, 59 | #[wire(bytes = 1)] 60 | pub sub_index: u8, 61 | } 62 | 63 | /// Defined in ETG1000.6 5.6.2.3.1 64 | #[derive(Clone, Copy, Debug, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)] 65 | #[wire(bytes = 1)] 66 | pub struct SegmentSdoHeader { 67 | #[wire(bits = 1)] 68 | pub is_last_segment: bool, 69 | 70 | /// Segment data size, `0x00` to `0x07`. 71 | #[wire(bits = 3)] 72 | pub segment_data_size: u8, 73 | 74 | #[wire(bits = 1)] 75 | pub toggle: bool, 76 | 77 | #[wire(bits = 3)] 78 | command: CoeCommand, 79 | } 80 | 81 | /// Subindex access. 82 | #[derive(Copy, Clone, Debug)] 83 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 84 | pub enum SubIndex { 85 | /// Complete access. 86 | /// 87 | /// Accesses the entire entry as a single slice of data. 88 | Complete, 89 | 90 | /// Individual sub-index access. 91 | Index(u8), 92 | } 93 | 94 | impl SubIndex { 95 | pub(crate) fn complete_access(&self) -> bool { 96 | matches!(self, Self::Complete) 97 | } 98 | 99 | pub(crate) fn sub_index(&self) -> u8 { 100 | match self { 101 | // 0th sub-index counts number of sub-indices in object, so we'll start from 1 102 | SubIndex::Complete => 1, 103 | SubIndex::Index(idx) => *idx, 104 | } 105 | } 106 | } 107 | 108 | impl From for SubIndex { 109 | fn from(value: u8) -> Self { 110 | Self::Index(value) 111 | } 112 | } 113 | 114 | /// A trait for types that can be transferred with a single expedited SDO upload. 115 | pub(crate) trait SdoExpedited: EtherCrabWireReadSized {} 116 | 117 | impl SdoExpedited for u8 {} 118 | impl SdoExpedited for u16 {} 119 | impl SdoExpedited for u32 {} 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | pub use super::*; 124 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireWriteSized}; 125 | 126 | #[test] 127 | fn sanity_coe_service() { 128 | assert_eq!(CoeService::SdoRequest.pack(), [0x02]); 129 | assert_eq!( 130 | CoeService::unpack_from_slice(&[0x02]), 131 | Ok(CoeService::SdoRequest) 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/command/reads.rs: -------------------------------------------------------------------------------- 1 | use crate::{MainDevice, error::Error, pdu_loop::ReceivedPdu}; 2 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireSized}; 3 | 4 | /// Read commands that send no data. 5 | #[derive(PartialEq, Eq, Debug, Copy, Clone)] 6 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 7 | #[cfg_attr(feature = "serde", derive(serde::Serialize))] 8 | pub enum Reads { 9 | /// APRD. 10 | Aprd { 11 | /// Auto increment counter. 12 | address: u16, 13 | 14 | /// Memory location to read from. 15 | register: u16, 16 | }, 17 | /// FPRD. 18 | Fprd { 19 | /// Configured station address. 20 | address: u16, 21 | 22 | /// Memory location to read from. 23 | register: u16, 24 | }, 25 | /// Broadcast Read (BRD). 26 | Brd { 27 | /// Autoincremented by each SubDevice visited. 28 | address: u16, 29 | 30 | /// Memory location to read from. 31 | register: u16, 32 | }, 33 | /// LRD. 34 | Lrd { 35 | /// Logical address. 36 | address: u32, 37 | }, 38 | /// FRMW. 39 | Frmw { 40 | /// Configured station address. 41 | address: u16, 42 | 43 | /// Memory location to read from. 44 | register: u16, 45 | }, 46 | } 47 | 48 | impl Reads { 49 | /// Wrap this command with a MainDevice to make it sendable over the wire. 50 | pub fn wrap(self) -> WrappedRead { 51 | WrappedRead::new(self) 52 | } 53 | } 54 | 55 | /// A wrapped version of a [`Reads`] exposing a builder API used to send/receive data over the wire. 56 | #[derive(Debug, Copy, Clone)] 57 | pub struct WrappedRead { 58 | /// EtherCAT command. 59 | pub command: Reads, 60 | /// Expected working counter. 61 | wkc: Option, 62 | } 63 | 64 | impl WrappedRead { 65 | pub(crate) fn new(command: Reads) -> Self { 66 | Self { 67 | command, 68 | wkc: Some(1), 69 | } 70 | } 71 | 72 | /// Do not return an error if the working counter is different from the expected value. 73 | /// 74 | /// The default value is `1` and can be overridden with [`with_wkc`](WrappedRead::with_wkc). 75 | pub fn ignore_wkc(self) -> Self { 76 | Self { wkc: None, ..self } 77 | } 78 | 79 | /// Change the expected working counter from its default of `1`. 80 | pub fn with_wkc(self, wkc: u16) -> Self { 81 | Self { 82 | wkc: Some(wkc), 83 | ..self 84 | } 85 | } 86 | 87 | /// Receive data and decode into a `T`. 88 | pub async fn receive<'maindevice, T>( 89 | self, 90 | maindevice: &'maindevice MainDevice<'maindevice>, 91 | ) -> Result 92 | where 93 | T: EtherCrabWireRead + EtherCrabWireSized, 94 | { 95 | self.common(maindevice, T::PACKED_LEN as u16) 96 | .await? 97 | .maybe_wkc(self.wkc) 98 | .and_then(|data| Ok(T::unpack_from_slice(&data)?)) 99 | } 100 | 101 | /// Receive a given number of bytes and return it as a slice. 102 | pub async fn receive_slice<'maindevice>( 103 | self, 104 | maindevice: &'maindevice MainDevice<'maindevice>, 105 | len: u16, 106 | ) -> Result, Error> { 107 | self.common(maindevice, len).await?.maybe_wkc(self.wkc) 108 | } 109 | 110 | /// Receive only the working counter. 111 | /// 112 | /// Any expected working counter value will be ignored when calling this method, regardless of 113 | /// any value set by [`with_wkc`](WrappedRead::with_wkc). 114 | /// 115 | /// `T` determines the length of the read, which is required for valid reads. It is otherwise 116 | /// ignored. 117 | pub(crate) async fn receive_wkc<'maindevice, T>( 118 | &self, 119 | maindevice: &'maindevice MainDevice<'maindevice>, 120 | ) -> Result 121 | where 122 | T: EtherCrabWireRead + EtherCrabWireSized, 123 | { 124 | self.common(maindevice, T::PACKED_LEN as u16) 125 | .await 126 | .map(|res| res.working_counter) 127 | } 128 | 129 | // Some manual monomorphisation 130 | fn common<'maindevice>( 131 | &self, 132 | maindevice: &'maindevice MainDevice<'maindevice>, 133 | len: u16, 134 | ) -> impl core::future::Future, Error>> { 135 | maindevice.single_pdu(self.command.into(), (), Some(len)) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/command/writes.rs: -------------------------------------------------------------------------------- 1 | use crate::{MainDevice, error::Error, pdu_loop::ReceivedPdu}; 2 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireWrite}; 3 | 4 | /// Write commands. 5 | #[derive(PartialEq, Eq, Debug, Copy, Clone)] 6 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 7 | #[cfg_attr(feature = "serde", derive(serde::Serialize))] 8 | pub enum Writes { 9 | /// BWR. 10 | Bwr { 11 | /// Autoincremented by each SubDevice visited. 12 | address: u16, 13 | 14 | /// Memory location to write to. 15 | register: u16, 16 | }, 17 | /// APWR. 18 | Apwr { 19 | /// Auto increment counter. 20 | address: u16, 21 | 22 | /// Memory location to write to. 23 | register: u16, 24 | }, 25 | /// FPWR. 26 | Fpwr { 27 | /// Configured station address. 28 | address: u16, 29 | 30 | /// Memory location to read from. 31 | register: u16, 32 | }, 33 | /// LWR. 34 | Lwr { 35 | /// Logical address. 36 | address: u32, 37 | }, 38 | 39 | /// LRW. 40 | Lrw { 41 | /// Logical address. 42 | address: u32, 43 | }, 44 | } 45 | 46 | /// A wrapped version of a [`Writes`] exposing a builder API used to send/receive data over the 47 | /// wire. 48 | #[derive(Debug, Copy, Clone)] 49 | pub struct WrappedWrite { 50 | /// EtherCAT command. 51 | pub command: Writes, 52 | /// Expected working counter. 53 | wkc: Option, 54 | len_override: Option, 55 | } 56 | 57 | impl WrappedWrite { 58 | pub(crate) fn new(command: Writes) -> Self { 59 | Self { 60 | command, 61 | wkc: Some(1), 62 | len_override: None, 63 | } 64 | } 65 | 66 | /// Set an explicit length for the PDU instead of taking it from the sent data. 67 | /// 68 | /// The length will be the _maximum_ of the value set here and the data sent. 69 | pub fn with_len(self, new_len: impl Into) -> Self { 70 | Self { 71 | len_override: Some(new_len.into()), 72 | ..self 73 | } 74 | } 75 | 76 | /// Do not return an error if the working counter is different from the expected value. 77 | /// 78 | /// The default value is `1` and can be overridden with [`with_wkc`](WrappedWrite::with_wkc). 79 | pub fn ignore_wkc(self) -> Self { 80 | Self { wkc: None, ..self } 81 | } 82 | 83 | /// Change the expected working counter from its default of `1`. 84 | pub fn with_wkc(self, wkc: u16) -> Self { 85 | Self { 86 | wkc: Some(wkc), 87 | ..self 88 | } 89 | } 90 | 91 | /// Send a payload with a length set by [`with_len`](WrappedWrite::with_len), ignoring the 92 | /// response. 93 | pub async fn send<'maindevice>( 94 | self, 95 | maindevice: &'maindevice MainDevice<'maindevice>, 96 | data: impl EtherCrabWireWrite, 97 | ) -> Result<(), Error> { 98 | self.common(maindevice, data, self.len_override).await?; 99 | 100 | Ok(()) 101 | } 102 | 103 | /// Send a value, returning the response returned from the network. 104 | pub async fn send_receive<'maindevice, T>( 105 | self, 106 | maindevice: &'maindevice MainDevice<'maindevice>, 107 | value: impl EtherCrabWireWrite, 108 | ) -> Result 109 | where 110 | T: EtherCrabWireRead, 111 | { 112 | self.common(maindevice, value, None) 113 | .await? 114 | .maybe_wkc(self.wkc) 115 | .and_then(|data| Ok(T::unpack_from_slice(&data)?)) 116 | } 117 | 118 | /// Similar to [`send_receive`](WrappedWrite::send_receive) but returns a slice. 119 | pub async fn send_receive_slice<'maindevice>( 120 | self, 121 | maindevice: &'maindevice MainDevice<'maindevice>, 122 | value: impl EtherCrabWireWrite, 123 | ) -> Result, Error> { 124 | self.common(maindevice, value, None) 125 | .await? 126 | .maybe_wkc(self.wkc) 127 | } 128 | 129 | // Some manual monomorphisation 130 | fn common<'maindevice>( 131 | &self, 132 | maindevice: &'maindevice MainDevice<'maindevice>, 133 | value: impl EtherCrabWireWrite, 134 | len_override: Option, 135 | ) -> impl core::future::Future, Error>> { 136 | maindevice.single_pdu(self.command.into(), value, len_override) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/dl_status.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Copy, Clone, PartialEq)] 2 | #[cfg_attr(not(test), derive(ethercrab_wire::EtherCrabWireRead))] 3 | #[cfg_attr( 4 | test, 5 | derive(arbitrary::Arbitrary, ethercrab_wire::EtherCrabWireReadWrite) 6 | )] 7 | #[wire(bytes = 2)] 8 | pub struct DlStatus { 9 | #[wire(bits = 1)] 10 | pub pdi_operational: bool, 11 | #[wire(bits = 1)] 12 | pub watchdog_ok: bool, 13 | #[wire(bits = 1, post_skip = 1)] 14 | pub extended_link_detection: bool, 15 | // pub _reserved: bool, 16 | /// True if port 0 has a physical link present. 17 | #[wire(bits = 1)] 18 | pub link_port0: bool, 19 | /// True if port 1 has a physical link present. 20 | #[wire(bits = 1)] 21 | pub link_port1: bool, 22 | /// True if port 2 has a physical link present. 23 | #[wire(bits = 1)] 24 | pub link_port2: bool, 25 | /// True if port 3 has a physical link present. 26 | #[wire(bits = 1)] 27 | pub link_port3: bool, 28 | 29 | /// True if port 0 forwards to itself (i.e. loopback) 30 | #[wire(bits = 1)] 31 | pub loopback_port0: bool, 32 | /// RX signal detected on port 0 33 | #[wire(bits = 1)] 34 | pub signal_port0: bool, 35 | /// True if port 1 forwards to itself (i.e. loopback) 36 | #[wire(bits = 1)] 37 | pub loopback_port1: bool, 38 | /// RX signal detected on port 1 39 | #[wire(bits = 1)] 40 | pub signal_port1: bool, 41 | /// True if port 2 forwards to itself (i.e. loopback) 42 | #[wire(bits = 1)] 43 | pub loopback_port2: bool, 44 | /// RX signal detected on port 2 45 | #[wire(bits = 1)] 46 | pub signal_port2: bool, 47 | /// True if port 3 forwards to itself (i.e. loopback) 48 | #[wire(bits = 1)] 49 | pub loopback_port3: bool, 50 | /// RX signal detected on port 3 51 | #[wire(bits = 1)] 52 | pub signal_port3: bool, 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use super::*; 58 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireWrite}; 59 | 60 | #[test] 61 | #[cfg_attr(miri, ignore)] 62 | fn dl_status_fuzz() { 63 | heckcheck::check(|status: DlStatus| { 64 | let mut buf = [0u8; 16]; 65 | 66 | let packed = status.pack_to_slice(&mut buf).expect("Pack"); 67 | 68 | let unpacked = DlStatus::unpack_from_slice(packed).expect("Unpack"); 69 | 70 | pretty_assertions::assert_eq!(status, unpacked); 71 | 72 | Ok(()) 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/eeprom/device_provider.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Command, MainDevice, 3 | eeprom::{ 4 | EepromDataProvider, 5 | types::{SiiControl, SiiRequest}, 6 | }, 7 | error::{EepromError, Error}, 8 | fmt, 9 | register::RegisterAddress, 10 | timer_factory::IntoTimeout, 11 | }; 12 | 13 | /// The address of the first proper category, positioned after the fixed fields defined in ETG2010 14 | /// Table 2. 15 | /// 16 | /// SII EEPROM is WORD-addressed. 17 | pub(crate) const SII_FIRST_CATEGORY_START: u16 = 0x0040u16; 18 | 19 | /// EEPROM data provider that communicates with a physical sub device. 20 | #[derive(Clone)] 21 | pub struct DeviceEeprom<'subdevice> { 22 | maindevice: &'subdevice MainDevice<'subdevice>, 23 | configured_address: u16, 24 | } 25 | 26 | impl<'subdevice> DeviceEeprom<'subdevice> { 27 | /// Create a new EEPROM reader instance. 28 | pub fn new(maindevice: &'subdevice MainDevice<'subdevice>, configured_address: u16) -> Self { 29 | Self { 30 | maindevice, 31 | configured_address, 32 | } 33 | } 34 | 35 | async fn wait_while_busy(&self) -> Result { 36 | let res = async { 37 | loop { 38 | let control: SiiControl = 39 | Command::fprd(self.configured_address, RegisterAddress::SiiControl.into()) 40 | .receive::(self.maindevice) 41 | .await?; 42 | 43 | if !control.busy { 44 | break Ok(control); 45 | } 46 | 47 | self.maindevice.timeouts.loop_tick().await; 48 | } 49 | } 50 | .timeout(self.maindevice.timeouts.eeprom) 51 | .await?; 52 | 53 | Ok(res) 54 | } 55 | } 56 | 57 | impl EepromDataProvider for DeviceEeprom<'_> { 58 | async fn read_chunk( 59 | &mut self, 60 | start_word: u16, 61 | ) -> Result, Error> { 62 | Command::fpwr(self.configured_address, RegisterAddress::SiiControl.into()) 63 | .send(self.maindevice, SiiRequest::read(start_word)) 64 | .await?; 65 | 66 | let status = self.wait_while_busy().await?; 67 | 68 | Command::fprd(self.configured_address, RegisterAddress::SiiData.into()) 69 | .receive_slice(self.maindevice, status.read_size.chunk_len()) 70 | .await 71 | .inspect(|data| { 72 | #[cfg(not(feature = "defmt"))] 73 | fmt::trace!("Read addr {:#06x}: {:02x?}", start_word, &data[..]); 74 | #[cfg(feature = "defmt")] 75 | fmt::trace!("Read addr {:#06x}: {=[u8]}", start_word, &data[..]); 76 | }) 77 | } 78 | 79 | async fn write_word(&mut self, start_word: u16, data: [u8; 2]) -> Result<(), Error> { 80 | // Check if the EEPROM is busy 81 | self.wait_while_busy().await?; 82 | 83 | let mut retry_count = 0; 84 | 85 | loop { 86 | // Set data to write 87 | Command::fpwr(self.configured_address, RegisterAddress::SiiData.into()) 88 | .send(self.maindevice, data) 89 | .await?; 90 | 91 | // Send control and address registers. A rising edge on the write flag will store whatever 92 | // is in `SiiAddress` into the EEPROM at the given address. 93 | Command::fpwr(self.configured_address, RegisterAddress::SiiControl.into()) 94 | .send(self.maindevice, SiiRequest::write(start_word)) 95 | .await?; 96 | 97 | // Wait for error or not busy 98 | let status = self.wait_while_busy().await?; 99 | 100 | if status.command_error && retry_count < 20 { 101 | fmt::debug!("Retrying EEPROM write"); 102 | 103 | retry_count += 1; 104 | } else { 105 | break; 106 | } 107 | } 108 | 109 | Ok(()) 110 | } 111 | 112 | async fn clear_errors(&self) -> Result<(), Error> { 113 | let status = Command::fprd(self.configured_address, RegisterAddress::SiiControl.into()) 114 | .receive::(self.maindevice) 115 | .await?; 116 | 117 | // Clear errors 118 | let status = if status.has_error() { 119 | fmt::trace!("Resetting EEPROM error flags"); 120 | 121 | Command::fpwr(self.configured_address, RegisterAddress::SiiControl.into()) 122 | .send_receive(self.maindevice, status.error_reset()) 123 | .await? 124 | } else { 125 | status 126 | }; 127 | 128 | if status.has_error() { 129 | Err(Error::Eeprom(EepromError::ClearErrors)) 130 | } else { 131 | Ok(()) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/eeprom/file_provider.rs: -------------------------------------------------------------------------------- 1 | //! An EEPROM reader backed by an EEPROM image file instead of a real device. 2 | //! 3 | //! Useful for debugging and unit testing. These items should not be used in production as they 4 | //! contain quite a few panics, unwraps and poor assumptions. 5 | 6 | use crate::{eeprom::EepromDataProvider, error::Error}; 7 | use std::io::{BufReader, Cursor, Read, Seek}; 8 | 9 | pub struct EepromFile { 10 | file_len: usize, 11 | bytes: BufReader>, 12 | buf: [u8; CHUNK], 13 | pub write_cache: Vec, 14 | } 15 | 16 | impl Clone for EepromFile { 17 | fn clone(&self) -> Self { 18 | Self { 19 | bytes: BufReader::new(self.bytes.get_ref().clone()), 20 | file_len: self.file_len, 21 | buf: self.buf, 22 | write_cache: self.write_cache.clone(), 23 | } 24 | } 25 | } 26 | 27 | impl EepromFile<8> { 28 | /// Create an EEPROM file reader that returns chunks of 8 bytes. 29 | // Allow unused as this is only used in unit tests. 30 | #[allow(unused)] 31 | pub fn new(bytes: &'static [u8]) -> Self { 32 | Self { 33 | file_len: bytes.len(), 34 | bytes: BufReader::new(Cursor::new(bytes)), 35 | buf: [0u8; 8], 36 | write_cache: vec![0u8; bytes.len()], 37 | } 38 | } 39 | } 40 | 41 | impl EepromFile<4> { 42 | // Allow unused as this is only used in unit tests. 43 | #[allow(unused)] 44 | pub fn new_short(bytes: &'static [u8]) -> Self { 45 | Self { 46 | file_len: bytes.len(), 47 | bytes: BufReader::new(Cursor::new(bytes)), 48 | buf: [0u8; 4], 49 | write_cache: vec![0u8; bytes.len()], 50 | } 51 | } 52 | } 53 | 54 | impl EepromDataProvider for EepromFile { 55 | async fn read_chunk( 56 | &mut self, 57 | start_word: u16, 58 | ) -> Result, Error> { 59 | let file_len = self.file_len; 60 | 61 | self.bytes 62 | .seek(std::io::SeekFrom::Start(u64::from(start_word) * 2)) 63 | .expect("Bad seek!"); 64 | 65 | // Make sure a partial read off the end of the file is ok, e.g. 8 byte buffer but 4 byte 66 | // read. 67 | let buf_len = self.buf.len().min(file_len - usize::from(start_word * 2)); 68 | 69 | let buf = &mut self.buf[0..buf_len]; 70 | 71 | assert!( 72 | buf_len == 4 || buf_len == 8, 73 | "Expected buf len of 4 or 8, got {}", 74 | buf_len 75 | ); 76 | 77 | self.bytes 78 | .read_exact(buf) 79 | .expect("Could not read from EEPROM file"); 80 | 81 | Ok(buf) 82 | } 83 | 84 | async fn write_word(&mut self, start_word: u16, data: [u8; 2]) -> Result<(), Error> { 85 | let start = usize::from(start_word * 2); 86 | 87 | self.write_cache[start..(start + 2)].copy_from_slice(&data); 88 | 89 | Ok(()) 90 | } 91 | 92 | async fn clear_errors(&self) -> Result<(), Error> { 93 | Ok(()) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/fmmu.rs: -------------------------------------------------------------------------------- 1 | /// Fieldbus Memory Management Unit (FMMU). 2 | /// 3 | /// Used to map segments of the Process Data Image (PDI) to various parts of the SubDevice memory 4 | /// space. 5 | /// 6 | /// ETG1000.4 Table 56 – Fieldbus memory management unit (FMMU) entity. 7 | #[derive(Default, Copy, Clone, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)] 8 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 9 | #[wire(bytes = 16)] 10 | pub struct Fmmu { 11 | /// This parameter shall contain the start address in octets in the logical memory area of the 12 | /// memory translation. 13 | #[wire(bytes = 4)] 14 | pub logical_start_address: u32, 15 | 16 | #[wire(bytes = 2)] 17 | pub length_bytes: u16, 18 | 19 | #[wire(bits = 3, post_skip = 5)] 20 | pub logical_start_bit: u8, 21 | 22 | #[wire(bits = 3, post_skip = 5)] 23 | pub logical_end_bit: u8, 24 | 25 | #[wire(bytes = 2)] 26 | pub physical_start_address: u16, 27 | 28 | #[wire(bits = 3, post_skip = 5)] 29 | pub physical_start_bit: u8, 30 | 31 | #[wire(bits = 1)] 32 | pub read_enable: bool, 33 | 34 | #[wire(bits = 1, post_skip = 6)] 35 | pub write_enable: bool, 36 | 37 | // Lots of spare bytes after this one! 38 | #[wire(bits = 1, post_skip = 31)] 39 | pub enable: bool, 40 | } 41 | 42 | impl core::fmt::Debug for Fmmu { 43 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 44 | f.debug_struct("Fmmu") 45 | .field( 46 | "logical_start_address", 47 | &format_args!("{:#010x}", self.logical_start_address), 48 | ) 49 | .field("length_bytes", &self.length_bytes) 50 | .field("logical_start_bit", &self.logical_start_bit) 51 | .field("logical_end_bit", &self.logical_end_bit) 52 | .field( 53 | "physical_start_address", 54 | &format_args!("{:#06x}", self.physical_start_address), 55 | ) 56 | .field("physical_start_bit", &self.physical_start_bit) 57 | .field("read_enable", &self.read_enable) 58 | .field("write_enable", &self.write_enable) 59 | .field("enable", &self.enable) 60 | .finish() 61 | } 62 | } 63 | 64 | impl core::fmt::Display for Fmmu { 65 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 66 | f.write_fmt(format_args!( 67 | "logical start {:#010x}:{}, size {}, logical end bit {}, physical start {:#06x}:{}, {}{}, {}", 68 | self.logical_start_address, 69 | self.logical_start_bit, 70 | self.length_bytes, 71 | self.logical_end_bit, 72 | self.physical_start_address, 73 | self.physical_start_bit, 74 | if self.read_enable { "R" } else { "" }, 75 | if self.write_enable { "W" } else { "O" }, 76 | if self.enable{ "enabled" } else { "disabled" }, 77 | )) 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | use core::mem; 85 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireSized}; 86 | 87 | #[test] 88 | fn size() { 89 | // Unpacked size 90 | assert_eq!(mem::size_of::(), 16); 91 | // Packed size 92 | assert_eq!(Fmmu::PACKED_LEN, 16); 93 | } 94 | 95 | #[test] 96 | fn decode_one() { 97 | let raw = [ 98 | // Logical start address 99 | 0x00, 0x00, 0x00, 0x00, // 100 | // Length 101 | 0x01, 0x00, // 102 | // Logical start bit 103 | 0x00, // 104 | // Logical end bit 105 | 0x03, // 106 | // Physical start address 107 | 0x00, 0x10, // 108 | // Phyiscal start bit 109 | 0x00, // 110 | // Read/write enable 111 | 0x01, // 112 | // FMMU enable 113 | 0x01, // 114 | // Padding 115 | 0x00, 0x00, 0x00, 116 | ]; 117 | 118 | let fmmu = Fmmu::unpack_from_slice(&raw).unwrap(); 119 | 120 | assert_eq!( 121 | fmmu, 122 | Fmmu { 123 | logical_start_address: 0, 124 | length_bytes: 1, 125 | logical_start_bit: 0, 126 | logical_end_bit: 3, 127 | physical_start_address: 0x1000, 128 | physical_start_bit: 0, 129 | read_enable: true, 130 | write_enable: false, 131 | enable: true, 132 | } 133 | ) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/generate.rs: -------------------------------------------------------------------------------- 1 | //! Functions used to populate buffers with multiple values. 2 | //! 3 | //! Like cookie_factory but much simpler and will **quite happily panic**. 4 | 5 | /// Write a packed struct into the slice. 6 | pub fn write_packed(value: T, buf: &mut [u8]) -> &mut [u8] 7 | where 8 | T: ethercrab_wire::EtherCrabWireWrite, 9 | { 10 | value.pack_to_slice_unchecked(buf); 11 | 12 | &mut buf[value.packed_len()..] 13 | } 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | /// Skip `n` bytes. 18 | fn skip(len: usize, buf: &mut [u8]) -> &mut [u8] { 19 | let len = len.min(buf.len()); 20 | 21 | &mut buf[len..] 22 | } 23 | 24 | #[test] 25 | fn skip_clamp() { 26 | let mut buf = [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 27 | 28 | assert_eq!(skip(10, &mut buf), &[10]); 29 | assert_eq!(skip(11, &mut buf), &[]); 30 | assert_eq!(skip(12, &mut buf), &[]); 31 | } 32 | 33 | #[test] 34 | fn skip_0() { 35 | let mut buf = [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 36 | let mut expected = buf; 37 | 38 | assert_eq!(skip(0, &mut buf), &mut expected); 39 | } 40 | 41 | #[test] 42 | fn skip_1() { 43 | let mut buf = [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 44 | let mut expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 45 | 46 | assert_eq!(skip(1, &mut buf), &mut expected); 47 | } 48 | 49 | #[test] 50 | fn skip_many() { 51 | let mut buf = [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 52 | let mut expected = [5, 6, 7, 8, 9, 10]; 53 | 54 | assert_eq!(skip(5, &mut buf), &mut expected); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/maindevice_config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration passed to [`MainDevice`](crate::MainDevice). 2 | 3 | /// Configuration passed to [`MainDevice`](crate::MainDevice). 4 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 5 | pub struct MainDeviceConfig { 6 | /// The number of `FRMW` packets to send during the static phase of Distributed Clocks (DC) 7 | /// synchronisation. 8 | /// 9 | /// Defaults to 10000. 10 | /// 11 | /// If this is set to zero, no static sync will be performed. 12 | pub dc_static_sync_iterations: u32, 13 | 14 | /// EtherCAT packet (PDU) network retry behaviour. 15 | pub retry_behaviour: RetryBehaviour, 16 | } 17 | 18 | impl Default for MainDeviceConfig { 19 | fn default() -> Self { 20 | Self { 21 | dc_static_sync_iterations: 10_000, 22 | retry_behaviour: RetryBehaviour::default(), 23 | } 24 | } 25 | } 26 | 27 | /// Network communication retry policy. 28 | /// 29 | /// Retries will be performed at the rate defined by [`Timeouts::pdu`](crate::Timeouts::pdu). 30 | #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] 31 | pub enum RetryBehaviour { 32 | /// Do not attempt to retry timed out packet sends (default). 33 | /// 34 | /// If this option is chosen, any timeouts will raise an 35 | /// [`Error::Timeout`](crate::error::Error::Timeout). 36 | #[default] 37 | None, 38 | 39 | /// Attempt to resend a PDU up to `N` times, then raise an 40 | /// [`Error::Timeout`](crate::error::Error::Timeout). 41 | Count(usize), 42 | 43 | /// Attempt to resend the PDU forever(*). 44 | /// 45 | /// Note that this can soft-lock a program if for example the EtherCAT network cable is removed 46 | /// as EtherCrab will attempt to resend the packet forever. It may be preferable to use 47 | /// [`RetryBehaviour::Count`] to set an upper bound on retries. 48 | /// 49 | /// (*) Forever in this case means a retry count of `usize::MAX`. 50 | Forever, 51 | } 52 | 53 | impl RetryBehaviour { 54 | pub(crate) const fn retry_count(&self) -> usize { 55 | match self { 56 | // Try at least once when used in a range like `for _ in 0..`. 57 | RetryBehaviour::None => 0, 58 | RetryBehaviour::Count(n) => *n, 59 | RetryBehaviour::Forever => usize::MAX, 60 | } 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | 68 | #[test] 69 | fn retry_count_sanity_check() { 70 | assert_eq!(RetryBehaviour::None.retry_count(), 0); 71 | assert_eq!(RetryBehaviour::Count(10).retry_count(), 10); 72 | assert_eq!(RetryBehaviour::Forever.retry_count(), usize::MAX); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/pdu_loop/frame_element/sendable_frame.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::Error, 3 | ethernet::EthernetFrame, 4 | fmt, 5 | pdu_loop::{ 6 | frame_element::{FrameBox, FrameElement, FrameState}, 7 | frame_header::EthercatFrameHeader, 8 | }, 9 | }; 10 | use core::{ptr::NonNull, sync::atomic::AtomicU8}; 11 | use ethercrab_wire::EtherCrabWireSized; 12 | 13 | /// An EtherCAT frame that is ready to be sent over the network. 14 | /// 15 | /// This struct can be acquired by calling 16 | /// [`PduLoop::next_sendable_frame`](crate::pdu_loop::PduTx::next_sendable_frame). 17 | /// 18 | /// # Examples 19 | /// 20 | /// ```rust,no_run 21 | /// # use ethercrab::PduStorage; 22 | /// use core::future::poll_fn; 23 | /// use core::task::Poll; 24 | /// 25 | /// # static PDU_STORAGE: PduStorage<2, { PduStorage::element_size(2) }> = PduStorage::new(); 26 | /// let (mut pdu_tx, _pdu_rx, _pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 27 | /// 28 | /// let mut buf = [0u8; 1530]; 29 | /// 30 | /// poll_fn(|ctx| { 31 | /// // Set the waker so this future is polled again when new EtherCAT frames are ready to 32 | /// // be sent. 33 | /// pdu_tx.replace_waker(ctx.waker()); 34 | /// 35 | /// if let Some(frame) = pdu_tx.next_sendable_frame() { 36 | /// frame.send_blocking(|data| { 37 | /// // Send packet over the network interface here 38 | /// 39 | /// // Return the number of bytes sent over the network 40 | /// Ok(data.len()) 41 | /// }); 42 | /// 43 | /// // Wake the future so it's polled again in case there are more frames to send 44 | /// ctx.waker().wake_by_ref(); 45 | /// } 46 | /// 47 | /// Poll::<()>::Pending 48 | /// }); 49 | /// ``` 50 | #[derive(Debug)] 51 | pub struct SendableFrame<'sto> { 52 | pub(in crate::pdu_loop) inner: FrameBox<'sto>, 53 | } 54 | 55 | unsafe impl Send for SendableFrame<'_> {} 56 | 57 | impl<'sto> SendableFrame<'sto> { 58 | pub(crate) fn claim_sending( 59 | frame: NonNull>, 60 | pdu_idx: &'sto AtomicU8, 61 | frame_data_len: usize, 62 | ) -> Option { 63 | let frame = unsafe { FrameElement::claim_sending(frame)? }; 64 | 65 | Some(Self { 66 | inner: FrameBox::new(frame, pdu_idx, frame_data_len), 67 | }) 68 | } 69 | 70 | /// The frame has been sent by the network driver. 71 | fn mark_sent(&self) { 72 | fmt::trace!("Frame index {} is sent", self.inner.storage_slot_index()); 73 | 74 | self.inner.set_state(FrameState::Sent); 75 | } 76 | 77 | pub(crate) fn storage_slot_index(&self) -> u8 { 78 | self.inner.storage_slot_index() 79 | } 80 | 81 | /// Used on send failure to release the frame sending claim so the frame can attempt to be sent 82 | /// again, or reclaimed for reuse. 83 | fn release_sending_claim(&self) { 84 | self.inner.set_state(FrameState::Sendable); 85 | } 86 | 87 | fn as_bytes(&self) -> &[u8] { 88 | let frame = self.inner.ethernet_frame().into_inner(); 89 | 90 | let len = EthernetFrame::<&[u8]>::buffer_len( 91 | EthercatFrameHeader::PACKED_LEN + self.inner.pdu_payload_len(), 92 | ); 93 | 94 | &frame[0..len] 95 | } 96 | 97 | /// Get the Ethernet frame length of this frame. 98 | #[allow(clippy::len_without_is_empty)] 99 | pub fn len(&self) -> usize { 100 | self.as_bytes().len() 101 | } 102 | 103 | /// Send the frame using a blocking callback. 104 | /// 105 | /// The closure must return the number of bytes sent over the network interface. If this does 106 | /// not match the length of the packet passed to the closure, this method will return an error. 107 | pub fn send_blocking( 108 | self, 109 | send: impl FnOnce(&[u8]) -> Result, 110 | ) -> Result { 111 | let len = self.as_bytes().len(); 112 | 113 | match send(self.as_bytes()) { 114 | Ok(bytes_sent) if bytes_sent == len => { 115 | self.mark_sent(); 116 | 117 | Ok(bytes_sent) 118 | } 119 | Ok(bytes_sent) => { 120 | self.release_sending_claim(); 121 | 122 | Err(Error::PartialSend { 123 | len, 124 | sent: bytes_sent, 125 | }) 126 | } 127 | Err(res) => { 128 | self.release_sending_claim(); 129 | 130 | Err(res) 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/pdu_loop/frame_header.rs: -------------------------------------------------------------------------------- 1 | //! An EtherCAT frame header. 2 | 3 | use crate::LEN_MASK; 4 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireSized, EtherCrabWireWrite}; 5 | 6 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, ethercrab_wire::EtherCrabWireRead)] 7 | #[repr(u8)] 8 | pub(crate) enum ProtocolType { 9 | DlPdu = 0x01u8, 10 | // Not currently supported. 11 | // NetworkVariables = 0x04, 12 | // Mailbox = 0x05, 13 | // #[wire(catch_all)] 14 | // Unknown(u8), 15 | } 16 | 17 | /// An EtherCAT frame header. 18 | /// 19 | /// An EtherCAT frame can contain one or more PDUs after this header, each starting with a 20 | /// [`PduHeader`](crate::pdu_loop::pdu_header). 21 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 22 | pub struct EthercatFrameHeader { 23 | pub(crate) payload_len: u16, 24 | pub(crate) protocol: ProtocolType, 25 | } 26 | 27 | impl EtherCrabWireSized for EthercatFrameHeader { 28 | const PACKED_LEN: usize = 2; 29 | 30 | type Buffer = [u8; 2]; 31 | 32 | fn buffer() -> Self::Buffer { 33 | [0u8; 2] 34 | } 35 | } 36 | 37 | impl EtherCrabWireRead for EthercatFrameHeader { 38 | fn unpack_from_slice(buf: &[u8]) -> Result { 39 | let raw = u16::unpack_from_slice(buf)?; 40 | 41 | Ok(Self { 42 | payload_len: raw & LEN_MASK, 43 | protocol: ProtocolType::try_from((raw >> 12) as u8)?, 44 | }) 45 | } 46 | } 47 | 48 | impl EtherCrabWireWrite for EthercatFrameHeader { 49 | fn pack_to_slice_unchecked<'buf>(&self, buf: &'buf mut [u8]) -> &'buf [u8] { 50 | // Protocol in last 4 bits 51 | let raw = self.payload_len | ((self.protocol as u16) << 12); 52 | 53 | raw.pack_to_slice_unchecked(buf) 54 | } 55 | 56 | fn packed_len(&self) -> usize { 57 | Self::PACKED_LEN 58 | } 59 | } 60 | 61 | impl EthercatFrameHeader { 62 | /// Create a new PDU frame header. 63 | pub fn pdu(len: u16) -> Self { 64 | debug_assert!( 65 | len <= LEN_MASK, 66 | "Frame length may not exceed {} bytes", 67 | LEN_MASK 68 | ); 69 | 70 | Self { 71 | payload_len: len & LEN_MASK, 72 | // Only support DlPdu (for now?) 73 | protocol: ProtocolType::DlPdu, 74 | } 75 | } 76 | 77 | /// Convenience method for naming consistency. 78 | pub(crate) const fn header_len() -> usize { 79 | Self::PACKED_LEN 80 | } 81 | } 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | use super::*; 86 | 87 | #[test] 88 | fn pdu_header() { 89 | let header = EthercatFrameHeader::pdu(0x28); 90 | 91 | let mut buf = [0u8; 2]; 92 | 93 | let packed = header.pack_to_slice_unchecked(&mut buf); 94 | 95 | let expected = &0b0001_0000_0010_1000u16.to_le_bytes(); 96 | 97 | assert_eq!(packed, expected); 98 | } 99 | 100 | #[test] 101 | fn decode_pdu_len() { 102 | let raw = 0b0001_0000_0010_1000u16; 103 | 104 | let header = EthercatFrameHeader::unpack_from_slice(&raw.to_le_bytes()).unwrap(); 105 | 106 | assert_eq!(header.payload_len, 0x28); 107 | assert_eq!(header.protocol, ProtocolType::DlPdu); 108 | } 109 | 110 | #[test] 111 | fn parse() { 112 | // Header from packet #39, soem-sdinfo-ek1100-only.pcapng 113 | let raw = [0x3cu8, 0x10]; 114 | 115 | let header = EthercatFrameHeader::unpack_from_slice(&raw).unwrap(); 116 | 117 | assert_eq!(header.payload_len, 0x3c); 118 | assert_eq!(header.protocol, ProtocolType::DlPdu); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/pdu_loop/pdu_flags.rs: -------------------------------------------------------------------------------- 1 | use crate::LEN_MASK; 2 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireSized, WireError}; 3 | 4 | /// PDU fields placed after ADP and ADO, e.g. `LEN`, `C` and `NEXT` fields in ETG1000.4 5.4.1.2 5 | /// Table 14 – Auto increment physical read (APRD). 6 | #[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] 7 | pub struct PduFlags { 8 | /// Data length of this PDU. 9 | pub(crate) length: u16, 10 | /// Circulating frame 11 | /// 12 | /// 0: Frame is not circulating, 13 | /// 1: Frame has circulated once 14 | pub(crate) circulated: bool, 15 | /// 0: last EtherCAT PDU in EtherCAT frame 16 | /// 1: EtherCAT PDU in EtherCAT frame follows 17 | pub(crate) more_follows: bool, 18 | } 19 | 20 | impl PduFlags { 21 | pub const fn new(data_len: u16, more_follows: bool) -> PduFlags { 22 | Self { 23 | length: data_len, 24 | more_follows, 25 | circulated: false, 26 | } 27 | } 28 | 29 | pub const fn with_len(len: u16) -> Self { 30 | Self::new(len, false) 31 | } 32 | 33 | pub const fn len(self) -> u16 { 34 | self.length 35 | } 36 | 37 | pub const fn const_default() -> Self { 38 | Self::with_len(0) 39 | } 40 | } 41 | 42 | impl ethercrab_wire::EtherCrabWireWrite for PduFlags { 43 | fn pack_to_slice_unchecked<'buf>(&self, buf: &'buf mut [u8]) -> &'buf [u8] { 44 | let raw = self.length & LEN_MASK 45 | | ((self.circulated as u16) << 14) 46 | | ((self.more_follows as u16) << 15); 47 | 48 | let buf = &mut buf[0..self.packed_len()]; 49 | 50 | buf.copy_from_slice(&raw.to_le_bytes()); 51 | 52 | buf 53 | } 54 | 55 | fn packed_len(&self) -> usize { 56 | ::PACKED_LEN 57 | } 58 | } 59 | 60 | impl EtherCrabWireRead for PduFlags { 61 | fn unpack_from_slice(buf: &[u8]) -> Result { 62 | let src = u16::unpack_from_slice(buf)?; 63 | 64 | let length = src & LEN_MASK; 65 | let circulated = (src >> 14) & 0x01 == 0x01; 66 | let is_not_last = (src >> 15) & 0x01 == 0x01; 67 | 68 | Ok(Self { 69 | length, 70 | circulated, 71 | more_follows: is_not_last, 72 | }) 73 | } 74 | } 75 | 76 | impl EtherCrabWireSized for PduFlags { 77 | const PACKED_LEN: usize = 2; 78 | 79 | type Buffer = [u8; Self::PACKED_LEN]; 80 | 81 | fn buffer() -> Self::Buffer { 82 | [0u8; Self::PACKED_LEN] 83 | } 84 | } 85 | 86 | impl ethercrab_wire::EtherCrabWireWriteSized for PduFlags { 87 | fn pack(&self) -> Self::Buffer { 88 | let mut buf = [0u8; Self::PACKED_LEN]; 89 | 90 | ethercrab_wire::EtherCrabWireWrite::pack_to_slice_unchecked(self, &mut buf); 91 | 92 | buf 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use ethercrab_wire::EtherCrabWireWriteSized; 99 | 100 | use super::*; 101 | 102 | #[test] 103 | fn pdu_flags_round_trip() { 104 | let flags = PduFlags { 105 | length: 0x110, 106 | circulated: false, 107 | more_follows: true, 108 | }; 109 | 110 | let packed = flags.pack(); 111 | 112 | assert_eq!(packed, [0x10, 0x81]); 113 | 114 | let unpacked = PduFlags::unpack_from_slice(&packed).unwrap(); 115 | 116 | assert_eq!(unpacked, flags); 117 | } 118 | 119 | #[test] 120 | fn correct_length() { 121 | let flags = PduFlags { 122 | length: 1036, 123 | circulated: false, 124 | more_follows: false, 125 | }; 126 | 127 | assert_eq!(flags.len(), 1036); 128 | 129 | assert_eq!(flags.pack(), [0b0000_1100, 0b0000_0100]); 130 | assert_eq!(flags.pack(), [0x0c, 0x04]); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/pdu_loop/pdu_rx.rs: -------------------------------------------------------------------------------- 1 | use super::storage::PduStorageRef; 2 | use crate::ethernet::{EthernetAddress, EthernetFrame}; 3 | use crate::{ 4 | ETHERCAT_ETHERTYPE, MAINDEVICE_ADDR, 5 | error::{Error, PduError}, 6 | fmt, 7 | pdu_loop::frame_header::EthercatFrameHeader, 8 | }; 9 | use core::sync::atomic::Ordering; 10 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireSized}; 11 | 12 | /// What happened to a received Ethernet frame. 13 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 14 | pub enum ReceiveAction { 15 | /// The frame was ignored. 16 | /// 17 | /// This can be caused by other, non-EtherCAT traffic on the chosen network interface, e.g. if 18 | /// sending EtherCAT packets through a switch. 19 | Ignored, 20 | 21 | /// The frame was successfully processed as an EtherCAT packet. 22 | Processed, 23 | } 24 | 25 | /// EtherCAT frame receive adapter. 26 | pub struct PduRx<'sto> { 27 | storage: PduStorageRef<'sto>, 28 | source_mac: EthernetAddress, 29 | } 30 | 31 | impl<'sto> PduRx<'sto> { 32 | pub(in crate::pdu_loop) fn new(storage: PduStorageRef<'sto>) -> Self { 33 | Self { 34 | storage, 35 | source_mac: MAINDEVICE_ADDR, 36 | } 37 | } 38 | 39 | /// Set the source MAC address to the given value. 40 | /// 41 | /// This is required on macOS (and BSD I believe) as the interface's MAC address cannot be 42 | /// overridden at the packet level for some reason. 43 | #[cfg(all(not(target_os = "linux"), unix))] 44 | pub(crate) fn set_source_mac(&mut self, new: EthernetAddress) { 45 | self.source_mac = new 46 | } 47 | 48 | /// Given a complete Ethernet II frame, parse a response PDU from it and wake the future that 49 | /// sent the frame. 50 | // NOTE: &mut self so this struct can only be used in one place. 51 | pub fn receive_frame(&mut self, ethernet_frame: &[u8]) -> Result { 52 | if self.should_exit() { 53 | return Ok(ReceiveAction::Ignored); 54 | } 55 | 56 | let raw_packet = EthernetFrame::new_checked(ethernet_frame)?; 57 | 58 | // Look for EtherCAT packets whilst ignoring broadcast packets sent from self. As per 59 | // , the 60 | // first SubDevice will set the second bit of the MSB of the MAC address (U/L bit). This means 61 | // if we send e.g. 10:10:10:10:10:10, we receive 12:10:10:10:10:10 which passes through this 62 | // filter. 63 | if raw_packet.ethertype() != ETHERCAT_ETHERTYPE || raw_packet.src_addr() == self.source_mac 64 | { 65 | fmt::trace!("Ignore frame"); 66 | 67 | return Ok(ReceiveAction::Ignored); 68 | } 69 | 70 | let i = raw_packet.payload(); 71 | 72 | let frame_header = EthercatFrameHeader::unpack_from_slice(i).inspect_err(|&e| { 73 | fmt::error!("Failed to parse frame header: {}", e); 74 | })?; 75 | 76 | if frame_header.payload_len == 0 { 77 | fmt::trace!("Ignoring empty frame"); 78 | 79 | return Ok(ReceiveAction::Ignored); 80 | } 81 | 82 | // Skip EtherCAT header and get PDU(s) payload 83 | let i = i 84 | .get( 85 | EthercatFrameHeader::PACKED_LEN 86 | ..(EthercatFrameHeader::PACKED_LEN + usize::from(frame_header.payload_len)), 87 | ) 88 | .ok_or_else(|| { 89 | fmt::error!("Received frame is too short"); 90 | 91 | Error::ReceiveFrame 92 | })?; 93 | 94 | // `i` now contains the EtherCAT frame payload, consisting of one or more PDUs including 95 | // their headers and payloads. 96 | 97 | // Second byte of first PDU header is the index 98 | let pdu_idx = *i.get(1).ok_or(Error::Internal)?; 99 | 100 | // We're assuming all PDUs in the returned frame have the same frame index, so we can just 101 | // use the first one. 102 | 103 | // PDU has its own EtherCAT index. This needs mapping back to the original frame. 104 | let frame_index = self 105 | .storage 106 | .frame_index_by_first_pdu_index(pdu_idx) 107 | .ok_or(Error::Pdu(PduError::Decode))?; 108 | 109 | fmt::trace!( 110 | "Receiving frame index {} (found from PDU {:#04x})", 111 | frame_index, 112 | pdu_idx 113 | ); 114 | 115 | let mut frame = self 116 | .storage 117 | .claim_receiving(frame_index) 118 | .ok_or(PduError::InvalidIndex(frame_index))?; 119 | 120 | let frame_data = frame.buf_mut(); 121 | 122 | frame_data 123 | .get_mut(0..i.len()) 124 | .ok_or(Error::Internal)? 125 | .copy_from_slice(i); 126 | 127 | frame.mark_received()?; 128 | 129 | Ok(ReceiveAction::Processed) 130 | } 131 | 132 | /// Returns `true` if the PDU sender should exit. 133 | /// 134 | /// This will be triggered by [`MainDevice::release_all`](crate::MainDevice::release_all). 135 | pub fn should_exit(&self) -> bool { 136 | self.storage.exit_flag.load(Ordering::Acquire) 137 | } 138 | 139 | /// Reset this object ready for reuse. 140 | /// 141 | /// When giving back ownership of the `PduRx`, be sure to call 142 | /// [`release`](crate::PduRx::release) to ensure all internal state is correct before reuse. 143 | pub fn release(self) -> Self { 144 | self.storage.exit_flag.store(false, Ordering::Relaxed); 145 | 146 | self 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/pdu_loop/pdu_tx.rs: -------------------------------------------------------------------------------- 1 | use super::{frame_element::sendable_frame::SendableFrame, storage::PduStorageRef}; 2 | use core::{sync::atomic::Ordering, task::Waker}; 3 | 4 | /// EtherCAT frame transmit adapter. 5 | pub struct PduTx<'sto> { 6 | storage: PduStorageRef<'sto>, 7 | } 8 | 9 | impl<'sto> PduTx<'sto> { 10 | pub(in crate::pdu_loop) fn new(storage: PduStorageRef<'sto>) -> Self { 11 | Self { storage } 12 | } 13 | 14 | /// The number of frames that can be in flight at once. 15 | pub fn capacity(&self) -> usize { 16 | self.storage.num_frames 17 | } 18 | 19 | /// Get the next sendable frame, if any are available. 20 | // NOTE: Mutable so it can only be used in one task. 21 | pub fn next_sendable_frame(&mut self) -> Option> { 22 | for idx in 0..self.storage.num_frames { 23 | if self.should_exit() { 24 | return None; 25 | } 26 | 27 | let frame = self.storage.frame_at_index(idx); 28 | 29 | let Some(sending) = SendableFrame::claim_sending( 30 | frame, 31 | self.storage.pdu_idx, 32 | self.storage.frame_data_len, 33 | ) else { 34 | continue; 35 | }; 36 | 37 | return Some(sending); 38 | } 39 | 40 | None 41 | } 42 | 43 | /// Set or replace the PDU loop waker. 44 | /// 45 | /// The waker must be set otherwise the future in charge of sending new packets will not be 46 | /// woken again, causing a timeout error. 47 | /// 48 | /// # Examples 49 | /// 50 | /// ```rust,no_run 51 | /// # use ethercrab::PduStorage; 52 | /// use core::future::poll_fn; 53 | /// use core::task::Poll; 54 | /// 55 | /// # static PDU_STORAGE: PduStorage<2, { PduStorage::element_size(2) }> = PduStorage::new(); 56 | /// let (pdu_tx, _pdu_rx, _pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 57 | /// 58 | /// poll_fn(|ctx| { 59 | /// // Set the waker so this future is polled again when new EtherCAT frames are ready to 60 | /// // be sent. 61 | /// pdu_tx.replace_waker(ctx.waker()); 62 | /// 63 | /// // Send and receive packets over the network interface here 64 | /// 65 | /// Poll::<()>::Pending 66 | /// }); 67 | /// ``` 68 | #[cfg_attr( 69 | any(target_os = "windows", target_os = "macos", not(feature = "std")), 70 | allow(unused) 71 | )] 72 | pub fn replace_waker(&self, waker: &Waker) { 73 | self.storage.tx_waker.register(waker); 74 | } 75 | 76 | /// Returns `true` if the PDU sender should exit. 77 | /// 78 | /// This will be triggered by [`MainDevice::release_all`](crate::MainDevice::release_all). When 79 | /// giving back ownership of the `PduTx`, be sure to call [`release`](crate::PduTx::release) to 80 | /// ensure all internal state is correct before reuse. 81 | pub fn should_exit(&self) -> bool { 82 | self.storage.exit_flag.load(Ordering::Acquire) 83 | } 84 | 85 | /// Reset this object ready for reuse. 86 | pub fn release(self) -> Self { 87 | self.storage.exit_flag.store(false, Ordering::Relaxed); 88 | 89 | self 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/std/mod.rs: -------------------------------------------------------------------------------- 1 | //! Items required for running in `std` environments. 2 | 3 | #[cfg(target_os = "linux")] 4 | mod io_uring; 5 | #[cfg(unix)] 6 | mod unix; 7 | #[cfg(target_os = "windows")] 8 | mod windows; 9 | #[cfg(all(target_os = "linux", feature = "xdp"))] 10 | mod xdp; 11 | 12 | use std::{ 13 | sync::Arc, 14 | task::Wake, 15 | thread::{self, Thread}, 16 | }; 17 | 18 | #[cfg(target_os = "windows")] 19 | pub use self::windows::{TxRxTaskConfig, ethercat_now, tx_rx_task_blocking}; 20 | #[cfg(unix)] 21 | pub use unix::{ethercat_now, tx_rx_task}; 22 | // io_uring is Linux-only 23 | #[cfg(target_os = "linux")] 24 | pub use io_uring::tx_rx_task_io_uring; 25 | #[cfg(all(target_os = "linux", feature = "xdp"))] 26 | pub use xdp::tx_rx_task_xdp; 27 | 28 | struct ParkSignal { 29 | current_thread: Thread, 30 | } 31 | 32 | impl ParkSignal { 33 | fn new() -> Self { 34 | Self { 35 | current_thread: thread::current(), 36 | } 37 | } 38 | 39 | fn wait(&self) { 40 | thread::park(); 41 | } 42 | 43 | // fn wait_timeout(&self, timeout: Duration) { 44 | // thread::park_timeout(timeout) 45 | // } 46 | } 47 | 48 | impl Wake for ParkSignal { 49 | fn wake(self: Arc) { 50 | self.current_thread.unpark(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/std/unix/linux.rs: -------------------------------------------------------------------------------- 1 | //! Copied from SmolTCP's RawSocketDesc, with inspiration from 2 | //! [https://github.com/embassy-rs/embassy](https://github.com/embassy-rs/embassy/blob/master/examples/std/src/tuntap.rs). 3 | 4 | use crate::{ 5 | ETHERCAT_ETHERTYPE, 6 | std::unix::{ifreq, ifreq_for}, 7 | }; 8 | use async_io::IoSafe; 9 | use core::ptr::addr_of; 10 | use std::{ 11 | io, mem, 12 | os::{ 13 | fd::{AsFd, BorrowedFd}, 14 | unix::io::{AsRawFd, RawFd}, 15 | }, 16 | }; 17 | 18 | pub struct RawSocketDesc { 19 | lower: i32, 20 | ifreq: ifreq, 21 | } 22 | 23 | impl RawSocketDesc { 24 | pub fn new(name: &str) -> io::Result { 25 | let protocol = ETHERCAT_ETHERTYPE as i16; 26 | 27 | let lower = unsafe { 28 | let lower = libc::socket( 29 | // Ethernet II frames 30 | libc::AF_PACKET, 31 | libc::SOCK_RAW | libc::SOCK_NONBLOCK, 32 | protocol.to_be() as i32, 33 | ); 34 | if lower == -1 { 35 | return Err(io::Error::last_os_error()); 36 | } 37 | lower 38 | }; 39 | 40 | let mut self_ = RawSocketDesc { 41 | lower, 42 | ifreq: ifreq_for(name), 43 | }; 44 | 45 | self_.bind_interface()?; 46 | 47 | Ok(self_) 48 | } 49 | 50 | fn bind_interface(&mut self) -> io::Result<()> { 51 | let protocol = ETHERCAT_ETHERTYPE as i16; 52 | 53 | let sockaddr = libc::sockaddr_ll { 54 | sll_family: libc::AF_PACKET as u16, 55 | sll_protocol: protocol.to_be() as u16, 56 | sll_ifindex: ifreq_ioctl(self.lower, &mut self.ifreq, libc::SIOCGIFINDEX)?, 57 | sll_hatype: 1, 58 | sll_pkttype: 0, 59 | sll_halen: 6, 60 | sll_addr: [0; 8], 61 | }; 62 | 63 | unsafe { 64 | #[allow(trivial_casts)] 65 | let res = libc::bind( 66 | self.lower, 67 | addr_of!(sockaddr).cast(), 68 | mem::size_of::() as libc::socklen_t, 69 | ); 70 | if res == -1 { 71 | return Err(io::Error::last_os_error()); 72 | } 73 | } 74 | 75 | Ok(()) 76 | } 77 | 78 | pub fn interface_mtu(&mut self) -> io::Result { 79 | ifreq_ioctl(self.lower, &mut self.ifreq, libc::SIOCGIFMTU).map(|mtu| mtu as usize) 80 | } 81 | } 82 | 83 | impl AsRawFd for RawSocketDesc { 84 | fn as_raw_fd(&self) -> RawFd { 85 | self.lower 86 | } 87 | } 88 | 89 | impl AsFd for RawSocketDesc { 90 | fn as_fd(&self) -> BorrowedFd<'_> { 91 | unsafe { BorrowedFd::borrow_raw(self.lower) } 92 | } 93 | } 94 | 95 | // SAFETY: Implementing this trait pledges that the underlying socket resource will not be dropped 96 | // by `Read` or `Write` impls. More information can be read 97 | // [here](https://docs.rs/async-io/latest/async_io/trait.IoSafe.html). 98 | unsafe impl IoSafe for RawSocketDesc {} 99 | 100 | impl Drop for RawSocketDesc { 101 | fn drop(&mut self) { 102 | unsafe { 103 | libc::close(self.lower); 104 | } 105 | } 106 | } 107 | 108 | impl io::Read for RawSocketDesc { 109 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 110 | let len = unsafe { libc::read(self.as_raw_fd(), buf.as_mut_ptr().cast(), buf.len()) }; 111 | if len == -1 { 112 | Err(io::Error::last_os_error()) 113 | } else { 114 | Ok(len as usize) 115 | } 116 | } 117 | } 118 | 119 | impl io::Write for RawSocketDesc { 120 | fn write(&mut self, buf: &[u8]) -> io::Result { 121 | let len = unsafe { libc::write(self.as_raw_fd(), buf.as_ptr().cast(), buf.len()) }; 122 | if len == -1 { 123 | Err(io::Error::last_os_error()) 124 | } else { 125 | Ok(len as usize) 126 | } 127 | } 128 | 129 | fn flush(&mut self) -> io::Result<()> { 130 | Ok(()) 131 | } 132 | } 133 | 134 | fn ifreq_ioctl( 135 | lower: libc::c_int, 136 | ifreq: &mut ifreq, 137 | cmd: libc::c_ulong, 138 | ) -> io::Result { 139 | unsafe { 140 | #[allow(trivial_casts)] 141 | #[cfg(target_env = "musl")] 142 | let res = libc::ioctl(lower, cmd as libc::c_int, ifreq as *mut ifreq); 143 | #[allow(trivial_casts)] 144 | #[cfg(not(target_env = "musl"))] 145 | let res = libc::ioctl(lower, cmd, ifreq as *mut ifreq); 146 | 147 | if res == -1 { 148 | return Err(io::Error::last_os_error()); 149 | } 150 | } 151 | 152 | Ok(ifreq.ifr_data) 153 | } 154 | -------------------------------------------------------------------------------- /src/subdevice/dc.rs: -------------------------------------------------------------------------------- 1 | //! Distributed Clock configuration for a single SubDevice. 2 | 3 | use core::{fmt, time::Duration}; 4 | 5 | /// DC sync configuration for a SubDevice. 6 | #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub enum DcSync { 9 | /// DC sync is disabled for this SubDevice. 10 | #[default] 11 | Disabled, 12 | 13 | /// This SubDevice synchronises on the SYNC0 pulse. 14 | Sync0, 15 | 16 | /// Both SYNC0 and SYNC1 are enabled. 17 | /// 18 | /// SubDevices with an `AssignActivate` value of `0x0700` in their ESI definition should set 19 | /// this value as well as [`sync0_period`](crate::subdevice_group::DcConfiguration::sync0_period) in 20 | /// the SubDevice group DC configuration. 21 | Sync01 { 22 | /// SYNC1 cycle time. 23 | sync1_period: Duration, 24 | }, 25 | } 26 | 27 | impl fmt::Display for DcSync { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | match self { 30 | DcSync::Disabled => f.write_str("disabled"), 31 | DcSync::Sync0 => f.write_str("SYNC0"), 32 | DcSync::Sync01 { sync1_period } => { 33 | write!(f, "SYNC0 with SYNC1 period {} us", sync1_period.as_micros()) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/subdevice/types.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eeprom::types::{MailboxProtocols, SyncManagerType}, 3 | pdi::PdiSegment, 4 | }; 5 | use core::fmt::{self, Debug}; 6 | 7 | /// SubDevice identity information (vendor ID, product ID, etc). 8 | #[derive(Default, Copy, Clone, PartialEq, ethercrab_wire::EtherCrabWireRead)] 9 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 10 | #[wire(bytes = 16)] 11 | #[doc(alias = "SlaveIdentity")] 12 | pub struct SubDeviceIdentity { 13 | /// Vendor ID. 14 | #[wire(bytes = 4)] 15 | pub vendor_id: u32, 16 | /// Product ID. 17 | #[wire(bytes = 4)] 18 | pub product_id: u32, 19 | /// Product revision. 20 | #[wire(bytes = 4)] 21 | pub revision: u32, 22 | /// Device serial number. 23 | #[wire(bytes = 4)] 24 | pub serial: u32, 25 | } 26 | 27 | impl fmt::Display for SubDeviceIdentity { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | f.write_fmt(format_args!( 30 | "vendor: {:#010x}, product {:#010x}, rev {}, serial {}", 31 | self.vendor_id, self.product_id, self.revision, self.serial 32 | )) 33 | } 34 | } 35 | 36 | impl Debug for SubDeviceIdentity { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | f.debug_struct("SubDeviceIdentity") 39 | .field("vendor_id", &format_args!("{:#010x}", self.vendor_id)) 40 | .field("product_id", &format_args!("{:#010x}", self.product_id)) 41 | .field( 42 | "revision", 43 | &format_args!("{:#010x} ({})", self.revision, self.revision), 44 | ) 45 | .field("serial", &self.serial) 46 | .finish() 47 | } 48 | } 49 | 50 | #[derive(Debug, Default, Clone, PartialEq)] 51 | pub struct SubDeviceConfig { 52 | pub io: IoRanges, 53 | pub mailbox: MailboxConfig, 54 | } 55 | 56 | #[derive(Debug, Default, Clone, PartialEq)] 57 | pub struct MailboxConfig { 58 | pub(in crate::subdevice) read: Option, 59 | pub(in crate::subdevice) write: Option, 60 | pub(in crate::subdevice) supported_protocols: MailboxProtocols, 61 | pub(in crate::subdevice) coe_sync_manager_types: heapless::Vec, 62 | pub(in crate::subdevice) has_coe: bool, 63 | /// True if Complete Access is supported. 64 | pub(in crate::subdevice) complete_access: bool, 65 | } 66 | 67 | #[derive(Debug, Default, Clone, Copy, PartialEq)] 68 | pub struct Mailbox { 69 | pub(in crate::subdevice) address: u16, 70 | pub(in crate::subdevice) len: u16, 71 | pub(in crate::subdevice) sync_manager: u8, 72 | } 73 | 74 | #[derive(Debug, Default, Clone, PartialEq)] 75 | pub struct IoRanges { 76 | pub input: PdiSegment, 77 | pub output: PdiSegment, 78 | } 79 | -------------------------------------------------------------------------------- /src/subdevice_group/group_id.rs: -------------------------------------------------------------------------------- 1 | /// A group's unique ID. 2 | #[doc(hidden)] 3 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] 4 | pub struct GroupId(pub(in crate::subdevice_group) usize); 5 | 6 | impl From for usize { 7 | fn from(value: GroupId) -> Self { 8 | value.0 9 | } 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | use crate::{SubDeviceGroup, subdevice_group::PreOp}; 15 | 16 | #[test] 17 | fn group_unique_id_defaults() { 18 | let g1 = SubDeviceGroup::<16, 16, PreOp>::default(); 19 | let g2 = SubDeviceGroup::<16, 16, PreOp>::default(); 20 | let g3 = SubDeviceGroup::<16, 16, PreOp>::default(); 21 | 22 | assert_ne!(g1.id, g2.id); 23 | assert_ne!(g2.id, g3.id); 24 | assert_ne!(g1.id, g3.id); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/subdevice_group/handle.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | GroupId, MainDevice, SubDevice, SubDeviceGroup, SubDeviceRef, error::Error, fmt, pdi::PdiOffset, 3 | }; 4 | 5 | /// A trait implemented only by [`SubDeviceGroup`] so multiple groups with different const params 6 | /// can be stored in a hashmap, `Vec`, etc. 7 | #[doc(hidden)] 8 | #[sealed::sealed] 9 | pub trait SubDeviceGroupHandle: Sync { 10 | /// Get the group's ID. 11 | fn id(&self) -> GroupId; 12 | 13 | /// Add a SubDevice device to this group. 14 | unsafe fn push(&self, subdevice: SubDevice) -> Result<(), Error>; 15 | 16 | /// Get a reference to the group with const generic params erased. 17 | fn as_ref(&self) -> SubDeviceGroupRef<'_>; 18 | } 19 | 20 | #[sealed::sealed] 21 | impl SubDeviceGroupHandle 22 | for SubDeviceGroup 23 | where 24 | S: Sync, 25 | { 26 | fn id(&self) -> GroupId { 27 | self.id 28 | } 29 | 30 | unsafe fn push(&self, subdevice: SubDevice) -> Result<(), Error> { 31 | unsafe { (*self.inner.get()).subdevices.push(subdevice) } 32 | .map_err(|_| Error::Capacity(crate::error::Item::SubDevice)) 33 | } 34 | 35 | fn as_ref(&self) -> SubDeviceGroupRef<'_> { 36 | SubDeviceGroupRef { 37 | max_pdi_len: MAX_PDI, 38 | inner: { 39 | let inner = unsafe { fmt::unwrap_opt!(self.inner.get().as_mut()) }; 40 | 41 | GroupInnerRef { 42 | subdevices: &mut inner.subdevices, 43 | pdi_start: &mut inner.pdi_start, 44 | } 45 | }, 46 | } 47 | } 48 | } 49 | 50 | #[derive(Debug)] 51 | struct GroupInnerRef<'a> { 52 | subdevices: &'a mut [SubDevice], 53 | pdi_start: &'a mut PdiOffset, 54 | } 55 | 56 | /// A reference to a [`SubDeviceGroup`](crate::SubDeviceGroup) returned by the closure passed to 57 | /// [`MainDevice::init`](crate::MainDevice::init). 58 | #[doc(alias = "SlaveGroupRef")] 59 | pub struct SubDeviceGroupRef<'a> { 60 | /// Maximum PDI length in bytes. 61 | max_pdi_len: usize, 62 | inner: GroupInnerRef<'a>, 63 | } 64 | 65 | impl SubDeviceGroupRef<'_> { 66 | /// Initialise all SubDevices in the group and place them in PRE-OP. 67 | // Clippy: shush 68 | #[allow(clippy::wrong_self_convention)] 69 | pub(crate) async fn into_pre_op<'sto>( 70 | &mut self, 71 | pdi_position: PdiOffset, 72 | maindevice: &'sto MainDevice<'sto>, 73 | ) -> Result { 74 | let inner = &mut self.inner; 75 | 76 | // Set the starting position in the PDI for this group's segment 77 | *inner.pdi_start = pdi_position; 78 | 79 | fmt::debug!( 80 | "Going to configure group with {} SubDevice(s), starting PDI offset {:#08x}", 81 | inner.subdevices.len(), 82 | inner.pdi_start.start_address 83 | ); 84 | 85 | // Configure master read PDI mappings in the first section of the PDI 86 | for subdevice in inner.subdevices.iter_mut() { 87 | let mut subdevice_config = 88 | SubDeviceRef::new(maindevice, subdevice.configured_address(), subdevice); 89 | 90 | // TODO: Move PRE-OP transition out of this so we can do it for the group just once 91 | subdevice_config.configure_mailboxes().await?; 92 | } 93 | 94 | Ok(pdi_position.increment(self.max_pdi_len as u16)) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/subdevice_group/tx_rx_response.rs: -------------------------------------------------------------------------------- 1 | use crate::SubDeviceState; 2 | use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireSized}; 3 | 4 | /// Response information from transmitting the Process Data Image (PDI). 5 | #[derive(Debug, PartialEq)] 6 | #[non_exhaustive] 7 | pub struct TxRxResponse { 8 | /// Working counter. 9 | pub working_counter: u16, 10 | 11 | /// The status of all SubDevices **within this group**. 12 | pub subdevice_states: heapless::Vec, 13 | 14 | /// Additional data, for example a [`CycleInfo`](crate::subdevice_group::CycleInfo) struct 15 | /// holding Distributed Clocks information. 16 | pub extra: T, 17 | } 18 | 19 | bitflags::bitflags! { 20 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 21 | pub struct GroupState: u8 { 22 | /// No state recorded/read/known. 23 | const NONE = 0x00; 24 | /// EtherCAT `INIT` state. 25 | const INIT = 0x01; 26 | /// EtherCAT `PRE-OP` state. 27 | const PRE_OP = 0x02; 28 | // Commented out as it creates an ambiguity between INIT | PREOP and BOOTSTRAP. 29 | // /// EtherCAT `BOOT` state. 30 | // const BOOTSTRAP = 0x03; 31 | /// EtherCAT `SAFE-OP` state. 32 | const SAFE_OP = 0x04; 33 | /// EtherCAT `OP` state. 34 | const OP = 0x08; 35 | } 36 | } 37 | 38 | impl EtherCrabWireSized for GroupState { 39 | const PACKED_LEN: usize = 1; 40 | 41 | type Buffer = [u8; Self::PACKED_LEN]; 42 | 43 | fn buffer() -> Self::Buffer { 44 | [0u8; Self::PACKED_LEN] 45 | } 46 | } 47 | 48 | impl EtherCrabWireRead for GroupState { 49 | fn unpack_from_slice(buf: &[u8]) -> Result { 50 | u8::unpack_from_slice(buf) 51 | .and_then(|value| Self::from_bits(value).ok_or(ethercrab_wire::WireError::InvalidValue)) 52 | } 53 | } 54 | 55 | impl TxRxResponse { 56 | /// Get a bitmap of all SubDevice states in this group. 57 | pub fn group_state(&self) -> GroupState { 58 | let bitmap = self 59 | .subdevice_states 60 | .iter() 61 | .fold(0u8, |acc, state| acc | u8::from(*state)); 62 | 63 | GroupState::from_bits_truncate(bitmap) 64 | } 65 | 66 | /// If all SubDevices in the group are in the same state, return that state. 67 | /// 68 | /// If more than one state is present in the group, `None` will be returned. 69 | pub fn group_in_single_state(&self) -> Option { 70 | let state = self.group_state(); 71 | 72 | if state.bits().count_ones() > 1 { 73 | None 74 | } else { 75 | match state { 76 | GroupState::NONE => Some(SubDeviceState::None), 77 | GroupState::INIT => Some(SubDeviceState::Init), 78 | GroupState::PRE_OP => Some(SubDeviceState::PreOp), 79 | GroupState::SAFE_OP => Some(SubDeviceState::SafeOp), 80 | GroupState::OP => Some(SubDeviceState::Op), 81 | _ => None, 82 | } 83 | } 84 | } 85 | 86 | /// Test if every SubDevice in the group is in the same given state. 87 | /// 88 | /// Note that testing for `SubDeviceState::Bootstrap` will always return false due to 89 | /// ambiguities between between `INIT | PREOP`` and `BOOTSTRAP`. 90 | pub fn is_in_state(&self, desired_state: SubDeviceState) -> bool { 91 | let state = self.group_state(); 92 | 93 | match desired_state { 94 | SubDeviceState::None => state == GroupState::NONE, 95 | SubDeviceState::Init => state == GroupState::INIT, 96 | SubDeviceState::PreOp => state == GroupState::PRE_OP, 97 | // Always false as this is ambiguous between INIT | PREOP and BOOTSTRAP 98 | SubDeviceState::Bootstrap => false, 99 | SubDeviceState::SafeOp => state == GroupState::SAFE_OP, 100 | SubDeviceState::Op => state == GroupState::OP, 101 | SubDeviceState::Other(n) => state.bits() == n, 102 | } 103 | } 104 | 105 | /// A helper method to ease EtherCrab version upgrades. 106 | pub fn all_op(&self) -> bool { 107 | self.group_in_single_state() 108 | .filter(|s| *s == SubDeviceState::Op) 109 | .is_some() 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use super::*; 116 | 117 | #[test] 118 | fn all_op() { 119 | let all_op = TxRxResponse { 120 | working_counter: 0, 121 | subdevice_states: { 122 | let mut v = heapless::Vec::<_, 3>::new(); 123 | 124 | let _ = v.push(SubDeviceState::Op); 125 | let _ = v.push(SubDeviceState::Op); 126 | let _ = v.push(SubDeviceState::Op); 127 | 128 | v 129 | }, 130 | extra: (), 131 | }; 132 | 133 | let some_op = TxRxResponse { 134 | working_counter: 0, 135 | subdevice_states: { 136 | let mut v = heapless::Vec::<_, 3>::new(); 137 | 138 | let _ = v.push(SubDeviceState::Op); 139 | let _ = v.push(SubDeviceState::SafeOp); 140 | let _ = v.push(SubDeviceState::Op); 141 | 142 | v 143 | }, 144 | extra: (), 145 | }; 146 | 147 | assert!(all_op.is_in_state(SubDeviceState::Op)); 148 | assert!(!some_op.is_in_state(SubDeviceState::Op)); 149 | } 150 | 151 | #[test] 152 | fn none_state() { 153 | let res = TxRxResponse { 154 | working_counter: 0, 155 | subdevice_states: { 156 | let mut v = heapless::Vec::<_, 3>::new(); 157 | 158 | let _ = v.push(SubDeviceState::Init); 159 | let _ = v.push(SubDeviceState::PreOp); 160 | let _ = v.push(SubDeviceState::PreOp); 161 | 162 | v 163 | }, 164 | extra: (), 165 | }; 166 | 167 | assert!(!res.is_in_state(SubDeviceState::SafeOp)); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/subdevice_state.rs: -------------------------------------------------------------------------------- 1 | /// AL (application layer) state for a single SubDevice. 2 | /// 3 | /// Read from register `0x0130` ([`RegisterAddress::AlStatus`](crate::register::RegisterAddress::AlStatus)). 4 | /// 5 | /// Defined in ETG1000.6 6.4.1, ETG1000.6 Table 9. 6 | #[derive(Debug, Copy, Clone, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)] 7 | #[doc(alias = "SlaveState")] 8 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 9 | #[cfg_attr(feature = "serde", derive(serde::Serialize))] 10 | #[repr(u8)] 11 | pub enum SubDeviceState { 12 | /// No state recorded/read/known. 13 | None = 0x00, 14 | /// EtherCAT `INIT` state. 15 | Init = 0x01, 16 | /// EtherCAT `PRE-OP` state. 17 | PreOp = 0x02, 18 | /// EtherCAT `BOOT` state. 19 | Bootstrap = 0x03, 20 | /// EtherCAT `SAFE-OP` state. 21 | SafeOp = 0x04, 22 | /// EtherCAT `OP` state. 23 | Op = 0x8, 24 | /// State is a combination of above variants or is an unknown value. 25 | #[wire(catch_all)] 26 | Other(u8), 27 | } 28 | 29 | impl Default for SubDeviceState { 30 | fn default() -> Self { 31 | Self::None 32 | } 33 | } 34 | 35 | impl core::fmt::Display for SubDeviceState { 36 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 37 | match self { 38 | SubDeviceState::None => f.write_str("None"), 39 | SubDeviceState::Init => f.write_str("Init"), 40 | SubDeviceState::PreOp => f.write_str("Pre-Operational"), 41 | SubDeviceState::Bootstrap => f.write_str("Bootstrap"), 42 | SubDeviceState::SafeOp => f.write_str("Safe-Operational"), 43 | SubDeviceState::Op => f.write_str("Operational"), 44 | SubDeviceState::Other(value) => write!(f, "Other({:01x})", value), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/timer_factory.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use core::{future::Future, pin::Pin, task::Poll, time::Duration}; 3 | 4 | #[cfg(not(feature = "std"))] 5 | pub(crate) type Timer = embassy_time::Timer; 6 | #[cfg(all(not(miri), feature = "std"))] 7 | pub(crate) type Timer = async_io::Timer; 8 | #[cfg(miri)] 9 | pub(crate) type Timer = core::future::Pending<()>; 10 | 11 | #[cfg(not(feature = "std"))] 12 | pub(crate) fn timer(duration: Duration) -> Timer { 13 | embassy_time::Timer::after(embassy_time::Duration::from_micros( 14 | duration.as_micros() as u64 15 | )) 16 | } 17 | 18 | #[cfg(all(not(miri), feature = "std"))] 19 | pub(crate) fn timer(duration: Duration) -> Timer { 20 | async_io::Timer::after(duration) 21 | } 22 | 23 | #[cfg(miri)] 24 | pub(crate) fn timer(_duration: Duration) -> Timer { 25 | core::future::pending() 26 | } 27 | 28 | pub(crate) trait IntoTimeout { 29 | fn timeout(self, timeout: Duration) -> TimeoutFuture>>; 30 | } 31 | 32 | impl IntoTimeout for T 33 | where 34 | T: Future>, 35 | { 36 | fn timeout(self, timeout: Duration) -> TimeoutFuture>> { 37 | TimeoutFuture { 38 | f: self, 39 | timeout: timer(timeout), 40 | #[cfg(miri)] 41 | duration: timeout, 42 | } 43 | } 44 | } 45 | 46 | pub(crate) struct TimeoutFuture { 47 | f: F, 48 | timeout: Timer, 49 | #[cfg(miri)] 50 | duration: Duration, 51 | } 52 | 53 | impl Future for TimeoutFuture 54 | where 55 | F: Future>, 56 | { 57 | type Output = Result; 58 | 59 | fn poll(self: Pin<&mut Self>, cx: &mut core::task::Context<'_>) -> Poll { 60 | let this = unsafe { self.get_unchecked_mut() }; 61 | let timeout = unsafe { Pin::new_unchecked(&mut this.timeout) }; 62 | let f = unsafe { Pin::new_unchecked(&mut this.f) }; 63 | 64 | #[cfg(miri)] 65 | if this.duration == Duration::ZERO { 66 | return Poll::Ready(Err(Error::Timeout)); 67 | } 68 | 69 | if timeout.poll(cx).is_ready() { 70 | return Poll::Ready(Err(Error::Timeout)); 71 | } 72 | 73 | if let Poll::Ready(x) = f.poll(cx) { 74 | return Poll::Ready(x); 75 | } 76 | 77 | Poll::Pending 78 | } 79 | } 80 | 81 | /// Timeout configuration for the EtherCrab master. 82 | #[derive(Copy, Clone, Debug)] 83 | pub struct Timeouts { 84 | /// How long to wait for a SubDevice state change, e.g. SAFE-OP to OP. 85 | /// 86 | /// This timeout is global for all state transitions. 87 | pub state_transition: Duration, 88 | 89 | /// How long to wait for a PDU response. 90 | pub pdu: Duration, 91 | 92 | /// How long to wait for a single EEPROM operation. 93 | pub eeprom: Duration, 94 | 95 | /// Polling duration of wait loops. 96 | /// 97 | /// Some operations require repeatedly reading something from a SubDevice until a value changes. 98 | /// This duration specifies the wait time between polls. 99 | /// 100 | /// This defaults to a timeout of 0 to keep latency to a minimum. 101 | pub wait_loop_delay: Duration, 102 | 103 | /// How long to wait for a SubDevice mailbox to become ready. 104 | pub mailbox_echo: Duration, 105 | 106 | /// How long to wait for a response to be read from the SubDevice's response mailbox. 107 | pub mailbox_response: Duration, 108 | } 109 | 110 | impl Timeouts { 111 | pub(crate) async fn loop_tick(&self) { 112 | #[cfg(not(miri))] 113 | timer(self.wait_loop_delay).await; 114 | #[cfg(miri)] 115 | std::thread::yield_now(); 116 | } 117 | } 118 | 119 | impl Default for Timeouts { 120 | fn default() -> Self { 121 | Self { 122 | state_transition: Duration::from_millis(5000), 123 | pdu: Duration::from_micros(30_000), 124 | eeprom: Duration::from_millis(10), 125 | wait_loop_delay: Duration::from_millis(0), 126 | mailbox_echo: Duration::from_millis(100), 127 | mailbox_response: Duration::from_millis(1000), 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests 2 | 3 | Uses Wireshark captures of known-good runs as replays to test for regressions against. 4 | 5 | ## Capturing replays 6 | 7 | Captures should be run in debug mode to make sure everything has time to breathe. If this is not 8 | done, the replays can fail in weird and confusing ways. 9 | 10 | ### With `just` 11 | 12 | From the repository root, run e.g. 13 | 14 | ```bash 15 | just capture-replay replay-ek1100-el2828-el2889 enx00e04c680066 16 | ``` 17 | 18 | The replay name must **exactly** match the name of a file in `tests/` (without the `.rs`) extension. 19 | 20 | Debian users may need to run: 21 | 22 | - `sudo apt install psmisc` 23 | - `cargo install fd-find` 24 | 25 | Note that the Debian apt package `fd-find` installs the binary as `fd-find`, however the script 26 | looks for just `fd`. Installing with `cargo` doesn't present this issue. 27 | 28 | ### Manually on local machine (terminal method) 29 | 30 | `sudo apt install -y tshark` 31 | 32 | Then e.g. 33 | 34 | ```bash 35 | tshark -w tests/replay-ek1100-el2828-el2889.pcapng --interface enp2s0 -f 'ether proto 0x88a4' 36 | ``` 37 | 38 | ### Remote machine (GUI method) 39 | 40 | On remote machine from project root e.g. 41 | 42 | ```bash 43 | ssh ethercrab 'sudo tcpdump -U -i enp2s0 -w -' | wireshark -f 'ecat' -i - -w ./tests/replay-ek1100-el2828-el2889.pcapng -k 44 | ``` 45 | 46 | Then run the test using real interface e.g.: 47 | 48 | ```bash 49 | INTERFACE=enp2s0 just linux-test replay 50 | ``` 51 | 52 | ### Filtering captured replays 53 | 54 | Use `tshark` to filter out only EtherCAT packets from provided dumps: 55 | 56 | ```bash 57 | tshark -r EL1014.pcapng -Y 'ecat' -w issue-63-el1014.pcapng 58 | ``` 59 | 60 | This isn't required if the original capture is filtered with `-f 'ether proto 0x88a4'`. 61 | -------------------------------------------------------------------------------- /tests/init-must-be-send.rs: -------------------------------------------------------------------------------- 1 | //! A weird looking test, but it just makes sure the EtherCrab init routines are `Send`. 2 | 3 | use core::future::Future; 4 | use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, Timeouts, std::ethercat_now}; 5 | use std::{sync::Arc, time::Duration}; 6 | 7 | #[test] 8 | fn init_must_be_send() { 9 | fn spawn<'a, F, T>(_fut: F) -> Result<(), ()> 10 | where 11 | F: Future + Send + 'a, 12 | T: 'a, 13 | { 14 | // Don't bother running the future - this is just a compile test 15 | Ok(()) 16 | } 17 | 18 | let _ = spawn(init()); 19 | } 20 | 21 | const MAX_SUBDEVICES: usize = 16; 22 | const MAX_PDU_DATA: usize = 1100; 23 | const MAX_FRAMES: usize = 16; 24 | const PDI_LEN: usize = 64; 25 | 26 | static PDU_STORAGE: PduStorage = PduStorage::new(); 27 | 28 | async fn init() { 29 | let (_tx, _rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 30 | 31 | let maindevice = Arc::new(MainDevice::new( 32 | pdu_loop, 33 | Timeouts { 34 | wait_loop_delay: Duration::from_millis(2), 35 | mailbox_response: Duration::from_millis(1000), 36 | ..Default::default() 37 | }, 38 | MainDeviceConfig::default(), 39 | )); 40 | 41 | let _group = maindevice 42 | .init_single_group::(ethercat_now) 43 | .await 44 | .expect("Init"); 45 | } 46 | -------------------------------------------------------------------------------- /tests/replay-dc.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/tests/replay-dc.pcapng -------------------------------------------------------------------------------- /tests/replay-ek1100-alias-address.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/tests/replay-ek1100-alias-address.pcapng -------------------------------------------------------------------------------- /tests/replay-ek1100-alias-address.rs: -------------------------------------------------------------------------------- 1 | //! Set alias address. 2 | //! 3 | //! Required hardware: 4 | //! 5 | //! - EK1100 6 | 7 | mod util; 8 | 9 | use env_logger::Env; 10 | use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, Timeouts, error::Error}; 11 | use std::{path::PathBuf, time::Duration}; 12 | use tokio::time::sleep; 13 | 14 | const MAX_SUBDEVICES: usize = 16; 15 | const MAX_PDU_DATA: usize = PduStorage::element_size(1100); 16 | const MAX_FRAMES: usize = 128; 17 | const PDI_LEN: usize = 64; 18 | 19 | #[tokio::test] 20 | #[cfg_attr(miri, ignore)] 21 | async fn replay_ek1100_alias_address() -> Result<(), Error> { 22 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 23 | 24 | static PDU_STORAGE: PduStorage = PduStorage::new(); 25 | 26 | let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 27 | 28 | let maindevice = MainDevice::new( 29 | pdu_loop, 30 | Timeouts::default(), 31 | MainDeviceConfig { 32 | dc_static_sync_iterations: 100, 33 | ..Default::default() 34 | }, 35 | ); 36 | 37 | let test_name = PathBuf::from(file!()) 38 | .file_stem() 39 | .unwrap() 40 | .to_string_lossy() 41 | .to_string(); 42 | 43 | util::spawn_tx_rx(&format!("tests/{test_name}.pcapng"), tx, rx); 44 | 45 | log::debug!("Beginning init"); 46 | 47 | let mut group = maindevice 48 | .init_single_group::(|| 0) 49 | .await 50 | .expect("Init"); 51 | 52 | log::debug!("Init complete"); 53 | 54 | let mut sds = group.iter_mut(&maindevice); 55 | 56 | let mut ek1100 = sds.next().expect("at least one subdevice required"); 57 | 58 | assert_eq!(ek1100.name(), "EK1100", "Group must start with EK1100"); 59 | 60 | ek1100.set_alias_address(0xabcd).await?; 61 | 62 | log::debug!("Alias set"); 63 | 64 | // Let EEPROM settle a bit 65 | sleep(Duration::from_millis(50)).await; 66 | 67 | assert_eq!( 68 | ek1100.read_alias_address_from_eeprom(&maindevice).await, 69 | Ok(0xabcd) 70 | ); 71 | 72 | sleep(Duration::from_millis(50)).await; 73 | 74 | // Reset 75 | ek1100.set_alias_address(0x0000).await?; 76 | 77 | sleep(Duration::from_millis(50)).await; 78 | 79 | assert_eq!( 80 | ek1100.read_alias_address_from_eeprom(&maindevice).await, 81 | Ok(0x0000) 82 | ); 83 | 84 | sleep(Duration::from_millis(50)).await; 85 | 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /tests/replay-ek1100-el2828-el2889.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/tests/replay-ek1100-el2828-el2889.pcapng -------------------------------------------------------------------------------- /tests/replay-ek1100-el2828-el2889.rs: -------------------------------------------------------------------------------- 1 | //! Replay comms between EK1100, EL2828, EL2889. Based on `multiple-groups` demo at time of writing. 2 | //! 3 | //! Required hardware: 4 | //! 5 | //! - EK1100 6 | //! - EL2828 7 | //! - EL2889 8 | 9 | mod util; 10 | 11 | use env_logger::Env; 12 | use ethercrab::{ 13 | MainDevice, MainDeviceConfig, PduStorage, SubDeviceGroup, Timeouts, error::Error, 14 | subdevice_group, 15 | }; 16 | use std::{path::PathBuf, time::Duration}; 17 | use tokio::time::MissedTickBehavior; 18 | 19 | const MAX_SUBDEVICES: usize = 16; 20 | const MAX_PDU_DATA: usize = PduStorage::element_size(1100); 21 | const MAX_FRAMES: usize = 128; 22 | 23 | #[derive(Default)] 24 | struct Groups { 25 | /// EL2889 and EK1100. 2 items, 2 bytes of PDI for 16 output bits. 26 | /// 27 | /// We'll keep the EK1100 in here as it has no PDI but still needs to live somewhere. 28 | slow_outputs: SubDeviceGroup<2, 2, subdevice_group::PreOp>, 29 | /// EL2828. 1 item, 1 byte of PDI for 8 output bits. 30 | fast_outputs: SubDeviceGroup<1, 1, subdevice_group::PreOp>, 31 | } 32 | 33 | #[tokio::test] 34 | #[cfg_attr(miri, ignore)] 35 | async fn replay_ek1100_el2828_el2889() -> Result<(), Error> { 36 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 37 | 38 | static PDU_STORAGE: PduStorage = PduStorage::new(); 39 | 40 | let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 41 | 42 | let maindevice = MainDevice::new( 43 | pdu_loop, 44 | Timeouts::default(), 45 | MainDeviceConfig { 46 | dc_static_sync_iterations: 100, 47 | ..Default::default() 48 | }, 49 | ); 50 | 51 | let test_name = PathBuf::from(file!()) 52 | .file_stem() 53 | .unwrap() 54 | .to_string_lossy() 55 | .to_string(); 56 | 57 | util::spawn_tx_rx(&format!("tests/{test_name}.pcapng"), tx, rx); 58 | 59 | // Read configurations from SubDevice EEPROMs and configure devices. 60 | let groups = maindevice 61 | .init::( 62 | || 0, 63 | |groups: &Groups, subdevice| match subdevice.name() { 64 | "EL2889" | "EK1100" => Ok(&groups.slow_outputs), 65 | "EL2828" => Ok(&groups.fast_outputs), 66 | _ => Err(Error::UnknownSubDevice), 67 | }, 68 | ) 69 | .await 70 | .expect("Init"); 71 | 72 | let Groups { 73 | slow_outputs, 74 | fast_outputs, 75 | } = groups; 76 | 77 | let slow_outputs = slow_outputs 78 | .into_op(&maindevice) 79 | .await 80 | .expect("Slow into OP"); 81 | let fast_outputs = fast_outputs 82 | .into_op(&maindevice) 83 | .await 84 | .expect("Fast into OP"); 85 | 86 | assert_eq!( 87 | slow_outputs.subdevice(&maindevice, 0).unwrap().name(), 88 | "EK1100" 89 | ); 90 | assert_eq!( 91 | slow_outputs.subdevice(&maindevice, 1).unwrap().name(), 92 | "EL2889" 93 | ); 94 | assert_eq!( 95 | fast_outputs.subdevice(&maindevice, 0).unwrap().name(), 96 | "EL2828" 97 | ); 98 | 99 | let mut slow_cycle_time = tokio::time::interval(Duration::from_millis(10)); 100 | slow_cycle_time.set_missed_tick_behavior(MissedTickBehavior::Skip); 101 | 102 | { 103 | let el2889 = slow_outputs 104 | .subdevice(&maindevice, 1) 105 | .expect("EL2889 not present!"); 106 | 107 | // Set initial output state 108 | el2889.outputs_raw_mut()[0] = 0x01; 109 | el2889.outputs_raw_mut()[1] = 0x80; 110 | } 111 | 112 | // Animate slow pattern for 8 ticks 113 | for _ in 0..8 { 114 | slow_outputs.tx_rx(&maindevice).await.expect("TX/RX"); 115 | 116 | let el2889 = slow_outputs 117 | .subdevice(&maindevice, 1) 118 | .expect("EL2889 not present!"); 119 | 120 | let mut o = el2889.outputs_raw_mut(); 121 | 122 | // Make a nice pattern on EL2889 LEDs 123 | o[0] = o[0].rotate_left(1); 124 | o[1] = o[1].rotate_right(1); 125 | 126 | slow_cycle_time.tick().await; 127 | } 128 | 129 | let mut fast_cycle_time = tokio::time::interval(Duration::from_millis(5)); 130 | fast_cycle_time.set_missed_tick_behavior(MissedTickBehavior::Skip); 131 | 132 | // Count up to 255 in binary 133 | for _ in 0..255 { 134 | fast_outputs.tx_rx(&maindevice).await.expect("TX/RX"); 135 | 136 | // Increment every output byte for every SubDevice by one 137 | for subdevice in fast_outputs.iter(&maindevice) { 138 | let mut o = subdevice.outputs_raw_mut(); 139 | 140 | for byte in o.iter_mut() { 141 | *byte = byte.wrapping_add(1); 142 | } 143 | } 144 | 145 | fast_cycle_time.tick().await; 146 | } 147 | 148 | Ok(()) 149 | } 150 | -------------------------------------------------------------------------------- /tests/replay-ek1914-el3004-configure.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/tests/replay-ek1914-el3004-configure.pcapng -------------------------------------------------------------------------------- /tests/replay-ek1914-el3004-configure.rs: -------------------------------------------------------------------------------- 1 | //! Test that SDO multi writes behave as expected. 2 | //! 3 | //! - EK1914 4 | //! - EL3004 5 | 6 | mod util; 7 | 8 | use env_logger::Env; 9 | use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, Timeouts, error::Error}; 10 | use std::path::PathBuf; 11 | 12 | const MAX_SUBDEVICES: usize = 16; 13 | const MAX_PDU_DATA: usize = PduStorage::element_size(1100); 14 | const MAX_FRAMES: usize = 128; 15 | const PDI_LEN: usize = 128; 16 | 17 | #[tokio::test] 18 | #[cfg_attr(miri, ignore)] 19 | async fn replay_ek1914_el3004_configure() -> Result<(), Error> { 20 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 21 | 22 | static PDU_STORAGE: PduStorage = PduStorage::new(); 23 | 24 | let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 25 | 26 | let maindevice = MainDevice::new( 27 | pdu_loop, 28 | Timeouts::default(), 29 | MainDeviceConfig { 30 | dc_static_sync_iterations: 100, 31 | ..Default::default() 32 | }, 33 | ); 34 | 35 | let test_name = PathBuf::from(file!()) 36 | .file_stem() 37 | .unwrap() 38 | .to_string_lossy() 39 | .to_string(); 40 | 41 | util::spawn_tx_rx(&format!("tests/{test_name}.pcapng"), tx, rx); 42 | 43 | // Read configurations from SubDevice EEPROMs and configure devices. 44 | let group = maindevice 45 | .init_single_group::(|| 0) 46 | .await 47 | .expect("Init"); 48 | 49 | assert_eq!(group.subdevice(&maindevice, 0)?.name(), "EK1914"); 50 | assert_eq!(group.subdevice(&maindevice, 1)?.name(), "EL3004"); 51 | 52 | let el3004 = group.subdevice(&maindevice, 1)?; 53 | 54 | el3004.sdo_write(0x1c12, 0, 0u8).await?; 55 | 56 | el3004 57 | .sdo_write_array(0x1c13, &[0x1a00u16, 0x1a02, 0x1a04, 0x1a06]) 58 | .await?; 59 | 60 | assert_eq!(el3004.sdo_read::(0x1c13, 1).await?, 0x1a00u16); 61 | assert_eq!(el3004.sdo_read::(0x1c13, 2).await?, 0x1a02u16); 62 | assert_eq!(el3004.sdo_read::(0x1c13, 3).await?, 0x1a04u16); 63 | assert_eq!(el3004.sdo_read::(0x1c13, 4).await?, 0x1a06u16); 64 | assert_eq!(el3004.sdo_read::(0x1c13, 0).await?, 4u8); 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /tests/replay-ek1914-el3004-mailbox.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/tests/replay-ek1914-el3004-mailbox.pcapng -------------------------------------------------------------------------------- /tests/replay-ek1914-el3004-mailbox.rs: -------------------------------------------------------------------------------- 1 | //! Test that mailboxes can be read/written. This test requires: 2 | //! 3 | //! - EK1914 4 | //! - EL3004 5 | 6 | mod util; 7 | 8 | use env_logger::Env; 9 | use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, Timeouts, error::Error}; 10 | use std::path::PathBuf; 11 | 12 | const MAX_SUBDEVICES: usize = 16; 13 | const MAX_PDU_DATA: usize = PduStorage::element_size(1100); 14 | const MAX_FRAMES: usize = 128; 15 | const PDI_LEN: usize = 128; 16 | 17 | #[tokio::test] 18 | #[cfg_attr(miri, ignore)] 19 | async fn replay_ek1914_el3004_mailbox() -> Result<(), Error> { 20 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 21 | 22 | static PDU_STORAGE: PduStorage = PduStorage::new(); 23 | 24 | let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 25 | 26 | let maindevice = MainDevice::new( 27 | pdu_loop, 28 | Timeouts::default(), 29 | MainDeviceConfig { 30 | dc_static_sync_iterations: 100, 31 | ..Default::default() 32 | }, 33 | ); 34 | 35 | let test_name = PathBuf::from(file!()) 36 | .file_stem() 37 | .unwrap() 38 | .to_string_lossy() 39 | .to_string(); 40 | 41 | util::spawn_tx_rx(&format!("tests/{test_name}.pcapng"), tx, rx); 42 | 43 | // Read configurations from SubDevice EEPROMs and configure devices. 44 | let group = maindevice 45 | .init_single_group::(|| 0) 46 | .await 47 | .expect("Init"); 48 | 49 | assert_eq!(group.subdevice(&maindevice, 0)?.name(), "EK1914"); 50 | assert_eq!(group.subdevice(&maindevice, 1)?.name(), "EL3004"); 51 | 52 | let mut configured = false; 53 | 54 | for subdevice in group.iter(&maindevice) { 55 | log::info!("--> SubDevice {}", subdevice.name()); 56 | 57 | if subdevice.name() == "EL3004" { 58 | log::info!("--> Configuring EL3004"); 59 | 60 | // Check we can read 61 | let fw_version = subdevice 62 | .sdo_read::>(0x100a, 0) 63 | .await?; 64 | 65 | log::info!("----> FW version: {}", fw_version); 66 | 67 | // Check we can write 68 | subdevice.sdo_write(0xf008, 0, 1u32).await?; 69 | 70 | log::info!("----> Wrote outputs"); 71 | 72 | assert_eq!( 73 | subdevice.sdo_read(0xf008, 0).await, 74 | Ok(1u32), 75 | "written value was not stored" 76 | ); 77 | 78 | configured = true; 79 | } 80 | } 81 | 82 | assert!(configured, "did not find target SubDevice"); 83 | 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /tests/replay-ek1914-no-complete-access.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/tests/replay-ek1914-no-complete-access.pcapng -------------------------------------------------------------------------------- /tests/replay-ek1914-no-complete-access.rs: -------------------------------------------------------------------------------- 1 | //! Checking that SubDevices with no support for CoE complete access still initialise. 2 | //! 3 | //! Required hardware: 4 | //! 5 | //! - EK1914 (does not support CoE complete access) 6 | 7 | mod util; 8 | 9 | use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts, error::Error}; 10 | use std::{path::PathBuf, time::Duration}; 11 | 12 | const MAX_SUBDEVICES: usize = 16; 13 | const MAX_PDU_DATA: usize = PduStorage::element_size(1100); 14 | const MAX_FRAMES: usize = 128; 15 | const PDI_LEN: usize = 128; 16 | 17 | #[tokio::test] 18 | #[cfg_attr(miri, ignore)] 19 | async fn replay_ek1914_no_complete_access() -> Result<(), Error> { 20 | static PDU_STORAGE: PduStorage = PduStorage::new(); 21 | 22 | let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 23 | 24 | let maindevice = MainDevice::new( 25 | pdu_loop, 26 | Timeouts { 27 | wait_loop_delay: Duration::from_millis(5), 28 | ..Timeouts::default() 29 | }, 30 | MainDeviceConfig { 31 | dc_static_sync_iterations: 100, 32 | retry_behaviour: RetryBehaviour::None, 33 | }, 34 | ); 35 | 36 | let test_name = PathBuf::from(file!()) 37 | .file_stem() 38 | .unwrap() 39 | .to_string_lossy() 40 | .to_string(); 41 | 42 | util::spawn_tx_rx(&format!("tests/{test_name}.pcapng"), tx, rx); 43 | 44 | // Read configurations from SubDevice EEPROMs and configure devices. 45 | let group = maindevice 46 | .init_single_group::(|| 0) 47 | .await 48 | .expect("Init"); 49 | 50 | assert_eq!(group.subdevice(&maindevice, 0)?.name(), "EK1914"); 51 | 52 | let _group = group.into_op(&maindevice).await.expect("PRE-OP -> OP"); 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /tests/replay-ek1914-segmented-upload.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/tests/replay-ek1914-segmented-upload.pcapng -------------------------------------------------------------------------------- /tests/replay-ek1914-segmented-upload.rs: -------------------------------------------------------------------------------- 1 | //! Test that segmented CoE uploads work. 2 | //! 3 | //! - EK1914 4 | 5 | mod util; 6 | 7 | use env_logger::Env; 8 | use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, Timeouts, error::Error}; 9 | use std::path::PathBuf; 10 | 11 | const MAX_SUBDEVICES: usize = 16; 12 | const MAX_PDU_DATA: usize = PduStorage::element_size(1100); 13 | const MAX_FRAMES: usize = 128; 14 | const PDI_LEN: usize = 128; 15 | 16 | #[tokio::test] 17 | #[cfg_attr(miri, ignore)] 18 | async fn replay_ek1914_segmented_upload() -> Result<(), Error> { 19 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 20 | 21 | static PDU_STORAGE: PduStorage = PduStorage::new(); 22 | 23 | let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 24 | 25 | let maindevice = MainDevice::new( 26 | pdu_loop, 27 | Timeouts::default(), 28 | MainDeviceConfig { 29 | dc_static_sync_iterations: 100, 30 | ..Default::default() 31 | }, 32 | ); 33 | 34 | let test_name = PathBuf::from(file!()) 35 | .file_stem() 36 | .unwrap() 37 | .to_string_lossy() 38 | .to_string(); 39 | 40 | util::spawn_tx_rx(&format!("tests/{test_name}.pcapng"), tx, rx); 41 | 42 | // Read configurations from SubDevice EEPROMs and configure devices. 43 | let group = maindevice 44 | .init_single_group::(|| 0) 45 | .await 46 | .expect("Init"); 47 | 48 | assert_eq!(group.subdevice(&maindevice, 0)?.name(), "EK1914"); 49 | 50 | let first = group 51 | .subdevice(&maindevice, 0) 52 | .expect("EK1914 must be first"); 53 | 54 | let name_coe = first 55 | .sdo_read::>(0x1008, 0) 56 | .await 57 | .expect("Failed to read name"); 58 | 59 | log::info!("Device name: {:?}", name_coe); 60 | 61 | assert_eq!(&name_coe, "EK1914"); 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /tests/replay-issue-255.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercrab-rs/ethercrab/4e337b7b5114111c21331b02504e0a02106e1b6e/tests/replay-issue-255.pcapng -------------------------------------------------------------------------------- /tests/replay-issue-255.rs: -------------------------------------------------------------------------------- 1 | //! Capture network traffic for `issue-255.rs` 2 | //! 3 | //! Required hardware: 4 | //! 5 | //! - EK1100 6 | //! - EL2828 7 | 8 | mod util; 9 | 10 | use env_logger::Env; 11 | use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, Timeouts, error::Error}; 12 | use std::{hint::black_box, path::PathBuf, time::Duration}; 13 | use tokio::time::MissedTickBehavior; 14 | 15 | const MAX_SUBDEVICES: usize = 16; 16 | const MAX_PDU_DATA: usize = PduStorage::element_size(1100); 17 | const MAX_FRAMES: usize = 128; 18 | const MAX_PDI: usize = 128; 19 | 20 | #[tokio::test] 21 | #[cfg_attr(miri, ignore)] 22 | async fn replay_issue_255() -> Result<(), Error> { 23 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 24 | 25 | static PDU_STORAGE: PduStorage = PduStorage::new(); 26 | 27 | let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); 28 | 29 | let maindevice = MainDevice::new( 30 | pdu_loop, 31 | Timeouts::default(), 32 | MainDeviceConfig { 33 | dc_static_sync_iterations: 0, 34 | ..Default::default() 35 | }, 36 | ); 37 | 38 | let test_name = PathBuf::from(file!()) 39 | .file_stem() 40 | .unwrap() 41 | .to_string_lossy() 42 | .to_string(); 43 | 44 | util::spawn_tx_rx(&format!("tests/{test_name}.pcapng"), tx, rx); 45 | 46 | // Read configurations from SubDevice EEPROMs and configure devices. 47 | let group = maindevice 48 | .init_single_group::(time_zero) 49 | .await 50 | .expect("Init"); 51 | 52 | let group = group.into_op(&maindevice).await.expect("Slow into OP"); 53 | 54 | let mut cycle_time = tokio::time::interval(Duration::from_millis(2)); 55 | cycle_time.set_missed_tick_behavior(MissedTickBehavior::Skip); 56 | 57 | // Animate slow pattern for 8 ticks 58 | for _ in 0..64 { 59 | group.tx_rx(&maindevice).await.expect("TX/RX"); 60 | 61 | let el2889 = group.subdevice(&maindevice, 1).unwrap(); 62 | 63 | let mut o = el2889.outputs_raw_mut(); 64 | 65 | do_stuff(black_box(&mut o)); 66 | 67 | cycle_time.tick().await; 68 | } 69 | 70 | Ok(()) 71 | } 72 | 73 | fn time_zero() -> u64 { 74 | 0 75 | } 76 | 77 | fn do_stuff(slice: &mut [u8]) { 78 | slice[0] += 1; 79 | slice[0] -= 1; 80 | } 81 | --------------------------------------------------------------------------------