Skip to content

Commit

Permalink
Poll for serial port changes from Rust
Browse files Browse the repository at this point in the history
Signed-off-by: xychen <xychen@listenai.com>
  • Loading branch information
xychen committed May 27, 2024
1 parent 75c5638 commit a97f52a
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 10 deletions.
63 changes: 63 additions & 0 deletions src-tauri/src/cmds_serialport.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use std::sync::Mutex;

use serialport::available_ports;
use tauri::{ipc::Channel, Manager, Resource, ResourceId, Runtime, Webview};

use crate::serialport_watcher::{SerialPortEventHandler, SerialPortWatcher, SerialPortWatcherImpl};

#[tauri::command]
pub fn list_ports() -> Vec<String> {
Expand All @@ -19,3 +24,61 @@ pub fn list_ports() -> Vec<String> {
Err(_) => Vec::new(),
}
}

struct WatcherHandler {
on_event: Channel,
last_ports: Vec<String>,
}

impl SerialPortEventHandler for WatcherHandler {
fn handle_event(&mut self) {
let current_ports = list_ports();
if current_ports != self.last_ports {
let _ = self.on_event.send(current_ports.clone());
self.last_ports = current_ports;
}
}
}

pub struct InnerWatcher {
watcher: SerialPortWatcherImpl,
}

pub struct WatcherResource(Mutex<InnerWatcher>);
impl WatcherResource {
pub fn new(watcher: SerialPortWatcherImpl) -> Self {
Self(Mutex::new(InnerWatcher { watcher }))
}

fn with_lock<R, F: FnMut(&mut InnerWatcher) -> R>(&self, mut f: F) -> R {
let mut watcher = self.0.lock().unwrap();
f(&mut watcher)
}
}

impl Resource for WatcherResource {}

#[tauri::command]
pub async fn watch_ports<R: Runtime>(webview: Webview<R>, on_event: Channel) -> ResourceId {
let event_handler = WatcherHandler {
on_event,
last_ports: Vec::new(),
};

let mut watcher = SerialPortWatcherImpl::new(event_handler);
watcher.watch();

webview.resources_table().add(WatcherResource::new(watcher))
}

#[tauri::command]
pub async fn unwatch_ports<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> () {
let watcher = webview
.resources_table()
.take::<WatcherResource>(rid)
.unwrap();

WatcherResource::with_lock(&watcher, |watcher| {
watcher.watcher.unwatch();
});
}
3 changes: 3 additions & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod cmds_serialport;
mod cmds_string;
mod error;
mod file;
mod serialport_watcher;

pub use error::Error;
type Result<T> = std::result::Result<T, Error>;
Expand All @@ -27,6 +28,8 @@ fn main() {
cmds_hex::read_hex,
cmds_lpk::read_lpk,
cmds_serialport::list_ports,
cmds_serialport::watch_ports,
cmds_serialport::unwatch_ports,
cmds_string::decode,
])
.run(tauri::generate_context!())
Expand Down
17 changes: 17 additions & 0 deletions src-tauri/src/serialport_watcher/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
mod poll;

pub trait SerialPortEventHandler: Send + 'static {
fn handle_event(&mut self);
}

pub trait SerialPortWatcher {
fn new<F: SerialPortEventHandler>(event_handler: F) -> Self
where
Self: Sized;

fn watch(&mut self) -> ();

fn unwatch(&mut self) -> ();
}

pub type SerialPortWatcherImpl = poll::SerialPortPollWatcher;
57 changes: 57 additions & 0 deletions src-tauri/src/serialport_watcher/poll.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use std::{
sync::{Arc, Mutex},
thread::{sleep, spawn},
time::Duration,
};

use super::{SerialPortEventHandler, SerialPortWatcher};

pub struct SerialPortPollWatcher {
polling: Arc<Mutex<bool>>,
event_handler: Arc<Mutex<dyn SerialPortEventHandler>>,
}

impl SerialPortWatcher for SerialPortPollWatcher {
fn new<F: SerialPortEventHandler>(event_handler: F) -> Self {
Self {
polling: Arc::new(Mutex::new(false)),
event_handler: Arc::new(Mutex::new(event_handler)),
}
}

fn watch(&mut self) -> () {
let polling = self.polling.clone();
let event_handler = self.event_handler.clone();

spawn(move || {
let mut is_polling = polling.lock().unwrap();
*is_polling = true;
drop(is_polling);

loop {
let is_polling = polling.lock().unwrap();
if !*is_polling {
break;
}
drop(is_polling);

let mut event_handler = event_handler.lock().unwrap();
event_handler.handle_event();
drop(event_handler);

sleep(Duration::from_secs(1));
}
});
}

fn unwatch(&mut self) -> () {
let mut polling = self.polling.lock().unwrap();
*polling = false;
}
}

impl Drop for SerialPortPollWatcher {
fn drop(&mut self) {
let _ = self.unwatch();
}
}
22 changes: 12 additions & 10 deletions src/components/sections/PortSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
</template>

<script lang="ts" setup>
import { computed, watch } from 'vue';
import { computed, ref } from 'vue';
import { NEmpty, NSelect, useThemeVars, type SelectOption } from 'naive-ui';
import { invoke } from '@tauri-apps/api/core';
import { useIntervally } from '@/composables/window/useIntervally';
import { watchPorts } from '@/utils/serialport';
import { useListen } from '@/composables/tauri/useListen';
const themeVars = useThemeVars();
Expand All @@ -22,17 +22,19 @@ const props = defineProps<{
const selectedPort = defineModel<string | null>('port');
const availablePorts = useIntervally(1000, async () => await invoke('list_ports'));
const availablePorts = ref<string[]>();
useListen(() => watchPorts((ports) => {
availablePorts.value = ports;
if (selectedPort.value && !ports?.includes(selectedPort.value)) {
selectedPort.value = null;
}
}));
const availableSelections = computed(() => (availablePorts.value ?? []).map((port) => ({
label: port,
value: port,
style: { fontFamily: themeVars.value.fontFamilyMono },
}) as SelectOption));
watch(availablePorts, (ports) => {
if (selectedPort.value && !ports?.includes(selectedPort.value)) {
selectedPort.value = null;
}
});
</script>
3 changes: 3 additions & 0 deletions src/tauri-invoke.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import '@tauri-apps/api/core';

declare module '@tauri-apps/api/core' {
import type { Channel } from '@tauri-apps/api/core';
import type { ISection } from '@/utils/readHex';
import type { IPartition } from '@/utils/images';
function invoke(cmd: 'read_hex', args: { path: string }): Promise<ISection[]>;
function invoke(cmd: 'read_lpk', args: { path: string }): Promise<IPartition[]>;
function invoke(cmd: 'list_ports'): Promise<string[]>;
function invoke<T>(cmd: 'watch_ports', args: { onEvent: Channel<T> }): Promise<number>;
function invoke(cmd: 'unwatch_ports', args: { rid: number }): Promise<void>;
function invoke(cmd: 'decode', args: { data: ArrayLike<number> }): Promise<string>;
}
20 changes: 20 additions & 0 deletions src/utils/serialport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { invoke, Channel } from '@tauri-apps/api/core';

type UnwatchFn = () => void;

export async function listPorts(): Promise<string[]> {
return await invoke('list_ports');
}

export async function watchPorts(cb: (ports: string[]) => void): Promise<UnwatchFn> {
const onEvent = new Channel<string[]>();
onEvent.onmessage = cb;

const rid = await invoke('watch_ports', {
onEvent: onEvent,
});

return () => {
void invoke('unwatch_ports', { rid });
};
}

0 comments on commit a97f52a

Please sign in to comment.