serialport::open() で開いたポートは、そのスコープを抜けると自動的に閉じられます。
接続を維持して読み書きを行うには、Tauri の State と Mutex を使ってポートのインスタンスを管理する必要があります。
1. 状態管理用の構造体定義
src-tauri/src/lib.rs (または main.rs) で、ポートを保持する構造体を定義します。
use std::sync::Mutex;
use serialport::SerialPort;
use tauri::State;
// シリアルポートを保持する State
// Send + Sync である必要があるため Mutex で包みます
pub struct SerialConnection {
pub port: Mutex<Option<Box<dyn SerialPort>>>,
}
run 関数で初期化します。
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(SerialConnection { port: Mutex::new(None) }) // 初期値は None
.invoke_handler(tauri::generate_handler![open_port, close_port, write_serial, write_serial_binary])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
2. コマンドの実装
接続 (Open)
use std::time::Duration;
#[tauri::command]
fn open_port(
port_name: String,
baud_rate: u32,
state: State<'_, SerialConnection>
) -> Result<String, String> {
let mut port_guard = state.port.lock().map_err(|e| e.to_string())?;
// すでに開いている場合は閉じるか、エラーにする
if port_guard.is_some() {
return Err("Port is already open".to_string());
}
let port = serialport::new(&port_name, baud_rate)
.timeout(Duration::from_millis(100))
.open()
.map_err(|e| e.to_string())?;
*port_guard = Some(port);
Ok(format!("{} connected", port_name))
}
切断 (Close)
#[tauri::command]
fn close_port(state: State<'_, SerialConnection>) -> Result<String, String> {
let mut port_guard = state.port.lock().map_err(|e| e.to_string())?;
if port_guard.is_some() {
*port_guard = None; // Drop させることでポートが閉じる
Ok("Disconnected".to_string())
} else {
Err("No connection".to_string())
}
}
送信 (Write)
#[tauri::command]
fn write_serial(content: String, state: State<'_, SerialConnection>) -> Result<String, String> {
let mut port_guard = state.port.lock().map_err(|e| e.to_string())?;
if let Some(port) = port_guard.as_mut() {
// 文字列をバイト列として送信
port.write_all(content.as_bytes()).map_err(|e| e.to_string())?;
Ok("Sent".to_string())
} else {
Err("Port not open".to_string())
}
}
#[tauri::command]
fn write_serial_binary(content: Vec<u8>, state: State<'_, SerialConnection>) -> Result<String, String> {
let mut port_guard = state.port.lock().map_err(|e| e.to_string())?;
if let Some(port) = port_guard.as_mut() {
port.write_all(&content).map_err(|e| e.to_string())?;
Ok("Sent".to_string())
} else {
Err("Port not open".to_string())
}
}
フロントエンドからの呼び出し例
import { invoke } from '@tauri-apps/api/core';
// 接続
async function connect() {
try {
const res = await invoke('open_port', { portName: 'COM3', baudRate: 9600 });
console.log(res);
} catch (e) {
console.error(e);
}
}
// 送信
async function sendData() {
try {
await invoke('write_serial', { content: 'Hello World\n' });
} catch (e) {
console.error(e);
}
}
// 送信 (バイナリ)
async function sendBinaryData() {
try {
// [0x01, 0x02, 0x03] を送信
await invoke('write_serial_binary', { content: [1, 2, 3] });
} catch (e) {
console.error(e);
}
}
// 切断
async function disconnect() {
try {
await invoke('close_port');
} catch (e) {
console.error(e);
}
}
3. 受信 (Read) の実装
受信は独立したスレッドで常時監視し、データを受け取ったら Tauri の Event システムを使ってフロントエンドへ通知する方式が一般的です。
src-tauri/src/lib.rs に以下の変更を加えます。
1. open_port コマンドの引数に AppHandle を追加
2. ポートを開いた直後に try_clone() して、別スレッドに渡す
3. スレッド内でループして read() を行う
use std::thread;
use std::io::Read;
use tauri::Emitter; // v2 でイベントを送るために必要
#[tauri::command]
fn open_port(
app: tauri::AppHandle, // 追加
port_name: String,
baud_rate: u32,
state: State<'_, SerialConnection>
) -> Result<String, String> {
let mut port_guard = state.port.lock().map_err(|e| e.to_string())?;
if port_guard.is_some() {
return Err("Port is already open".to_string());
}
let port = serialport::new(&port_name, baud_rate)
.timeout(Duration::from_millis(100))
.open()
.map_err(|e| e.to_string())?;
// --- 受信スレッドの開始 ---
// ポートをクローン(複製)する
let mut clone = port.try_clone().map_err(|e| e.to_string())?;
thread::spawn(move || {
let mut buffer: Vec<u8> = vec![0; 1024];
loop {
match clone.read(buffer.as_mut_slice()) {
Ok(0) => break, // EOF
Ok(n) => {
// 受信データを取得
let data = &buffer[..n];
// ここでは単純化のため String に変換して送信
// (実際にはバイナリ配列として送るか、プロトコルに合わせてパースします)
if let Ok(text) = String::from_utf8(data.to_vec()) {
// フロントエンドへ "serial-payload" イベントを発行
let _ = app.emit("serial-payload", text);
}
}
Err(ref e) if e.kind() == std::io::ErrorKind::TimedOut => {
// タイムアウト時はリトライ
continue;
}
Err(_) => {
// エラー時はスレッド終了
break;
}
}
}
});
// ------------------------
*port_guard = Some(port);
Ok(format!("{} connected", port_name))
}
フロントエンド側での受信:
import { listen } from '@tauri-apps/api/event';
// イベントリスナーの登録
await listen<string>('serial-payload', (event) => {
console.log('Received:', event.payload);
});