use crate::utils::ffi;
use crate::data::error::{DarraError, FoEErrorCode, Result};
use std::ffi::{CStr, CString};
use std::sync::{Arc, Mutex, OnceLock};
pub struct FoEInstance {
master_index: u16,
slave_index: u16,
pub default_timeout_ms: i32,
pub default_password: u32,
last_error: Option<FoEErrorCode>,
}
impl FoEInstance {
pub fn new(master_index: u16, slave_index: u16) -> Self {
Self {
master_index,
slave_index,
default_timeout_ms: 5000,
default_password: 0,
last_error: None,
}
}
pub fn last_error(&self) -> Option<FoEErrorCode> {
self.last_error
}
pub fn is_supported(&self) -> bool {
let proto = unsafe { ffi::GetSlaveMailboxProto(self.master_index, self.slave_index) };
(proto & 0x08) != 0
}
pub fn download(
&self,
filename: &str,
password: Option<u32>,
timeout_ms: Option<i32>,
) -> Result<Vec<u8>> {
let c_filename = CString::new(filename)
.map_err(|_| DarraError::InvalidParameter("文件名包含空字节".into()))?;
let pwd = password.unwrap_or(self.default_password);
let timeout_us = (timeout_ms.unwrap_or(self.default_timeout_ms)) * 1000;
let mut data_ptr = std::ptr::null_mut();
let mut file_size: i32 = 0;
let result = unsafe {
ffi::FOERead(
self.master_index,
self.slave_index,
c_filename.as_ptr(),
pwd,
&mut data_ptr,
&mut file_size,
timeout_us,
)
};
if result == 0 || data_ptr.is_null() || file_size <= 0 {
if !data_ptr.is_null() {
unsafe { ffi::FreeMemory(data_ptr as *mut std::os::raw::c_void); }
}
return Err(DarraError::FoeFailed(format!(
"FoE 下载失败: 从站={}, 文件={}",
self.slave_index, filename
)));
}
let data = unsafe {
let slice = std::slice::from_raw_parts(data_ptr as *const u8, file_size as usize);
let vec = slice.to_vec();
ffi::FreeMemory(data_ptr as *mut std::os::raw::c_void);
vec
};
Ok(data)
}
pub fn upload(
&self,
filename: &str,
file_data: &[u8],
password: Option<u32>,
timeout_ms: Option<i32>,
) -> Result<()> {
let c_filename = CString::new(filename)
.map_err(|_| DarraError::InvalidParameter("文件名包含空字节".into()))?;
if file_data.is_empty() {
return Err(DarraError::InvalidParameter("文件数据不能为空".into()));
}
let pwd = password.unwrap_or(self.default_password);
let timeout_us = (timeout_ms.unwrap_or(self.default_timeout_ms)) * 1000;
let result = unsafe {
ffi::FOEWrite(
self.master_index,
self.slave_index,
c_filename.as_ptr(),
pwd,
file_data.as_ptr() as *const std::os::raw::c_void,
file_data.len() as i32,
timeout_us,
)
};
if result == 0 {
return Err(DarraError::FoeFailed(format!(
"FoE 上传失败: 从站={}, 文件={}",
self.slave_index, filename
)));
}
Ok(())
}
pub fn set_progress_hook(&self, callback: ffi::FoEProgressCallback) -> bool {
unsafe { ffi::FOESetProgressHook(self.master_index, Some(callback)) != 0 }
}
pub fn clear_progress_hook(&self) -> bool {
unsafe { ffi::FOEClearProgressHook(self.master_index) != 0 }
}
pub fn cancel(&self) -> bool {
unsafe { ffi::FOERequestCancel(self.master_index, self.slave_index) != 0 }
}
pub fn clear_cancel(&self) -> bool {
unsafe { ffi::FOEClearCancel(self.master_index, self.slave_index) != 0 }
}
pub fn set_busy_hook(&self, cb: FoEBusyCallback) {
add_busy_hook(self.master_index, cb);
}
pub fn clear_busy_hooks(&self) {
clear_busy_hooks(self.master_index);
}
pub fn estimate_packet_count(file_size: usize, mailbox_size: usize) -> usize {
if file_size == 0 || mailbox_size == 0 {
return 0;
}
let data_per_packet = if mailbox_size > 6 { mailbox_size - 6 } else { 128 };
(file_size + data_per_packet - 1) / data_per_packet
}
pub fn default_timeout_ms(&self) -> i32 {
self.default_timeout_ms
}
pub fn set_default_timeout_ms(&mut self, ms: i32) {
self.default_timeout_ms = ms;
}
pub fn default_password(&self) -> u32 {
self.default_password
}
pub fn set_default_password(&mut self, pw: u32) {
self.default_password = pw;
}
pub fn download_with_crc(
&mut self,
filename: &str,
password: Option<u32>,
timeout_ms: Option<i32>,
enable_crc: bool,
) -> Result<Vec<u8>> {
let c_filename = CString::new(filename)
.map_err(|_| DarraError::InvalidParameter("文件名包含空字节".into()))?;
let pwd = password.unwrap_or(self.default_password);
let timeout_us = (timeout_ms.unwrap_or(self.default_timeout_ms)) * 1000;
let mut data_ptr = std::ptr::null_mut();
let mut file_size: i32 = 0;
let mut options = ffi::FoEOptions {
enable_crc: if enable_crc { 1 } else { 0 },
strict_mode: if enable_crc { 1 } else { 0 },
auto_append_crc: if enable_crc { 1 } else { 0 },
expected_crc: 0,
crc_progress_callback: None,
crc_callback_userdata: std::ptr::null_mut(),
reserved: [0u32; 8],
};
let result = unsafe {
ffi::FOEReadEx(
self.master_index,
self.slave_index,
c_filename.as_ptr(),
pwd,
&mut data_ptr,
&mut file_size,
timeout_us,
&mut options,
)
};
if result == 0 || data_ptr.is_null() || file_size <= 0 {
if !data_ptr.is_null() {
unsafe { ffi::FreeMemory(data_ptr as *mut std::os::raw::c_void); }
}
self.last_error = Some(FoEErrorCode::NotDefined);
return Err(DarraError::FoeFailed(format!(
"FoE CRC下载失败: 从站={}, 文件={}",
self.slave_index, filename
)));
}
let data = unsafe {
let slice = std::slice::from_raw_parts(data_ptr as *const u8, file_size as usize);
let vec = slice.to_vec();
ffi::FreeMemory(data_ptr as *mut std::os::raw::c_void);
vec
};
self.last_error = None;
Ok(data)
}
pub fn upload_with_crc(
&mut self,
filename: &str,
file_data: &[u8],
password: Option<u32>,
timeout_ms: Option<i32>,
enable_crc: bool,
) -> Result<()> {
let c_filename = CString::new(filename)
.map_err(|_| DarraError::InvalidParameter("文件名包含空字节".into()))?;
if file_data.is_empty() {
return Err(DarraError::InvalidParameter("文件数据不能为空".into()));
}
let pwd = password.unwrap_or(self.default_password);
let timeout_us = (timeout_ms.unwrap_or(self.default_timeout_ms)) * 1000;
let mut options = ffi::FoEOptions {
enable_crc: if enable_crc { 1 } else { 0 },
strict_mode: if enable_crc { 1 } else { 0 },
auto_append_crc: if enable_crc { 1 } else { 0 },
expected_crc: 0,
crc_progress_callback: None,
crc_callback_userdata: std::ptr::null_mut(),
reserved: [0u32; 8],
};
let result = unsafe {
ffi::FOEWriteEx(
self.master_index,
self.slave_index,
c_filename.as_ptr(),
pwd,
file_data.as_ptr() as *const std::os::raw::c_void,
file_data.len() as i32,
timeout_us,
&mut options,
)
};
if result == 0 {
self.last_error = Some(FoEErrorCode::NotDefined);
return Err(DarraError::FoeFailed(format!(
"FoE CRC上传失败: 从站={}, 文件={}",
self.slave_index, filename
)));
}
self.last_error = None;
Ok(())
}
pub fn get_error_description(error_code: FoEErrorCode) -> &'static str {
match error_code {
FoEErrorCode::NotDefined => "未定义错误",
FoEErrorCode::NotFound => "文件未找到",
FoEErrorCode::AccessDenied => "访问被拒绝",
FoEErrorCode::DiskFull => "磁盘已满",
FoEErrorCode::Illegal => "非法操作",
FoEErrorCode::PacketNumberWrong => "数据包序号错误",
FoEErrorCode::AlreadyExists => "文件已存在",
FoEErrorCode::NoUser => "无此用户",
FoEErrorCode::BootstrapOnly => "仅 Bootstrap 模式可用",
FoEErrorCode::NotBootstrap => "不在 Bootstrap 模式",
FoEErrorCode::NoRights => "权限不足",
FoEErrorCode::ProgramError => "程序错误",
}
}
}
impl FoEInstance {
pub fn download_blocking(
master_index: u16,
slave_index: u16,
filename: String,
password: Option<u32>,
timeout_ms: Option<i32>,
) -> std::thread::JoinHandle<Result<Vec<u8>>> {
std::thread::spawn(move || {
let foe = FoEInstance::new(master_index, slave_index);
foe.download(&filename, password, timeout_ms)
})
}
pub fn upload_blocking(
master_index: u16,
slave_index: u16,
filename: String,
file_data: Vec<u8>,
password: Option<u32>,
timeout_ms: Option<i32>,
) -> std::thread::JoinHandle<Result<()>> {
std::thread::spawn(move || {
let foe = FoEInstance::new(master_index, slave_index);
foe.upload(&filename, &file_data, password, timeout_ms)
})
}
}
#[cfg(feature = "async-tokio")]
impl FoEInstance {
pub async fn download_async(
&self,
filename: String,
password: Option<u32>,
timeout_ms: Option<i32>,
) -> Result<Vec<u8>> {
let master = self.master_index;
let slave = self.slave_index;
let default_pwd = self.default_password;
let default_to = self.default_timeout_ms;
tokio::task::spawn_blocking(move || {
let mut foe = FoEInstance::new(master, slave);
foe.default_password = default_pwd;
foe.default_timeout_ms = default_to;
foe.download(&filename, password, timeout_ms)
})
.await
.map_err(|e| DarraError::Other(format!("tokio join error: {}", e)))?
}
pub async fn upload_async(
&self,
filename: String,
file_data: Vec<u8>,
password: Option<u32>,
timeout_ms: Option<i32>,
) -> Result<()> {
let master = self.master_index;
let slave = self.slave_index;
let default_pwd = self.default_password;
let default_to = self.default_timeout_ms;
tokio::task::spawn_blocking(move || {
let mut foe = FoEInstance::new(master, slave);
foe.default_password = default_pwd;
foe.default_timeout_ms = default_to;
foe.upload(&filename, &file_data, password, timeout_ms)
})
.await
.map_err(|e| DarraError::Other(format!("tokio join error: {}", e)))?
}
}
impl crate::abstractions::MailboxProtocol for FoEInstance {
fn protocol_type(&self) -> u8 { 0x04 }
fn protocol_name(&self) -> &'static str { "FoE" }
fn is_supported(&self) -> bool {
FoEInstance::is_supported(self)
}
fn last_error_code(&self) -> u32 {
match self.last_error {
Some(e) => e as u32,
None => 0,
}
}
fn statistics(&self) -> crate::abstractions::MailboxStatistics {
let mut stats = ffi::EcMbxStatsC::default();
let rc = unsafe {
ffi::mbx_get_stats_by_master(
self.master_index, self.slave_index, 0x04, &mut stats,
)
};
if rc == 1 {
stats.into()
} else {
crate::abstractions::MailboxStatistics::empty()
}
}
fn reset_statistics(&self) {
unsafe {
ffi::mbx_reset_stats_by_master(self.master_index, self.slave_index, 0x04);
}
}
}
#[derive(Debug, Clone)]
pub struct FoEBusyEvent {
pub slave_index: u16,
pub done: u16,
pub entire: u16,
pub text: String,
pub retry_idx: i32,
pub percent: u32,
}
impl FoEBusyEvent {
fn new(slave: u16, done: u16, entire: u16, text: String, retry_idx: i32) -> Self {
let percent = if entire > 0 {
(((done as u64) * 100) / entire as u64).min(100) as u32
} else {
0
};
Self {
slave_index: slave,
done,
entire,
text,
retry_idx,
percent,
}
}
}
pub type FoEBusyCallback = Arc<dyn Fn(&FoEBusyEvent) + Send + Sync>;
fn busy_registry() -> &'static Mutex<std::collections::HashMap<u16, Vec<FoEBusyCallback>>> {
static REG: OnceLock<Mutex<std::collections::HashMap<u16, Vec<FoEBusyCallback>>>> =
OnceLock::new();
REG.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
}
fn busy_bound_masters() -> &'static Mutex<std::collections::HashSet<u16>> {
static BOUND: OnceLock<Mutex<std::collections::HashSet<u16>>> = OnceLock::new();
BOUND.get_or_init(|| Mutex::new(std::collections::HashSet::new()))
}
extern "C" fn busy_trampoline(
slave: u16,
done: u16,
entire: u16,
text: *const std::os::raw::c_char,
retry_idx: std::os::raw::c_int,
) {
let text_str = if text.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(text) }
.to_string_lossy()
.into_owned()
};
let snapshots: Vec<(u16, Vec<FoEBusyCallback>)> = match busy_registry().lock() {
Ok(g) => g.iter().map(|(k, v)| (*k, v.clone())).collect(),
Err(_) => return,
};
if snapshots.is_empty() {
return;
}
for (_master, cbs) in &snapshots {
for cb in cbs {
let ev = FoEBusyEvent::new(slave, done, entire, text_str.clone(), retry_idx as i32);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cb(&ev)));
}
}
}
fn add_busy_hook(master_index: u16, cb: FoEBusyCallback) {
if let Ok(mut reg) = busy_registry().lock() {
reg.entry(master_index).or_default().push(cb);
}
let need_bind = match busy_bound_masters().lock() {
Ok(mut set) => set.insert(master_index),
Err(_) => false,
};
if need_bind {
unsafe {
let _ = ffi::FOESetBusyHook(master_index, Some(busy_trampoline));
}
}
}
fn clear_busy_hooks(master_index: u16) {
if let Ok(mut reg) = busy_registry().lock() {
reg.remove(&master_index);
}
let was_bound = match busy_bound_masters().lock() {
Ok(mut set) => set.remove(&master_index),
Err(_) => false,
};
if was_bound {
unsafe {
let _ = ffi::FOESetBusyHook(master_index, None);
}
}
}