use super::super::{
ConnectorSnapshot, Duration, HeartbeatTask, Message, MissedTickBehavior,
OcppErrorCode, OcppVersion, Result, Simulator, SimulatorCommand,
SimulatorSnapshot, SinkExt, UiEvent, UiLogLevel, Value, WsWrite,
build_call_error, build_call_result, json,
};
impl Simulator {
pub(in crate::simulator) fn start_heartbeat(&mut self, seconds: u64) {
if seconds == 0 {
self.log(UiLogLevel::Warn, "Heartbeat interval must be positive.");
return;
}
self.stop_heartbeat();
let tx = self.self_cmd_tx.clone();
let handle = tokio::spawn(async move {
let mut ticker = tokio::time::interval(Duration::from_secs(seconds));
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
loop {
ticker.tick().await;
if tx.send(SimulatorCommand::HeartbeatTick).is_err() {
break;
}
}
});
self.heartbeat = Some(HeartbeatTask { seconds, handle });
self.log(
UiLogLevel::Info,
format!("Periodic heartbeat started: every {seconds}s"),
);
self.emit_snapshot();
}
pub(in crate::simulator) fn stop_heartbeat(&mut self) {
if let Some(task) = self.heartbeat.take() {
task.handle.abort();
self.log(UiLogLevel::Info, "Periodic heartbeat stopped.");
self.emit_snapshot();
}
}
pub(in crate::simulator) async fn send_call_result(
&mut self,
write: &mut WsWrite,
message_id: &str,
payload: Value,
) -> Result<()> {
let text = build_call_result(message_id, &payload);
self
.send_text(
write,
text,
UiLogLevel::Tx,
format!("CALLRESULT {message_id}"),
)
.await
}
pub(in crate::simulator) async fn send_call_error(
&mut self,
write: &mut WsWrite,
message_id: &str,
code: &str,
description: &str,
details: Value,
) -> Result<()> {
let text = build_call_error(message_id, code, description, &details);
self
.send_text(
write,
text,
UiLogLevel::Tx,
format!("CALLERROR {message_id} {code}"),
)
.await
}
pub(in crate::simulator) async fn send_formation_violation(
&mut self,
write: &mut WsWrite,
message_id: &str,
description: &str,
) -> Result<()> {
self
.send_call_error(
write,
message_id,
OcppErrorCode::FormationViolation.as_str(),
description,
json!({}),
)
.await
}
pub(in crate::simulator) async fn send_text(
&mut self,
write: &mut WsWrite,
text: String,
level: UiLogLevel,
summary: String,
) -> Result<()> {
write.send(Message::Text(text.clone().into())).await?;
if self.config.trace_frames {
self.log(level, text);
} else {
self.log(level, summary);
}
Ok(())
}
pub(in crate::simulator) fn next_message_id(&mut self) -> String {
let message_id = format!("m{}", self.next_message_id);
self.next_message_id = self.next_message_id.saturating_add(1);
message_id
}
pub(in crate::simulator) fn emit_snapshot(&self) {
let heartbeat_seconds = self.heartbeat.as_ref().map(|task| task.seconds);
let connectors = self
.connectors
.iter()
.map(|(id, state)| ConnectorSnapshot {
id: *id,
status: state.status.display().to_string(),
meter_wh: state.meter_wh,
transaction: state.transaction.as_ref().map(|tx| {
match self.config.protocol {
OcppVersion::V1_6 => format!(
"local={} remote={}",
tx.local_id,
tx.v1_6_transaction_id
.map_or_else(|| "-".to_string(), |value| value.to_string())
),
OcppVersion::V2_0_1 | OcppVersion::V2_1 => {
format!("id={}", tx.transaction_uid)
}
}
}),
})
.collect();
let pending_action = self.pending.as_ref().map(|pending| {
format!("{} ({})", pending.call.action, pending.message_id)
});
let connection_url = self
.connection_url()
.map_or_else(|_| self.config.ws_url.clone(), |u| u.to_string());
let snapshot = SimulatorSnapshot {
cp_id: self.config.cp_id.clone(),
protocol: self.config.protocol.label().to_string(),
connection_url,
connected: self.connected,
heartbeat_seconds,
queue_depth: self.queue.len(),
pending_action,
connectors,
};
let _ = self.ui_tx.send(UiEvent::Snapshot(snapshot));
}
pub(in crate::simulator) fn log<S: Into<String>>(
&self,
level: UiLogLevel,
message: S,
) {
let _ = self.ui_tx.send(UiEvent::Log {
level,
message: message.into(),
});
}
}