use super::{save_config, App, BackgroundTask, ImportPayload, LoadConfigTaskResult};
use crate::compat;
use crate::config::{Config, CustomRules, Startup};
use crate::custom::CustomRule;
use crate::modbus::ModbusDevice;
use crate::register::RegisterCell;
use crate::state::{DumpParams, ImportParams, Outcome, Popup, State, StatusMessage};
use std::collections::BTreeMap;
use std::fs;
impl App {
pub fn open_dump(&mut self) {
self.read_mut().popup = Some(Popup::Dump(DumpParams::default()));
}
fn parse_import(data: &str) -> Option<ImportPayload> {
let payload: ImportPayload = serde_json::from_str(data.trim()).ok()?;
(payload.total() > 0).then_some(payload)
}
pub fn paste_import(&mut self, data: &str) {
match Self::parse_import(data) {
Some(payload) => {
let params = ImportParams {
pins: payload.pins(),
labels: payload.labels(),
rules: payload.rules(),
};
self.pending_import = Some(payload);
self.read_mut().popup = Some(Popup::Import(params));
}
None => self.set_read_status(StatusMessage::warn(
"Pasted text isn't pinned/labels/custom data",
)),
}
}
pub fn cancel_import(&mut self) {
self.pending_import = None;
self.close_popup();
}
pub fn apply_import(&mut self) {
let Some(payload) = self.pending_import.take() else {
self.close_popup();
return;
};
let mut pins = 0;
if let Some(p) = payload.pinned_registers {
let incoming: Vec<RegisterCell> = p.into();
pins = incoming.len();
for cell in incoming {
if !self.pinned_registers.contains(&cell) {
self.pinned_registers.push(cell);
}
}
self.pinned_registers.sort();
self.pinned_registers.dedup();
}
let mut labels = 0;
if let Some(l) = payload.labels {
let incoming: BTreeMap<RegisterCell, String> = l.into();
labels = incoming.len();
self.labels.extend(incoming);
}
let mut rules = 0;
if let Some(r) = payload.custom_rules {
let incoming: BTreeMap<RegisterCell, CustomRule> = r.into();
rules = incoming.len();
self.custom_rules.extend(incoming);
}
self.refresh_dirty();
self.close_popup();
log::info!("Imported {pins} pin(s), {labels} label(s), {rules} rule(s) from clipboard");
self.set_read_status(StatusMessage::ok(format!(
"Imported {pins} pin(s), {labels} label(s), {rules} rule(s)"
)));
}
fn effective_config(&self) -> Config {
let mut config = self.config.clone();
config.labels = (&self.labels).into();
let rebuilt: CustomRules = (&self.custom_rules).into();
config.custom_rules.holdings = rebuilt.holdings;
config.custom_rules.inputs = rebuilt.inputs;
config.custom_rules.coils = rebuilt.coils;
config.custom_rules.discretes = rebuilt.discretes;
config.pinned_registers = self.pinned_registers.as_slice().into();
config.interpretations = self.interpreter.config();
config
}
fn serialize_config(config: &Config) -> String {
serde_json::to_string(config).unwrap_or_default()
}
pub(super) fn mark_config_saved(&mut self) {
self.saved_config = Self::serialize_config(&self.effective_config());
self.dirty = false;
}
pub(super) fn refresh_dirty(&mut self) {
self.dirty = Self::serialize_config(&self.effective_config()) != self.saved_config;
}
pub(super) fn persist_config(&mut self) -> Outcome {
self.config = self.effective_config();
if let State::Read(p) = &self.state {
self.config.startup = Startup {
address: p.position,
register_type: p.register_type,
panel: p.panel,
};
}
let result = save_config(&self.config_path, &self.config)
.map(|()| format!("Saved to {}", self.config_path))
.map_err(|e| format!("Save failed: {e}"));
if result.is_ok() {
self.mark_config_saved();
}
result
}
pub fn config_path(&self) -> &str {
&self.config_path
}
pub(super) fn start_config_load(&mut self, path: String) -> Result<(), String> {
if path.is_empty() {
return Err("Load failed: enter a file name".to_string());
}
let content = fs::read_to_string(&path).map_err(|e| format!("Load failed: {e}"))?;
let config: Config =
serde_json::from_str(&content).map_err(|e| format!("Load failed: {e}"))?;
let device_config = config.device.clone();
self.background_task = Some(BackgroundTask::LoadConfig(compat::spawn(async move {
let result = ModbusDevice::new(&device_config)
.await
.map_err(|e| e.to_string());
LoadConfigTaskResult {
path,
config: Box::new(config),
result,
}
})));
Ok(())
}
pub(super) fn apply_load_config_result(&mut self, result: Option<LoadConfigTaskResult>) {
let outcome: Outcome = match result {
Some(LoadConfigTaskResult {
path,
config,
result,
}) => match result {
Ok(device) => {
self.apply_config(*config, Some(device));
self.config_path = path.clone();
self.mark_config_saved();
let read = self.startup_read_params();
match &mut self.state {
State::Settings(s) => s.previous = read,
State::Read(p) => *p = read,
_ => {}
}
Ok(format!("Loaded {path}"))
}
Err(e) => Err(format!("Load failed: device: {e}")),
},
None => Err("Load failed: task stopped unexpectedly".to_string()),
};
match &outcome {
Ok(message) => log::info!("{message}"),
Err(error) => log::error!("{error}"),
}
match &self.state {
State::Read(_) => self.set_read_status(outcome.into()),
State::Settings(_) => self.set_settings_status(outcome.into()),
_ => {}
}
}
fn cycle_target(&self) -> Option<String> {
let next = self.config.next_config.trim();
if !next.is_empty() {
Some(next.to_string())
} else if self.config_path != self.origin_config_path {
Some(self.origin_config_path.clone())
} else {
None
}
}
pub fn cycle_config(&mut self) {
let Some(target) = self.cycle_target() else {
self.set_read_status(StatusMessage::info("No next configuration set"));
return;
};
if self.dirty && !self.config.ignore_dirty {
self.read_mut().popup = Some(Popup::CycleConfig);
return;
}
self.load_config_target(target);
}
pub fn confirm_cycle_config(&mut self) {
self.close_popup();
if let Some(target) = self.cycle_target() {
self.load_config_target(target);
}
}
fn load_config_target(&mut self, target: String) {
if !self.free_background_slot() {
self.set_read_status(StatusMessage::info("Device is busy."));
return;
}
match self.start_config_load(target) {
Ok(()) => self.set_read_status(StatusMessage::info("Loading\u{2026}")),
Err(error) => {
log::error!("{error}");
self.set_read_status(StatusMessage::err(error));
}
}
}
pub fn commit_dump(&mut self) {
let result = self.dump_read_log();
if let Some(d) = self.popup_as_mut::<DumpParams>() {
d.result = Some(result);
}
}
}