#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
use std::{collections::BTreeMap, sync::Arc};
use bevy_app::prelude::*;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::prelude::*;
use bevy_utils::default;
use bytes::Bytes;
use parking_lot::Mutex;
pub use serialport::{DataBits, FlowControl, Parity, StopBits};
use tokio::runtime::Builder;
pub use error::SerialError;
pub use serial_wrap::*;
pub use utils::*;
pub mod codec;
mod error;
mod serial_wrap;
pub mod utils;
pub struct SerialPortPlugin;
impl Plugin for SerialPortPlugin {
fn build(&self, app: &mut App) {
let tokio_rt = SerialPortRuntime(Arc::new(
Builder::new_multi_thread()
.enable_all()
.thread_name("bevy-serialport")
.build()
.expect("Failed to create Tokio runtime for serial port operations"),
));
app.insert_resource(tokio_rt)
.init_resource::<SerialResource>()
.add_message::<SerialData>()
.add_systems(PreUpdate, broadcast_serial_message);
}
}
#[derive(Debug, Message)]
pub struct SerialData {
pub port: String,
pub data: Bytes,
}
impl SerialData {
pub fn new(port: impl Into<String>, data: Bytes) -> Self {
Self {
port: port.into(),
data,
}
}
pub fn as_string_lossy(&self) -> String {
String::from_utf8_lossy(&self.data).to_string()
}
pub fn as_string(&self) -> Result<String, std::string::FromUtf8Error> {
String::from_utf8(self.data.to_vec())
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
#[derive(Resource, Deref, DerefMut)]
pub struct SerialPortRuntime(Arc<tokio::runtime::Runtime>);
pub type ArcRuntime = Arc<tokio::runtime::Runtime>;
pub type RecvQueue = Arc<Mutex<Vec<Bytes>>>;
#[derive(Default, Resource)]
pub struct SerialResource {
pub ports: BTreeMap<String, SerialPortWrap>,
}
impl SerialResource {
pub fn open(
&mut self,
task_pool: ArcRuntime,
port: impl ToString,
baud_rate: u32,
) -> Result<(), SerialError> {
let port_str = port.to_string();
let setting = SerialPortSetting {
port_name: port_str.clone(),
baud_rate,
..default()
};
self.open_with_setting(task_pool, setting)
}
pub fn open_with_setting(
&mut self,
task_pool: ArcRuntime,
setting: SerialPortSetting,
) -> Result<(), SerialError> {
let port_name = setting.port_name.clone();
if port_name.is_empty() {
return Err(SerialError::InvalidConfiguration {
reason: "Port name cannot be empty".to_string(),
});
}
if setting.baud_rate == 0 {
return Err(SerialError::InvalidConfiguration {
reason: "Baud rate must be greater than 0".to_string(),
});
}
let serial_port = SerialPortWrap::new(task_pool, setting).map_err(|e| match e {
SerialError::SerialPortError { source, .. } => {
SerialError::serial_port_error(&port_name, source)
}
other => other,
})?;
self.ports.insert(port_name, serial_port);
Ok(())
}
pub fn send_message(&mut self, port: &str, message: Bytes) -> Result<(), SerialError> {
match self.ports.get_mut(port) {
Some(serial_wrap) => serial_wrap
.msg_sender
.lock()
.send(message)
.map_err(|_| SerialError::channel_closed(port)),
None => Err(SerialError::port_not_found(port)),
}
}
pub fn send_string(&mut self, port: &str, message: &str) -> Result<(), SerialError> {
self.send_message(port, Bytes::from(message.as_bytes().to_vec()))
}
pub fn is_port_connected(&self, port: &str) -> bool {
self.ports.contains_key(port)
}
pub fn connected_ports(&self) -> Vec<&String> {
self.ports.keys().collect()
}
pub fn close_port(&mut self, port: &str) -> bool {
self.ports.remove(port).is_some()
}
pub fn close_all_ports(&mut self) {
self.ports.clear();
}
}
fn broadcast_serial_message(
mut serial_res: ResMut<SerialResource>,
mut message_ev: MessageWriter<SerialData>,
) {
let messages: Vec<SerialData> = serial_res
.ports
.iter_mut()
.flat_map(|(port_name, port_wrap)| {
port_wrap.get_messages().into_iter().map(|data| SerialData {
port: port_name.clone(),
data,
})
})
.collect();
if !messages.is_empty() {
message_ev.write_batch(messages);
}
}
#[cfg(test)]
mod unit_tests {
use bevy::prelude::{App, MinimalPlugins};
use crate::SerialPortPlugin;
#[test]
fn smoke_test_basic_integration_with_bevy_app() {
let mut app = App::new();
app.add_plugins((MinimalPlugins, SerialPortPlugin));
app.update();
app.update();
}
}