iec104 0.4.0

A rust implementation of the IEC-60870-5-104 protocol.
Documentation
//! Dispatch incoming command ASDUs to
//! [`super::command_handler::RtuCommandHandler`], apply returned model updates,
//! send activation or deactivation confirmation (COT matches IEC 60870-5-104).

use std::{collections::HashMap, net::SocketAddr, sync::Arc};

use super::{
	command_handler::{CommandContext, RtuCommandHandler},
	model::{PointAddress, PointValue},
	output::spontaneous_asdu,
};
use crate::{
	asdu::Asdu,
	cot::Cot,
	server::{ConnectionId, Server, error::ServerError},
	types_id::TypeId,
};

#[must_use]
const fn is_command_cot(cot: Cot) -> bool {
	matches!(cot, Cot::Request | Cot::Activation | Cot::Deactivation)
}

/// Process commands (`C_SC_*`, `C_DC_*`, `C_RC_*`, `C_SE_*`, `C_BO_*`) with
/// [`Cot::Request`], [`Cot::Activation`], or [`Cot::Deactivation`].
#[must_use]
pub(super) const fn is_process_command(asdu: &Asdu) -> bool {
	is_command_cot(asdu.cot) && is_supported_command_type(asdu.type_id)
}

#[must_use]
pub(super) const fn is_supported_command_type(type_id: TypeId) -> bool {
	matches!(
		type_id,
		TypeId::C_SC_NA_1
			| TypeId::C_DC_NA_1
			| TypeId::C_RC_NA_1
			| TypeId::C_SE_NA_1
			| TypeId::C_SE_NB_1
			| TypeId::C_SE_NC_1
			| TypeId::C_BO_NA_1
			| TypeId::C_SC_TA_1
			| TypeId::C_DC_TA_1
			| TypeId::C_RC_TA_1
			| TypeId::C_SE_TA_1
			| TypeId::C_SE_TB_1
			| TypeId::C_SE_TC_1
			| TypeId::C_BO_TA_1
	)
}

const fn command_confirmation_asdu(
	incoming: &Asdu,
	reply_cot: Cot,
	type_id: TypeId,
	information_objects: crate::types::InformationObjects,
	negative: bool,
) -> Asdu {
	Asdu {
		type_id,
		cot: reply_cot,
		originator_address: incoming.originator_address,
		address_field: incoming.address_field,
		sequence: incoming.sequence,
		test: incoming.test,
		negative,
		information_objects,
	}
}

#[must_use]
const fn command_reply_cot(incoming_cot: Cot) -> Cot {
	match incoming_cot {
		Cot::Deactivation => Cot::DeactivationConfirmation,
		_ => Cot::ActivationConfirmation,
	}
}

pub(super) async fn send_command_confirmation(
	server: &Server,
	connection_id: ConnectionId,
	incoming: &Asdu,
	type_id: TypeId,
	objs: crate::types::InformationObjects,
	negative: bool,
) -> Result<(), ServerError> {
	let reply_cot = command_reply_cot(incoming.cot);
	let reply = command_confirmation_asdu(incoming, reply_cot, type_id, objs, negative);
	server.send_asdu(connection_id, reply).await
}

fn can_apply_update(
	model: &HashMap<PointAddress, PointValue>,
	addr: PointAddress,
	val: &PointValue,
) -> bool {
	model.get(&addr).is_some_and(|ex| ex.type_id() == val.type_id())
}

async fn apply_model_updates(
	model: &mut HashMap<PointAddress, PointValue>,
	server: &Server,
	updates: &[(PointAddress, PointValue)],
) -> Result<(), ServerError> {
	for (addr, val) in updates {
		if !can_apply_update(model, *addr, val) {
			tracing::warn!(
				%addr,
				"command apply_updates: unknown point or type mismatch; skipping this entry"
			);
			continue;
		}
		model.insert(*addr, val.clone());
		server.broadcast_asdu(spontaneous_asdu(*addr, val)).await?;
	}
	Ok(())
}

/// Handle a process command ASDU. Call only when [`is_process_command`] is
/// true.
pub(super) async fn handle_process_command(
	model: &mut HashMap<PointAddress, PointValue>,
	server: &Server,
	asdu: &Asdu,
	connection_id: ConnectionId,
	peer: SocketAddr,
	handler: &Arc<dyn RtuCommandHandler>,
) -> Result<(), ServerError> {
	let handling = {
		let ctx = CommandContext { connection_id, peer, asdu, model: &*model };
		handler.handle_command(ctx).await
	};

	if !handling.negative {
		apply_model_updates(model, server, &handling.apply_updates).await?;
	}

	send_command_confirmation(
		server,
		connection_id,
		asdu,
		handling.reply_type_id,
		handling.reply_information_objects,
		handling.negative,
	)
	.await?;
	Ok(())
}

#[cfg(test)]
mod tests {
	use super::command_reply_cot;
	use crate::cot::Cot;

	#[test]
	fn command_reply_cot_matches_incoming() {
		assert_eq!(command_reply_cot(Cot::Activation), Cot::ActivationConfirmation);
		assert_eq!(command_reply_cot(Cot::Request), Cot::ActivationConfirmation);
		assert_eq!(command_reply_cot(Cot::Deactivation), Cot::DeactivationConfirmation);
	}
}