lib_g29/
lib.rs

1use events::{Event, EventHandler, EventMap, HandlerFn};
2use hidapi::{DeviceInfo, HidApi};
3
4use std::{
5    env::consts::OS,
6    ops::BitOr,
7    process::exit,
8    sync::{atomic::AtomicBool, Arc, Mutex, RwLock},
9    thread::{self, sleep},
10    time::Duration,
11};
12
13pub mod events;
14// pub mod state;
15mod state;
16// The size of the data frame that the G29 sends
17const FRAME_SIZE: usize = 12;
18
19///
20/// DpadPosition
21///
22/// Represents the position of the Dpad on the G29
23#[derive(Debug, PartialEq, Clone, Eq, Hash)]
24pub enum DpadPosition {
25    Up,
26    TopRight,
27    Right,
28    BottomRight,
29    Down,
30    BottomLeft,
31    Left,
32    TopLeft,
33    None,
34}
35
36///
37/// Event
38/// Events that can be triggered by the G29
39///
40
41type Frame = [u8; FRAME_SIZE];
42
43///
44/// GearSelector
45///
46/// Represents the gear selected on the G29
47///
48#[derive(Debug, PartialEq, Clone, Eq, Hash)]
49pub enum GearSelector {
50    Neutral = 0,
51    First = 1,
52    Second = 2,
53    Third = 4,
54    Fourth = 8,
55    Fifth = 16,
56    Sixth = 32,
57    Reverse = 64,
58}
59
60///
61/// Led
62///
63/// Represents the LED lights on the G29
64///
65#[repr(u8)]
66#[derive(Debug, PartialEq, Copy, Clone)]
67pub enum Led {
68    None = 0x0,
69    GreenOne = 0x01,
70    GreenTwo = 0x02,
71    OrangeOne = 0x04,
72    OrangeTwo = 0x08,
73    Red = 0x10,
74    All = 0x1F,
75    Other(u8),
76}
77
78impl Led {
79    fn as_u8(&self) -> u8 {
80        match self {
81            Led::None => 0x0,
82            Led::GreenOne => 0x01,
83            Led::GreenTwo => 0x02,
84            Led::OrangeOne => 0x04,
85            Led::OrangeTwo => 0x08,
86            Led::Red => 0x10,
87            Led::All => 0x1F,
88            Led::Other(val) => *val,
89        }
90    }
91}
92
93impl BitOr for Led {
94    type Output = Led;
95
96    fn bitor(self, other: Self) -> Self::Output {
97        match (self, other) {
98            (Led::None, _) => other,
99            (_, Led::None) => self,
100            _ => Led::Other(self.as_u8() | other.as_u8()),
101        }
102    }
103}
104
105static CONNECTED: AtomicBool = AtomicBool::new(false);
106
107///
108/// G29
109/// Establishes a connection to the Logitech G29 Racing Wheel and provides methods to interact with it.
110///
111/// # Example
112///
113/// ```rust
114/// use lib_g29{G29, Options, Led};
115/// use std::time::Duration;
116/// use std::thread::sleep;
117///
118///    let options = Options {
119///      ..Default::default()
120///   };
121///
122///   let g29 = G29::connect(options);
123///
124///   g29.set_leds(Led::All);
125///
126///   sleep(Duration::from_secs(5));
127///
128///   g29.disconnect();
129/// ```
130///
131#[derive(Debug, Clone)]
132pub struct G29 {
133    options: Options,
134    prepend_write: bool,
135    calibrated: bool,
136    inner: Arc<RwLock<InnerG29>>,
137}
138
139#[derive(Debug)]
140struct InnerG29 {
141    data: Arc<RwLock<Frame>>,
142    reader_handle: Option<thread::JoinHandle<()>>,
143    event_handlers: EventMap,
144    wheel: Option<Mutex<hidapi::HidDevice>>,
145}
146
147///
148/// The options that can be set when connecting to the G29
149/// - debug: `bool` - Enable debug mode (default: `false`)
150/// - range: `u16` - The range of the wheel (40 - 900) (default: `900`)
151/// - auto_center: `[u8; 2]` - The auto center force and turning multiplier (default: `[0x07, 0xff]`)
152/// - auto_center_enabled: `bool` - Enable auto centering (default: `true`)
153///
154/// # Example
155///
156/// ```rust
157/// use lib_g29Options;
158///
159/// let options = Options {
160///    range: 540,
161///    auto_center_enabled: false,
162///   ..Default::default()
163/// };
164/// ```
165///
166#[derive(Debug, PartialEq, Copy, Clone, Eq, Hash)]
167pub struct Options {
168    pub debug: bool,
169    pub range: u16,
170    pub auto_center: [u8; 2],
171    pub auto_center_enabled: bool,
172}
173
174impl Default for Options {
175    fn default() -> Self {
176        Options {
177            auto_center: [0x07, 0xff],
178            debug: false,
179            range: 900,
180            auto_center_enabled: true,
181        }
182    }
183}
184
185fn is_logitech_g29(device: &DeviceInfo) -> bool {
186    device.vendor_id() == 1133
187        && (device.product_string().unwrap() == "G29 Driving Force Racing Wheel"
188            || device.product_id() == 49743)
189        && (device.interface_number() == 0 || device.usage_page() == 1)
190}
191
192fn get_wheel_info(api: &HidApi) -> DeviceInfo {
193    let list = api.device_list();
194
195    list.into_iter()
196        .find(|device| is_logitech_g29(device))
197        .expect("No wheel found")
198        .clone()
199}
200
201impl G29 {
202    ///
203    /// Connect to the G29 Racing Wheel
204    ///
205    pub fn connect(options: Options) -> G29 {
206        if options.debug {
207            println!("userOptions -> {:?}", options);
208        }
209        // get wheel
210        let api = HidApi::new().expect("Failed to initialize HID API");
211
212        let wheel_info = get_wheel_info(&api);
213
214        if wheel_info.path().is_empty() {
215            if options.debug {
216                println!("findWheel -> Oops, could not find a G29 Wheel. Is it plugged in?");
217                exit(1);
218            }
219        } else if options.debug {
220            println!("findWheel -> Found G29 Wheel at {:?}", wheel_info.path());
221        }
222
223        let wheel = wheel_info.open_device(&api).expect("Failed to open device");
224        wheel
225            .set_blocking_mode(false)
226            .expect("Failed to set non-blocking mode");
227
228        let prepend_write: bool = { matches!(OS, "windows") };
229
230        let mut g29 = G29 {
231            options,
232            prepend_write,
233            calibrated: false,
234            inner: Arc::new(RwLock::new(InnerG29 {
235                wheel: Some(Mutex::new(wheel)),
236                data: Arc::new(RwLock::new([0; FRAME_SIZE])),
237                reader_handle: None,
238                event_handlers: EventMap::new(),
239            })),
240        };
241        CONNECTED.store(true, std::sync::atomic::Ordering::Release);
242
243        g29.initialize();
244
245        g29
246    }
247
248    fn initialize(&mut self) {
249        self.inner
250            .read()
251            .unwrap()
252            .wheel
253            .as_ref()
254            .unwrap()
255            .lock()
256            .unwrap()
257            .set_blocking_mode(false)
258            .expect("Failed to set non-blocking mode");
259
260        let mut data = [0u8; FRAME_SIZE];
261        let data_size = self
262            .inner
263            .read()
264            .unwrap()
265            .wheel
266            .as_ref()
267            .unwrap()
268            .lock()
269            .unwrap()
270            .read(&mut data)
271            .expect("connect -> Error reading from device.");
272
273        self.force_off(0xf3);
274
275        if data_size == FRAME_SIZE || self.calibrated {
276            if self.options.debug {
277                println!("connect -> Wheel already in high precision mode.");
278            }
279            self.listen(true);
280        } else {
281            if self.options.debug {
282                println!("connect -> Initializing Wheel.");
283            }
284
285            if !self.calibrated {
286                self.calibrate_wheel();
287                self.calibrated = true;
288            }
289
290            self.listen(false);
291        }
292    }
293
294    fn calibrate_wheel(&mut self) {
295        // G29 Wheel init from - https://github.com/torvalds/linux/blob/master/drivers/hid/hid-lg4ff.c
296        self.relay_os([0xf8, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00], "init_1");
297        self.relay_os([0xf8, 0x09, 0x05, 0x01, 0x01, 0x00, 0x00], "init_2");
298
299        sleep(Duration::from_secs(8));
300    }
301
302    fn listen(&mut self, ready: bool) {
303        if !ready {
304            let new_wheel = get_wheel_info(&HidApi::new().unwrap())
305                .open_device(&HidApi::new().unwrap())
306                .unwrap();
307
308            *self
309                .inner
310                .read()
311                .unwrap()
312                .wheel
313                .as_ref()
314                .unwrap()
315                .lock()
316                .unwrap() = new_wheel;
317
318            self.initialize();
319            return;
320        }
321
322        self.set_range();
323        self.set_auto_center();
324
325        if self.options.debug {
326            println!("listen -> Ready to listen for wheel events.");
327        }
328
329        // use thread to listen for wheel events and trigger events
330        let mut g29_clone = self.clone();
331        let local_self = self.inner.clone();
332        let thread_handle = thread::spawn(move || {
333            while CONNECTED.load(std::sync::atomic::Ordering::Relaxed) {
334                let mut new_data = [0u8; FRAME_SIZE];
335                match local_self
336                    .read()
337                    .unwrap()
338                    .wheel
339                    .as_ref()
340                    .unwrap()
341                    .lock()
342                    .unwrap()
343                    .read(&mut new_data)
344                {
345                    Ok(size_read) if size_read == FRAME_SIZE => {
346                        let local_self_write = local_self.read().unwrap();
347                        let mut prev_data = local_self_write.data.write().unwrap();
348
349                        if new_data == *prev_data {
350                            continue;
351                        }
352
353                        local_self_write.event_handlers.trigger_events(
354                            &prev_data,
355                            &new_data,
356                            &mut g29_clone,
357                        );
358
359                        *prev_data = new_data;
360                    }
361                    Ok(_) => {
362                        if g29_clone.options.debug {
363                            println!("listen -> Incomplete data read from device.");
364                        }
365                    }
366                    Err(e) => {
367                        if g29_clone.options.debug {
368                            println!("listen -> Error reading from device: {:?}", e);
369                        }
370                    }
371                };
372            }
373        });
374        self.inner.write().unwrap().reader_handle = Some(thread_handle);
375    }
376
377    // fn auto_center_complex(
378    //     &self,
379    //     clockwise_angle: u8,
380    //     counter_clockwise_angle: u8,
381    //     clockwise_force: u8,
382    //     counter_clockwise_force: u8,
383    //     reverse: bool,
384    //     centering_force: u8,
385    // ) {
386    //     if !self.options.auto_center_enabled {
387    //         return;
388    //     }
389
390    //     self.force_off(0xf5);
391
392    //     // auto-center on
393    //     self.relay_os(
394    //         [
395    //             0xfc,
396    //             0x01,
397    //             clockwise_angle,
398    //             counter_clockwise_angle,
399    //             clockwise_force | counter_clockwise_force,
400    //             reverse as u8,
401    //             centering_force,
402    //         ],
403    //         "set_auto_center_complex",
404    //     );
405    // }
406
407    fn set_auto_center(&self) {
408        /*
409            Set wheel autocentering based on existing options.
410        */
411        if self.options.auto_center_enabled {
412            // auto-center on
413            self.relay_os([0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], "auto_center on");
414            self.relay_os(
415                [
416                    0xfe,
417                    0x0d,
418                    self.options.auto_center[0],
419                    self.options.auto_center[0],
420                    self.options.auto_center[1],
421                    0x00,
422                    0x00,
423                ],
424                "set_auto_center_force",
425            );
426        } else {
427            // auto-center off
428            self.relay_os(
429                [0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
430                "auto_center off",
431            );
432        }
433    }
434
435    fn set_range(&mut self) {
436        /*
437            Set wheel range.
438        */
439        if self.options.range < 40 {
440            self.options.range = 40;
441        }
442
443        if self.options.range > 900 {
444            self.options.range = 900;
445        }
446
447        let range1 = self.options.range & 0x00ff;
448        let range2 = (self.options.range & 0xff00) >> 8;
449
450        self.relay_os(
451            [0xf8, 0x81, range1 as u8, range2 as u8, 0x00, 0x00, 0x00],
452            "set_range",
453        );
454    }
455
456    fn force_off(&self, slot: u8) {
457        // turn off effects (except for auto-center)
458        self.relay_os([slot, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], "force_off");
459    }
460
461    fn relay_os(&self, data: [u8; 7], operation: &str) {
462        /*
463        Relay low level commands directly to the hardware after applying OS specific tweaks, if needed.
464        @param  {Object}  data  Array of data to write. For example: [0xf8, 0x12, 0x1f, 0x00, 0x00, 0x00, 0x01]
465        */
466
467        let mut new_data: [u8; 8] = [0; 8];
468
469        if self.prepend_write {
470            // data.unshift(0x00)
471            new_data = [
472                0x00, data[0], data[1], data[2], data[3], data[4], data[5], data[6],
473            ];
474        }
475
476        self.inner
477            .read()
478            .unwrap()
479            .wheel
480            .as_ref()
481            .expect("relay_os -> Wheel not found")
482            .lock()
483            .unwrap()
484            .write(if self.prepend_write { &new_data } else { &data })
485            .unwrap_or_else(|_| {
486                panic!(
487                    "relay_os -> Error writing to device. Operation: {}",
488                    operation
489                )
490            });
491    }
492
493    /// Set auto-center force.
494    ///
495    /// # Arguments
496    /// - `strength` - The strength of the auto-center force (**0x00** to **0x0f**)
497    /// - `turning_multiplier` - The rate the effect strength rises as the wheel turns (**0x00** to **0xff**)
498    ///
499    /// # Example
500    ///
501    /// ```rust
502    /// use lib_g29{G29, Options};
503    ///
504    ///   let options = Options {
505    ///     ..Default::default()
506    ///   };
507    ///
508    ///
509    ///   let mut g29 = G29::connect(options);
510    ///
511    ///   g29.set_auto_center_force(0x0f, 0xff);
512    ///
513    ///   loop {}
514    /// ```
515    ///
516    pub fn set_auto_center_force(&mut self, strength: u8, turning_multiplier: u8) {
517        self.options.auto_center = [strength, turning_multiplier];
518
519        self.set_auto_center();
520    }
521
522    /// Set the LED lights on the G29.
523    /// # Arguments
524    /// - `leds` - The LED lights to set
525    /// # Example
526    /// ```rust
527    /// use lib_g29{G29, Options, Led};
528    /// use std::time::Duration;
529    /// use std::thread::sleep;
530    ///
531    ///   let options = Options {
532    ///     ..Default::default()
533    ///   };
534    ///
535    ///   let g29 = G29::connect(options);
536    ///
537    ///   loop {
538    ///     g29.set_leds(Led::All);
539    ///     sleep(Duration::from_secs(1));
540    ///     g29.set_leds(Led::Red | Led::GreenOne);
541    ///     sleep(Duration::from_secs(1));
542    ///   }
543    /// ````
544    pub fn set_leds(&self, leds: Led) {
545        /*
546            Set the LED lights on the G29.
547        */
548        let data = [0xf8, 0x12, leds.as_u8(), 0x00, 0x00, 0x00, 0x01];
549
550        self.relay_os(data, "set_leds");
551    }
552
553    /// Set the force feedback on the G29.
554    /// # Arguments
555    /// - `left` - The strength of the left motor (**0x00** to **0x07**)
556    /// - `right` - The strength of the right motor (**0x00** to **0x07**)
557    ///
558    /// # Example
559    /// ```rust
560    /// use lib_g29{G29, Options};
561    ///
562    ///   let options = Options {
563    ///     ..Default::default()
564    ///   };
565    ///
566    ///   let mut g29 = G29::connect(options);
567    ///
568    ///   g29.force_friction(0x07, 0x07);
569    ///
570    ///   loop {}
571    /// ```
572    pub fn force_friction(&self, mut left: u8, mut right: u8) {
573        if left | right == 0 {
574            self.force_off(2);
575            return;
576        }
577
578        left *= 7;
579        right *= 7;
580
581        self.relay_os(
582            [0x21, 0x02, left, 0x00, right, 0x00, 0x00],
583            "force_friction",
584        );
585    }
586
587    /// Get the throttle value.
588    ///  255 is depressed, 0 is fully pressed
589    pub fn throttle(&self) -> u8 {
590        state::throttle(&self.inner.read().unwrap().data.read().unwrap())
591    }
592
593    /// Get the brake value.
594    ///  255 is depressed, 0 is fully pressed
595    pub fn brake(&self) -> u8 {
596        state::brake(&self.inner.read().unwrap().data.read().unwrap())
597    }
598
599    /// Get the steering value.
600    /// 255 is fully right, 0 is fully left
601    pub fn steering(&self) -> u8 {
602        state::steering(&self.inner.read().unwrap().data.read().unwrap())
603    }
604
605    /// Get the fine steering value.
606    /// 255 is fully right, 0 is fully left
607    pub fn steering_fine(&self) -> u8 {
608        state::steering_fine(&self.inner.read().unwrap().data.read().unwrap())
609    }
610
611    /// Get the Dpad position.
612    /// # Example
613    /// ```rust
614    /// if g29.dpad() == DpadPosition::Top {
615    ///    println!("Dpad is at the top");
616    /// }
617    /// ````
618    pub fn dpad(&self) -> DpadPosition {
619        state::dpad(&self.inner.read().unwrap().data.read().unwrap())
620    }
621
622    /// Returns `true` if the x button is pressed.
623    pub fn x_button(&self) -> bool {
624        state::x_button(&self.inner.read().unwrap().data.read().unwrap())
625    }
626
627    /// Returns true if the square button is pressed.
628    pub fn square_button(&self) -> bool {
629        state::square_button(&self.inner.read().unwrap().data.read().unwrap())
630    }
631
632    /// Returns true if the circle button is pressed.
633    pub fn circle_button(&self) -> bool {
634        state::circle_button(&self.inner.read().unwrap().data.read().unwrap())
635    }
636
637    /// Returns true if the triangle button is pressed.
638    pub fn triangle_button(&self) -> bool {
639        state::triangle_button(&self.inner.read().unwrap().data.read().unwrap())
640    }
641
642    /// returns true if the right shifter is pressed.
643    pub fn right_shifter(&self) -> bool {
644        state::right_shifter(&self.inner.read().unwrap().data.read().unwrap())
645    }
646
647    /// Returns true if the left shifter is pressed.
648    pub fn left_shifter(&self) -> bool {
649        state::left_shifter(&self.inner.read().unwrap().data.read().unwrap())
650    }
651
652    /// Returns true if the r2 button is pressed.
653    pub fn r2_button(&self) -> bool {
654        state::r2_button(&self.inner.read().unwrap().data.read().unwrap())
655    }
656
657    /// Returns true if the l2 button is pressed.
658    pub fn l2_button(&self) -> bool {
659        state::l2_button(&self.inner.read().unwrap().data.read().unwrap())
660    }
661
662    /// Returns true if the share button is pressed.
663    pub fn share_button(&self) -> bool {
664        state::share_button(&self.inner.read().unwrap().data.read().unwrap())
665    }
666
667    /// Returns true if the option button is pressed.
668    pub fn option_button(&self) -> bool {
669        state::options_button(&self.inner.read().unwrap().data.read().unwrap())
670    }
671
672    /// Returns true if the r3 button is pressed.
673    pub fn r3_button(&self) -> bool {
674        state::r3_button(&self.inner.read().unwrap().data.read().unwrap())
675    }
676
677    /// Returns true if the l3 button is pressed.
678    pub fn l3_button(&self) -> bool {
679        state::l3_button(&self.inner.read().unwrap().data.read().unwrap())
680    }
681
682    /// Get the gear selector position.
683    ///
684    /// # Example
685    ///
686    /// ```rust
687    /// if g29.gear_selector() == GearSelector::First {
688    ///    println!("Gear is in first");
689    /// }
690    /// ```
691    ///
692    pub fn gear_selector(&self) -> GearSelector {
693        state::gear_selector(&self.inner.read().unwrap().data.read().unwrap())
694    }
695
696    /// Returns true if the plus button is pressed.
697    pub fn plus_button(&self) -> bool {
698        state::plus_button(&self.inner.read().unwrap().data.read().unwrap())
699    }
700
701    /// Returns true if the minus button is pressed.
702    pub fn minus_button(&self) -> bool {
703        state::minus_button(&self.inner.read().unwrap().data.read().unwrap())
704    }
705
706    /// Returns true if the spinner is rotating clockwise.
707    pub fn spinner_right(&self) -> bool {
708        state::spinner_right(&self.inner.read().unwrap().data.read().unwrap())
709    }
710
711    /// Returns true if the spinner is rotating counter-clockwise.
712    pub fn spinner_left(&self) -> bool {
713        state::spinner_left(&self.inner.read().unwrap().data.read().unwrap())
714    }
715
716    /// Returns true if the spinner button is pressed.
717    pub fn spinner_button(&self) -> bool {
718        state::spinner_button(&self.inner.read().unwrap().data.read().unwrap())
719    }
720
721    /// Returns true if the playstation button is pressed.
722    pub fn playstation_button(&self) -> bool {
723        state::playstation_button(&self.inner.read().unwrap().data.read().unwrap())
724    }
725
726    /// Returns the value of the clutch pedal.
727    /// 255 is depressed, 0 is fully pressed
728    pub fn clutch(&self) -> u8 {
729        state::clutch(&self.inner.read().unwrap().data.read().unwrap())
730    }
731
732    /// Returns the value of the shifter x axis.
733    pub fn shifter_x(&self) -> u8 {
734        state::shifter_x(&self.inner.read().unwrap().data.read().unwrap())
735    }
736
737    /// Returns the value of the shifter y axis.
738    pub fn shifter_y(&self) -> u8 {
739        state::shifter_y(&self.inner.read().unwrap().data.read().unwrap())
740    }
741
742    /// Returns true if the shifter is pressed.
743    pub fn shifter_pressed(&self) -> bool {
744        state::shifter_pressed(&self.inner.read().unwrap().data.read().unwrap())
745    }
746
747    /// Disconnect from the G29.
748    /// # Example
749    /// ```rust
750    /// use lib_g29{G29, Options};
751    /// use std::time::Duration;
752    /// use std::thread::sleep;
753    ///
754    ///   let options = Options {
755    ///     ..Default::default()
756    ///   };
757    ///
758    ///   let mut g29 = G29::connect(options);
759    ///   
760    ///   sleep(Duration::from_secs(5));
761    ///   
762    ///   g29.disconnect();
763    /// ```
764    pub fn disconnect(&mut self) {
765        if !self.connected() {
766            return;
767        }
768
769        self.force_off(0xf3);
770        self.set_leds(Led::None);
771        self.force_friction(0, 0);
772        self.options.auto_center = [0x00, 0x00];
773        self.set_auto_center();
774
775        // set connected to false
776
777        CONNECTED.store(false, std::sync::atomic::Ordering::Release);
778        self.inner.write().unwrap().wheel = None;
779        // join all threads
780        if let Some(handle) = self.inner.write().unwrap().reader_handle.take() {
781            handle.join().unwrap();
782        }
783    }
784
785    pub fn connected(&self) -> bool {
786        CONNECTED.load(std::sync::atomic::Ordering::Relaxed)
787            && self.inner.read().unwrap().wheel.is_some()
788    }
789
790    ///
791    /// Register an event handler for a specific event.
792    /// # Arguments
793    /// - `event` - The event to register the handler for
794    /// - `handler` - The handler function
795    /// # Example
796    /// ```rust
797    /// use lib_g29{G29, Options, Event, EventHandler};
798    /// use std::time::Duration;
799    /// use std::thread::sleep;
800    ///
801    ///  let options = Options {
802    ///   ..Default::default()
803    /// };
804    ///
805    /// let g29 = G29::connect(options);
806    ///
807    /// let handler: EventHandler = g29.register_event_handler(Event::Steering, |g29| {
808    ///    println!("Steering: {}", g29.steering());
809    /// });
810    ///
811    /// sleep(Duration::from_secs(5));
812    ///
813    /// g29.unregister_event_handler(handler);
814    ///
815    /// g29.disconnect();
816    /// ```
817    pub fn register_event_handler(&self, event: Event, handler: HandlerFn) -> Option<EventHandler> {
818        self.inner
819            .write()
820            .unwrap()
821            .event_handlers
822            .insert(event, handler)
823    }
824
825    ///
826    /// Unregister an event handler for a specific event.
827    /// # Arguments
828    /// - `event_handler` - The event handler to unregister
829    /// # Example
830    /// ```rust
831    /// use lib_g29{G29, Options, Event, EventHandler};
832    /// use std::time::Duration;
833    /// use std::thread::sleep;
834    ///
835    ///  let options = Options {
836    ///   ..Default::default()
837    /// };
838    ///
839    /// let g29 = G29::connect(options);
840    ///
841    /// let handler: EventHandler = g29.register_event_handler(Event::Steering, |g29| {
842    ///    println!("Steering: {}", g29.steering());
843    /// });
844    ///
845    /// sleep(Duration::from_secs(5));
846    ///
847    /// g29.unregister_event_handler(handler);
848    ///
849    /// g29.disconnect();
850    /// ```
851    pub fn unregister_event_handler(&mut self, event_handler: EventHandler) {
852        self.inner
853            .write()
854            .unwrap()
855            .event_handlers
856            .remove(event_handler);
857    }
858}