Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ pub struct Args {
/// Geometry scale around center (0,0); range: (0, 10]
#[arg(long, default_value_t = 1.0, value_parser = parse_scale)]
pub scale: f32,

/// Output point rate in points per second
#[arg(long, default_value_t = 30_000, value_parser = clap::value_parser!(u32).range(1..))]
pub pps: u32,
}

#[derive(Copy, Clone, ValueEnum)]
Expand Down
2 changes: 1 addition & 1 deletion examples/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fn main() -> Result<()> {

let device = open_device(&device_info.id)?;

let config = FrameSessionConfig::new(30_000);
let config = FrameSessionConfig::new(args.pps);
let (session, info) = device.start_frame_session(config)?;

println!(
Expand Down
2 changes: 1 addition & 1 deletion examples/reconnect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ fn main() -> Result<()> {
println!(" Found: {} ({})", device_info.name, device_info.kind);

// Open device and create a reconnecting stream via config
let config = StreamConfig::new(30_000).with_reconnect(
let config = StreamConfig::new(args.pps).with_reconnect(
ReconnectConfig::new()
.backoff(Duration::from_secs(1))
.on_disconnect(|err| eprintln!("\nDisconnected: {}", err))
Expand Down
2 changes: 1 addition & 1 deletion examples/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn main() -> Result<()> {
let device = open_device(&device_info.id)?;

// Start streaming
let config = StreamConfig::new(30_000);
let config = StreamConfig::new(args.pps);
let (stream, info) = device.start_stream(config)?;

println!(
Expand Down
3 changes: 3 additions & 0 deletions src/protocols/lasercube_network/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,9 @@ mod tests {
backend.connect().unwrap();

backend.set_shutter(true).unwrap();
backend
.try_write_points(30_000, &[LaserPoint::blanked(0.0, 0.0)])
.unwrap();
assert!(
mock::wait_until(Duration::from_millis(1000), || dac
.cmd_packets()
Expand Down
24 changes: 23 additions & 1 deletion src/protocols/lasercube_network/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ pub struct LaserCubeNetworkDiscoverer {
impl LaserCubeNetworkDiscoverer {
pub fn new() -> Self {
Self {
timeout: Duration::from_millis(100),
// Wi-Fi LaserCubes can take well over 100 ms to answer broadcast
// discovery, especially while their output worker is active.
timeout: Duration::from_millis(500),
}
}
}
Expand Down Expand Up @@ -153,6 +155,26 @@ fn send_discovery_broadcasts(
interface_sockets: &[UdpSocket],
) {
let alive_socket = passive_socket.unwrap_or(socket);

// Broadcast discovery is unreliable on some LaserCube AP firmware. Allow a
// known address to be probed from the same socket that receives the reply.
if let Some(ip) = std::env::var_os("LASER_DAC_LASERCUBE_IP") {
match ip.to_string_lossy().parse::<Ipv4Addr>() {
Ok(ip) => {
let addr = SocketAddrV4::new(ip, CMD_PORT);
if let Err(e) = socket.send_to(&command::get_full_info(), addr) {
log::warn!("discovery: unicast probe to {addr} failed: {e}");
}
// A known address is authoritative. Avoid also flooding fragile
// AP-mode firmware with every broadcast discovery variant.
return;
}
Err(e) => log::warn!(
"discovery: ignoring invalid LASER_DAC_LASERCUBE_IP={:?}: {e}",
ip
),
}
}
for iface in interfaces {
log::debug!(
"discovery: interface {} netmask {} -> directed broadcast {}",
Expand Down
14 changes: 1 addition & 13 deletions src/protocols/lasercube_network/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use std::time::Duration;

use super::command;
use super::profiles::ConnectionProfile;
use super::protocol::DEFAULT_POINT_RATE;
use super::status::LaserCubeNetworkStatus;

pub use handle::TransportHandle;
Expand Down Expand Up @@ -111,7 +110,6 @@ pub(super) fn startup_commands(
let mut commands = vec![
command::set_output(false).to_vec(),
command::enable_buffer_size_response(true).to_vec(),
command::set_rate(super::clamp_point_rate(status, DEFAULT_POINT_RATE)).to_vec(),
];
if command::threshold_supported(status) {
let threshold = profile.remote_buffer_cutoff;
Expand All @@ -133,7 +131,6 @@ pub(super) fn would_block(err: &io::Error) -> bool {
mod tests {
use std::net::{IpAddr, Ipv4Addr};

use super::super::command;
use super::super::profiles::ConnectionProfile;
use super::super::status::LaserCubeNetworkStatus;
use super::*;
Expand All @@ -146,17 +143,8 @@ mod tests {
let commands = startup_commands(&status, profile);
assert_eq!(commands[0], vec![0x80, 0x00]);
assert_eq!(commands[1], vec![0x78, 0x01]);
assert_eq!(commands[2], vec![0x82, 0x30, 0x75, 0x00, 0x00]);
assert!(commands.iter().all(|cmd| cmd.first() != Some(&0x82)));
assert!(commands.iter().all(|cmd| cmd.first() != Some(&0x8D)));
assert!(commands.iter().all(|cmd| cmd.first() != Some(&0xA9)));
}

#[test]
fn startup_rate_is_clamped_to_advertised_device_max() {
let mut status = LaserCubeNetworkStatus::minimal(IpAddr::V4(Ipv4Addr::LOCALHOST));
status.point_rate_max = 20_000;
let profile = ConnectionProfile::unknown_conservative(6000);
let commands = startup_commands(&status, profile);
assert_eq!(commands[2], command::set_rate(20_000).to_vec());
}
}
42 changes: 38 additions & 4 deletions src/protocols/lasercube_network/transport/handle.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use socket2::SockRef;
use std::net::UdpSocket;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{self, Sender, SyncSender, TrySendError};
Expand All @@ -7,7 +8,7 @@ use std::time::Duration;

use super::super::command;
use super::super::error::CommunicationError;
use super::super::protocol::CMD_GET_FULL_INFO;
use super::super::protocol::{CMD_GET_FULL_INFO, CMD_PORT, DATA_PORT};
use super::state::SharedTransportState;
use super::worker::TransportWorker;
use super::{
Expand All @@ -19,6 +20,8 @@ use super::{
const CONNECT_HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(300);
/// Number of retries (in addition to the first attempt) for the handshake.
const CONNECT_HANDSHAKE_RETRIES: usize = 2;
/// Socket buffer size used by the reference LaserCube network driver.
const SOCKET_BUFFER_BYTES: usize = 5_250_000;

pub struct TransportHandle {
tx: SyncSender<TransportCommand>,
Expand All @@ -30,11 +33,13 @@ pub struct TransportHandle {

impl TransportHandle {
pub fn connect(device: AddressedDevice) -> Result<Self, CommunicationError> {
let cmd_socket = UdpSocket::bind("0.0.0.0:0")?;
let cmd_socket = bind_transport_socket(CMD_PORT, device.cmd_port)?;
cmd_socket.connect(device.cmd_addr())?;
configure_socket_buffers(&cmd_socket)?;

let data_socket = UdpSocket::bind("0.0.0.0:0")?;
let data_socket = bind_transport_socket(DATA_PORT, device.data_port)?;
data_socket.connect(device.data_addr())?;
configure_socket_buffers(&data_socket)?;
data_socket.set_nonblocking(true)?;

for cmd in startup_commands(&device.status, device.profile) {
Expand Down Expand Up @@ -142,6 +147,35 @@ impl Drop for TransportHandle {
}
}

/// Production firmware and the reference driver use the well-known ports at
/// both ends. Loopback tests use ephemeral remote ports and therefore keep an
/// ephemeral local endpoint. If another local LaserCube client already owns a
/// production port, fall back rather than making discovery/open fail outright.
fn bind_transport_socket(
preferred_port: u16,
remote_port: u16,
) -> Result<UdpSocket, CommunicationError> {
if remote_port == preferred_port {
match UdpSocket::bind(("0.0.0.0", preferred_port)) {
Ok(socket) => return Ok(socket),
Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
log::warn!(
"LaserCube network local UDP port {preferred_port} is already in use; falling back to an ephemeral port"
);
}
Err(e) => return Err(e.into()),
}
}
Ok(UdpSocket::bind("0.0.0.0:0")?)
}

fn configure_socket_buffers(socket: &UdpSocket) -> Result<(), CommunicationError> {
let socket = SockRef::from(socket);
socket.set_send_buffer_size(SOCKET_BUFFER_BYTES)?;
socket.set_recv_buffer_size(SOCKET_BUFFER_BYTES)?;
Ok(())
}

/// Request full-info and wait for a reply, retrying a couple of times with a
/// short timeout. Returns an error if the device never answers, so `connect()`
/// fails fast for an unreachable cube. The socket is left blocking; the caller
Expand Down Expand Up @@ -213,7 +247,6 @@ mod tests {
let cmds = dac.cmd_packets();
cmds.iter().any(|p| p.as_slice() == [0x80, 0x00]) // set_output(false)
&& cmds.iter().any(|p| p.as_slice() == [0x78, 0x01]) // enable buffer size resp
&& cmds.iter().any(|p| p.first() == Some(&0x82)) // set_rate
}),
"startup commands missing: {:?}",
dac.cmd_packets()
Expand All @@ -226,6 +259,7 @@ mod tests {
let handle = TransportHandle::connect(device_for(&dac)).unwrap();

handle.set_output(true).unwrap();
handle.enqueue(30_000, vec![Point::blank()]).unwrap();
assert!(
mock::wait_until(Duration::from_millis(1000), || dac
.cmd_packets()
Expand Down
Loading
Loading