use std::cell::RefCell;
use std::io::{Read, Write};
use std::time::Duration;
use serialport::SerialPort;
use crate::coordinates::{Coordinates, EquatorialPosition, HorizontalPosition, TrackingRates};
use crate::error::{MountError, MountResult};
use crate::mount::{Mount, MountStatus, SiteLocation};
use crate::protocol::{Direction, MountMode, Protocol, ResponseParser, SlewRate, TrackingRate};
pub const DEFAULT_BAUD_RATE: u32 = 9600;
pub const DEFAULT_TIMEOUT_MS: u64 = 2000;
#[derive(Debug, Clone)]
pub struct SerialConfig {
pub port: String,
pub baud_rate: u32,
pub timeout_ms: u64,
pub retry_count: u8,
}
impl SerialConfig {
pub fn new(port: impl Into<String>) -> Self {
Self {
port: port.into(),
baud_rate: DEFAULT_BAUD_RATE,
timeout_ms: DEFAULT_TIMEOUT_MS,
retry_count: 3,
}
}
pub fn with_baud_rate(mut self, baud_rate: u32) -> Self {
self.baud_rate = baud_rate;
self
}
pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
self.timeout_ms = timeout_ms;
self
}
pub fn with_retry_count(mut self, count: u8) -> Self {
self.retry_count = count;
self
}
}
impl Default for SerialConfig {
fn default() -> Self {
Self::new("/dev/ttyUSB0")
}
}
pub struct SerialMount {
config: SerialConfig,
port: RefCell<Option<Box<dyn SerialPort>>>,
slew_rate: RefCell<SlewRate>,
guide_rate: RefCell<f64>,
site_location: RefCell<SiteLocation>,
}
impl SerialMount {
pub fn new(port: impl Into<String>) -> Self {
Self {
config: SerialConfig::new(port),
port: RefCell::new(None),
slew_rate: RefCell::new(SlewRate::MAX),
guide_rate: RefCell::new(0.5),
site_location: RefCell::new(SiteLocation::default()),
}
}
pub fn with_config(config: SerialConfig) -> Self {
Self {
config,
port: RefCell::new(None),
slew_rate: RefCell::new(SlewRate::MAX),
guide_rate: RefCell::new(0.5),
site_location: RefCell::new(SiteLocation::default()),
}
}
fn send_command(&self, command: &str) -> MountResult<String> {
let mut port_ref = self.port.borrow_mut();
let port = port_ref.as_mut().ok_or(MountError::NotConnected)?;
let _ = port.clear(serialport::ClearBuffer::Input);
port.write_all(command.as_bytes()).map_err(MountError::Io)?;
port.flush().map_err(MountError::Io)?;
log::debug!("Sent command: {}", command);
let mut response = String::new();
let mut buf = [0u8; 1];
let start = std::time::Instant::now();
let timeout = Duration::from_millis(self.config.timeout_ms);
loop {
if start.elapsed() > timeout {
return Err(MountError::Timeout(self.config.timeout_ms));
}
match port.read(&mut buf) {
Ok(1) => {
let c = buf[0] as char;
response.push(c);
if c == '#' {
break;
}
}
Ok(0) => {
std::thread::sleep(Duration::from_millis(10));
}
Ok(_) => unreachable!(),
Err(ref e) if e.kind() == std::io::ErrorKind::TimedOut => {
continue;
}
Err(e) => return Err(MountError::Io(e)),
}
}
log::debug!("Received response: {}", response);
Ok(response)
}
fn send_command_with_retry(&self, command: &str) -> MountResult<String> {
let mut last_error = None;
for attempt in 0..self.config.retry_count {
match self.send_command(command) {
Ok(response) => return Ok(response),
Err(e) => {
log::warn!(
"Command '{}' failed (attempt {}/{}): {}",
command,
attempt + 1,
self.config.retry_count,
e
);
last_error = Some(e);
std::thread::sleep(Duration::from_millis(100));
}
}
}
Err(last_error.unwrap_or_else(|| MountError::other("Unknown error")))
}
fn send_command_no_response(&self, command: &str) -> MountResult<()> {
let mut port_ref = self.port.borrow_mut();
let port = port_ref.as_mut().ok_or(MountError::NotConnected)?;
port.write_all(command.as_bytes()).map_err(MountError::Io)?;
port.flush().map_err(MountError::Io)?;
log::debug!("Sent command (no response): {}", command);
std::thread::sleep(Duration::from_millis(50));
Ok(())
}
fn send_command_bool_response(&self, command: &str) -> MountResult<bool> {
let mut port_ref = self.port.borrow_mut();
let port = port_ref.as_mut().ok_or(MountError::NotConnected)?;
let _ = port.clear(serialport::ClearBuffer::Input);
port.write_all(command.as_bytes()).map_err(MountError::Io)?;
port.flush().map_err(MountError::Io)?;
log::debug!("Sent command (bool response): {}", command);
let mut response = String::new();
let mut buf = [0u8; 1];
let start = std::time::Instant::now();
let timeout = Duration::from_millis(self.config.timeout_ms);
loop {
if start.elapsed() > timeout {
if response == "1" {
log::debug!("Received bool response (no terminator): true");
return Ok(true);
} else if response == "0" {
log::debug!("Received bool response (no terminator): false");
return Ok(false);
}
return Err(MountError::Timeout(self.config.timeout_ms));
}
match port.read(&mut buf) {
Ok(1) => {
let c = buf[0] as char;
if c == '#' {
break;
}
response.push(c);
if response == "1" || response == "0" {
std::thread::sleep(Duration::from_millis(100));
}
}
Ok(0) => {
if response == "1" || response == "0" {
std::thread::sleep(Duration::from_millis(50));
break;
}
std::thread::sleep(Duration::from_millis(10));
}
Ok(_) => unreachable!(),
Err(ref e) if e.kind() == std::io::ErrorKind::TimedOut => {
if response == "1" || response == "0" {
break;
}
continue;
}
Err(e) => return Err(MountError::Io(e)),
}
}
let result = response == "1";
log::debug!("Received bool response: {} -> {}", response, result);
Ok(result)
}
fn send_command_bool_with_retry(&self, command: &str) -> MountResult<bool> {
let mut last_error = None;
for attempt in 0..self.config.retry_count {
match self.send_command_bool_response(command) {
Ok(result) => return Ok(result),
Err(e) => {
log::warn!(
"Command '{}' failed (attempt {}/{}): {}",
command,
attempt + 1,
self.config.retry_count,
e
);
last_error = Some(e);
std::thread::sleep(Duration::from_millis(100));
}
}
}
Err(last_error.unwrap_or_else(|| MountError::other("Unknown error")))
}
fn ensure_connected(&self) -> MountResult<()> {
if self.port.borrow().is_none() {
Err(MountError::NotConnected)
} else {
Ok(())
}
}
fn get_status_flags(&self) -> MountResult<(bool, bool, bool, MountMode)> {
let response = self.send_command_with_retry(Protocol::get_status())?;
Ok(ResponseParser::parse_status(&response))
}
pub fn list_ports() -> Vec<String> {
serialport::available_ports()
.unwrap_or_default()
.into_iter()
.map(|p| p.port_name)
.collect()
}
}
impl Mount for SerialMount {
fn connect(&mut self) -> MountResult<()> {
if self.port.borrow().is_some() {
log::warn!("Mount already connected");
return Ok(());
}
log::info!("Connecting to mount on port {}", self.config.port);
let port = serialport::new(&self.config.port, self.config.baud_rate)
.timeout(Duration::from_millis(self.config.timeout_ms))
.data_bits(serialport::DataBits::Eight)
.parity(serialport::Parity::None)
.stop_bits(serialport::StopBits::One)
.flow_control(serialport::FlowControl::None)
.open()
.map_err(MountError::SerialPort)?;
*self.port.borrow_mut() = Some(port);
std::thread::sleep(Duration::from_millis(100));
match self.get_model() {
Ok(model) => {
log::info!("Connected to mount: {}", model);
Ok(())
}
Err(e) => {
*self.port.borrow_mut() = None;
Err(e)
}
}
}
fn disconnect(&mut self) -> MountResult<()> {
if let Some(port) = self.port.borrow_mut().take() {
drop(port);
log::info!("Mount disconnected");
}
Ok(())
}
fn is_connected(&self) -> bool {
self.port.borrow().is_some()
}
fn get_position(&self) -> MountResult<EquatorialPosition> {
self.ensure_connected()?;
let ra_response = self.send_command_with_retry(Protocol::get_ra())?;
let (ra_h, ra_m, ra_s) = ResponseParser::parse_ra(&ra_response)
.ok_or_else(|| MountError::invalid_response(&ra_response))?;
let dec_response = self.send_command_with_retry(Protocol::get_dec())?;
let (dec_d, dec_m, dec_s) = ResponseParser::parse_dec(&dec_response)
.ok_or_else(|| MountError::invalid_response(&dec_response))?;
let ra = Coordinates::hms_to_decimal(ra_h, ra_m, ra_s);
let dec = Coordinates::dms_to_decimal(dec_d, dec_m, dec_s);
Ok(EquatorialPosition::new(ra, dec))
}
fn get_altaz(&self) -> MountResult<HorizontalPosition> {
self.ensure_connected()?;
let az_response = self.send_command_with_retry(Protocol::get_azimuth())?;
let (az_d, az_m, az_s) = ResponseParser::parse_azimuth(&az_response)
.ok_or_else(|| MountError::invalid_response(&az_response))?;
let alt_response = self.send_command_with_retry(Protocol::get_altitude())?;
let (alt_d, alt_m, alt_s) = ResponseParser::parse_altitude(&alt_response)
.ok_or_else(|| MountError::invalid_response(&alt_response))?;
let az = az_d as f64 + az_m as f64 / 60.0 + az_s / 3600.0;
let alt = Coordinates::dms_to_decimal(alt_d as i16, alt_m, alt_s);
Ok(HorizontalPosition::new(az, alt))
}
fn goto_equatorial(&mut self, position: EquatorialPosition) -> MountResult<()> {
self.ensure_connected()?;
let set_ra_cmd = Protocol::set_target_ra_decimal(position.ra);
if !self.send_command_bool_with_retry(&set_ra_cmd)? {
return Err(MountError::invalid_coords("Invalid RA coordinates"));
}
let set_dec_cmd = Protocol::set_target_dec_decimal(position.dec);
if !self.send_command_bool_with_retry(&set_dec_cmd)? {
return Err(MountError::invalid_coords("Invalid Dec coordinates"));
}
let response = self.send_command_with_retry(Protocol::goto())?;
ResponseParser::parse_goto_response(&response).map_err(MountError::other)?;
log::info!(
"Slewing to RA={:.4}h, Dec={:.4}°",
position.ra,
position.dec
);
Ok(())
}
fn goto_altaz(&mut self, position: HorizontalPosition) -> MountResult<()> {
self.ensure_connected()?;
if !position.is_above_horizon() {
return Err(MountError::BelowHorizon(position.altitude));
}
log::debug!(
"Starting Alt-Az slew to Az={:.2}°, Alt={:.2}°",
position.azimuth,
position.altitude
);
const TOLERANCE_DEG: f64 = 0.1; const COARSE_THRESHOLD: f64 = 5.0; const FINE_THRESHOLD: f64 = 1.0; const MAX_ITERATIONS: u32 = 600; const LOOP_INTERVAL_MS: u64 = 100;
let mut iterations = 0u32;
let mut last_az_moving = false;
let mut last_alt_moving = false;
loop {
iterations += 1;
if iterations > MAX_ITERATIONS {
self.send_command_no_response(Protocol::stop_all())?;
return Err(MountError::Timeout(
MAX_ITERATIONS as u64 * LOOP_INTERVAL_MS,
));
}
let current = self.get_altaz()?;
let mut az_error = position.azimuth - current.azimuth;
if az_error > 180.0 {
az_error -= 360.0;
} else if az_error < -180.0 {
az_error += 360.0;
}
let alt_error = position.altitude - current.altitude;
let az_abs = az_error.abs();
let alt_abs = alt_error.abs();
if az_abs < TOLERANCE_DEG && alt_abs < TOLERANCE_DEG {
self.send_command_no_response(Protocol::stop_all())?;
log::debug!(
"Slew complete. Final position: Az={:.2}°, Alt={:.2}°",
current.azimuth,
current.altitude
);
return Ok(());
}
let max_error = az_abs.max(alt_abs);
let rate = if max_error > COARSE_THRESHOLD {
SlewRate::MAX } else if max_error > FINE_THRESHOLD {
SlewRate::FIND } else {
SlewRate::CENTER };
self.send_command_no_response(&Protocol::set_slew_rate(rate))?;
let az_should_move = az_abs >= TOLERANCE_DEG;
if az_should_move {
if az_error > 0.0 {
self.send_command_no_response(Protocol::move_east())?;
} else {
self.send_command_no_response(Protocol::move_west())?;
}
last_az_moving = true;
} else if last_az_moving {
self.send_command_no_response(Protocol::stop_east())?;
self.send_command_no_response(Protocol::stop_west())?;
last_az_moving = false;
}
let alt_should_move = alt_abs >= TOLERANCE_DEG;
if alt_should_move {
if alt_error > 0.0 {
self.send_command_no_response(Protocol::move_north())?;
} else {
self.send_command_no_response(Protocol::move_south())?;
}
last_alt_moving = true;
} else if last_alt_moving {
self.send_command_no_response(Protocol::stop_north())?;
self.send_command_no_response(Protocol::stop_south())?;
last_alt_moving = false;
}
if iterations % 20 == 0 {
log::debug!(
"Slewing: Az error={:.2}°, Alt error={:.2}°, rate={}",
az_error,
alt_error,
rate.value()
);
}
std::thread::sleep(Duration::from_millis(LOOP_INTERVAL_MS));
}
}
fn is_slewing(&self) -> MountResult<bool> {
self.ensure_connected()?;
let (_, slewing, _, _) = self.get_status_flags()?;
Ok(slewing)
}
fn abort_slew(&mut self) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::stop_all())?;
log::info!("Slew aborted");
Ok(())
}
fn move_axis(&mut self, direction: Direction) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::move_direction(direction))?;
log::debug!("Moving {:?}", direction);
Ok(())
}
fn stop_axis(&mut self, direction: Direction) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::stop_direction(direction))?;
log::debug!("Stopped {:?}", direction);
Ok(())
}
fn stop_all(&mut self) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::stop_all())?;
log::info!("All movement stopped");
Ok(())
}
fn set_slew_rate(&mut self, rate: SlewRate) -> MountResult<()> {
self.ensure_connected()?;
let cmd = Protocol::set_slew_rate(rate);
self.send_command_no_response(&cmd)?;
*self.slew_rate.borrow_mut() = rate;
log::debug!("Slew rate set to {}", rate.value());
Ok(())
}
fn set_tracking(&mut self, rate: TrackingRate) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::set_tracking_rate(rate))?;
log::info!("Tracking rate set to {:?}", rate);
Ok(())
}
fn tracking_on(&mut self) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::tracking_on())?;
log::info!("Tracking enabled");
Ok(())
}
fn tracking_off(&mut self) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::tracking_off())?;
log::info!("Tracking disabled");
Ok(())
}
fn is_tracking(&self) -> MountResult<bool> {
self.ensure_connected()?;
let (tracking, _, _, _) = self.get_status_flags()?;
Ok(tracking)
}
fn set_custom_tracking_rates(&mut self, rates: TrackingRates) -> MountResult<()> {
self.ensure_connected()?;
if !rates.is_valid_for_satellite() {
return Err(MountError::invalid_coords(format!(
"Tracking rates too high: RA={:.2}\"/s, Dec={:.2}\"/s",
rates.ra_rate, rates.dec_rate
)));
}
log::warn!("Custom tracking rates may not be supported by ZWO mounts");
Ok(())
}
fn set_guide_rate(&mut self, rate: f64) -> MountResult<()> {
self.ensure_connected()?;
let clamped = rate.clamp(0.1, 0.9);
let cmd = Protocol::set_guide_rate(clamped);
self.send_command_no_response(&cmd)?;
*self.guide_rate.borrow_mut() = clamped;
log::debug!("Guide rate set to {:.1}", clamped);
Ok(())
}
fn get_guide_rate(&self) -> MountResult<f64> {
self.ensure_connected()?;
Ok(*self.guide_rate.borrow())
}
fn guide_pulse(&mut self, direction: Direction, duration_ms: u32) -> MountResult<()> {
self.ensure_connected()?;
let cmd = Protocol::guide_pulse(direction, duration_ms);
self.send_command_no_response(&cmd)?;
log::debug!("Guide pulse: {:?} for {}ms", direction, duration_ms);
Ok(())
}
fn set_altaz_mode(&mut self) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::set_altaz_mode())?;
log::info!("Mount set to Alt-Az mode");
Ok(())
}
fn set_polar_mode(&mut self) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::set_polar_mode())?;
log::info!("Mount set to Polar/Equatorial mode");
Ok(())
}
fn get_mount_mode(&self) -> MountResult<MountMode> {
self.ensure_connected()?;
let (_, _, _, mode) = self.get_status_flags()?;
Ok(mode)
}
fn go_home(&mut self) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::find_home())?;
log::info!("Finding home position");
Ok(())
}
fn park(&mut self) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::goto_park())?;
log::info!("Mount parking");
Ok(())
}
fn unpark(&mut self) -> MountResult<()> {
self.ensure_connected()?;
self.send_command_no_response(Protocol::unpark())?;
log::info!("Mount unparked");
Ok(())
}
fn is_parked(&self) -> MountResult<bool> {
self.ensure_connected()?;
let (_, _, at_home, _) = self.get_status_flags()?;
Ok(at_home)
}
fn sync(&mut self, position: EquatorialPosition) -> MountResult<()> {
self.ensure_connected()?;
let set_ra_cmd = Protocol::set_target_ra_decimal(position.ra);
self.send_command_bool_with_retry(&set_ra_cmd)?;
let set_dec_cmd = Protocol::set_target_dec_decimal(position.dec);
self.send_command_bool_with_retry(&set_dec_cmd)?;
let _response = self.send_command_with_retry(Protocol::sync())?;
log::info!(
"Mount synced to RA={:.4}h, Dec={:.4}°",
position.ra,
position.dec
);
Ok(())
}
fn set_site_location(&mut self, location: SiteLocation) -> MountResult<()> {
self.ensure_connected()?;
let set_lat_cmd = Protocol::set_latitude(location.latitude);
if !self.send_command_bool_with_retry(&set_lat_cmd)? {
return Err(MountError::invalid_coords("Invalid latitude"));
}
let set_lon_cmd = Protocol::set_longitude(location.longitude);
if !self.send_command_bool_with_retry(&set_lon_cmd)? {
return Err(MountError::invalid_coords("Invalid longitude"));
}
*self.site_location.borrow_mut() = location;
log::info!(
"Site location set to lat={:.4}°, lon={:.4}°",
location.latitude,
location.longitude
);
Ok(())
}
fn get_site_location(&self) -> MountResult<SiteLocation> {
self.ensure_connected()?;
Ok(*self.site_location.borrow())
}
fn get_status(&self) -> MountResult<MountStatus> {
self.ensure_connected()?;
let (is_tracking, is_slewing, at_home, mount_mode) = self.get_status_flags()?;
let pier_response = self.send_command_with_retry(Protocol::get_cardinal_direction())?;
let pier_side = pier_response.trim_end_matches('#').to_string();
Ok(MountStatus {
is_tracking,
is_slewing,
mount_mode,
tracking_rate: TrackingRate::Sidereal, is_parked: at_home,
is_connected: true,
slew_rate: *self.slew_rate.borrow(),
guide_rate: *self.guide_rate.borrow(),
pier_side,
})
}
fn get_firmware_version(&self) -> MountResult<String> {
self.ensure_connected()?;
let response = self.send_command_with_retry(Protocol::get_version())?;
Ok(response.trim_end_matches('#').to_string())
}
fn get_model(&self) -> MountResult<String> {
self.ensure_connected()?;
let response = self.send_command_with_retry(Protocol::get_mount_model())?;
Ok(response.trim_end_matches('#').to_string())
}
fn send_raw_command(&mut self, command: &str) -> MountResult<String> {
self.ensure_connected()?;
self.send_command_with_retry(command)
}
}
impl Drop for SerialMount {
fn drop(&mut self) {
if self.port.borrow().is_some() {
let _ = self.disconnect();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serial_config() {
let config = SerialConfig::new("/dev/ttyUSB0")
.with_baud_rate(115200)
.with_timeout(5000)
.with_retry_count(5);
assert_eq!(config.port, "/dev/ttyUSB0");
assert_eq!(config.baud_rate, 115200);
assert_eq!(config.timeout_ms, 5000);
assert_eq!(config.retry_count, 5);
}
#[test]
fn test_serial_config_default() {
let config = SerialConfig::default();
assert_eq!(config.baud_rate, DEFAULT_BAUD_RATE);
assert_eq!(config.timeout_ms, DEFAULT_TIMEOUT_MS);
}
#[test]
fn test_list_ports() {
let ports = SerialMount::list_ports();
let _ = ports;
}
}