init commit
This commit is contained in:
commit
e37e4b4d54
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.direnv/
|
||||||
|
target/
|
||||||
|
*.obt
|
2570
Cargo.lock
generated
Normal file
2570
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "obt"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
obd2={ path = "../obd2", features = [ "ftdi_comm" ] }
|
||||||
|
env_logger = "0.10"
|
||||||
|
vin_parser = "1.0.0"
|
||||||
|
chrono = "0.4.40"
|
||||||
|
anyhow = "1.0.97"
|
||||||
|
log = "0.4"
|
||||||
|
gpsd_client = "0.1.5"
|
||||||
|
sqlx = { version = "0.8.3", features = ["runtime-async-std", "sqlite"]}
|
||||||
|
bincode = "1.3.3"
|
||||||
|
serde = "1.0.219"
|
||||||
|
serde_json = "1.0.140"
|
4
README.md
Normal file
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# On-Board Telematics (OBT)
|
||||||
|
|
||||||
|
On-Board Telematics is a suite of software to capture data from your automobile
|
||||||
|
as well as (in the future) remotely control your automobile.
|
8
shell.nix
Normal file
8
shell.nix
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
pkgs.mkShell {
|
||||||
|
packages = [ pkgs.rustc pkgs.cargo pkgs.libftdi1 pkgs.systemd pkgs.pkg-config pkgs.alsa-lib pkgs.ncurses pkgs.openssl ];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
export DEBUG=1
|
||||||
|
'';
|
||||||
|
}
|
110
src/auto_events/auto_events.rs
Normal file
110
src/auto_events/auto_events.rs
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[derive(Debug,Serialize,Deserialize,PartialEq,Clone)]
|
||||||
|
pub struct SpeedEvent {
|
||||||
|
pub speed: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,Serialize,Deserialize,PartialEq,Clone)]
|
||||||
|
pub struct GPSSpeedEvent {
|
||||||
|
pub speed: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,Serialize,Deserialize,PartialEq,Clone)]
|
||||||
|
pub struct RPMEvent {
|
||||||
|
pub rpm: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,Serialize,Deserialize,PartialEq,Clone)]
|
||||||
|
pub struct FuelLevelEvent {
|
||||||
|
pub level_pct: f32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,Serialize,Deserialize,PartialEq,Clone)]
|
||||||
|
pub struct GPSLocationEvent {
|
||||||
|
pub lat: f64,
|
||||||
|
pub lng: f64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,Serialize,Deserialize,PartialEq,Clone)]
|
||||||
|
pub struct ThrottleEvent {
|
||||||
|
pub throttle_pct: f32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,Serialize,Deserialize,PartialEq,Clone)]
|
||||||
|
pub enum AutoEventType {
|
||||||
|
Speed(SpeedEvent),
|
||||||
|
RPM(RPMEvent),
|
||||||
|
FuelLevel(FuelLevelEvent),
|
||||||
|
GPSLocation(GPSLocationEvent),
|
||||||
|
Throttle(ThrottleEvent),
|
||||||
|
GPSSpeed(GPSSpeedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,Serialize,Deserialize,PartialEq,Clone)]
|
||||||
|
pub struct AutoEvent {
|
||||||
|
pub content: Vec<AutoEventType>,
|
||||||
|
pub timestamp: i64
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RPMEvent {
|
||||||
|
pub fn new(rpm: u32) -> AutoEventType {
|
||||||
|
return AutoEventType::RPM(
|
||||||
|
RPMEvent {
|
||||||
|
rpm
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThrottleEvent {
|
||||||
|
pub fn new(throttle_pct: f32) -> AutoEventType {
|
||||||
|
return AutoEventType::Throttle(
|
||||||
|
ThrottleEvent {
|
||||||
|
throttle_pct
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpeedEvent {
|
||||||
|
pub fn new(speed: u32) -> AutoEventType {
|
||||||
|
return AutoEventType::Speed(
|
||||||
|
SpeedEvent {
|
||||||
|
speed
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GPSSpeedEvent {
|
||||||
|
pub fn new(speed: u32) -> AutoEventType {
|
||||||
|
return AutoEventType::GPSSpeed(
|
||||||
|
GPSSpeedEvent {
|
||||||
|
speed
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FuelLevelEvent {
|
||||||
|
pub fn new(level_pct: f32) -> AutoEventType {
|
||||||
|
return AutoEventType::FuelLevel(
|
||||||
|
FuelLevelEvent {
|
||||||
|
level_pct
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GPSLocationEvent {
|
||||||
|
pub fn new(lat: f64, lng: f64) -> AutoEventType {
|
||||||
|
return AutoEventType::GPSLocation(
|
||||||
|
GPSLocationEvent {
|
||||||
|
lat,
|
||||||
|
lng
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
2
src/auto_events/mod.rs
Normal file
2
src/auto_events/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod auto_events;
|
||||||
|
pub use auto_events::*;
|
111
src/file_format.rs
Normal file
111
src/file_format.rs
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
use crate::auto_events::AutoEvent;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||||
|
pub enum AvailableAutoEventType {
|
||||||
|
Speed = 1,
|
||||||
|
RPM = 2,
|
||||||
|
FuelLevel = 3,
|
||||||
|
GPSLocation = 4,
|
||||||
|
Throttle = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||||
|
pub struct FileHeader {
|
||||||
|
magic_number: u32,
|
||||||
|
version: u8,
|
||||||
|
event_types: Vec<AvailableAutoEventType>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||||
|
pub struct FileContents {
|
||||||
|
header: FileHeader,
|
||||||
|
records: Vec<AutoEvent>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct File {
|
||||||
|
file_handle: std::fs::File,
|
||||||
|
contents: FileContents
|
||||||
|
}
|
||||||
|
|
||||||
|
impl File {
|
||||||
|
pub fn create(path: &str) -> ::anyhow::Result<Self> {
|
||||||
|
let new_file = File {
|
||||||
|
file_handle: std::fs::File::create_new(path)?,
|
||||||
|
contents: FileContents {
|
||||||
|
header: FileHeader {
|
||||||
|
magic_number: 0x0BD2_2018,
|
||||||
|
version: 1,
|
||||||
|
event_types: vec![
|
||||||
|
AvailableAutoEventType::Speed,
|
||||||
|
AvailableAutoEventType::RPM,
|
||||||
|
AvailableAutoEventType::FuelLevel,
|
||||||
|
AvailableAutoEventType::GPSLocation,
|
||||||
|
AvailableAutoEventType::Throttle,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
records: vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(new_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(path: &str) -> ::anyhow::Result<Self> {
|
||||||
|
println!("0");
|
||||||
|
let file_bytes: Vec<u8> = std::fs::read(path)?;
|
||||||
|
println!("1");
|
||||||
|
let handle = std::fs::OpenOptions::new().read(true).write(true).open(path)?;
|
||||||
|
println!("2");
|
||||||
|
|
||||||
|
//let magic_number = bincode::deserialize::<u32>(&file_bytes[0..3])?;
|
||||||
|
//let version = bincode::deserialize::<u8>(&file_bytes[4..4])?;
|
||||||
|
println!("3");
|
||||||
|
|
||||||
|
//if magic_number != 0x0BD2_2018 {
|
||||||
|
// return Err(anyhow::anyhow!("Bad magic number. Is this an obt file?"));
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
|
||||||
|
let contents = bincode::deserialize::<FileContents>(file_bytes.as_slice())?;
|
||||||
|
|
||||||
|
if contents.header.version != 1 {
|
||||||
|
return Err(anyhow::anyhow!("File version {} is unsupported. We only support file version 1.", contents.header.version));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
file_handle: handle,
|
||||||
|
contents
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_record(&mut self, record: AutoEvent) {
|
||||||
|
self.contents.records.push(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&mut self) -> ::anyhow::Result<()> {
|
||||||
|
self.file_handle.set_len(0)?;
|
||||||
|
|
||||||
|
let new_contents = bincode::serialize::<FileContents>(&self.contents)?;
|
||||||
|
|
||||||
|
self.file_handle.write_all(&new_contents)?;
|
||||||
|
|
||||||
|
self.file_handle.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_json(&mut self) -> ::anyhow::Result<()> {
|
||||||
|
println!("$");
|
||||||
|
self.file_handle.set_len(0)?;
|
||||||
|
|
||||||
|
let new_contents = serde_json::to_string::<FileContents>(&self.contents)?;
|
||||||
|
|
||||||
|
self.file_handle.write_all(&new_contents.as_bytes())?;
|
||||||
|
|
||||||
|
self.file_handle.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
246
src/main.rs
Normal file
246
src/main.rs
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
use obd2::{commands::Obd2DataRetrieval, device::{Elm327, SerialPort}, Obd2};
|
||||||
|
use std::thread;
|
||||||
|
use std::thread::JoinHandle;
|
||||||
|
use std::time::Duration;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use gpsd_client::*;
|
||||||
|
|
||||||
|
mod auto_events;
|
||||||
|
use auto_events::{AutoEvent, AutoEventType};
|
||||||
|
|
||||||
|
mod file_format;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static THREAD_DELAY: Duration = Duration::from_millis(1_000);
|
||||||
|
static trip_id: Mutex<u32> = Mutex::new(0);
|
||||||
|
|
||||||
|
fn get_speed(device: &mut Obd2::<Elm327<SerialPort>>) -> ::anyhow::Result<AutoEventType> {
|
||||||
|
if let Some(speed_kmph) = device.get_speed()?.first() {
|
||||||
|
let speed_kmph_u32: u32 = speed_kmph.clone().into();
|
||||||
|
let speed_mph: u32 = (speed_kmph_u32 * 621371) / 1000000;
|
||||||
|
|
||||||
|
Ok(auto_events::SpeedEvent::new(speed_mph))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Unable to read speed from OBDII"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_rpm(device: &mut Obd2::<Elm327<SerialPort>>) -> ::anyhow::Result<AutoEventType> {
|
||||||
|
if let Some(rpm) = device.get_rpm()?.first() {
|
||||||
|
Ok(auto_events::RPMEvent::new(rpm.round() as u32))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Unable to read rpm from OBDII"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_throttle(device: &mut Obd2::<Elm327<SerialPort>>) -> ::anyhow::Result<AutoEventType> {
|
||||||
|
if let Some(throttle_level) = device.get_throttle_position()?.first() {
|
||||||
|
Ok(auto_events::ThrottleEvent::new(f32::from(throttle_level.clone())/255.0))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Unable to read rpm from OBDII"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_fuel_level(device: &mut Obd2::<Elm327<SerialPort>>) -> ::anyhow::Result<AutoEventType> {
|
||||||
|
if let Some(fuel_level) = device.get_fuel_level()?.first() {
|
||||||
|
Ok(auto_events::FuelLevelEvent::new(f32::from(fuel_level.clone())/255.0))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Unable to read rpm from OBDII"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_reconnect(device: &mut Option<Obd2<Elm327<SerialPort>>>, vin: &mut Option<String>) {
|
||||||
|
loop {
|
||||||
|
thread::sleep(THREAD_DELAY);
|
||||||
|
|
||||||
|
*device = None;
|
||||||
|
|
||||||
|
if let Ok(port) = SerialPort::new("/dev/ttyUSB0") {
|
||||||
|
match Elm327::new(port) {
|
||||||
|
Ok(new_device) =>{
|
||||||
|
if let Ok(mut dev) = Obd2::new(new_device) {
|
||||||
|
if let Err(x) = dev.reset() {
|
||||||
|
log::error!("OBDII initialization failed, retrying... {}", x);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Connected to OBDII");
|
||||||
|
|
||||||
|
match dev.get_vin() {
|
||||||
|
Ok(new_vin) => {
|
||||||
|
*vin = Some(new_vin.clone());
|
||||||
|
match vin_parser::get_info(new_vin.as_str()) {
|
||||||
|
Ok(vin_result) => {
|
||||||
|
let year = vin_result.years().last().map_or("Unknown Year".to_string(), |y| y.to_string());
|
||||||
|
log::info!("Vehicle is: {} {}", vin_result.manufacturer, year);
|
||||||
|
}
|
||||||
|
Err(_) => log::warn!("Unable to decode VIN"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
*vin = None;
|
||||||
|
log::warn!("Unable to read VIN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*device = Some(dev);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
log::warn!("Reconnect failed. Ignition may be off.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(x)=>{
|
||||||
|
log::warn!("Reconnect failed. Ignition may be off. {}", x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_handler(events: Arc<Mutex<VecDeque<AutoEvent>>>) {
|
||||||
|
thread::sleep(THREAD_DELAY / 2);
|
||||||
|
let mut cur_tid: u32 = trip_id.lock().unwrap().clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
|
||||||
|
let cur_datetime = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap();
|
||||||
|
let mut file = file_format::File::create(format!("./out_{}.obt", cur_datetime.as_secs()).as_str()).unwrap();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
thread::sleep(THREAD_DELAY);
|
||||||
|
|
||||||
|
let tid = trip_id.lock().unwrap();
|
||||||
|
|
||||||
|
if *tid > cur_tid {
|
||||||
|
file.write().unwrap();
|
||||||
|
cur_tid = tid.clone();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ref mut el = events.lock().unwrap();
|
||||||
|
while !el.is_empty() {
|
||||||
|
if let Some(event) = el.pop_front() {
|
||||||
|
log::info!("{:?}", event);
|
||||||
|
file.add_record(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_ts() -> i64 {
|
||||||
|
let time = chrono::Utc::now();
|
||||||
|
time.timestamp_millis()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_getter(events: Arc<Mutex<VecDeque<AutoEvent>>>, device: Arc<Mutex<Option<Obd2<Elm327<SerialPort>>>>>, vin: Arc<Mutex<Option<String>>>) {
|
||||||
|
let event_getters: Vec<fn(&mut Obd2::<Elm327<SerialPort>>) -> ::anyhow::Result<AutoEventType>> = vec![
|
||||||
|
get_speed,
|
||||||
|
get_rpm,
|
||||||
|
get_throttle,
|
||||||
|
get_fuel_level
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut ignition_on: bool = false;
|
||||||
|
let mut gps: GPS = GPS::connect().unwrap();
|
||||||
|
|
||||||
|
'record_loop: loop {
|
||||||
|
thread::sleep(THREAD_DELAY);
|
||||||
|
let mut full_event: AutoEvent = AutoEvent{ content: vec![], timestamp: 0 };
|
||||||
|
|
||||||
|
for event in &event_getters {
|
||||||
|
let ref mut x = device.lock().unwrap();
|
||||||
|
if let Some(ref mut dev) = *(*x) {
|
||||||
|
match event(dev) {
|
||||||
|
Ok(event) => {
|
||||||
|
// Derive ignition on/off events from RPM
|
||||||
|
if let AutoEventType::RPM(ev) = event.clone() {
|
||||||
|
if ev.rpm > 0 && !ignition_on{
|
||||||
|
log::info!("Ignition ON");
|
||||||
|
ignition_on = true;
|
||||||
|
continue 'record_loop;
|
||||||
|
} else if ignition_on && ev.rpm == 0 {
|
||||||
|
ignition_on = false;
|
||||||
|
{
|
||||||
|
let mut mtx = trip_id.lock().unwrap();
|
||||||
|
*mtx += 1;
|
||||||
|
}
|
||||||
|
log::info!("Ignition OFF");
|
||||||
|
continue 'record_loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ignition_on {
|
||||||
|
full_event.content.push(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
if let Some(obd2::Error::Device(obd2::error::DeviceError(obd2::device::Error::IO(io_err)))) = err.downcast_ref::<obd2::Error>() {
|
||||||
|
if io_err.kind() == std::io::ErrorKind::BrokenPipe {
|
||||||
|
log::warn!("Disconnected from OBDII reader. Ignition may be off. Attempting to reconnect.");
|
||||||
|
wait_for_reconnect(x, &mut vin.lock().unwrap());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if let Some(obd2::Error::Device(obd2::error::DeviceError(obd2::device::Error::Communication(_)))) = err.downcast_ref::<obd2::Error>() {
|
||||||
|
log::error!("OBDII connection was broken. Resetting connection.");
|
||||||
|
wait_for_reconnect(x, &mut vin.lock().unwrap());
|
||||||
|
log::error!("{}", err);
|
||||||
|
} else {
|
||||||
|
log::error!("{}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if full_event.content.len() == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: GPSData = gps.current_data().unwrap();
|
||||||
|
full_event.content.push(auto_events::GPSLocationEvent::new(data.lat, data.lon));
|
||||||
|
let speed_ms = data.speed;
|
||||||
|
let speed_mph = speed_ms * 2.2369362921;
|
||||||
|
full_event.content.push(auto_events::GPSSpeedEvent::new(speed_mph.round() as u32));
|
||||||
|
full_event.timestamp = unix_ts();
|
||||||
|
|
||||||
|
events.lock().unwrap().push_back(full_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ::anyhow::Result<()> {
|
||||||
|
file_format::File::open("weis_drive.obt")?.write_json()?;
|
||||||
|
return Ok(());
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let events: Arc<Mutex<VecDeque<AutoEvent>>> = Arc::new(Mutex::new(vec![].into()));
|
||||||
|
let device: Arc<Mutex<Option<Obd2<Elm327<SerialPort>>>>> = Arc::new(Mutex::new(None));
|
||||||
|
let vin: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||||
|
|
||||||
|
log::info!("Starting On Board Telematics v1.0.0");
|
||||||
|
|
||||||
|
wait_for_reconnect(&mut *device.lock().unwrap(), &mut vin.lock().unwrap());
|
||||||
|
|
||||||
|
|
||||||
|
// Start worker threads
|
||||||
|
let mut threads: Vec<JoinHandle<()>> = vec![];
|
||||||
|
let mut events_cloned = Arc::clone(&events);
|
||||||
|
/*hreads.push(*//*)*/
|
||||||
|
thread::spawn(move|| {event_handler(events_cloned)});
|
||||||
|
|
||||||
|
events_cloned = Arc::clone(&events);
|
||||||
|
let device_cloned = Arc::clone(&device);
|
||||||
|
let vin_cloned = Arc::clone(&vin);
|
||||||
|
threads.push(thread::spawn(move|| {event_getter(events_cloned, device_cloned, vin_cloned)}));
|
||||||
|
|
||||||
|
for thread in threads {
|
||||||
|
thread.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in a new issue