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)
}
#[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(())
}
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);
}
}