├── .github └── workflows │ ├── ci.yml │ ├── publish-crate-dry-run.yml │ ├── publish-crate.yml │ ├── publish-dry-run.yml │ └── publish.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── clippy.toml ├── edge-captive ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── fmt.rs │ ├── io.rs │ └── lib.rs ├── edge-dhcp ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── client.rs │ ├── fmt.rs │ ├── io.rs │ ├── io │ ├── client.rs │ └── server.rs │ ├── lib.rs │ └── server.rs ├── edge-http ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── fmt.rs │ ├── io.rs │ ├── io │ ├── client.rs │ └── server.rs │ └── lib.rs ├── edge-mdns ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── buf.rs │ ├── fmt.rs │ ├── host.rs │ ├── io.rs │ └── lib.rs ├── edge-mqtt ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── io.rs │ └── lib.rs ├── edge-nal-embassy ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── dns.rs │ ├── fmt.rs │ ├── lib.rs │ ├── tcp.rs │ └── udp.rs ├── edge-nal-std ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── edge-nal ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── multicast.rs │ ├── raw.rs │ ├── readable.rs │ ├── stack.rs │ ├── stack │ ├── dns.rs │ ├── raw.rs │ ├── tcp.rs │ └── udp.rs │ ├── tcp.rs │ ├── timeout.rs │ └── udp.rs ├── edge-raw ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── bytes.rs │ ├── fmt.rs │ ├── io.rs │ ├── ip.rs │ ├── lib.rs │ └── udp.rs ├── edge-ws ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── fmt.rs │ ├── io.rs │ └── lib.rs ├── examples ├── captive_portal.rs ├── dhcp_client.rs ├── dhcp_server.rs ├── http_client.rs ├── http_server.rs ├── mdns_responder.rs ├── mdns_service_responder.rs ├── mqtt_client.rs ├── nal_std.rs ├── ws_client.rs └── ws_server.rs └── src └── lib.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | - cron: '50 4 * * *' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | compile: 14 | name: Compile 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | rust_toolchain: 20 | - nightly 21 | - 1.83 # MSRV 22 | 23 | steps: 24 | - name: Setup | Checkout 25 | uses: actions/checkout@v2 26 | - name: Setup | Rust 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: ${{ matrix.rust_toolchain }} 30 | components: rustfmt, clippy 31 | - name: Setup | Std 32 | run: rustup component add rust-src --toolchain ${{ matrix.rust_toolchain }}-x86_64-unknown-linux-gnu 33 | - name: Setup | Set default toolchain 34 | run: rustup default ${{ matrix.rust_toolchain }} 35 | - name: Build | Fmt Check 36 | run: cargo fmt -- --check 37 | - name: Build | Clippy 38 | run: cargo clippy --features std,embedded-svc,edge-nal-embassy/all --examples --no-deps -- -Dwarnings 39 | - name: Build | Clippy - defmt 40 | run: cargo clippy --features std,embedded-svc,edge-nal-embassy/all,defmt --no-deps -- -Dwarnings 41 | - name: Build | Clippy - log 42 | run: cargo clippy --features std,embedded-svc,edge-nal-embassy/all,log --examples --no-deps -- -Dwarnings 43 | - name: Build | Default 44 | run: cargo build --features log 45 | - name: Build | Non-default 46 | run: cargo build --no-default-features 47 | - name: Build | Embassy 48 | run: cargo build --no-default-features --features embassy,defmt 49 | - name: Build | Examples 50 | run: cargo build --examples --features log 51 | - name: Build | Examples - defmt 52 | run: export DEFMT_LOG=trace; cargo check --examples --features std,defmt 53 | -------------------------------------------------------------------------------- /.github/workflows/publish-crate-dry-run.yml: -------------------------------------------------------------------------------- 1 | name: PublishCrateDryRun 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | crate: 7 | required: true 8 | type: choice 9 | description: Crate to publish 10 | options: 11 | - edge-nal 12 | - edge-nal-std 13 | - edge-nal-embassy 14 | - edge-captive 15 | - edge-dhcp 16 | - edge-http 17 | - edge-mdns 18 | - edge-mqtt 19 | - edge-raw 20 | - edge-ws 21 | 22 | env: 23 | rust_toolchain: stable 24 | 25 | jobs: 26 | publishdryrun: 27 | name: Publish Dry Run 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Setup | Checkout 31 | uses: actions/checkout@v2 32 | - name: Setup | Rust 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: ${{ env.rust_toolchain }} 36 | - name: Setup | Std 37 | run: rustup component add rust-src --toolchain ${{ env.rust_toolchain }}-x86_64-unknown-linux-gnu 38 | - name: Setup | Set default toolchain 39 | run: rustup default ${{ env.rust_toolchain }} 40 | - name: Build | Publish Dry Run 41 | run: cd "${{ github.event.inputs.crate }}"; cargo publish --dry-run 42 | -------------------------------------------------------------------------------- /.github/workflows/publish-crate.yml: -------------------------------------------------------------------------------- 1 | name: PublishCrate 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | crate: 7 | required: true 8 | type: choice 9 | description: Crate to publish 10 | options: 11 | - edge-nal 12 | - edge-nal-std 13 | - edge-nal-embassy 14 | - edge-captive 15 | - edge-dhcp 16 | - edge-http 17 | - edge-mdns 18 | - edge-mqtt 19 | - edge-raw 20 | - edge-ws 21 | 22 | env: 23 | rust_toolchain: stable 24 | 25 | jobs: 26 | publish: 27 | name: Publish 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Setup | Checkout 31 | uses: actions/checkout@v2 32 | - name: Setup | Rust 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: ${{ env.rust_toolchain }} 36 | - name: Setup | Std 37 | run: rustup component add rust-src --toolchain ${{ env.rust_toolchain }}-x86_64-unknown-linux-gnu 38 | - name: Setup | Set default toolchain 39 | run: rustup default ${{ env.rust_toolchain }} 40 | - name: Login 41 | run: cargo login ${{ secrets.crates_io_token }} 42 | - name: Build | Publish 43 | run: cd "${{ github.event.inputs.crate }}"; cargo publish 44 | -------------------------------------------------------------------------------- /.github/workflows/publish-dry-run.yml: -------------------------------------------------------------------------------- 1 | name: PublishDryRun 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | env: 7 | rust_toolchain: stable 8 | 9 | jobs: 10 | publishdryrun: 11 | name: Publish Dry Run 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Setup | Checkout 15 | uses: actions/checkout@v2 16 | - name: Setup | Rust 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: ${{ env.rust_toolchain }} 20 | - name: Setup | Std 21 | run: rustup component add rust-src --toolchain ${{ env.rust_toolchain }}-x86_64-unknown-linux-gnu 22 | - name: Setup | Set default toolchain 23 | run: rustup default ${{ env.rust_toolchain }} 24 | - name: Build | Publish Dry Run 25 | run: cargo publish --dry-run 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | env: 7 | rust_toolchain: stable 8 | CRATE_NAME: edge-net 9 | 10 | jobs: 11 | publish: 12 | name: Publish 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Setup | Checkout 16 | uses: actions/checkout@v2 17 | - name: Setup | Rust 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: ${{ env.rust_toolchain }} 21 | - name: Setup | Std 22 | run: rustup component add rust-src --toolchain ${{ env.rust_toolchain }}-x86_64-unknown-linux-gnu 23 | - name: Setup | Set default toolchain 24 | run: rustup default ${{ env.rust_toolchain }} 25 | - name: Login 26 | run: cargo login ${{ secrets.crates_io_token }} 27 | - name: Build | Publish 28 | run: cargo publish 29 | - name: Get the crate version from cargo 30 | run: | 31 | version=$(cargo metadata --format-version=1 --no-deps | jq -r ".packages[] | select(.name == \"${{env.CRATE_NAME}}\") | .version") 32 | echo "crate_version=$version" >> $GITHUB_ENV 33 | echo "${{env.CRATE_NAME}} version: $version" 34 | - name: Tag the new release 35 | uses: rickstaa/action-create-tag@v1 36 | with: 37 | tag: v${{env.crate_version}} 38 | message: "Release v${{env.crate_version}}" 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /.espressif 3 | /.embuild 4 | /target 5 | /Cargo.lock 6 | **/*.rs.bk 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-net" 3 | version = "0.11.0" 4 | authors = ["Ivan Markov "] 5 | edition = "2021" 6 | categories = ["embedded", "hardware-support", "network-programming", "asynchronous"] 7 | keywords = ["embedded", "network"] 8 | description = "no_std and no-alloc async implementations of various network protocols." 9 | repository = "https://github.com/ivmarkov/edge-net" 10 | license = "MIT OR Apache-2.0" 11 | readme = "README.md" 12 | rust-version = "1.83" 13 | 14 | [features] 15 | default = ["io"] 16 | async-io-mini = ["std", "edge-nal-std/async-io-mini"] 17 | std = ["io", "edge-captive/std", "edge-dhcp/std", "edge-http/std", "edge-mdns/std", "edge-raw/std", "edge-mqtt", "edge-ws/std", "edge-nal-std"] 18 | embassy = ["io", "edge-nal-embassy/all"] 19 | io = ["edge-captive/io", "edge-dhcp/io", "edge-http/io", "edge-mdns/io", "edge-raw/io", "edge-ws/io", "edge-nal"] 20 | log = ["edge-captive/log", "edge-dhcp/log", "edge-http/log", "edge-mdns/log", "edge-raw/log", "edge-ws/log", "edge-nal-embassy?/log"] 21 | defmt = ["edge-captive/defmt", "edge-dhcp/defmt", "edge-http/defmt", "edge-mdns/defmt", "edge-raw/defmt", "edge-ws/defmt", "edge-nal-embassy?/defmt"] 22 | embedded-svc = ["edge-http/embedded-svc", "edge-mqtt/embedded-svc", "edge-ws/embedded-svc"] 23 | nightly = [] 24 | 25 | [dependencies] 26 | edge-captive = { workspace = true } 27 | edge-dhcp = { workspace = true } 28 | edge-http = { workspace = true } 29 | edge-mdns = { workspace = true } 30 | edge-mqtt = { workspace = true, optional = true } 31 | edge-nal = { workspace = true, optional = true } 32 | edge-raw = { workspace = true } 33 | edge-ws = { workspace = true } 34 | edge-nal-std = { workspace = true, optional = true } 35 | edge-nal-embassy = { workspace = true, optional = true } 36 | 37 | [dev-dependencies] 38 | log = "0.4" 39 | anyhow = "1" 40 | env_logger = "0.10" 41 | embedded-io-async = "0.6" 42 | embassy-time = { version = "0.4", features = ["std", "generic-queue-64"] } 43 | embassy-sync = "0.7" 44 | embassy-futures = "0.1" 45 | embedded-svc = { version = "0.28", features = ["std"] } 46 | futures-lite = "2" 47 | rand = "0.8" 48 | tokio = "1" # For the `mqtt_client` example 49 | async-compat = "0.2" # For the `mqtt_client` example 50 | 51 | [[example]] 52 | name = "captive_portal" 53 | required-features = ["std"] 54 | 55 | [[example]] 56 | name = "dhcp_client" 57 | required-features = ["std"] 58 | 59 | [[example]] 60 | name = "dhcp_server" 61 | required-features = ["std"] 62 | 63 | [[example]] 64 | name = "http_client" 65 | required-features = ["std"] 66 | 67 | [[example]] 68 | name = "http_server" 69 | required-features = ["std"] 70 | 71 | [[example]] 72 | name = "mdns_responder" 73 | required-features = ["std"] 74 | 75 | [[example]] 76 | name = "mdns_service_responder" 77 | required-features = ["std"] 78 | 79 | [[example]] 80 | name = "ws_client" 81 | required-features = ["std"] 82 | 83 | [[example]] 84 | name = "ws_server" 85 | required-features = ["std"] 86 | 87 | [[example]] 88 | name = "nal_std" 89 | required-features = ["std"] 90 | 91 | [[example]] 92 | name = "mqtt_client" 93 | required-features = ["std", "embedded-svc"] 94 | 95 | [workspace] 96 | members = [ 97 | ".", 98 | "edge-captive", 99 | "edge-dhcp", 100 | "edge-http", 101 | "edge-mdns", 102 | "edge-mqtt", 103 | "edge-nal", 104 | "edge-raw", 105 | "edge-ws", 106 | "edge-nal-std", 107 | "edge-nal-embassy" 108 | ] 109 | 110 | [workspace.dependencies] 111 | embassy-futures = { version = "0.1", default-features = false } 112 | embassy-sync = { version = "0.7", default-features = false } 113 | embassy-time = { version = "0.4", default-features = false } 114 | embedded-io-async = { version = "0.6", default-features = false } 115 | embedded-svc = { version = "0.28", default-features = false } 116 | heapless = { version = "0.8", default-features = false } 117 | domain = { version = "0.10", default-features = false, features = ["heapless"] } 118 | 119 | edge-captive = { version = "0.6.0", path = "edge-captive", default-features = false } 120 | edge-dhcp = { version = "0.6.0", path = "edge-dhcp", default-features = false } 121 | edge-http = { version = "0.6.0", path = "edge-http", default-features = false } 122 | edge-mdns = { version = "0.6.0", path = "edge-mdns", default-features = false } 123 | edge-mqtt = { version = "0.4.0", path = "edge-mqtt", default-features = false } 124 | edge-nal = { version = "0.5.0", path = "edge-nal", default-features = false } 125 | edge-raw = { version = "0.6.0", path = "edge-raw", default-features = false } 126 | edge-ws = { version = "0.5.0", path = "edge-ws", default-features = false } 127 | edge-nal-std = { version = "0.5.0", path = "edge-nal-std", default-features = false } 128 | edge-nal-embassy = { version = "0.6.0", path = "edge-nal-embassy", default-features = false } 129 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # edge-net 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | This crate ships async + `no_std` + no-alloc implementations of various network protocols. 8 | 9 | Suitable for microcontrollers and embedded systems in general. 10 | 11 | ## Supported protocols 12 | 13 | * [HTTP client and server](edge-http) 14 | * [Websocket send/receive](edge-ws) 15 | * [DNS Captive Portal](edge-captive) 16 | * [mDNS responder](edge-mdns) 17 | * [DHCP cient and server](edge-dhcp) 18 | * [Raw IP & UDP packet send/receive](edge-raw) (useful in combination with the DHCP client and server) 19 | * [MQTT client](edge-mqtt) (currently just a slim wrapper around [`rumqttc`](https://github.com/bytebeamio/rumqtt/tree/main/rumqttc), so needs STD) 20 | * [TCP, UDP and raw sockets](edge-nal) 21 | 22 | ## Supported platforms 23 | 24 | * [The Rust Standard library](edge-nal-std) 25 | * [The networking stack of Embassy](edge-nal-embassy) 26 | * Any other platform, as long as you implement (a subset of) [edge-nal](edge-nal) 27 | * The necessary minimum being the `Read` / `Write` traits from [embedded_io_async](https://crates.io/crates/embedded-io-async/0.5.0) - for modeling TCP sockets - and `UdpReceive` / `UdpSend` from [edge-nal](edge-nal) - for modeling UDP sockets 28 | * Most crates ([edge-captive](edge-captive), [edge-dhcp](edge-dhcp), [edge-ws](edge-ws), [edge-raw](edge-raw)) also provide a compute-only subset that does not need [embedded-io-async](https://crates.io/crates/embedded-io-async/0.5.0) or [edge-nal](edge-nal) traits 29 | 30 | **PRs welcome!** 31 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | future-size-threshold = 300 2 | -------------------------------------------------------------------------------- /edge-captive/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.0] - 2025-05-29 9 | * Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): 10 | * `log` - uses the `log` crate for all logging 11 | * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` 12 | 13 | ## [0.5.0] - 2025-01-15 14 | * Updated dependencies for compatibility with `embassy-time-driver` v0.2 15 | 16 | ## [0.4.0] - 2025-01-02 17 | * Reduce logging level (#32) 18 | * Derive for DnsError 19 | * Option to erase the generics from the IO errors 20 | 21 | ## [0.3.0] - 2024-09-10 22 | * Migrated to the `edge-nal` traits 23 | * Updated `domain` to 0.10 24 | * Raised MSRV to 1.77 25 | -------------------------------------------------------------------------------- /edge-captive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-captive" 3 | version = "0.6.0" 4 | edition = "2021" 5 | rust-version = "1.83" 6 | description = "Async + `no_std` + no-alloc implementation of a Captive Portal DNS" 7 | repository = "https://github.com/ivmarkov/edge-net" 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | categories = [ 11 | "embedded", 12 | "no-std::no-alloc", 13 | "asynchronous", 14 | "network-programming", 15 | ] 16 | 17 | [features] 18 | default = ["io"] 19 | std = ["io"] 20 | io = ["edge-nal"] 21 | 22 | [dependencies] 23 | log = { version = "0.4", default-features = false, optional = true } 24 | defmt = { version = "0.3", optional = true } 25 | domain = { workspace = true } 26 | edge-nal = { workspace = true, optional = true } -------------------------------------------------------------------------------- /edge-captive/README.md: -------------------------------------------------------------------------------- 1 | # edge-captive 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | Async + `no_std` + no-alloc implementation of a Captive Portal DNS (i.e. a DNS server that resolves every domain name to a fixed IP address). 8 | 9 | The implementation is based on the splendid [domain](https://github.com/NLnetLabs/domain) library. 10 | 11 | For other protocols, look at the [edge-net](https://github.com/ivmarkov/edge-net) aggregator crate documentation. 12 | 13 | ## Example 14 | 15 | ```rust 16 | use core::net::{IpAddr, Ipv4Addr, SocketAddr}; 17 | use core::time::Duration; 18 | 19 | use edge_captive::io::run; 20 | 21 | use log::*; 22 | 23 | fn main() { 24 | env_logger::init_from_env( 25 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 26 | ); 27 | 28 | let stack = edge_nal_std::Stack::new(); 29 | 30 | let mut tx_buf = [0; 1500]; 31 | let mut rx_buf = [0; 1500]; 32 | 33 | info!("Running Captive Portal DNS on UDP port 8853..."); 34 | 35 | futures_lite::future::block_on(run( 36 | &stack, 37 | // Can't use DEFAULT_SOCKET because it uses DNS port 53 which needs root 38 | SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8853), 39 | &mut tx_buf, 40 | &mut rx_buf, 41 | Ipv4Addr::new(192, 168, 0, 1), 42 | Duration::from_secs(60), 43 | )) 44 | .unwrap(); 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /edge-captive/src/io.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; 3 | use core::time::Duration; 4 | 5 | use edge_nal::{UdpBind, UdpReceive, UdpSend}; 6 | 7 | use super::*; 8 | 9 | pub const DEFAULT_SOCKET: SocketAddr = SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), PORT); 10 | 11 | const PORT: u16 = 53; 12 | 13 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] 14 | pub enum DnsIoError { 15 | DnsError(DnsError), 16 | IoError(E), 17 | } 18 | 19 | pub type DnsIoErrorKind = DnsIoError; 20 | 21 | impl DnsIoError 22 | where 23 | E: edge_nal::io::Error, 24 | { 25 | pub fn erase(&self) -> DnsIoError { 26 | match self { 27 | Self::DnsError(e) => DnsIoError::DnsError(*e), 28 | Self::IoError(e) => DnsIoError::IoError(e.kind()), 29 | } 30 | } 31 | } 32 | 33 | impl From for DnsIoError { 34 | fn from(err: DnsError) -> Self { 35 | Self::DnsError(err) 36 | } 37 | } 38 | 39 | impl fmt::Display for DnsIoError 40 | where 41 | E: fmt::Display, 42 | { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | match self { 45 | Self::DnsError(err) => write!(f, "DNS error: {}", err), 46 | Self::IoError(err) => write!(f, "IO error: {}", err), 47 | } 48 | } 49 | } 50 | 51 | #[cfg(feature = "defmt")] 52 | impl defmt::Format for DnsIoError 53 | where 54 | E: defmt::Format, 55 | { 56 | fn format(&self, f: defmt::Formatter<'_>) { 57 | match self { 58 | Self::DnsError(err) => defmt::write!(f, "DNS error: {}", err), 59 | Self::IoError(err) => defmt::write!(f, "IO error: {}", err), 60 | } 61 | } 62 | } 63 | 64 | #[cfg(feature = "std")] 65 | impl std::error::Error for DnsIoError where E: std::error::Error {} 66 | 67 | pub async fn run( 68 | stack: &S, 69 | local_addr: SocketAddr, 70 | tx_buf: &mut [u8], 71 | rx_buf: &mut [u8], 72 | ip: Ipv4Addr, 73 | ttl: Duration, 74 | ) -> Result<(), DnsIoError> 75 | where 76 | S: UdpBind, 77 | { 78 | let mut udp = stack.bind(local_addr).await.map_err(DnsIoError::IoError)?; 79 | 80 | loop { 81 | debug!("Waiting for data"); 82 | 83 | let (len, remote) = udp.receive(rx_buf).await.map_err(DnsIoError::IoError)?; 84 | 85 | let request = &rx_buf[..len]; 86 | 87 | debug!("Received {} bytes from {}", request.len(), remote); 88 | 89 | let len = match crate::reply(request, &ip.octets(), ttl, tx_buf) { 90 | Ok(len) => len, 91 | Err(err) => match err { 92 | DnsError::InvalidMessage => { 93 | warn!("Got invalid message from {}, skipping", remote); 94 | continue; 95 | } 96 | other => Err(other)?, 97 | }, 98 | }; 99 | 100 | udp.send(remote, &tx_buf[..len]) 101 | .await 102 | .map_err(DnsIoError::IoError)?; 103 | 104 | debug!("Sent {} bytes to {}", len, remote); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /edge-captive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | #![warn(clippy::large_futures)] 3 | #![allow(clippy::uninlined_format_args)] 4 | #![allow(unknown_lints)] 5 | 6 | use core::fmt::Display; 7 | use core::time::Duration; 8 | 9 | use domain::base::wire::Composer; 10 | use domain::dep::octseq::{OctetsBuilder, Truncate}; 11 | 12 | use domain::{ 13 | base::{ 14 | iana::{Class, Opcode, Rcode}, 15 | message::ShortMessage, 16 | message_builder::PushError, 17 | record::Ttl, 18 | wire::ParseError, 19 | Record, Rtype, 20 | }, 21 | dep::octseq::ShortBuf, 22 | rdata::A, 23 | }; 24 | 25 | // This mod MUST go first, so that the others see its macros. 26 | pub(crate) mod fmt; 27 | 28 | #[cfg(feature = "io")] 29 | pub mod io; 30 | 31 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] 32 | pub enum DnsError { 33 | ShortBuf, 34 | InvalidMessage, 35 | } 36 | 37 | impl Display for DnsError { 38 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 39 | match self { 40 | Self::ShortBuf => write!(f, "ShortBuf"), 41 | Self::InvalidMessage => write!(f, "InvalidMessage"), 42 | } 43 | } 44 | } 45 | 46 | #[cfg(feature = "defmt")] 47 | impl defmt::Format for DnsError { 48 | fn format(&self, f: defmt::Formatter<'_>) { 49 | match self { 50 | Self::ShortBuf => defmt::write!(f, "ShortBuf"), 51 | Self::InvalidMessage => defmt::write!(f, "InvalidMessage"), 52 | } 53 | } 54 | } 55 | 56 | #[cfg(feature = "std")] 57 | impl std::error::Error for DnsError {} 58 | 59 | impl From for DnsError { 60 | fn from(_: ShortBuf) -> Self { 61 | Self::ShortBuf 62 | } 63 | } 64 | 65 | impl From for DnsError { 66 | fn from(_: PushError) -> Self { 67 | Self::ShortBuf 68 | } 69 | } 70 | 71 | impl From for DnsError { 72 | fn from(_: ShortMessage) -> Self { 73 | Self::InvalidMessage 74 | } 75 | } 76 | 77 | impl From for DnsError { 78 | fn from(_: ParseError) -> Self { 79 | Self::InvalidMessage 80 | } 81 | } 82 | 83 | pub fn reply( 84 | request: &[u8], 85 | ip: &[u8; 4], 86 | ttl: Duration, 87 | buf: &mut [u8], 88 | ) -> Result { 89 | let buf = Buf(buf, 0); 90 | 91 | let message = domain::base::Message::from_octets(request)?; 92 | debug!( 93 | "Processing message with header: {:?}", 94 | debug2format!(message.header()) 95 | ); 96 | 97 | let mut responseb = domain::base::MessageBuilder::from_target(buf)?; 98 | 99 | let buf = if matches!(message.header().opcode(), Opcode::QUERY) { 100 | debug!("Message is of type Query, processing all questions"); 101 | 102 | let mut answerb = responseb.start_answer(&message, Rcode::NOERROR)?; 103 | 104 | for question in message.question() { 105 | let question = question?; 106 | 107 | if matches!(question.qtype(), Rtype::A) && matches!(question.qclass(), Class::IN) { 108 | let record = Record::new( 109 | question.qname(), 110 | Class::IN, 111 | Ttl::from_duration_lossy(ttl), 112 | A::from_octets(ip[0], ip[1], ip[2], ip[3]), 113 | ); 114 | debug!( 115 | "Answering {:?} with {:?}", 116 | debug2format!(question), 117 | debug2format!(record) 118 | ); 119 | answerb.push(record)?; 120 | } else { 121 | debug!( 122 | "Question {:?} is not of type A, not answering", 123 | debug2format!(question) 124 | ); 125 | } 126 | } 127 | 128 | answerb.finish() 129 | } else { 130 | debug!("Message is not of type Query, replying with NotImp"); 131 | 132 | let headerb = responseb.header_mut(); 133 | 134 | headerb.set_id(message.header().id()); 135 | headerb.set_opcode(message.header().opcode()); 136 | headerb.set_rd(message.header().rd()); 137 | headerb.set_rcode(domain::base::iana::Rcode::NOTIMP); 138 | 139 | responseb.finish() 140 | }; 141 | 142 | Ok(buf.1) 143 | } 144 | 145 | struct Buf<'a>(pub &'a mut [u8], pub usize); 146 | 147 | impl Composer for Buf<'_> {} 148 | 149 | impl OctetsBuilder for Buf<'_> { 150 | type AppendError = ShortBuf; 151 | 152 | fn append_slice(&mut self, slice: &[u8]) -> Result<(), Self::AppendError> { 153 | if self.1 + slice.len() <= self.0.len() { 154 | let end = self.1 + slice.len(); 155 | self.0[self.1..end].copy_from_slice(slice); 156 | self.1 = end; 157 | 158 | Ok(()) 159 | } else { 160 | Err(ShortBuf) 161 | } 162 | } 163 | } 164 | 165 | impl Truncate for Buf<'_> { 166 | fn truncate(&mut self, len: usize) { 167 | self.1 = len; 168 | } 169 | } 170 | 171 | impl AsMut<[u8]> for Buf<'_> { 172 | fn as_mut(&mut self) -> &mut [u8] { 173 | &mut self.0[..self.1] 174 | } 175 | } 176 | 177 | impl AsRef<[u8]> for Buf<'_> { 178 | fn as_ref(&self) -> &[u8] { 179 | &self.0[..self.1] 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /edge-dhcp/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.0] - 2025-05-29 9 | * Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): 10 | * `log` - uses the `log` crate for all logging 11 | * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` 12 | 13 | ## [0.5.0] - 2025-01-15 14 | * Updated dependencies for compatibility with `embassy-time-driver` v0.2 15 | 16 | ## [0.4.0] - 2025-01-02 17 | * Reduce logging level (#32) 18 | * Support for Captive Portal URLs (#31) 19 | * Option to erase the generics from the IO errors 20 | * Make embassy-time optional 21 | 22 | ## [0.3.0] - 2024-09-10 23 | * Migrated the client and the server to the `edge-nal` traits 24 | * Migrated the server to only require `UdpSend` and `UdpReceive`, without the need to manipulate raw IP payloads anymore 25 | * Raised MSRV to 1.77 26 | 27 | -------------------------------------------------------------------------------- /edge-dhcp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-dhcp" 3 | version = "0.6.0" 4 | edition = "2021" 5 | rust-version = "1.83" 6 | description = "Async + `no_std` + no-alloc implementation of the DHCP protocol" 7 | repository = "https://github.com/ivmarkov/edge-net" 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | categories = [ 11 | "embedded", 12 | "no-std::no-alloc", 13 | "asynchronous", 14 | "network-programming", 15 | ] 16 | 17 | [features] 18 | default = ["io"] 19 | std = ["io"] 20 | io = ["embassy-futures", "embassy-time", "edge-nal"] 21 | defmt = ["dep:defmt", "heapless/defmt-03", "embassy-time?/defmt"] 22 | 23 | [dependencies] 24 | heapless = { workspace = true } 25 | log = { version = "0.4", default-features = false, optional = true } 26 | defmt = { version = "0.3", optional = true, features = ["ip_in_core"] } 27 | rand_core = "0.6" 28 | embassy-futures = { workspace = true, optional = true } 29 | embassy-time = { workspace = true, default-features = false, optional = true } 30 | edge-nal = { workspace = true, optional = true } 31 | num_enum = { version = "0.7", default-features = false } 32 | edge-raw = { workspace = true, default-features = false } 33 | -------------------------------------------------------------------------------- /edge-dhcp/README.md: -------------------------------------------------------------------------------- 1 | # edge-dhcp 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | Async + `no_std` + no-alloc implementation of the DHCP protocol. 8 | 9 | For other protocols, look at the [edge-net](https://github.com/ivmarkov/edge-net) aggregator crate documentation. 10 | 11 | ## Examples 12 | 13 | ### DHCP client 14 | 15 | ```rust 16 | //! NOTE: Run this example with `sudo` to be able to bind to the interface, as it uses raw sockets which require root privileges. 17 | 18 | use core::net::{Ipv4Addr, SocketAddrV4}; 19 | 20 | use edge_dhcp::client::Client; 21 | use edge_dhcp::io::{client::Lease, DEFAULT_CLIENT_PORT, DEFAULT_SERVER_PORT}; 22 | use edge_nal::{MacAddr, RawBind}; 23 | use edge_raw::io::RawSocket2Udp; 24 | 25 | use log::info; 26 | 27 | fn main() { 28 | env_logger::init_from_env( 29 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 30 | ); 31 | 32 | futures_lite::future::block_on(run( 33 | 2, // The interface index of the interface (e.g. eno0) to use; run `ip addr` to see it 34 | [0x4c, 0xcc, 0x6a, 0xa2, 0x23, 0xf5], // Your MAC addr here; run `ip addr` to see it 35 | )) 36 | .unwrap(); 37 | } 38 | 39 | async fn run(if_index: u32, if_mac: MacAddr) -> Result<(), anyhow::Error> { 40 | let mut client = Client::new(rand::thread_rng(), if_mac); 41 | 42 | let stack = edge_nal_std::Interface::new(if_index); 43 | let mut buf = [0; 1500]; 44 | 45 | loop { 46 | let mut socket: RawSocket2Udp<_> = RawSocket2Udp::new( 47 | stack.bind().await?, 48 | Some(SocketAddrV4::new( 49 | Ipv4Addr::UNSPECIFIED, 50 | DEFAULT_CLIENT_PORT, 51 | )), 52 | Some(SocketAddrV4::new( 53 | Ipv4Addr::UNSPECIFIED, 54 | DEFAULT_SERVER_PORT, 55 | )), 56 | [255; 6], // Broadcast 57 | ); 58 | 59 | let (mut lease, options) = Lease::new(&mut client, &mut socket, &mut buf).await?; 60 | 61 | info!("Got lease {lease:?} with options {options:?}"); 62 | 63 | info!("Entering an endless loop to keep the lease..."); 64 | 65 | lease.keep(&mut client, &mut socket, &mut buf).await?; 66 | } 67 | } 68 | ``` 69 | 70 | ### DHCP server 71 | 72 | ```rust 73 | //! NOTE: Run this example with `sudo` to be able to bind to the interface, as it uses raw sockets which require root privileges. 74 | 75 | use core::net::{Ipv4Addr, SocketAddrV4}; 76 | 77 | use edge_dhcp::io::{self, DEFAULT_CLIENT_PORT, DEFAULT_SERVER_PORT}; 78 | use edge_dhcp::server::{Server, ServerOptions}; 79 | use edge_nal::RawBind; 80 | use edge_raw::io::RawSocket2Udp; 81 | 82 | fn main() { 83 | env_logger::init_from_env( 84 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 85 | ); 86 | 87 | futures_lite::future::block_on(run( 88 | 0, // The interface index of the interface (e.g. eno0) to use; run `ip addr` to see it 89 | )) 90 | .unwrap(); 91 | } 92 | 93 | async fn run(if_index: u32) -> Result<(), anyhow::Error> { 94 | let stack = edge_nal_std::Interface::new(if_index); 95 | 96 | let mut buf = [0; 1500]; 97 | 98 | let ip = Ipv4Addr::new(192, 168, 0, 1); 99 | 100 | let mut socket: RawSocket2Udp<_> = RawSocket2Udp::new( 101 | stack.bind().await?, 102 | Some(SocketAddrV4::new( 103 | Ipv4Addr::UNSPECIFIED, 104 | DEFAULT_SERVER_PORT, 105 | )), 106 | Some(SocketAddrV4::new( 107 | Ipv4Addr::UNSPECIFIED, 108 | DEFAULT_CLIENT_PORT, 109 | )), 110 | [0; 6], 111 | ); 112 | 113 | let mut gw_buf = [Ipv4Addr::UNSPECIFIED]; 114 | 115 | io::server::run( 116 | &mut Server::<_, 64>::new_with_et(ip), // Will give IP addresses in the range 192.168.0.50 - 192.168.0.200, subnet 255.255.255.0 117 | &ServerOptions::new(ip, Some(&mut gw_buf)), 118 | &mut socket, 119 | &mut buf, 120 | ) 121 | .await?; 122 | 123 | Ok(()) 124 | } 125 | ``` 126 | -------------------------------------------------------------------------------- /edge-dhcp/src/client.rs: -------------------------------------------------------------------------------- 1 | use rand_core::RngCore; 2 | 3 | use super::*; 4 | 5 | /// A simple DHCP client. 6 | /// The client is unaware of the IP/UDP transport layer and operates purely in terms of packets 7 | /// represented as Rust slices. 8 | /// 9 | /// As such, the client can generate all BOOTP requests and parse BOOTP replies. 10 | pub struct Client { 11 | pub rng: T, 12 | pub mac: [u8; 6], 13 | } 14 | 15 | impl Client 16 | where 17 | T: RngCore, 18 | { 19 | pub const fn new(rng: T, mac: [u8; 6]) -> Self { 20 | Self { rng, mac } 21 | } 22 | 23 | pub fn discover<'o>( 24 | &mut self, 25 | opt_buf: &'o mut [DhcpOption<'o>], 26 | secs: u16, 27 | ip: Option, 28 | ) -> (Packet<'o>, u32) { 29 | self.bootp_request(secs, None, true, Options::discover(ip, opt_buf)) 30 | } 31 | 32 | pub fn request<'o>( 33 | &mut self, 34 | opt_buf: &'o mut [DhcpOption<'o>], 35 | secs: u16, 36 | ip: Ipv4Addr, 37 | broadcast: bool, 38 | ) -> (Packet<'o>, u32) { 39 | self.bootp_request(secs, None, broadcast, Options::request(ip, opt_buf)) 40 | } 41 | 42 | pub fn release<'o>( 43 | &mut self, 44 | opt_buf: &'o mut [DhcpOption<'o>], 45 | secs: u16, 46 | ip: Ipv4Addr, 47 | ) -> Packet<'o> { 48 | self.bootp_request(secs, Some(ip), false, Options::release(opt_buf)) 49 | .0 50 | } 51 | 52 | pub fn decline<'o>( 53 | &mut self, 54 | opt_buf: &'o mut [DhcpOption<'o>], 55 | secs: u16, 56 | ip: Ipv4Addr, 57 | ) -> Packet<'o> { 58 | self.bootp_request(secs, Some(ip), false, Options::decline(opt_buf)) 59 | .0 60 | } 61 | 62 | pub fn is_offer(&self, reply: &Packet<'_>, xid: u32) -> bool { 63 | self.is_bootp_reply_for_us(reply, xid, Some(&[MessageType::Offer])) 64 | } 65 | 66 | pub fn is_ack(&self, reply: &Packet<'_>, xid: u32) -> bool { 67 | self.is_bootp_reply_for_us(reply, xid, Some(&[MessageType::Ack])) 68 | } 69 | 70 | pub fn is_nak(&self, reply: &Packet<'_>, xid: u32) -> bool { 71 | self.is_bootp_reply_for_us(reply, xid, Some(&[MessageType::Nak])) 72 | } 73 | 74 | #[allow(clippy::too_many_arguments)] 75 | pub fn bootp_request<'o>( 76 | &mut self, 77 | secs: u16, 78 | ip: Option, 79 | broadcast: bool, 80 | options: Options<'o>, 81 | ) -> (Packet<'o>, u32) { 82 | let xid = self.rng.next_u32(); 83 | 84 | ( 85 | Packet::new_request(self.mac, xid, secs, ip, broadcast, options), 86 | xid, 87 | ) 88 | } 89 | 90 | pub fn is_bootp_reply_for_us( 91 | &self, 92 | reply: &Packet<'_>, 93 | xid: u32, 94 | expected_message_types: Option<&[MessageType]>, 95 | ) -> bool { 96 | if reply.reply && reply.is_for_us(&self.mac, xid) { 97 | if let Some(expected_message_types) = expected_message_types { 98 | let mt = reply.options.iter().find_map(|option| { 99 | if let DhcpOption::MessageType(mt) = option { 100 | Some(mt) 101 | } else { 102 | None 103 | } 104 | }); 105 | 106 | expected_message_types.iter().any(|emt| mt == Some(*emt)) 107 | } else { 108 | true 109 | } 110 | } else { 111 | false 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /edge-dhcp/src/io.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::{self, Debug}; 2 | use core::net::{SocketAddr, SocketAddrV4}; 3 | 4 | use crate as dhcp; 5 | 6 | pub mod client; 7 | pub mod server; 8 | 9 | pub const DEFAULT_SERVER_PORT: u16 = 67; 10 | pub const DEFAULT_CLIENT_PORT: u16 = 68; 11 | 12 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 13 | pub enum Error { 14 | Io(E), 15 | Format(dhcp::Error), 16 | } 17 | 18 | pub type ErrorKind = Error; 19 | 20 | impl Error 21 | where 22 | E: edge_nal::io::Error, 23 | { 24 | pub fn erase(&self) -> Error { 25 | match self { 26 | Self::Io(e) => Error::Io(e.kind()), 27 | Self::Format(e) => Error::Format(*e), 28 | } 29 | } 30 | } 31 | 32 | impl From for Error { 33 | fn from(value: dhcp::Error) -> Self { 34 | Self::Format(value) 35 | } 36 | } 37 | 38 | impl fmt::Display for Error 39 | where 40 | E: fmt::Display, 41 | { 42 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 43 | match self { 44 | Self::Io(err) => write!(f, "IO error: {err}"), 45 | Self::Format(err) => write!(f, "Format error: {err}"), 46 | } 47 | } 48 | } 49 | 50 | #[cfg(feature = "defmt")] 51 | impl defmt::Format for Error 52 | where 53 | E: defmt::Format, 54 | { 55 | fn format(&self, f: defmt::Formatter<'_>) { 56 | match self { 57 | Self::Io(err) => defmt::write!(f, "IO error: {}", err), 58 | Self::Format(err) => defmt::write!(f, "Format error: {}", err), 59 | } 60 | } 61 | } 62 | 63 | #[cfg(feature = "std")] 64 | impl std::error::Error for Error where E: std::error::Error {} 65 | -------------------------------------------------------------------------------- /edge-dhcp/src/io/server.rs: -------------------------------------------------------------------------------- 1 | use core::net::Ipv4Addr; 2 | 3 | use edge_nal::{UdpReceive, UdpSend}; 4 | 5 | use self::dhcp::{Options, Packet}; 6 | 7 | pub use super::*; 8 | 9 | /// Runs the provided DHCP server asynchronously using the supplied UDP socket and server options. 10 | /// 11 | /// All incoming BOOTP requests are processed by updating the DHCP server's internal simple database of leases, 12 | /// and by issuing replies. 13 | /// 14 | /// Dropping this future is safe in that it won't remove the internal leases' database, 15 | /// so users are free to drop the future in case they would like to take a snapshot of the leases or inspect them otherwise. 16 | /// 17 | /// Note that the UDP socket that the server takes need to be capable of sending and receiving broadcast UDP packets. 18 | /// 19 | /// Furthermore, some DHCP clients do send DHCP OFFER packets without the broadcast flag in the DHCP payload being set to true. 20 | /// To support these clients, the socket needs to also be capable of sending packets with a broadcast IP destination address 21 | /// - yet - with the destination *MAC* address in the Ethernet frame set to the MAC address of the DHCP client. 22 | /// 23 | /// This is currently only possible with STD's BSD raw sockets' implementation. Unfortunately, `smoltcp` and thus `embassy-net` 24 | /// do not have an equivalent (yet). 25 | pub async fn run( 26 | server: &mut dhcp::server::Server, 27 | server_options: &dhcp::server::ServerOptions<'_>, 28 | socket: &mut T, 29 | buf: &mut [u8], 30 | ) -> Result<(), Error> 31 | where 32 | T: UdpReceive + UdpSend, 33 | F: FnMut() -> u64, 34 | { 35 | info!( 36 | "Running DHCP server for addresses {}-{} with configuration {:?}", 37 | server.range_start, server.range_end, server_options 38 | ); 39 | 40 | loop { 41 | let (len, remote) = socket.receive(buf).await.map_err(Error::Io)?; 42 | let packet = &buf[..len]; 43 | 44 | let request = match Packet::decode(packet) { 45 | Ok(request) => request, 46 | Err(err) => { 47 | warn!("Decoding packet returned error: {:?}", err); 48 | continue; 49 | } 50 | }; 51 | 52 | let mut opt_buf = Options::buf(); 53 | 54 | if let Some(reply) = server.handle_request(&mut opt_buf, server_options, &request) { 55 | let remote = if let SocketAddr::V4(socket) = remote { 56 | if request.broadcast || *socket.ip() == Ipv4Addr::UNSPECIFIED { 57 | SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::BROADCAST, socket.port())) 58 | } else { 59 | remote 60 | } 61 | } else { 62 | remote 63 | }; 64 | 65 | socket 66 | .send(remote, reply.encode(buf)?) 67 | .await 68 | .map_err(Error::Io)?; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /edge-http/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.0] - 2025-05-29 9 | * Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): 10 | * `log` - uses the `log` crate for all logging 11 | * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` 12 | 13 | ## [0.5.1] - 2025-02-02 14 | * Fix multiple websocket-related connection issues (#58) 15 | 16 | ## [0.5.0] - 2025-01-15 17 | * Updated dependencies for compatibility with `embassy-time-driver` v0.2 18 | 19 | ## [0.4.0] - 2025-01-02 20 | * Connection type support (#33) 21 | * Proper TCP socket shutdown; Generic TCP timeout utils; built-in HTTP server timeouts; update docu (#34) 22 | * Always send a SP after status code, even if no reason is given (#36) 23 | * edge-http: make fields in {Req,Resp}Headers non-optional (#37) 24 | * Combine Handler and TaskHandler; eradicate all explicit timeouts, now that both TcpAccept and Handler are implemented for WithTimeout 25 | * Fix memory consumption when using a handler with a timeout 26 | * (edge-http) Server non-static handler (#40) 27 | * Option to erase the generics from the IO errors 28 | * HTTP client: Close method for connections 29 | 30 | ## [0.3.0] - 2024-09-10 31 | * Migrated the client and the server to the `edge-nal` traits 32 | * Fixed a nasty bug where when multiple HTTP requests were carried over a single TCP connection, in certain cases the server was "eating" into the data of the next HTTP request 33 | * #20 - Removed a misleading warning log "Connection(IncompleteHeaders)" 34 | 35 | ## [0.2.1] - 2024-02-01 36 | * Fixed a wrong header name which caused WS client socket upgrade to fail 37 | 38 | ## [0.2.0] - 2024-02-01 39 | * Remove unnecessary lifetimes when implementing the `embedded-svc` traits 40 | * Server: new trait, `TaskHandler` which has an extra `task_id` parameter of type `usize`. This allows the request handling code to take advantage of the fact that - since the number of handlers when running a `Server` instance is fixed - it can store data related to handlers in a simple static array of the same size as the number of handlers that the server is running 41 | * Breaking change: structures `Server` and `ServerBuffers` united, because `Server` was actually stateless. Turbofish syntax for specifying max number of HTTP headers and queue size is no longer necessary 42 | * Breaking change: introduce an optional timeout for HTTP server connections and for the server itself 43 | * Breaking change: remove the `const W: usize` parameter from the `Server` struct, as the accept queue is no longer necessary (using an async mutex now internally) 44 | * Fix a bug where the Websockets' `Sec-Key-Accept` header was computed incorrectly 45 | * Implement `Sec-Key-Accept` header validation in the HTTP client 46 | * Breaking change: `UpgradeError::SecKeyTooLong` removed as it is no longer used 47 | -------------------------------------------------------------------------------- /edge-http/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-http" 3 | version = "0.6.0" 4 | edition = "2021" 5 | rust-version = "1.83" 6 | description = "Async + `no_std` + no-alloc implementation of the HTTP protocol" 7 | repository = "https://github.com/ivmarkov/edge-net" 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | categories = [ 11 | "embedded", 12 | "no-std::no-alloc", 13 | "asynchronous", 14 | "web-programming::http-client", 15 | "web-programming::http-server", 16 | ] 17 | 18 | [features] 19 | default = ["io"] 20 | std = ["io"] 21 | io = ["embedded-io-async", "edge-nal", "embassy-sync", "embassy-futures", "embassy-time"] 22 | defmt = ["dep:defmt", "heapless/defmt-03", "embedded-svc?/defmt"] 23 | 24 | [dependencies] 25 | embedded-io-async = { workspace = true, optional = true } 26 | edge-nal = { workspace = true, optional = true } 27 | embedded-svc = { workspace = true, optional = true, default-features = false } 28 | heapless = { workspace = true } 29 | log = { version = "0.4", default-features = false, optional = true } 30 | defmt = { version = "0.3", optional = true } 31 | embassy-sync = { workspace = true, optional = true } 32 | embassy-futures = { workspace = true, optional = true } 33 | embassy-time = { workspace = true, optional = true } 34 | httparse = { version = "1.7", default-features = false } 35 | base64 = { version = "0.13", default-features = false } 36 | sha1_smol = { version = "1", default-features = false } 37 | -------------------------------------------------------------------------------- /edge-http/README.md: -------------------------------------------------------------------------------- 1 | # edge-http 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | Async + `no_std` + no-alloc implementation of the HTTP protocol. 8 | 9 | The implementation is based on the splendid [httparse](https://github.com/seanmonstar/httparse) library. 10 | 11 | For other protocols, look at the [edge-net](https://github.com/ivmarkov/edge-net) aggregator crate documentation. 12 | 13 | ## Next steps 14 | 15 | Optimize further the memory consumption of the generated futures: 16 | * In particular, the `edge_http::io:::server::Server::run` future - with the defaults of 4 handlers and size for 64 headers in the request currently weights ~ **6 - 9KB** 17 | * Moreover, *this is without the memory buffers that the `edge_http::io:::server::Server` needs to operate* - which - while already optimized - take another 2048 * 4 ~ **8KB** (but that's of course normal and to be accepted) 18 | 19 | [Relevant material linking various Rust open issues on the subject](https://swatinem.de/blog/future-size) 20 | 21 | ## Examples 22 | 23 | ### HTTP client 24 | 25 | ```rust 26 | use core::net::SocketAddr; 27 | 28 | use embedded_io_async::Read; 29 | 30 | use edge_http::io::{client::Connection, Error}; 31 | use edge_http::Method; 32 | use edge_nal::{AddrType, Dns, TcpConnect}; 33 | 34 | use log::*; 35 | 36 | fn main() { 37 | env_logger::init_from_env( 38 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 39 | ); 40 | 41 | let stack: edge_nal_std::Stack = Default::default(); 42 | 43 | let mut buf = [0_u8; 8192]; 44 | 45 | futures_lite::future::block_on(read(&stack, &mut buf)).unwrap(); 46 | } 47 | 48 | async fn read( 49 | stack: &T, 50 | buf: &mut [u8], 51 | ) -> Result<(), Error<::Error>> 52 | where 53 | ::Error: Into<::Error>, 54 | { 55 | info!("About to open an HTTP connection to httpbin.org port 80"); 56 | 57 | let ip = stack 58 | .get_host_by_name("httpbin.org", AddrType::IPv4) 59 | .await 60 | .map_err(|e| Error::Io(e.into()))?; 61 | 62 | let mut conn: Connection<_> = Connection::new(buf, stack, SocketAddr::new(ip, 80)); 63 | 64 | for uri in ["/ip", "/headers"] { 65 | request(&mut conn, uri).await?; 66 | } 67 | 68 | Ok(()) 69 | } 70 | 71 | async fn request( 72 | conn: &mut Connection<'_, T, N>, 73 | uri: &str, 74 | ) -> Result<(), Error> { 75 | conn.initiate_request(true, Method::Get, uri, &[("Host", "httpbin.org")]) 76 | .await?; 77 | 78 | conn.initiate_response().await?; 79 | 80 | let mut result = Vec::new(); 81 | 82 | let mut buf = [0_u8; 1024]; 83 | 84 | loop { 85 | let len = conn.read(&mut buf).await?; 86 | 87 | if len > 0 { 88 | result.extend_from_slice(&buf[0..len]); 89 | } else { 90 | break; 91 | } 92 | } 93 | 94 | info!( 95 | "Request to httpbin.org, URI \"{}\" returned:\nBody:\n=================\n{}\n=================\n\n\n\n", 96 | uri, 97 | core::str::from_utf8(&result).unwrap()); 98 | 99 | Ok(()) 100 | } 101 | ``` 102 | 103 | ### HTTP server 104 | 105 | ```rust 106 | use core::fmt::{Debug, Display}; 107 | 108 | use edge_http::io::server::{Connection, DefaultServer, Handler}; 109 | use edge_http::io::Error; 110 | use edge_http::Method; 111 | use edge_nal::TcpBind; 112 | 113 | use embedded_io_async::{Read, Write}; 114 | 115 | use log::info; 116 | 117 | fn main() { 118 | env_logger::init_from_env( 119 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 120 | ); 121 | 122 | let mut server = DefaultServer::new(); 123 | 124 | futures_lite::future::block_on(run(&mut server)).unwrap(); 125 | } 126 | 127 | pub async fn run(server: &mut DefaultServer) -> Result<(), anyhow::Error> { 128 | let addr = "0.0.0.0:8881"; 129 | 130 | info!("Running HTTP server on {addr}"); 131 | 132 | let acceptor = edge_nal_std::Stack::new() 133 | .bind(addr.parse().unwrap()) 134 | .await?; 135 | 136 | server.run(None, acceptor, HttpHandler).await?; 137 | 138 | Ok(()) 139 | } 140 | 141 | struct HttpHandler; 142 | 143 | impl Handler for HttpHandler { 144 | type Error 145 | = Error 146 | where 147 | E: Debug; 148 | 149 | async fn handle( 150 | &self, 151 | _task_id: impl Display + Copy, 152 | conn: &mut Connection<'_, T, N>, 153 | ) -> Result<(), Self::Error> 154 | where 155 | T: Read + Write, 156 | { 157 | let headers = conn.headers()?; 158 | 159 | if headers.method != Method::Get { 160 | conn.initiate_response(405, Some("Method Not Allowed"), &[]) 161 | .await?; 162 | } else if headers.path != "/" { 163 | conn.initiate_response(404, Some("Not Found"), &[]).await?; 164 | } else { 165 | conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/plain")]) 166 | .await?; 167 | 168 | conn.write_all(b"Hello world!").await?; 169 | } 170 | 171 | Ok(()) 172 | } 173 | } 174 | ``` 175 | 176 | 177 | ### More examples 178 | 179 | #### HTTPS server 180 | 181 | HTTPS server with embassy and embedded-tls [here](https://github.com/esp-rs/esp-mbedtls/blob/main/examples/edge_server.rs) 182 | 183 | -------------------------------------------------------------------------------- /edge-mdns/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.0] - 2025-05-29 9 | * Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): 10 | * `log` - uses the `log` crate for all logging 11 | * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` 12 | 13 | ## [0.5.0] - 2025-01-15 14 | * Updated dependencies for compatibility with `embassy-time-driver` v0.2 15 | 16 | ## [0.4.0] - 2025-01-02 17 | * Fix for #24 / avahi - always broadcast to any of the enabled muticast addresses, regardless how we were contacted with a query 18 | * Support for one-shot queries 19 | * Option to erase the generics from the IO errors 20 | * Reduce logging level for the mDNS responder (#43) 21 | * Provide an IPv4-only default socket for mdns (#51) 22 | * wait_readable flag; waiting for the socket is now turned off by default due to suspicions that it does not work quite right with embassy-net; Only lock the send buffer once we received a packet 23 | 24 | ## [0.3.0] - 2024-09-10 25 | Almost a complete rewrite: 26 | * New query API (`Mdns::query`) complementing the responder / query answers' processing one (`Mdns::run`) 27 | * `domain` API is now also a public API of `edge-mdns`, re-exported as `edge_mdns::domain` 28 | * IO layer now uses the UDP traits from `edge-net` 29 | * Traits: 30 | * `MdnsHandler` - abstracts the overall processing of an incoming mDNS message 31 | * `HostAnswers` - abstracts the generation of answers to peer queries (implemented by the pre-existing `Host` and `Service` struct types) 32 | * `HostQuestions` - , `PeerAnswers` 33 | Smaller items: 34 | * Raised the `domain` dependency to 0.10 35 | * Optimized memory usage by avoiding on-stack allocation of large `heapless::String`s 36 | * IO layer of `edge-mdns` can now share its buffers with other code (see the `BufferAccess` trait) 37 | * Raised MSRV to 1.77 38 | -------------------------------------------------------------------------------- /edge-mdns/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-mdns" 3 | version = "0.6.0" 4 | edition = "2021" 5 | description = "Async + `no_std` + no-alloc implementation of an mDNS responder" 6 | repository = "https://github.com/ivmarkov/edge-net" 7 | readme = "README.md" 8 | license = "MIT OR Apache-2.0" 9 | categories = [ 10 | "embedded", 11 | "no-std::no-alloc", 12 | "asynchronous", 13 | "network-programming", 14 | ] 15 | 16 | [features] 17 | default = ["io"] 18 | std = ["io"] 19 | io = ["embassy-futures", "embassy-sync", "embassy-time", "edge-nal"] 20 | defmt = ["dep:defmt", "heapless/defmt-03"] 21 | 22 | [dependencies] 23 | log = { version = "0.4", default-features = false, optional = true } 24 | defmt = { version = "0.3", optional = true, features = ["ip_in_core"] } 25 | heapless = { workspace = true } 26 | domain = { workspace = true } 27 | embassy-futures = { workspace = true, optional = true } 28 | embassy-sync = { workspace = true, optional = true } 29 | embassy-time = { workspace = true, optional = true } 30 | edge-nal = { workspace = true, optional = true } 31 | -------------------------------------------------------------------------------- /edge-mdns/README.md: -------------------------------------------------------------------------------- 1 | # edge-mdns 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | Async + `no_std` + no-alloc implementation of an mDNS responder. 8 | 9 | The implementation is based on the splendid [domain](https://github.com/NLnetLabs/domain) library. 10 | 11 | For other protocols, look at the [edge-net](https://github.com/ivmarkov/edge-net) aggregator crate documentation. 12 | 13 | ## Example 14 | 15 | ```rust 16 | use core::net::{Ipv4Addr, Ipv6Addr}; 17 | 18 | use edge_mdns::buf::{BufferAccess, VecBufAccess}; 19 | use edge_mdns::domain::base::Ttl; 20 | use edge_mdns::io::{self, MdnsIoError, DEFAULT_SOCKET}; 21 | use edge_mdns::{host::Host, HostAnswersMdnsHandler}; 22 | use edge_nal::{UdpBind, UdpSplit}; 23 | 24 | use embassy_sync::blocking_mutex::raw::NoopRawMutex; 25 | use embassy_sync::signal::Signal; 26 | 27 | use log::*; 28 | 29 | use rand::{thread_rng, RngCore}; 30 | 31 | // Change this to the IP address of the machine where you'll run this example 32 | const OUR_IP: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); 33 | 34 | const OUR_NAME: &str = "mypc"; 35 | 36 | fn main() { 37 | env_logger::init_from_env( 38 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 39 | ); 40 | 41 | let stack = edge_nal_std::Stack::new(); 42 | 43 | let (recv_buf, send_buf) = ( 44 | VecBufAccess::::new(), 45 | VecBufAccess::::new(), 46 | ); 47 | 48 | futures_lite::future::block_on(run::( 49 | &stack, &recv_buf, &send_buf, OUR_NAME, OUR_IP, 50 | )) 51 | .unwrap(); 52 | } 53 | 54 | async fn run( 55 | stack: &T, 56 | recv_buf: RB, 57 | send_buf: SB, 58 | our_name: &str, 59 | our_ip: Ipv4Addr, 60 | ) -> Result<(), MdnsIoError> 61 | where 62 | T: UdpBind, 63 | RB: BufferAccess<[u8]>, 64 | SB: BufferAccess<[u8]>, 65 | { 66 | info!("About to run an mDNS responder for our PC. It will be addressable using {our_name}.local, so try to `ping {our_name}.local`."); 67 | 68 | let mut socket = io::bind(stack, DEFAULT_SOCKET, Some(Ipv4Addr::UNSPECIFIED), Some(0)).await?; 69 | 70 | let (recv, send) = socket.split(); 71 | 72 | let host = Host { 73 | hostname: our_name, 74 | ipv4: our_ip, 75 | ipv6: Ipv6Addr::UNSPECIFIED, 76 | ttl: Ttl::from_secs(60), 77 | }; 78 | 79 | // A way to notify the mDNS responder that the data in `Host` had changed 80 | // We don't use it in this example, because the data is hard-coded 81 | let signal = Signal::new(); 82 | 83 | let mdns = io::Mdns::::new( 84 | Some(Ipv4Addr::UNSPECIFIED), 85 | Some(0), 86 | recv, 87 | send, 88 | recv_buf, 89 | send_buf, 90 | |buf| thread_rng().fill_bytes(buf), 91 | &signal, 92 | ); 93 | 94 | mdns.run(HostAnswersMdnsHandler::new(&host)).await 95 | } 96 | ``` 97 | -------------------------------------------------------------------------------- /edge-mdns/src/buf.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Deref, DerefMut}; 2 | 3 | use embassy_sync::{ 4 | blocking_mutex::raw::RawMutex, 5 | mutex::{Mutex, MutexGuard}, 6 | }; 7 | 8 | /// A trait for getting access to a `&mut T` buffer, potentially awaiting until a buffer becomes available. 9 | pub trait BufferAccess 10 | where 11 | T: ?Sized, 12 | { 13 | type Buffer<'a>: DerefMut 14 | where 15 | Self: 'a; 16 | 17 | /// Get a reference to a buffer. 18 | /// Might await until a buffer is available, as it might be in use by somebody else. 19 | /// 20 | /// Depending on its internal implementation details, access to a buffer might also be denied 21 | /// immediately, or after a certain amount of time (subject to the concrete implementation of the method). 22 | /// In that case, the method will return `None`. 23 | async fn get(&self) -> Option>; 24 | } 25 | 26 | impl BufferAccess for &B 27 | where 28 | B: BufferAccess, 29 | T: ?Sized, 30 | { 31 | type Buffer<'a> 32 | = B::Buffer<'a> 33 | where 34 | Self: 'a; 35 | 36 | async fn get(&self) -> Option> { 37 | (*self).get().await 38 | } 39 | } 40 | 41 | pub struct VecBufAccess(Mutex>) 42 | where 43 | M: RawMutex; 44 | 45 | impl VecBufAccess 46 | where 47 | M: RawMutex, 48 | { 49 | pub const fn new() -> Self { 50 | Self(Mutex::new(heapless::Vec::new())) 51 | } 52 | } 53 | 54 | pub struct VecBuf<'a, M, const N: usize>(MutexGuard<'a, M, heapless::Vec>) 55 | where 56 | M: RawMutex; 57 | 58 | impl Drop for VecBuf<'_, M, N> 59 | where 60 | M: RawMutex, 61 | { 62 | fn drop(&mut self) { 63 | self.0.clear(); 64 | } 65 | } 66 | 67 | impl Deref for VecBuf<'_, M, N> 68 | where 69 | M: RawMutex, 70 | { 71 | type Target = [u8]; 72 | 73 | fn deref(&self) -> &Self::Target { 74 | &self.0 75 | } 76 | } 77 | 78 | impl DerefMut for VecBuf<'_, M, N> 79 | where 80 | M: RawMutex, 81 | { 82 | fn deref_mut(&mut self) -> &mut Self::Target { 83 | &mut self.0 84 | } 85 | } 86 | 87 | impl BufferAccess<[u8]> for VecBufAccess 88 | where 89 | M: RawMutex, 90 | { 91 | type Buffer<'a> 92 | = VecBuf<'a, M, N> 93 | where 94 | Self: 'a; 95 | 96 | async fn get(&self) -> Option> { 97 | let mut guard = self.0.lock().await; 98 | 99 | unwrap!(guard.resize_default(N)); 100 | 101 | Some(VecBuf(guard)) 102 | } 103 | } 104 | 105 | impl Default for VecBufAccess 106 | where 107 | M: RawMutex, 108 | { 109 | fn default() -> Self { 110 | Self::new() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /edge-mdns/src/host.rs: -------------------------------------------------------------------------------- 1 | use core::net::{Ipv4Addr, Ipv6Addr}; 2 | 3 | use crate::domain::base::{iana::Class, Record, Ttl}; 4 | use crate::domain::rdata::{Aaaa, AllRecordData, Ptr, Srv, A}; 5 | 6 | use crate::{HostAnswer, HostAnswers, MdnsError, NameSlice, RecordDataChain, Txt, DNS_SD_OWNER}; 7 | 8 | /// A simple representation of a host that can be used to generate mDNS answers. 9 | /// 10 | /// This structure implements the `HostAnswers` trait, which allows it to be used 11 | /// as a responder for mDNS queries coming from other network peers. 12 | #[derive(Debug, Clone)] 13 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 14 | pub struct Host<'a> { 15 | /// The name of the host. I.e. a name "foo" will be pingable as "foo.local" 16 | pub hostname: &'a str, 17 | /// The IPv4 address of the host. 18 | /// Leaving it as `Ipv4Addr::UNSPECIFIED` means that the host will not aswer it to A queries. 19 | pub ipv4: Ipv4Addr, 20 | /// The IPv6 address of the host. 21 | /// Leaving it as `Ipv6Addr::UNSPECIFIED` means that the host will not aswer it to AAAA queries. 22 | pub ipv6: Ipv6Addr, 23 | /// The time-to-live of the mDNS answers. 24 | #[cfg_attr(feature = "defmt", defmt(Debug2Format))] 25 | pub ttl: Ttl, 26 | } 27 | 28 | impl Host<'_> { 29 | fn visit_answers(&self, mut f: F) -> Result<(), E> 30 | where 31 | F: FnMut(HostAnswer) -> Result<(), E>, 32 | E: From, 33 | { 34 | let owner = &[self.hostname, "local"]; 35 | 36 | if !self.ipv4.is_unspecified() { 37 | f(Record::new( 38 | NameSlice::new(owner), 39 | Class::IN, 40 | self.ttl, 41 | RecordDataChain::Next(AllRecordData::A(A::new(domain::base::net::Ipv4Addr::from( 42 | self.ipv4.octets(), 43 | )))), 44 | ))?; 45 | } 46 | 47 | if !self.ipv6.is_unspecified() { 48 | f(Record::new( 49 | NameSlice::new(owner), 50 | Class::IN, 51 | self.ttl, 52 | RecordDataChain::Next(AllRecordData::Aaaa(Aaaa::new( 53 | domain::base::net::Ipv6Addr::from(self.ipv6.octets()), 54 | ))), 55 | ))?; 56 | } 57 | 58 | Ok(()) 59 | } 60 | } 61 | 62 | impl HostAnswers for Host<'_> { 63 | fn visit(&self, mut f: F) -> Result<(), E> 64 | where 65 | F: FnMut(HostAnswer) -> Result<(), E>, 66 | E: From, 67 | { 68 | self.visit_answers(&mut f) 69 | } 70 | } 71 | 72 | /// A simple representation of a DNS-SD service that can be used to generate mDNS answers. 73 | /// 74 | /// This structure (indirectly - via the `ServiceAnswers` wraper which also provides the hostname) 75 | /// implements the `HostAnswers` trait, which allows it to be used as a responder for mDNS queries 76 | /// coming from other network peers. 77 | #[derive(Debug, Clone)] 78 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 79 | pub struct Service<'a> { 80 | /// The name of the service. 81 | pub name: &'a str, 82 | /// The priority of the service. 83 | pub priority: u16, 84 | /// The weight of the service. 85 | pub weight: u16, 86 | /// The service type. I.e. "_http" 87 | pub service: &'a str, 88 | /// The protocol of the service. I.e. "_tcp" or "_udp" 89 | pub protocol: &'a str, 90 | /// The TCP/UDP port where the service listens for incoming requests. 91 | pub port: u16, 92 | /// The subtypes of the service, if any. 93 | pub service_subtypes: &'a [&'a str], 94 | /// The key-value pairs that will be included in the TXT record, as per the DNS-SD spec. 95 | pub txt_kvs: &'a [(&'a str, &'a str)], 96 | } 97 | 98 | impl Service<'_> { 99 | fn visit_answers(&self, host: &Host, mut f: F) -> Result<(), E> 100 | where 101 | F: FnMut(HostAnswer) -> Result<(), E>, 102 | E: From, 103 | { 104 | host.visit_answers(&mut f)?; 105 | 106 | let owner = &[self.name, self.service, self.protocol, "local"]; 107 | let stype = &[self.service, self.protocol, "local"]; 108 | let target = &[host.hostname, "local"]; 109 | 110 | f(Record::new( 111 | NameSlice::new(owner), 112 | Class::IN, 113 | host.ttl, 114 | RecordDataChain::Next(AllRecordData::Srv(Srv::new( 115 | self.priority, 116 | self.weight, 117 | self.port, 118 | NameSlice::new(target), 119 | ))), 120 | ))?; 121 | 122 | f(Record::new( 123 | NameSlice::new(owner), 124 | Class::IN, 125 | host.ttl, 126 | RecordDataChain::This(Txt::new(self.txt_kvs)), 127 | ))?; 128 | 129 | f(Record::new( 130 | DNS_SD_OWNER, 131 | Class::IN, 132 | host.ttl, 133 | RecordDataChain::Next(AllRecordData::Ptr(Ptr::new(NameSlice::new(stype)))), 134 | ))?; 135 | 136 | f(Record::new( 137 | NameSlice::new(stype), 138 | Class::IN, 139 | host.ttl, 140 | RecordDataChain::Next(AllRecordData::Ptr(Ptr::new(NameSlice::new(owner)))), 141 | ))?; 142 | 143 | for subtype in self.service_subtypes { 144 | let subtype_owner = &[subtype, self.name, self.service, self.protocol, "local"]; 145 | let subtype = &[subtype, "_sub", self.service, self.protocol, "local"]; 146 | 147 | f(Record::new( 148 | NameSlice::new(subtype_owner), 149 | Class::IN, 150 | host.ttl, 151 | RecordDataChain::Next(AllRecordData::Ptr(Ptr::new(NameSlice::new(owner)))), 152 | ))?; 153 | 154 | f(Record::new( 155 | NameSlice::new(subtype), 156 | Class::IN, 157 | host.ttl, 158 | RecordDataChain::Next(AllRecordData::Ptr(Ptr::new(NameSlice::new(subtype_owner)))), 159 | ))?; 160 | 161 | f(Record::new( 162 | DNS_SD_OWNER, 163 | Class::IN, 164 | host.ttl, 165 | RecordDataChain::Next(AllRecordData::Ptr(Ptr::new(NameSlice::new(subtype)))), 166 | ))?; 167 | } 168 | 169 | Ok(()) 170 | } 171 | } 172 | 173 | /// A wrapper around a `Service` that also provides the Host of the service 174 | /// and thus allows the `HostAnswers` trait contract to be fullfilled for a `Service` instance. 175 | pub struct ServiceAnswers<'a> { 176 | host: &'a Host<'a>, 177 | service: &'a Service<'a>, 178 | } 179 | 180 | impl<'a> ServiceAnswers<'a> { 181 | /// Create a new `ServiceAnswers` instance. 182 | pub const fn new(host: &'a Host<'a>, service: &'a Service<'a>) -> Self { 183 | Self { host, service } 184 | } 185 | } 186 | 187 | impl HostAnswers for ServiceAnswers<'_> { 188 | fn visit(&self, mut f: F) -> Result<(), E> 189 | where 190 | F: FnMut(HostAnswer) -> Result<(), E>, 191 | E: From, 192 | { 193 | self.service.visit_answers(self.host, &mut f) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /edge-mqtt/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.4.0] - 2025-01-02 11 | * Bump the version number as all other crates were bumped to 0.4 12 | 13 | ## [0.3.0] - 2024-09-10 14 | * Raised MSRV to 1.77 15 | * Support for `embedded-svc` 0.28 -------------------------------------------------------------------------------- /edge-mqtt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-mqtt" 3 | version = "0.4.0" 4 | edition = "2021" 5 | rust-version = "1.77" 6 | description = "Implements the embedded-svc MQTT traits on top of the rumqttc crate" 7 | repository = "https://github.com/ivmarkov/edge-net" 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | categories = [ 11 | "embedded", 12 | "asynchronous", 13 | "network-programming", 14 | ] 15 | 16 | [dependencies] 17 | rumqttc = "0.23" 18 | log = { version = "0.4", default-features = false } 19 | embedded-svc = { workspace = true, optional = true, default-features = false, features = ["std"] } 20 | -------------------------------------------------------------------------------- /edge-mqtt/README.md: -------------------------------------------------------------------------------- 1 | # edge-mqtt 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | A wrapper for the [`rumqttc`](https://github.com/bytebeamio/rumqtt) crate that adapts it to the async [MQTT traits](https://github.com/esp-rs/embedded-svc/blob/master/src/mqtt/client.rs) of the `embedded-svc` crate. 8 | 9 | **NOTE**: Needs STD! 10 | 11 | The plan for the future is to retire this crate in favor of [rust-mqtt](https://github.com/obabec/rust-mqtt) once the latter gets MQTT 3.1 compatibility, and implements a more ergonomic API where sending can be done independently from receiving MQTT messages. 12 | 13 | ... or implement a true `no_std` no-alloc alternative - just like all other `edge-*` crates - if `rust-mqtt` does not see further development. 14 | 15 | ## Example 16 | 17 | ```rust 18 | use async_compat::CompatExt; 19 | 20 | use embedded_svc::mqtt::client::asynch::{Client, Connection, Publish, QoS}; 21 | use embedded_svc::mqtt::client::Event; 22 | 23 | use embassy_futures::select::{select, Either}; 24 | use embassy_time::{Duration, Timer}; 25 | 26 | use edge_mqtt::io::{AsyncClient, MqttClient, MqttConnection, MqttOptions}; 27 | 28 | use log::*; 29 | 30 | const MQTT_HOST: &str = "broker.emqx.io"; 31 | const MQTT_PORT: u16 = 1883; 32 | const MQTT_CLIENT_ID: &str = "edge-mqtt-demo"; 33 | const MQTT_TOPIC: &str = "edge-mqtt-demo"; 34 | 35 | fn main() { 36 | env_logger::init_from_env( 37 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 38 | ); 39 | 40 | let (client, conn) = mqtt_create(MQTT_CLIENT_ID, MQTT_HOST, MQTT_PORT).unwrap(); 41 | 42 | futures_lite::future::block_on( 43 | run(client, conn, MQTT_TOPIC).compat(), /* necessary for tokio */ 44 | ) 45 | .unwrap() 46 | } 47 | 48 | async fn run(mut client: M, mut connection: C, topic: &str) -> Result<(), anyhow::Error> 49 | where 50 | M: Client + Publish + 'static, 51 | M::Error: std::error::Error + Send + Sync + 'static, 52 | C: Connection + 'static, 53 | { 54 | info!("About to start the MQTT client"); 55 | 56 | info!("MQTT client started"); 57 | 58 | client.subscribe(topic, QoS::AtMostOnce).await?; 59 | 60 | info!("Subscribed to topic \"{topic}\""); 61 | 62 | let res = select( 63 | async move { 64 | info!("MQTT Listening for messages"); 65 | 66 | while let Ok(event) = connection.next().await { 67 | info!("[Queue] Event: {}", event.payload()); 68 | } 69 | 70 | info!("Connection closed"); 71 | 72 | Ok(()) 73 | }, 74 | async move { 75 | // Just to give a chance of our connection to get even the first published message 76 | Timer::after(Duration::from_millis(500)).await; 77 | 78 | let payload = "Hello from edge-mqtt-demo!"; 79 | 80 | loop { 81 | client 82 | .publish(topic, QoS::AtMostOnce, false, payload.as_bytes()) 83 | .await?; 84 | 85 | info!("Published \"{payload}\" to topic \"{topic}\""); 86 | 87 | let sleep_secs = 2; 88 | 89 | info!("Now sleeping for {sleep_secs}s..."); 90 | Timer::after(Duration::from_secs(sleep_secs)).await; 91 | } 92 | }, 93 | ) 94 | .await; 95 | 96 | match res { 97 | Either::First(res) => res, 98 | Either::Second(res) => res, 99 | } 100 | } 101 | 102 | fn mqtt_create( 103 | client_id: &str, 104 | host: &str, 105 | port: u16, 106 | ) -> Result<(MqttClient, MqttConnection), anyhow::Error> { 107 | let mut mqtt_options = MqttOptions::new(client_id, host, port); 108 | 109 | mqtt_options.set_keep_alive(core::time::Duration::from_secs(10)); 110 | 111 | let (rumqttc_client, rumqttc_eventloop) = AsyncClient::new(mqtt_options, 10); 112 | 113 | let mqtt_client = MqttClient::new(rumqttc_client); 114 | let mqtt_conn = MqttConnection::new(rumqttc_eventloop); 115 | 116 | Ok((mqtt_client, mqtt_conn)) 117 | } 118 | ``` 119 | -------------------------------------------------------------------------------- /edge-mqtt/src/io.rs: -------------------------------------------------------------------------------- 1 | pub use rumqttc::*; 2 | 3 | #[cfg(feature = "embedded-svc")] 4 | pub use embedded_svc_compat::*; 5 | 6 | #[cfg(feature = "embedded-svc")] 7 | mod embedded_svc_compat { 8 | use embedded_svc::mqtt::client::asynch::{ 9 | Client, Connection, Details, ErrorType, Event, EventPayload, MessageId, Publish, QoS, 10 | }; 11 | 12 | use log::trace; 13 | 14 | use rumqttc::{self, AsyncClient, EventLoop, PubAck, SubAck, UnsubAck}; 15 | 16 | pub use rumqttc::{ClientError, ConnectionError, RecvError}; 17 | 18 | pub struct MqttClient(AsyncClient); 19 | 20 | impl MqttClient { 21 | pub const fn new(client: AsyncClient) -> Self { 22 | Self(client) 23 | } 24 | } 25 | 26 | impl ErrorType for MqttClient { 27 | type Error = ClientError; 28 | } 29 | 30 | impl Client for MqttClient { 31 | async fn subscribe(&mut self, topic: &str, qos: QoS) -> Result { 32 | self.0.subscribe(topic, to_qos(qos)).await?; 33 | 34 | Ok(0) 35 | } 36 | 37 | async fn unsubscribe(&mut self, topic: &str) -> Result { 38 | self.0.unsubscribe(topic).await?; 39 | 40 | Ok(0) 41 | } 42 | } 43 | 44 | impl Publish for MqttClient { 45 | async fn publish( 46 | &mut self, 47 | topic: &str, 48 | qos: embedded_svc::mqtt::client::QoS, 49 | retain: bool, 50 | payload: &[u8], 51 | ) -> Result { 52 | self.0.publish(topic, to_qos(qos), retain, payload).await?; 53 | 54 | Ok(0) 55 | } 56 | } 57 | 58 | pub struct MqttEvent(Result); 59 | 60 | impl MqttEvent { 61 | fn payload(&self) -> EventPayload<'_, ConnectionError> { 62 | self.maybe_payload().unwrap() 63 | } 64 | 65 | fn maybe_payload(&self) -> Option> { 66 | match &self.0 { 67 | Ok(event) => match event { 68 | rumqttc::Event::Incoming(incoming) => match incoming { 69 | rumqttc::Packet::Connect(_) => Some(EventPayload::BeforeConnect), 70 | rumqttc::Packet::ConnAck(_) => Some(EventPayload::Connected(true)), 71 | rumqttc::Packet::Disconnect => Some(EventPayload::Disconnected), 72 | rumqttc::Packet::PubAck(PubAck { pkid, .. }) => { 73 | Some(EventPayload::Published(*pkid as _)) 74 | } 75 | rumqttc::Packet::SubAck(SubAck { pkid, .. }) => { 76 | Some(EventPayload::Subscribed(*pkid as _)) 77 | } 78 | rumqttc::Packet::UnsubAck(UnsubAck { pkid, .. }) => { 79 | Some(EventPayload::Unsubscribed(*pkid as _)) 80 | } 81 | rumqttc::Packet::Publish(rumqttc::Publish { 82 | pkid, 83 | topic, 84 | payload, 85 | .. 86 | }) => Some(EventPayload::Received { 87 | id: *pkid as _, 88 | topic: Some(topic.as_str()), 89 | data: payload, 90 | details: Details::Complete, 91 | }), 92 | _ => None, 93 | }, 94 | rumqttc::Event::Outgoing(_) => None, 95 | }, 96 | Err(err) => Some(EventPayload::Error(err)), 97 | } 98 | } 99 | } 100 | 101 | impl ErrorType for MqttEvent { 102 | type Error = ConnectionError; 103 | } 104 | 105 | impl Event for MqttEvent { 106 | fn payload(&self) -> EventPayload<'_, Self::Error> { 107 | MqttEvent::payload(self) 108 | } 109 | } 110 | 111 | pub struct MqttConnection(EventLoop, bool); 112 | 113 | impl MqttConnection { 114 | pub const fn new(event_loop: EventLoop) -> Self { 115 | Self(event_loop, false) 116 | } 117 | } 118 | 119 | impl ErrorType for MqttConnection { 120 | type Error = RecvError; 121 | } 122 | 123 | impl Connection for MqttConnection { 124 | type Event<'a> 125 | = MqttEvent 126 | where 127 | Self: 'a; 128 | 129 | #[allow(clippy::large_futures)] 130 | async fn next(&mut self) -> Result, Self::Error> { 131 | if self.1 { 132 | Err(RecvError) 133 | } else { 134 | loop { 135 | let event = self.0.poll().await; 136 | trace!("Got event: {:?}", event); 137 | 138 | let event = MqttEvent(event); 139 | if let Some(payload) = event.maybe_payload() { 140 | if matches!(payload, EventPayload::Error(ConnectionError::RequestsDone)) { 141 | self.1 = true; 142 | trace!("Done with requests"); 143 | break Err(RecvError); 144 | } else { 145 | break Ok(event); 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | fn to_qos(qos: QoS) -> rumqttc::QoS { 154 | match qos { 155 | QoS::AtMostOnce => rumqttc::QoS::AtMostOnce, 156 | QoS::AtLeastOnce => rumqttc::QoS::AtLeastOnce, 157 | QoS::ExactlyOnce => rumqttc::QoS::ExactlyOnce, 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /edge-mqtt/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::large_futures)] 2 | #![allow(clippy::uninlined_format_args)] 3 | #![allow(unknown_lints)] 4 | 5 | pub mod io; 6 | -------------------------------------------------------------------------------- /edge-nal-embassy/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.0] - 2025-05-29 9 | * Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): 10 | * `log` - uses the `log` crate for all logging 11 | * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` 12 | * Updated to `embassy-net` 0.7 13 | * Re-export all `embassy-net` features as `edge-nal-embassy` features; `all` feature that enables all features of `embassy-net` 14 | 15 | ## [0.5.0] - 2025-01-15 16 | * Updated dependencies for compatibility with `embassy-time-driver` v0.2 17 | 18 | ## [0.4.1] - 2025-01-05 19 | * Fix regression: ability to UDP/TCP bind to socket 0.0.0.0 20 | 21 | ## [0.4.0] - 2024-01-02 22 | * Proper TCP socket shutdown; Generic TCP timeout utils; built-in HTTP server timeouts; update docu (#34) 23 | * fix a typo (#44) 24 | * Document the N generic for Udp as done for Tcp (#47) 25 | * Update to embassy-net 0.5 (#50) 26 | 27 | ## [0.3.0] - 2024-09-10 28 | * First release (with version 0.3.0 to align with the other `edge-net` crates) 29 | -------------------------------------------------------------------------------- /edge-nal-embassy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-nal-embassy" 3 | version = "0.6.0" 4 | edition = "2021" 5 | rust-version = "1.83" 6 | description = "An implementation of edge-nal based on `embassy-net`" 7 | repository = "https://github.com/ivmarkov/edge-net" 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | categories = [ 11 | "embedded", 12 | "no-std::no-alloc", 13 | "asynchronous", 14 | "network-programming", 15 | ] 16 | 17 | [features] 18 | default = ["all"] 19 | all = ["proto-ipv4", "proto-ipv6", "medium-ethernet", "medium-ip", "dns", "udp", "tcp", "multicast", "icmp", "dhcpv4", "dhcpv4-hostname"] 20 | defmt = ["dep:defmt", "heapless/defmt-03", "embassy-net/defmt"] 21 | proto-ipv4 = ["embassy-net/proto-ipv4"] 22 | proto-ipv6 = ["embassy-net/proto-ipv6"] 23 | medium-ethernet = ["embassy-net/medium-ethernet"] 24 | medium-ip = ["embassy-net/medium-ip"] 25 | dns = ["embassy-net/dns"] 26 | udp = ["embassy-net/udp"] 27 | tcp = ["embassy-net/tcp"] 28 | icmp = ["embassy-net/icmp"] 29 | multicast = ["embassy-net/multicast"] 30 | dhcpv4 = ["embassy-net/dhcpv4"] 31 | dhcpv4-hostname = ["embassy-net/dhcpv4-hostname"] 32 | 33 | [dependencies] 34 | log = { version = "0.4", default-features = false, optional = true } 35 | defmt = { version = "0.3", optional = true } 36 | embedded-io-async = { workspace = true } 37 | edge-nal = { workspace = true } 38 | heapless = { workspace = true } 39 | embassy-net = "0.7" 40 | embassy-futures = { workspace = true } 41 | -------------------------------------------------------------------------------- /edge-nal-embassy/README.md: -------------------------------------------------------------------------------- 1 | # edge-nal-embassy 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | A bare-metal implementation of `edge-nal` based on the [embassy-net](https://crates.io/crates/embassy-net) crate - the networking stack of the Embassy ecosystem. 8 | 9 | ## Implemented Traits 10 | 11 | ### TCP 12 | 13 | All traits. 14 | 15 | ### UDP 16 | 17 | * All traits except `UdpConnect`. 18 | 19 | ### Raw sockets 20 | 21 | Not implemented yet, as `embassy-net` does not expose raw sockets 22 | -------------------------------------------------------------------------------- /edge-nal-embassy/src/dns.rs: -------------------------------------------------------------------------------- 1 | use core::net::IpAddr; 2 | 3 | use edge_nal::AddrType; 4 | 5 | use embassy_net::{ 6 | dns::{DnsQueryType, Error}, 7 | Stack, 8 | }; 9 | use embedded_io_async::ErrorKind; 10 | 11 | /// A struct that implements the `Dns` trait from `edge-nal` 12 | pub struct Dns<'a> { 13 | stack: Stack<'a>, 14 | } 15 | 16 | impl<'a> Dns<'a> { 17 | /// Create a new `Dns` instance for the provided Embassy networking stack 18 | /// 19 | /// NOTE: If using DHCP, make sure it has reconfigured the stack to ensure the DNS servers are updated 20 | pub fn new(stack: Stack<'a>) -> Self { 21 | Self { stack } 22 | } 23 | } 24 | 25 | impl edge_nal::Dns for Dns<'_> { 26 | type Error = DnsError; 27 | 28 | async fn get_host_by_name( 29 | &self, 30 | host: &str, 31 | addr_type: AddrType, 32 | ) -> Result { 33 | let qtype = match addr_type { 34 | AddrType::IPv6 => DnsQueryType::Aaaa, 35 | _ => DnsQueryType::A, 36 | }; 37 | let addrs = self.stack.dns_query(host, qtype).await?; 38 | if let Some(first) = addrs.first() { 39 | Ok((*first).into()) 40 | } else { 41 | Err(Error::Failed.into()) 42 | } 43 | } 44 | 45 | async fn get_host_by_address( 46 | &self, 47 | _addr: IpAddr, 48 | _result: &mut [u8], 49 | ) -> Result { 50 | todo!() 51 | } 52 | } 53 | 54 | #[derive(PartialEq, Eq, Clone, Copy, Debug)] 55 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 56 | pub struct DnsError(Error); 57 | 58 | impl From for DnsError { 59 | fn from(e: Error) -> Self { 60 | DnsError(e) 61 | } 62 | } 63 | 64 | // TODO 65 | impl embedded_io_async::Error for DnsError { 66 | fn kind(&self) -> ErrorKind { 67 | ErrorKind::Other 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /edge-nal-embassy/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(async_fn_in_trait)] 3 | #![warn(clippy::large_futures)] 4 | #![allow(clippy::uninlined_format_args)] 5 | #![allow(unknown_lints)] 6 | 7 | use core::cell::{Cell, UnsafeCell}; 8 | use core::mem::MaybeUninit; 9 | use core::net::{IpAddr, SocketAddr}; 10 | use core::ptr::NonNull; 11 | 12 | use embassy_net::{IpAddress, IpEndpoint, IpListenEndpoint}; 13 | 14 | #[cfg(feature = "dns")] 15 | pub use dns::*; 16 | #[cfg(feature = "tcp")] 17 | pub use tcp::*; 18 | #[cfg(feature = "udp")] 19 | pub use udp::*; 20 | 21 | // This mod MUST go first, so that the others see its macros. 22 | pub(crate) mod fmt; 23 | 24 | #[cfg(feature = "dns")] 25 | mod dns; 26 | #[cfg(feature = "tcp")] 27 | mod tcp; 28 | #[cfg(feature = "udp")] 29 | mod udp; 30 | 31 | pub(crate) struct Pool { 32 | used: [Cell; N], 33 | data: [UnsafeCell>; N], 34 | } 35 | 36 | impl Pool { 37 | #[allow(clippy::declare_interior_mutable_const)] 38 | const VALUE: Cell = Cell::new(false); 39 | #[allow(clippy::declare_interior_mutable_const)] 40 | const UNINIT: UnsafeCell> = UnsafeCell::new(MaybeUninit::uninit()); 41 | 42 | const fn new() -> Self { 43 | Self { 44 | used: [Self::VALUE; N], 45 | data: [Self::UNINIT; N], 46 | } 47 | } 48 | } 49 | 50 | impl Pool { 51 | fn alloc(&self) -> Option> { 52 | for n in 0..N { 53 | // this can't race because Pool is not Sync. 54 | if !self.used[n].get() { 55 | self.used[n].set(true); 56 | let p = self.data[n].get() as *mut T; 57 | return Some(unsafe { NonNull::new_unchecked(p) }); 58 | } 59 | } 60 | None 61 | } 62 | 63 | /// safety: p must be a pointer obtained from self.alloc that hasn't been freed yet. 64 | unsafe fn free(&self, p: NonNull) { 65 | let origin = self.data.as_ptr() as *mut T; 66 | let n = p.as_ptr().offset_from(origin); 67 | assert!(n >= 0); 68 | assert!((n as usize) < N); 69 | self.used[n as usize].set(false); 70 | } 71 | } 72 | 73 | pub(crate) fn to_net_socket(socket: IpEndpoint) -> SocketAddr { 74 | SocketAddr::new(socket.addr.into(), socket.port) 75 | } 76 | 77 | pub(crate) fn to_emb_socket(socket: SocketAddr) -> Option { 78 | Some(IpEndpoint { 79 | addr: to_emb_addr(socket.ip())?, 80 | port: socket.port(), 81 | }) 82 | } 83 | 84 | pub(crate) fn to_emb_bind_socket(socket: SocketAddr) -> Option { 85 | let addr = if socket.ip().is_unspecified() { 86 | None 87 | } else { 88 | Some(to_emb_addr(socket.ip())?) 89 | }; 90 | 91 | Some(IpListenEndpoint { 92 | addr, 93 | port: socket.port(), 94 | }) 95 | } 96 | 97 | pub(crate) fn to_emb_addr(addr: IpAddr) -> Option { 98 | match addr { 99 | #[cfg(feature = "proto-ipv4")] 100 | IpAddr::V4(addr) => Some(addr.into()), 101 | #[cfg(feature = "proto-ipv6")] 102 | IpAddr::V6(addr) => Some(addr.into()), 103 | #[allow(unreachable_patterns)] 104 | _ => None, 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /edge-nal-std/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.5.0] - 2025-01-15 11 | * Updated dependencies for compatibility with `embassy-time-driver` v0.2 12 | 13 | ## [0.4.0] - 2025-01-02 14 | * Proper TCP socket shutdown; Generic TCP timeout utils; built-in HTTP server timeouts; update docu (#34) 15 | * Fix forgotten ref to async-io 16 | * Clone for the STD stack 17 | * For now assume the raw packet API is only usable on Linux 18 | 19 | ## [0.3.0] - 2024-09-10 20 | * Crate name changed from `edge-std-nal-async` to just `edge-nal-std` 21 | * Support for `embedded-nal-async` removed as the traits in that crate are too limited 22 | * STD support is now targetting the traits of our own "nal" crate - `edge-nal` 23 | * STD support can optionally use the `async-io-mini` crate instead of the default `async-io` 24 | 25 | ## [0.2.0] - 2024-02-01 26 | * Do not compile the `RawSocket` implementation for the ESP IDF, as it is missing the `sockaddr_ll` structure in `libc` 27 | * Retire `StdTcpSocketRef` in favor of simply using `&StdTcpSocket` 28 | * ESP-IDF bugfix: The `TcpAccept` implementation was incorrect, because lwIP - unfortunately - does not support `select`-ing on server-side listening sockets 29 | -------------------------------------------------------------------------------- /edge-nal-std/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-nal-std" 3 | version = "0.5.0" 4 | edition = "2021" 5 | rust-version = "1.83" 6 | description = "A STD implementation of edge-nal based on `async-io`" 7 | repository = "https://github.com/ivmarkov/edge-net" 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | categories = [ 11 | "embedded", 12 | "no-std::no-alloc", 13 | "asynchronous", 14 | "network-programming" 15 | ] 16 | 17 | [dependencies] 18 | log = { version = "0.4", default-features = true } 19 | embedded-io-async = { workspace = true, features = ["std"] } 20 | edge-nal = { workspace = true } 21 | async-io = "2" 22 | async-io-mini = { version = "0.3", git = "https://github.com/ivmarkov/async-io-mini", optional = true } 23 | futures-lite = "2" 24 | libc = "0.2" 25 | heapless = { workspace = true } 26 | -------------------------------------------------------------------------------- /edge-nal-std/README.md: -------------------------------------------------------------------------------- 1 | # edge-nal-std 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | An implementation of the `edge-nal` traits for the Rust Standard Library. 8 | 9 | ## Implemented Traits 10 | 11 | All. 12 | 13 | ## Implementation Details 14 | 15 | The implementation is based on the minimalistic [async-io](https://github.com/smol-rs/async-io) crate from the [smol](https://github.com/smol-rs/smol) async echosystem. 16 | 17 | Works out of the box on a variety of operating systems, including [Espressif's ESP IDF](https://github.com/espressif/esp-idf). 18 | -------------------------------------------------------------------------------- /edge-nal/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.5.0] - 2025-01-15 11 | * Updated `embassy-time` to v0.4 12 | 13 | ## [0.4.0] - 2025-01-02 14 | * Proper TCP socket shutdown with a new `TcpShutdown` trait; Generic TCP timeout utils (#34) 15 | * WithTimeout impl for TcpAccept; with_timeout now usable for any fallible future 16 | * Option to erase the generics from the IO errors 17 | 18 | ## [0.3.0] - 2024-09-10 19 | * First release (with version 0.3.0 to align with the other `edge-net` crates) 20 | -------------------------------------------------------------------------------- /edge-nal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-nal" 3 | version = "0.5.0" 4 | edition = "2021" 5 | rust-version = "1.83" 6 | description = "Hosts a bunch of traits which are not yet available in the embedded-nal-async crate" 7 | repository = "https://github.com/ivmarkov/edge-net" 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | categories = [ 11 | "embedded", 12 | "no-std::no-alloc", 13 | "asynchronous", 14 | "network-programming" 15 | ] 16 | 17 | [dependencies] 18 | embedded-io-async = { workspace = true } 19 | embassy-time = { workspace = true } 20 | -------------------------------------------------------------------------------- /edge-nal/README.md: -------------------------------------------------------------------------------- 1 | # edge-nal 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | Hosts a bunch of networking (UDP, TCP and raw ethernet) traits. 8 | 9 | ## Differences with [embedded-nal-async](https://github.com/rust-embedded-community/embedded-nal/tree/master/embedded-nal-async) 10 | 11 | ### TCP 12 | 13 | * Factory traits for the creation of TCP server sockets - `TcpBind` and `TcpAccept`. `embedded-nal-async` only has `TcpConnect` 14 | * Splittable sockets with `TcpSplit` (can be optionally implemented by `TcpConnect` and `TcpAccept`) 15 | * Socket shutdown with `TcpShutdown` 16 | 17 | ### UDP 18 | 19 | * Separate `UdpSend` and `UdpReceive` traits for modeling the sending / receiving functinality of a UDP socket. Necessary for protocols that need UDP socket splitting, like mDNS responder 20 | * Binding to a UDP socket and connecting to a UDP socket modeled with separate traits - `UdpBind` and `UdpConnect`, as not all platforms currently have capabilities to connect to a UDP socket (i.e. the networking stack of Embassy) 21 | * Returning the local address of a UDP socket bind / connect operation is not supported, as not all platforms currently have this capability (i.e. the networking stack of Embassy) 22 | * "Unbound" UDP sockets are currently not supported, as not all platforms have these capabilities (i.e. the networking stack of Embassy). Also, I've yet to find a good use case for these. 23 | * Splittable sockets with `UdpSplit` 24 | * `MulticastV4` and `MulticastV6` traits for joining / leaving IPv4 and IPv6 multicast groups (can be optionally implemented by `UdpConnect` and `UdpBind`) 25 | * `Readable` trait for waiting until a socket becomes readable 26 | 27 | ## Justification 28 | 29 | These traits are necessary to unlock the full functionality of some crates in `edge-net`, which is not possible with the current traits of `embedded-nal-async`. 30 | 31 | Namely: 32 | * [edge-mdns](../edge-mdns) - needs UDP multicast capabilities as well as socket splitting 33 | * [edge-dhcp](../edge-dhcp) - needs raw ethernet socket capabilities or at least sending/receiving UDP packets to/from peers identified by their MAC addresses rather than by their IP addresses 34 | * [edge-http](../edge-http) - (full server only) needs a way to bind to a server-side TCP socket 35 | * [edge-ws](../edge-ws) - Most WebSocket use cases do require a splittable TCP socket (separate read and write halves) 36 | 37 | ## Traits 38 | 39 | ### TCP 40 | 41 | * [TcpSplit](src/stack/tcp.rs) 42 | * A trait that - when implemented on a TCP socket - allows for splitting the send and receive halves of the socket for full-duplex functionality 43 | * [TcpConnect](src/stack/tcp.rs) 44 | * Client-side TCP socket factory similar in spirit to STD's `std::net::TcpListener::connect` method 45 | * [TcpBind](src/stack/tcp.rs) 46 | * Server-side TCP socket factory similar in spirit to STD's `std::net::TcpListener::bind` method and `std::net::TcpListener` struct 47 | * [TcpAccept](src/stack/tcp.rs) 48 | * The acceptor of the server-side TCP socket factory similar in spirit to STD's `std::net::TcpListener::bind` method and `std::net::TcpListener` struct 49 | 50 | ### UDP 51 | 52 | * [UdpReceive](src/udp.rs) 53 | * The receiver half of a UDP socket 54 | * [UdpSend](src/udp.rs) 55 | * The sender half of a UDP socket 56 | * [UdpSplit](src/stack/udp.rs) 57 | * A trait that - when implemented on a UDP socket - allows for splitting the send and receive halves of the socket for full-duplex functionality 58 | * [UdpBind](src/stack/udp.rs) 59 | * Udp socket factory similar in spirit to STD's `std::net::UdpSocket::bind` method 60 | * [UdpConnect](src/stack/udp.rs) 61 | * Udp socket factory similar in spirit to STD's `std::net::UdpSocket::connect` method 62 | * [Multicastv4 and MulticastV6](src/multicast.rs) 63 | * Extra traits for UDP sockets allowing subscription to multicast groups 64 | * [Readable](src/readable.rs) 65 | * Extra trait for UDP, TCP and raw sockets allowing one to wait until the socket becomes readable 66 | 67 | ### Traits for sending/receiving raw ethernet payloads (a.k.a. raw sockets) 68 | 69 | * [RawReceive](src/raw.rs) 70 | * The receiver half of a raw socket 71 | * [RawSend](src/raw.rs) 72 | * The sender half of a raw socket 73 | * [RawSplit](src/stack/raw.rs) 74 | * A trait that - when implemented on a raw socket - allows for splitting the send and receive halves of the socket for full-duplex functionality 75 | * [RawBind](src/stack/raw.rs) 76 | * A raw socket factory 77 | -------------------------------------------------------------------------------- /edge-nal/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(async_fn_in_trait)] 3 | #![allow(clippy::uninlined_format_args)] 4 | #![allow(unknown_lints)] 5 | 6 | pub use multicast::*; 7 | pub use raw::*; 8 | pub use readable::*; 9 | pub use tcp::*; 10 | pub use timeout::*; 11 | pub use udp::*; 12 | 13 | pub use stack::*; 14 | 15 | mod multicast; 16 | mod raw; 17 | mod readable; 18 | mod stack; 19 | mod tcp; 20 | mod timeout; 21 | mod udp; 22 | 23 | pub mod io { 24 | pub use embedded_io_async::*; 25 | } 26 | -------------------------------------------------------------------------------- /edge-nal/src/multicast.rs: -------------------------------------------------------------------------------- 1 | use core::net::{Ipv4Addr, Ipv6Addr}; 2 | 3 | use embedded_io_async::ErrorType; 4 | 5 | pub trait MulticastV4: ErrorType { 6 | async fn join_v4( 7 | &mut self, 8 | multicast_addr: Ipv4Addr, 9 | interface: Ipv4Addr, 10 | ) -> Result<(), Self::Error>; 11 | async fn leave_v4( 12 | &mut self, 13 | multicast_addr: Ipv4Addr, 14 | interface: Ipv4Addr, 15 | ) -> Result<(), Self::Error>; 16 | } 17 | 18 | impl MulticastV4 for &mut T 19 | where 20 | T: MulticastV4, 21 | { 22 | async fn join_v4( 23 | &mut self, 24 | multicast_addr: Ipv4Addr, 25 | interface: Ipv4Addr, 26 | ) -> Result<(), Self::Error> { 27 | (**self).join_v4(multicast_addr, interface).await 28 | } 29 | 30 | async fn leave_v4( 31 | &mut self, 32 | multicast_addr: Ipv4Addr, 33 | interface: Ipv4Addr, 34 | ) -> Result<(), Self::Error> { 35 | (**self).leave_v4(multicast_addr, interface).await 36 | } 37 | } 38 | 39 | pub trait MulticastV6: ErrorType { 40 | async fn join_v6( 41 | &mut self, 42 | multicast_addr: Ipv6Addr, 43 | interface: u32, 44 | ) -> Result<(), Self::Error>; 45 | async fn leave_v6( 46 | &mut self, 47 | multicast_addr: Ipv6Addr, 48 | interface: u32, 49 | ) -> Result<(), Self::Error>; 50 | } 51 | 52 | impl MulticastV6 for &mut T 53 | where 54 | T: MulticastV6, 55 | { 56 | async fn join_v6( 57 | &mut self, 58 | multicast_addr: Ipv6Addr, 59 | interface: u32, 60 | ) -> Result<(), Self::Error> { 61 | (**self).join_v6(multicast_addr, interface).await 62 | } 63 | 64 | async fn leave_v6( 65 | &mut self, 66 | multicast_addr: Ipv6Addr, 67 | interface: u32, 68 | ) -> Result<(), Self::Error> { 69 | (**self).leave_v6(multicast_addr, interface).await 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /edge-nal/src/raw.rs: -------------------------------------------------------------------------------- 1 | //! Traits for modeling raw sockets' sending/receiving functionality on embedded devices 2 | 3 | use embedded_io_async::ErrorType; 4 | 5 | /// A MAC address 6 | pub type MacAddr = [u8; 6]; 7 | 8 | /// This trait is implemented by raw sockets and models their datagram receiving functionality. 9 | pub trait RawReceive: ErrorType { 10 | /// Receive a datagram into the provided buffer. 11 | /// 12 | /// If the received datagram exceeds the buffer's length, it is received regardless, and the 13 | /// remaining bytes are discarded. The full datagram size is still indicated in the result, 14 | /// allowing the recipient to detect that truncation. 15 | /// 16 | /// The remote Mac address is given in the result along with the number 17 | /// of bytes. 18 | async fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, MacAddr), Self::Error>; 19 | } 20 | 21 | /// This trait is implemented by UDP sockets and models their datagram sending functionality. 22 | pub trait RawSend: ErrorType { 23 | /// Send the provided data to a peer. 24 | /// 25 | /// A MAC address is provided to specify the destination. 26 | /// If the destination mac address contains all `0xff`, the packet is broadcasted. 27 | async fn send(&mut self, addr: MacAddr, data: &[u8]) -> Result<(), Self::Error>; 28 | } 29 | 30 | impl RawReceive for &mut T 31 | where 32 | T: RawReceive, 33 | { 34 | async fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, MacAddr), Self::Error> { 35 | (**self).receive(buffer).await 36 | } 37 | } 38 | 39 | impl RawSend for &mut T 40 | where 41 | T: RawSend, 42 | { 43 | async fn send(&mut self, addr: MacAddr, data: &[u8]) -> Result<(), Self::Error> { 44 | (**self).send(addr, data).await 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /edge-nal/src/readable.rs: -------------------------------------------------------------------------------- 1 | use embedded_io_async::ErrorType; 2 | 3 | pub trait Readable: ErrorType { 4 | async fn readable(&mut self) -> Result<(), Self::Error>; 5 | } 6 | 7 | impl Readable for &mut T 8 | where 9 | T: Readable, 10 | { 11 | async fn readable(&mut self) -> Result<(), Self::Error> { 12 | (**self).readable().await 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /edge-nal/src/stack.rs: -------------------------------------------------------------------------------- 1 | pub use dns::*; 2 | pub use raw::*; 3 | pub use tcp::*; 4 | pub use udp::*; 5 | 6 | mod dns; 7 | mod raw; 8 | mod tcp; 9 | mod udp; 10 | -------------------------------------------------------------------------------- /edge-nal/src/stack/dns.rs: -------------------------------------------------------------------------------- 1 | //! A trait for performing DNS lookups on embedded devices 2 | 3 | use core::net::IpAddr; 4 | 5 | /// This is the host address type to be returned by `gethostbyname`. 6 | /// 7 | /// An IPv4 address type always looks for `A` records, while IPv6 address type 8 | /// will look for `AAAA` records 9 | #[derive(Clone, Debug, PartialEq)] 10 | pub enum AddrType { 11 | /// Result is `A` record 12 | IPv4, 13 | /// Result is `AAAA` record 14 | IPv6, 15 | /// Result is either a `A` record, or a `AAAA` record 16 | Either, 17 | } 18 | 19 | /// This trait provides DNS resolution facility for embedded applications. 20 | /// 21 | /// It does not handle every DNS record type, but is meant as an 22 | /// embedded alternative to [`ToSocketAddrs`], and is as such meant to resolve 23 | /// an ip address from a hostname, or a hostname from an ip address. This means 24 | /// that it only deals in host address records `A` (IPv4) and `AAAA` (IPv6). 25 | /// 26 | /// [`ToSocketAddrs`]: 27 | /// https://doc.rust-lang.org/std/net/trait.ToSocketAddrs.html 28 | pub trait Dns { 29 | /// The type returned when we have an error 30 | type Error: embedded_io_async::Error; 31 | 32 | /// Resolve the first ip address of a host, given its hostname and a desired 33 | /// address record type to look for 34 | async fn get_host_by_name( 35 | &self, 36 | host: &str, 37 | addr_type: AddrType, 38 | ) -> Result; 39 | 40 | /// Resolve the hostname of a host, given its ip address. 41 | /// 42 | /// The hostname is stored at the beginning of `result`, the length is returned. 43 | /// 44 | /// If the buffer is too small to hold the domain name, an error should be returned. 45 | /// 46 | /// **Note**: A fully qualified domain name (FQDN), has a maximum length of 47 | /// 255 bytes according to [`rfc1035`]. Therefore, you can pass a 255-byte long 48 | /// buffer to guarantee it'll always be large enough. 49 | /// 50 | /// [`rfc1035`]: https://tools.ietf.org/html/rfc1035 51 | async fn get_host_by_address( 52 | &self, 53 | addr: IpAddr, 54 | result: &mut [u8], 55 | ) -> Result; 56 | } 57 | 58 | impl Dns for &T 59 | where 60 | T: Dns, 61 | { 62 | type Error = T::Error; 63 | 64 | async fn get_host_by_name( 65 | &self, 66 | host: &str, 67 | addr_type: AddrType, 68 | ) -> Result { 69 | T::get_host_by_name(self, host, addr_type).await 70 | } 71 | 72 | async fn get_host_by_address( 73 | &self, 74 | addr: IpAddr, 75 | result: &mut [u8], 76 | ) -> Result { 77 | T::get_host_by_address(self, addr, result).await 78 | } 79 | } 80 | 81 | impl Dns for &mut T 82 | where 83 | T: Dns, 84 | { 85 | type Error = T::Error; 86 | 87 | async fn get_host_by_name( 88 | &self, 89 | host: &str, 90 | addr_type: AddrType, 91 | ) -> Result { 92 | T::get_host_by_name(self, host, addr_type).await 93 | } 94 | 95 | async fn get_host_by_address( 96 | &self, 97 | addr: IpAddr, 98 | result: &mut [u8], 99 | ) -> Result { 100 | T::get_host_by_address(self, addr, result).await 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /edge-nal/src/stack/raw.rs: -------------------------------------------------------------------------------- 1 | //! Factory traits for creating raw sockets on embedded devices 2 | 3 | use embedded_io_async::ErrorType; 4 | 5 | use crate::raw::{RawReceive, RawSend}; 6 | use crate::Readable; 7 | 8 | /// This trait is implemented by raw sockets that can be split into separate `send` and `receive` halves that can operate 9 | /// independently from each other (i.e., a full-duplex connection) 10 | pub trait RawSplit: ErrorType { 11 | type Receive<'a>: RawReceive + Readable 12 | where 13 | Self: 'a; 14 | type Send<'a>: RawSend 15 | where 16 | Self: 'a; 17 | 18 | fn split(&mut self) -> (Self::Receive<'_>, Self::Send<'_>); 19 | } 20 | 21 | impl RawSplit for &mut T 22 | where 23 | T: RawSplit, 24 | { 25 | type Receive<'a> 26 | = T::Receive<'a> 27 | where 28 | Self: 'a; 29 | type Send<'a> 30 | = T::Send<'a> 31 | where 32 | Self: 'a; 33 | 34 | fn split(&mut self) -> (Self::Receive<'_>, Self::Send<'_>) { 35 | (**self).split() 36 | } 37 | } 38 | 39 | /// This trait is implemented by raw socket stacks. The trait allows the underlying driver to 40 | /// construct multiple connections that implement the raw socket traits 41 | pub trait RawBind { 42 | /// Error type returned on socket creation failure 43 | type Error: embedded_io_async::Error; 44 | 45 | /// The socket type returned by the stack 46 | type Socket<'a>: RawReceive 47 | + RawSend 48 | + RawSplit 49 | + Readable 50 | where 51 | Self: 'a; 52 | 53 | /// Create a raw socket 54 | /// 55 | /// On most operating systems, creating a raw socket requires admin privileges. 56 | async fn bind(&self) -> Result, Self::Error>; 57 | } 58 | 59 | impl RawBind for &T 60 | where 61 | T: RawBind, 62 | { 63 | type Error = T::Error; 64 | 65 | type Socket<'a> 66 | = T::Socket<'a> 67 | where 68 | Self: 'a; 69 | 70 | async fn bind(&self) -> Result, Self::Error> { 71 | (*self).bind().await 72 | } 73 | } 74 | 75 | impl RawBind for &mut T 76 | where 77 | T: RawBind, 78 | { 79 | type Error = T::Error; 80 | 81 | type Socket<'a> 82 | = T::Socket<'a> 83 | where 84 | Self: 'a; 85 | 86 | async fn bind(&self) -> Result, Self::Error> { 87 | (**self).bind().await 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /edge-nal/src/stack/tcp.rs: -------------------------------------------------------------------------------- 1 | //! Factory traits for creating TCP sockets on embedded devices 2 | 3 | use core::net::SocketAddr; 4 | 5 | use embedded_io_async::{Error, ErrorType, Read, Write}; 6 | 7 | use crate::{Readable, TcpShutdown}; 8 | 9 | /// This trait is implemented by TCP sockets that can be split into separate `send` and `receive` halves that can operate 10 | /// independently from each other (i.e., a full-duplex connection). 11 | pub trait TcpSplit: ErrorType { 12 | type Read<'a>: Read + Readable 13 | where 14 | Self: 'a; 15 | type Write<'a>: Write 16 | where 17 | Self: 'a; 18 | 19 | fn split(&mut self) -> (Self::Read<'_>, Self::Write<'_>); 20 | } 21 | 22 | impl TcpSplit for &mut T 23 | where 24 | T: TcpSplit, 25 | { 26 | type Read<'a> 27 | = T::Read<'a> 28 | where 29 | Self: 'a; 30 | type Write<'a> 31 | = T::Write<'a> 32 | where 33 | Self: 'a; 34 | 35 | fn split(&mut self) -> (Self::Read<'_>, Self::Write<'_>) { 36 | (**self).split() 37 | } 38 | } 39 | 40 | /// This is a factory trait for connecting to remote TCP peers 41 | pub trait TcpConnect { 42 | /// Error type returned on socket creation failure 43 | type Error: Error; 44 | 45 | /// The socket type returned by the factory 46 | type Socket<'a>: Read 47 | + Write 48 | + Readable 49 | + TcpSplit 50 | + TcpShutdown 51 | where 52 | Self: 'a; 53 | 54 | /// Connect to a remote socket 55 | async fn connect(&self, remote: SocketAddr) -> Result, Self::Error>; 56 | } 57 | 58 | /// This is a factory trait for creating server-side TCP sockets 59 | pub trait TcpBind { 60 | /// Error type returned on bind failure 61 | type Error: Error; 62 | 63 | /// The acceptor type returned by the factory 64 | type Accept<'a>: TcpAccept 65 | where 66 | Self: 'a; 67 | 68 | /// Bind to a local socket listening for incoming connections 69 | /// 70 | /// Depending on the platform, this method might actually be a no-op and just return a new acceptor 71 | /// implementation, that does the actual binding. 72 | /// Platforms that do not maintain internal acceptor queue (Embassy networking stack and `smoltcp`) are such examples. 73 | async fn bind(&self, local: SocketAddr) -> Result, Self::Error>; 74 | } 75 | 76 | /// This is a factory trait for accepting incoming connections on server-side TCP sockets 77 | pub trait TcpAccept { 78 | /// Error type returned on socket creation failure 79 | type Error: Error; 80 | 81 | /// The socket type returned by the factory 82 | type Socket<'a>: Read 83 | + Write 84 | + Readable 85 | + TcpSplit 86 | + TcpShutdown 87 | where 88 | Self: 'a; 89 | 90 | /// Accepts an incoming connection 91 | /// Returns the socket address of the remote peer, as well as the accepted socket. 92 | async fn accept(&self) -> Result<(SocketAddr, Self::Socket<'_>), Self::Error>; 93 | } 94 | 95 | impl TcpConnect for &T 96 | where 97 | T: TcpConnect, 98 | { 99 | type Error = T::Error; 100 | 101 | type Socket<'a> 102 | = T::Socket<'a> 103 | where 104 | Self: 'a; 105 | 106 | async fn connect(&self, remote: SocketAddr) -> Result, Self::Error> { 107 | (*self).connect(remote).await 108 | } 109 | } 110 | 111 | impl TcpConnect for &mut T 112 | where 113 | T: TcpConnect, 114 | { 115 | type Error = T::Error; 116 | 117 | type Socket<'a> 118 | = T::Socket<'a> 119 | where 120 | Self: 'a; 121 | 122 | async fn connect(&self, remote: SocketAddr) -> Result, Self::Error> { 123 | (**self).connect(remote).await 124 | } 125 | } 126 | 127 | impl TcpBind for &T 128 | where 129 | T: TcpBind, 130 | { 131 | type Error = T::Error; 132 | 133 | type Accept<'a> 134 | = T::Accept<'a> 135 | where 136 | Self: 'a; 137 | 138 | async fn bind(&self, local: SocketAddr) -> Result, Self::Error> { 139 | (*self).bind(local).await 140 | } 141 | } 142 | 143 | impl TcpBind for &mut T 144 | where 145 | T: TcpBind, 146 | { 147 | type Error = T::Error; 148 | 149 | type Accept<'a> 150 | = T::Accept<'a> 151 | where 152 | Self: 'a; 153 | 154 | async fn bind(&self, local: SocketAddr) -> Result, Self::Error> { 155 | (**self).bind(local).await 156 | } 157 | } 158 | 159 | impl TcpAccept for &T 160 | where 161 | T: TcpAccept, 162 | { 163 | type Error = T::Error; 164 | 165 | type Socket<'a> 166 | = T::Socket<'a> 167 | where 168 | Self: 'a; 169 | 170 | async fn accept(&self) -> Result<(SocketAddr, Self::Socket<'_>), Self::Error> { 171 | (*self).accept().await 172 | } 173 | } 174 | 175 | impl TcpAccept for &mut T 176 | where 177 | T: TcpAccept, 178 | { 179 | type Error = T::Error; 180 | 181 | type Socket<'a> 182 | = T::Socket<'a> 183 | where 184 | Self: 'a; 185 | 186 | async fn accept(&self) -> Result<(SocketAddr, Self::Socket<'_>), Self::Error> { 187 | (**self).accept().await 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /edge-nal/src/stack/udp.rs: -------------------------------------------------------------------------------- 1 | //! Factory traits for creating UDP sockets on embedded devices 2 | 3 | use core::net::SocketAddr; 4 | 5 | use embedded_io_async::ErrorType; 6 | 7 | use crate::udp::{UdpReceive, UdpSend}; 8 | use crate::{MulticastV4, MulticastV6, Readable}; 9 | 10 | /// This trait is implemented by UDP sockets that can be split into separate `send` and `receive` halves that can operate 11 | /// independently from each other (i.e., a full-duplex connection) 12 | pub trait UdpSplit: ErrorType { 13 | type Receive<'a>: UdpReceive + Readable 14 | where 15 | Self: 'a; 16 | type Send<'a>: UdpSend 17 | where 18 | Self: 'a; 19 | 20 | fn split(&mut self) -> (Self::Receive<'_>, Self::Send<'_>); 21 | } 22 | 23 | impl UdpSplit for &mut T 24 | where 25 | T: UdpSplit, 26 | { 27 | type Receive<'a> 28 | = T::Receive<'a> 29 | where 30 | Self: 'a; 31 | type Send<'a> 32 | = T::Send<'a> 33 | where 34 | Self: 'a; 35 | 36 | fn split(&mut self) -> (Self::Receive<'_>, Self::Send<'_>) { 37 | (**self).split() 38 | } 39 | } 40 | 41 | /// This is a factory trait for creating connected UDP sockets 42 | pub trait UdpConnect { 43 | /// Error type returned on socket creation failure 44 | type Error: embedded_io_async::Error; 45 | 46 | /// The socket type returned by the factory 47 | type Socket<'a>: UdpReceive 48 | + UdpSend 49 | + UdpSplit 50 | + MulticastV4 51 | + MulticastV6 52 | + Readable 53 | where 54 | Self: 'a; 55 | 56 | /// Connect to a remote socket 57 | async fn connect( 58 | &self, 59 | local: SocketAddr, 60 | remote: SocketAddr, 61 | ) -> Result, Self::Error>; 62 | } 63 | 64 | /// This is a factory trait for binding UDP sockets 65 | pub trait UdpBind { 66 | /// Error type returned on socket creation failure 67 | type Error: embedded_io_async::Error; 68 | 69 | /// The socket type returned by the stack 70 | type Socket<'a>: UdpReceive 71 | + UdpSend 72 | + UdpSplit 73 | + MulticastV4 74 | + MulticastV6 75 | + Readable 76 | where 77 | Self: 'a; 78 | 79 | /// Bind to a local socket address 80 | async fn bind(&self, local: SocketAddr) -> Result, Self::Error>; 81 | } 82 | 83 | impl UdpConnect for &T 84 | where 85 | T: UdpConnect, 86 | { 87 | type Error = T::Error; 88 | type Socket<'a> 89 | = T::Socket<'a> 90 | where 91 | Self: 'a; 92 | 93 | async fn connect( 94 | &self, 95 | local: SocketAddr, 96 | remote: SocketAddr, 97 | ) -> Result, Self::Error> { 98 | (*self).connect(local, remote).await 99 | } 100 | } 101 | 102 | impl UdpConnect for &mut T 103 | where 104 | T: UdpConnect, 105 | { 106 | type Error = T::Error; 107 | type Socket<'a> 108 | = T::Socket<'a> 109 | where 110 | Self: 'a; 111 | 112 | async fn connect( 113 | &self, 114 | local: SocketAddr, 115 | remote: SocketAddr, 116 | ) -> Result, Self::Error> { 117 | (**self).connect(local, remote).await 118 | } 119 | } 120 | 121 | impl UdpBind for &T 122 | where 123 | T: UdpBind, 124 | { 125 | type Error = T::Error; 126 | type Socket<'a> 127 | = T::Socket<'a> 128 | where 129 | Self: 'a; 130 | 131 | async fn bind(&self, local: SocketAddr) -> Result, Self::Error> { 132 | (*self).bind(local).await 133 | } 134 | } 135 | 136 | impl UdpBind for &mut T 137 | where 138 | T: UdpBind, 139 | { 140 | type Error = T::Error; 141 | type Socket<'a> 142 | = T::Socket<'a> 143 | where 144 | Self: 'a; 145 | 146 | async fn bind(&self, local: SocketAddr) -> Result, Self::Error> { 147 | (**self).bind(local).await 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /edge-nal/src/tcp.rs: -------------------------------------------------------------------------------- 1 | //! Trait for modeling TCP socket shutdown 2 | 3 | use embedded_io_async::ErrorType; 4 | 5 | /// Enum representing the different ways to close a TCP socket 6 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 7 | pub enum Close { 8 | /// Close the read half of the socket 9 | Read, 10 | /// Close the write half of the socket 11 | Write, 12 | /// Close both the read and write halves of the socket 13 | Both, 14 | } 15 | 16 | /// This trait is implemented by TCP sockets and models their shutdown functionality, 17 | /// which is unique to the TCP protocol (UDP sockets do not have a shutdown procedure). 18 | pub trait TcpShutdown: ErrorType { 19 | /// Gracefully shutdown either or both the read and write halves of the socket. 20 | /// 21 | /// The write half is closed by sending a FIN packet to the peer and then waiting 22 | /// until the FIN packet is ACKed. 23 | /// 24 | /// The read half is "closed" by reading from it until the peer indicates there is 25 | /// no more data to read (i.e. it sends a FIN packet to the local socket). 26 | /// Whether the other peer will send a FIN packet or not is not guaranteed, as that's 27 | /// application protocol-specific. Usually, closing the write half means the peer will 28 | /// notice and will send a FIN packet to the read half, thus "closing" it too. 29 | /// 30 | /// Note that - on certain platforms that don't have built-in timeouts - this method might never 31 | /// complete if the peer is unreachable / misbehaving, so it has to be used with a 32 | /// proper timeout in-place. 33 | /// 34 | /// Also note that calling this function multiple times may result in different behavior, 35 | /// depending on the platform. 36 | async fn close(&mut self, what: Close) -> Result<(), Self::Error>; 37 | 38 | /// Abort the connection, sending an RST packet to the peer 39 | /// 40 | /// This method will not wait forever, because the RST packet is not ACKed by the peer. 41 | /// 42 | /// Note that on certain platforms (STD for example) this method might be a no-op 43 | /// as the connection there is automatically aborted when the socket is dropped. 44 | /// 45 | /// Also note that calling this function multiple times may result in different behavior, 46 | /// depending on the platform. 47 | async fn abort(&mut self) -> Result<(), Self::Error>; 48 | } 49 | 50 | impl TcpShutdown for &mut T 51 | where 52 | T: TcpShutdown, 53 | { 54 | async fn close(&mut self, what: Close) -> Result<(), Self::Error> { 55 | (**self).close(what).await 56 | } 57 | 58 | async fn abort(&mut self) -> Result<(), Self::Error> { 59 | (**self).abort().await 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /edge-nal/src/timeout.rs: -------------------------------------------------------------------------------- 1 | //! This module provides utility function and a decorator struct 2 | //! for adding timeouts to IO types. 3 | //! 4 | //! Note that the presence of this module in the `edge-nal` crate 5 | //! is a bit controversial, as it is a utility, while `edge-nal` is a 6 | //! pure traits' crate otherwise. 7 | //! 8 | //! Therefore, the module might be moved to another location in future. 9 | 10 | use core::{ 11 | fmt::{self, Display}, 12 | future::Future, 13 | net::SocketAddr, 14 | }; 15 | 16 | use embassy_time::Duration; 17 | use embedded_io_async::{ErrorKind, ErrorType, Read, Write}; 18 | 19 | use crate::{Readable, TcpAccept, TcpConnect, TcpShutdown, TcpSplit}; 20 | 21 | /// Error type for the `with_timeout` function and `WithTimeout` struct. 22 | #[derive(Debug)] 23 | pub enum WithTimeoutError { 24 | /// An error occurred during the execution of the operation 25 | Error(E), 26 | /// The operation timed out 27 | Timeout, 28 | } 29 | 30 | impl From for WithTimeoutError { 31 | fn from(e: E) -> Self { 32 | Self::Error(e) 33 | } 34 | } 35 | 36 | impl fmt::Display for WithTimeoutError 37 | where 38 | E: Display, 39 | { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | match self { 42 | Self::Error(e) => write!(f, "{}", e), 43 | Self::Timeout => write!(f, "Operation timed out"), 44 | } 45 | } 46 | } 47 | 48 | impl embedded_io_async::Error for WithTimeoutError 49 | where 50 | E: embedded_io_async::Error, 51 | { 52 | fn kind(&self) -> ErrorKind { 53 | match self { 54 | Self::Error(e) => e.kind(), 55 | Self::Timeout => ErrorKind::TimedOut, 56 | } 57 | } 58 | } 59 | 60 | /// Run a fallible future with a timeout. 61 | /// 62 | /// A future is a fallible future if it resolves to a `Result`. 63 | /// 64 | /// If the future completes before the timeout, its output is returned. 65 | /// Otherwise, on timeout, a timeout error is returned. 66 | /// 67 | /// Parameters: 68 | /// - `timeout_ms`: The timeout duration in milliseconds 69 | /// - `fut`: The future to run 70 | pub async fn with_timeout(timeout_ms: u32, fut: F) -> Result> 71 | where 72 | F: Future>, 73 | { 74 | map_result(embassy_time::with_timeout(Duration::from_millis(timeout_ms as _), fut).await) 75 | } 76 | 77 | /// A type that wraps an IO stream type and adds a timeout to all operations. 78 | /// 79 | /// The operations decorated with a timeout are the ones offered via the following traits: 80 | /// - `embedded_io_async::Read` 81 | /// - `embedded_io_async::Write` 82 | /// - `Readable` 83 | /// - `TcpConnect` 84 | /// - `TcpShutdown` 85 | /// 86 | /// Additionally, wrapping with `WithTimeout` an IO type that implements `TcpAccept` will result 87 | /// in a `TcpAccept` implementation that - while waiting potentially indefinitely for an incoming 88 | /// connection - will return a connected socket readily wrapped with a timeout. 89 | pub struct WithTimeout(T, u32); 90 | 91 | impl WithTimeout { 92 | /// Create a new `WithTimeout` instance. 93 | /// 94 | /// Parameters: 95 | /// - `timeout_ms`: The timeout duration in milliseconds 96 | /// - `io`: The IO type to add a timeout to 97 | pub const fn new(timeout_ms: u32, io: T) -> Self { 98 | Self(io, timeout_ms) 99 | } 100 | 101 | /// Get a reference to the inner IO type. 102 | pub fn io(&self) -> &T { 103 | &self.0 104 | } 105 | 106 | /// Get a mutable reference to the inner IO type. 107 | pub fn io_mut(&mut self) -> &mut T { 108 | &mut self.0 109 | } 110 | 111 | /// Get the timeout duration in milliseconds. 112 | pub fn timeout_ms(&self) -> u32 { 113 | self.1 114 | } 115 | 116 | /// Get the IO type by destructuring the `WithTimeout` instance. 117 | pub fn into_io(self) -> T { 118 | self.0 119 | } 120 | } 121 | 122 | impl ErrorType for WithTimeout 123 | where 124 | T: ErrorType, 125 | { 126 | type Error = WithTimeoutError; 127 | } 128 | 129 | impl Read for WithTimeout 130 | where 131 | T: Read, 132 | { 133 | async fn read(&mut self, buf: &mut [u8]) -> Result { 134 | with_timeout(self.1, self.0.read(buf)).await 135 | } 136 | } 137 | 138 | impl Write for WithTimeout 139 | where 140 | T: Write, 141 | { 142 | async fn write(&mut self, buf: &[u8]) -> Result { 143 | with_timeout(self.1, self.0.write(buf)).await 144 | } 145 | 146 | async fn flush(&mut self) -> Result<(), Self::Error> { 147 | with_timeout(self.1, self.0.flush()).await 148 | } 149 | } 150 | 151 | impl TcpConnect for WithTimeout 152 | where 153 | T: TcpConnect, 154 | { 155 | type Error = WithTimeoutError; 156 | 157 | type Socket<'a> 158 | = WithTimeout> 159 | where 160 | Self: 'a; 161 | 162 | async fn connect(&self, remote: SocketAddr) -> Result, Self::Error> { 163 | with_timeout(self.1, self.0.connect(remote)) 164 | .await 165 | .map(|s| WithTimeout::new(self.1, s)) 166 | } 167 | } 168 | 169 | impl Readable for WithTimeout 170 | where 171 | T: Readable, 172 | { 173 | async fn readable(&mut self) -> Result<(), Self::Error> { 174 | with_timeout(self.1, self.0.readable()).await 175 | } 176 | } 177 | 178 | impl TcpSplit for WithTimeout 179 | where 180 | T: TcpSplit, 181 | { 182 | type Read<'a> 183 | = WithTimeout> 184 | where 185 | Self: 'a; 186 | 187 | type Write<'a> 188 | = WithTimeout> 189 | where 190 | Self: 'a; 191 | 192 | fn split(&mut self) -> (Self::Read<'_>, Self::Write<'_>) { 193 | let (r, w) = self.0.split(); 194 | (WithTimeout::new(self.1, r), WithTimeout::new(self.1, w)) 195 | } 196 | } 197 | 198 | impl TcpShutdown for WithTimeout 199 | where 200 | T: TcpShutdown, 201 | { 202 | async fn close(&mut self, what: crate::Close) -> Result<(), Self::Error> { 203 | with_timeout(self.1, self.0.close(what)).await 204 | } 205 | 206 | async fn abort(&mut self) -> Result<(), Self::Error> { 207 | with_timeout(self.1, self.0.abort()).await 208 | } 209 | } 210 | 211 | impl TcpAccept for WithTimeout 212 | where 213 | T: TcpAccept, 214 | { 215 | type Error = WithTimeoutError; 216 | 217 | type Socket<'a> 218 | = WithTimeout> 219 | where 220 | Self: 'a; 221 | 222 | async fn accept(&self) -> Result<(SocketAddr, Self::Socket<'_>), Self::Error> { 223 | let (addr, socket) = self.0.accept().await?; 224 | 225 | Ok((addr, WithTimeout::new(self.1, socket))) 226 | } 227 | } 228 | 229 | fn map_result( 230 | result: Result, embassy_time::TimeoutError>, 231 | ) -> Result> { 232 | match result { 233 | Ok(Ok(t)) => Ok(t), 234 | Ok(Err(e)) => Err(WithTimeoutError::Error(e)), 235 | Err(_) => Err(WithTimeoutError::Timeout), 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /edge-nal/src/udp.rs: -------------------------------------------------------------------------------- 1 | //! Traits for modeling UDP sending/receiving functionality on embedded devices 2 | 3 | use core::net::SocketAddr; 4 | 5 | use embedded_io_async::ErrorType; 6 | 7 | /// This trait is implemented by UDP sockets and models their datagram receiving functionality. 8 | /// 9 | /// The socket it represents might be either bound (has a local IP address, port and interface) or 10 | /// connected (also has a remote IP address and port). 11 | /// 12 | /// The term "connected" here refers to the semantics of POSIX datagram sockets, through which datagrams 13 | /// are sent and received without having a remote address per call. It does not imply any process 14 | /// of establishing a connection (which is absent in UDP). While there is typically no POSIX 15 | /// `bind()` call in the creation of such sockets, these are implicitly bound to a suitable local 16 | /// address at connect time. 17 | pub trait UdpReceive: ErrorType { 18 | /// Receive a datagram into the provided buffer. 19 | /// 20 | /// If the received datagram exceeds the buffer's length, it is received regardless, and the 21 | /// remaining bytes are discarded. The full datagram size is still indicated in the result, 22 | /// allowing the recipient to detect that truncation. 23 | /// 24 | /// The remote addresses is given in the result along with the number of bytes. 25 | async fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), Self::Error>; 26 | } 27 | 28 | /// This trait is implemented by UDP sockets and models their datagram sending functionality. 29 | /// 30 | /// The socket it represents might be either bound (has a local IP address, port and interface) or 31 | /// connected (also has a remote IP address and port). 32 | /// 33 | /// The term "connected" here refers to the semantics of POSIX datagram sockets, through which datagrams 34 | /// are sent and received without having a remote address per call. It does not imply any process 35 | /// of establishing a connection (which is absent in UDP). While there is typically no POSIX 36 | /// `bind()` call in the creation of such sockets, these are implicitly bound to a suitable local 37 | /// address at connect time. 38 | pub trait UdpSend: ErrorType { 39 | /// Send the provided data to a peer: 40 | /// - In case the socket is connected, the provided remote address is ignored. 41 | /// - In case the socket is unconnected the remote address is used. 42 | async fn send(&mut self, remote: SocketAddr, data: &[u8]) -> Result<(), Self::Error>; 43 | } 44 | 45 | pub trait UdpSocket: UdpReceive + UdpSend {} 46 | 47 | impl UdpReceive for &mut T 48 | where 49 | T: UdpReceive, 50 | { 51 | async fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), Self::Error> { 52 | (**self).receive(buffer).await 53 | } 54 | } 55 | 56 | impl UdpSend for &mut T 57 | where 58 | T: UdpSend, 59 | { 60 | async fn send(&mut self, remote: SocketAddr, data: &[u8]) -> Result<(), Self::Error> { 61 | (**self).send(remote, data).await 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /edge-raw/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.0] - 2025-05-29 9 | * Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): 10 | * `log` - uses the `log` crate for all logging 11 | * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` 12 | 13 | ## [0.5.0] - 2025-01-15 14 | * Updated dependencies for compatibility with `embassy-time-driver` v0.2 15 | 16 | ## [0.4.0] - 2025-01-02 17 | * Derive more for Error (Copy, Clone, Eq, PartialEq, Hash) 18 | 19 | ## [0.3.0] - 2024-09-10 20 | * IO layer now implements the raw-networking traits from `edge-net` 21 | -------------------------------------------------------------------------------- /edge-raw/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-raw" 3 | version = "0.6.0" 4 | edition = "2021" 5 | rust-version = "1.83" 6 | description = "Async + `no_std` + no-alloc implementation of IP and UDP packet creation and parsing" 7 | repository = "https://github.com/ivmarkov/edge-net" 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | categories = [ 11 | "embedded", 12 | "no-std::no-alloc", 13 | "asynchronous", 14 | "network-programming", 15 | ] 16 | 17 | [features] 18 | default = ["io"] 19 | std = ["io"] 20 | io = ["embedded-io-async", "edge-nal"] 21 | 22 | [dependencies] 23 | log = { version = "0.4", default-features = false, optional = true } 24 | defmt = { version = "0.3", optional = true } 25 | embedded-io-async = { workspace = true, default-features = false, optional = true } 26 | edge-nal = { workspace = true, default-features = false, optional = true } 27 | -------------------------------------------------------------------------------- /edge-raw/README.md: -------------------------------------------------------------------------------- 1 | # edge-raw 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | Async + `no_std` + no-alloc implementation of IP and UDP packet creation and parsing. 8 | 9 | The `edge_raw::io` module contains implementations of the `edge_nal::RawBind` trait, as well as of the `edge_nal::RawReceive` and `edge_nal::RawSend` traits. 10 | 11 | These are useful in the context of protocols like DHCP, which - while working on top of UDP - need to be capable of receiving 12 | and sending packets to peers that do not have an IP address assigned yet. 13 | 14 | For other protocols, look at the [edge-net](https://github.com/ivmarkov/edge-net) aggregator crate documentation. 15 | 16 | ## Examples 17 | 18 | Look at the [edge-dhcp](../edge-dhcp) crate as to how to utilize the capabilities of `edge-raw`. 19 | -------------------------------------------------------------------------------- /edge-raw/src/bytes.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] 2 | pub enum Error { 3 | BufferOverflow, 4 | DataUnderflow, 5 | InvalidFormat, 6 | } 7 | 8 | pub struct BytesIn<'a> { 9 | data: &'a [u8], 10 | offset: usize, 11 | } 12 | 13 | impl<'a> BytesIn<'a> { 14 | pub const fn new(data: &'a [u8]) -> Self { 15 | Self { data, offset: 0 } 16 | } 17 | 18 | pub fn is_empty(&self) -> bool { 19 | self.offset == self.data.len() 20 | } 21 | 22 | pub fn offset(&self) -> usize { 23 | self.offset 24 | } 25 | 26 | pub fn byte(&mut self) -> Result { 27 | self.arr::<1>().map(|arr| arr[0]) 28 | } 29 | 30 | pub fn slice(&mut self, len: usize) -> Result<&'a [u8], Error> { 31 | if len > self.data.len() - self.offset { 32 | Err(Error::DataUnderflow) 33 | } else { 34 | let data = &self.data[self.offset..self.offset + len]; 35 | self.offset += len; 36 | 37 | Ok(data) 38 | } 39 | } 40 | 41 | pub fn arr(&mut self) -> Result<[u8; N], Error> { 42 | let slice = self.slice(N)?; 43 | 44 | let mut data = [0; N]; 45 | data.copy_from_slice(slice); 46 | 47 | Ok(data) 48 | } 49 | 50 | pub fn remaining(&mut self) -> &'a [u8] { 51 | let data = unwrap!(self.slice(self.data.len() - self.offset), "Unreachable"); 52 | 53 | self.offset = self.data.len(); 54 | 55 | data 56 | } 57 | 58 | pub fn remaining_byte(&mut self) -> Result { 59 | Ok(self.remaining_arr::<1>()?[0]) 60 | } 61 | 62 | pub fn remaining_arr(&mut self) -> Result<[u8; N], Error> { 63 | if self.data.len() - self.offset > N { 64 | Err(Error::InvalidFormat) 65 | } else { 66 | self.arr::() 67 | } 68 | } 69 | } 70 | 71 | pub struct BytesOut<'a> { 72 | buf: &'a mut [u8], 73 | offset: usize, 74 | } 75 | 76 | impl<'a> BytesOut<'a> { 77 | pub fn new(buf: &'a mut [u8]) -> Self { 78 | Self { buf, offset: 0 } 79 | } 80 | 81 | pub fn is_empty(&self) -> bool { 82 | self.len() == 0 83 | } 84 | 85 | pub fn len(&self) -> usize { 86 | self.offset 87 | } 88 | 89 | pub fn byte(&mut self, data: u8) -> Result<&mut Self, Error> { 90 | self.push(&[data]) 91 | } 92 | 93 | pub fn push(&mut self, data: &[u8]) -> Result<&mut Self, Error> { 94 | if data.len() > self.buf.len() - self.offset { 95 | Err(Error::BufferOverflow) 96 | } else { 97 | self.buf[self.offset..self.offset + data.len()].copy_from_slice(data); 98 | self.offset += data.len(); 99 | 100 | Ok(self) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /edge-raw/src/io.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | use core::mem::MaybeUninit; 3 | use core::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; 4 | 5 | use embedded_io_async::{ErrorKind, ErrorType}; 6 | 7 | use edge_nal::{MacAddr, RawReceive, RawSend, RawSplit, Readable, UdpReceive, UdpSend, UdpSplit}; 8 | 9 | use crate as raw; 10 | 11 | /// An error that can occur when sending or receiving UDP packets over a raw socket. 12 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] 13 | pub enum Error { 14 | Io(E), 15 | UnsupportedProtocol, 16 | RawError(raw::Error), 17 | } 18 | 19 | impl From for Error { 20 | fn from(value: raw::Error) -> Self { 21 | Self::RawError(value) 22 | } 23 | } 24 | 25 | impl embedded_io_async::Error for Error 26 | where 27 | E: embedded_io_async::Error, 28 | { 29 | fn kind(&self) -> ErrorKind { 30 | match self { 31 | Self::Io(err) => err.kind(), 32 | Self::UnsupportedProtocol => ErrorKind::InvalidInput, 33 | Self::RawError(_) => ErrorKind::InvalidData, 34 | } 35 | } 36 | } 37 | 38 | impl core::fmt::Display for Error 39 | where 40 | E: core::fmt::Display, 41 | { 42 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 43 | match self { 44 | Self::Io(err) => write!(f, "IO error: {}", err), 45 | Self::UnsupportedProtocol => write!(f, "Unsupported protocol"), 46 | Self::RawError(err) => write!(f, "Raw error: {}", err), 47 | } 48 | } 49 | } 50 | 51 | #[cfg(feature = "defmt")] 52 | impl defmt::Format for Error 53 | where 54 | E: defmt::Format, 55 | { 56 | fn format(&self, f: defmt::Formatter<'_>) { 57 | match self { 58 | Self::Io(err) => defmt::write!(f, "IO error: {}", err), 59 | Self::UnsupportedProtocol => defmt::write!(f, "Unsupported protocol"), 60 | Self::RawError(err) => defmt::write!(f, "Raw error: {}", err), 61 | } 62 | } 63 | } 64 | 65 | #[cfg(feature = "std")] 66 | impl std::error::Error for Error where E: std::error::Error {} 67 | 68 | /// A utility struct allowing to send and receive UDP packets over a raw socket. 69 | /// 70 | /// The major difference between this struct and a regular `UdpSend` and `UdpReceive` pair of UDP socket halves 71 | /// is that `RawSocket2Udp` requires the MAC address of the remote host to send packets to. 72 | /// 73 | /// This allows DHCP clients to operate even when the local peer does not yet have a valid IP address. 74 | /// It also allows DHCP servers to send packets to specific clients which don't yet have an IP address, and are 75 | /// thus only addressable either by broadcasting, or by their MAC address. 76 | pub struct RawSocket2Udp { 77 | socket: T, 78 | filter_local: Option, 79 | filter_remote: Option, 80 | remote_mac: MacAddr, 81 | } 82 | 83 | impl RawSocket2Udp { 84 | pub fn new( 85 | socket: T, 86 | filter_local: Option, 87 | filter_remote: Option, 88 | remote_mac: MacAddr, 89 | ) -> Self { 90 | Self { 91 | socket, 92 | filter_local, 93 | filter_remote, 94 | remote_mac, 95 | } 96 | } 97 | } 98 | 99 | impl ErrorType for RawSocket2Udp 100 | where 101 | T: ErrorType, 102 | { 103 | type Error = Error; 104 | } 105 | 106 | impl UdpReceive for RawSocket2Udp 107 | where 108 | T: RawReceive, 109 | { 110 | async fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), Self::Error> { 111 | let (len, _local, remote, _) = udp_receive::<_, N>( 112 | &mut self.socket, 113 | self.filter_local, 114 | self.filter_remote, 115 | buffer, 116 | ) 117 | .await?; 118 | 119 | Ok((len, remote)) 120 | } 121 | } 122 | 123 | impl Readable for RawSocket2Udp 124 | where 125 | T: Readable, 126 | { 127 | async fn readable(&mut self) -> Result<(), Self::Error> { 128 | self.socket.readable().await.map_err(Error::Io) 129 | } 130 | } 131 | 132 | impl UdpSend for RawSocket2Udp 133 | where 134 | T: RawSend, 135 | { 136 | async fn send(&mut self, remote: SocketAddr, data: &[u8]) -> Result<(), Self::Error> { 137 | let remote = match remote { 138 | SocketAddr::V4(remote) => remote, 139 | SocketAddr::V6(_) => Err(Error::UnsupportedProtocol)?, 140 | }; 141 | 142 | udp_send::<_, N>( 143 | &mut self.socket, 144 | SocketAddr::V4( 145 | self.filter_local 146 | .unwrap_or(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)), 147 | ), 148 | SocketAddr::V4(remote), 149 | self.remote_mac, 150 | data, 151 | ) 152 | .await 153 | } 154 | } 155 | 156 | impl UdpSplit for RawSocket2Udp 157 | where 158 | T: RawSplit, 159 | { 160 | type Receive<'a> 161 | = RawSocket2Udp, N> 162 | where 163 | Self: 'a; 164 | type Send<'a> 165 | = RawSocket2Udp, N> 166 | where 167 | Self: 'a; 168 | 169 | fn split(&mut self) -> (Self::Receive<'_>, Self::Send<'_>) { 170 | let (receive, send) = self.socket.split(); 171 | 172 | ( 173 | RawSocket2Udp::new( 174 | receive, 175 | self.filter_local, 176 | self.filter_remote, 177 | self.remote_mac, 178 | ), 179 | RawSocket2Udp::new(send, self.filter_local, self.filter_remote, self.remote_mac), 180 | ) 181 | } 182 | } 183 | 184 | /// Sends a UDP packet to a remote peer identified by its MAC address 185 | pub async fn udp_send( 186 | mut socket: T, 187 | local: SocketAddr, 188 | remote: SocketAddr, 189 | remote_mac: MacAddr, 190 | data: &[u8], 191 | ) -> Result<(), Error> { 192 | let (SocketAddr::V4(local), SocketAddr::V4(remote)) = (local, remote) else { 193 | Err(Error::UnsupportedProtocol)? 194 | }; 195 | 196 | let mut buf = MaybeUninit::<[u8; N]>::uninit(); 197 | let buf = unsafe { buf.assume_init_mut() }; 198 | 199 | let data = raw::ip_udp_encode(buf, local, remote, |buf| { 200 | if data.len() <= buf.len() { 201 | buf[..data.len()].copy_from_slice(data); 202 | 203 | Ok(data.len()) 204 | } else { 205 | Err(raw::Error::BufferOverflow) 206 | } 207 | })?; 208 | 209 | socket.send(remote_mac, data).await.map_err(Error::Io) 210 | } 211 | 212 | /// Receives a UDP packet from a remote peer 213 | pub async fn udp_receive( 214 | mut socket: T, 215 | filter_local: Option, 216 | filter_remote: Option, 217 | buffer: &mut [u8], 218 | ) -> Result<(usize, SocketAddr, SocketAddr, MacAddr), Error> { 219 | let mut buf = MaybeUninit::<[u8; N]>::uninit(); 220 | let buf = unsafe { buf.assume_init_mut() }; 221 | 222 | let (len, local, remote, remote_mac) = loop { 223 | let (len, remote_mac) = socket.receive(buf).await.map_err(Error::Io)?; 224 | 225 | match raw::ip_udp_decode(&buf[..len], filter_remote, filter_local) { 226 | Ok(Some((remote, local, data))) => { 227 | if data.len() > buffer.len() { 228 | Err(Error::RawError(raw::Error::BufferOverflow))?; 229 | } 230 | 231 | buffer[..data.len()].copy_from_slice(data); 232 | 233 | break (data.len(), local, remote, remote_mac); 234 | } 235 | Ok(None) => continue, 236 | Err(raw::Error::InvalidFormat) | Err(raw::Error::InvalidChecksum) => continue, 237 | Err(other) => Err(other)?, 238 | } 239 | }; 240 | 241 | Ok(( 242 | len, 243 | SocketAddr::V4(local), 244 | SocketAddr::V4(remote), 245 | remote_mac, 246 | )) 247 | } 248 | -------------------------------------------------------------------------------- /edge-raw/src/ip.rs: -------------------------------------------------------------------------------- 1 | use core::net::Ipv4Addr; 2 | 3 | use super::bytes::{BytesIn, BytesOut}; 4 | 5 | use super::{checksum_accumulate, checksum_finish, Error}; 6 | 7 | #[allow(clippy::type_complexity)] 8 | pub fn decode( 9 | packet: &[u8], 10 | filter_src: Ipv4Addr, 11 | filter_dst: Ipv4Addr, 12 | filter_proto: Option, 13 | ) -> Result, Error> { 14 | let data = Ipv4PacketHeader::decode_with_payload(packet, filter_src, filter_dst, filter_proto)? 15 | .map(|(hdr, payload)| (hdr.src, hdr.dst, hdr.p, payload)); 16 | 17 | Ok(data) 18 | } 19 | 20 | pub fn encode( 21 | buf: &mut [u8], 22 | src: Ipv4Addr, 23 | dst: Ipv4Addr, 24 | proto: u8, 25 | encoder: F, 26 | ) -> Result<&[u8], Error> 27 | where 28 | F: FnOnce(&mut [u8]) -> Result, 29 | { 30 | let mut hdr = Ipv4PacketHeader::new(src, dst, proto); 31 | 32 | hdr.encode_with_payload(buf, encoder) 33 | } 34 | 35 | /// Represents a parsed IP header 36 | #[derive(Clone, Debug)] 37 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 38 | pub struct Ipv4PacketHeader { 39 | /// Version 40 | pub version: u8, 41 | /// Header length 42 | pub hlen: u8, 43 | /// Type of service 44 | pub tos: u8, 45 | /// Total length 46 | pub len: u16, 47 | /// Identification 48 | pub id: u16, 49 | /// Fragment offset field 50 | pub off: u16, 51 | /// Time to live 52 | pub ttl: u8, 53 | /// Protocol 54 | pub p: u8, 55 | /// Checksum 56 | pub sum: u16, 57 | /// Source address 58 | pub src: Ipv4Addr, 59 | /// Dest address 60 | pub dst: Ipv4Addr, 61 | } 62 | 63 | impl Ipv4PacketHeader { 64 | pub const MIN_SIZE: usize = 20; 65 | pub const CHECKSUM_WORD: usize = 5; 66 | 67 | pub const IP_DF: u16 = 0x4000; // Don't fragment flag 68 | pub const IP_MF: u16 = 0x2000; // More fragments flag 69 | 70 | /// Create a new header instance 71 | pub fn new(src: Ipv4Addr, dst: Ipv4Addr, proto: u8) -> Self { 72 | Self { 73 | version: 4, 74 | hlen: Self::MIN_SIZE as _, 75 | tos: 0, 76 | len: Self::MIN_SIZE as _, 77 | id: 0, 78 | off: 0, 79 | ttl: 64, 80 | p: proto, 81 | sum: 0, 82 | src, 83 | dst, 84 | } 85 | } 86 | 87 | /// Decodes the header from a byte slice 88 | pub fn decode(data: &[u8]) -> Result { 89 | let mut bytes = BytesIn::new(data); 90 | 91 | let vhl = bytes.byte()?; 92 | 93 | Ok(Self { 94 | version: vhl >> 4, 95 | hlen: (vhl & 0x0f) * 4, 96 | tos: bytes.byte()?, 97 | len: u16::from_be_bytes(bytes.arr()?), 98 | id: u16::from_be_bytes(bytes.arr()?), 99 | off: u16::from_be_bytes(bytes.arr()?), 100 | ttl: bytes.byte()?, 101 | p: bytes.byte()?, 102 | sum: u16::from_be_bytes(bytes.arr()?), 103 | src: u32::from_be_bytes(bytes.arr()?).into(), 104 | dst: u32::from_be_bytes(bytes.arr()?).into(), 105 | }) 106 | } 107 | 108 | /// Encodes the header into the provided buf slice 109 | pub fn encode<'o>(&self, buf: &'o mut [u8]) -> Result<&'o [u8], Error> { 110 | let mut bytes = BytesOut::new(buf); 111 | 112 | bytes 113 | .byte((self.version << 4) | (self.hlen / 4 + (if self.hlen % 4 > 0 { 1 } else { 0 })))? 114 | .byte(self.tos)? 115 | .push(&u16::to_be_bytes(self.len))? 116 | .push(&u16::to_be_bytes(self.id))? 117 | .push(&u16::to_be_bytes(self.off))? 118 | .byte(self.ttl)? 119 | .byte(self.p)? 120 | .push(&u16::to_be_bytes(self.sum))? 121 | .push(&u32::to_be_bytes(self.src.into()))? 122 | .push(&u32::to_be_bytes(self.dst.into()))?; 123 | 124 | let len = bytes.len(); 125 | 126 | Ok(&buf[..len]) 127 | } 128 | 129 | /// Encodes the header and the provided payload into the provided buf slice 130 | pub fn encode_with_payload<'o, F>( 131 | &mut self, 132 | buf: &'o mut [u8], 133 | encoder: F, 134 | ) -> Result<&'o [u8], Error> 135 | where 136 | F: FnOnce(&mut [u8]) -> Result, 137 | { 138 | let hdr_len = self.hlen as usize; 139 | if hdr_len < Self::MIN_SIZE || buf.len() < hdr_len { 140 | Err(Error::BufferOverflow)?; 141 | } 142 | 143 | let (hdr_buf, payload_buf) = buf.split_at_mut(hdr_len); 144 | 145 | let payload_len = encoder(payload_buf)?; 146 | 147 | let len = hdr_len + payload_len; 148 | self.len = len as _; 149 | 150 | let min_hdr_len = self.encode(hdr_buf)?.len(); 151 | assert_eq!(min_hdr_len, Self::MIN_SIZE); 152 | 153 | hdr_buf[Self::MIN_SIZE..hdr_len].fill(0); 154 | 155 | let checksum = Self::checksum(hdr_buf); 156 | self.sum = checksum; 157 | 158 | Self::inject_checksum(hdr_buf, checksum); 159 | 160 | Ok(&buf[..len]) 161 | } 162 | 163 | /// Decodes the provided packet into a header and a payload slice 164 | pub fn decode_with_payload( 165 | packet: &[u8], 166 | filter_src: Ipv4Addr, 167 | filter_dst: Ipv4Addr, 168 | filter_proto: Option, 169 | ) -> Result, Error> { 170 | let hdr = Self::decode(packet)?; 171 | if hdr.version == 4 { 172 | // IPv4 173 | 174 | if !filter_src.is_unspecified() && !hdr.src.is_broadcast() && filter_src != hdr.src { 175 | return Ok(None); 176 | } 177 | 178 | if !filter_dst.is_unspecified() && !hdr.dst.is_broadcast() && filter_dst != hdr.dst { 179 | return Ok(None); 180 | } 181 | 182 | if let Some(filter_proto) = filter_proto { 183 | if filter_proto != hdr.p { 184 | return Ok(None); 185 | } 186 | } 187 | 188 | let len = hdr.len as usize; 189 | if packet.len() < len { 190 | Err(Error::DataUnderflow)?; 191 | } 192 | 193 | let checksum = Self::checksum(&packet[..len]); 194 | 195 | trace!("IP header decoded, total_size={}, src={}, dst={}, hlen={}, size={}, checksum={}, ours={}", packet.len(), hdr.src, hdr.dst, hdr.hlen, hdr.len, hdr.sum, checksum); 196 | 197 | if checksum != hdr.sum { 198 | Err(Error::InvalidChecksum)?; 199 | } 200 | 201 | let packet = &packet[..len]; 202 | let hdr_len = hdr.hlen as usize; 203 | if packet.len() < hdr_len { 204 | Err(Error::DataUnderflow)?; 205 | } 206 | 207 | Ok(Some((hdr, &packet[hdr_len..]))) 208 | } else { 209 | Err(Error::InvalidFormat) 210 | } 211 | } 212 | 213 | /// Injects the checksum into the provided packet 214 | pub fn inject_checksum(packet: &mut [u8], checksum: u16) { 215 | let checksum = checksum.to_be_bytes(); 216 | 217 | let offset = Self::CHECKSUM_WORD << 1; 218 | packet[offset] = checksum[0]; 219 | packet[offset + 1] = checksum[1]; 220 | } 221 | 222 | /// Computes the checksum for an already encoded packet 223 | pub fn checksum(packet: &[u8]) -> u16 { 224 | let hlen = (packet[0] & 0x0f) as usize * 4; 225 | 226 | let sum = checksum_accumulate(&packet[..hlen], Self::CHECKSUM_WORD); 227 | 228 | checksum_finish(sum) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /edge-raw/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | #![allow(async_fn_in_trait)] 3 | #![warn(clippy::large_futures)] 4 | #![allow(clippy::uninlined_format_args)] 5 | #![allow(unknown_lints)] 6 | 7 | use core::net::{Ipv4Addr, SocketAddrV4}; 8 | 9 | use self::udp::UdpPacketHeader; 10 | 11 | // This mod MUST go first, so that the others see its macros. 12 | pub(crate) mod fmt; 13 | 14 | #[cfg(feature = "io")] 15 | pub mod io; 16 | 17 | pub mod bytes; 18 | pub mod ip; 19 | pub mod udp; 20 | 21 | use bytes::BytesIn; 22 | 23 | /// An error type for decoding and encoding IP and UDP oackets 24 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] 25 | pub enum Error { 26 | DataUnderflow, 27 | BufferOverflow, 28 | InvalidFormat, 29 | InvalidChecksum, 30 | } 31 | 32 | impl From for Error { 33 | fn from(value: bytes::Error) -> Self { 34 | match value { 35 | bytes::Error::BufferOverflow => Self::BufferOverflow, 36 | bytes::Error::DataUnderflow => Self::DataUnderflow, 37 | bytes::Error::InvalidFormat => Self::InvalidFormat, 38 | } 39 | } 40 | } 41 | 42 | impl core::fmt::Display for Error { 43 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 44 | let str = match self { 45 | Self::DataUnderflow => "Data underflow", 46 | Self::BufferOverflow => "Buffer overflow", 47 | Self::InvalidFormat => "Invalid format", 48 | Self::InvalidChecksum => "Invalid checksum", 49 | }; 50 | 51 | write!(f, "{}", str) 52 | } 53 | } 54 | 55 | #[cfg(feature = "defmt")] 56 | impl defmt::Format for Error { 57 | fn format(&self, f: defmt::Formatter<'_>) { 58 | let str = match self { 59 | Self::DataUnderflow => "Data underflow", 60 | Self::BufferOverflow => "Buffer overflow", 61 | Self::InvalidFormat => "Invalid format", 62 | Self::InvalidChecksum => "Invalid checksum", 63 | }; 64 | 65 | defmt::write!(f, "{}", str) 66 | } 67 | } 68 | 69 | #[cfg(feature = "std")] 70 | impl std::error::Error for Error {} 71 | 72 | /// Decodes an IP packet and its UDP payload 73 | #[allow(clippy::type_complexity)] 74 | pub fn ip_udp_decode( 75 | packet: &[u8], 76 | filter_src: Option, 77 | filter_dst: Option, 78 | ) -> Result, Error> { 79 | if let Some((src, dst, _proto, udp_packet)) = ip::decode( 80 | packet, 81 | filter_src.map(|a| *a.ip()).unwrap_or(Ipv4Addr::UNSPECIFIED), 82 | filter_dst.map(|a| *a.ip()).unwrap_or(Ipv4Addr::UNSPECIFIED), 83 | Some(UdpPacketHeader::PROTO), 84 | )? { 85 | udp::decode( 86 | src, 87 | dst, 88 | udp_packet, 89 | filter_src.map(|a| a.port()), 90 | filter_dst.map(|a| a.port()), 91 | ) 92 | } else { 93 | Ok(None) 94 | } 95 | } 96 | 97 | /// Encodes an IP packet and its UDP payload 98 | pub fn ip_udp_encode( 99 | buf: &mut [u8], 100 | src: SocketAddrV4, 101 | dst: SocketAddrV4, 102 | encoder: F, 103 | ) -> Result<&[u8], Error> 104 | where 105 | F: FnOnce(&mut [u8]) -> Result, 106 | { 107 | ip::encode(buf, *src.ip(), *dst.ip(), UdpPacketHeader::PROTO, |buf| { 108 | Ok(udp::encode(buf, src, dst, encoder)?.len()) 109 | }) 110 | } 111 | 112 | pub fn checksum_accumulate(bytes: &[u8], checksum_word: usize) -> u32 { 113 | let mut bytes = BytesIn::new(bytes); 114 | 115 | let mut sum: u32 = 0; 116 | while !bytes.is_empty() { 117 | let skip = (bytes.offset() >> 1) == checksum_word; 118 | let arr = bytes 119 | .arr() 120 | .ok() 121 | .unwrap_or_else(|| [unwrap!(bytes.byte(), "Unreachable"), 0]); 122 | 123 | let word = if skip { 0 } else { u16::from_be_bytes(arr) }; 124 | 125 | sum += word as u32; 126 | } 127 | 128 | sum 129 | } 130 | 131 | pub fn checksum_finish(mut sum: u32) -> u16 { 132 | while sum >> 16 != 0 { 133 | sum = (sum >> 16) + (sum & 0xffff); 134 | } 135 | 136 | !sum as u16 137 | } 138 | -------------------------------------------------------------------------------- /edge-raw/src/udp.rs: -------------------------------------------------------------------------------- 1 | use core::net::{Ipv4Addr, SocketAddrV4}; 2 | 3 | use super::bytes::{BytesIn, BytesOut}; 4 | 5 | use super::{checksum_accumulate, checksum_finish, Error}; 6 | 7 | #[allow(clippy::type_complexity)] 8 | pub fn decode( 9 | src: Ipv4Addr, 10 | dst: Ipv4Addr, 11 | packet: &[u8], 12 | filter_src: Option, 13 | filter_dst: Option, 14 | ) -> Result, Error> { 15 | let data = UdpPacketHeader::decode_with_payload(packet, src, dst, filter_src, filter_dst)?.map( 16 | |(hdr, payload)| { 17 | ( 18 | SocketAddrV4::new(src, hdr.src), 19 | SocketAddrV4::new(dst, hdr.dst), 20 | payload, 21 | ) 22 | }, 23 | ); 24 | 25 | Ok(data) 26 | } 27 | 28 | pub fn encode( 29 | buf: &mut [u8], 30 | src: SocketAddrV4, 31 | dst: SocketAddrV4, 32 | payload: F, 33 | ) -> Result<&[u8], Error> 34 | where 35 | F: FnOnce(&mut [u8]) -> Result, 36 | { 37 | let mut hdr = UdpPacketHeader::new(src.port(), dst.port()); 38 | 39 | hdr.encode_with_payload(buf, *src.ip(), *dst.ip(), |buf| payload(buf)) 40 | } 41 | 42 | /// Represents a parsed UDP header 43 | #[derive(Clone, Debug)] 44 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 45 | pub struct UdpPacketHeader { 46 | /// Source port 47 | pub src: u16, 48 | /// Destination port 49 | pub dst: u16, 50 | /// UDP length 51 | pub len: u16, 52 | /// UDP checksum 53 | pub sum: u16, 54 | } 55 | 56 | impl UdpPacketHeader { 57 | pub const PROTO: u8 = 17; 58 | 59 | pub const SIZE: usize = 8; 60 | pub const CHECKSUM_WORD: usize = 3; 61 | 62 | /// Create a new header instance 63 | pub fn new(src: u16, dst: u16) -> Self { 64 | Self { 65 | src, 66 | dst, 67 | len: 0, 68 | sum: 0, 69 | } 70 | } 71 | 72 | /// Decodes the header from a byte slice 73 | pub fn decode(data: &[u8]) -> Result { 74 | let mut bytes = BytesIn::new(data); 75 | 76 | Ok(Self { 77 | src: u16::from_be_bytes(bytes.arr()?), 78 | dst: u16::from_be_bytes(bytes.arr()?), 79 | len: u16::from_be_bytes(bytes.arr()?), 80 | sum: u16::from_be_bytes(bytes.arr()?), 81 | }) 82 | } 83 | 84 | /// Encodes the header into the provided buf slice 85 | pub fn encode<'o>(&self, buf: &'o mut [u8]) -> Result<&'o [u8], Error> { 86 | let mut bytes = BytesOut::new(buf); 87 | 88 | bytes 89 | .push(&u16::to_be_bytes(self.src))? 90 | .push(&u16::to_be_bytes(self.dst))? 91 | .push(&u16::to_be_bytes(self.len))? 92 | .push(&u16::to_be_bytes(self.sum))?; 93 | 94 | let len = bytes.len(); 95 | 96 | Ok(&buf[..len]) 97 | } 98 | 99 | /// Encodes the header and the provided payload into the provided buf slice 100 | pub fn encode_with_payload<'o, F>( 101 | &mut self, 102 | buf: &'o mut [u8], 103 | src: Ipv4Addr, 104 | dst: Ipv4Addr, 105 | encoder: F, 106 | ) -> Result<&'o [u8], Error> 107 | where 108 | F: FnOnce(&mut [u8]) -> Result, 109 | { 110 | if buf.len() < Self::SIZE { 111 | Err(Error::BufferOverflow)?; 112 | } 113 | 114 | let (hdr_buf, payload_buf) = buf.split_at_mut(Self::SIZE); 115 | 116 | let payload_len = encoder(payload_buf)?; 117 | 118 | let len = Self::SIZE + payload_len; 119 | self.len = len as _; 120 | 121 | let hdr_len = self.encode(hdr_buf)?.len(); 122 | assert_eq!(Self::SIZE, hdr_len); 123 | 124 | let packet = &mut buf[..len]; 125 | 126 | let checksum = Self::checksum(packet, src, dst); 127 | self.sum = checksum; 128 | 129 | Self::inject_checksum(packet, checksum); 130 | 131 | Ok(packet) 132 | } 133 | 134 | /// Decodes the provided packet into a header and a payload slice 135 | pub fn decode_with_payload( 136 | packet: &[u8], 137 | src: Ipv4Addr, 138 | dst: Ipv4Addr, 139 | filter_src: Option, 140 | filter_dst: Option, 141 | ) -> Result, Error> { 142 | let hdr = Self::decode(packet)?; 143 | 144 | if let Some(filter_src) = filter_src { 145 | if filter_src != hdr.src { 146 | return Ok(None); 147 | } 148 | } 149 | 150 | if let Some(filter_dst) = filter_dst { 151 | if filter_dst != hdr.dst { 152 | return Ok(None); 153 | } 154 | } 155 | 156 | let len = hdr.len as usize; 157 | if packet.len() < len { 158 | Err(Error::DataUnderflow)?; 159 | } 160 | 161 | let checksum = Self::checksum(&packet[..len], src, dst); 162 | 163 | trace!( 164 | "UDP header decoded, src={}, dst={}, size={}, checksum={}, ours={}", 165 | hdr.src, 166 | hdr.dst, 167 | hdr.len, 168 | hdr.sum, 169 | checksum 170 | ); 171 | 172 | if checksum != hdr.sum { 173 | Err(Error::InvalidChecksum)?; 174 | } 175 | 176 | let packet = &packet[..len]; 177 | 178 | let payload_data = &packet[Self::SIZE..]; 179 | 180 | Ok(Some((hdr, payload_data))) 181 | } 182 | 183 | /// Injects the checksum into the provided packet 184 | pub fn inject_checksum(packet: &mut [u8], checksum: u16) { 185 | let checksum = checksum.to_be_bytes(); 186 | 187 | let offset = Self::CHECKSUM_WORD << 1; 188 | packet[offset] = checksum[0]; 189 | packet[offset + 1] = checksum[1]; 190 | } 191 | 192 | /// Computes the checksum for an already encoded packet 193 | pub fn checksum(packet: &[u8], src: Ipv4Addr, dst: Ipv4Addr) -> u16 { 194 | let mut buf = [0; 12]; 195 | 196 | // Pseudo IP-header for UDP checksum calculation 197 | let len = unwrap!( 198 | unwrap!( 199 | unwrap!( 200 | unwrap!( 201 | unwrap!( 202 | BytesOut::new(&mut buf).push(&u32::to_be_bytes(src.into())), 203 | "Unreachable" 204 | ) 205 | .push(&u32::to_be_bytes(dst.into())), 206 | "Unreachable" 207 | ) 208 | .byte(0), 209 | "Unreachable" 210 | ) 211 | .byte(UdpPacketHeader::PROTO), 212 | "Unreachable" 213 | ) 214 | .push(&u16::to_be_bytes(packet.len() as u16)), 215 | "Unreachable" 216 | ) 217 | .len(); 218 | 219 | let sum = checksum_accumulate(&buf[..len], usize::MAX) 220 | + checksum_accumulate(packet, Self::CHECKSUM_WORD); 221 | 222 | checksum_finish(sum) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /edge-ws/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.0] - 2025-05-29 9 | * Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): 10 | * `log` - uses the `log` crate for all logging 11 | * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` 12 | * Respect payload length of control messages 13 | 14 | ## [0.4.0] - 2025-01-02 15 | * Option to erase the generics from the IO errors 16 | 17 | ## [0.3.0] - 2024-09-10 18 | * Migrated to the `edge-nal` traits 19 | * New method, `FrameHeader::mask_with` that takes a user-supplied mask 20 | -------------------------------------------------------------------------------- /edge-ws/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edge-ws" 3 | version = "0.5.0" 4 | edition = "2021" 5 | rust-version = "1.77" 6 | description = "Async + `no_std` + no-alloc implementation of the Websockets protocol" 7 | repository = "https://github.com/ivmarkov/edge-net" 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | categories = [ 11 | "embedded", 12 | "no-std::no-alloc", 13 | "asynchronous", 14 | "network-programming", 15 | "web-programming::websocket" 16 | ] 17 | 18 | [features] 19 | default = ["io"] 20 | std = ["io"] 21 | io = ["embedded-io-async"] 22 | defmt = ["dep:defmt", "embedded-svc?/defmt"] 23 | 24 | [dependencies] 25 | log = { version = "0.4", default-features = false, optional = true } 26 | defmt = { version = "0.3", optional = true } 27 | embedded-io-async = { workspace = true, optional = true } 28 | embedded-svc = { workspace = true, optional = true, default-features = false } 29 | -------------------------------------------------------------------------------- /edge-ws/README.md: -------------------------------------------------------------------------------- 1 | # edge-ws 2 | 3 | [![CI](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/edge-net/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/edge-net.svg) 5 | [![Documentation](https://docs.rs/edge-net/badge.svg)](https://docs.rs/edge-net) 6 | 7 | Async + `no_std` + no-alloc implementation of the Websockets protocol. 8 | 9 | For other protocols, look at the [edge-net](https://github.com/ivmarkov/edge-net) aggregator crate documentation. 10 | 11 | ## Examples 12 | 13 | **NOTE** 14 | 15 | To connect the Websocket client example to the Websocket server example - rather that to the public Websocket echo server, 16 | just run it with some argument, i.e. 17 | 18 | ```sh 19 | ./target/debug/examples/ws_client 1 20 | ``` 21 | 22 | ### Websocket client 23 | 24 | ```rust 25 | use core::net::SocketAddr; 26 | 27 | use anyhow::bail; 28 | 29 | use edge_http::io::client::Connection; 30 | use edge_http::ws::{MAX_BASE64_KEY_LEN, MAX_BASE64_KEY_RESPONSE_LEN, NONCE_LEN}; 31 | use edge_nal::{AddrType, Dns, TcpConnect}; 32 | use edge_ws::{FrameHeader, FrameType}; 33 | 34 | use rand::{thread_rng, RngCore}; 35 | 36 | use log::*; 37 | 38 | // NOTE: HTTP-only echo WS servers seem to be hard to find, this one might or might not work... 39 | const PUBLIC_ECHO_SERVER: (&str, u16, &str) = ("websockets.chilkat.io", 80, "/wsChilkatEcho.ashx"); 40 | const OUR_ECHO_SERVER: (&str, u16, &str) = ("127.0.0.1", 8881, "/"); 41 | 42 | fn main() { 43 | env_logger::init_from_env( 44 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 45 | ); 46 | 47 | let stack = edge_nal_std::Stack::new(); 48 | 49 | let mut buf = [0_u8; 8192]; 50 | 51 | futures_lite::future::block_on(work(&stack, &mut buf)).unwrap(); 52 | } 53 | 54 | async fn work(stack: &T, buf: &mut [u8]) -> Result<(), anyhow::Error> 55 | where 56 | ::Error: Send + Sync + std::error::Error + 'static, 57 | ::Error: Send + Sync + std::error::Error + 'static, 58 | { 59 | let mut args = std::env::args(); 60 | args.next(); // Skip the executable name 61 | 62 | let (fqdn, port, path) = if args.next().is_some() { 63 | OUR_ECHO_SERVER 64 | } else { 65 | PUBLIC_ECHO_SERVER 66 | }; 67 | 68 | info!("About to open an HTTP connection to {fqdn} port {port}"); 69 | 70 | let ip = stack.get_host_by_name(fqdn, AddrType::IPv4).await?; 71 | 72 | let mut conn: Connection<_> = Connection::new(buf, stack, SocketAddr::new(ip, port)); 73 | 74 | let mut rng_source = thread_rng(); 75 | 76 | let mut nonce = [0_u8; NONCE_LEN]; 77 | rng_source.fill_bytes(&mut nonce); 78 | 79 | let mut buf = [0_u8; MAX_BASE64_KEY_LEN]; 80 | conn.initiate_ws_upgrade_request(Some(fqdn), Some("foo.com"), path, None, &nonce, &mut buf) 81 | .await?; 82 | conn.initiate_response().await?; 83 | 84 | let mut buf = [0_u8; MAX_BASE64_KEY_RESPONSE_LEN]; 85 | if !conn.is_ws_upgrade_accepted(&nonce, &mut buf)? { 86 | bail!("WS upgrade failed"); 87 | } 88 | 89 | conn.complete().await?; 90 | 91 | // Now we have the TCP socket in a state where it can be operated as a WS connection 92 | // Send some traffic to a WS echo server and read it back 93 | 94 | let (mut socket, buf) = conn.release(); 95 | 96 | info!("Connection upgraded to WS, starting traffic now"); 97 | 98 | for payload in ["Hello world!", "How are you?", "I'm fine, thanks!"] { 99 | let header = FrameHeader { 100 | frame_type: FrameType::Text(false), 101 | payload_len: payload.len() as _, 102 | mask_key: rng_source.next_u32().into(), 103 | }; 104 | 105 | info!("Sending {header}, with payload \"{payload}\""); 106 | header.send(&mut socket).await?; 107 | header.send_payload(&mut socket, payload.as_bytes()).await?; 108 | 109 | let header = FrameHeader::recv(&mut socket).await?; 110 | let payload = header.recv_payload(&mut socket, buf).await?; 111 | 112 | match header.frame_type { 113 | FrameType::Text(_) => { 114 | info!( 115 | "Got {header}, with payload \"{}\"", 116 | core::str::from_utf8(payload).unwrap() 117 | ); 118 | } 119 | FrameType::Binary(_) => { 120 | info!("Got {header}, with payload {payload:?}"); 121 | } 122 | _ => { 123 | bail!("Unexpected {}", header); 124 | } 125 | } 126 | 127 | if !header.frame_type.is_final() { 128 | bail!("Unexpected fragmented frame"); 129 | } 130 | } 131 | 132 | // Inform the server we are closing the connection 133 | 134 | let header = FrameHeader { 135 | frame_type: FrameType::Close, 136 | payload_len: 0, 137 | mask_key: rng_source.next_u32().into(), 138 | }; 139 | 140 | info!("Closing"); 141 | 142 | header.send(&mut socket).await?; 143 | 144 | Ok(()) 145 | } 146 | ``` 147 | 148 | ### Websocket echo server 149 | 150 | ```rust 151 | use core::fmt::{Debug, Display}; 152 | 153 | use edge_http::io::server::{Connection, DefaultServer, Handler}; 154 | use edge_http::io::Error; 155 | use edge_http::ws::MAX_BASE64_KEY_RESPONSE_LEN; 156 | use edge_http::Method; 157 | use edge_nal::TcpBind; 158 | use edge_ws::{FrameHeader, FrameType}; 159 | 160 | use embedded_io_async::{Read, Write}; 161 | 162 | use log::info; 163 | 164 | fn main() { 165 | env_logger::init_from_env( 166 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 167 | ); 168 | 169 | let mut server = DefaultServer::new(); 170 | 171 | futures_lite::future::block_on(run(&mut server)).unwrap(); 172 | } 173 | 174 | pub async fn run(server: &mut DefaultServer) -> Result<(), anyhow::Error> { 175 | let addr = "0.0.0.0:8881"; 176 | 177 | info!("Running HTTP server on {addr}"); 178 | 179 | let acceptor = edge_nal_std::Stack::new() 180 | .bind(addr.parse().unwrap()) 181 | .await?; 182 | 183 | server.run(None, acceptor, WsHandler).await?; 184 | 185 | Ok(()) 186 | } 187 | 188 | #[derive(Debug)] 189 | enum WsHandlerError { 190 | Connection(C), 191 | Ws(W), 192 | } 193 | 194 | impl From for WsHandlerError { 195 | fn from(e: C) -> Self { 196 | Self::Connection(e) 197 | } 198 | } 199 | 200 | struct WsHandler; 201 | 202 | impl Handler for WsHandler { 203 | type Error 204 | = WsHandlerError, edge_ws::Error> 205 | where 206 | E: Debug; 207 | 208 | async fn handle( 209 | &self, 210 | _task_id: impl Display + Clone, 211 | conn: &mut Connection<'_, T, N>, 212 | ) -> Result<(), Self::Error> 213 | where 214 | T: Read + Write, 215 | { 216 | let headers = conn.headers()?; 217 | 218 | if headers.method != Method::Get { 219 | conn.initiate_response(405, Some("Method Not Allowed"), &[]) 220 | .await?; 221 | } else if headers.path != "/" { 222 | conn.initiate_response(404, Some("Not Found"), &[]).await?; 223 | } else if !conn.is_ws_upgrade_request()? { 224 | conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/plain")]) 225 | .await?; 226 | 227 | conn.write_all(b"Initiate WS Upgrade request to switch this connection to WS") 228 | .await?; 229 | } else { 230 | let mut buf = [0_u8; MAX_BASE64_KEY_RESPONSE_LEN]; 231 | conn.initiate_ws_upgrade_response(&mut buf).await?; 232 | 233 | conn.complete().await?; 234 | 235 | info!("Connection upgraded to WS, starting a simple WS echo server now"); 236 | 237 | // Now we have the TCP socket in a state where it can be operated as a WS connection 238 | // Run a simple WS echo server here 239 | 240 | let mut socket = conn.unbind()?; 241 | 242 | let mut buf = [0_u8; 8192]; 243 | 244 | loop { 245 | let mut header = FrameHeader::recv(&mut socket) 246 | .await 247 | .map_err(WsHandlerError::Ws)?; 248 | let payload = header 249 | .recv_payload(&mut socket, &mut buf) 250 | .await 251 | .map_err(WsHandlerError::Ws)?; 252 | 253 | match header.frame_type { 254 | FrameType::Text(_) => { 255 | info!( 256 | "Got {header}, with payload \"{}\"", 257 | core::str::from_utf8(payload).unwrap() 258 | ); 259 | } 260 | FrameType::Binary(_) => { 261 | info!("Got {header}, with payload {payload:?}"); 262 | } 263 | FrameType::Close => { 264 | info!("Got {header}, client closed the connection cleanly"); 265 | break; 266 | } 267 | _ => { 268 | info!("Got {header}"); 269 | } 270 | } 271 | 272 | // Echo it back now 273 | 274 | header.mask_key = None; // Servers never mask the payload 275 | 276 | if matches!(header.frame_type, FrameType::Ping) { 277 | header.frame_type = FrameType::Pong; 278 | } 279 | 280 | info!("Echoing back as {header}"); 281 | 282 | header.send(&mut socket).await.map_err(WsHandlerError::Ws)?; 283 | header 284 | .send_payload(&mut socket, payload) 285 | .await 286 | .map_err(WsHandlerError::Ws)?; 287 | } 288 | } 289 | 290 | Ok(()) 291 | } 292 | } 293 | ``` 294 | -------------------------------------------------------------------------------- /edge-ws/src/fmt.rs: -------------------------------------------------------------------------------- 1 | //! A module that re-exports the `log` macros if `defmt` is not enabled, or `defmt` macros if `defmt` is enabled. 2 | //! 3 | //! The module also defines: 4 | //! - Custom versions of the core assert macros (`assert!`, `assert_eq!`, etc.) that use the `defmt` macros if the `defmt` feature is enabled. 5 | //! - Custom versions of the `panic!`, `todo!`, and `unreachable!` macros that use the `defmt` macros if the `defmt` feature is enabled. 6 | //! - A custom `unwrap!` macro that uses the `defmt` macros if the `defmt` feature is enabled, otherwise it uses the standard library's `unwrap` method. 7 | //! - A custom `Bytes` struct that formats byte slices as hex in a way compatible with `defmt`. 8 | #![macro_use] 9 | #![allow(unused)] 10 | 11 | use core::fmt::{Debug, Display, LowerHex}; 12 | 13 | #[collapse_debuginfo(yes)] 14 | macro_rules! assert { 15 | ($($x:tt)*) => { 16 | { 17 | #[cfg(not(feature = "defmt"))] 18 | ::core::assert!($($x)*); 19 | #[cfg(feature = "defmt")] 20 | ::defmt::assert!($($x)*); 21 | } 22 | }; 23 | } 24 | 25 | #[collapse_debuginfo(yes)] 26 | macro_rules! assert_eq { 27 | ($($x:tt)*) => { 28 | { 29 | #[cfg(not(feature = "defmt"))] 30 | ::core::assert_eq!($($x)*); 31 | #[cfg(feature = "defmt")] 32 | ::defmt::assert_eq!($($x)*); 33 | } 34 | }; 35 | } 36 | 37 | #[collapse_debuginfo(yes)] 38 | macro_rules! assert_ne { 39 | ($($x:tt)*) => { 40 | { 41 | #[cfg(not(feature = "defmt"))] 42 | ::core::assert_ne!($($x)*); 43 | #[cfg(feature = "defmt")] 44 | ::defmt::assert_ne!($($x)*); 45 | } 46 | }; 47 | } 48 | 49 | #[collapse_debuginfo(yes)] 50 | macro_rules! debug_assert { 51 | ($($x:tt)*) => { 52 | { 53 | #[cfg(not(feature = "defmt"))] 54 | ::core::debug_assert!($($x)*); 55 | #[cfg(feature = "defmt")] 56 | ::defmt::debug_assert!($($x)*); 57 | } 58 | }; 59 | } 60 | 61 | #[collapse_debuginfo(yes)] 62 | macro_rules! debug_assert_eq { 63 | ($($x:tt)*) => { 64 | { 65 | #[cfg(not(feature = "defmt"))] 66 | ::core::debug_assert_eq!($($x)*); 67 | #[cfg(feature = "defmt")] 68 | ::defmt::debug_assert_eq!($($x)*); 69 | } 70 | }; 71 | } 72 | 73 | #[collapse_debuginfo(yes)] 74 | macro_rules! debug_assert_ne { 75 | ($($x:tt)*) => { 76 | { 77 | #[cfg(not(feature = "defmt"))] 78 | ::core::debug_assert_ne!($($x)*); 79 | #[cfg(feature = "defmt")] 80 | ::defmt::debug_assert_ne!($($x)*); 81 | } 82 | }; 83 | } 84 | 85 | #[collapse_debuginfo(yes)] 86 | macro_rules! todo { 87 | ($($x:tt)*) => { 88 | { 89 | #[cfg(not(feature = "defmt"))] 90 | ::core::todo!($($x)*); 91 | #[cfg(feature = "defmt")] 92 | ::defmt::todo!($($x)*); 93 | } 94 | }; 95 | } 96 | 97 | #[collapse_debuginfo(yes)] 98 | macro_rules! unreachable { 99 | ($($x:tt)*) => { 100 | { 101 | #[cfg(not(feature = "defmt"))] 102 | ::core::unreachable!($($x)*); 103 | #[cfg(feature = "defmt")] 104 | ::defmt::unreachable!($($x)*); 105 | } 106 | }; 107 | } 108 | 109 | #[collapse_debuginfo(yes)] 110 | macro_rules! panic { 111 | ($($x:tt)*) => { 112 | { 113 | #[cfg(not(feature = "defmt"))] 114 | ::core::panic!($($x)*); 115 | #[cfg(feature = "defmt")] 116 | ::defmt::panic!($($x)*); 117 | } 118 | }; 119 | } 120 | 121 | #[collapse_debuginfo(yes)] 122 | macro_rules! trace { 123 | ($s:literal $(, $x:expr)* $(,)?) => { 124 | { 125 | #[cfg(feature = "log")] 126 | ::log::trace!($s $(, $x)*); 127 | #[cfg(feature = "defmt")] 128 | ::defmt::trace!($s $(, $x)*); 129 | #[cfg(not(any(feature = "log", feature="defmt")))] 130 | let _ = ($( & $x ),*); 131 | } 132 | }; 133 | } 134 | 135 | #[collapse_debuginfo(yes)] 136 | macro_rules! debug { 137 | ($s:literal $(, $x:expr)* $(,)?) => { 138 | { 139 | #[cfg(feature = "log")] 140 | ::log::debug!($s $(, $x)*); 141 | #[cfg(feature = "defmt")] 142 | ::defmt::debug!($s $(, $x)*); 143 | #[cfg(not(any(feature = "log", feature="defmt")))] 144 | let _ = ($( & $x ),*); 145 | } 146 | }; 147 | } 148 | 149 | #[collapse_debuginfo(yes)] 150 | macro_rules! info { 151 | ($s:literal $(, $x:expr)* $(,)?) => { 152 | { 153 | #[cfg(feature = "log")] 154 | ::log::info!($s $(, $x)*); 155 | #[cfg(feature = "defmt")] 156 | ::defmt::info!($s $(, $x)*); 157 | #[cfg(not(any(feature = "log", feature="defmt")))] 158 | let _ = ($( & $x ),*); 159 | } 160 | }; 161 | } 162 | 163 | #[collapse_debuginfo(yes)] 164 | macro_rules! warn { 165 | ($s:literal $(, $x:expr)* $(,)?) => { 166 | { 167 | #[cfg(feature = "log")] 168 | ::log::warn!($s $(, $x)*); 169 | #[cfg(feature = "defmt")] 170 | ::defmt::warn!($s $(, $x)*); 171 | #[cfg(not(any(feature = "log", feature="defmt")))] 172 | let _ = ($( & $x ),*); 173 | } 174 | }; 175 | } 176 | 177 | #[collapse_debuginfo(yes)] 178 | macro_rules! error { 179 | ($s:literal $(, $x:expr)* $(,)?) => { 180 | { 181 | #[cfg(feature = "log")] 182 | ::log::error!($s $(, $x)*); 183 | #[cfg(feature = "defmt")] 184 | ::defmt::error!($s $(, $x)*); 185 | #[cfg(not(any(feature = "log", feature="defmt")))] 186 | let _ = ($( & $x ),*); 187 | } 188 | }; 189 | } 190 | 191 | #[cfg(feature = "defmt")] 192 | #[collapse_debuginfo(yes)] 193 | macro_rules! unwrap { 194 | ($($x:tt)*) => { 195 | ::defmt::unwrap!($($x)*) 196 | }; 197 | } 198 | 199 | #[cfg(not(feature = "defmt"))] 200 | #[collapse_debuginfo(yes)] 201 | macro_rules! unwrap { 202 | ($arg:expr) => { 203 | match $crate::fmt::Try::into_result($arg) { 204 | ::core::result::Result::Ok(t) => t, 205 | ::core::result::Result::Err(e) => { 206 | ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); 207 | } 208 | } 209 | }; 210 | ($arg:expr, $($msg:expr),+ $(,)? ) => { 211 | match $crate::fmt::Try::into_result($arg) { 212 | ::core::result::Result::Ok(t) => t, 213 | ::core::result::Result::Err(e) => { 214 | ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); 215 | } 216 | } 217 | } 218 | } 219 | 220 | #[cfg(feature = "defmt")] 221 | #[collapse_debuginfo(yes)] 222 | macro_rules! write_unwrap { 223 | ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { 224 | { 225 | unwrap!(write!($f, $s $(, $x)*).map_err($crate::fmt::FmtError)); 226 | } 227 | }; 228 | } 229 | 230 | #[cfg(not(feature = "defmt"))] 231 | #[collapse_debuginfo(yes)] 232 | macro_rules! write_unwrap { 233 | ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { 234 | { 235 | unwrap!(write!($f, $s $(, $x)*)); 236 | } 237 | }; 238 | } 239 | 240 | #[cfg(feature = "defmt")] 241 | #[collapse_debuginfo(yes)] 242 | macro_rules! display2format { 243 | ($arg:expr) => { 244 | ::defmt::Display2Format(&$arg) 245 | }; 246 | } 247 | 248 | #[cfg(not(feature = "defmt"))] 249 | #[collapse_debuginfo(yes)] 250 | macro_rules! display2format { 251 | ($arg:expr) => { 252 | $arg 253 | }; 254 | } 255 | 256 | #[cfg(feature = "defmt")] 257 | #[collapse_debuginfo(yes)] 258 | macro_rules! debug2format { 259 | ($arg:expr) => { 260 | ::defmt::Debug2Format(&$arg) 261 | }; 262 | } 263 | 264 | #[cfg(not(feature = "defmt"))] 265 | #[collapse_debuginfo(yes)] 266 | macro_rules! debug2format { 267 | ($arg:expr) => { 268 | $arg 269 | }; 270 | } 271 | 272 | /// A way to `{:x?}` format a byte slice which is compatible with `defmt` 273 | pub struct Bytes<'a>(pub &'a [u8]); 274 | 275 | impl Debug for Bytes<'_> { 276 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 277 | write!(f, "{:#02x?}", self.0) 278 | } 279 | } 280 | 281 | impl Display for Bytes<'_> { 282 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 283 | write!(f, "{:#02x?}", self.0) 284 | } 285 | } 286 | 287 | impl LowerHex for Bytes<'_> { 288 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 289 | write!(f, "{:#02x?}", self.0) 290 | } 291 | } 292 | 293 | #[cfg(feature = "defmt")] 294 | impl defmt::Format for Bytes<'_> { 295 | fn format(&self, fmt: defmt::Formatter) { 296 | defmt::write!(fmt, "{:02x}", self.0) 297 | } 298 | } 299 | 300 | /// Support for the `unwrap!` macro for `Option` and `Result`. 301 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 302 | pub struct NoneError; 303 | 304 | /// Support for the `unwrap!` macro for `Option` and `Result`. 305 | pub trait Try { 306 | type Ok; 307 | type Error; 308 | #[allow(unused)] 309 | fn into_result(self) -> Result; 310 | } 311 | 312 | impl Try for Option { 313 | type Ok = T; 314 | type Error = NoneError; 315 | 316 | #[inline] 317 | fn into_result(self) -> Result { 318 | self.ok_or(NoneError) 319 | } 320 | } 321 | 322 | impl Try for Result { 323 | type Ok = T; 324 | type Error = E; 325 | 326 | #[inline] 327 | fn into_result(self) -> Self { 328 | self 329 | } 330 | } 331 | 332 | #[cfg(feature = "defmt")] 333 | pub(crate) struct FmtError(pub(crate) core::fmt::Error); 334 | 335 | #[cfg(feature = "defmt")] 336 | impl defmt::Format for FmtError { 337 | fn format(&self, f: defmt::Formatter<'_>) { 338 | defmt::write!(f, "{}", "FmtError") 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /edge-ws/src/io.rs: -------------------------------------------------------------------------------- 1 | use core::cmp::min; 2 | 3 | use embedded_io_async::{self, Read, ReadExactError, Write}; 4 | 5 | use super::*; 6 | 7 | #[cfg(feature = "embedded-svc")] 8 | pub use embedded_svc_compat::*; 9 | 10 | pub type Error = super::Error; 11 | 12 | impl Error 13 | where 14 | E: embedded_io_async::Error, 15 | { 16 | pub fn erase(&self) -> Error { 17 | match self { 18 | Self::Incomplete(size) => Error::Incomplete(*size), 19 | Self::Invalid => Error::Invalid, 20 | Self::BufferOverflow => Error::BufferOverflow, 21 | Self::InvalidLen => Error::InvalidLen, 22 | Self::Io(e) => Error::Io(e.kind()), 23 | } 24 | } 25 | } 26 | 27 | impl From> for Error { 28 | fn from(e: ReadExactError) -> Self { 29 | match e { 30 | ReadExactError::UnexpectedEof => Error::Invalid, 31 | ReadExactError::Other(e) => Error::Io(e), 32 | } 33 | } 34 | } 35 | 36 | impl FrameHeader { 37 | pub async fn recv(mut read: R) -> Result> 38 | where 39 | R: Read, 40 | { 41 | let mut header_buf = [0; FrameHeader::MAX_LEN]; 42 | let mut read_offset = 0; 43 | let mut read_end = FrameHeader::MIN_LEN; 44 | 45 | loop { 46 | read.read_exact(&mut header_buf[read_offset..read_end]) 47 | .await 48 | .map_err(Error::from)?; 49 | 50 | match FrameHeader::deserialize(&header_buf[..read_end]) { 51 | Ok((header, _)) => return Ok(header), 52 | Err(Error::Incomplete(more)) => { 53 | read_offset = read_end; 54 | read_end += more; 55 | } 56 | Err(e) => return Err(e.recast()), 57 | } 58 | } 59 | } 60 | 61 | pub async fn send(&self, mut write: W) -> Result<(), Error> 62 | where 63 | W: Write, 64 | { 65 | let mut header_buf = [0; FrameHeader::MAX_LEN]; 66 | let header_len = unwrap!(self.serialize(&mut header_buf)); 67 | 68 | write 69 | .write_all(&header_buf[..header_len]) 70 | .await 71 | .map_err(Error::Io) 72 | } 73 | 74 | pub async fn recv_payload<'a, R>( 75 | &self, 76 | mut read: R, 77 | payload_buf: &'a mut [u8], 78 | ) -> Result<&'a [u8], Error> 79 | where 80 | R: Read, 81 | { 82 | if (payload_buf.len() as u64) < self.payload_len { 83 | Err(Error::BufferOverflow) 84 | } else if self.payload_len == 0 { 85 | Ok(&[]) 86 | } else { 87 | let payload = &mut payload_buf[..self.payload_len as _]; 88 | 89 | read.read_exact(payload).await.map_err(Error::from)?; 90 | 91 | self.mask(payload, 0); 92 | 93 | Ok(payload) 94 | } 95 | } 96 | 97 | pub async fn send_payload<'a, W>( 98 | &'a self, 99 | mut write: W, 100 | payload: &'a [u8], 101 | ) -> Result<(), Error> 102 | where 103 | W: Write, 104 | { 105 | let payload_buf_len = payload.len() as u64; 106 | 107 | if payload_buf_len != self.payload_len { 108 | Err(Error::InvalidLen) 109 | } else if payload.is_empty() { 110 | Ok(()) 111 | } else if self.mask_key.is_none() { 112 | write.write_all(payload).await.map_err(Error::Io) 113 | } else { 114 | let mut buf = [0_u8; 32]; 115 | 116 | let mut offset = 0; 117 | 118 | while offset < payload.len() { 119 | let len = min(buf.len(), payload.len() - offset); 120 | 121 | let buf = &mut buf[..len]; 122 | 123 | buf.copy_from_slice(&payload[offset..offset + len]); 124 | 125 | self.mask(buf, offset); 126 | 127 | write.write_all(buf).await.map_err(Error::Io)?; 128 | 129 | offset += len; 130 | } 131 | 132 | Ok(()) 133 | } 134 | } 135 | } 136 | 137 | pub async fn recv( 138 | mut read: R, 139 | frame_data_buf: &mut [u8], 140 | ) -> Result<(FrameType, usize), Error> 141 | where 142 | R: Read, 143 | { 144 | let header = FrameHeader::recv(&mut read).await?; 145 | header.recv_payload(read, frame_data_buf).await?; 146 | 147 | Ok((header.frame_type, header.payload_len as _)) 148 | } 149 | 150 | pub async fn send( 151 | mut write: W, 152 | frame_type: FrameType, 153 | mask_key: Option, 154 | frame_data_buf: &[u8], 155 | ) -> Result<(), Error> 156 | where 157 | W: Write, 158 | { 159 | let header = FrameHeader { 160 | frame_type, 161 | payload_len: frame_data_buf.len() as _, 162 | mask_key, 163 | }; 164 | 165 | header.send(&mut write).await?; 166 | header.send_payload(write, frame_data_buf).await 167 | } 168 | 169 | #[cfg(feature = "embedded-svc")] 170 | mod embedded_svc_compat { 171 | use core::convert::TryInto; 172 | 173 | use embedded_io_async::{Read, Write}; 174 | use embedded_svc::io::ErrorType as IoErrorType; 175 | use embedded_svc::ws::asynch::Sender; 176 | use embedded_svc::ws::ErrorType; 177 | use embedded_svc::ws::{asynch::Receiver, FrameType}; 178 | 179 | use super::Error; 180 | 181 | pub struct WsConnection(T, M); 182 | 183 | impl WsConnection { 184 | pub const fn new(connection: T, mask_gen: M) -> Self { 185 | Self(connection, mask_gen) 186 | } 187 | } 188 | 189 | impl ErrorType for WsConnection 190 | where 191 | T: IoErrorType, 192 | { 193 | type Error = Error; 194 | } 195 | 196 | impl Receiver for WsConnection 197 | where 198 | T: Read, 199 | { 200 | async fn recv( 201 | &mut self, 202 | frame_data_buf: &mut [u8], 203 | ) -> Result<(FrameType, usize), Self::Error> { 204 | super::recv(&mut self.0, frame_data_buf) 205 | .await 206 | .map(|(frame_type, payload_len)| (frame_type.into(), payload_len)) 207 | } 208 | } 209 | 210 | impl Sender for WsConnection 211 | where 212 | T: Write, 213 | M: Fn() -> Option, 214 | { 215 | async fn send( 216 | &mut self, 217 | frame_type: FrameType, 218 | frame_data: &[u8], 219 | ) -> Result<(), Self::Error> { 220 | super::send( 221 | &mut self.0, 222 | unwrap!(frame_type.try_into(), "Invalid frame type"), 223 | (self.1)(), 224 | frame_data, 225 | ) 226 | .await 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /examples/captive_portal.rs: -------------------------------------------------------------------------------- 1 | use core::net::{IpAddr, Ipv4Addr, SocketAddr}; 2 | use core::time::Duration; 3 | 4 | use edge_captive::io::run; 5 | 6 | use log::*; 7 | 8 | fn main() { 9 | env_logger::init_from_env( 10 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 11 | ); 12 | 13 | let stack = edge_nal_std::Stack::new(); 14 | 15 | let mut tx_buf = [0; 1500]; 16 | let mut rx_buf = [0; 1500]; 17 | 18 | info!("Running Captive Portal DNS on UDP port 8853..."); 19 | 20 | futures_lite::future::block_on(run( 21 | &stack, 22 | // Can't use DEFAULT_SOCKET because it uses DNS port 53 which needs root 23 | SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8853), 24 | &mut tx_buf, 25 | &mut rx_buf, 26 | Ipv4Addr::new(192, 168, 0, 1), 27 | Duration::from_secs(60), 28 | )) 29 | .unwrap(); 30 | } 31 | -------------------------------------------------------------------------------- /examples/dhcp_client.rs: -------------------------------------------------------------------------------- 1 | //! NOTE: Run this example with `sudo` to be able to bind to the interface, as it uses raw sockets which require root privileges. 2 | 3 | use core::net::{Ipv4Addr, SocketAddrV4}; 4 | 5 | use edge_dhcp::client::Client; 6 | use edge_dhcp::io::{client::Lease, DEFAULT_CLIENT_PORT, DEFAULT_SERVER_PORT}; 7 | use edge_nal::{MacAddr, RawBind}; 8 | use edge_raw::io::RawSocket2Udp; 9 | 10 | use log::info; 11 | 12 | fn main() { 13 | env_logger::init_from_env( 14 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 15 | ); 16 | 17 | futures_lite::future::block_on(run( 18 | 2, // The interface index of the interface (e.g. eno0) to use; run `ip addr` to see it 19 | [0x4c, 0xcc, 0x6a, 0xa2, 0x23, 0xf5], // Your MAC addr here; run `ip addr` to see it 20 | )) 21 | .unwrap(); 22 | } 23 | 24 | async fn run(if_index: u32, if_mac: MacAddr) -> Result<(), anyhow::Error> { 25 | let mut client = Client::new(rand::thread_rng(), if_mac); 26 | 27 | let stack = edge_nal_std::Interface::new(if_index); 28 | let mut buf = [0; 1500]; 29 | 30 | loop { 31 | let mut socket: RawSocket2Udp<_> = RawSocket2Udp::new( 32 | stack.bind().await?, 33 | Some(SocketAddrV4::new( 34 | Ipv4Addr::UNSPECIFIED, 35 | DEFAULT_CLIENT_PORT, 36 | )), 37 | Some(SocketAddrV4::new( 38 | Ipv4Addr::UNSPECIFIED, 39 | DEFAULT_SERVER_PORT, 40 | )), 41 | [255; 6], // Broadcast 42 | ); 43 | 44 | let (mut lease, options) = Lease::new(&mut client, &mut socket, &mut buf).await?; 45 | 46 | info!("Got lease {lease:?} with options {options:?}"); 47 | 48 | info!("Entering an endless loop to keep the lease..."); 49 | 50 | lease.keep(&mut client, &mut socket, &mut buf).await?; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/dhcp_server.rs: -------------------------------------------------------------------------------- 1 | //! NOTE: Run this example with `sudo` to be able to bind to the interface, as it uses raw sockets which require root privileges. 2 | 3 | use core::net::{Ipv4Addr, SocketAddrV4}; 4 | 5 | use edge_dhcp::io::{self, DEFAULT_CLIENT_PORT, DEFAULT_SERVER_PORT}; 6 | use edge_dhcp::server::{Server, ServerOptions}; 7 | use edge_nal::RawBind; 8 | use edge_raw::io::RawSocket2Udp; 9 | 10 | fn main() { 11 | env_logger::init_from_env( 12 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 13 | ); 14 | 15 | futures_lite::future::block_on(run( 16 | 0, // The interface index of the interface (e.g. eno0) to use; run `ip addr` to see it 17 | )) 18 | .unwrap(); 19 | } 20 | 21 | async fn run(if_index: u32) -> Result<(), anyhow::Error> { 22 | let stack = edge_nal_std::Interface::new(if_index); 23 | 24 | let mut buf = [0; 1500]; 25 | 26 | let ip = Ipv4Addr::new(192, 168, 0, 1); 27 | 28 | let mut socket: RawSocket2Udp<_> = RawSocket2Udp::new( 29 | stack.bind().await?, 30 | Some(SocketAddrV4::new( 31 | Ipv4Addr::UNSPECIFIED, 32 | DEFAULT_SERVER_PORT, 33 | )), 34 | Some(SocketAddrV4::new( 35 | Ipv4Addr::UNSPECIFIED, 36 | DEFAULT_CLIENT_PORT, 37 | )), 38 | [0; 6], 39 | ); 40 | 41 | let mut gw_buf = [Ipv4Addr::UNSPECIFIED]; 42 | 43 | io::server::run( 44 | &mut Server::<_, 64>::new_with_et(ip), // Will give IP addresses in the range 192.168.0.50 - 192.168.0.200, subnet 255.255.255.0 45 | &ServerOptions::new(ip, Some(&mut gw_buf)), 46 | &mut socket, 47 | &mut buf, 48 | ) 49 | .await?; 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /examples/http_client.rs: -------------------------------------------------------------------------------- 1 | use core::net::SocketAddr; 2 | 3 | use embedded_io_async::Read; 4 | 5 | use edge_http::io::{client::Connection, Error}; 6 | use edge_http::Method; 7 | use edge_nal::{AddrType, Dns, TcpConnect}; 8 | 9 | use log::*; 10 | 11 | fn main() { 12 | env_logger::init_from_env( 13 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 14 | ); 15 | 16 | let stack: edge_nal_std::Stack = Default::default(); 17 | 18 | let mut buf = [0_u8; 8192]; 19 | 20 | futures_lite::future::block_on(read(&stack, &mut buf)).unwrap(); 21 | } 22 | 23 | async fn read( 24 | stack: &T, 25 | buf: &mut [u8], 26 | ) -> Result<(), Error<::Error>> 27 | where 28 | ::Error: Into<::Error>, 29 | { 30 | info!("About to open an HTTP connection to httpbin.org port 80"); 31 | 32 | let ip = stack 33 | .get_host_by_name("httpbin.org", AddrType::IPv4) 34 | .await 35 | .map_err(|e| Error::Io(e.into()))?; 36 | 37 | let mut conn: Connection<_> = Connection::new(buf, stack, SocketAddr::new(ip, 80)); 38 | 39 | for uri in ["/ip", "/headers"] { 40 | request(&mut conn, uri).await?; 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | async fn request( 47 | conn: &mut Connection<'_, T, N>, 48 | uri: &str, 49 | ) -> Result<(), Error> { 50 | conn.initiate_request(true, Method::Get, uri, &[("Host", "httpbin.org")]) 51 | .await?; 52 | 53 | conn.initiate_response().await?; 54 | 55 | let mut result = Vec::new(); 56 | 57 | let mut buf = [0_u8; 1024]; 58 | 59 | loop { 60 | let len = conn.read(&mut buf).await?; 61 | 62 | if len > 0 { 63 | result.extend_from_slice(&buf[0..len]); 64 | } else { 65 | break; 66 | } 67 | } 68 | 69 | info!( 70 | "Request to httpbin.org, URI \"{}\" returned:\nBody:\n=================\n{}\n=================\n\n\n\n", 71 | uri, 72 | core::str::from_utf8(&result).unwrap()); 73 | 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /examples/http_server.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::{Debug, Display}; 2 | 3 | use edge_http::io::server::{Connection, DefaultServer, Handler}; 4 | use edge_http::io::Error; 5 | use edge_http::Method; 6 | use edge_nal::TcpBind; 7 | 8 | use embedded_io_async::{Read, Write}; 9 | 10 | use log::info; 11 | 12 | fn main() { 13 | env_logger::init_from_env( 14 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 15 | ); 16 | 17 | let mut server = DefaultServer::new(); 18 | 19 | futures_lite::future::block_on(run(&mut server)).unwrap(); 20 | } 21 | 22 | pub async fn run(server: &mut DefaultServer) -> Result<(), anyhow::Error> { 23 | let addr = "0.0.0.0:8881"; 24 | 25 | info!("Running HTTP server on {addr}"); 26 | 27 | let acceptor = edge_nal_std::Stack::new() 28 | .bind(addr.parse().unwrap()) 29 | .await?; 30 | 31 | server.run(None, acceptor, HttpHandler).await?; 32 | 33 | Ok(()) 34 | } 35 | 36 | struct HttpHandler; 37 | 38 | impl Handler for HttpHandler { 39 | type Error 40 | = Error 41 | where 42 | E: Debug; 43 | 44 | async fn handle( 45 | &self, 46 | _task_id: impl Display + Copy, 47 | conn: &mut Connection<'_, T, N>, 48 | ) -> Result<(), Self::Error> 49 | where 50 | T: Read + Write, 51 | { 52 | let headers = conn.headers()?; 53 | 54 | if headers.method != Method::Get { 55 | conn.initiate_response(405, Some("Method Not Allowed"), &[]) 56 | .await?; 57 | } else if headers.path != "/" { 58 | conn.initiate_response(404, Some("Not Found"), &[]).await?; 59 | } else { 60 | conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/plain")]) 61 | .await?; 62 | 63 | conn.write_all(b"Hello world!").await?; 64 | } 65 | 66 | Ok(()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/mdns_responder.rs: -------------------------------------------------------------------------------- 1 | use core::net::{Ipv4Addr, Ipv6Addr}; 2 | 3 | use edge_mdns::buf::{BufferAccess, VecBufAccess}; 4 | use edge_mdns::domain::base::Ttl; 5 | use edge_mdns::io::{self, MdnsIoError, DEFAULT_SOCKET}; 6 | use edge_mdns::{host::Host, HostAnswersMdnsHandler}; 7 | use edge_nal::{UdpBind, UdpSplit}; 8 | 9 | use embassy_sync::blocking_mutex::raw::NoopRawMutex; 10 | use embassy_sync::signal::Signal; 11 | 12 | use log::*; 13 | 14 | use rand::{thread_rng, RngCore}; 15 | 16 | // Change this to the IP address of the machine where you'll run this example 17 | const OUR_IP: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); 18 | 19 | const OUR_NAME: &str = "mypc"; 20 | 21 | fn main() { 22 | env_logger::init_from_env( 23 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 24 | ); 25 | 26 | let stack = edge_nal_std::Stack::new(); 27 | 28 | let (recv_buf, send_buf) = ( 29 | VecBufAccess::::new(), 30 | VecBufAccess::::new(), 31 | ); 32 | 33 | futures_lite::future::block_on(run::( 34 | &stack, &recv_buf, &send_buf, OUR_NAME, OUR_IP, 35 | )) 36 | .unwrap(); 37 | } 38 | 39 | async fn run( 40 | stack: &T, 41 | recv_buf: RB, 42 | send_buf: SB, 43 | our_name: &str, 44 | our_ip: Ipv4Addr, 45 | ) -> Result<(), MdnsIoError> 46 | where 47 | T: UdpBind, 48 | RB: BufferAccess<[u8]>, 49 | SB: BufferAccess<[u8]>, 50 | { 51 | info!("About to run an mDNS responder for our PC. It will be addressable using {our_name}.local, so try to `ping {our_name}.local`."); 52 | 53 | let mut socket = io::bind(stack, DEFAULT_SOCKET, Some(Ipv4Addr::UNSPECIFIED), Some(0)).await?; 54 | 55 | let (recv, send) = socket.split(); 56 | 57 | let host = Host { 58 | hostname: our_name, 59 | ipv4: our_ip, 60 | ipv6: Ipv6Addr::UNSPECIFIED, 61 | ttl: Ttl::from_secs(60), 62 | }; 63 | 64 | // A way to notify the mDNS responder that the data in `Host` had changed 65 | // We don't use it in this example, because the data is hard-coded 66 | let signal = Signal::new(); 67 | 68 | let mdns = io::Mdns::::new( 69 | Some(Ipv4Addr::UNSPECIFIED), 70 | Some(0), 71 | recv, 72 | send, 73 | recv_buf, 74 | send_buf, 75 | |buf| thread_rng().fill_bytes(buf), 76 | &signal, 77 | ); 78 | 79 | mdns.run(HostAnswersMdnsHandler::new(&host)).await 80 | } 81 | -------------------------------------------------------------------------------- /examples/mdns_service_responder.rs: -------------------------------------------------------------------------------- 1 | use core::net::{Ipv4Addr, Ipv6Addr}; 2 | 3 | use edge_mdns::buf::{BufferAccess, VecBufAccess}; 4 | use edge_mdns::domain::base::Ttl; 5 | use edge_mdns::host::{Service, ServiceAnswers}; 6 | use edge_mdns::io::{self, MdnsIoError, DEFAULT_SOCKET}; 7 | use edge_mdns::{host::Host, HostAnswersMdnsHandler}; 8 | use edge_nal::{UdpBind, UdpSplit}; 9 | 10 | use embassy_sync::blocking_mutex::raw::NoopRawMutex; 11 | use embassy_sync::signal::Signal; 12 | 13 | use log::*; 14 | 15 | use rand::{thread_rng, RngCore}; 16 | 17 | // Change this to the IP address of the machine where you'll run this example 18 | const OUR_IP: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); 19 | 20 | const OUR_NAME: &str = "mypc"; 21 | 22 | fn main() { 23 | env_logger::init_from_env( 24 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 25 | ); 26 | 27 | let stack = edge_nal_std::Stack::new(); 28 | 29 | let (recv_buf, send_buf) = ( 30 | VecBufAccess::::new(), 31 | VecBufAccess::::new(), 32 | ); 33 | 34 | futures_lite::future::block_on(run::( 35 | &stack, &recv_buf, &send_buf, OUR_NAME, OUR_IP, 36 | )) 37 | .unwrap(); 38 | } 39 | 40 | async fn run( 41 | stack: &T, 42 | recv_buf: RB, 43 | send_buf: SB, 44 | our_name: &str, 45 | our_ip: Ipv4Addr, 46 | ) -> Result<(), MdnsIoError> 47 | where 48 | T: UdpBind, 49 | RB: BufferAccess<[u8]>, 50 | SB: BufferAccess<[u8]>, 51 | { 52 | info!("About to run an mDNS responder for our PC. It will be addressable using {our_name}.local, so try to `ping {our_name}.local`."); 53 | 54 | let mut socket = io::bind(stack, DEFAULT_SOCKET, Some(Ipv4Addr::UNSPECIFIED), Some(0)).await?; 55 | 56 | let (recv, send) = socket.split(); 57 | 58 | let host = Host { 59 | hostname: our_name, 60 | ipv4: our_ip, 61 | ipv6: Ipv6Addr::UNSPECIFIED, 62 | ttl: Ttl::from_secs(60), 63 | }; 64 | 65 | let service = Service { 66 | name: "my-service", 67 | priority: 1, 68 | weight: 5, 69 | service: "_https", 70 | protocol: "_tcp", 71 | port: 443, 72 | service_subtypes: &[], 73 | txt_kvs: &[], 74 | }; 75 | 76 | // A way to notify the mDNS responder that the data in `Host` had changed 77 | // We don't use it in this example, because the data is hard-coded 78 | let signal = Signal::new(); 79 | 80 | let mdns = io::Mdns::::new( 81 | Some(Ipv4Addr::UNSPECIFIED), 82 | Some(0), 83 | recv, 84 | send, 85 | recv_buf, 86 | send_buf, 87 | |buf| thread_rng().fill_bytes(buf), 88 | &signal, 89 | ); 90 | 91 | mdns.run(HostAnswersMdnsHandler::new(ServiceAnswers::new( 92 | &host, &service, 93 | ))) 94 | .await 95 | } 96 | -------------------------------------------------------------------------------- /examples/mqtt_client.rs: -------------------------------------------------------------------------------- 1 | use async_compat::CompatExt; 2 | 3 | use embedded_svc::mqtt::client::asynch::{Client, Connection, Publish, QoS}; 4 | use embedded_svc::mqtt::client::Event; 5 | 6 | use embassy_futures::select::{select, Either}; 7 | use embassy_time::{Duration, Timer}; 8 | 9 | use edge_mqtt::io::{AsyncClient, MqttClient, MqttConnection, MqttOptions}; 10 | 11 | use log::*; 12 | 13 | const MQTT_HOST: &str = "broker.emqx.io"; 14 | const MQTT_PORT: u16 = 1883; 15 | const MQTT_CLIENT_ID: &str = "edge-mqtt-demo"; 16 | const MQTT_TOPIC: &str = "edge-mqtt-demo"; 17 | 18 | fn main() { 19 | env_logger::init_from_env( 20 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 21 | ); 22 | 23 | let (client, conn) = mqtt_create(MQTT_CLIENT_ID, MQTT_HOST, MQTT_PORT).unwrap(); 24 | 25 | futures_lite::future::block_on( 26 | run(client, conn, MQTT_TOPIC).compat(), /* necessary for tokio */ 27 | ) 28 | .unwrap() 29 | } 30 | 31 | async fn run(mut client: M, mut connection: C, topic: &str) -> Result<(), anyhow::Error> 32 | where 33 | M: Client + Publish + 'static, 34 | M::Error: std::error::Error + Send + Sync + 'static, 35 | C: Connection + 'static, 36 | { 37 | info!("About to start the MQTT client"); 38 | 39 | info!("MQTT client started"); 40 | 41 | client.subscribe(topic, QoS::AtMostOnce).await?; 42 | 43 | info!("Subscribed to topic \"{topic}\""); 44 | 45 | let res = select( 46 | async move { 47 | info!("MQTT Listening for messages"); 48 | 49 | while let Ok(event) = connection.next().await { 50 | info!("[Queue] Event: {}", event.payload()); 51 | } 52 | 53 | info!("Connection closed"); 54 | 55 | Ok(()) 56 | }, 57 | async move { 58 | // Just to give a chance of our connection to get even the first published message 59 | Timer::after(Duration::from_millis(500)).await; 60 | 61 | let payload = "Hello from edge-mqtt-demo!"; 62 | 63 | loop { 64 | client 65 | .publish(topic, QoS::AtMostOnce, false, payload.as_bytes()) 66 | .await?; 67 | 68 | info!("Published \"{payload}\" to topic \"{topic}\""); 69 | 70 | let sleep_secs = 2; 71 | 72 | info!("Now sleeping for {sleep_secs}s..."); 73 | Timer::after(Duration::from_secs(sleep_secs)).await; 74 | } 75 | }, 76 | ) 77 | .await; 78 | 79 | match res { 80 | Either::First(res) => res, 81 | Either::Second(res) => res, 82 | } 83 | } 84 | 85 | fn mqtt_create( 86 | client_id: &str, 87 | host: &str, 88 | port: u16, 89 | ) -> Result<(MqttClient, MqttConnection), anyhow::Error> { 90 | let mut mqtt_options = MqttOptions::new(client_id, host, port); 91 | 92 | mqtt_options.set_keep_alive(core::time::Duration::from_secs(10)); 93 | 94 | let (rumqttc_client, rumqttc_eventloop) = AsyncClient::new(mqtt_options, 10); 95 | 96 | let mqtt_client = MqttClient::new(rumqttc_client); 97 | let mqtt_conn = MqttConnection::new(rumqttc_eventloop); 98 | 99 | Ok((mqtt_client, mqtt_conn)) 100 | } 101 | -------------------------------------------------------------------------------- /examples/nal_std.rs: -------------------------------------------------------------------------------- 1 | use core::net::{IpAddr, Ipv4Addr, SocketAddr}; 2 | 3 | use embedded_io_async::{Read, Write}; 4 | 5 | use edge_nal::TcpConnect; 6 | 7 | use log::*; 8 | 9 | fn main() { 10 | env_logger::init_from_env( 11 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 12 | ); 13 | 14 | let stack = edge_nal_std::Stack::new(); 15 | 16 | futures_lite::future::block_on(read(&stack)).unwrap(); 17 | } 18 | 19 | async fn read(stack: &T) -> Result<(), T::Error> { 20 | info!("About to open a TCP connection to 1.1.1.1 port 80"); 21 | 22 | let mut connection = stack 23 | .connect(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 80)) 24 | .await?; 25 | 26 | connection.write_all(b"GET / HTTP/1.0\n\n").await?; 27 | 28 | let mut result = Vec::new(); 29 | 30 | let mut buf = [0_u8; 1024]; 31 | 32 | loop { 33 | let len = connection.read(&mut buf).await?; 34 | 35 | if len > 0 { 36 | result.extend_from_slice(&buf[0..len]); 37 | } else { 38 | break; 39 | } 40 | } 41 | 42 | info!( 43 | "1.1.1.1 returned:\n=================\n{}\n=================\nSince it returned something, all seems OK!", 44 | core::str::from_utf8(&result).unwrap()); 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /examples/ws_client.rs: -------------------------------------------------------------------------------- 1 | use core::net::SocketAddr; 2 | 3 | use anyhow::bail; 4 | 5 | use edge_http::io::client::Connection; 6 | use edge_http::ws::{MAX_BASE64_KEY_LEN, MAX_BASE64_KEY_RESPONSE_LEN, NONCE_LEN}; 7 | use edge_nal::{AddrType, Dns, TcpConnect}; 8 | use edge_ws::{FrameHeader, FrameType}; 9 | 10 | use rand::{thread_rng, RngCore}; 11 | 12 | use log::*; 13 | 14 | // NOTE: HTTP-only echo WS servers seem to be hard to find, this one might or might not work... 15 | const PUBLIC_ECHO_SERVER: (&str, u16, &str) = ("websockets.chilkat.io", 80, "/wsChilkatEcho.ashx"); 16 | const OUR_ECHO_SERVER: (&str, u16, &str) = ("127.0.0.1", 8881, "/"); 17 | 18 | fn main() { 19 | env_logger::init_from_env( 20 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 21 | ); 22 | 23 | let stack = edge_nal_std::Stack::new(); 24 | 25 | let mut buf = [0_u8; 8192]; 26 | 27 | futures_lite::future::block_on(work(&stack, &mut buf)).unwrap(); 28 | } 29 | 30 | async fn work(stack: &T, buf: &mut [u8]) -> Result<(), anyhow::Error> 31 | where 32 | ::Error: Send + Sync + std::error::Error + 'static, 33 | ::Error: Send + Sync + std::error::Error + 'static, 34 | { 35 | let mut args = std::env::args(); 36 | args.next(); // Skip the executable name 37 | 38 | let (fqdn, port, path) = if args.next().is_some() { 39 | OUR_ECHO_SERVER 40 | } else { 41 | PUBLIC_ECHO_SERVER 42 | }; 43 | 44 | info!("About to open an HTTP connection to {fqdn} port {port}"); 45 | 46 | let ip = stack.get_host_by_name(fqdn, AddrType::IPv4).await?; 47 | 48 | let mut conn: Connection<_> = Connection::new(buf, stack, SocketAddr::new(ip, port)); 49 | 50 | let mut rng_source = thread_rng(); 51 | 52 | let mut nonce = [0_u8; NONCE_LEN]; 53 | rng_source.fill_bytes(&mut nonce); 54 | 55 | let mut buf = [0_u8; MAX_BASE64_KEY_LEN]; 56 | conn.initiate_ws_upgrade_request(Some(fqdn), Some("foo.com"), path, None, &nonce, &mut buf) 57 | .await?; 58 | conn.initiate_response().await?; 59 | 60 | let mut buf = [0_u8; MAX_BASE64_KEY_RESPONSE_LEN]; 61 | if !conn.is_ws_upgrade_accepted(&nonce, &mut buf)? { 62 | bail!("WS upgrade failed"); 63 | } 64 | 65 | conn.complete().await?; 66 | 67 | // Now we have the TCP socket in a state where it can be operated as a WS connection 68 | // Send some traffic to a WS echo server and read it back 69 | 70 | let (mut socket, buf) = conn.release(); 71 | 72 | info!("Connection upgraded to WS, starting traffic now"); 73 | 74 | for payload in ["Hello world!", "How are you?", "I'm fine, thanks!"] { 75 | let header = FrameHeader { 76 | frame_type: FrameType::Text(false), 77 | payload_len: payload.len() as _, 78 | mask_key: rng_source.next_u32().into(), 79 | }; 80 | 81 | info!("Sending {header}, with payload \"{payload}\""); 82 | header.send(&mut socket).await?; 83 | header.send_payload(&mut socket, payload.as_bytes()).await?; 84 | 85 | let header = FrameHeader::recv(&mut socket).await?; 86 | let payload = header.recv_payload(&mut socket, buf).await?; 87 | 88 | match header.frame_type { 89 | FrameType::Text(_) => { 90 | info!( 91 | "Got {header}, with payload \"{}\"", 92 | core::str::from_utf8(payload).unwrap() 93 | ); 94 | } 95 | FrameType::Binary(_) => { 96 | info!("Got {header}, with payload {payload:?}"); 97 | } 98 | _ => { 99 | bail!("Unexpected {}", header); 100 | } 101 | } 102 | 103 | if !header.frame_type.is_final() { 104 | bail!("Unexpected fragmented frame"); 105 | } 106 | } 107 | 108 | // Inform the server we are closing the connection 109 | 110 | let header = FrameHeader { 111 | frame_type: FrameType::Close, 112 | payload_len: 0, 113 | mask_key: rng_source.next_u32().into(), 114 | }; 115 | 116 | info!("Closing"); 117 | 118 | header.send(&mut socket).await?; 119 | 120 | Ok(()) 121 | } 122 | -------------------------------------------------------------------------------- /examples/ws_server.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::{Debug, Display}; 2 | 3 | use edge_http::io::server::{Connection, DefaultServer, Handler}; 4 | use edge_http::io::Error; 5 | use edge_http::ws::MAX_BASE64_KEY_RESPONSE_LEN; 6 | use edge_http::Method; 7 | use edge_nal::TcpBind; 8 | use edge_ws::{FrameHeader, FrameType}; 9 | 10 | use embedded_io_async::{Read, Write}; 11 | 12 | use log::info; 13 | 14 | fn main() { 15 | env_logger::init_from_env( 16 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), 17 | ); 18 | 19 | let mut server = DefaultServer::new(); 20 | 21 | futures_lite::future::block_on(run(&mut server)).unwrap(); 22 | } 23 | 24 | pub async fn run(server: &mut DefaultServer) -> Result<(), anyhow::Error> { 25 | let addr = "0.0.0.0:8881"; 26 | 27 | info!("Running HTTP server on {addr}"); 28 | 29 | let acceptor = edge_nal_std::Stack::new() 30 | .bind(addr.parse().unwrap()) 31 | .await?; 32 | 33 | server.run(None, acceptor, WsHandler).await?; 34 | 35 | Ok(()) 36 | } 37 | 38 | #[derive(Debug)] 39 | enum WsHandlerError { 40 | Connection(C), 41 | Ws(W), 42 | } 43 | 44 | impl From for WsHandlerError { 45 | fn from(e: C) -> Self { 46 | Self::Connection(e) 47 | } 48 | } 49 | 50 | struct WsHandler; 51 | 52 | impl Handler for WsHandler { 53 | type Error 54 | = WsHandlerError, edge_ws::Error> 55 | where 56 | E: Debug; 57 | 58 | async fn handle( 59 | &self, 60 | _task_id: impl Display + Clone, 61 | conn: &mut Connection<'_, T, N>, 62 | ) -> Result<(), Self::Error> 63 | where 64 | T: Read + Write, 65 | { 66 | let headers = conn.headers()?; 67 | 68 | if headers.method != Method::Get { 69 | conn.initiate_response(405, Some("Method Not Allowed"), &[]) 70 | .await?; 71 | } else if headers.path != "/" { 72 | conn.initiate_response(404, Some("Not Found"), &[]).await?; 73 | } else if !conn.is_ws_upgrade_request()? { 74 | conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/plain")]) 75 | .await?; 76 | 77 | conn.write_all(b"Initiate WS Upgrade request to switch this connection to WS") 78 | .await?; 79 | } else { 80 | let mut buf = [0_u8; MAX_BASE64_KEY_RESPONSE_LEN]; 81 | conn.initiate_ws_upgrade_response(&mut buf).await?; 82 | 83 | conn.complete().await?; 84 | 85 | info!("Connection upgraded to WS, starting a simple WS echo server now"); 86 | 87 | // Now we have the TCP socket in a state where it can be operated as a WS connection 88 | // Run a simple WS echo server here 89 | 90 | let mut socket = conn.unbind()?; 91 | 92 | let mut buf = [0_u8; 8192]; 93 | 94 | loop { 95 | let mut header = FrameHeader::recv(&mut socket) 96 | .await 97 | .map_err(WsHandlerError::Ws)?; 98 | let payload = header 99 | .recv_payload(&mut socket, &mut buf) 100 | .await 101 | .map_err(WsHandlerError::Ws)?; 102 | 103 | match header.frame_type { 104 | FrameType::Text(_) => { 105 | info!( 106 | "Got {header}, with payload \"{}\"", 107 | core::str::from_utf8(payload).unwrap() 108 | ); 109 | } 110 | FrameType::Binary(_) => { 111 | info!("Got {header}, with payload {payload:?}"); 112 | } 113 | FrameType::Close => { 114 | info!("Got {header}, client closed the connection cleanly"); 115 | break; 116 | } 117 | _ => { 118 | info!("Got {header}"); 119 | } 120 | } 121 | 122 | // Echo it back now 123 | 124 | header.mask_key = None; // Servers never mask the payload 125 | 126 | if matches!(header.frame_type, FrameType::Ping) { 127 | header.frame_type = FrameType::Pong; 128 | } 129 | 130 | info!("Echoing back as {header}"); 131 | 132 | header.send(&mut socket).await.map_err(WsHandlerError::Ws)?; 133 | header 134 | .send_payload(&mut socket, payload) 135 | .await 136 | .map_err(WsHandlerError::Ws)?; 137 | } 138 | } 139 | 140 | Ok(()) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | #![allow(async_fn_in_trait)] 3 | 4 | pub use edge_captive as captive; 5 | pub use edge_dhcp as dhcp; 6 | pub use edge_http as http; 7 | pub use edge_mdns as mdns; 8 | #[cfg(feature = "std")] 9 | pub use edge_mqtt as mqtt; 10 | #[cfg(feature = "io")] 11 | pub use edge_nal as nal; 12 | #[cfg(feature = "embassy")] 13 | pub use edge_nal_embassy as embassy; 14 | #[cfg(feature = "std")] 15 | pub use edge_nal_std as std; 16 | pub use edge_raw as raw; 17 | pub use edge_ws as ws; 18 | --------------------------------------------------------------------------------