From cb5931d583d283dda3a1b5ed2014c086bb8f98ae Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Wed, 3 Feb 2021 05:09:37 +0100 Subject: [PATCH] :rainbow: --- .gitignore | 3 + Cargo.toml | 48 ++++++ LICENSE-APACHE | 201 +++++++++++++++++++++++ LICENSE-MIT | 25 +++ README.md | 34 ++++ embassy-net-examples/Cargo.toml | 17 ++ embassy-net-examples/src/main.rs | 79 ++++++++++ embassy-net-examples/src/tuntap.rs | 200 +++++++++++++++++++++++ embassy-net/Cargo.toml | 46 ++++++ embassy-net/src/config/dhcp.rs | 80 ++++++++++ embassy-net/src/config/mod.rs | 34 ++++ embassy-net/src/config/statik.rs | 26 +++ embassy-net/src/device.rs | 103 ++++++++++++ embassy-net/src/fmt.rs | 118 ++++++++++++++ embassy-net/src/lib.rs | 31 ++++ embassy-net/src/packet_pool.rs | 88 +++++++++++ embassy-net/src/pool.rs | 245 +++++++++++++++++++++++++++++ embassy-net/src/stack.rs | 212 +++++++++++++++++++++++++ embassy-net/src/tcp_socket.rs | 178 +++++++++++++++++++++ test-build.sh | 25 +++ 20 files changed, 1793 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 embassy-net-examples/Cargo.toml create mode 100644 embassy-net-examples/src/main.rs create mode 100644 embassy-net-examples/src/tuntap.rs create mode 100644 embassy-net/Cargo.toml create mode 100644 embassy-net/src/config/dhcp.rs create mode 100644 embassy-net/src/config/mod.rs create mode 100644 embassy-net/src/config/statik.rs create mode 100644 embassy-net/src/device.rs create mode 100644 embassy-net/src/fmt.rs create mode 100644 embassy-net/src/lib.rs create mode 100644 embassy-net/src/packet_pool.rs create mode 100644 embassy-net/src/pool.rs create mode 100644 embassy-net/src/stack.rs create mode 100644 embassy-net/src/tcp_socket.rs create mode 100755 test-build.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..25d18d5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +third_party diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..1b301282 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,48 @@ + +[workspace] +members = [ + "embassy-net", + "embassy-net-examples", +] + +exclude = [ + "third_party" +] + +[profile.dev] +codegen-units = 1 +debug = 2 +debug-assertions = true +incremental = false +opt-level = 3 +overflow-checks = true + +[profile.release] +codegen-units = 1 +debug = 2 +debug-assertions = false +incremental = false +lto = "fat" +opt-level = 's' +overflow-checks = false + +# do not optimize proc-macro crates = faster builds from scratch +[profile.dev.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false + +[profile.release.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false + +[patch.crates-io] +embassy = { git = "https://github.com/akiles/embassy" } +embassy-std = { git = "https://github.com/akiles/embassy" } +embassy-macros = { git = "https://github.com/akiles/embassy" } +smoltcp = { git = "https://github.com/akiles/smoltcp" } \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 00000000..16fe87b0 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..dacc57b2 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2020 Dario Nieuwenhuis + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..64f65670 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# embassy-net + +embassy-net contains an async network API based on smoltcp and embassy, designed +for embedded systems. + +## Running the example + +First, create the tap0 interface. You only need to do this once. + +```sh +sudo ip tuntap add name tap0 mode tap user $USER +sudo ip link set tap0 up +sudo ip addr add 192.168.69.100/24 dev tap0 +sudo ip -6 addr add fe80::100/64 dev tap0 +sudo ip -6 addr add fdaa::100/64 dev tap0 +sudo ip -6 route add fe80::/64 dev tap0 +sudo ip -6 route add fdaa::/64 dev tap0 +``` + +Then, run it + +```sh +cargo run --bin embassy-net-examples +``` + +## License + +This work is licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. diff --git a/embassy-net-examples/Cargo.toml b/embassy-net-examples/Cargo.toml new file mode 100644 index 00000000..944e9191 --- /dev/null +++ b/embassy-net-examples/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "embassy-net-examples" +version = "0.1.0" +authors = ["Dario Nieuwenhuis "] +edition = "2018" + +[dependencies] +heapless = { version = "0.5.6", default-features = false } +embassy = { version = "0.1.0", features=["std", "log"] } +embassy-std = { version = "0.1.0" } +embassy-net = { version = "0.1.0", path = "../embassy-net", features=["std", "log"] } +env_logger = "0.8.2" +log = "0.4.11" +futures = "0.3.8" +libc = "0.2.81" +async-io = "1.3.1" +smoltcp = { version = "0.6.0", default-features = false } diff --git a/embassy-net-examples/src/main.rs b/embassy-net-examples/src/main.rs new file mode 100644 index 00000000..bc413f1a --- /dev/null +++ b/embassy-net-examples/src/main.rs @@ -0,0 +1,79 @@ +#![feature(type_alias_impl_trait)] + +use embassy::executor::{Spawner, task}; +use embassy::io::{AsyncBufReadExt, AsyncWriteExt}; +use embassy::time::{Duration, Timer}; +use embassy::util::Forever; +use embassy_net::*; +use embassy_std::Executor; +use heapless::Vec; +use log::*; + +mod tuntap; + +use crate::tuntap::TunTapDevice; + +static DEVICE: Forever = Forever::new(); +static CONFIG: Forever = Forever::new(); + +#[task] +async fn net_task() { + embassy_net::run().await +} + +#[task] +async fn main_task(spawner: Spawner) { + // Init network device + let device = TunTapDevice::new("tap0").unwrap(); + + // Static IP configuration + let config = StaticConfigurator::new(UpConfig { + address: Ipv4Cidr::new(Ipv4Address::new(192, 168, 69, 1), 24), + dns_servers: Vec::new(), + gateway: Ipv4Address::new(192, 168, 69, 100), + }); + + // Init network stack + embassy_net::init(DEVICE.put(device), CONFIG.put(config)); + + // Launch network task + spawner.spawn(net_task()).unwrap(); + + // Then we can use it! + let mut rx_buffer = [0; 4096]; + let mut tx_buffer = [0; 4096]; + let mut socket = TcpSocket::new(&mut rx_buffer, &mut tx_buffer); + + socket.set_timeout(Some(embassy_net::SmolDuration::from_secs(10))); + + let remote_endpoint = (Ipv4Address::new(192, 168, 69, 100), 8000); + info!("connecting to {:?}...", remote_endpoint); + let r = socket.connect(remote_endpoint).await; + if let Err(e) = r { + warn!("connect error: {:?}", e); + return; + } + info!("connected!"); + loop { + let r = socket.write_all(b"Hello!\n").await; + if let Err(e) = r { + warn!("write error: {:?}", e); + return; + } + } +} + +static EXECUTOR: Forever = Forever::new(); + +fn main() { + env_logger::builder() + .filter_level(log::LevelFilter::Debug) + .filter_module("async_io", log::LevelFilter::Info) + .format_timestamp_nanos() + .init(); + + let executor = EXECUTOR.put(Executor::new()); + executor.run(|spawner| { + spawner.spawn(main_task(spawner)).unwrap(); + }); +} diff --git a/embassy-net-examples/src/tuntap.rs b/embassy-net-examples/src/tuntap.rs new file mode 100644 index 00000000..5c138c06 --- /dev/null +++ b/embassy-net-examples/src/tuntap.rs @@ -0,0 +1,200 @@ +use async_io::Async; +use embassy::util::WakerRegistration; +use libc; +use smoltcp::wire::EthernetFrame; +use std::io; +use std::io::{Read, Write}; +use std::os::unix::io::{AsRawFd, RawFd}; +use log::*; + +pub const SIOCGIFMTU: libc::c_ulong = 0x8921; +pub const SIOCGIFINDEX: libc::c_ulong = 0x8933; +pub const ETH_P_ALL: libc::c_short = 0x0003; +pub const TUNSETIFF: libc::c_ulong = 0x400454CA; +pub const IFF_TUN: libc::c_int = 0x0001; +pub const IFF_TAP: libc::c_int = 0x0002; +pub const IFF_NO_PI: libc::c_int = 0x1000; + +#[repr(C)] +#[derive(Debug)] +struct ifreq { + ifr_name: [libc::c_char; libc::IF_NAMESIZE], + ifr_data: libc::c_int, /* ifr_ifindex or ifr_mtu */ +} + +fn ifreq_for(name: &str) -> ifreq { + let mut ifreq = ifreq { + ifr_name: [0; libc::IF_NAMESIZE], + ifr_data: 0, + }; + for (i, byte) in name.as_bytes().iter().enumerate() { + ifreq.ifr_name[i] = *byte as libc::c_char + } + ifreq +} + +fn ifreq_ioctl( + lower: libc::c_int, + ifreq: &mut ifreq, + cmd: libc::c_ulong, +) -> io::Result { + unsafe { + let res = libc::ioctl(lower, cmd as _, ifreq as *mut ifreq); + if res == -1 { + return Err(io::Error::last_os_error()); + } + } + + Ok(ifreq.ifr_data) +} + +#[derive(Debug)] +pub struct TunTap { + fd: libc::c_int, + ifreq: ifreq, + mtu: usize, +} + +impl AsRawFd for TunTap { + fn as_raw_fd(&self) -> RawFd { + self.fd + } +} + +impl TunTap { + pub fn new(name: &str) -> io::Result { + unsafe { + let fd = libc::open( + "/dev/net/tun\0".as_ptr() as *const libc::c_char, + libc::O_RDWR | libc::O_NONBLOCK, + ); + if fd == -1 { + return Err(io::Error::last_os_error()); + } + + let mut ifreq = ifreq_for(name); + ifreq.ifr_data = IFF_TAP | IFF_NO_PI; + ifreq_ioctl(fd, &mut ifreq, TUNSETIFF)?; + + let socket = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, libc::IPPROTO_IP); + if socket == -1 { + return Err(io::Error::last_os_error()); + } + + let ip_mtu = ifreq_ioctl(socket, &mut ifreq, SIOCGIFMTU); + libc::close(socket); + let ip_mtu = ip_mtu? as usize; + + // SIOCGIFMTU returns the IP MTU (typically 1500 bytes.) + // smoltcp counts the entire Ethernet packet in the MTU, so add the Ethernet header size to it. + let mtu = ip_mtu + EthernetFrame::<&[u8]>::header_len(); + + Ok(TunTap { fd, mtu, ifreq }) + } + } +} + +impl Drop for TunTap { + fn drop(&mut self) { + unsafe { + libc::close(self.fd); + } + } +} + +impl io::Read for TunTap { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let len = unsafe { libc::read(self.fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) }; + if len == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(len as usize) + } + } +} + +impl io::Write for TunTap { + fn write(&mut self, buf: &[u8]) -> io::Result { + let len = unsafe { libc::write(self.fd, buf.as_ptr() as *mut libc::c_void, buf.len()) }; + if len == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(len as usize) + } + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +pub struct TunTapDevice { + device: Async, + waker: WakerRegistration, +} + +impl TunTapDevice { + pub fn new(name: &str) -> io::Result { + Ok(Self { + device: Async::new(TunTap::new(name)?)?, + waker: WakerRegistration::new(), + }) + } +} + +use embassy_net::{LinkState, DeviceCapabilities, Packet, PacketBox, PacketBuf}; +use core::task::Waker; + +impl crate::Device for TunTapDevice { + fn is_transmit_ready(&mut self) -> bool { + true + } + + fn transmit(&mut self, pkt: PacketBuf) { + // todo handle WouldBlock + match self.device.get_mut().write(&pkt) { + Ok(_) => {} + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + info!("transmit WouldBlock"); + } + Err(e) => panic!("transmit error: {:?}", e), + } + } + + fn receive(&mut self) -> Option { + let mut pkt = PacketBox::new(Packet::new()).unwrap(); + loop { + match self.device.get_mut().read(&mut pkt[..]) { + Ok(n) => { + return Some(pkt.slice(0..n)); + } + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + let ready = if let Some(mut cx) = self.waker.context() { + let ready = self.device.poll_readable(&mut cx).is_ready(); + ready + } else { + false + }; + if !ready { + return None; + } + } + Err(e) => panic!("read error: {:?}", e), + } + } + } + + fn register_waker(&mut self, waker: &Waker) { + self.waker.register(waker) + } + + fn capabilities(&mut self) -> DeviceCapabilities { + let mut caps = DeviceCapabilities::default(); + caps.max_transmission_unit = self.device.get_ref().mtu; + caps + } + + fn link_state(&mut self) -> LinkState { + LinkState::Up + } +} diff --git a/embassy-net/Cargo.toml b/embassy-net/Cargo.toml new file mode 100644 index 00000000..aec6b796 --- /dev/null +++ b/embassy-net/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "embassy-net" +version = "0.1.0" +authors = ["Dario Nieuwenhuis "] +edition = "2018" + +[features] +std = [] +defmt-trace = [] +defmt-debug = [] +defmt-info = [] +defmt-warn = [] +defmt-error = [] + +[dependencies] + +defmt = { version = "0.1.3", optional = true } +log = { version = "0.4.11", optional = true } + +embassy = { version = "0.1.0" } + +managed = { version = "0.8.0", default-features = false, features = [ "map" ]} +heapless = { version = "0.5.6", default-features = false } +as-slice = { version = "0.1.4" } +generic-array = { version = "0.14.4", default-features = false } +stable_deref_trait = { version = "1.2.0", default-features = false } +futures = { version = "0.3.5", default-features = false, features = [ "async-await" ]} + +[dependencies.smoltcp] +version = "0.6.0" +#git = "https://github.com/akiles/smoltcp" +#rev = "00952e2c5cdf5667a1dfb6142258055f58d3851c" +default-features = false +features = [ + "medium-ethernet", + "medium-ip", + "proto-ipv4", + "proto-dhcpv4", + #"proto-igmp", + #"proto-ipv6", + #"socket-raw", + #"socket-icmp", + #"socket-udp", + "socket-tcp", + "async", +] diff --git a/embassy-net/src/config/dhcp.rs b/embassy-net/src/config/dhcp.rs new file mode 100644 index 00000000..f5d598bd --- /dev/null +++ b/embassy-net/src/config/dhcp.rs @@ -0,0 +1,80 @@ +use embassy::util::Forever; +use heapless::consts::*; +use heapless::Vec; +use smoltcp::dhcp::Dhcpv4Client; +use smoltcp::socket::{RawPacketMetadata, RawSocketBuffer}; +use smoltcp::time::Instant; +use smoltcp::wire::{Ipv4Address, Ipv4Cidr}; + +use super::*; +use crate::{device::LinkState, fmt::*}; +use crate::{Interface, SocketSet}; + +pub struct DhcpResources { + rx_buffer: [u8; 900], + tx_buffer: [u8; 600], + rx_meta: [RawPacketMetadata; 1], + tx_meta: [RawPacketMetadata; 1], +} + +pub struct DhcpConfigurator { + client: Option, +} + +impl DhcpConfigurator { + pub fn new() -> Self { + Self { client: None } + } +} + +static DHCP_RESOURCES: Forever = Forever::new(); + +impl Configurator for DhcpConfigurator { + fn poll( + &mut self, + iface: &mut Interface, + sockets: &mut SocketSet, + timestamp: Instant, + ) -> Option { + if self.client.is_none() { + let res = DHCP_RESOURCES.put(DhcpResources { + rx_buffer: [0; 900], + tx_buffer: [0; 600], + rx_meta: [RawPacketMetadata::EMPTY; 1], + tx_meta: [RawPacketMetadata::EMPTY; 1], + }); + let rx_buffer = RawSocketBuffer::new(&mut res.rx_meta[..], &mut res.rx_buffer[..]); + let tx_buffer = RawSocketBuffer::new(&mut res.tx_meta[..], &mut res.tx_buffer[..]); + let dhcp = Dhcpv4Client::new(sockets, rx_buffer, tx_buffer, timestamp); + info!("created dhcp"); + self.client = Some(dhcp) + } + + let client = self.client.as_mut().unwrap(); + + let link_up = iface.device_mut().device.link_state() == LinkState::Up; + if !link_up { + client.reset(timestamp); + return Some(Config::Down); + } + + let config = client.poll(iface, sockets, timestamp).unwrap_or(None)?; + + if config.address.is_none() { + return Some(Config::Down); + } + + let mut dns_servers = Vec::new(); + for s in &config.dns_servers { + if let Some(addr) = s { + dns_servers.push(addr.clone()).unwrap(); + } + } + + return Some(Config::Up(UpConfig { + address: config.address.unwrap(), + gateway: config.router.unwrap_or(Ipv4Address::UNSPECIFIED), + dns_servers, + })); + } +} diff --git a/embassy-net/src/config/mod.rs b/embassy-net/src/config/mod.rs new file mode 100644 index 00000000..596374f9 --- /dev/null +++ b/embassy-net/src/config/mod.rs @@ -0,0 +1,34 @@ +use heapless::consts::*; +use heapless::Vec; +use smoltcp::time::Instant; +use smoltcp::wire::{Ipv4Address, Ipv4Cidr}; + +use crate::fmt::*; +use crate::{Interface, SocketSet}; + +mod dhcp; +mod statik; +pub use dhcp::DhcpConfigurator; +pub use statik::StaticConfigurator; + +#[derive(Debug, Clone)] +pub enum Config { + Down, + Up(UpConfig), +} + +#[derive(Debug, Clone)] +pub struct UpConfig { + pub address: Ipv4Cidr, + pub gateway: Ipv4Address, + pub dns_servers: Vec, +} + +pub trait Configurator { + fn poll( + &mut self, + iface: &mut Interface, + sockets: &mut SocketSet, + timestamp: Instant, + ) -> Option; +} diff --git a/embassy-net/src/config/statik.rs b/embassy-net/src/config/statik.rs new file mode 100644 index 00000000..52196f48 --- /dev/null +++ b/embassy-net/src/config/statik.rs @@ -0,0 +1,26 @@ +use smoltcp::time::Instant; + +use super::*; +use crate::fmt::*; +use crate::{Interface, SocketSet}; + +pub struct StaticConfigurator { + config: UpConfig, +} + +impl StaticConfigurator { + pub fn new(config: UpConfig) -> Self { + Self { config } + } +} + +impl Configurator for StaticConfigurator { + fn poll( + &mut self, + _iface: &mut Interface, + _sockets: &mut SocketSet, + _timestamp: Instant, + ) -> Option { + Some(Config::Up(self.config.clone())) + } +} diff --git a/embassy-net/src/device.rs b/embassy-net/src/device.rs new file mode 100644 index 00000000..95a62e79 --- /dev/null +++ b/embassy-net/src/device.rs @@ -0,0 +1,103 @@ +use core::task::{Poll, Waker}; +use smoltcp::phy::Device as SmolDevice; +use smoltcp::phy::DeviceCapabilities; +use smoltcp::time::Instant as SmolInstant; +use smoltcp::Result; + +use crate::fmt::*; +use crate::{Packet, PacketBox, PacketBuf}; + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum LinkState { + Down, + Up, +} + +pub trait Device { + fn is_transmit_ready(&mut self) -> bool; + fn transmit(&mut self, pkt: PacketBuf); + fn receive(&mut self) -> Option; + + fn register_waker(&mut self, waker: &Waker); + fn capabilities(&mut self) -> DeviceCapabilities; + fn link_state(&mut self) -> LinkState; +} + +pub struct DeviceAdapter { + pub device: &'static mut dyn Device, + caps: DeviceCapabilities, +} + +impl DeviceAdapter { + pub(crate) fn new(device: &'static mut dyn Device) -> Self { + Self { + caps: device.capabilities(), + device, + } + } +} + +impl<'a> SmolDevice<'a> for DeviceAdapter { + type RxToken = RxToken; + type TxToken = TxToken<'a>; + + fn receive(&'a mut self) -> Option<(Self::RxToken, Self::TxToken)> { + let rx_pkt = self.device.receive()?; + let tx_pkt = PacketBox::new(Packet::new()).unwrap(); // TODO: not sure about unwrap + let rx_token = RxToken { pkt: rx_pkt }; + let tx_token = TxToken { + device: self.device, + pkt: tx_pkt, + }; + + Some((rx_token, tx_token)) + } + + /// Construct a transmit token. + fn transmit(&'a mut self) -> Option { + if !self.device.is_transmit_ready() { + return None; + } + + let tx_pkt = PacketBox::new(Packet::new())?; + Some(TxToken { + device: self.device, + pkt: tx_pkt, + }) + } + + /// Get a description of device capabilities. + fn capabilities(&self) -> DeviceCapabilities { + self.caps.clone() + } +} + +pub struct RxToken { + pkt: PacketBuf, +} + +impl smoltcp::phy::RxToken for RxToken { + fn consume(mut self, _timestamp: SmolInstant, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> Result, + { + f(&mut self.pkt) + } +} + +pub struct TxToken<'a> { + device: &'a mut dyn Device, + pkt: PacketBox, +} + +impl<'a> smoltcp::phy::TxToken for TxToken<'a> { + fn consume(mut self, _timestamp: SmolInstant, len: usize, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> Result, + { + let mut buf = self.pkt.slice(0..len); + let r = f(&mut buf)?; + self.device.transmit(buf); + Ok(r) + } +} diff --git a/embassy-net/src/fmt.rs b/embassy-net/src/fmt.rs new file mode 100644 index 00000000..4da69766 --- /dev/null +++ b/embassy-net/src/fmt.rs @@ -0,0 +1,118 @@ +#![macro_use] + +#[cfg(all(feature = "defmt", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +pub use fmt::*; + +#[cfg(feature = "defmt")] +mod fmt { + pub use defmt::{ + assert, assert_eq, assert_ne, debug, debug_assert, debug_assert_eq, debug_assert_ne, error, + info, panic, todo, trace, unreachable, unwrap, warn, + }; +} + +#[cfg(feature = "log")] +mod fmt { + pub use core::{ + assert, assert_eq, assert_ne, debug_assert, debug_assert_eq, debug_assert_ne, panic, todo, + unreachable, + }; + pub use log::{debug, error, info, trace, warn}; +} + +#[cfg(not(any(feature = "defmt", feature = "log")))] +mod fmt { + #![macro_use] + + pub use core::{ + assert, assert_eq, assert_ne, debug_assert, debug_assert_eq, debug_assert_ne, panic, todo, + unreachable, + }; + + #[macro_export] + macro_rules! trace { + ($($msg:expr),+ $(,)?) => { + () + }; + } + + #[macro_export] + macro_rules! debug { + ($($msg:expr),+ $(,)?) => { + () + }; + } + + #[macro_export] + macro_rules! info { + ($($msg:expr),+ $(,)?) => { + () + }; + } + + #[macro_export] + macro_rules! warn { + ($($msg:expr),+ $(,)?) => { + () + }; + } + + #[macro_export] + macro_rules! error { + ($($msg:expr),+ $(,)?) => { + () + }; + } +} + +#[cfg(not(feature = "defmt"))] +#[macro_export] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} diff --git a/embassy-net/src/lib.rs b/embassy-net/src/lib.rs new file mode 100644 index 00000000..a2a320ad --- /dev/null +++ b/embassy-net/src/lib.rs @@ -0,0 +1,31 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![feature(const_fn)] +#![feature(const_in_array_repeat_expressions)] +#![feature(const_generics)] +#![feature(const_evaluatable_checked)] +#![allow(incomplete_features)] + +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; + +mod pool; // TODO extract to embassy, or to own crate + +mod config; +mod device; +mod packet_pool; +mod stack; +mod tcp_socket; + +pub use config::{Config, Configurator, DhcpConfigurator, StaticConfigurator, UpConfig}; +pub use device::{Device, LinkState}; +pub use packet_pool::{Packet, PacketBox, PacketBuf}; +pub use stack::{init, is_init, run}; +pub use tcp_socket::TcpSocket; + +// smoltcp reexports +pub use smoltcp::phy::{DeviceCapabilities, Medium}; +pub use smoltcp::time::Duration as SmolDuration; +pub use smoltcp::time::Instant as SmolInstant; +pub use smoltcp::wire::{IpAddress, IpCidr, Ipv4Address, Ipv4Cidr}; +pub type Interface = smoltcp::iface::Interface<'static, device::DeviceAdapter>; +pub type SocketSet = smoltcp::socket::SocketSet<'static>; diff --git a/embassy-net/src/packet_pool.rs b/embassy-net/src/packet_pool.rs new file mode 100644 index 00000000..24635643 --- /dev/null +++ b/embassy-net/src/packet_pool.rs @@ -0,0 +1,88 @@ +use as_slice::{AsMutSlice, AsSlice}; +use core::ops::{Deref, DerefMut, Range}; + +use super::pool::{BitPool, Box, StaticPool}; + +pub const MTU: usize = 1514; +pub const PACKET_POOL_SIZE: usize = 4; + +pool!(pub PacketPool: [Packet; PACKET_POOL_SIZE]); +pub type PacketBox = Box; + +pub struct Packet(pub [u8; MTU]); + +impl Packet { + pub const fn new() -> Self { + Self([0; MTU]) + } +} + +impl Box { + pub fn slice(self, range: Range) -> PacketBuf { + PacketBuf { + packet: self, + range, + } + } +} + +impl AsSlice for Packet { + type Element = u8; + + fn as_slice(&self) -> &[Self::Element] { + &self.deref()[..] + } +} + +impl AsMutSlice for Packet { + fn as_mut_slice(&mut self) -> &mut [Self::Element] { + &mut self.deref_mut()[..] + } +} + +impl Deref for Packet { + type Target = [u8; MTU]; + + fn deref(&self) -> &[u8; MTU] { + &self.0 + } +} + +impl DerefMut for Packet { + fn deref_mut(&mut self) -> &mut [u8; MTU] { + &mut self.0 + } +} + +pub struct PacketBuf { + packet: PacketBox, + range: Range, +} + +impl AsSlice for PacketBuf { + type Element = u8; + + fn as_slice(&self) -> &[Self::Element] { + &self.packet[self.range.clone()] + } +} + +impl AsMutSlice for PacketBuf { + fn as_mut_slice(&mut self) -> &mut [Self::Element] { + &mut self.packet[self.range.clone()] + } +} + +impl Deref for PacketBuf { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + &self.packet[self.range.clone()] + } +} + +impl DerefMut for PacketBuf { + fn deref_mut(&mut self) -> &mut [u8] { + &mut self.packet[self.range.clone()] + } +} diff --git a/embassy-net/src/pool.rs b/embassy-net/src/pool.rs new file mode 100644 index 00000000..3ab36e4c --- /dev/null +++ b/embassy-net/src/pool.rs @@ -0,0 +1,245 @@ +#![macro_use] + +use as_slice::{AsMutSlice, AsSlice}; +use core::cmp; +use core::fmt; +use core::hash::{Hash, Hasher}; +use core::mem::MaybeUninit; +use core::ops::{Deref, DerefMut}; +use core::sync::atomic::{AtomicU32, Ordering}; + +use crate::fmt::{assert, *}; + +struct AtomicBitset +where + [AtomicU32; (N + 31) / 32]: Sized, +{ + used: [AtomicU32; (N + 31) / 32], +} + +impl AtomicBitset +where + [AtomicU32; (N + 31) / 32]: Sized, +{ + const fn new() -> Self { + const Z: AtomicU32 = AtomicU32::new(0); + Self { + used: [Z; (N + 31) / 32], + } + } + + fn alloc(&self) -> Option { + for (i, val) in self.used.iter().enumerate() { + let res = val.fetch_update(Ordering::AcqRel, Ordering::Acquire, |val| { + let n = val.trailing_ones() as usize + i * 32; + if n >= N { + None + } else { + Some(val | (1 << n)) + } + }); + if let Ok(val) = res { + let n = val.trailing_ones() as usize + i * 32; + return Some(n); + } + } + None + } + fn free(&self, i: usize) { + assert!(i < N); + self.used[i / 32].fetch_and(!(1 << ((i % 32) as u32)), Ordering::AcqRel); + } +} + +pub trait Pool { + fn alloc(&self) -> Option<*mut T>; + unsafe fn free(&self, p: *mut T); +} + +pub struct BitPool +where + [AtomicU32; (N + 31) / 32]: Sized, +{ + used: AtomicBitset, + data: MaybeUninit<[T; N]>, +} + +impl BitPool +where + [AtomicU32; (N + 31) / 32]: Sized, +{ + pub const fn new() -> Self { + Self { + used: AtomicBitset::new(), + data: MaybeUninit::uninit(), + } + } +} + +impl Pool for BitPool +where + [AtomicU32; (N + 31) / 32]: Sized, +{ + fn alloc(&self) -> Option<*mut T> { + let n = self.used.alloc()?; + let origin = self.data.as_ptr() as *mut T; + Some(unsafe { origin.add(n) }) + } + + /// safety: p must be a pointer obtained from self.alloc that hasn't been freed yet. + unsafe fn free(&self, p: *mut T) { + let origin = self.data.as_ptr() as *mut T; + let n = p.offset_from(origin); + assert!(n >= 0); + assert!((n as usize) < N); + self.used.free(n as usize); + } +} + +pub trait StaticPool: 'static { + type Item: 'static; + type Pool: Pool; + fn get() -> &'static Self::Pool; +} + +pub struct Box { + ptr: *mut P::Item, +} + +impl Box

{ + pub fn new(item: P::Item) -> Option { + let p = match P::get().alloc() { + Some(p) => p, + None => { + warn!("alloc failed!"); + return None; + } + }; + //trace!("allocated {:u32}", p as u32); + unsafe { p.write(item) }; + Some(Self { ptr: p }) + } +} + +impl Drop for Box

{ + fn drop(&mut self) { + unsafe { + //trace!("dropping {:u32}", self.ptr as u32); + self.ptr.drop_in_place(); + P::get().free(self.ptr); + }; + } +} + +unsafe impl Send for Box

where P::Item: Send {} + +unsafe impl Sync for Box

where P::Item: Sync {} + +unsafe impl stable_deref_trait::StableDeref for Box

{} + +impl AsSlice for Box

+where + P::Item: AsSlice, +{ + type Element = ::Element; + + fn as_slice(&self) -> &[Self::Element] { + self.deref().as_slice() + } +} + +impl AsMutSlice for Box

+where + P::Item: AsMutSlice, +{ + fn as_mut_slice(&mut self) -> &mut [Self::Element] { + self.deref_mut().as_mut_slice() + } +} + +impl Deref for Box

{ + type Target = P::Item; + + fn deref(&self) -> &P::Item { + unsafe { &*self.ptr } + } +} + +impl DerefMut for Box

{ + fn deref_mut(&mut self) -> &mut P::Item { + unsafe { &mut *self.ptr } + } +} + +impl fmt::Debug for Box

+where + P::Item: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(self, f) + } +} + +impl fmt::Display for Box

+where + P::Item: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(self, f) + } +} + +impl PartialEq for Box

+where + P::Item: PartialEq, +{ + fn eq(&self, rhs: &Box

) -> bool { + ::eq(self, rhs) + } +} + +impl Eq for Box

where P::Item: Eq {} + +impl PartialOrd for Box

+where + P::Item: PartialOrd, +{ + fn partial_cmp(&self, rhs: &Box

) -> Option { + ::partial_cmp(self, rhs) + } +} + +impl Ord for Box

+where + P::Item: Ord, +{ + fn cmp(&self, rhs: &Box

) -> cmp::Ordering { + ::cmp(self, rhs) + } +} + +impl Hash for Box

+where + P::Item: Hash, +{ + fn hash(&self, state: &mut H) + where + H: Hasher, + { + ::hash(self, state) + } +} + +macro_rules! pool { + ($vis:vis $name:ident: [$ty:ty; $size:expr]) => { + $vis struct $name; + impl StaticPool for $name { + type Item = $ty; + type Pool = BitPool<$ty, $size>; + fn get() -> &'static Self::Pool { + static POOL: BitPool<$ty, $size> = BitPool::new(); + &POOL + } + } + }; +} diff --git a/embassy-net/src/stack.rs b/embassy-net/src/stack.rs new file mode 100644 index 00000000..c353f1bb --- /dev/null +++ b/embassy-net/src/stack.rs @@ -0,0 +1,212 @@ +use core::future::Future; +use core::task::Context; +use core::task::Poll; +use core::{cell::RefCell, future}; +use embassy::time::{Instant, Timer}; +use embassy::util::ThreadModeMutex; +use embassy::util::{Forever, WakerRegistration}; +use futures::pin_mut; +use smoltcp::iface::{InterfaceBuilder, Neighbor, NeighborCache, Route, Routes}; +use smoltcp::phy::Device as _; +use smoltcp::phy::Medium; +use smoltcp::socket::SocketSetItem; +use smoltcp::time::Instant as SmolInstant; +use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address}; + +use crate::device::{Device, DeviceAdapter}; +use crate::fmt::*; +use crate::{ + config::{Config, Configurator}, + device::LinkState, +}; +use crate::{Interface, SocketSet}; + +const ADDRESSES_LEN: usize = 1; +const NEIGHBOR_CACHE_LEN: usize = 8; +const SOCKETS_LEN: usize = 2; +const LOCAL_PORT_MIN: u16 = 1025; +const LOCAL_PORT_MAX: u16 = 65535; + +struct StackResources { + addresses: [IpCidr; ADDRESSES_LEN], + neighbor_cache: [Option<(IpAddress, Neighbor)>; NEIGHBOR_CACHE_LEN], + sockets: [Option>; SOCKETS_LEN], + routes: [Option<(IpCidr, Route)>; 1], +} + +static STACK_RESOURCES: Forever = Forever::new(); +static STACK: ThreadModeMutex>> = ThreadModeMutex::new(RefCell::new(None)); + +pub(crate) struct Stack { + iface: Interface, + pub sockets: SocketSet, + link_up: bool, + next_local_port: u16, + configurator: &'static mut dyn Configurator, + waker: WakerRegistration, +} + +impl Stack { + pub(crate) fn with(f: impl FnOnce(&mut Stack) -> R) -> R { + let mut stack = STACK.borrow().borrow_mut(); + let stack = stack.as_mut().unwrap(); + f(stack) + } + + pub fn get_local_port(&mut self) -> u16 { + let res = self.next_local_port; + self.next_local_port = if res >= LOCAL_PORT_MAX { + LOCAL_PORT_MIN + } else { + res + 1 + }; + res + } + + pub(crate) fn wake(&mut self) { + self.waker.wake() + } + + fn poll_configurator(&mut self, timestamp: SmolInstant) { + if let Some(config) = self + .configurator + .poll(&mut self.iface, &mut self.sockets, timestamp) + { + let medium = self.iface.device().capabilities().medium; + + let (addr, gateway) = match config { + Config::Up(config) => (config.address.into(), Some(config.gateway)), + Config::Down => (IpCidr::new(Ipv4Address::UNSPECIFIED.into(), 32), None), + }; + + self.iface.update_ip_addrs(|addrs| { + let curr_addr = &mut addrs[0]; + if *curr_addr != addr { + info!("IPv4 address: {:?} -> {:?}", *curr_addr, addr); + *curr_addr = addr; + } + }); + + if medium == Medium::Ethernet { + self.iface.routes_mut().update(|r| { + let cidr = IpCidr::new(IpAddress::v4(0, 0, 0, 0), 0); + let curr_gateway = r.get(&cidr).map(|r| r.via_router); + + if curr_gateway != gateway.map(|a| a.into()) { + info!("IPv4 gateway: {:?} -> {:?}", curr_gateway, gateway); + if let Some(gateway) = gateway { + r.insert(cidr, Route::new_ipv4_gateway(gateway)).unwrap(); + } else { + r.remove(&cidr); + } + } + }); + } + } + } + + fn poll(&mut self, cx: &mut Context<'_>) { + self.iface.device_mut().device.register_waker(cx.waker()); + self.waker.register(cx.waker()); + + let timestamp = instant_to_smoltcp(Instant::now()); + if let Err(e) = self.iface.poll(&mut self.sockets, timestamp) { + // If poll() returns error, it may not be done yet, so poll again later. + cx.waker().wake_by_ref(); + return; + } + + // Update link up + let old_link_up = self.link_up; + self.link_up = self.iface.device_mut().device.link_state() == LinkState::Up; + + // Print when changed + if old_link_up != self.link_up { + if self.link_up { + info!("Link up!"); + } else { + info!("Link down!"); + } + } + + if old_link_up || self.link_up { + self.poll_configurator(timestamp) + } + + if let Some(poll_at) = self.iface.poll_at(&mut self.sockets, timestamp) { + let t = Timer::at(instant_from_smoltcp(poll_at)); + pin_mut!(t); + if t.poll(cx).is_ready() { + cx.waker().wake_by_ref(); + } + } + } +} + +/// Initialize embassy_net. +/// This function must be called from thread mode. +pub fn init(device: &'static mut dyn Device, configurator: &'static mut dyn Configurator) { + let res = STACK_RESOURCES.put(StackResources { + addresses: [IpCidr::new(Ipv4Address::UNSPECIFIED.into(), 32)], + neighbor_cache: [None; NEIGHBOR_CACHE_LEN], + sockets: [None; SOCKETS_LEN], + routes: [None; 1], + }); + + let ethernet_addr = EthernetAddress([0x02, 0x02, 0x02, 0x02, 0x02, 0x02]); + + let medium = device.capabilities().medium; + + let mut b = InterfaceBuilder::new(DeviceAdapter::new(device)); + b = b.ip_addrs(&mut res.addresses[..]); + + if medium == Medium::Ethernet { + b = b.ethernet_addr(ethernet_addr); + b = b.neighbor_cache(NeighborCache::new(&mut res.neighbor_cache[..])); + b = b.routes(Routes::new(&mut res.routes[..])); + } + + let iface = b.finalize(); + + let sockets = SocketSet::new(&mut res.sockets[..]); + + let local_port = loop { + let mut res = [0u8; 2]; + embassy::rand::rand(&mut res); + let port = u16::from_le_bytes(res); + if port >= LOCAL_PORT_MIN && port <= LOCAL_PORT_MAX { + break port; + } + }; + + let stack = Stack { + iface, + sockets, + link_up: false, + configurator, + next_local_port: local_port, + waker: WakerRegistration::new(), + }; + + *STACK.borrow().borrow_mut() = Some(stack); +} + +pub fn is_init() -> bool { + STACK.borrow().borrow().is_some() +} + +pub async fn run() { + futures::future::poll_fn(|cx| { + Stack::with(|stack| stack.poll(cx)); + Poll::<()>::Pending + }) + .await +} + +fn instant_to_smoltcp(instant: Instant) -> SmolInstant { + SmolInstant::from_millis(instant.as_millis() as i64) +} + +fn instant_from_smoltcp(instant: SmolInstant) -> Instant { + Instant::from_millis(instant.total_millis() as u64) +} diff --git a/embassy-net/src/tcp_socket.rs b/embassy-net/src/tcp_socket.rs new file mode 100644 index 00000000..7f4eb014 --- /dev/null +++ b/embassy-net/src/tcp_socket.rs @@ -0,0 +1,178 @@ +use core::marker::PhantomData; +use core::mem; +use core::pin::Pin; +use core::task::{Context, Poll}; +use embassy::io; +use embassy::io::{AsyncBufRead, AsyncWrite}; +use smoltcp::socket::SocketHandle; +use smoltcp::socket::TcpSocket as SyncTcpSocket; +use smoltcp::socket::{TcpSocketBuffer, TcpState}; +use smoltcp::time::Duration; +use smoltcp::wire::IpEndpoint; +use smoltcp::{Error, Result}; + +use super::stack::Stack; +use crate::fmt::*; + +pub struct TcpSocket<'a> { + handle: SocketHandle, + ghost: PhantomData<&'a mut [u8]>, +} + +impl<'a> Unpin for TcpSocket<'a> {} + +impl<'a> TcpSocket<'a> { + pub fn new(rx_buffer: &'a mut [u8], tx_buffer: &'a mut [u8]) -> Self { + let handle = Stack::with(|stack| { + let rx_buffer: &'static mut [u8] = unsafe { mem::transmute(rx_buffer) }; + let tx_buffer: &'static mut [u8] = unsafe { mem::transmute(tx_buffer) }; + stack.sockets.add(SyncTcpSocket::new( + TcpSocketBuffer::new(rx_buffer), + TcpSocketBuffer::new(tx_buffer), + )) + }); + + Self { + handle, + ghost: PhantomData, + } + } + + pub async fn connect(&mut self, remote_endpoint: T) -> Result<()> + where + T: Into, + { + let local_port = Stack::with(|stack| stack.get_local_port()); + self.with(|s| s.connect(remote_endpoint, local_port))?; + + futures::future::poll_fn(|cx| { + self.with(|s| match s.state() { + TcpState::Closed | TcpState::TimeWait => Poll::Ready(Err(Error::Unaddressable)), + TcpState::Listen => Poll::Ready(Err(Error::Illegal)), + TcpState::SynSent | TcpState::SynReceived => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + _ => Poll::Ready(Ok(())), + }) + }) + .await + } + + pub fn set_timeout(&mut self, duration: Option) { + self.with(|s| s.set_timeout(duration)) + } + + pub fn set_keep_alive(&mut self, interval: Option) { + self.with(|s| s.set_keep_alive(interval)) + } + + pub fn set_hop_limit(&mut self, hop_limit: Option) { + self.with(|s| s.set_hop_limit(hop_limit)) + } + + pub fn local_endpoint(&self) -> IpEndpoint { + self.with(|s| s.local_endpoint()) + } + + pub fn remote_endpoint(&self) -> IpEndpoint { + self.with(|s| s.remote_endpoint()) + } + + pub fn state(&self) -> TcpState { + self.with(|s| s.state()) + } + + pub fn close(&mut self) { + self.with(|s| s.close()) + } + + pub fn abort(&mut self) { + self.with(|s| s.abort()) + } + + pub fn may_send(&self) -> bool { + self.with(|s| s.may_send()) + } + + pub fn may_recv(&self) -> bool { + self.with(|s| s.may_recv()) + } + + fn with(&self, f: impl FnOnce(&mut SyncTcpSocket) -> R) -> R { + Stack::with(|stack| { + let res = { + let mut s = stack.sockets.get::(self.handle); + f(&mut *s) + }; + stack.wake(); + res + }) + } +} + +fn to_ioerr(e: Error) -> io::Error { + warn!("smoltcp err: {:?}", e); + // todo + io::Error::Other +} + +impl<'a> Drop for TcpSocket<'a> { + fn drop(&mut self) { + Stack::with(|stack| { + stack.sockets.remove(self.handle); + }) + } +} + +impl<'a> AsyncBufRead for TcpSocket<'a> { + fn poll_fill_buf<'z>( + self: Pin<&'z mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.with(|socket| match socket.peek(1 << 30) { + // No data ready + Ok(buf) if buf.len() == 0 => { + socket.register_recv_waker(cx.waker()); + Poll::Pending + } + // Data ready! + Ok(buf) => { + // Safety: + // - User can't touch the inner TcpSocket directly at all. + // - The socket itself won't touch these bytes until consume() is called, which + // requires the user to release this borrow. + let buf: &'z [u8] = unsafe { core::mem::transmute(&*buf) }; + Poll::Ready(Ok(buf)) + } + // EOF + Err(Error::Finished) => Poll::Ready(Ok(&[][..])), + // Error + Err(e) => Poll::Ready(Err(to_ioerr(e))), + }) + } + + fn consume(self: Pin<&mut Self>, amt: usize) { + self.with(|s| s.recv(|_| (amt, ()))).unwrap() + } +} + +impl<'a> AsyncWrite for TcpSocket<'a> { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.with(|s| match s.send_slice(buf) { + // Not ready to send (no space in the tx buffer) + Ok(0) => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + // Some data sent + Ok(n) => Poll::Ready(Ok(n)), + // Error + Err(e) => Poll::Ready(Err(to_ioerr(e))), + }) + } +} diff --git a/test-build.sh b/test-build.sh new file mode 100755 index 00000000..f67cc5b2 --- /dev/null +++ b/test-build.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -euxo pipefail + +# embassy std +(cd embassy; cargo build --features log,std) + +# embassy embedded +(cd embassy; cargo build --target thumbv7em-none-eabi) +(cd embassy; cargo build --target thumbv7em-none-eabi --features log) +(cd embassy; cargo build --target thumbv7em-none-eabi --features defmt) + +# embassy-nrf + +(cd embassy-nrf-examples; cargo build --target thumbv7em-none-eabi --bins) + +(cd embassy-nrf; cargo build --target thumbv7em-none-eabi --features 52810) +#(cd embassy-nrf; cargo build --target thumbv7em-none-eabi --features 52811) # nrf52811-hal doesn't exist yet +(cd embassy-nrf; cargo build --target thumbv7em-none-eabi --features 52832) +(cd embassy-nrf; cargo build --target thumbv7em-none-eabi --features 52833) +(cd embassy-nrf; cargo build --target thumbv7em-none-eabi --features 52840) + +(cd embassy-nrf; cargo build --target thumbv7em-none-eabi --features 52840,log) +(cd embassy-nrf; cargo build --target thumbv7em-none-eabi --features 52840,defmt) +