diff --git a/src/packet.rs b/src/packet.rs index 74789d5..1900e25 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -316,8 +316,12 @@ pub const NO_SYNC_UNIVERSE: u16 = 0; /// The timeout before data loss is assumed for an E131 source, as defined in Appendix A of ANSI E1.31-2018. pub const E131_NETWORK_DATA_LOSS_TIMEOUT: Duration = Duration::from_millis(2500); -/// The timeout before a discovered source is assumed to be lost as defined in section 12.2 of ANSI E1.31-2018. -pub const UNIVERSE_DISCOVERY_SOURCE_TIMEOUT: Duration = E131_NETWORK_DATA_LOSS_TIMEOUT; +/// The timeout before a discovered source is assumed to be lost. +/// +/// Sources send universe discovery packets every `E131_UNIVERSE_DISCOVERY_INTERVAL` (10 seconds, ANSI E1.31-2016 Appendix A), +/// so this must be longer than that interval or discovered sources expire between adverts. The standard doesn't define a +/// receiver timeout, so two intervals are used: a source is only treated as lost after missing a whole advert. +pub const UNIVERSE_DISCOVERY_SOURCE_TIMEOUT: Duration = Duration::from_secs(2 * E131_UNIVERSE_DISCOVERY_INTERVAL.as_secs()); /// Converts the given ANSI E1.31-2018 universe into an Ipv4 multicast address with the port set to the acn multicast port as defined /// in `packet::ACN_SDT_MULTICAST_PORT`. diff --git a/src/receive.rs b/src/receive.rs index 15f4141..9359e67 100644 --- a/src/receive.rs +++ b/src/receive.rs @@ -2192,6 +2192,35 @@ mod test { assert_eq!(dmx_rcv.discovered_sources[0].pages[0].universes, universes); } + /// Sources only advertise every `E131_UNIVERSE_DISCOVERY_INTERVAL`, so a discovered source must not expire before its next advert. + #[test] + fn test_discovered_source_kept_between_adverts() { + assert!(UNIVERSE_DISCOVERY_SOURCE_TIMEOUT > E131_UNIVERSE_DISCOVERY_INTERVAL); + + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), ACN_SDT_MULTICAST_PORT); + let mut dmx_rcv = SacnReceiver::with_ip(addr, None).unwrap(); + + let discovery_pkt = UniverseDiscoveryPacketFramingLayer { + source_name: "Test Src".into(), + data: UniverseDiscoveryPacketUniverseDiscoveryLayer { + page: 0, + last_page: 0, + universes: vec![1u16, 2].into(), + }, + }; + dmx_rcv.handle_universe_discovery_packet(Uuid::new_v4(), discovery_pkt); + + // Pretend the advert arrived just before the next one is due. + let advert_age = E131_UNIVERSE_DISCOVERY_INTERVAL - Duration::from_millis(100); + dmx_rcv.discovered_sources[0].last_updated = Instant::now() - advert_age; + assert_eq!(dmx_rcv.get_discovered_sources().len(), 1); + + // Once the source has missed adverts for longer than the timeout it is removed. + dmx_rcv.discovered_sources[0].last_updated = + Instant::now() - UNIVERSE_DISCOVERY_SOURCE_TIMEOUT - Duration::from_millis(100); + assert!(dmx_rcv.get_discovered_sources().is_empty()); + } + #[test] fn test_handle_multi_page_discovery_packet() { let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), ACN_SDT_MULTICAST_PORT);