Skip to main content

laser_dac/
types.rs

1//! DAC types for laser output.
2//!
3//! Provides DAC-agnostic types for laser frames and points,
4//! as well as device enumeration types.
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9use std::fmt;
10
11/// A DAC-agnostic laser point with full-precision f32 coordinates.
12///
13/// Coordinates are normalized:
14/// - x: -1.0 (left) to 1.0 (right)
15/// - y: -1.0 (bottom) to 1.0 (top)
16///
17/// Colors are 16-bit (0-65535) to support high-resolution DACs.
18/// DACs with lower resolution (8-bit) will downscale automatically.
19///
20/// This allows each DAC to convert to its native format:
21/// - Helios: 12-bit unsigned (0-4095), inverted
22/// - EtherDream: 16-bit signed (-32768 to 32767)
23#[derive(Debug, Clone, Copy, PartialEq, Default)]
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25pub struct LaserPoint {
26    /// X coordinate, -1.0 to 1.0
27    pub x: f32,
28    /// Y coordinate, -1.0 to 1.0
29    pub y: f32,
30    /// Red channel (0-65535)
31    pub r: u16,
32    /// Green channel (0-65535)
33    pub g: u16,
34    /// Blue channel (0-65535)
35    pub b: u16,
36    /// Intensity (0-65535)
37    pub intensity: u16,
38}
39
40impl LaserPoint {
41    /// Creates a new laser point.
42    pub fn new(x: f32, y: f32, r: u16, g: u16, b: u16, intensity: u16) -> Self {
43        Self {
44            x,
45            y,
46            r,
47            g,
48            b,
49            intensity,
50        }
51    }
52
53    /// Creates a blanked point (laser off) at the given position.
54    pub fn blanked(x: f32, y: f32) -> Self {
55        Self {
56            x,
57            y,
58            ..Default::default()
59        }
60    }
61}
62
63/// Types of laser DAC hardware supported.
64#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
66pub enum DacType {
67    /// Helios laser DAC (USB connection).
68    Helios,
69    /// Ether Dream laser DAC (network connection).
70    EtherDream,
71    /// IDN laser DAC (ILDA Digital Network, network connection).
72    Idn,
73    /// LaserCube WiFi laser DAC (network connection).
74    LasercubeWifi,
75    /// LaserCube USB laser DAC (USB connection, also known as LaserDock).
76    LasercubeUsb,
77    /// Custom DAC implementation (for external/third-party backends).
78    Custom(String),
79}
80
81impl DacType {
82    /// Returns all available DAC types.
83    pub fn all() -> &'static [DacType] {
84        &[
85            DacType::Helios,
86            DacType::EtherDream,
87            DacType::Idn,
88            DacType::LasercubeWifi,
89            DacType::LasercubeUsb,
90        ]
91    }
92
93    /// Returns the display name for this DAC type.
94    pub fn display_name(&self) -> &str {
95        match self {
96            DacType::Helios => "Helios",
97            DacType::EtherDream => "Ether Dream",
98            DacType::Idn => "IDN",
99            DacType::LasercubeWifi => "LaserCube WiFi",
100            DacType::LasercubeUsb => "LaserCube USB (Laserdock)",
101            DacType::Custom(name) => name,
102        }
103    }
104
105    /// Returns a description of this DAC type.
106    pub fn description(&self) -> &'static str {
107        match self {
108            DacType::Helios => "USB laser DAC",
109            DacType::EtherDream => "Network laser DAC",
110            DacType::Idn => "ILDA Digital Network laser DAC",
111            DacType::LasercubeWifi => "WiFi laser DAC",
112            DacType::LasercubeUsb => "USB laser DAC",
113            DacType::Custom(_) => "Custom DAC",
114        }
115    }
116}
117
118impl fmt::Display for DacType {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(f, "{}", self.display_name())
121    }
122}
123
124/// Set of enabled DAC types for discovery.
125#[derive(Debug, Clone, PartialEq, Eq)]
126#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
127pub struct EnabledDacTypes {
128    types: HashSet<DacType>,
129}
130
131impl EnabledDacTypes {
132    /// Creates a new set with all DAC types enabled.
133    pub fn all() -> Self {
134        Self {
135            types: DacType::all().iter().cloned().collect(),
136        }
137    }
138
139    /// Creates an empty set (no DAC types enabled).
140    #[allow(dead_code)]
141    pub fn none() -> Self {
142        Self {
143            types: HashSet::new(),
144        }
145    }
146
147    /// Returns true if the given DAC type is enabled.
148    pub fn is_enabled(&self, dac_type: DacType) -> bool {
149        self.types.contains(&dac_type)
150    }
151
152    /// Enables a DAC type for discovery.
153    ///
154    /// Returns `&mut Self` to allow method chaining.
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use laser_dac::{EnabledDacTypes, DacType};
160    ///
161    /// let mut enabled = EnabledDacTypes::none();
162    /// enabled.enable(DacType::Helios).enable(DacType::EtherDream);
163    ///
164    /// assert!(enabled.is_enabled(DacType::Helios));
165    /// assert!(enabled.is_enabled(DacType::EtherDream));
166    /// ```
167    pub fn enable(&mut self, dac_type: DacType) -> &mut Self {
168        self.types.insert(dac_type);
169        self
170    }
171
172    /// Disables a DAC type for discovery.
173    ///
174    /// Returns `&mut Self` to allow method chaining.
175    ///
176    /// # Examples
177    ///
178    /// ```
179    /// use laser_dac::{EnabledDacTypes, DacType};
180    ///
181    /// let mut enabled = EnabledDacTypes::all();
182    /// enabled.disable(DacType::Helios).disable(DacType::EtherDream);
183    ///
184    /// assert!(!enabled.is_enabled(DacType::Helios));
185    /// assert!(!enabled.is_enabled(DacType::EtherDream));
186    /// ```
187    pub fn disable(&mut self, dac_type: DacType) -> &mut Self {
188        self.types.remove(&dac_type);
189        self
190    }
191
192    /// Returns an iterator over enabled DAC types.
193    #[allow(dead_code)]
194    pub fn iter(&self) -> impl Iterator<Item = DacType> + '_ {
195        self.types.iter().cloned()
196    }
197
198    /// Returns true if no DAC types are enabled.
199    #[allow(dead_code)]
200    pub fn is_empty(&self) -> bool {
201        self.types.is_empty()
202    }
203}
204
205impl Default for EnabledDacTypes {
206    fn default() -> Self {
207        Self::all()
208    }
209}
210
211impl std::iter::FromIterator<DacType> for EnabledDacTypes {
212    fn from_iter<I: IntoIterator<Item = DacType>>(iter: I) -> Self {
213        Self {
214            types: iter.into_iter().collect(),
215        }
216    }
217}
218
219impl Extend<DacType> for EnabledDacTypes {
220    fn extend<I: IntoIterator<Item = DacType>>(&mut self, iter: I) {
221        self.types.extend(iter);
222    }
223}
224
225/// Information about a discovered DAC device.
226/// The name is the unique identifier for the device.
227#[derive(Debug, Clone, PartialEq, Eq, Hash)]
228#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
229pub struct DacDevice {
230    pub name: String,
231    pub dac_type: DacType,
232}
233
234impl DacDevice {
235    pub fn new(name: String, dac_type: DacType) -> Self {
236        Self { name, dac_type }
237    }
238}
239
240/// Connection state for a single DAC device.
241#[derive(Debug, Clone, PartialEq, Eq, Hash)]
242#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
243pub enum DacConnectionState {
244    /// Successfully connected and ready to receive frames.
245    Connected { name: String },
246    /// Worker stopped normally (callback returned None or stop() was called).
247    Stopped { name: String },
248    /// Connection was lost due to an error.
249    Lost { name: String, error: Option<String> },
250}
251
252// =============================================================================
253// Streaming Types
254// =============================================================================
255
256/// DAC capabilities that inform the stream scheduler about safe chunk sizes and behaviors.
257#[derive(Clone, Debug)]
258#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
259pub struct DacCapabilities {
260    /// Minimum points-per-second (hardware/protocol limit where known).
261    ///
262    /// A value of 1 means no known protocol constraint. Helios (7) and
263    /// Ether Dream (1) have true hardware minimums. Note that very low PPS
264    /// increases point dwell time and can produce flickery output.
265    pub pps_min: u32,
266    /// Maximum supported points-per-second (hardware limit).
267    pub pps_max: u32,
268    /// Maximum number of points allowed per chunk submission.
269    pub max_points_per_chunk: usize,
270    /// Some DACs dislike per-chunk PPS changes.
271    pub prefers_constant_pps: bool,
272    /// Best-effort: can we estimate device queue depth/latency?
273    pub can_estimate_queue: bool,
274    /// The scheduler-relevant output model.
275    pub output_model: OutputModel,
276}
277
278impl Default for DacCapabilities {
279    fn default() -> Self {
280        Self {
281            pps_min: 1,
282            pps_max: 100_000,
283            max_points_per_chunk: 4096,
284            prefers_constant_pps: false,
285            can_estimate_queue: false,
286            output_model: OutputModel::NetworkFifo,
287        }
288    }
289}
290
291/// Get default capabilities for a DAC type.
292///
293/// This delegates to each protocol's `default_capabilities()` function.
294/// For optimal performance, backends should query actual device capabilities
295/// at runtime where the protocol supports it (e.g., LaserCube's `max_dac_rate`
296/// and ringbuffer queries).
297pub fn caps_for_dac_type(dac_type: &DacType) -> DacCapabilities {
298    match dac_type {
299        #[cfg(feature = "helios")]
300        DacType::Helios => crate::protocols::helios::default_capabilities(),
301        #[cfg(not(feature = "helios"))]
302        DacType::Helios => DacCapabilities::default(),
303
304        #[cfg(feature = "ether-dream")]
305        DacType::EtherDream => crate::protocols::ether_dream::default_capabilities(),
306        #[cfg(not(feature = "ether-dream"))]
307        DacType::EtherDream => DacCapabilities::default(),
308
309        #[cfg(feature = "idn")]
310        DacType::Idn => crate::protocols::idn::default_capabilities(),
311        #[cfg(not(feature = "idn"))]
312        DacType::Idn => DacCapabilities::default(),
313
314        #[cfg(feature = "lasercube-wifi")]
315        DacType::LasercubeWifi => crate::protocols::lasercube_wifi::default_capabilities(),
316        #[cfg(not(feature = "lasercube-wifi"))]
317        DacType::LasercubeWifi => DacCapabilities::default(),
318
319        #[cfg(feature = "lasercube-usb")]
320        DacType::LasercubeUsb => crate::protocols::lasercube_usb::default_capabilities(),
321        #[cfg(not(feature = "lasercube-usb"))]
322        DacType::LasercubeUsb => DacCapabilities::default(),
323
324        DacType::Custom(_) => DacCapabilities::default(),
325    }
326}
327
328/// The scheduler-relevant output model for a DAC.
329#[derive(Clone, Debug, PartialEq, Eq)]
330#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
331pub enum OutputModel {
332    /// Frame swap / limited queue depth (e.g., Helios-style double-buffering).
333    UsbFrameSwap,
334    /// FIFO-ish buffer where "top up" is natural (e.g., Ether Dream-style).
335    NetworkFifo,
336    /// Timed UDP chunks where OS send may not reflect hardware pacing.
337    UdpTimed,
338}
339
340/// A point in stream time, measured in points since stream start.
341#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
342#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
343pub struct StreamInstant(pub u64);
344
345impl StreamInstant {
346    /// Create a new stream instant from a point count.
347    pub fn new(points: u64) -> Self {
348        Self(points)
349    }
350
351    /// Returns the number of points since stream start.
352    pub fn points(&self) -> u64 {
353        self.0
354    }
355
356    /// Convert this instant to seconds at the given points-per-second rate.
357    pub fn as_seconds(&self, pps: u32) -> f64 {
358        self.0 as f64 / pps as f64
359    }
360
361    /// Create a stream instant from a duration in seconds at the given PPS.
362    pub fn from_seconds(seconds: f64, pps: u32) -> Self {
363        Self((seconds * pps as f64) as u64)
364    }
365
366    /// Add a number of points to this instant.
367    pub fn add_points(&self, points: u64) -> Self {
368        Self(self.0.saturating_add(points))
369    }
370
371    /// Subtract a number of points from this instant (saturating at 0).
372    pub fn sub_points(&self, points: u64) -> Self {
373        Self(self.0.saturating_sub(points))
374    }
375}
376
377impl std::ops::Add<u64> for StreamInstant {
378    type Output = Self;
379    fn add(self, rhs: u64) -> Self::Output {
380        self.add_points(rhs)
381    }
382}
383
384impl std::ops::Sub<u64> for StreamInstant {
385    type Output = Self;
386    fn sub(self, rhs: u64) -> Self::Output {
387        self.sub_points(rhs)
388    }
389}
390
391impl std::ops::AddAssign<u64> for StreamInstant {
392    fn add_assign(&mut self, rhs: u64) {
393        self.0 = self.0.saturating_add(rhs);
394    }
395}
396
397impl std::ops::SubAssign<u64> for StreamInstant {
398    fn sub_assign(&mut self, rhs: u64) {
399        self.0 = self.0.saturating_sub(rhs);
400    }
401}
402
403/// Configuration for starting a stream.
404#[derive(Clone, Debug)]
405#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
406pub struct StreamConfig {
407    /// Points per second output rate.
408    pub pps: u32,
409    /// Exact chunk size to request/write. If `None`, the library chooses a default.
410    pub chunk_points: Option<usize>,
411    /// Target amount of queued data expressed in points.
412    pub target_queue_points: usize,
413    /// What to do when the producer can't keep up.
414    pub underrun: UnderrunPolicy,
415}
416
417impl Default for StreamConfig {
418    fn default() -> Self {
419        Self {
420            pps: 30_000,
421            chunk_points: None,
422            target_queue_points: 3000,
423            underrun: UnderrunPolicy::default(),
424        }
425    }
426}
427
428impl StreamConfig {
429    /// Create a new stream configuration with the given PPS.
430    pub fn new(pps: u32) -> Self {
431        Self {
432            pps,
433            ..Default::default()
434        }
435    }
436
437    /// Set the chunk size (builder pattern).
438    pub fn with_chunk_points(mut self, chunk_points: usize) -> Self {
439        self.chunk_points = Some(chunk_points);
440        self
441    }
442
443    /// Set the target queue depth in points (builder pattern).
444    pub fn with_target_queue_points(mut self, points: usize) -> Self {
445        self.target_queue_points = points;
446        self
447    }
448
449    /// Set the underrun policy (builder pattern).
450    pub fn with_underrun(mut self, policy: UnderrunPolicy) -> Self {
451        self.underrun = policy;
452        self
453    }
454}
455
456/// Policy for what to do when the producer can't keep up with the stream.
457#[derive(Clone, Debug, PartialEq)]
458#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
459#[derive(Default)]
460pub enum UnderrunPolicy {
461    /// Repeat the last chunk of points.
462    RepeatLast,
463    /// Output blanked points (laser off).
464    #[default]
465    Blank,
466    /// Park the beam at a specific position with laser off.
467    Park { x: f32, y: f32 },
468    /// Stop the stream entirely on underrun.
469    Stop,
470}
471
472/// A request from the stream for a chunk of points.
473#[derive(Clone, Debug)]
474pub struct ChunkRequest {
475    /// The stream instant at which this chunk starts.
476    pub start: StreamInstant,
477    /// The points-per-second rate for this chunk.
478    pub pps: u32,
479    /// Number of points requested for this chunk.
480    pub n_points: usize,
481    /// How many points are currently scheduled ahead of `start`.
482    pub scheduled_ahead_points: u64,
483    /// Best-effort: points reported by the device as queued.
484    pub device_queued_points: Option<u64>,
485}
486
487/// Current status of a stream.
488#[derive(Clone, Debug)]
489pub struct StreamStatus {
490    /// Whether the device is connected.
491    pub connected: bool,
492    /// The resolved chunk size chosen for this stream.
493    pub chunk_points: usize,
494    /// Library-owned scheduled amount.
495    pub scheduled_ahead_points: u64,
496    /// Best-effort device/backend estimate.
497    pub device_queued_points: Option<u64>,
498    /// Optional statistics for diagnostics.
499    pub stats: Option<StreamStats>,
500}
501
502/// Stream statistics for diagnostics and debugging.
503#[derive(Clone, Debug, Default)]
504pub struct StreamStats {
505    /// Number of times the stream underran.
506    pub underrun_count: u64,
507    /// Number of chunks that arrived late.
508    pub late_chunk_count: u64,
509    /// Number of times the device reconnected.
510    pub reconnect_count: u64,
511    /// Total chunks written since stream start.
512    pub chunks_written: u64,
513    /// Total points written since stream start.
514    pub points_written: u64,
515}
516
517/// How a callback-mode stream run ended.
518#[derive(Clone, Debug, PartialEq, Eq)]
519pub enum RunExit {
520    /// Stream was stopped via `StreamControl::stop()`.
521    Stopped,
522    /// Producer returned `None` (graceful completion).
523    ProducerEnded,
524    /// Device disconnected. No auto-reconnect; new streams start disarmed.
525    Disconnected,
526}
527
528/// Information about a discovered DAC before connection.
529#[derive(Clone, Debug)]
530#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
531pub struct DacInfo {
532    /// Stable, unique identifier used for (re)selecting DACs.
533    pub id: String,
534    /// Human-readable name for the DAC.
535    pub name: String,
536    /// The type of DAC hardware.
537    pub kind: DacType,
538    /// DAC capabilities.
539    pub caps: DacCapabilities,
540}
541
542impl DacInfo {
543    /// Create a new DAC info.
544    pub fn new(
545        id: impl Into<String>,
546        name: impl Into<String>,
547        kind: DacType,
548        caps: DacCapabilities,
549    ) -> Self {
550        Self {
551            id: id.into(),
552            name: name.into(),
553            kind,
554            caps,
555        }
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    // ==========================================================================
564    // LaserPoint Tests
565    // ==========================================================================
566
567    #[test]
568    fn test_laser_point_blanked_sets_all_colors_to_zero() {
569        // blanked() should set all color channels to 0 while preserving position
570        let point = LaserPoint::blanked(0.25, 0.75);
571        assert_eq!(point.x, 0.25);
572        assert_eq!(point.y, 0.75);
573        assert_eq!(point.r, 0);
574        assert_eq!(point.g, 0);
575        assert_eq!(point.b, 0);
576        assert_eq!(point.intensity, 0);
577    }
578
579    // ==========================================================================
580    // DacType Tests
581    // ==========================================================================
582
583    #[test]
584    fn test_dac_type_all_returns_all_five_types() {
585        let all_types = DacType::all();
586        assert_eq!(all_types.len(), 5);
587        assert!(all_types.contains(&DacType::Helios));
588        assert!(all_types.contains(&DacType::EtherDream));
589        assert!(all_types.contains(&DacType::Idn));
590        assert!(all_types.contains(&DacType::LasercubeWifi));
591        assert!(all_types.contains(&DacType::LasercubeUsb));
592    }
593
594    #[test]
595    fn test_dac_type_display_uses_display_name() {
596        // Display trait should delegate to display_name
597        assert_eq!(
598            format!("{}", DacType::Helios),
599            DacType::Helios.display_name()
600        );
601        assert_eq!(
602            format!("{}", DacType::EtherDream),
603            DacType::EtherDream.display_name()
604        );
605    }
606
607    #[test]
608    fn test_dac_type_can_be_used_in_hashset() {
609        use std::collections::HashSet;
610
611        let mut set = HashSet::new();
612        set.insert(DacType::Helios);
613        set.insert(DacType::Helios); // Duplicate should not increase count
614
615        assert_eq!(set.len(), 1);
616    }
617
618    // ==========================================================================
619    // EnabledDacTypes Tests
620    // ==========================================================================
621
622    #[test]
623    fn test_enabled_dac_types_all_enables_everything() {
624        let enabled = EnabledDacTypes::all();
625        for dac_type in DacType::all() {
626            assert!(
627                enabled.is_enabled(dac_type.clone()),
628                "{:?} should be enabled",
629                dac_type
630            );
631        }
632        assert!(!enabled.is_empty());
633    }
634
635    #[test]
636    fn test_enabled_dac_types_none_disables_everything() {
637        let enabled = EnabledDacTypes::none();
638        for dac_type in DacType::all() {
639            assert!(
640                !enabled.is_enabled(dac_type.clone()),
641                "{:?} should be disabled",
642                dac_type
643            );
644        }
645        assert!(enabled.is_empty());
646    }
647
648    #[test]
649    fn test_enabled_dac_types_enable_disable_toggles_correctly() {
650        let mut enabled = EnabledDacTypes::none();
651
652        // Enable one
653        enabled.enable(DacType::Helios);
654        assert!(enabled.is_enabled(DacType::Helios));
655        assert!(!enabled.is_enabled(DacType::EtherDream));
656
657        // Enable another
658        enabled.enable(DacType::EtherDream);
659        assert!(enabled.is_enabled(DacType::Helios));
660        assert!(enabled.is_enabled(DacType::EtherDream));
661
662        // Disable first
663        enabled.disable(DacType::Helios);
664        assert!(!enabled.is_enabled(DacType::Helios));
665        assert!(enabled.is_enabled(DacType::EtherDream));
666    }
667
668    #[test]
669    fn test_enabled_dac_types_iter_only_returns_enabled() {
670        let mut enabled = EnabledDacTypes::none();
671        enabled.enable(DacType::Helios);
672        enabled.enable(DacType::Idn);
673
674        let types: Vec<DacType> = enabled.iter().collect();
675        assert_eq!(types.len(), 2);
676        assert!(types.contains(&DacType::Helios));
677        assert!(types.contains(&DacType::Idn));
678        assert!(!types.contains(&DacType::EtherDream));
679    }
680
681    #[test]
682    fn test_enabled_dac_types_default_enables_all() {
683        let enabled = EnabledDacTypes::default();
684        // Default should be same as all()
685        for dac_type in DacType::all() {
686            assert!(enabled.is_enabled(dac_type.clone()));
687        }
688    }
689
690    #[test]
691    fn test_enabled_dac_types_idempotent_operations() {
692        let mut enabled = EnabledDacTypes::none();
693
694        // Enabling twice should have same effect as once
695        enabled.enable(DacType::Helios);
696        enabled.enable(DacType::Helios);
697        assert!(enabled.is_enabled(DacType::Helios));
698
699        // Disabling twice should have same effect as once
700        enabled.disable(DacType::Helios);
701        enabled.disable(DacType::Helios);
702        assert!(!enabled.is_enabled(DacType::Helios));
703    }
704
705    #[test]
706    fn test_enabled_dac_types_chaining() {
707        let mut enabled = EnabledDacTypes::none();
708        enabled
709            .enable(DacType::Helios)
710            .enable(DacType::EtherDream)
711            .disable(DacType::Helios);
712
713        assert!(!enabled.is_enabled(DacType::Helios));
714        assert!(enabled.is_enabled(DacType::EtherDream));
715    }
716
717    // ==========================================================================
718    // DacConnectionState Tests
719    // ==========================================================================
720
721    #[test]
722    fn test_dac_connection_state_equality() {
723        let s1 = DacConnectionState::Connected {
724            name: "DAC1".to_string(),
725        };
726        let s2 = DacConnectionState::Connected {
727            name: "DAC1".to_string(),
728        };
729        let s3 = DacConnectionState::Connected {
730            name: "DAC2".to_string(),
731        };
732        let s4 = DacConnectionState::Lost {
733            name: "DAC1".to_string(),
734            error: None,
735        };
736
737        assert_eq!(s1, s2);
738        assert_ne!(s1, s3); // Different name
739        assert_ne!(s1, s4); // Different variant
740    }
741}