mtui 0.7.1

An extensive Modbus client (TCP, RTU & mock) for your terminal.
Documentation
#[cfg(not(target_arch = "wasm32"))]
use super::{scan_subnet, subnet_prefix_from, ScanProgress};
use super::{App, ReconnectState};
#[cfg(not(target_arch = "wasm32"))]
use crate::compat::{self, TaskPoll};
use crate::config::Config;
use crate::modbus::{
    DeviceConfig, Interface, InterfaceNetworkParams, InterfaceWiredParams, ModbusDevice,
};
use crate::state::{
    ConnectionStatus, DiscoveryField, DiscoveryParams, InterfaceKind, State, StatusMessage,
};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;

impl App {
    pub fn discovery(&self) -> Option<&DiscoveryParams> {
        match &self.state {
            State::Discovery(d) => Some(d),
            _ => None,
        }
    }

    pub fn discovery_mut(&mut self) -> Option<&mut DiscoveryParams> {
        match &mut self.state {
            State::Discovery(d) => Some(d),
            _ => None,
        }
    }

    pub(super) fn discovery_params(config: &Config) -> DiscoveryParams {
        let device = &config.device;
        let mut d = DiscoveryParams {
            ports: Self::available_ports(),
            slave_id: device.slave_id,
            connect_timeout_ms: device.timeout_connect_ms,
            command_timeout_ms: device.timeout_command_ms,
            between_commands_ms: device.time_between_commands_ms,
            word_order: device.word_order,
            ..Default::default()
        };
        match &device.interface {
            Interface::Wired(w) => {
                d.interface = InterfaceKind::Wired;
                d.baud_rate = w.baud_rate;
                d.data_bits = w.data_bits;
                d.parity = w.parity;
                d.stop_bits = w.stop_bits;
                if let Some(i) = d.ports.iter().position(|p| p == &w.path) {
                    d.port_index = i as u16;
                }
            }
            Interface::Network(n) => {
                d.interface = InterfaceKind::Network;
                d.ip = n.ip.clone();
                d.net_port = n.port;
            }
            Interface::Mock => d.interface = InterfaceKind::Mock,
        }
        d
    }

    pub fn open_discovery(&mut self) {
        self.background_task = None;
        let previous = std::mem::take(self.read_mut());
        let mut params = Self::discovery_params(&self.config);
        params.previous = Some(previous);
        self.state = State::Discovery(params);
    }

    pub fn return_to_read(&mut self) {
        if self.device.is_none() {
            return;
        }
        self.connection = ConnectionStatus::Unknown;
        self.logged_connection = ConnectionStatus::Unknown;
        self.reconnect = ReconnectState::default();
        self.restore_previous_read();
    }

    fn restore_previous_read(&mut self) {
        let previous = match &mut self.state {
            State::Discovery(d) => d.previous.take(),
            _ => None,
        };
        let mut read = previous.unwrap_or_else(|| self.startup_read_params());
        read.loading = false;
        read.popup = None;
        self.state = State::Read(read);
    }

    pub async fn discovery_connect(&mut self) {
        let device_config = {
            let Some(d) = self.discovery() else {
                return;
            };
            let interface = match d.interface {
                InterfaceKind::Mock => Interface::Mock,
                InterfaceKind::Wired => Interface::Wired(InterfaceWiredParams {
                    path: d
                        .ports
                        .get(d.port_index as usize)
                        .cloned()
                        .unwrap_or_default(),
                    baud_rate: d.baud_rate,
                    data_bits: d.data_bits,
                    parity: d.parity,
                    stop_bits: d.stop_bits,
                }),
                InterfaceKind::Network => Interface::Network(InterfaceNetworkParams {
                    ip: d.ip.clone(),
                    port: d.net_port,
                }),
            };
            DeviceConfig {
                interface,
                slave_id: d.slave_id,
                timeout_connect_ms: d.connect_timeout_ms,
                timeout_command_ms: d.command_timeout_ms,
                time_between_commands_ms: d.between_commands_ms,
                word_order: d.word_order,
            }
        };

        self.set_discovery_status(StatusMessage::warn("Connecting\u{2026}"));

        match ModbusDevice::new(&device_config).await {
            Ok(device) => {
                self.device = Some(device);
                self.sync_api_device();
                self.refresh_writes_log_state();
                self.interpreter.set_word_order(device_config.word_order);
                self.config.device = device_config;
                self.dirty = true;
                self.clear_read_accumulation();
                self.connection = ConnectionStatus::Unknown;
                self.logged_connection = ConnectionStatus::Unknown;
                self.reconnect = ReconnectState::default();
                let device = self.config.display_device();
                log::info!("Switched device \u{b7} {device}");
                self.restore_previous_read();
            }
            Err(e) => {
                log::error!("Connect failed \u{b7} {e}");
                self.set_discovery_status(StatusMessage::err(format!("Connection failed: {e}")));
            }
        }
    }

    pub fn scan_progress(&self) -> Option<(usize, usize)> {
        self.network_scan
            .as_ref()
            .map(|s| (s.done.load(Ordering::Relaxed), s.total))
    }

    pub fn use_found_ip(&mut self, index: u16) {
        if let Some(d) = self.discovery_mut() {
            let Some(ip) = d.found.get(index as usize).cloned() else {
                return;
            };
            d.ip = ip;
            d.scan_open = false;
            if let Some(pos) = d
                .fields()
                .iter()
                .position(|f| *f == DiscoveryField::Connect)
            {
                d.selected = pos as u16;
            }
        }
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub fn start_network_scan(&mut self) {
        if self.network_scan.is_some() {
            return;
        }
        let Some(d) = self.discovery() else {
            return;
        };
        if d.interface != InterfaceKind::Network {
            return;
        }
        let Some(prefix) = subnet_prefix_from(&d.ip).or_else(crate::state::local_subnet_prefix)
        else {
            self.set_discovery_status(StatusMessage::err("Couldn't determine a subnet to scan"));
            return;
        };
        let port = d.net_port;
        let per_host = Duration::from_millis(d.connect_timeout_ms.clamp(100, 2_000));
        let total = 254;
        let done = Arc::new(AtomicUsize::new(0));
        self.network_scan = Some(ScanProgress {
            done: done.clone(),
            total,
        });
        if let Some(d) = self.discovery_mut() {
            d.found.clear();
            d.scan_open = true;
            d.scan_selected = 0;
            d.status = Some(StatusMessage::warn(format!(
                "Scanning {prefix}0/24\u{2026}"
            )));
        }
        self.network_scan_task = Some(compat::spawn(scan_subnet(prefix, port, per_host, done)));
    }

    #[cfg(target_arch = "wasm32")]
    pub fn start_network_scan(&mut self) {
        self.set_discovery_status(StatusMessage::warn(
            "Network scan isn't available in the web demo",
        ));
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub(super) fn poll_network_scan(&mut self) {
        let Some(handle) = self.network_scan_task.as_mut() else {
            return;
        };
        match handle.poll_result() {
            TaskPoll::Pending => {}
            TaskPoll::Finished(found) => self.finish_network_scan(found),
            TaskPoll::Gone => self.finish_network_scan(Vec::new()),
        }
    }

    #[cfg(not(target_arch = "wasm32"))]
    fn finish_network_scan(&mut self, found: Vec<String>) {
        self.network_scan_task = None;
        self.network_scan = None;
        let count = found.len();
        if let Some(d) = self.discovery_mut() {
            d.found = found;
            d.status = Some(if count == 0 {
                StatusMessage::warn("No devices found on this subnet")
            } else {
                StatusMessage::ok(format!("Found {count} device(s)"))
            });
        }
    }

    fn set_discovery_status(&mut self, msg: StatusMessage) {
        if let Some(d) = self.discovery_mut() {
            d.status = Some(msg);
        }
    }
}