diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index a14fafd11..0bd400186 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -42,6 +42,8 @@ Highlights: * ``PCA9548`` I2C switch class renamed to ``I2CSwitch``, to accomodate support for PCA9547, and possibly other switches in future. Readback has been removed, and now only one channel per switch is supported. +* The "ip" config option can now be set to "use_dhcp" in order to use DHCP to obtain an IP address. + DHCP will also be used if no "ip" config option is set. Breaking changes: diff --git a/artiq/applets/progress_bar.py b/artiq/applets/progress_bar.py new file mode 100644 index 000000000..bbded954c --- /dev/null +++ b/artiq/applets/progress_bar.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +from PyQt5 import QtWidgets + +from artiq.applets.simple import SimpleApplet + + +class ProgressWidget(QtWidgets.QProgressBar): + def __init__(self, args): + QtWidgets.QProgressBar.__init__(self) + self.setMinimum(args.min) + self.setMaximum(args.max) + self.dataset_value = args.value + + def data_changed(self, data, mods): + try: + value = round(data[self.dataset_value][1]) + except (KeyError, ValueError, TypeError): + value = 0 + self.setValue(value) + + + +def main(): + applet = SimpleApplet(ProgressWidget) + applet.add_dataset("value", "counter") + applet.argparser.add_argument("--min", type=int, default=0, + help="minimum (left) value of the bar") + applet.argparser.add_argument("--max", type=int, default=100, + help="maximum (right) value of the bar") + applet.run() + +if __name__ == "__main__": + main() diff --git a/artiq/firmware/Cargo.lock b/artiq/firmware/Cargo.lock index e5d303a00..8e0c7f95a 100644 --- a/artiq/firmware/Cargo.lock +++ b/artiq/firmware/Cargo.lock @@ -240,6 +240,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75de51135344a4f8ed3cfe2720dc27736f7711989703a0b43aadf3753c55577" +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + [[package]] name = "memchr" version = "2.4.1" @@ -321,7 +327,7 @@ dependencies = [ "io", "log", "logger_artiq", - "managed", + "managed 0.7.2", "proto_artiq", "riscv", "smoltcp", @@ -365,13 +371,13 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "smoltcp" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe46639fd2ec79eadf8fe719f237a7a0bd4dac5d957f1ca5bbdbc1c3c39e53a" +checksum = "d2308a1657c8db1f5b4993bab4e620bdbe5623bd81f254cf60326767bb243237" dependencies = [ "bitflags", "byteorder", - "managed", + "managed 0.8.0", ] [[package]] diff --git a/artiq/firmware/bootloader/Cargo.toml b/artiq/firmware/bootloader/Cargo.toml index 5c2a54ba5..9e5ba820a 100644 --- a/artiq/firmware/bootloader/Cargo.toml +++ b/artiq/firmware/bootloader/Cargo.toml @@ -16,5 +16,5 @@ build_misoc = { path = "../libbuild_misoc" } byteorder = { version = "1.0", default-features = false } crc = { version = "1.7", default-features = false } board_misoc = { path = "../libboard_misoc", features = ["uart_console", "smoltcp"] } -smoltcp = { version = "0.6.0", default-features = false, features = ["ethernet", "proto-ipv4", "proto-ipv6", "socket-tcp"] } +smoltcp = { version = "0.8.0", default-features = false, features = ["medium-ethernet", "proto-ipv4", "proto-ipv6", "socket-tcp"] } riscv = { version = "0.6.0", features = ["inline-asm"] } diff --git a/artiq/firmware/bootloader/main.rs b/artiq/firmware/bootloader/main.rs index 555f8b0b6..c496a79b7 100644 --- a/artiq/firmware/bootloader/main.rs +++ b/artiq/firmware/bootloader/main.rs @@ -18,6 +18,8 @@ use board_misoc::slave_fpga; use board_misoc::{clock, ethmac, net_settings}; use board_misoc::uart_console::Console; use riscv::register::{mcause, mepc, mtval}; +use smoltcp::iface::SocketStorage; +use smoltcp::wire::{HardwareAddress, IpAddress, Ipv4Address}; fn check_integrity() -> bool { extern { @@ -396,6 +398,9 @@ fn network_boot() { println!("Initializing network..."); + // Assuming only one socket is ever needed by the bootloader. + // The smoltcp reuses the listening socket when the connection is established. + let mut sockets = [SocketStorage::EMPTY]; let mut net_device = unsafe { ethmac::EthernetDevice::new() }; net_device.reset_phy_if_any(); @@ -405,22 +410,25 @@ fn network_boot() { let net_addresses = net_settings::get_adresses(); println!("Network addresses: {}", net_addresses); let mut ip_addrs = [ - IpCidr::new(net_addresses.ipv4_addr, 0), + IpCidr::new(IpAddress::Ipv4(Ipv4Address::UNSPECIFIED), 0), IpCidr::new(net_addresses.ipv6_ll_addr, 0), IpCidr::new(net_addresses.ipv6_ll_addr, 0) ]; + if let net_settings::Ipv4AddrConfig::Static(ipv4) = net_addresses.ipv4_addr { + ip_addrs[0] = IpCidr::new(IpAddress::Ipv4(ipv4), 0); + } let mut interface = match net_addresses.ipv6_addr { Some(addr) => { ip_addrs[2] = IpCidr::new(addr, 0); - smoltcp::iface::EthernetInterfaceBuilder::new(net_device) - .ethernet_addr(net_addresses.hardware_addr) + smoltcp::iface::InterfaceBuilder::new(net_device, &mut sockets[..]) + .hardware_addr(HardwareAddress::Ethernet(net_addresses.hardware_addr)) .ip_addrs(&mut ip_addrs[..]) .neighbor_cache(neighbor_cache) .finalize() } None => - smoltcp::iface::EthernetInterfaceBuilder::new(net_device) - .ethernet_addr(net_addresses.hardware_addr) + smoltcp::iface::InterfaceBuilder::new(net_device, &mut sockets[..]) + .hardware_addr(HardwareAddress::Ethernet(net_addresses.hardware_addr)) .ip_addrs(&mut ip_addrs[..2]) .neighbor_cache(neighbor_cache) .finalize() @@ -429,14 +437,10 @@ fn network_boot() { let mut rx_storage = [0; 4096]; let mut tx_storage = [0; 128]; - let mut socket_set_entries: [_; 1] = Default::default(); - let mut sockets = - smoltcp::socket::SocketSet::new(&mut socket_set_entries[..]); - let tcp_rx_buffer = smoltcp::socket::TcpSocketBuffer::new(&mut rx_storage[..]); let tcp_tx_buffer = smoltcp::socket::TcpSocketBuffer::new(&mut tx_storage[..]); let tcp_socket = smoltcp::socket::TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer); - let tcp_handle = sockets.add(tcp_socket); + let tcp_handle = interface.add_socket(tcp_socket); let mut net_conn = NetConn::new(); let mut boot_time = None; @@ -446,7 +450,7 @@ fn network_boot() { loop { let timestamp = clock::get_ms() as i64; { - let socket = &mut *sockets.get::(tcp_handle); + let socket = &mut *interface.get_socket::(tcp_handle); match boot_time { None => { @@ -475,7 +479,7 @@ fn network_boot() { } } - match interface.poll(&mut sockets, smoltcp::time::Instant::from_millis(timestamp)) { + match interface.poll(smoltcp::time::Instant::from_millis(timestamp)) { Ok(_) => (), Err(smoltcp::Error::Unrecognized) => (), Err(err) => println!("Network error: {}", err) diff --git a/artiq/firmware/libboard_misoc/Cargo.toml b/artiq/firmware/libboard_misoc/Cargo.toml index 0d8d705cc..69c1d46d3 100644 --- a/artiq/firmware/libboard_misoc/Cargo.toml +++ b/artiq/firmware/libboard_misoc/Cargo.toml @@ -15,7 +15,7 @@ build_misoc = { path = "../libbuild_misoc" } [dependencies] byteorder = { version = "1.0", default-features = false } log = { version = "0.4", default-features = false, optional = true } -smoltcp = { version = "0.6.0", default-features = false, optional = true } +smoltcp = { version = "0.8.0", default-features = false, optional = true } riscv = { version = "0.6.0", features = ["inline-asm"] } [features] diff --git a/artiq/firmware/libboard_misoc/net_settings.rs b/artiq/firmware/libboard_misoc/net_settings.rs index 2f02cb157..36c85319f 100644 --- a/artiq/firmware/libboard_misoc/net_settings.rs +++ b/artiq/firmware/libboard_misoc/net_settings.rs @@ -1,15 +1,43 @@ use core::fmt; +use core::fmt::{Display, Formatter}; +use core::str::FromStr; -use smoltcp::wire::{EthernetAddress, IpAddress}; +use smoltcp::wire::{EthernetAddress, IpAddress, Ipv4Address}; use config; #[cfg(soc_platform = "kasli")] use i2c_eeprom; +pub enum Ipv4AddrConfig { + UseDhcp, + Static(Ipv4Address), +} + +impl FromStr for Ipv4AddrConfig { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(if s == "use_dhcp" { + Ipv4AddrConfig::UseDhcp + } else { + Ipv4AddrConfig::Static(Ipv4Address::from_str(s)?) + }) + } +} + +impl Display for Ipv4AddrConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Ipv4AddrConfig::UseDhcp => write!(f, "use_dhcp"), + Ipv4AddrConfig::Static(ipv4) => write!(f, "{}", ipv4) + } + } +} + pub struct NetAddresses { pub hardware_addr: EthernetAddress, - pub ipv4_addr: IpAddress, + pub ipv4_addr: Ipv4AddrConfig, pub ipv6_ll_addr: IpAddress, pub ipv6_addr: Option } @@ -47,12 +75,7 @@ pub fn get_adresses() -> NetAddresses { let ipv4_addr; match config::read_str("ip", |r| r.map(|s| s.parse())) { Ok(Ok(addr)) => ipv4_addr = addr, - _ => { - #[cfg(soc_platform = "kasli")] - { ipv4_addr = IpAddress::v4(192, 168, 1, 70); } - #[cfg(soc_platform = "kc705")] - { ipv4_addr = IpAddress::v4(192, 168, 1, 50); } - } + _ => ipv4_addr = Ipv4AddrConfig::UseDhcp, } let ipv6_ll_addr = IpAddress::v6( diff --git a/artiq/firmware/runtime/Cargo.toml b/artiq/firmware/runtime/Cargo.toml index ee987ad2d..85d05768c 100644 --- a/artiq/firmware/runtime/Cargo.toml +++ b/artiq/firmware/runtime/Cargo.toml @@ -27,9 +27,13 @@ board_misoc = { path = "../libboard_misoc", features = ["uart_console", "smoltcp logger_artiq = { path = "../liblogger_artiq" } board_artiq = { path = "../libboard_artiq" } proto_artiq = { path = "../libproto_artiq", features = ["log", "alloc"] } -smoltcp = { version = "0.6.0", default-features = false, features = ["alloc", "ethernet", "proto-ipv4", "proto-ipv6", "socket-tcp"] } riscv = { version = "0.6.0", features = ["inline-asm"] } +[dependencies.smoltcp] +version = "0.8.0" +default-features = false +features = ["alloc", "medium-ethernet", "proto-ipv4", "proto-ipv6", "socket-tcp", "socket-dhcpv4"] + [dependencies.fringe] git = "https://git.m-labs.hk/M-Labs/libfringe.git" rev = "3ecbe5" diff --git a/artiq/firmware/runtime/analyzer.rs b/artiq/firmware/runtime/analyzer.rs index 2e9d74c14..5b090f732 100644 --- a/artiq/firmware/runtime/analyzer.rs +++ b/artiq/firmware/runtime/analyzer.rs @@ -79,5 +79,7 @@ pub fn thread(io: Io) { Ok(()) => (), Err(err) => error!("analyzer aborted: {}", err) } + + stream.close().expect("analyzer: close socket") } } diff --git a/artiq/firmware/runtime/dhcp.rs b/artiq/firmware/runtime/dhcp.rs new file mode 100644 index 000000000..0e7d3e438 --- /dev/null +++ b/artiq/firmware/runtime/dhcp.rs @@ -0,0 +1,59 @@ +use board_misoc::clock; +use sched; +use sched::Dhcpv4Socket; +use smoltcp::socket::Dhcpv4Event; +use smoltcp::wire::{Ipv4Address, Ipv4Cidr}; + +pub fn dhcp_thread(io: sched::Io) { + let mut socket = Dhcpv4Socket::new(&io); + let mut last_ip: Option = None; + let mut done_reset = false; + let start_time = clock::get_ms(); + + loop { + // A significant amount of the time our first discover isn't received + // by the server. This is likely to be because the ethernet device isn't quite + // ready at the point that it is sent. The following makes recovery from + // that faster. + if !done_reset && last_ip.is_none() && start_time + 1000 < clock::get_ms() { + info!("Didn't get initial IP in first second. Assuming packet loss, trying a reset."); + socket.reset(); + done_reset = true; + } + if let Some(event) = socket.poll() { + match event { + Dhcpv4Event::Configured(config) => { + // Only compare the ip address in the config with previous config because we + // ignore the rest of the config i.e. we don't do any DNS or require a default + // gateway. + let changed = if let Some(last_ip) = last_ip { + if config.address != last_ip { + info!("IP address changed {} -> {}", last_ip, config.address); + true + } else { + false + } + } else { + info!("Acquired IP address: None -> {}", config.address); + true + }; + if changed { + last_ip = Some(config.address); + io.set_ipv4_address(&config.address); + } + } + Dhcpv4Event::Deconfigured => { + if let Some(ip) = last_ip { + info!("Lost IP address {} -> None", ip); + io.set_ipv4_address(&Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0)); + last_ip = None; + } + // We always get one of these events at the start, ignore that one + } + } + } + // We want to poll after every poll of the interface. So we need to + // do a minimal yield here. + io.relinquish().unwrap(); + } +} diff --git a/artiq/firmware/runtime/ip_addr_storage.rs b/artiq/firmware/runtime/ip_addr_storage.rs new file mode 100644 index 000000000..310c21941 --- /dev/null +++ b/artiq/firmware/runtime/ip_addr_storage.rs @@ -0,0 +1,40 @@ +use smoltcp::iface::{Interface, InterfaceBuilder}; +use smoltcp::phy::Device; +use smoltcp::wire::{IpAddress, IpCidr, Ipv4Address, Ipv4Cidr}; +use board_misoc::net_settings::{Ipv4AddrConfig, NetAddresses}; + + +const IPV4_INDEX: usize = 0; +const IPV6_LL_INDEX: usize = 1; +const IPV6_INDEX: usize = 2; +const IP_ADDRESS_STORAGE_SIZE: usize = 3; + +pub trait InterfaceBuilderEx { + fn init_ip_addrs(self, net_addresses: &NetAddresses) -> Self; +} + +impl<'a, DeviceT: for<'d> Device<'d>> InterfaceBuilderEx for InterfaceBuilder<'a, DeviceT> { + fn init_ip_addrs(self, net_addresses: &NetAddresses) -> Self { + let mut storage = [ + IpCidr::new(IpAddress::Ipv4(Ipv4Address::UNSPECIFIED), 0); IP_ADDRESS_STORAGE_SIZE + ]; + if let Ipv4AddrConfig::Static(ipv4) = net_addresses.ipv4_addr { + storage[IPV4_INDEX] = IpCidr::new(IpAddress::Ipv4(ipv4), 0); + } + storage[IPV6_LL_INDEX] = IpCidr::new(net_addresses.ipv6_ll_addr, 0); + if let Some(ipv6) = net_addresses.ipv6_addr { + storage[IPV6_INDEX] = IpCidr::new(ipv6, 0); + } + self.ip_addrs(storage) + } +} + +pub trait InterfaceEx { + fn update_ipv4_addr(&mut self, addr: &Ipv4Cidr); +} + +impl<'a, DeviceT: for<'d> Device<'d>> InterfaceEx for Interface<'a, DeviceT> { + fn update_ipv4_addr(&mut self, addr: &Ipv4Cidr) { + self.update_ip_addrs(|storage| storage[IPV4_INDEX] = IpCidr::Ipv4(*addr)) + } +} diff --git a/artiq/firmware/runtime/main.rs b/artiq/firmware/runtime/main.rs index 2846b02f2..f91f4d391 100644 --- a/artiq/firmware/runtime/main.rs +++ b/artiq/firmware/runtime/main.rs @@ -27,11 +27,12 @@ extern crate riscv; use core::cell::RefCell; use core::convert::TryFrom; -use smoltcp::wire::IpCidr; +use smoltcp::wire::HardwareAddress; use board_misoc::{csr, ident, clock, spiflash, config, net_settings, pmp, boot}; #[cfg(has_ethmac)] use board_misoc::ethmac; +use board_misoc::net_settings::{Ipv4AddrConfig}; #[cfg(has_drtio)] use board_artiq::drtioaux; use board_artiq::drtio_routing; @@ -41,6 +42,7 @@ use proto_artiq::{mgmt_proto, moninj_proto, rpc_proto, session_proto, kernel_pro use proto_artiq::analyzer_proto; use riscv::register::{mcause, mepc, mtval}; +use ip_addr_storage::InterfaceBuilderEx; mod rtio_clocking; mod rtio_mgt; @@ -58,6 +60,8 @@ mod session; mod moninj; #[cfg(has_rtio_analyzer)] mod analyzer; +mod dhcp; +mod ip_addr_storage; #[cfg(has_grabber)] fn grabber_thread(io: sched::Io) { @@ -123,54 +127,35 @@ fn startup() { net_device.reset_phy_if_any(); let net_device = { - use smoltcp::time::Instant; - use smoltcp::wire::PrettyPrinter; - use smoltcp::wire::EthernetFrame; + use smoltcp::phy::Tracer; - fn net_trace_writer(timestamp: Instant, printer: PrettyPrinter>) { - print!("\x1b[37m[{:6}.{:03}s]\n{}\x1b[0m\n", - timestamp.secs(), timestamp.millis(), printer) - } - - fn net_trace_silent(_timestamp: Instant, _printer: PrettyPrinter>) {} - - let net_trace_fn: fn(Instant, PrettyPrinter>); + // We can't create the function pointer as a separate variable here because the type of + // the packet argument Packet isn't accessible and rust's type inference isn't sufficient + // to propagate in to a local var. match config::read_str("net_trace", |r| r.map(|s| s == "1")) { - Ok(true) => net_trace_fn = net_trace_writer, - _ => net_trace_fn = net_trace_silent + Ok(true) => Tracer::new(net_device, |timestamp, packet| { + print!("\x1b[37m[{:6}.{:03}s]\n{}\x1b[0m\n", + timestamp.secs(), timestamp.millis(), packet) + }), + _ => Tracer::new(net_device, |_, _| {}), } - smoltcp::phy::EthernetTracer::new(net_device, net_trace_fn) }; let neighbor_cache = smoltcp::iface::NeighborCache::new(alloc::collections::btree_map::BTreeMap::new()); let net_addresses = net_settings::get_adresses(); info!("network addresses: {}", net_addresses); - let mut interface = match net_addresses.ipv6_addr { - Some(addr) => { - let ip_addrs = [ - IpCidr::new(net_addresses.ipv4_addr, 0), - IpCidr::new(net_addresses.ipv6_ll_addr, 0), - IpCidr::new(addr, 0) - ]; - smoltcp::iface::EthernetInterfaceBuilder::new(net_device) - .ethernet_addr(net_addresses.hardware_addr) - .ip_addrs(ip_addrs) - .neighbor_cache(neighbor_cache) - .finalize() - } - None => { - let ip_addrs = [ - IpCidr::new(net_addresses.ipv4_addr, 0), - IpCidr::new(net_addresses.ipv6_ll_addr, 0) - ]; - smoltcp::iface::EthernetInterfaceBuilder::new(net_device) - .ethernet_addr(net_addresses.hardware_addr) - .ip_addrs(ip_addrs) - .neighbor_cache(neighbor_cache) - .finalize() - } + let use_dhcp = if matches!(net_addresses.ipv4_addr, Ipv4AddrConfig::UseDhcp) { + info!("Will try to acquire an IPv4 address with DHCP"); + true + } else { + false }; + let interface = smoltcp::iface::InterfaceBuilder::new(net_device, vec![]) + .hardware_addr(HardwareAddress::Ethernet(net_addresses.hardware_addr)) + .init_ip_addrs(&net_addresses) + .neighbor_cache(neighbor_cache) + .finalize(); #[cfg(has_drtio)] let drtio_routing_table = urc::Urc::new(RefCell::new( @@ -184,9 +169,13 @@ fn startup() { drtio_routing::interconnect_disable_all(); let aux_mutex = sched::Mutex::new(); - let mut scheduler = sched::Scheduler::new(); + let mut scheduler = sched::Scheduler::new(interface); let io = scheduler.io(); + if use_dhcp { + io.spawn(4096, dhcp::dhcp_thread); + } + rtio_mgt::startup(&io, &aux_mutex, &drtio_routing_table, &up_destinations); io.spawn(4096, mgmt::thread); @@ -211,19 +200,7 @@ fn startup() { let mut net_stats = ethmac::EthernetStatistics::new(); loop { scheduler.run(); - - { - let sockets = &mut *scheduler.sockets().borrow_mut(); - loop { - let timestamp = smoltcp::time::Instant::from_millis(clock::get_ms() as i64); - match interface.poll(sockets, timestamp) { - Ok(true) => (), - Ok(false) => break, - Err(smoltcp::Error::Unrecognized) => (), - Err(err) => debug!("network error: {}", err) - } - } - } + scheduler.run_network(); if let Some(_net_stats_diff) = net_stats.update() { debug!("ethernet mac:{}", ethmac::EthernetStatistics::new()); diff --git a/artiq/firmware/runtime/mgmt.rs b/artiq/firmware/runtime/mgmt.rs index c100df05f..449ed91be 100644 --- a/artiq/firmware/runtime/mgmt.rs +++ b/artiq/firmware/runtime/mgmt.rs @@ -131,6 +131,7 @@ pub fn thread(io: Io) { Err(Error::Io(IoError::UnexpectedEnd)) => (), Err(err) => error!("aborted: {}", err) } + stream.close().expect("mgmt: close socket"); }); } } diff --git a/artiq/firmware/runtime/moninj.rs b/artiq/firmware/runtime/moninj.rs index 6feb7ec55..36d4bbb47 100644 --- a/artiq/firmware/runtime/moninj.rs +++ b/artiq/firmware/runtime/moninj.rs @@ -214,6 +214,7 @@ pub fn thread(io: Io, aux_mutex: &Mutex, routing_table: &Urc {}, Err(err) => error!("moninj aborted: {}", err) } + stream.close().expect("moninj: close socket"); }); } } diff --git a/artiq/firmware/runtime/sched.rs b/artiq/firmware/runtime/sched.rs index d6a347269..1f9c79bc2 100644 --- a/artiq/firmware/runtime/sched.rs +++ b/artiq/firmware/runtime/sched.rs @@ -2,18 +2,21 @@ use core::mem; use core::result; -use core::cell::{Cell, RefCell}; +use core::cell::{Cell, RefCell, RefMut}; use alloc::vec::Vec; use fringe::OwnedStack; use fringe::generator::{Generator, Yielder, State as GeneratorState}; use smoltcp::time::Duration; use smoltcp::Error as NetworkError; -use smoltcp::wire::IpEndpoint; -use smoltcp::socket::{SocketHandle, SocketRef}; +use smoltcp::wire::{IpEndpoint, Ipv4Cidr}; +use smoltcp::iface::{Interface, SocketHandle}; use io::{Read, Write}; use board_misoc::clock; use urc::Urc; +use board_misoc::ethmac::EthernetDevice; +use smoltcp::phy::Tracer; +use ip_addr_storage::InterfaceEx; #[derive(Fail, Debug)] pub enum Error { @@ -31,8 +34,6 @@ impl From for Error { } } -type SocketSet = ::smoltcp::socket::SocketSet<'static, 'static, 'static>; - #[derive(Debug)] struct WaitRequest { event: Option<*mut dyn FnMut() -> bool>, @@ -59,7 +60,7 @@ impl Thread { unsafe fn new(io: &Io, stack_size: usize, f: F) -> ThreadHandle where F: 'static + FnOnce(Io) + Send { let spawned = io.spawned.clone(); - let sockets = io.sockets.clone(); + let network = io.network.clone(); // Add a 4k stack guard to the stack of any new threads let stack = OwnedStack::new(stack_size + 4096); @@ -67,8 +68,8 @@ impl Thread { generator: Generator::unsafe_new(stack, |yielder, _| { f(Io { yielder: Some(yielder), - spawned: spawned, - sockets: sockets + spawned, + network }) }), waiting_for: WaitRequest { @@ -115,19 +116,21 @@ impl ThreadHandle { } } +type Network = Interface<'static, Tracer>; + pub struct Scheduler { threads: Vec, spawned: Urc>>, - sockets: Urc>, + network: Urc>, run_idx: usize, } impl Scheduler { - pub fn new() -> Scheduler { + pub fn new(network: Network) -> Scheduler { Scheduler { threads: Vec::new(), spawned: Urc::new(RefCell::new(Vec::new())), - sockets: Urc::new(RefCell::new(SocketSet::new(Vec::new()))), + network: Urc::new(RefCell::new(network)), run_idx: 0, } } @@ -136,13 +139,11 @@ impl Scheduler { Io { yielder: None, spawned: self.spawned.clone(), - sockets: self.sockets.clone() + network: self.network.clone() } } pub fn run(&mut self) { - self.sockets.borrow_mut().prune(); - self.threads.append(&mut *self.spawned.borrow_mut()); if self.threads.len() == 0 { return } @@ -188,8 +189,17 @@ impl Scheduler { } } - pub fn sockets(&self) -> &RefCell { - &*self.sockets + pub fn run_network(&mut self) { + let mut interface = self.network.borrow_mut(); + loop { + let timestamp = smoltcp::time::Instant::from_millis(clock::get_ms() as i64); + match interface.poll(timestamp) { + Ok(true) => (), + Ok(false) => break, + Err(smoltcp::Error::Unrecognized) => (), + Err(err) => debug!("network error: {}", err) + } + } } } @@ -197,7 +207,7 @@ impl Scheduler { pub struct Io<'a> { yielder: Option<&'a Yielder>, spawned: Urc>>, - sockets: Urc>, + network: Urc>, } impl<'a> Io<'a> { @@ -264,6 +274,10 @@ impl<'a> Io<'a> { pub fn join(&self, handle: ThreadHandle) -> Result<(), Error> { self.until(move || handle.terminated()) } + + pub fn set_ipv4_address(&self, addr: &Ipv4Cidr) { + self.network.borrow_mut().update_ipv4_addr(addr) + } } #[derive(Clone)] @@ -291,10 +305,10 @@ impl<'a> Drop for MutexGuard<'a> { macro_rules! until { ($socket:expr, $ty:ty, |$var:ident| $cond:expr) => ({ - let (sockets, handle) = ($socket.io.sockets.clone(), $socket.handle); + let (network, handle) = ($socket.io.network.clone(), $socket.handle); $socket.io.until(move || { - let mut sockets = sockets.borrow_mut(); - let $var = sockets.get::<$ty>(handle); + let mut network = network.borrow_mut(); + let $var = network.get_socket::<$ty>(handle); $cond }) }) @@ -316,9 +330,9 @@ impl<'a> TcpListener<'a> { fn new_lower(io: &'a Io<'a>, buffer_size: usize) -> SocketHandle { let rx_buffer = vec![0; buffer_size]; let tx_buffer = vec![0; buffer_size]; - io.sockets + io.network .borrow_mut() - .add(TcpSocketLower::new( + .add_socket(TcpSocketLower::new( TcpSocketBuffer::new(rx_buffer), TcpSocketBuffer::new(tx_buffer))) } @@ -333,9 +347,9 @@ impl<'a> TcpListener<'a> { } fn with_lower(&self, f: F) -> R - where F: FnOnce(SocketRef) -> R { - let mut sockets = self.io.sockets.borrow_mut(); - let result = f(sockets.get(self.handle.get())); + where F: FnOnce(&mut TcpSocketLower) -> R { + let mut network = self.io.network.borrow_mut(); + let result = f(network.get_socket(self.handle.get())); result } @@ -353,7 +367,7 @@ impl<'a> TcpListener<'a> { pub fn listen>(&self, endpoint: T) -> Result<(), Error> { let endpoint = endpoint.into(); - self.with_lower(|mut s| s.listen(endpoint)) + self.with_lower(|s| s.listen(endpoint)) .map(|()| { self.endpoint.set(endpoint); () @@ -361,14 +375,18 @@ impl<'a> TcpListener<'a> { .map_err(|err| err.into()) } + /// Accept a TCP connection + /// + /// When the returned TcpStream is dropped it is immediately forgotten about. In order to + /// ensure that pending data is sent and the far end is notified, `close` must be called. pub fn accept(&self) -> Result, Error> { // We're waiting until at least one half of the connection becomes open. // This handles the case where a remote socket immediately sends a FIN-- // that still counts as accepting even though nothing may be sent. - let (sockets, handle) = (self.io.sockets.clone(), self.handle.get()); + let (network, handle) = (self.io.network.clone(), self.handle.get()); self.io.until(move || { - let mut sockets = sockets.borrow_mut(); - let socket = sockets.get::(handle); + let mut network = network.borrow_mut(); + let socket = network.get_socket::(handle); socket.may_send() || socket.may_recv() })?; @@ -385,14 +403,14 @@ impl<'a> TcpListener<'a> { } pub fn close(&self) { - self.with_lower(|mut s| s.close()) + self.with_lower(|s| s.close()) } } impl<'a> Drop for TcpListener<'a> { fn drop(&mut self) { - self.with_lower(|mut s| s.close()); - self.io.sockets.borrow_mut().release(self.handle.get()) + self.with_lower(|s| s.close()); + self.io.network.borrow_mut().remove_socket(self.handle.get()); } } @@ -416,9 +434,9 @@ impl<'a> TcpStream<'a> { } fn with_lower(&self, f: F) -> R - where F: FnOnce(SocketRef) -> R { - let mut sockets = self.io.sockets.borrow_mut(); - let result = f(sockets.get(self.handle)); + where F: FnOnce(&mut TcpSocketLower) -> R { + let mut network = self.io.network.borrow_mut(); + let result = f(network.get_socket(self.handle)); result } @@ -455,7 +473,7 @@ impl<'a> TcpStream<'a> { } pub fn set_timeout(&self, value: Option) { - self.with_lower(|mut s| s.set_timeout(value.map(Duration::from_millis))) + self.with_lower(|s| s.set_timeout(value.map(Duration::from_millis))) } pub fn keep_alive(&self) -> Option { @@ -463,11 +481,11 @@ impl<'a> TcpStream<'a> { } pub fn set_keep_alive(&self, value: Option) { - self.with_lower(|mut s| s.set_keep_alive(value.map(Duration::from_millis))) + self.with_lower(|s| s.set_keep_alive(value.map(Duration::from_millis))) } pub fn close(&self) -> Result<(), Error> { - self.with_lower(|mut s| s.close()); + self.with_lower(|s| s.close()); until!(self, TcpSocketLower, |s| !s.is_open())?; // right now the socket may be in TIME-WAIT state. if we don't give it a chance to send // a packet, and the user code executes a loop { s.listen(); s.read(); s.close(); } @@ -481,23 +499,33 @@ impl<'a> Read for TcpStream<'a> { fn read(&mut self, buf: &mut [u8]) -> Result { // Only borrow the underlying socket for the span of the next statement. - let result = self.with_lower(|mut s| s.recv_slice(buf)); + let result = self.with_lower(|s| s.recv_slice(buf)); match result { // Slow path: we need to block until buffer is non-empty. Ok(0) => { until!(self, TcpSocketLower, |s| s.can_recv() || !s.may_recv())?; - match self.with_lower(|mut s| s.recv_slice(buf)) { + match self.with_lower(|s| s.recv_slice(buf)) { Ok(length) => Ok(length), + Err(NetworkError::Finished) | Err(NetworkError::Illegal) => Ok(0), - _ => unreachable!() + Err(e) => { + panic!("Unexpected error from smoltcp: {}", e); + } } } // Fast path: we had data in buffer. Ok(length) => Ok(length), + // We've received a fin. + Err(NetworkError::Finished) | // Error path: the receive half of the socket is not open. Err(NetworkError::Illegal) => Ok(0), // No other error may be returned. - Err(_) => unreachable!() + Err(e) => { + // This could return Err(Error::Network(e)) rather than panic, + // but I expect that'll just cause a panic later perhaps with + // less interesting context. + panic!("Unexpected error from smoltcp: {}", e); + } } } } @@ -508,12 +536,12 @@ impl<'a> Write for TcpStream<'a> { fn write(&mut self, buf: &[u8]) -> Result { // Only borrow the underlying socket for the span of the next statement. - let result = self.with_lower(|mut s| s.send_slice(buf)); + let result = self.with_lower(|s| s.send_slice(buf)); match result { // Slow path: we need to block until buffer is non-full. Ok(0) => { until!(self, TcpSocketLower, |s| s.can_send() || !s.may_send())?; - match self.with_lower(|mut s| s.send_slice(buf)) { + match self.with_lower(|s| s.send_slice(buf)) { Ok(length) => Ok(length), Err(NetworkError::Illegal) => Ok(0), _ => unreachable!() @@ -540,7 +568,62 @@ impl<'a> Write for TcpStream<'a> { impl<'a> Drop for TcpStream<'a> { fn drop(&mut self) { - self.with_lower(|mut s| s.close()); - self.io.sockets.borrow_mut().release(self.handle) + // There's no point calling the lower close here unless we also defer the removal of the + // socket from smoltcp until it's had a chance to process the event + let (unsent_bytes, is_open) = self.with_lower( + |s| (s.send_queue(), s.is_open()) + ); + if is_open { + warn!( + "Dropping open TcpStream in state {}, with {} unsent bytes", + self.with_lower(|s| s.state()), unsent_bytes + ) + } else if unsent_bytes != 0 { + debug!("Dropping socket with {} bytes unsent", unsent_bytes) + } + + self.io.network.borrow_mut().remove_socket(self.handle); + } +} + +pub struct Dhcpv4Socket<'a> { + io: &'a Io<'a>, + handle: SocketHandle, +} + +impl<'a> Dhcpv4Socket<'a> { + + fn new_lower(io: &'a Io<'a>) -> SocketHandle { + let socket = smoltcp::socket::Dhcpv4Socket::new(); + io.network.borrow_mut().add_socket(socket) + } + + pub fn new(io: &'a Io<'a>) -> Self { + Self { + io, + handle: Self::new_lower(io) + } + } + + fn lower(&mut self) -> RefMut { + RefMut::map( + self.io.network.borrow_mut(), + |network| network.get_socket::(self.handle), + ) + } + + pub fn poll(&mut self) -> Option { + self.lower().poll() + } + + pub fn reset(&mut self) { + self.lower().reset() + } +} + +impl<'a> Drop for Dhcpv4Socket<'a> { + fn drop(&mut self) { + let mut network = self.io.network.borrow_mut(); + network.remove_socket(self.handle); } } diff --git a/artiq/firmware/runtime/session.rs b/artiq/firmware/runtime/session.rs index aec47afe1..bd1562269 100644 --- a/artiq/firmware/runtime/session.rs +++ b/artiq/firmware/runtime/session.rs @@ -650,6 +650,7 @@ pub fn thread(io: Io, aux_mutex: &Mutex, error!("session aborted: {}", err); } } + stream.close().expect("session: close socket"); }); } diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 178593a07..940149631 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -225,6 +225,7 @@ _templates = [ "HIST_BIN_BOUNDARIES_DATASET " "HISTS_COUNTS_DATASET"), ("Image", "${artiq_applet}image IMG_DATASET"), + ("Progress bar", "${artiq_applet}progress_bar VALUE"), ] diff --git a/artiq/test/coredevice/test_embedding.py b/artiq/test/coredevice/test_embedding.py index 55fa79c94..b93dea7e8 100644 --- a/artiq/test/coredevice/test_embedding.py +++ b/artiq/test/coredevice/test_embedding.py @@ -539,3 +539,19 @@ class _Alignment(EnvExperiment): class AlignmentTest(ExperimentCase): def test_tuple(self): self.create(_Alignment).run() + + +class _NumpyQuoting(EnvExperiment): + def build(self): + self.setattr_device("core") + + @kernel + def run(self): + a = numpy.array([10, 20]) + b = numpy.sqrt(4.0) + + +class NumpyQuotingTest(ExperimentCase): + def test_issue_1871(self): + """Ensure numpy.array() does not break NumPy math functions""" + self.create(_NumpyQuoting).run() diff --git a/doc/manual/installing.rst b/doc/manual/installing.rst index 3821fefd8..af1d58d43 100644 --- a/doc/manual/installing.rst +++ b/doc/manual/installing.rst @@ -231,13 +231,19 @@ If you purchased a Kasli device from M-Labs, it usually comes with the IP addres and then reboot the device (with ``artiq_flash start`` or a power cycle). -In other cases, install OpenOCD as before, and flash the IP (and, if necessary, MAC) addresses directly: :: +If the ip config field is not set, or set to "use_dhcp" then the device will attempt to obtain an IP address using +DHCP. If a static IP address is wanted, install OpenOCD as before, and flash the IP (and, if necessary, MAC) addresses +directly: :: $ artiq_mkfs flash_storage.img -s mac xx:xx:xx:xx:xx:xx -s ip xx.xx.xx.xx $ artiq_flash -t [board] -V [variant] -f flash_storage.img storage start For Kasli devices, flashing a MAC address is not necessary as they can obtain it from their EEPROM. +If DHCP has been used the address can be found in the console output, which can be viewed using: :: + + $ python -m misoc.tools.flterm /dev/ttyUSB2 + Check that you can ping the device. If ping fails, check that the Ethernet link LED is ON - on Kasli, it is the LED next to the SFP0 connector. As a next step, look at the messages emitted on the UART during boot. Use a program such as flterm or PuTTY to connect to the device's serial port at 115200bps 8-N-1 and reboot the device. On Kasli, the serial port is on FTDI channel 2 with v1.1 hardware (with channel 0 being JTAG) and on FTDI channel 1 with v1.0 hardware. If you want to use IPv6, the device also has a link-local address that corresponds to its EUI-64, and an additional arbitrary IPv6 address can be defined by using the ``ip6`` configuration key. All IPv4 and IPv6 addresses can be used at the same time. diff --git a/flake.nix b/flake.nix index 244bbb491..f8e53232e 100644 --- a/flake.nix +++ b/flake.nix @@ -176,10 +176,11 @@ pkgs.stdenv.mkDerivation { name = "artiq-board-${target}-${variant}"; phases = [ "buildPhase" "checkPhase" "installPhase" ]; - cargoDeps = rustPlatform.fetchCargoTarball { - name = "artiq-firmware-cargo-deps"; - src = "${self}/artiq/firmware"; - sha256 = "sha256-YyycMsDzR+JRcMZJd6A/CRi2J9nKmaWY/KXUnAQaZ00="; + cargoDeps = rustPlatform.importCargoLock { + lockFile = ./artiq/firmware/Cargo.lock; + outputHashes = { + "fringe-1.2.1" = "sha256-m4rzttWXRlwx53LWYpaKuU5AZe4GSkbjHS6oINt5d3Y="; + }; }; nativeBuildInputs = [ (pkgs.python3.withPackages(ps: [ ps.jsonschema migen misoc artiq])) @@ -359,6 +360,9 @@ pkgs.python3Packages.sphinx pkgs.python3Packages.sphinx_rtd_theme pkgs.python3Packages.sphinx-argparse sphinxcontrib-wavedrom latex-artiq-manual ]; + shellHook = '' + export LIBARTIQ_SUPPORT=`libartiq-support` + ''; }; hydraJobs = {