### Install ocppsim with Homebrew Source: https://docs.rs/ocppsim/latest/index.html Install ocppsim using Homebrew from the zhongruoyu/tap tap. ```bash brew install zhongruoyu/tap/ocppsim ``` -------------------------------- ### Install ocppsim with Cargo Source: https://docs.rs/ocppsim/latest/index.html Install the ocppsim crate using Cargo. ```bash cargo install ocppsim ``` -------------------------------- ### Install certificate from payload Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/security.rs.html Parses a certificate installation request from a JSON payload and validates the certificate length. ```rust pub(in crate::simulator) fn install_certificate_from_payload( &mut self, payload: &Value, ) -> Result { let certificate_type = required_string_field(payload, "certificateType")?; let certificate = required_string_field(payload, "certificate")?; if certificate.len() > MAX_INSTALL_CERTIFICATE_LENGTH { return Ok(ResponseStatus::Rejected); } Ok(self.install_certificate(certificate_type, certificate)) } ``` -------------------------------- ### Certificate Signing and Installation Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/security.rs.html Functions to process signed certificates, validate chain sizes, and install certificates into the security store. ```rust fn certificate_signed( &mut self, payload: &Value, certificate_type: &str, invalid_event_type: &str, ) -> Result { let certificate_chain = required_string_field(payload, "certificateChain")?; if certificate_chain.len() > self.security.certificate_signed_max_chain_size || is_simulated_invalid(certificate_chain) { self.record_security_event( invalid_event_type, Some("CertificateSigned certificate chain rejected".to_string()), ); return Ok(ResponseStatus::Rejected); } Ok(self.install_certificate(certificate_type, certificate_chain)) } fn install_certificate( &mut self, certificate_type: &str, certificate: &str, ) -> ResponseStatus { if certificate.trim().is_empty() || is_simulated_invalid(certificate) { return ResponseStatus::Rejected; } let hash = synthetic_certificate_hash(certificate_type, certificate); if self .security .certificates .iter() .any(|item| item.hash == hash) { return ResponseStatus::Accepted; } if self.security.additional_root_certificate_check && is_central_system_root_certificate_type(certificate_type) { return self.install_central_system_root_with_fallback( certificate_type, certificate, hash, ); } if self.security.certificates.len() >= self.security.certificate_store_max_length { return ResponseStatus::Rejected; } self.security.certificates.push(InstalledCertificate { certificate_type: certificate_type.to_string(), hash, }); ResponseStatus::Accepted } ``` -------------------------------- ### Test setup for log file appending Source: https://docs.rs/ocppsim/latest/src/ocppsim/app.rs.html Test module setup for verifying UI log entries are correctly appended to a file sink. ```rust #[cfg(test)] mod tests { use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crate::ocpp::OcppVersion; use crate::simulator::{SimulatorRuntimeState, UiEvent}; use super::{InputAction, TerminalApp, visible_input_view}; static TEMP_LOG_COUNTER: AtomicU64 = AtomicU64::new(0); #[test] /// Verifies UI log entries are appended to configured log file sink. fn appends_logs_to_file() { let path = temp_log_path(); ``` -------------------------------- ### Parse Start Command Source: https://docs.rs/ocppsim/latest/src/ocppsim/commands.rs.html Parses the start command, requiring a connector ID and an idToken. ```rust fn parse_start_command(parts: &[&str]) -> Result { ensure_exact(parts, 3, USAGE_START)?; let connector = parse_u16(parts.get(1), USAGE_START)?; let id_token = parse_required(parts.get(2), USAGE_START)?; Ok(UserCommand::Start { connector, id_token: id_token.to_string(), }) } ``` -------------------------------- ### Use a named profile from a TOML config file Source: https://docs.rs/ocppsim/latest/index.html Starts ocppsim using a named profile from a TOML config file in ~/.config/ocppsim/ocppsim.toml or a custom path. ```bash ocppsim some-profile ``` ```bash ocppsim some-profile --config-path ./ocppsim.toml ``` -------------------------------- ### Start ocppsim directly against a CSMS endpoint Source: https://docs.rs/ocppsim/latest/index.html Starts ocppsim with a WebSocket URL and charge point ID. OCPP 2.x charge point IDs must be at most 48 characters and cannot contain ':'. ```bash ocppsim --ws-url ws://csms.example.com/ocpp --cp-id CP-001 ``` -------------------------------- ### enqueue_remote_start_authorize_v1_6 Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/workflow/enqueue.rs.html Enqueues a remote start authorization request for OCPP 1.6, validating the ID token and charging profile before queuing the call. ```rust pub(in crate::simulator) fn enqueue_remote_start_authorize_v1_6( &mut self, connector: u16, id_token: String, charging_profile: Option, ) -> Result<()> { validate_outbound_id_token(self.config.protocol, &id_token) .map_err(anyhow::Error::msg)?; Self::validate_remote_start_charging_profile(charging_profile.as_ref())?; let payload = Self::authorize_v1_6_payload(&id_token); self.try_enqueue_call( OutgoingAction::Authorize.as_str(), payload, PendingContext::RemoteStartAuthorizeV1_6 { connector, id_token, charging_profile, }, ) } ``` -------------------------------- ### Certificate Querying Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/security.rs.html Methods to retrieve installed certificates by type or a list of types. ```rust fn certificates_of_type( &self, certificate_type: &str, ) -> Vec<&InstalledCertificate> { self .security .certificates .iter() .filter(|item| item.certificate_type == certificate_type) .collect() } fn certificates_matching_types( &self, certificate_types: Option<&[&str]>, ) -> Vec<&InstalledCertificate> { self .security .certificates .iter() .filter(|item| { certificate_types.is_none_or(|types| { types.iter().any(|value| *value == item.certificate_type) }) }) .collect() } ``` -------------------------------- ### Initialize Simulator Runtime Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/mod.rs.html Configures logging and heartbeat settings during simulator startup. ```rust fn initialize_simulator_runtime(simulator: &mut Simulator) { simulator.log( UiLogLevel::Info, "OCPP framing follows CALL/CALLRESULT/CALLERROR arrays from OCPP-J.", ); simulator.log( UiLogLevel::Info, format!( "Configured WebSocket subprotocol: {}", simulator.config.protocol.subprotocol() ), ); if simulator.config.strict { simulator.log( UiLogLevel::Info, "Strict inbound schema validation is enabled.", ); } simulator.emit_snapshot(); if let Some(seconds) = simulator.config.heartbeat_seconds { simulator.start_heartbeat(seconds); } } ``` -------------------------------- ### Resolve CLI Arguments with Configuration File Source: https://docs.rs/ocppsim/latest/src/ocppsim/args.rs.html Demonstrates resolving CLI arguments against a configuration file and verifying the resulting state. ```rust 1082heartbeat-seconds = 12 1083outbound-queue-limit = 12 1084security-event-limit = 34 1085"#, 1086 ); 1087 1088 let args = CliArgs { 1089 profile: Some("demo".to_string()), 1090 config_path: Some(path.clone()), 1091 protocol: Some(super::ProtocolArg::V2_1), 1092 vendor: Some("cli-vendor".to_string()), 1093 model: Some("cli-model".to_string()), 1094 firmware: Some("cli-fw".to_string()), 1095 log_path: Some(PathBuf::from("./cli.log")), 1096 trace_frames: true, 1097 strict: true, 1098 request_timeout_seconds: Some(99), 1099 heartbeat_seconds: Some(0), 1100 outbound_queue_limit: Some(10), 1101 security_event_limit: Some(20), 1102 ..base_args() 1103 }; 1104 1105 let resolved = args.resolve().expect("profile should resolve"); 1106 assert_eq!(resolved.protocol, OcppVersion::V2_1); 1107 assert_eq!(resolved.vendor, "cli-vendor"); 1108 assert_eq!(resolved.model, "cli-model"); 1109 assert_eq!(resolved.firmware, "cli-fw"); 1110 assert_eq!(resolved.log_path, Some(PathBuf::from("./cli.log"))); 1111 assert!(resolved.trace_frames); 1112 assert!(resolved.strict); 1113 assert_eq!(resolved.request_timeout_seconds, 99); 1114 assert_eq!(resolved.heartbeat_seconds, None); 1115 assert_eq!(resolved.outbound_queue_limit, 10); 1116 assert_eq!(resolved.security_event_limit, 20); 1117 1118 let _ = fs::remove_file(path); 1119 } ``` -------------------------------- ### Get terminal width Source: https://docs.rs/ocppsim/latest/src/ocppsim/app.rs.html Retrieves the current terminal width, defaulting to 80 if unavailable. ```rust fn terminal_width() -> usize { terminal::size() .map_or(80, |(width, _)| width as usize) .max(PROMPT_PREFIX_WIDTH) } ``` -------------------------------- ### Helper functions: dispatch, next_command, command_test_context, base_cli_args Source: https://docs.rs/ocppsim/latest/src/ocppsim/lib.rs.html Utility functions used by the tests: dispatch parses and handles a command, next_command receives a simulator command, command_test_context creates a test context with OCPP V1.6, and base_cli_args returns a CliArgs with all fields set to defaults. ```rust fn dispatch( input: &str, cli_args: &CliArgs, cmd_tx: &mpsc::UnboundedSender, app: &mut TerminalApp, ) -> bool { let command = parse_command(input).expect("valid command"); handle_user_command(command, cli_args, cmd_tx, app) } ``` ```rust fn next_command( cmd_rx: &mut mpsc::UnboundedReceiver, ) -> SimulatorCommand { cmd_rx.try_recv().expect("simulator command") } ``` ```rust fn command_test_context() -> ( CliArgs, mpsc::UnboundedSender, mpsc::UnboundedReceiver, TerminalApp, ) { let protocol = ocpp::OcppVersion::V1_6; let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); let app = TerminalApp::new(protocol); (base_cli_args(), cmd_tx, cmd_rx, app) } ``` ```rust fn base_cli_args() -> CliArgs { CliArgs { profile: None, config_path: None, ws_url: None, cp_id: None, no_append_cp_id: false, connectors: None, protocol: None, vendor: None, model: None, firmware: None, log_path: None, trace_frames: false, strict: false, request_timeout_seconds: None, heartbeat_seconds: None, outbound_queue_limit: None, security_event_limit: None, security_profile: None, basic_auth_password: None, ca_cert: None, client_cert: None, client_key: None, } } ``` -------------------------------- ### Parse Heartbeat Command Source: https://docs.rs/ocppsim/latest/src/ocppsim/commands.rs.html Parses heartbeat commands, supporting simple heartbeat, starting with an interval, or stopping. ```rust fn parse_heartbeat_command(parts: &[&str]) -> Result { if parts.len() == 1 { return Ok(UserCommand::Heartbeat); } if parts.len() == 3 && parts[1].eq_ignore_ascii_case("start") { let seconds = parse_u64(parts.get(2), USAGE_HEARTBEAT_START)?; if seconds == 0 { return Err("Heartbeat interval must be positive.".to_string()); } return Ok(UserCommand::HeartbeatStart { seconds }); } if parts.len() == 2 && parts[1].eq_ignore_ascii_case("stop") { return Ok(UserCommand::HeartbeatStop); } Err(USAGE_HEARTBEAT_ALL.to_string()) } ``` -------------------------------- ### TerminalApp initialization and configuration Source: https://docs.rs/ocppsim/latest/src/ocppsim/app.rs.html Methods for creating a new terminal application state and configuring connection targets. ```rust impl TerminalApp { /// Creates a fresh terminal app state for the selected protocol. pub fn new(protocol: OcppVersion) -> Self { Self { protocol, profile_name: None, ws_url: String::new(), connected: false, pending_logs: VecDeque::new(), input: String::new(), cursor: 0, history: CommandHistory::new(), known_connectors: Vec::new(), profile_completions: Vec::new(), completion: None, log_sink: None, screen_clear_requested: false, runtime_state: SimulatorRuntimeState::default(), exit_confirmation: None, } } /// Sets the connection target shown in the inline taskbar. pub fn set_connection_target( &mut self, profile_name: Option, ws_url: Option, ) { self.profile_name = profile_name; self.ws_url = ws_url .map(|url| redact_url_secrets(&url)) .unwrap_or_default(); } /// Returns the active protocol for command help and completions. pub fn protocol(&self) -> OcppVersion { self.protocol } /// Sets profile names offered for `connect ` completion. pub fn set_profile_completions(&mut self, profiles: Vec) { self.profile_completions = profiles; } } ``` -------------------------------- ### Run ocppsim for local simulation Source: https://docs.rs/ocppsim/latest/index.html Starts ocppsim without a CSMS connection target for local simulation. ```bash ocppsim ``` -------------------------------- ### Apply heartbeat configuration Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/mod.rs.html Updates heartbeat interval and starts or stops the heartbeat timer accordingly. Returns early if the value is unchanged. ```rust fn apply_heartbeat_config(&mut self, heartbeat_seconds: Option) { if self.config.heartbeat_seconds == heartbeat_seconds { return; } self.config.heartbeat_seconds = heartbeat_seconds; if let Some(seconds) = heartbeat_seconds { self.start_heartbeat(seconds); } else { self.stop_heartbeat(); } } ``` -------------------------------- ### TerminalSession::render_prompt() method Source: https://docs.rs/ocppsim/latest/src/ocppsim/app.rs.html Renders the inline command composer and taskbar block. Computes input width, handles prompt shadow, and queues the prompt display. ```rust /// Renders the inline command composer and taskbar block. fn render_prompt( &mut self, app: &TerminalApp, width: usize, ) -> io::Result<()> { let content_width = width.saturating_sub(1); let input_width = content_width.saturating_sub(PROMPT_PREFIX_WIDTH); let prompt_shadow = app.prompt_shadow(); let (view, show_placeholder) = if let Some(shadow) = prompt_shadow { (visible_input_view(shadow, shadow.len(), input_width), false) } else { ( visible_input_view(app.input(), app.cursor(), input_width), app.input().is_empty(), ) }; queue!( self.stdout, MoveToColumn(0), Clear(ClearType::CurrentLine), Print("\r\n"), MoveToColumn(0), Clear(ClearType::CurrentLine), SetAttribute(Attribute::Bold), Print(">"), SetAttribute(Attribute::Reset), ``` -------------------------------- ### Path and configuration utility functions Source: https://docs.rs/ocppsim/latest/src/ocppsim/args.rs.html Utilities for normalizing heartbeat intervals and expanding tilde-based file paths. ```rust fn normalize_heartbeat_seconds(value: Option) -> Option { value.and_then(|seconds| if seconds == 0 { None } else { Some(seconds) }) } fn expand_tilde_path(path: &Path) -> PathBuf { let raw = path.to_string_lossy(); if (raw == "~" || raw.starts_with("~/")) && let Some(home) = env::var_os("HOME") { let mut expanded = PathBuf::from(home); if raw.len() > 2 { expanded.push(raw.trim_start_matches("~/")); } return expanded; } path.to_path_buf() } fn default_config_path() -> PathBuf { ``` -------------------------------- ### Central System Root Certificate Fallback Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/security.rs.html Handles the installation of central system root certificates with a fallback mechanism to maintain store limits. ```rust fn install_central_system_root_with_fallback( &mut self, certificate_type: &str, certificate: &str, hash: CertificateHashData, ) -> ResponseStatus { if is_simulated_invalid(certificate) { return ResponseStatus::Rejected; } let fallback = self .security .certificates .iter() .rev() .find(|item| { is_central_system_root_certificate_type(&item.certificate_type) }) .cloned(); self.security.certificates.retain(|item| { !is_central_system_root_certificate_type(&item.certificate_type) }); if let Some(fallback) = fallback { self.security.certificates.push(fallback); } if self.security.certificates.len() >= self.security.certificate_store_max_length { return ResponseStatus::Rejected; } self.security.certificates.push(InstalledCertificate { certificate_type: certificate_type.to_string(), hash, }); ResponseStatus::Accepted } ``` -------------------------------- ### Initialize Simulator state Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/mod.rs.html Builds an initialized simulator instance with default configuration entries and connector states. ```rust impl Simulator { /// Builds an initialized simulator state with default connector entries. fn new( config: SimulatorConfig, ui_tx: UnboundedSender, self_cmd_tx: UnboundedSender, ) -> Self { let configuration = default_configuration_entries(&config); let mut connectors = BTreeMap::new(); for connector in 1..=config.connectors { connectors.insert( connector, ConnectorState { status: ConnectorStatus::Available, meter_wh: 0, offered_limit: None, scheduled_availability: None, transaction: None, }, ); } let security = SecurityState::new(&config); Self { config, ui_tx, self_cmd_tx, incoming_request_validators: BTreeMap::new(), incoming_response_validators: BTreeMap::new(), incoming_message_ids: BTreeSet::new(), connectors, configuration, reservations: BTreeMap::new(), charging_profiles: BTreeMap::new(), security, local_auth_list_version: 0, queue: VecDeque::new(), pending: None, next_message_id: 1, next_tx_id: 1, last_boot_notification_payload: None, heartbeat: None, connected: false, boot_registration_status: BootRegistrationStatus::Accepted, boot_retry_after: None, } } ``` -------------------------------- ### Handle missing configuration file Source: https://docs.rs/ocppsim/latest/src/ocppsim/args.rs.html Ensures that providing a path to a non-existent configuration file results in an error. ```rust #[test] /// Verifies that a nonexistent config file path produces an error. fn missing_config_file_produces_error() { let args = CliArgs { profile: Some("demo".to_string()), config_path: Some(PathBuf::from("/nonexistent/path/ocppsim.toml")), ..base_args() }; let error = args.resolve().expect_err("should fail"); assert!( error.to_string().contains("Failed to read"), "unexpected error: {error}" ); } ``` -------------------------------- ### StartTransactionV1_6Request struct Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/payloads.rs.html Request payload for OCPP 1.6 StartTransaction message. Includes connector ID, id tag, meter start, and timestamp. ```rust #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub(super) struct StartTransactionV1_6Request<'a> { pub connector_id: u16, pub id_tag: &'a str, pub meter_start: i64, pub timestamp: &'a str, } ``` -------------------------------- ### Get Profile Completion Names Source: https://docs.rs/ocppsim/latest/src/ocppsim/args.rs.html Retrieves a list of profile names for interactive command completion, optionally using a provided configuration path. ```rust pub fn profile_completion_names(config_path: Option<&Path>) -> Vec { let path = config_path.map_or_else(default_config_path, expand_tilde_path); profile_names(&path).unwrap_or_default() } ``` -------------------------------- ### ocppsim lib.rs source code Source: https://docs.rs/ocppsim/latest/src/ocppsim/lib.rs.html The complete source code of the lib.rs module, including the `run()` function, `install_rustls_provider()`, and `install_panic_hook()`. It sets up the simulator, terminal UI, and command handling. ```rust 1#![doc = include_str!("../README.md")] 2 3mod app; 4mod args; 5mod commands; 6mod config; 7mod embedded_schemas; 8mod ocpp; 9mod sensitive; 10mod simulator; 11mod version; 12 13use std::backtrace::Backtrace; 14use std::panic; 15use std::process; 16use std::thread; 17 18use anyhow::Result; 19use app::{InputAction, TerminalApp, TerminalSession, restore_console}; 20use args::{Cli, CliArgs, CliCommand, ResolvedCliArgs}; 21use clap::Parser; 22use commands::{ 23 ConnectTarget, UserCommand, help_text, parse_command, 24 redact_sensitive_command, standards_text, 25}; 26use sensitive::redact_text_secrets; 27use simulator::{ 28 SimulatorCommand, SimulatorConfig, SimulatorConnectionConfig, run_simulator, 29}; 30use tokio::sync::mpsc; 31use version::version_string; 32 33/// Runs the interactive OCPP simulator command-line application. 34/// 35/// # Errors 36/// /// Returns an error if the terminal cannot be initialized, the WebSocket /// connection fails, or a critical runtime error occurs. 39pub async fn run() -> Result<()> { 40 install_panic_hook(); 41 args::complete_from_env(); 42 install_rustls_provider(); 43 44 let cli = Cli::parse(); 45 if let Some(CliCommand::Completions { shell }) = cli.command { 46 args::write_completion_script(shell, &mut std::io::stdout())?; 47 return Ok(()); 48 } 49 50 let cli_args = cli.args; 51 let resolved = cli_args.clone().resolve()?; 52 let protocol = resolved.protocol; 53 54 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::(); 55 let (ui_tx, mut ui_rx) = mpsc::unbounded_channel(); 56 57 let config = SimulatorConfig::from_resolved(&resolved); 58 let simulator_task = 59 tokio::spawn(run_simulator(config, cmd_rx, ui_tx, cmd_tx.clone())); 60 61 let mut terminal = TerminalSession::new()?; 62 let mut app = TerminalApp::new(protocol); 63 app.set_profile_completions(args::profile_completion_names( 64 resolved.config_path.as_deref(), 65 )); 66 // Seed taskbar metadata before the simulator emits its first snapshot. 67 app.set_connection_target(resolved.profile.clone(), resolved.ws_url.clone()); 68 if let Some(path) = resolved.log_path.as_ref() { 69 app.enable_log_path(path)?; 70 app.push_info(format!("Appending logs to {}", path.display())); 71 } 72 73 log_profile_source(&mut app, &resolved); 74 app.push_info(format!( 75 "Simulator ready: cp-id={} protocol={} connectors={}", 76 display_optional(resolved.cp_id.as_deref()), 77 protocol.label(), 78 resolved.connectors 79 )); 80 app.push_info("Type `help` for commands."); 81 app.push_info( 82 "Type `connect [ | ]` \ 83 to open a CSMS connection.", 84 ); 85 86 let mut should_exit = false; 87 while !should_exit { 88 while let Ok(event) = ui_rx.try_recv() { 89 app.apply(event); 90 } 91 92 terminal.draw(&mut app)?; 93 match TerminalSession::poll_input(&mut app)? { 94 InputAction::None => {} 95 InputAction::ExitRequested => { 96 should_exit = app.request_exit(); 97 } 98 InputAction::Submitted(line) => { 99 if line.is_empty() { 100 continue; 101 } 102 app.push_user_input(&redact_sensitive_command(&line)); 103 104 match parse_command(&line) { 105 Ok(command) => { 106 should_exit = 107 handle_user_command(command, &cli_args, &cmd_tx, &mut app); 108 } 109 Err(message) => { 110 app.push_error(message); 111 } 112 } 113 } 114 } 115 } 116 117 terminal.draw(&mut app)?; 118 let _ = cmd_tx.send(SimulatorCommand::Shutdown); 119 let _ = simulator_task.await; 120 Ok(()) 121} 122 123/// Installs the `ring` rustls crypto provider for TLS WebSocket connections. 124fn install_rustls_provider() { 125 let _ = rustls::crypto::ring::default_provider().install_default(); 126} 127 128/// Installs a process-wide panic hook that restores the terminal and prints 129/// a structured panic report with a captured backtrace. 130fn install_panic_hook() { 131 panic::set_hook(Box::new(|info| { 132 restore_console(); 133 134 let current_thread = thread::current(); 135 let thread_name = current_thread.name().unwrap_or("").to_string(); 136 let location = info.location().map_or_else( 137 || "".to_string(), 138 |loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()), 139 ); 140 let message = redact_text_secrets(&panic_message(info)); 141 let backtrace = 142 redact_text_secrets(&Backtrace::force_capture().to_string()); 143 144 eprintln!(); 145 eprintln!("=================================================="); 146 eprintln!("ocppsim panic"); 147 eprintln!("thread: {thread_name}"); 148 eprintln!("location: {location}"); 149 eprintln!("message: {message}"); 150 eprintln!(); 151} ``` -------------------------------- ### Build taskbar line Source: https://docs.rs/ocppsim/latest/src/ocppsim/app.rs.html Generates the compact taskbar text string based on the current profile or URL and connection status. ```rust fn taskbar_line(&self) -> String { let target = self.profile_name.as_ref().map_or_else( || format!("url {}", display_taskbar_value(&self.ws_url)), |name| format!("profile {}", display_taskbar_value(name)), ); let status = if self.connected { "connected" } else { "disconnected" }; format!(" {target} | {status}") } ``` -------------------------------- ### Record Secure Connection Failure Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/security.rs.html Records a security event if a secure connection setup fails, provided the security profile is set to 2 or 3. ```rust pub(in crate::simulator) fn record_secure_connection_failure( &mut self, error: &WsError, ) { let Some(event_type) = self.secure_connection_failure_event_type(error) else { return; }; self.record_security_event( event_type, Some(format!("Secure connection setup failed: {error}")), ); } ``` -------------------------------- ### Test connection commands dispatch to simulator commands Source: https://docs.rs/ocppsim/latest/src/ocppsim/lib.rs.html Test that connection-oriented commands (connect, status, boot, disconnect) dispatch the expected SimulatorCommand variants. Uses command_test_context and next_command helpers. ```rust #[cfg(test)] mod tests { use tokio::sync::mpsc; use super::*; #[test] /// Verifies connection-oriented commands dispatch the expected variants. fn parsed_connection_commands_dispatch_to_simulator_commands() { let (cli_args, cmd_tx, mut cmd_rx, mut app) = command_test_context(); assert!(!dispatch("connect", &cli_args, &cmd_tx, &mut app)); match next_command(&mut cmd_rx) { SimulatorCommand::Connect { config } => assert!(config.is_none()), other => panic!("unexpected command: {other:?}"), } assert!(!dispatch( "connect ws://localhost:9000/ocpp CP-TEST", &cli_args, &cmd_tx, &mut app, )); match next_command(&mut cmd_rx) { SimulatorCommand::Connect { config } => { let config = config.expect("direct connect config"); assert_eq!(config.ws_url, "ws://localhost:9000/ocpp"); assert_eq!(config.cp_id, "CP-TEST"); } other => panic!("unexpected command: {other:?}"), } assert!(!dispatch("status", &cli_args, &cmd_tx, &mut app)); assert!(matches!( next_command(&mut cmd_rx), SimulatorCommand::Status )); assert!(!dispatch("boot", &cli_args, &cmd_tx, &mut app)); assert!(matches!(next_command(&mut cmd_rx), SimulatorCommand::Boot)); assert!(!dispatch("disconnect", &cli_args, &cmd_tx, &mut app)); assert!(matches!( next_command(&mut cmd_rx), SimulatorCommand::Disconnect )); } ``` -------------------------------- ### Test help text content Source: https://docs.rs/ocppsim/latest/src/ocppsim/commands.rs.html Verifies that the help text contains essential usage guidance for simulator commands. ```rust #[test] /// Verifies help text includes core command usage guidance. fn help_text_is_descriptive() { let help = help_text(); assert!(help.contains("Show current simulator snapshot")); assert!(help.contains("connect [ | ]")); assert!(help.contains("connector-status ")); assert!(help.contains("Valid statuses:")); assert!(help.contains("exit | quit")); } ``` -------------------------------- ### Handling RequestStartTransaction Call Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/incoming/dispatch_v2_x.rs.html Processes the RequestStartTransaction call by validating the payload, checking connector availability, and managing transaction state or remote start charging profiles. ```rust async fn handle_request_start_transaction_call_v2_x( &mut self, write: &mut impl WsMessageSink, message_id: &str, payload: &Value, ) -> Result<()> { let request = match RequestStartTransactionRequest_V2_X::parse(payload) { Ok(value) => value, Err(error) => { return self .send_format_violation(write, message_id, &error.to_string()) .await; } }; if !self.post_boot_ocpp_requests_allowed() { return self .send_status_response(write, message_id, ResponseStatus::Rejected) .await; } let Some(connector) = self .resolve_start_connector_or_reject(write, message_id, request.connector) .await? else { return Ok(()); }; if let Some(existing) = self.active_transaction_uid(connector) { let status = if self.active_transaction_authorized(connector) { ResponseStatus::Rejected } else if let Err(error) = self.apply_remote_start_charging_profile( connector, request.charging_profile.as_ref(), ) { self.log( UiLogLevel::Warn, format!( "RequestStartTransaction charging profile rejected on EVSE \ {connector}: {error}" ), ); ResponseStatus::Rejected } else { ResponseStatus::Accepted }; let transaction_id = (status == ResponseStatus::Accepted).then_some(existing.as_str()); return self .send_call_result( write, message_id, to_value(&RequestStartTransactionResponse { status: status.as_str(), transaction_id, }), ) .await; } let RequestStartTransactionRequest_V2_X { connector: _, remote_start_id, id_token, charging_profile, } = request; let status = if let Err(error) = Self::validate_remote_start_charging_profile(charging_profile.as_ref()) { self.log( UiLogLevel::Warn, format!( "RequestStartTransaction charging profile rejected on EVSE \ {connector}: {error}" ), ); ResponseStatus::Rejected } else if self .start_transaction(connector, id_token, true, Some(remote_start_id), true) .and_then(|()| { self.apply_remote_start_charging_profile( connector, charging_profile.as_ref(), ) }) .is_ok() { ResponseStatus::Accepted } else { ResponseStatus::Rejected }; self.send_status_response(write, message_id, status).await } ``` -------------------------------- ### Test transaction commands dispatch to simulator commands Source: https://docs.rs/ocppsim/latest/src/ocppsim/lib.rs.html Test that transaction-oriented commands (authorize, data-transfer, start, stop) dispatch the expected SimulatorCommand variants and preserve parsed fields. ```rust #[test] /// Verifies transaction-oriented commands retain their parsed fields. fn parsed_transaction_commands_dispatch_to_simulator_commands() { let (cli_args, cmd_tx, mut cmd_rx, mut app) = command_test_context(); assert!(!dispatch("authorize TOKEN", &cli_args, &cmd_tx, &mut app)); match next_command(&mut cmd_rx) { SimulatorCommand::Authorize { id_token } => assert_eq!(id_token, "TOKEN"), other => panic!("unexpected command: {other:?}"), } assert!(!dispatch( "data-transfer vendor message hello world", &cli_args, &cmd_tx, &mut app, )); match next_command(&mut cmd_rx) { SimulatorCommand::DataTransfer { vendor_id, message_id, data, } => { assert_eq!(vendor_id, "vendor"); assert_eq!(message_id.as_deref(), Some("message")); assert_eq!(data.as_deref(), Some("hello world")); } other => panic!("unexpected command: {other:?}"), } assert!(!dispatch("start 2 ID", &cli_args, &cmd_tx, &mut app)); match next_command(&mut cmd_rx) { SimulatorCommand::StartTransaction { connector, id_token, } => { assert_eq!(connector, 2); assert_eq!(id_token, "ID"); } other => panic!("unexpected command: {other:?}"), } assert!(!dispatch( "stop 2 EVDisconnected", &cli_args, &cmd_tx, &mut app, )); match next_command(&mut cmd_rx) { SimulatorCommand::StopTransaction { connector, reason } => { assert_eq!(connector, 2); assert_eq!(reason.as_deref(), Some("EVDisconnected")); } other => panic!("unexpected command: {other:?}"), } } ``` -------------------------------- ### Firmware and Certificate Response Handling Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/security.rs.html Methods for enqueuing firmware status notifications and generating signed update firmware responses. ```rust self.enqueue_firmware_status_notification( ResponseStatus::Installed.as_str(), Some(request_id), ); self.record_security_event("FirmwareUpdated", None); Ok(ResponseStatus::Accepted) } pub(in crate::simulator) fn signed_update_firmware_response( status: ResponseStatus, ) -> Value { to_value(&StatusResponse { status: status.as_str(), }) } ``` -------------------------------- ### Open WebSocket connection with subprotocol negotiation Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/mod.rs.html Builds a client request with the OCPP subprotocol, optional basic auth, and TLS connector, then connects and validates the negotiated subprotocol. Logs connection progress and clears pending state on success. ```rust async fn connect(&mut self) -> Result { let url = self.connection_url()?; self.validate_connection_security(&url)?; let mut request = url.as_str().into_client_request()?; request.headers_mut().insert( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_str(self.config.protocol.subprotocol())?, ); if let Some(header) = self.basic_auth_header()? { request.headers_mut().insert(AUTHORIZATION, header); } self.log( UiLogLevel::Info, format!( "Connecting to {} with subprotocol {} ...", redact_url_secrets(url.as_str()), self.config.protocol.subprotocol() ), ); let connector = match self.tls_connector() { Ok(connector) => connector, Err(error) => return Err(error), }; let ws_config = Some(websocket_config()); let connection_result = if connector.is_some() { connect_async_tls_with_config(request, ws_config, false, connector).await } else { connect_async_with_config(request, ws_config, false).await }; let (stream, response) = match connection_result { Ok(connection) => connection, Err(error) => { self.record_secure_connection_failure(&error); return Err(error.into()); } }; let expected_subprotocol = self.config.protocol.subprotocol(); let negotiated = response .headers() .get(SEC_WEBSOCKET_PROTOCOL) .and_then(|value| value.to_str().ok()); let negotiated = validate_negotiated_subprotocol(expected_subprotocol, negotiated)?; self.connected = true; self.pending = None; self.queue.clear(); self.incoming_message_ids.clear(); self.log( UiLogLevel::Info, format!("Connected. Negotiated WebSocket subprotocol: {negotiated}"), ); } ``` -------------------------------- ### Apply RemoteStart Authorize Call Result Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/workflow/call_result.rs.html Handles the result of a RemoteStartTransaction authorization. If accepted, validates the charging profile and starts the transaction; otherwise logs a warning. Uses parse_v1_6_id_tag_status to determine status. ```rust fn apply_remote_start_authorize_call_result( &mut self, connector: u16, id_token: &str, charging_profile: Option<&Value>, payload: &Value, ) { let status = parse_v1_6_id_tag_status(payload); self.log( UiLogLevel::Info, format!( "RemoteStartTransaction authorization {} status={}", REDACTED_SENSITIVE_VALUE, status.as_str() ), ); if status == ResponseStatus::Accepted { if let Err(error) = Self::validate_remote_start_charging_profile(charging_profile) { self.log( UiLogLevel::Warn, format!( "RemoteStartTransaction authorization accepted but charging \ profile was rejected on connector {connector}: {error}" ), ); return; } if let Err(error) = self .start_transaction(connector, id_token.to_string(), true, None, true) .and_then(|()| { self.apply_remote_start_charging_profile(connector, charging_profile) }) { self.log( UiLogLevel::Warn, format!( "RemoteStartTransaction authorization accepted but \ start/profile application failed on connector {connector}: \ {error}" ), ); } } else { self.log( UiLogLevel::Warn, format!( "RemoteStartTransaction not started on connector {}: \ authorization status={}", connector, status.as_str() ), ); } } ``` -------------------------------- ### Apply StartTransaction Call Result Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/workflow/call_result.rs.html Handles the result of a StartTransaction call. If accepted, sets the transaction ID and enqueues a status notification; otherwise cancels the transaction start and enqueues a status notification. Returns a Result. ```rust fn apply_start_transaction_call_result( &mut self, connector: u16, local_tx_id: u64, payload: &Value, ) -> Result<()> { let status = parse_v1_6_id_tag_status(payload); let transaction_id = payload.get("transactionId").and_then(Value::as_i64); if status == ResponseStatus::Accepted { if let Some(tx_id) = transaction_id { self.set_v1_6_transaction_id(connector, local_tx_id, tx_id)?; self.log( UiLogLevel::Info, format!( "StartTransaction accepted on connector {connector} \ transactionId={tx_id}" ), ); } else { self.log( UiLogLevel::Warn, "StartTransaction accepted without transactionId.", ); } self.enqueue_status_notification(connector)?; } else { self.cancel_transaction_start(connector, local_tx_id)?; self.enqueue_status_notification(connector)?; self.log( UiLogLevel::Warn, format!( "StartTransaction status={} on connector {}.", status.as_str(), connector ), ); } Ok(()) } ``` -------------------------------- ### Help Text Function Source: https://docs.rs/ocppsim/latest/src/ocppsim/commands.rs.html Returns the formatted help text string displayed in the terminal command palette. ```rust pub fn help_text() -> &'static str { "Interactive commands: status Show current simulator snapshot (connection, queue, connectors). connect [ | ] Open WebSocket to CSMS and send boot/status notifications. disconnect Close the active WebSocket connection. boot Send BootNotification immediately (must be connected). authorize Send Authorize for idToken. data-transfer [messageId] [data...]" ``` -------------------------------- ### Rust: Helper functions for variable extraction and validation Source: https://docs.rs/ocppsim/latest/src/ocppsim/simulator/incoming/v2_x.rs.html Utility functions to extract component name, variable name, and attribute type from an entry, and to check if a component or attribute type is supported. Used by the get and set variable handlers. ```rust fn variable_component(entry: &Value) -> Result<&str> { entry .get("component") .and_then(Value::as_object) .and_then(|component| component.get("name")) .and_then(Value::as_str) .ok_or_else(|| anyhow!("component.name is required.")) } fn variable_name(entry: &Value) -> Result<&str> { entry .get("variable") .and_then(Value::as_object) .and_then(|variable| variable.get("name")) .and_then(Value::as_str) .ok_or_else(|| anyhow!("variable.name is required.")) } fn variable_attribute_type(entry: &Value) -> Option { entry.get("attributeType").and_then(Value::as_str).map_or( Some(VariableAttributeType::Actual), VariableAttributeType::parse, ) } fn is_supported_variable_component(component: &str) -> bool { matches!( normalize_identifier(component).as_str(), "chargingstation" | "securityctrl" | "securityctrlr" ) } fn is_supported_variable_attribute( attribute_type: Option, ) -> bool { matches!( attribute_type, Some(VariableAttributeType::Actual | VariableAttributeType::Target) ) } ```