Skip to main content

sccp_protocol/
server.rs

1//! Stateful SCCP station-session boundary.
2//!
3//! A successful socket write or TCP acknowledgement proves only transport
4//! delivery. Media and other correlated operations remain provisional until
5//! the station sends the matching SCCP response. Every new media transaction
6//! gets a fresh nonzero wire token (a deliberately coupled ORC/SMT pair shares
7//! one), and the session accepts an acknowledgement only for the exact
8//! direction and current request generation. The zero-party acknowledgement
9//! fallback is limited to the first generation, where the
10//! stable call reference still makes it unambiguous; reopened media fails
11//! closed instead of letting an old acknowledgement settle a new request.
12//! Deadlines retire that same generation before a late response is considered.
13//! Handset presentation, receive media, transmit media, and call ownership are
14//! therefore separate states rather than one broad "connected" flag.
15//!
16//! # Runtime workflow
17//!
18//! [`Server::bind`] creates a plain TCP listener, while
19//! [`Server::with_ingress`] lets a transport owner inject clear or already
20//! decrypted streams through [`ServerIngress`]. Both constructors return a
21//! [`ServerHandle`] for commands and a bounded [`Event`] receiver for handset
22//! input and session outcomes. The caller must run [`Server::run`] for any of
23//! those channels to make progress and should consume events continuously so a
24//! full event queue cannot apply backpressure to station sessions.
25//!
26//! A station becomes addressable by [`Command`] only after registration has
27//! selected a configured [`DeviceDefinition`] and emitted
28//! [`DeviceEventKind::Registered`]. Call adapters reserve or receive a
29//! [`CallId`], send commands through the handle, and react to correlated media
30//! acknowledgements delivered as device events. [`ServerHandle::send`] confirms
31//! queue admission; [`ServerHandle::send_confirmed`] additionally waits for the
32//! complete encoded command to reach the station stream. Neither operation
33//! substitutes for a protocol acknowledgement when the command defines one.
34//!
35//! Reconfiguration replaces definitions atomically and disconnects only the
36//! sessions named by [`ReconfigureResult`] or explicitly marked as affected.
37//! [`ServerHandle::shutdown`] asks the run loop to disconnect all sessions and
38//! finish. Dropping every handle also closes the command channel and causes the
39//! same orderly exit.
40
41mod qos;
42mod transport;
43
44pub use qos::{
45    SignalingSocket, SocketQosFailure, SocketQosMark, SocketQosPolicy, SocketQosReport,
46    StationSocketQos, apply_socket_qos,
47};
48pub use transport::{ServerIngress, StationIo};
49
50use std::collections::{BTreeMap, HashMap, HashSet};
51use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
52use std::num::NonZeroU16;
53use std::sync::atomic::{AtomicU64, Ordering};
54use std::sync::{Arc, RwLock};
55use std::time::{Duration, SystemTime, UNIX_EPOCH};
56
57use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
58use thiserror::Error;
59use tokio::io::{AsyncReadExt, AsyncWriteExt};
60use tokio::net::TcpListener;
61#[cfg(test)]
62use tokio::net::TcpStream;
63use tokio::sync::{Mutex, mpsc, oneshot};
64use tokio::time::Instant;
65use tracing::{debug, info, warn};
66
67use crate::message::BUTTON_TEMPLATE_ENTRIES_PER_CHUNK;
68use crate::message::capabilities::StationMediaCapabilities;
69use crate::message::values::{
70    AlarmSeverity, BusyLampFieldState, ButtonType, CallHistoryDisposition, CallState, Codec,
71    CodecKind, DeviceType, Digit, DtmfMode, EchoCancellation, EncryptionCapability, G723BitRate,
72    IpAddressType, KeyMode, LampMode, MediaStatus, MicrophoneMode, MiscCommandType,
73    NotificationPriority, PhoneFeatures, ProtocolVersion, ReceiveTransmit, ResetType, RingDuration,
74    RingerMode, SilenceSuppression, SoftKey, SpeakerMode, StationSessionContext,
75    StatisticsProcessing, Stimulus, SubscriptionCause, Tone, ToneDirection,
76};
77use crate::message::wire::{CodecError, FrameDecoder};
78use crate::message::{
79    AnnouncementEntry, AudioStreamControl, BoundedBytes, ButtonTemplateEntry,
80    CALL_COUNT_RESPONSE_MAX_LINE_ENTRIES, CallCountLineData, CallCountResponse, ClientMessage,
81    ConnectionStatistics, MediaEncryption, MediaEndpointAddress, MediaRequestIdentity,
82    MediaRequestToken, MiscellaneousCommand, MulticastMediaReception, MulticastMediaTransmission,
83    MultimediaPayload, MultimediaPayloadDirection, MultimediaStreamControl, OpenMultimediaChannel,
84    ServerMessage, SignalingServerEndpoint,
85    StartMultimediaTransmission as MultimediaTransmissionStart, UserDataV1Message,
86    VideoFlowControl,
87};
88#[cfg(test)]
89use crate::message::{ControlMessage, MediaCapability, XmlAlarmMessage};
90use crate::phone::service::{
91    PhoneServiceEvent, PhoneServiceExtendedRouting, PhoneServiceMessageKind, PhoneServicePayload,
92    PhoneServiceRouting, parse_phone_service_payload,
93};
94#[cfg(test)]
95use crate::phone::xml::{
96    self as phone_xml, CiscoIpPhoneGraphicFileMenu, CiscoIpPhoneImageFile, CiscoIpPhoneInputItem,
97    CiscoIpPhoneKeyItem, CiscoIpPhoneSoftKeyItem, CiscoIpPhoneStatus, CiscoIpPhoneStatusFile,
98    CiscoIpPhoneTouchAreaMenuItem, PHONE_EXECUTE_MAX_ITEMS, PHONE_STATUS_BITMAP_MAX_BYTES,
99    PhoneBackgroundHttpUrl, PhoneBitmapData, PhoneExecutePriority, PhoneImageUrl, PhoneInputFlags,
100    PhoneInputParameterName, PhoneRingtoneUrl, PhoneTouchArea, PhoneXmlKey,
101};
102use crate::phone::xml::{
103    CiscoIpPhoneExecute, CiscoIpPhoneExecuteItem, CiscoIpPhoneInput, CiscoIpPhoneMenu,
104    CiscoIpPhoneMenuItem, CiscoIpPhoneSetBackground, CiscoIpPhoneSetBackgroundPreview,
105    CiscoIpPhoneSetRingTone, CiscoIpPhoneText, ConferenceListAction, ConferenceListDocument,
106    ConferenceListEntry, ConferenceMenuFamily, ConferenceParticipantActionsDocument,
107    PHONE_BACKGROUND_APPLICATION_ID, PHONE_EXECUTE_MAX_BYTES, PHONE_IMAGE_MAX_BYTES,
108    PHONE_INPUT_MAX_BYTES, PHONE_RINGTONE_APPLICATION_ID, PHONE_STATUS_MAX_BYTES,
109    PHONE_TEXT_APPLICATION_ID, PHONE_TEXT_LEGACY_MAX_CHARS, PhoneAlarmTelemetry,
110    PhoneBackgroundControlDocument, PhoneImageDocument, PhoneLocationTelemetry,
111    PhoneServicePriority, PhoneStatusDocument, PhoneXmlError, parse_phone_alarm,
112    parse_phone_location,
113};
114use crate::types::SignalingQos;
115use crate::types::{
116    ApplicationId, AudioProcessingPolicy, BlfCallerInfo, BlfState, ButtonDefinition, CallId,
117    CallInfo, CallReference, ConferenceId, DEFAULT_AUDIO_MAX_FRAMES_PER_PACKET,
118    DEFAULT_AUDIO_PACKET_MS, DeviceDefinition, DeviceId, DeviceRegistration, LineAppearance,
119    LineDefinition, LineInstance, MediaEndpoint, MediaTrafficClass, ParticipantId,
120    PassthroughPartyId, SessionGeneration, SoftKeyProfile, StationTransport,
121    StationTransportRequirement, TransactionId,
122};
123use transport::AcceptedStation;
124
125const EVENT_CAPACITY: usize = 1024;
126const COMMAND_CAPACITY: usize = 1024;
127const SESSION_COMMAND_CAPACITY: usize = 256;
128const SESSION_ACCEPT_CAPACITY: usize = 128;
129/// Maximum time a phone may leave an ordering-sensitive media command
130/// unacknowledged before the call owner is notified and the stale correlation
131/// state is retired.
132pub const HANDSET_ACKNOWLEDGEMENT_TIMEOUT: Duration = Duration::from_secs(5);
133/// Bound for the writer acknowledgement used to serialize commands whose
134/// resources must remain owned until their complete frame reaches the socket.
135pub const ORDERING_ACKNOWLEDGEMENT_TIMEOUT: Duration = Duration::from_secs(5);
136// A 79x1 normally follows the active accessory's Off event with OnHook within
137// a few dozen milliseconds.  A route change instead reports the replacement
138// accessory On immediately.  Keep the release pending long enough to
139// distinguish those two transactions when firmware omits the final OnHook.
140const MEDIA_PATH_RELEASE_GRACE: Duration = Duration::from_millis(150);
141// A timeout only releases pending correlation state; statistics are never polled.
142const CONNECTION_STATISTICS_TIMEOUT: Duration = Duration::from_secs(10);
143const MAX_PENDING_CONNECTION_STATISTICS: usize = 32;
144// Retired references prevent a late reply from binding to a replacement call.
145const MAX_STATISTICS_REFERENCES_PER_SESSION: usize = 4096;
146// Conservative defaults for phones that support multiple calls per line.
147const DEFAULT_MAX_CALLS_PER_LINE: u16 = 4;
148const DEFAULT_BUSY_TRIGGER_PER_LINE: u16 = 2;
149const PARKING_APPLICATION_ID: u32 = 9090;
150/// Shortest retry delay accepted for a rejected registration token.
151pub const MIN_REGISTRATION_BACKOFF: Duration = Duration::from_secs(30);
152/// Longest retry delay accepted for a rejected registration token.
153pub const MAX_REGISTRATION_BACKOFF: Duration = Duration::from_secs(86_400);
154/// Maximum number of parked calls rendered in one station selection menu.
155///
156/// Higher-level parking state may contain more calls; callers select and order
157/// the bounded subset passed to [`CommandAction::ShowParkingMenu`].
158pub const PARKING_MENU_MAX_ITEMS: usize = 32;
159
160/// One selectable parked call rendered by the station parking application.
161///
162/// `slot` is the stable parking-space identifier returned by a subsequent
163/// [`DeviceEventKind::ParkingMenuSelection`]. The caller and connected-party
164/// fields are presentation text and do not participate in selection identity.
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub struct ParkingMenuEntry {
167    pub slot: u32,
168    pub caller_name: String,
169    pub caller_number: String,
170    pub connected_name: String,
171    pub connected_number: String,
172}
173
174/// Audible presentation to apply to a newly offered incoming call.
175///
176/// Passing `None` to an incoming-offer method presents the call silently;
177/// [`IncomingRing::default`] selects an ordinary inside ring.
178#[derive(Clone, Copy, Debug, Eq, PartialEq)]
179pub struct IncomingRing {
180    pub mode: RingerMode,
181    pub duration: RingDuration,
182}
183
184#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
185pub enum IncomingPresentation {
186    #[default]
187    RingIn,
188    CallWaiting,
189}
190
191#[derive(Clone, Debug, Eq, PartialEq)]
192pub enum IncomingOfferDelivery {
193    Presented,
194    SessionMissing,
195    SessionStale {
196        actual_generation: SessionGeneration,
197    },
198    CancelledBeforePresentation,
199    WriteFailed,
200}
201
202#[derive(Clone, Debug, Eq, PartialEq)]
203pub struct StationSessionTarget {
204    device_id: DeviceId,
205    generation: SessionGeneration,
206}
207
208impl StationSessionTarget {
209    pub fn new(device_id: DeviceId, generation: SessionGeneration) -> Self {
210        Self {
211            device_id,
212            generation,
213        }
214    }
215}
216
217#[derive(Debug)]
218pub struct IncomingOfferReceipt(oneshot::Receiver<IncomingOfferDelivery>);
219
220impl IncomingOfferReceipt {
221    pub fn try_recv(&mut self) -> Result<Option<IncomingOfferDelivery>, ServerError> {
222        match self.0.try_recv() {
223            Ok(delivery) => Ok(Some(delivery)),
224            Err(oneshot::error::TryRecvError::Empty) => Ok(None),
225            Err(oneshot::error::TryRecvError::Closed) => Err(ServerError::Stopped),
226        }
227    }
228
229    pub async fn wait(self) -> Result<IncomingOfferDelivery, ServerError> {
230        self.0.await.map_err(|_| ServerError::Stopped)
231    }
232}
233
234impl IncomingPresentation {
235    const fn call_state(self) -> CallState {
236        match self {
237            Self::RingIn => CallState::RingIn,
238            Self::CallWaiting => CallState::CallWaiting,
239        }
240    }
241}
242
243/// Device-wide do-not-disturb state rendered by configured feature buttons.
244#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
245pub enum DoNotDisturbMode {
246    #[default]
247    Off,
248    Silent,
249    Reject,
250}
251
252/// Behavior selected for one do-not-disturb feature button.
253#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
254pub enum DoNotDisturbButtonMode {
255    #[default]
256    Cycle,
257    Silent,
258    Reject,
259}
260
261impl Default for IncomingRing {
262    fn default() -> Self {
263        Self {
264            mode: RingerMode::Inside,
265            duration: RingDuration::Normal,
266        }
267    }
268}
269
270/// One fully correlated, privacy-safe station media snapshot retained independently of call
271/// teardown. The firmware-specific quality payload remains opaque and is represented only by its
272/// bounded byte count.
273#[derive(Clone, Debug, Eq, PartialEq)]
274pub struct MediaStatisticsSnapshot {
275    /// Monotonic generation assigned to the statistics request.
276    ///
277    /// A response is retained only when it matches the live request generation,
278    /// preventing a delayed response from replacing newer statistics.
279    pub request_generation: u64,
280    pub call_id: CallId,
281    pub line_instance: LineInstance,
282    pub codec: Codec,
283    pub packet_ms: u32,
284    pub max_frames_per_packet: u32,
285    pub receive_peer: Option<MediaEndpoint>,
286    pub transmit_peer: Option<MediaEndpoint>,
287    pub packets_sent: u32,
288    pub octets_sent: u32,
289    pub packets_received: u32,
290    pub octets_received: u32,
291    pub packets_lost: u32,
292    pub jitter_millis: u32,
293    pub latency_millis: u32,
294    /// Length of the bounded opaque quality report; its contents are not
295    /// retained in management state.
296    pub quality_byte_count: usize,
297}
298
299/// Device-wide status-line mutation.
300///
301/// The optional priority selects a protocol-specific notification plane. A
302/// clear with a priority removes only that plane; an unqualified clear removes
303/// the ordinary status message.
304#[derive(Clone, Debug, Eq, PartialEq)]
305pub enum HandsetStatusMessage {
306    Display {
307        text: String,
308        /// Zero keeps the message until another status mutation replaces it.
309        timeout_seconds: u8,
310        priority: Option<NotificationPriority>,
311    },
312    Clear {
313        priority: Option<NotificationPriority>,
314    },
315}
316
317/// Immutable policy shared by every session owned by one [`Server`].
318///
319/// Station definitions are supplied separately to the constructor and may be
320/// replaced at runtime through [`ServerHandle::reconfigure`].
321#[derive(Clone, Debug)]
322pub struct ServerConfig {
323    pub bind: SocketAddr,
324    /// Baseline marking applied before a station identifies itself. A device
325    /// definition may replace it for the remainder of that session.
326    pub signaling_qos: SignalingQos,
327    /// Configured fallback used only if the accepted socket does not have a
328    /// concrete local address. Normal server-list replies use the local
329    /// interface selected by the operating system for that connection.
330    pub advertised_address: Ipv4Addr,
331    /// IPv6 fallback for an unspecified accepted local socket.
332    pub advertised_ipv6_address: Option<Ipv6Addr>,
333    pub server_name: String,
334    /// Keepalive interval advertised to stations; session expiry uses a bounded
335    /// multiple of this interval.
336    pub keepalive_seconds: u32,
337    /// Keepalive interval advertised for sessions using a secondary server.
338    pub secondary_keepalive_seconds: u32,
339    /// Ordered failover endpoints. An empty list advertises only the endpoint
340    /// that accepted the current connection.
341    pub signaling_servers: Vec<SignalingServerRoute>,
342    /// Admission and retry policy for pre-registration token probes.
343    pub registration_tokens: RegistrationTokenPolicy,
344    pub firmware_version: String,
345    pub dial_terminator: Digit,
346    pub record_dial_terminator: bool,
347    pub call_answer_order: CallSelectionOrder,
348    /// Fixed station wall-clock offset from UTC. SCCP does not carry a named
349    /// timezone or daylight-saving transition table.
350    pub timezone_offset_minutes: i16,
351    pub date_template: crate::types::DateTemplate,
352    /// Optional policy-neutral station template for an otherwise unknown
353    /// guest-hotline registration. The dial destination remains owned by the
354    /// channel adapter and is never exposed in the handset definition.
355    pub anonymous_hotline: Option<AnonymousHotlineDefinition>,
356}
357
358/// One configured server and the ports available for each signaling transport.
359#[derive(Clone, Debug, Eq, PartialEq)]
360pub struct SignalingServerRoute {
361    pub priority: u8,
362    pub name: String,
363    pub address: IpAddr,
364    pub clear_port: Option<NonZeroU16>,
365    pub secure_port: Option<NonZeroU16>,
366}
367
368impl SignalingServerRoute {
369    fn endpoint(&self, transport: StationTransport) -> Option<SignalingServerEndpoint> {
370        let port = match transport {
371            StationTransport::Clear => self.clear_port,
372            StationTransport::Secure => self.secure_port,
373        }?;
374        Some(SignalingServerEndpoint {
375            name: self.name.clone(),
376            address: self.address,
377            port,
378        })
379    }
380}
381
382/// Decision applied to a pre-registration token probe after station and
383/// transport eligibility have been established.
384#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
385pub enum RegistrationFallback {
386    #[default]
387    Reject,
388    ReturnToPrimary,
389    DeviceIdOdd,
390    DeviceIdEven,
391}
392
393/// Policy applied when a station probes this server while registered elsewhere.
394#[derive(Clone, Debug, Eq, PartialEq)]
395pub struct RegistrationTokenPolicy {
396    pub fallback: RegistrationFallback,
397    pub backoff: Duration,
398    pub server_priority: u8,
399}
400
401impl Default for RegistrationTokenPolicy {
402    fn default() -> Self {
403        Self {
404            fallback: RegistrationFallback::Reject,
405            backoff: Duration::from_secs(60),
406            server_priority: 1,
407        }
408    }
409}
410
411impl RegistrationTokenPolicy {
412    fn accepts(&self, device_id: &DeviceId) -> bool {
413        let last_nibble = device_id
414            .as_str()
415            .strip_prefix("SEP")
416            .filter(|mac| mac.len() == 12 && mac.bytes().all(|byte| byte.is_ascii_hexdigit()))
417            .and_then(|mac| mac.as_bytes().last().copied())
418            .and_then(|byte| char::from(byte).to_digit(16));
419        match self.fallback {
420            RegistrationFallback::Reject => false,
421            RegistrationFallback::ReturnToPrimary => self.server_priority == 1,
422            RegistrationFallback::DeviceIdOdd => last_nibble.is_some_and(|value| value % 2 == 1),
423            RegistrationFallback::DeviceIdEven => last_nibble.is_some_and(|value| value % 2 == 0),
424        }
425    }
426}
427
428/// Restricted definition used to admit an otherwise unknown station as a
429/// single-line hotline device.
430///
431/// The server supplies only station-visible policy. Routing and authorization
432/// of the resulting off-hook event remain the adapter's responsibility.
433#[derive(Clone, Debug, Eq, PartialEq)]
434pub struct AnonymousHotlineDefinition {
435    label: String,
436}
437
438impl AnonymousHotlineDefinition {
439    /// Build a hotline template with a validated station-visible label.
440    ///
441    /// Labels must contain 1 through 79 bytes and no control characters.
442    pub fn new(label: impl Into<String>) -> Result<Self, ServerError> {
443        let label = label.into();
444        if label.is_empty() || label.len() > 79 || label.chars().any(char::is_control) {
445            return Err(ServerError::InvalidConfig(
446                "anonymous-hotline label must contain 1..=79 non-control bytes".into(),
447            ));
448        }
449        Ok(Self { label })
450    }
451
452    fn device_definition(&self, id: DeviceId) -> DeviceDefinition {
453        let soft_keys = SoftKeyProfile::new(KeyMode::ALL_KNOWN.iter().copied().map(|mode| {
454            let actions = match mode {
455                KeyMode::OnHook => vec![SoftKey::NewCall],
456                KeyMode::OffHook | KeyMode::RingOut => vec![SoftKey::EndCall],
457                _ => Vec::new(),
458            };
459            (mode, actions)
460        }))
461        .expect("minimal anonymous-hotline soft keys are valid");
462        DeviceDefinition {
463            id,
464            description: self.label.clone(),
465            transport: StationTransportRequirement::Either,
466            signaling_qos: None,
467            buttons: vec![ButtonDefinition::Line(LineAppearance::new(
468                1,
469                LineDefinition {
470                    number: "hotline".into(),
471                    display_name: self.label.clone(),
472                },
473            ))],
474            soft_keys,
475            ui: Default::default(),
476        }
477    }
478}
479
480/// Ordering used when a station asks to answer without identifying a call.
481#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
482pub enum CallSelectionOrder {
483    #[default]
484    OldestFirst,
485    LastFirst,
486}
487
488/// Network and framing policy for one station multicast audio direction.
489#[derive(Clone, Copy, Debug, Eq, PartialEq)]
490pub struct MulticastMediaRoute {
491    pub address: IpAddr,
492    pub port: u16,
493    pub codec: Codec,
494    pub packet_millis: u32,
495}
496
497/// Complete application-owned description of one station video receive flow.
498///
499/// Session-owned call, line, and request identities are deliberately absent.
500/// The opaque payload is accepted only when retained from a decoded receive
501/// message for the live session's protocol version.
502#[derive(Clone, Debug, Eq, PartialEq)]
503pub struct MultimediaReceiveDescriptor {
504    pub conference_id: ConferenceId,
505    pub payload: MultimediaPayload,
506    pub conference_creator: bool,
507    pub encryption: Option<MediaEncryption>,
508    pub stream_passthrough_id: u32,
509    pub associated_stream_id: u32,
510    pub source: MediaEndpointAddress,
511    pub requested_address_type: IpAddressType,
512}
513
514impl MultimediaReceiveDescriptor {
515    /// Rejects descriptors whose typed envelope cannot represent a video
516    /// receive flow. Session-specific protocol and station capabilities are
517    /// checked when the command is dispatched.
518    pub fn validate(self) -> Result<Self, ServerError> {
519        validate_multimedia_receive_descriptor(&self)?;
520        Ok(self)
521    }
522}
523
524/// Complete application-owned description of one station video transmit flow.
525///
526/// The opaque payload is accepted only when retained from a decoded transmit
527/// message for the live session's protocol version. Call and request identities
528/// remain session-owned.
529#[derive(Clone, Debug, Eq, PartialEq)]
530pub struct MultimediaTransmitDescriptor {
531    pub conference_id: ConferenceId,
532    pub endpoint: MediaEndpointAddress,
533    pub payload: MultimediaPayload,
534    /// Full traffic-class octet; configuration DSCP is shifted left by two.
535    pub traffic_class: MediaTrafficClass,
536    pub encryption: Option<MediaEncryption>,
537    pub stream_passthrough_id: u32,
538    pub associated_stream_id: u32,
539}
540
541impl MultimediaTransmitDescriptor {
542    /// Rejects descriptors whose typed envelope cannot represent a video
543    /// transmit flow. Live session policy is checked during dispatch.
544    pub fn validate(self) -> Result<Self, ServerError> {
545        validate_multimedia_transmit_descriptor(&self)?;
546        Ok(self)
547    }
548}
549
550/// Parameters for one command applied to an exact live station video encoder.
551///
552/// The server derives the wire command selector and fixed parameter area from
553/// these variants. Arbitrary parameter bytes are intentionally unavailable at
554/// this stateful command boundary.
555#[derive(Clone, Debug, Eq, PartialEq)]
556pub enum MultimediaTransmitControl {
557    FreezePicture,
558    FastPictureUpdate {
559        first_gob: u32,
560        gob_count: u32,
561    },
562    FastGobUpdate {
563        first_gob: u32,
564        gob_count: u32,
565    },
566    FastMacroblockUpdate {
567        first_gob: u32,
568        first_macroblock: u32,
569        macroblock_count: u32,
570    },
571    LostPicture {
572        picture_number: u32,
573        long_term_picture_index: u32,
574    },
575    LostPartialPicture {
576        picture_number: u32,
577        long_term_picture_index: u32,
578        first_macroblock: u32,
579        macroblock_count: u32,
580    },
581    /// Requests recovery from at most four prior picture references.
582    RecoveryReferencePicture {
583        pictures: VideoPictureReferences,
584    },
585    TemporalSpatialTradeoff {
586        value: u32,
587    },
588}
589
590/// Picture identity carried by recovery feedback.
591#[derive(Clone, Copy, Debug, Eq, PartialEq)]
592pub struct VideoPictureReference {
593    pub picture_number: u32,
594    pub long_term_picture_index: u32,
595}
596
597/// A recovery request's bounded ordered picture-reference list.
598#[derive(Clone, Debug, Eq, PartialEq)]
599pub struct VideoPictureReferences(Box<[VideoPictureReference]>);
600
601impl VideoPictureReferences {
602    /// Collects at most five items before rejecting a list beyond the wire
603    /// capacity, so even an unbounded iterator cannot cause unbounded storage.
604    pub fn new(
605        pictures: impl IntoIterator<Item = VideoPictureReference>,
606    ) -> Result<Self, ServerError> {
607        let pictures = pictures.into_iter().take(5).collect::<Vec<_>>();
608        if pictures.len() > 4 {
609            return Err(ServerError::InvalidMultimediaTransmitControl(
610                "recovery picture count exceeds four",
611            ));
612        }
613        Ok(Self(pictures.into_boxed_slice()))
614    }
615
616    /// Returns picture identities in wire order.
617    pub fn as_slice(&self) -> &[VideoPictureReference] {
618        &self.0
619    }
620}
621
622impl TryFrom<Vec<VideoPictureReference>> for VideoPictureReferences {
623    type Error = ServerError;
624
625    fn try_from(pictures: Vec<VideoPictureReference>) -> Result<Self, Self::Error> {
626        Self::new(pictures)
627    }
628}
629
630impl Default for ServerConfig {
631    fn default() -> Self {
632        Self {
633            bind: SocketAddr::from(([0, 0, 0, 0], 2000)),
634            signaling_qos: SignalingQos::default(),
635            advertised_address: Ipv4Addr::LOCALHOST,
636            advertised_ipv6_address: None,
637            server_name: "sccp-protocol".to_string(),
638            keepalive_seconds: 30,
639            secondary_keepalive_seconds: 30,
640            signaling_servers: Vec::new(),
641            registration_tokens: RegistrationTokenPolicy::default(),
642            firmware_version: String::new(),
643            dial_terminator: Digit::Pound,
644            record_dial_terminator: false,
645            call_answer_order: CallSelectionOrder::OldestFirst,
646            timezone_offset_minutes: 0,
647            date_template: Default::default(),
648            anonymous_hotline: None,
649        }
650    }
651}
652
653#[derive(Clone, Debug, Eq, PartialEq)]
654/// Output emitted by the running server to its integration adapter.
655///
656/// The receiver returned by a server constructor is the sole event stream for
657/// every accepted connection. Consumers should drain it continuously and use
658/// the device ID carried by [`Event::Device`] rather than inferring ownership
659/// from event order. They should also retain the generation from the latest
660/// registration and reject later-delivered events from replaced sessions.
661pub enum Event {
662    SessionError {
663        peer: SocketAddr,
664        error: String,
665    },
666    /// A malformed non-registration message was discarded while the session
667    /// remained usable.
668    ProtocolWarning {
669        peer: SocketAddr,
670        /// Registered device identity, or `None` before registration.
671        device_id: Option<DeviceId>,
672        message_id: u32,
673        error: String,
674    },
675    Device(DeviceEvent),
676}
677
678impl Event {
679    pub fn device(
680        device_id: DeviceId,
681        session_generation: SessionGeneration,
682        event: DeviceEventKind,
683    ) -> Self {
684        Self::Device(DeviceEvent::new(device_id, session_generation, event))
685    }
686}
687
688/// One station-scoped item in the server event stream.
689#[derive(Clone, Debug, Eq, PartialEq)]
690pub struct DeviceEvent {
691    pub device_id: DeviceId,
692    /// Identifies the connection that produced this event across reconnects.
693    pub session_generation: SessionGeneration,
694    pub event: DeviceEventKind,
695}
696
697impl DeviceEvent {
698    pub fn new(
699        device_id: DeviceId,
700        session_generation: SessionGeneration,
701        event: DeviceEventKind,
702    ) -> Self {
703        Self {
704            device_id,
705            session_generation,
706            event,
707        }
708    }
709}
710
711/// State transitions and handset input produced by one station session.
712///
713/// Call-bearing variants use the server-owned [`CallId`] rather than the raw
714/// wire reference. Media success and failure variants are emitted only after
715/// correlation against the current request generation.
716#[derive(Clone, Debug, Eq, PartialEq)]
717pub enum DeviceEventKind {
718    /// Reports a station that completed registration and session initialization.
719    /// Carries the accepted device definition, peer details, and negotiated state.
720    Registered(DeviceRegistration),
721    /// Reports that the station session ended or was replaced by a newer session.
722    /// Consumers should retire state associated with this event's generation.
723    Disconnected {},
724    /// Replaces the station's advertised audio and multimedia capabilities.
725    /// The snapshot is scoped to the connection generation that reported it.
726    Capabilities {
727        capabilities: StationMediaCapabilities,
728    },
729    /// Reports that the handset went off hook on a resolved line and call.
730    /// The server has already allocated or selected the addressable call identity.
731    OffHook {
732        call_id: CallId,
733        line_instance: LineInstance,
734    },
735    /// Reports that the handset went on hook for a resolved line and call.
736    /// Integrations can use it to request hangup or complete hook-driven actions.
737    OnHook {
738        call_id: CallId,
739        line_instance: LineInstance,
740    },
741    /// Reports one keypad digit associated with an addressable handset call.
742    /// The digit is decoded but no dialing or in-call policy is applied here.
743    Digit { call_id: CallId, digit: Digit },
744    /// Reports a complete en-bloc number submitted for a newly allocated call.
745    /// The line and server-owned call identity are resolved before emission.
746    EnblocCall {
747        call_id: CallId,
748        line_instance: LineInstance,
749        number: String,
750    },
751    /// Reports activation of a configured speed dial on a resolved call and line.
752    /// Indicates whether the configuration permits collecting additional digits.
753    SpeedDial {
754        call_id: CallId,
755        line_instance: LineInstance,
756        number: String,
757        await_further_digits: bool,
758    },
759    /// Reports a decoded soft-key press with its resolved station context.
760    /// Call identity is present only when the selected key is call-scoped.
761    SoftKey {
762        /// Active call when the key is call-scoped.
763        call_id: Option<CallId>,
764        line_instance: LineInstance,
765        soft_key: SoftKey,
766    },
767    /// Reports a physical line-button press and any call selected on that line.
768    /// The line is always resolved even when no addressable call is active.
769    LineButton {
770        line_instance: LineInstance,
771        call_id: Option<CallId>,
772    },
773    /// Reports a handset hook-flash gesture on the resolved line context.
774    /// Call identity is included when the gesture belongs to an active call.
775    HookFlash {
776        call_id: Option<CallId>,
777        line_instance: LineInstance,
778    },
779    /// Reports activation of a configured generic feature button.
780    /// The instance identifies the exact feature definition on the station.
781    FeatureButton { instance: LineInstance },
782    /// Reports activation of a configured do-not-disturb feature button.
783    /// The instance identifies which station feature definition was pressed.
784    DoNotDisturbButton { instance: LineInstance },
785    /// Reports activation of a configured mobility feature button.
786    /// The instance identifies the owner of any temporary mobility appearance.
787    MobilityButton { instance: LineInstance },
788    /// Reports a configured voicemail-button press on an exact line and call.
789    /// The server allocates or resolves the addressable call before emission.
790    VoicemailButton {
791        call_id: CallId,
792        line_instance: LineInstance,
793    },
794    /// Reports activation of a configured parking-lot feature button.
795    /// Includes its feature instance, line context, and active call when present.
796    ParkingLotButton {
797        /// Feature-button instance, distinct from the call line instance.
798        instance: LineInstance,
799        /// Active call associated with the press, when any.
800        call_id: Option<CallId>,
801        line_instance: LineInstance,
802    },
803    /// Reports a parking slot selected from the station parking menu.
804    /// Carries the configured lot identity and numeric slot chosen by the user.
805    ParkingMenuSelection { lot: String, slot: u32 },
806    /// Reports a correlated response submitted by a station phone service.
807    /// The typed response retains application routing and submitted form values.
808    PhoneServiceResponse { response: PhoneServiceEvent },
809    /// Reports an action selected from a station conference service document.
810    /// The typed action identifies its conference, participant, and operation.
811    ConferenceListAction { action: ConferenceListAction },
812    /// Reports that the current audio receive request was acknowledged.
813    /// Carries the correlated call, media status, and allocated station endpoint.
814    ReceiveChannelOpened {
815        call_id: CallId,
816        status: MediaStatus,
817        endpoint: MediaEndpoint,
818    },
819    /// Reports that the current multimedia receive channel opened successfully.
820    /// Carries the correlated call, codec, endpoint, and passthrough identity.
821    MultimediaReceiveChannelOpened {
822        call_id: CallId,
823        codec: Codec,
824        endpoint: MediaEndpointAddress,
825        passthrough_party_id: PassthroughPartyId,
826    },
827    /// Reports a negative acknowledgement for the current multimedia receive request.
828    /// Carries the failed endpoint, codec, status, and correlated passthrough identity.
829    MultimediaReceiveChannelFailed {
830        call_id: CallId,
831        codec: Codec,
832        status: MediaStatus,
833        endpoint: MediaEndpointAddress,
834        passthrough_party_id: PassthroughPartyId,
835    },
836    /// Reports expiry of the current multimedia receive acknowledgement deadline.
837    /// Carries the call, codec, and passthrough identity of the retired request.
838    MultimediaReceiveChannelTimedOut {
839        call_id: CallId,
840        codec: Codec,
841        passthrough_party_id: PassthroughPartyId,
842    },
843    /// Reports that the current multimedia transmit request started successfully.
844    /// Carries the correlated call, codec, endpoint, and passthrough identity.
845    MultimediaTransmitStarted {
846        call_id: CallId,
847        codec: Codec,
848        endpoint: MediaEndpointAddress,
849        passthrough_party_id: PassthroughPartyId,
850    },
851    /// Reports a negative acknowledgement for the current multimedia transmit request.
852    /// Carries the failed endpoint, codec, status, and correlated passthrough identity.
853    MultimediaTransmitFailed {
854        call_id: CallId,
855        codec: Codec,
856        status: MediaStatus,
857        endpoint: MediaEndpointAddress,
858        passthrough_party_id: PassthroughPartyId,
859    },
860    /// Reports expiry of the current multimedia transmit acknowledgement deadline.
861    /// Carries the call, codec, and passthrough identity of the retired request.
862    MultimediaTransmitTimedOut {
863        call_id: CallId,
864        codec: Codec,
865        passthrough_party_id: PassthroughPartyId,
866    },
867    TransmitChannelOpen {
868        call_id: CallId,
869        outcome: TransmitOpenOutcome,
870        endpoint: MediaEndpoint,
871    },
872    /// Reports expiry of an audio receive or transmit acknowledgement deadline.
873    /// Identifies the correlated call and the acknowledgement type that expired.
874    HandsetAcknowledgementTimedOut {
875        call_id: CallId,
876        acknowledgement: HandsetAcknowledgement,
877    },
878    /// Reports a station-declared failure of an active audio transmission.
879    /// Carries the correlated call, media status, and failed remote endpoint.
880    MediaTransmissionFailed {
881        call_id: CallId,
882        status: MediaStatus,
883        endpoint: MediaEndpoint,
884    },
885    /// Reports that the current multicast receive request started successfully.
886    /// Carries the exact conference, call, and admitted multicast route.
887    MulticastReceptionStarted {
888        conference_id: ConferenceId,
889        call_id: CallId,
890        route: MulticastMediaRoute,
891    },
892    /// Reports a negative acknowledgement for the current multicast receive request.
893    /// Carries the exact conference, call, and station-reported failure status.
894    MulticastReceptionFailed {
895        conference_id: ConferenceId,
896        call_id: CallId,
897        status: MediaStatus,
898    },
899    /// Reports expiry of the current multicast receive acknowledgement deadline.
900    /// Identifies the exact conference and call whose request was retired.
901    MulticastReceptionTimedOut {
902        conference_id: ConferenceId,
903        call_id: CallId,
904    },
905    /// Reports that multicast transmission began for the requested route.
906    /// Carries the exact conference, call, and admitted multicast destination.
907    MulticastTransmissionStarted {
908        conference_id: ConferenceId,
909        call_id: CallId,
910        route: MulticastMediaRoute,
911    },
912    /// Reports a station-declared failure of multicast audio transmission.
913    /// Carries the conference, call, status, and failed multicast endpoint.
914    MulticastTransmissionFailed {
915        conference_id: ConferenceId,
916        call_id: CallId,
917        status: MediaStatus,
918        address: IpAddr,
919        port: u16,
920    },
921    /// Reports a correlated media-statistics response retained by the server.
922    /// Carries the complete snapshot for the exact call and media generation.
923    ConnectionStatisticsCollected { snapshot: MediaStatisticsSnapshot },
924    /// Reports a decoded fixed-layout station alarm and optional parameters.
925    /// Carries the station-provided severity and bounded human-readable text.
926    Alarm {
927        severity: AlarmSeverity,
928        text: String,
929        parameters: Option<[u32; 2]>,
930    },
931    /// Reports a typed alarm decoded from a station XML telemetry document.
932    /// The parsed telemetry exposes only the validated alarm schema and values.
933    XmlAlarm { telemetry: PhoneAlarmTelemetry },
934    /// Reports typed station location data decoded from an XML document.
935    /// The telemetry retains validated address and location fields in wire order.
936    LocationInformation { telemetry: PhoneLocationTelemetry },
937    /// Reports that the station headset-enabled state changed.
938    /// Carries the newly reported boolean state for integration-side handling.
939    HeadsetStatusChanged { enabled: bool },
940    /// Reports a state transition on a station media path or accessory.
941    /// Carries the typed path identity and the event reported for that path.
942    MediaPathChanged {
943        path: crate::message::values::MediaPathId,
944        event: crate::message::values::MediaPathEvent,
945    },
946    /// Reports a valid client message with no implemented server-side behavior.
947    /// Preserves the decoded message so integrations can observe or log it.
948    UnhandledMessage { message: ClientMessage },
949}
950
951/// Station acknowledgement whose correlation deadline expired.
952#[derive(Clone, Copy, Debug, Eq, PartialEq)]
953pub enum HandsetAcknowledgement {
954    OpenReceiveChannel,
955}
956
957#[derive(Clone, Copy, Debug, Eq, PartialEq)]
958pub enum TransmitOpenOutcome {
959    Acknowledged,
960    Implied,
961    NotReported,
962    Rejected(MediaStatus),
963}
964
965/// One station-targeted operation submitted through [`ServerHandle`].
966///
967/// Construction does not consult live session state; target and call
968/// availability are checked when the running server dispatches the action.
969#[derive(Clone, Debug)]
970pub struct Command {
971    pub device_id: DeviceId,
972    pub action: CommandAction,
973}
974
975impl Command {
976    pub fn new(device_id: DeviceId, action: CommandAction) -> Self {
977        Self { device_id, action }
978    }
979}
980
981/// Operation applied to the target station session in command-queue order.
982///
983/// Call-scoped actions resolve the server-owned [`CallId`] to the current wire
984/// reference inside the session. Actions for stale calls are ignored, except
985/// that [`Self::CloseCall`] also records an early cancellation so it can retire
986/// an incoming offer that has not reached the session yet.
987///
988/// Outbound presentation normally progresses from [`Self::BeginCall`] through
989/// proceeding or ringing to [`Self::CommitOutboundCall`]. Media actions allocate
990/// a fresh request identity for each generation; close and stop actions retire
991/// that identity so a late acknowledgement cannot settle replacement media.
992#[derive(Clone, Debug)]
993pub enum CommandAction {
994    /// Creates an outbound call in the station session and makes it active.
995    /// Presents the line off hook using the supplied codec and call identity.
996    BeginCall {
997        line_instance: LineInstance,
998        call_id: CallId,
999        codec: Codec,
1000    },
1001    /// Moves a held call into transfer mode and creates a consultation call.
1002    /// Makes the consultation call active with transfer-specific station UI.
1003    BeginTransfer {
1004        source_call_id: CallId,
1005        consultation_line_instance: LineInstance,
1006        consultation_call_id: CallId,
1007        codec: Codec,
1008    },
1009    /// Replaces the calling and called-party information shown for a call.
1010    /// Also updates the directory number used for later media statistics.
1011    SetCallInfo { call_id: CallId, info: CallInfo },
1012    /// Commits collected outbound digits and advances the call to proceeding.
1013    /// Updates call information, dialed-number history, lamps, and station UI.
1014    CommitOutboundCall { call_id: CallId, info: CallInfo },
1015    /// Presents an outbound call as accepted and currently proceeding.
1016    /// Stops dial tone and refreshes call information and the station prompt.
1017    PresentOutboundProceeding { call_id: CallId, info: CallInfo },
1018    /// Presents remote alerting for an outbound call on the station.
1019    /// Updates call information, prompt, soft keys, and local ringback tone.
1020    PresentOutboundRinging { call_id: CallId, info: CallInfo },
1021    /// Changes the protocol-visible state of an existing station call.
1022    /// Reconciles active-call ownership, history, lamps, prompts, and soft keys.
1023    SetCallState { call_id: CallId, state: CallState },
1024    /// Marks or unmarks a call in the station's selected-call set.
1025    /// Emits the selection status for the call's current wire reference.
1026    SetCallSelected { call_id: CallId, selected: bool },
1027    /// Displays call-scoped prompt text for an optional number of seconds.
1028    /// Resolves the server call identity to the station line and wire call.
1029    DisplayPrompt {
1030        call_id: CallId,
1031        timeout_seconds: u32,
1032        text: String,
1033    },
1034    /// Clears the prompt currently associated with a station call.
1035    /// Targets the call's resolved line instance and current wire reference.
1036    ClearPrompt { call_id: CallId },
1037    /// Replaces the station's persistent status notification and beep policy.
1038    /// Selects the appropriate static or dynamic display frames for the phone.
1039    SetStatusMessage {
1040        message: HandsetStatusMessage,
1041        beep: bool,
1042    },
1043    /// Enables or disables the station microphone outside a media renegotiation.
1044    /// Sends the handset microphone-mode command in command-queue order.
1045    SetMicrophoneMode { enabled: bool },
1046    /// Shows or clears the recording indicator for a particular call.
1047    /// Resolves the call before emitting the station recording-status frame.
1048    SetRecordingStatus { call_id: CallId, active: bool },
1049    /// Requests that the station reset using the selected reset behavior.
1050    /// The reset type controls whether the device restarts or resets its state.
1051    ResetDevice { reset_type: ResetType },
1052    /// Sets the message-waiting state for one configured line appearance.
1053    /// Caches the state and updates the corresponding station lamp immediately.
1054    SetMwi {
1055        line_instance: LineInstance,
1056        enabled: bool,
1057    },
1058    /// Replaces all three forwarding destinations retained for one line.
1059    /// A missing destination clears that forwarding kind and its display.
1060    SetForwardStatus {
1061        line_instance: LineInstance,
1062        forward_all: Option<String>,
1063        forward_busy: Option<String>,
1064        forward_no_answer: Option<String>,
1065    },
1066    /// Sets the enabled state of a configured generic feature button.
1067    /// Caches and emits the station-specific feature projection when present.
1068    SetFeatureStatus {
1069        instance: LineInstance,
1070        enabled: bool,
1071    },
1072    /// Updates a configured do-not-disturb button and its current mode.
1073    /// Projects the configured button behavior into the station UI and cache.
1074    SetDoNotDisturbStatus {
1075        instance: LineInstance,
1076        mode: DoNotDisturbMode,
1077        button_mode: DoNotDisturbButtonMode,
1078    },
1079    /// Installs or removes the temporary line owned by a mobility button.
1080    /// Rebuilds the button template and line status as one validated change.
1081    SetMobilityAppearance {
1082        mobility_instance: LineInstance,
1083        appearance: Option<LineAppearance>,
1084    },
1085    /// Updates a configured busy-lamp-field button and optional caller details.
1086    /// Refreshes its cached feature state and any hinted-ringing notification.
1087    SetBlfStatus {
1088        instance: LineInstance,
1089        state: BlfState,
1090        caller: Option<BlfCallerInfo>,
1091    },
1092    /// Displays a parking-lot menu containing the supplied parked calls.
1093    /// Records its transaction so a later station selection can be correlated.
1094    ShowParkingMenu {
1095        instance: LineInstance,
1096        transaction_id: TransactionId,
1097        lot: String,
1098        calls: Vec<ParkingMenuEntry>,
1099    },
1100    /// Displays the participant list for a conference attached to a call.
1101    /// Selects the phone-compatible menu family and sends its service document.
1102    ShowConferenceList {
1103        call_id: CallId,
1104        conference_id: ConferenceId,
1105        participants: Vec<ConferenceListEntry>,
1106    },
1107    /// Displays the allowed actions for one conference participant.
1108    /// Builds removal and demotion choices for the target call and phone family.
1109    ShowConferenceParticipantActions {
1110        call_id: CallId,
1111        conference_id: ConferenceId,
1112        participant: ConferenceListEntry,
1113        removable: bool,
1114        demotable: bool,
1115    },
1116    /// Displays a text-service document in the requested application envelope.
1117    /// Segments and encodes it according to the negotiated station protocol.
1118    ShowTextService {
1119        line_instance: LineInstance,
1120        call_reference: CallReference,
1121        transaction_id: TransactionId,
1122        priority: PhoneServicePriority,
1123        document: CiscoIpPhoneText,
1124    },
1125    /// Displays an input form and establishes its response correlation fields.
1126    /// A submitted form returns as a phone-service response device event.
1127    ShowInputService {
1128        line_instance: LineInstance,
1129        call_reference: CallReference,
1130        application_id: ApplicationId,
1131        transaction_id: TransactionId,
1132        priority: PhoneServicePriority,
1133        document: CiscoIpPhoneInput,
1134    },
1135    /// Sends a document instructing the station to execute phone actions.
1136    /// Preserves application routing, transaction identity, and display priority.
1137    ExecutePhoneActions {
1138        line_instance: LineInstance,
1139        call_reference: CallReference,
1140        application_id: ApplicationId,
1141        transaction_id: TransactionId,
1142        priority: PhoneServicePriority,
1143        document: CiscoIpPhoneExecute,
1144    },
1145    /// Displays an image-service document using the target application envelope.
1146    /// Encodes and segments the image for the negotiated station protocol.
1147    ShowImageService {
1148        line_instance: LineInstance,
1149        call_reference: CallReference,
1150        application_id: ApplicationId,
1151        transaction_id: TransactionId,
1152        priority: PhoneServicePriority,
1153        document: PhoneImageDocument,
1154    },
1155    /// Displays a status-service document with its application routing fields.
1156    /// Encodes and segments status content for the negotiated station protocol.
1157    ShowStatusService {
1158        line_instance: LineInstance,
1159        call_reference: CallReference,
1160        application_id: ApplicationId,
1161        transaction_id: TransactionId,
1162        priority: PhoneServicePriority,
1163        document: PhoneStatusDocument,
1164    },
1165    /// Applies a background image described by the supplied control document.
1166    /// Sends the request with the provided service transaction identity.
1167    SetBackgroundImage {
1168        transaction_id: TransactionId,
1169        document: CiscoIpPhoneSetBackground,
1170    },
1171    /// Previews a background image without selecting it as the active image.
1172    /// Sends the preview control document under the supplied transaction.
1173    PreviewBackgroundImage {
1174        transaction_id: TransactionId,
1175        document: CiscoIpPhoneSetBackgroundPreview,
1176    },
1177    /// Selects the station ringtone described by the supplied control document.
1178    /// Sends the ringtone operation under the provided service transaction.
1179    SetRingtone {
1180        transaction_id: TransactionId,
1181        document: CiscoIpPhoneSetRingTone,
1182    },
1183    /// Starts a user-direction tone for a call, or stops tone for `Silence`.
1184    /// Resolves the call to its current station line and wire reference.
1185    StartTone { call_id: CallId, tone: Tone },
1186    /// Carries a service-node request to start conference announcements.
1187    /// Station sessions reject it because announcement routing is not station-local.
1188    StartAnnouncement {
1189        conference_id: ConferenceId,
1190        announcements: Vec<AnnouncementEntry>,
1191        /// Marks the final request in an acknowledgement-delimited sequence.
1192        end_of_ack: bool,
1193        participant_ids: Vec<ParticipantId>,
1194        /// Bit mask selecting which listed participants hear the announcement.
1195        hearing_participant_mask: u32,
1196        /// Protocol playback-mode value retained for station interpretation.
1197        play_mode: u32,
1198    },
1199    /// Carries a service-node request to stop conference announcements.
1200    /// Station sessions reject it because announcement routing is not station-local.
1201    StopAnnouncement { conference_id: ConferenceId },
1202    /// Carries a conference announcement completion and playback result.
1203    /// Station sessions reject it because completion belongs to the service node.
1204    AnnouncementFinish {
1205        conference_id: ConferenceId,
1206        play_status: u32,
1207    },
1208    /// Starts audible ringing and the associated line indication for a call.
1209    /// Resolves the call to its current station line and wire reference.
1210    StartRinging { call_id: CallId },
1211    /// Stops audible ringing and clears the associated call indication.
1212    /// Leaves the call itself intact for a following state transition or close.
1213    StopRinging { call_id: CallId },
1214    /// Allocates the station's audio receive channel for a correlated request.
1215    /// Applies codec, packetization, DTMF, source, and processing constraints.
1216    OpenReceiveChannel {
1217        call_id: CallId,
1218        /// Optional RTP source restriction. `None` accepts media from any
1219        /// source and is encoded as the SCCP wildcard endpoint `0.0.0.0:0`.
1220        source: Option<MediaEndpoint>,
1221        codec: Codec,
1222        packet_ms: u32,
1223        max_frames_per_packet: u32,
1224        dtmf_mode: DtmfMode,
1225        audio_processing: AudioProcessingPolicy,
1226    },
1227    /// Opens a video receive channel matching an advertised station capability.
1228    /// Replacing an existing generation closes and retires that generation first.
1229    OpenMultimediaReceiveChannel {
1230        call_id: CallId,
1231        descriptor: MultimediaReceiveDescriptor,
1232    },
1233    /// Closes the active multimedia receive channel for a call.
1234    /// Retires its request identity so late acknowledgements cannot settle it.
1235    CloseMultimediaReceiveChannel { call_id: CallId },
1236    /// Starts station video transmission using an advertised transmit capability.
1237    /// Replacing an existing generation stops and retires that generation first.
1238    StartMultimediaTransmission {
1239        call_id: CallId,
1240        descriptor: MultimediaTransmitDescriptor,
1241    },
1242    /// Stops the active multimedia transmission for a call.
1243    /// Retires its request identity so late acknowledgements cannot settle it.
1244    StopMultimediaTransmission { call_id: CallId },
1245    /// Sets the maximum bit rate of an exact live station video encoder.
1246    /// Requires the current passthrough token to reject stale stream controls.
1247    SetMultimediaTransmitBitRate {
1248        call_id: CallId,
1249        passthrough_party_id: PassthroughPartyId,
1250        maximum_bit_rate: u32,
1251    },
1252    /// Notifies the exact live station video encoder of a bit-rate change.
1253    /// Requires the current passthrough token to reject stale notifications.
1254    NotifyMultimediaTransmitBitRate {
1255        call_id: CallId,
1256        passthrough_party_id: PassthroughPartyId,
1257        maximum_bit_rate: u32,
1258    },
1259    /// Applies typed flow, picture, or display feedback to a video encoder.
1260    /// Requires the current passthrough token to target the exact live stream.
1261    ControlMultimediaTransmission {
1262        call_id: CallId,
1263        passthrough_party_id: PassthroughPartyId,
1264        control: MultimediaTransmitControl,
1265    },
1266    /// Opens both audio directions for an outbound call in one writer action.
1267    /// Writes receive then transmit setup without an intervening queue boundary.
1268    OpenOutboundMedia {
1269        call_id: CallId,
1270        source: Option<MediaEndpoint>,
1271        endpoint: MediaEndpoint,
1272        codec: Codec,
1273        packet_ms: u32,
1274        max_frames_per_packet: u32,
1275        dtmf_mode: DtmfMode,
1276        audio_processing: AudioProcessingPolicy,
1277        traffic_class: MediaTrafficClass,
1278    },
1279    /// Closes the station's audio receive leg for a call.
1280    /// Retires its pending request so a late acknowledgement is ignored.
1281    CloseReceiveChannel { call_id: CallId },
1282    /// Starts the station's audio transmit leg toward the supplied endpoint.
1283    /// Applies DTMF, processing, and traffic-class policy to the new generation.
1284    StartMedia {
1285        call_id: CallId,
1286        endpoint: MediaEndpoint,
1287        dtmf_mode: DtmfMode,
1288        audio_processing: AudioProcessingPolicy,
1289        traffic_class: MediaTrafficClass,
1290    },
1291    /// Starts station reception of an admitted multicast audio route.
1292    /// Correlates the route with the call and requested audio processing policy.
1293    StartMulticastReception {
1294        conference_id: ConferenceId,
1295        call_id: CallId,
1296        route: MulticastMediaRoute,
1297        echo_cancellation: EchoCancellation,
1298        g723_bitrate: G723BitRate,
1299    },
1300    /// Stops multicast audio reception for the exact call and conference.
1301    /// Retires the active receive transaction associated with that route.
1302    StopMulticastReception {
1303        conference_id: ConferenceId,
1304        call_id: CallId,
1305    },
1306    /// Starts station transmission onto an admitted multicast audio route.
1307    /// Applies precedence, silence, packetization, and G.723 rate parameters.
1308    StartMulticastTransmission {
1309        conference_id: ConferenceId,
1310        call_id: CallId,
1311        route: MulticastMediaRoute,
1312        precedence: u32,
1313        silence_suppression: SilenceSuppression,
1314        max_frames_per_packet: u32,
1315        g723_bitrate: G723BitRate,
1316    },
1317    /// Stops multicast audio transmission for the exact call and conference.
1318    /// Retires the active transmit transaction associated with that route.
1319    StopMulticastTransmission {
1320        conference_id: ConferenceId,
1321        call_id: CallId,
1322    },
1323    /// Stops the station's audio transmit leg for a call.
1324    /// Retires its pending request so a late acknowledgement is ignored.
1325    StopMedia { call_id: CallId },
1326    /// Tears down all station media and presentation for a call.
1327    /// Requests final statistics when applicable and retires the call identity.
1328    CloseCall { call_id: CallId },
1329    /// Drains all active media and terminates the target station session.
1330    /// Causes the running server to disconnect that device after command handling.
1331    DisconnectDevice {},
1332}
1333
1334/// Failure returned by server construction, command submission, session I/O,
1335/// or stateful command validation.
1336///
1337/// Queue-admission failures do not imply that an earlier command failed.
1338/// [`Self::CommandWrite`] and [`Self::CommandAcknowledgementTimeout`] are
1339/// specific to [`ServerHandle::send_confirmed`]; protocol-level media outcomes
1340/// instead arrive through [`Event`].
1341#[derive(Debug, Error)]
1342pub enum ServerError {
1343    #[error("failed to bind SCCP server: {0}")]
1344    Bind(#[source] std::io::Error),
1345    #[error("SCCP server I/O failed: {0}")]
1346    Io(#[from] std::io::Error),
1347    #[error("SCCP protocol error: {0}")]
1348    Protocol(#[from] CodecError),
1349    #[error("invalid SCCP server configuration: {0}")]
1350    InvalidConfig(String),
1351    #[error("phone XML error: {0}")]
1352    PhoneXml(#[from] PhoneXmlError),
1353    #[error("device {0} is not connected")]
1354    DeviceNotConnected(DeviceId),
1355    #[error("call {0:?} does not exist")]
1356    UnknownCall(CallId),
1357    #[error("device {device} has no BLF feature button instance {instance}")]
1358    UnknownBlfButton { device: DeviceId, instance: u32 },
1359    #[error("call {call_id:?} cannot {operation} while in state {state:?}")]
1360    InvalidCallTransaction {
1361        call_id: CallId,
1362        operation: &'static str,
1363        state: CallState,
1364    },
1365    #[error("SCCP server has stopped")]
1366    Stopped,
1367    #[error("SCCP server command queue is full")]
1368    CommandQueueFull,
1369    #[error("SCCP command could not be written to the device: {0}")]
1370    CommandWrite(String),
1371    #[error("SCCP command writer acknowledgement timed out")]
1372    CommandAcknowledgementTimeout,
1373    #[error("SCCP media request identity space is exhausted")]
1374    MediaRequestIdentityExhausted,
1375    #[error("SCCP station session generation space is exhausted")]
1376    SessionGenerationExhausted,
1377    #[error("invalid multicast media policy: {0}")]
1378    InvalidMulticastMedia(&'static str),
1379    #[error("station does not advertise the requested multicast codec")]
1380    UnsupportedMulticastCodec,
1381    #[error("invalid multimedia receive policy: {0}")]
1382    InvalidMultimediaReceive(&'static str),
1383    #[error("station does not advertise the requested video receive capability")]
1384    UnsupportedMultimediaReceive,
1385    #[error("invalid multimedia transmit policy: {0}")]
1386    InvalidMultimediaTransmit(&'static str),
1387    #[error("station does not advertise the requested video transmit capability")]
1388    UnsupportedMultimediaTransmit,
1389    #[error("invalid multimedia transmit control: {0}")]
1390    InvalidMultimediaTransmitControl(&'static str),
1391    #[error(
1392        "call {call_id:?} has no open multimedia transmit stream with passthrough token {passthrough_party_id}"
1393    )]
1394    StaleMultimediaTransmitControl {
1395        call_id: CallId,
1396        passthrough_party_id: PassthroughPartyId,
1397    },
1398    #[error("{message} is a control/service-node message, not a station command")]
1399    InvalidStationCommand { message: &'static str },
1400}
1401
1402impl ServerError {
1403    const fn is_nonfatal_command_rejection(&self) -> bool {
1404        matches!(
1405            self,
1406            Self::InvalidCallTransaction { .. }
1407                | Self::UnknownBlfButton { .. }
1408                | Self::InvalidStationCommand { .. }
1409                | Self::InvalidMulticastMedia(_)
1410                | Self::UnsupportedMulticastCodec
1411                | Self::InvalidMultimediaReceive(_)
1412                | Self::UnsupportedMultimediaReceive
1413                | Self::InvalidMultimediaTransmit(_)
1414                | Self::UnsupportedMultimediaTransmit
1415                | Self::InvalidMultimediaTransmitControl(_)
1416                | Self::StaleMultimediaTransmitControl { .. }
1417        )
1418    }
1419}
1420
1421/// Cloneable command and management endpoint for a running [`Server`].
1422///
1423/// The handle does not drive I/O itself: [`Server::run`] must remain active.
1424/// Clones share call-ID allocation, retained media statistics, and the bounded
1425/// command queue. Dropping the last handle closes that queue and lets the run
1426/// loop perform its normal session shutdown.
1427#[derive(Clone, Debug)]
1428pub struct ServerHandle {
1429    command_tx: mpsc::Sender<ServerCommand>,
1430    next_call_id: Arc<AtomicU64>,
1431    latest_media_statistics: Arc<RwLock<HashMap<DeviceId, MediaStatisticsSnapshot>>>,
1432    call_answer_order: Arc<RwLock<CallSelectionOrder>>,
1433}
1434
1435/// The station definitions changed by one atomic server reconfiguration.
1436///
1437/// Only connected devices in `changed` or `removed` are disconnected. Added
1438/// devices have no session to disrupt, while definitions absent from every
1439/// list keep their live session and calls.
1440#[derive(Clone, Debug, Default, Eq, PartialEq)]
1441pub struct ReconfigureResult {
1442    pub added: Vec<DeviceId>,
1443    pub changed: Vec<DeviceId>,
1444    pub removed: Vec<DeviceId>,
1445}
1446
1447impl ReconfigureResult {
1448    pub fn is_unchanged(&self) -> bool {
1449        self.added.is_empty() && self.changed.is_empty() && self.removed.is_empty()
1450    }
1451
1452    fn disconnected_devices(&self) -> impl Iterator<Item = &DeviceId> {
1453        self.changed.iter().chain(&self.removed)
1454    }
1455}
1456
1457impl ServerHandle {
1458    /// Applies to future answer requests that omit their call reference.
1459    /// Explicit references and existing session calls are not rewritten.
1460    pub fn set_call_answer_order(&self, order: CallSelectionOrder) {
1461        *self
1462            .call_answer_order
1463            .write()
1464            .expect("SCCP call-answer-order lock poisoned") = order;
1465    }
1466
1467    /// Return the latest fully correlated statistics response for a device.
1468    ///
1469    /// Snapshots survive call and session teardown until a newer response for
1470    /// that device replaces them or the server is dropped.
1471    pub fn latest_media_statistics(&self, device_id: &DeviceId) -> Option<MediaStatisticsSnapshot> {
1472        self.latest_media_statistics
1473            .read()
1474            .expect("SCCP media-statistics lock poisoned")
1475            .get(device_id)
1476            .cloned()
1477    }
1478
1479    /// Clone every retained per-device snapshot, releasing the internal lock before a caller
1480    /// sorts, filters, or formats management output.
1481    pub fn media_statistics(&self) -> Vec<(DeviceId, MediaStatisticsSnapshot)> {
1482        self.latest_media_statistics
1483            .read()
1484            .expect("SCCP media-statistics lock poisoned")
1485            .iter()
1486            .map(|(device_id, snapshot)| (device_id.clone(), snapshot.clone()))
1487            .collect()
1488    }
1489
1490    /// Enqueue a station command, waiting for capacity in the server queue.
1491    ///
1492    /// Success confirms queue admission only. The command may subsequently be
1493    /// discarded if the target session retired, and any station response is
1494    /// reported separately through [`Event`]. Use [`Self::send_confirmed`] when
1495    /// adapter resource lifetime depends on completion of the stream write.
1496    pub async fn send(&self, command: Command) -> Result<(), ServerError> {
1497        self.command_tx
1498            .send(ServerCommand::Public(Box::new(command)))
1499            .await
1500            .map_err(|_| ServerError::Stopped)
1501    }
1502
1503    /// Send a command and wait until its complete encoded frame has been
1504    /// written to the registered device's TCP stream.
1505    ///
1506    /// This is intentionally stronger than [`Self::send`], whose completion
1507    /// only means the command entered the server queue. Lifecycle-sensitive
1508    /// callers use this boundary before releasing resources that protect the
1509    /// command's on-device operation.
1510    pub async fn send_confirmed(&self, command: Command) -> Result<(), ServerError> {
1511        let expires_at = Instant::now() + ORDERING_ACKNOWLEDGEMENT_TIMEOUT;
1512        tokio::time::timeout_at(expires_at, async {
1513            let (written_tx, written_rx) = oneshot::channel();
1514            self.command_tx
1515                .send(ServerCommand::Confirmed {
1516                    command: Box::new(command),
1517                    written: written_tx,
1518                    expires_at,
1519                })
1520                .await
1521                .map_err(|_| ServerError::Stopped)?;
1522            written_rx
1523                .await
1524                .map_err(|_| ServerError::Stopped)?
1525                .map_err(ServerError::CommandWrite)
1526        })
1527        .await
1528        .map_err(|_| ServerError::CommandAcknowledgementTimeout)?
1529    }
1530
1531    /// Enqueue a command without yielding, preserving the ordering of
1532    /// synchronous channel-driver callbacks such as call followed by hangup.
1533    pub fn try_send(&self, command: Command) -> Result<(), ServerError> {
1534        self.command_tx
1535            .try_send(ServerCommand::Public(Box::new(command)))
1536            .map_err(|error| match error {
1537                mpsc::error::TrySendError::Full(_) => ServerError::CommandQueueFull,
1538                mpsc::error::TrySendError::Closed(_) => ServerError::Stopped,
1539            })
1540    }
1541
1542    /// Allocate a call ID and enqueue an ordinarily ringing incoming offer.
1543    ///
1544    /// The returned identity is stable across all later commands and handset
1545    /// events for the offer. A failed enqueue still consumes the reserved ID.
1546    pub async fn offer_incoming_call(
1547        &self,
1548        device_id: DeviceId,
1549        line_instance: LineInstance,
1550        info: CallInfo,
1551    ) -> Result<CallId, ServerError> {
1552        let call_id = self.reserve_call_id();
1553        self.offer_incoming_call_with_id(device_id, line_instance, call_id, info)
1554            .await?;
1555        Ok(call_id)
1556    }
1557
1558    /// Reserve a call ID before exposing a call to a protocol session.
1559    ///
1560    /// Channel-driver adapters use this to install all private channel state
1561    /// before the handset can answer the subsequent offer.
1562    pub fn reserve_call_id(&self) -> CallId {
1563        CallId(self.next_call_id.fetch_add(1, Ordering::Relaxed))
1564    }
1565
1566    /// Offer an incoming call using an ID previously returned by
1567    /// [`Self::reserve_call_id`].
1568    pub async fn offer_incoming_call_with_id(
1569        &self,
1570        device_id: DeviceId,
1571        line_instance: LineInstance,
1572        call_id: CallId,
1573        info: CallInfo,
1574    ) -> Result<(), ServerError> {
1575        self.offer_incoming_call_with_id_and_ring(device_id, line_instance, call_id, info, true)
1576            .await
1577    }
1578
1579    pub async fn offer_incoming_call_with_id_and_ring(
1580        &self,
1581        device_id: DeviceId,
1582        line_instance: LineInstance,
1583        call_id: CallId,
1584        info: CallInfo,
1585        audible_ring: bool,
1586    ) -> Result<(), ServerError> {
1587        self.offer_incoming_call_with_id_and_ringer(
1588            device_id,
1589            line_instance,
1590            call_id,
1591            info,
1592            IncomingPresentation::RingIn,
1593            audible_ring.then_some(IncomingRing::default()),
1594        )
1595        .await
1596    }
1597
1598    /// Enqueue an incoming offer with explicit audible presentation.
1599    ///
1600    /// `None` creates a silent offer. `Some` applies the supplied ring mode and
1601    /// duration before selecting the incoming-call soft-key state.
1602    pub async fn offer_incoming_call_with_id_and_ringer(
1603        &self,
1604        device_id: DeviceId,
1605        line_instance: LineInstance,
1606        call_id: CallId,
1607        info: CallInfo,
1608        presentation: IncomingPresentation,
1609        ringer: Option<IncomingRing>,
1610    ) -> Result<(), ServerError> {
1611        self.command_tx
1612            .send(ServerCommand::OfferIncoming {
1613                device_id,
1614                expected_generation: None,
1615                line_instance,
1616                call_id,
1617                info,
1618                presentation,
1619                ringer,
1620                delivery: None,
1621            })
1622            .await
1623            .map_err(|_| ServerError::Stopped)?;
1624        Ok(())
1625    }
1626
1627    /// Enqueue an incoming offer without yielding. Channel drivers should use
1628    /// this from their synchronous call callback so a following hangup cannot
1629    /// overtake the offer.
1630    pub fn try_offer_incoming_call_with_id(
1631        &self,
1632        device_id: DeviceId,
1633        line_instance: LineInstance,
1634        call_id: CallId,
1635        info: CallInfo,
1636    ) -> Result<(), ServerError> {
1637        self.try_offer_incoming_call_with_id_and_ring(device_id, line_instance, call_id, info, true)
1638    }
1639
1640    /// Non-blocking form of [`Self::offer_incoming_call_with_id_and_ring`].
1641    ///
1642    /// Returns [`ServerError::CommandQueueFull`] without changing session state
1643    /// when immediate queue capacity is unavailable.
1644    pub fn try_offer_incoming_call_with_id_and_ring(
1645        &self,
1646        device_id: DeviceId,
1647        line_instance: LineInstance,
1648        call_id: CallId,
1649        info: CallInfo,
1650        audible_ring: bool,
1651    ) -> Result<(), ServerError> {
1652        self.try_offer_incoming_call_with_id_and_ringer(
1653            device_id,
1654            line_instance,
1655            call_id,
1656            info,
1657            IncomingPresentation::RingIn,
1658            audible_ring.then_some(IncomingRing::default()),
1659        )
1660    }
1661
1662    pub fn try_offer_incoming_call_with_id_and_ringer(
1663        &self,
1664        device_id: DeviceId,
1665        line_instance: LineInstance,
1666        call_id: CallId,
1667        info: CallInfo,
1668        presentation: IncomingPresentation,
1669        ringer: Option<IncomingRing>,
1670    ) -> Result<(), ServerError> {
1671        self.command_tx
1672            .try_send(ServerCommand::OfferIncoming {
1673                device_id,
1674                expected_generation: None,
1675                line_instance,
1676                call_id,
1677                info,
1678                presentation,
1679                ringer,
1680                delivery: None,
1681            })
1682            .map_err(|error| match error {
1683                mpsc::error::TrySendError::Full(_) => ServerError::CommandQueueFull,
1684                mpsc::error::TrySendError::Closed(_) => ServerError::Stopped,
1685            })
1686    }
1687
1688    pub async fn offer_incoming_call_for_session(
1689        &self,
1690        target: StationSessionTarget,
1691        line_instance: LineInstance,
1692        call_id: CallId,
1693        info: CallInfo,
1694        presentation: IncomingPresentation,
1695        ringer: Option<IncomingRing>,
1696    ) -> Result<IncomingOfferReceipt, ServerError> {
1697        let (delivery, receipt) = oneshot::channel();
1698        let StationSessionTarget {
1699            device_id,
1700            generation,
1701        } = target;
1702        self.command_tx
1703            .send(ServerCommand::OfferIncoming {
1704                device_id,
1705                expected_generation: Some(generation),
1706                line_instance,
1707                call_id,
1708                info,
1709                presentation,
1710                ringer,
1711                delivery: Some(delivery),
1712            })
1713            .await
1714            .map_err(|_| ServerError::Stopped)?;
1715        Ok(IncomingOfferReceipt(receipt))
1716    }
1717
1718    pub fn try_offer_incoming_call_for_session(
1719        &self,
1720        target: StationSessionTarget,
1721        line_instance: LineInstance,
1722        call_id: CallId,
1723        info: CallInfo,
1724        presentation: IncomingPresentation,
1725        ringer: Option<IncomingRing>,
1726    ) -> Result<IncomingOfferReceipt, ServerError> {
1727        let (delivery, receipt) = oneshot::channel();
1728        let StationSessionTarget {
1729            device_id,
1730            generation,
1731        } = target;
1732        self.command_tx
1733            .try_send(ServerCommand::OfferIncoming {
1734                device_id,
1735                expected_generation: Some(generation),
1736                line_instance,
1737                call_id,
1738                info,
1739                presentation,
1740                ringer,
1741                delivery: Some(delivery),
1742            })
1743            .map_err(|error| match error {
1744                mpsc::error::TrySendError::Full(_) => ServerError::CommandQueueFull,
1745                mpsc::error::TrySendError::Closed(_) => ServerError::Stopped,
1746            })?;
1747        Ok(IncomingOfferReceipt(receipt))
1748    }
1749
1750    /// Request orderly server shutdown.
1751    ///
1752    /// Success means the request entered the queue. The owner must still await
1753    /// the [`Server::run`] future to know that it stopped accepting streams and
1754    /// issued disconnects to every registered session.
1755    pub async fn shutdown(&self) -> Result<(), ServerError> {
1756        self.command_tx
1757            .send(ServerCommand::Shutdown)
1758            .await
1759            .map_err(|_| ServerError::Stopped)
1760    }
1761
1762    /// Atomically replace the configured station definitions. Only connected
1763    /// stations whose definition changed or was removed are asked to register
1764    /// again; unchanged live sessions and calls are preserved. Success means
1765    /// the replacement was committed and disconnect requests were queued, not
1766    /// that every affected transport has already closed.
1767    pub async fn reconfigure(
1768        &self,
1769        definitions: impl IntoIterator<Item = DeviceDefinition>,
1770    ) -> Result<ReconfigureResult, ServerError> {
1771        self.reconfigure_affected(definitions, []).await
1772    }
1773
1774    /// Atomically replaces station definitions and reconnects the explicit
1775    /// set in addition to stations whose wire definition changed. This lets a
1776    /// higher-level configuration owner apply line or global policy changes
1777    /// whose effects are not represented in [`DeviceDefinition`].
1778    pub async fn reconfigure_affected(
1779        &self,
1780        definitions: impl IntoIterator<Item = DeviceDefinition>,
1781        affected: impl IntoIterator<Item = DeviceId>,
1782    ) -> Result<ReconfigureResult, ServerError> {
1783        let mut by_id = HashMap::new();
1784        for definition in definitions {
1785            definition.validate()?;
1786            by_id.insert(definition.id.clone(), definition);
1787        }
1788        let (applied_tx, applied_rx) = oneshot::channel();
1789        self.command_tx
1790            .send(ServerCommand::Reconfigure {
1791                definitions: by_id,
1792                affected: affected.into_iter().collect(),
1793                applied: applied_tx,
1794            })
1795            .await
1796            .map_err(|_| ServerError::Stopped)?;
1797        applied_rx.await.map_err(|_| ServerError::Stopped)
1798    }
1799
1800    /// Commits station definitions and unknown-device admission as one server
1801    /// transaction before any affected session is disconnected.
1802    pub async fn reconfigure_station_policy(
1803        &self,
1804        definitions: impl IntoIterator<Item = DeviceDefinition>,
1805        affected: impl IntoIterator<Item = DeviceId>,
1806        anonymous_hotline: Option<AnonymousHotlineDefinition>,
1807    ) -> Result<ReconfigureResult, ServerError> {
1808        let mut by_id = HashMap::new();
1809        for definition in definitions {
1810            definition.validate()?;
1811            by_id.insert(definition.id.clone(), definition);
1812        }
1813        let (applied_tx, applied_rx) = oneshot::channel();
1814        self.command_tx
1815            .send(ServerCommand::ReconfigureStationPolicy {
1816                definitions: by_id,
1817                affected: affected.into_iter().collect(),
1818                anonymous_hotline,
1819                applied: applied_tx,
1820            })
1821            .await
1822            .map_err(|_| ServerError::Stopped)?;
1823        applied_rx.await.map_err(|_| ServerError::Stopped)
1824    }
1825
1826    /// Replace the unknown-device guest template for future registrations.
1827    /// A changed policy disconnects only sessions that were admitted through
1828    /// the previous anonymous template; configured sessions are untouched. The
1829    /// returned count is the number of such sessions asked to disconnect.
1830    pub async fn reconfigure_anonymous_hotline(
1831        &self,
1832        definition: Option<AnonymousHotlineDefinition>,
1833    ) -> Result<usize, ServerError> {
1834        let (applied_tx, applied_rx) = oneshot::channel();
1835        self.command_tx
1836            .send(ServerCommand::ReconfigureAnonymousHotline {
1837                definition,
1838                applied: applied_tx,
1839            })
1840            .await
1841            .map_err(|_| ServerError::Stopped)?;
1842        applied_rx.await.map_err(|_| ServerError::Stopped)
1843    }
1844}
1845
1846/// Stateful owner of station admission, registration, command dispatch, and
1847/// event correlation.
1848///
1849/// Construction is inert: callers must poll [`Self::run`]. The server owns its
1850/// listener or injected-ingress receiver and all registered session routing;
1851/// integration code normally retains only the returned [`ServerHandle`] and
1852/// event receiver after spawning the run future. Dropping the `Server` future
1853/// directly is abrupt, so normal shutdown should use [`ServerHandle::shutdown`]
1854/// and then await `run`.
1855#[derive(Debug)]
1856pub struct Server {
1857    listener: Option<TcpListener>,
1858    accepted_rx: mpsc::Receiver<AcceptedStation>,
1859    config: Arc<ServerConfig>,
1860    anonymous_hotline: Arc<RwLock<Option<AnonymousHotlineDefinition>>>,
1861    definitions: Arc<RwLock<HashMap<DeviceId, DeviceDefinition>>>,
1862    sessions: Sessions,
1863    event_tx: mpsc::Sender<Event>,
1864    command_rx: mpsc::Receiver<ServerCommand>,
1865    next_generation: Arc<AtomicU64>,
1866    next_statistics_generation: Arc<AtomicU64>,
1867    next_call_id: Arc<AtomicU64>,
1868    latest_media_statistics: Arc<RwLock<HashMap<DeviceId, MediaStatisticsSnapshot>>>,
1869    call_answer_order: Arc<RwLock<CallSelectionOrder>>,
1870}
1871
1872type Sessions = Arc<Mutex<HashMap<DeviceId, SessionSender>>>;
1873type CommandWriteConfirmation = oneshot::Sender<Result<(), String>>;
1874type IncomingOfferConfirmation = oneshot::Sender<IncomingOfferDelivery>;
1875
1876#[derive(Clone, Debug)]
1877struct SessionSender {
1878    generation: SessionGeneration,
1879    anonymous_hotline: bool,
1880    tx: mpsc::Sender<SessionCommand>,
1881}
1882
1883#[derive(Debug)]
1884enum ServerCommand {
1885    Public(Box<Command>),
1886    Confirmed {
1887        command: Box<Command>,
1888        written: CommandWriteConfirmation,
1889        expires_at: Instant,
1890    },
1891    OfferIncoming {
1892        device_id: DeviceId,
1893        expected_generation: Option<SessionGeneration>,
1894        line_instance: LineInstance,
1895        call_id: CallId,
1896        info: CallInfo,
1897        presentation: IncomingPresentation,
1898        ringer: Option<IncomingRing>,
1899        delivery: Option<IncomingOfferConfirmation>,
1900    },
1901    Reconfigure {
1902        definitions: HashMap<DeviceId, DeviceDefinition>,
1903        affected: HashSet<DeviceId>,
1904        applied: oneshot::Sender<ReconfigureResult>,
1905    },
1906    ReconfigureStationPolicy {
1907        definitions: HashMap<DeviceId, DeviceDefinition>,
1908        affected: HashSet<DeviceId>,
1909        anonymous_hotline: Option<AnonymousHotlineDefinition>,
1910        applied: oneshot::Sender<ReconfigureResult>,
1911    },
1912    ReconfigureAnonymousHotline {
1913        definition: Option<AnonymousHotlineDefinition>,
1914        applied: oneshot::Sender<usize>,
1915    },
1916    Shutdown,
1917}
1918
1919#[derive(Debug)]
1920enum AnonymousHotlineUpdate {
1921    Preserve,
1922    Replace(Option<AnonymousHotlineDefinition>),
1923}
1924
1925#[derive(Debug)]
1926enum SessionCommand {
1927    Public(Box<Command>),
1928    Confirmed {
1929        command: Box<Command>,
1930        written: CommandWriteConfirmation,
1931        expires_at: Instant,
1932    },
1933    OfferIncoming {
1934        line_instance: LineInstance,
1935        call_id: CallId,
1936        info: Box<CallInfo>,
1937        presentation: IncomingPresentation,
1938        ringer: Option<IncomingRing>,
1939        delivery: Option<IncomingOfferConfirmation>,
1940    },
1941    Disconnect,
1942}
1943
1944#[derive(Clone, Debug)]
1945struct SessionCall {
1946    call_id: CallId,
1947    wire_reference: u32,
1948    line_instance: u32,
1949    media: CallMedia,
1950    video_receive: VideoReceive,
1951    video_transmit: VideoTransmit,
1952    state: CallState,
1953    ringer: Option<IncomingRing>,
1954    history_disposition: CallHistoryDisposition,
1955    dialed_number: String,
1956    statistics_directory_number: String,
1957    transfer_role: Option<SessionTransferRole>,
1958}
1959
1960#[derive(Clone, Debug, Default)]
1961struct VideoReceive {
1962    generation: u64,
1963    leg: Option<VideoReceiveLeg>,
1964}
1965
1966#[derive(Clone, Debug)]
1967struct VideoReceiveLeg {
1968    request: MediaRequestIdentity,
1969    conference_id: ConferenceId,
1970    codec: Codec,
1971    requested_address_type: IpAddressType,
1972    state: MediaChannelState,
1973    deadline: Option<Instant>,
1974}
1975
1976#[derive(Debug)]
1977struct ExpiredVideoReceive {
1978    call_id: CallId,
1979    codec: Codec,
1980    passthrough_party_id: PassthroughPartyId,
1981    close: ServerMessage,
1982}
1983
1984#[derive(Clone, Debug, Default)]
1985struct VideoTransmit {
1986    generation: u64,
1987    leg: Option<VideoTransmitLeg>,
1988}
1989
1990#[derive(Clone, Debug)]
1991struct VideoTransmitLeg {
1992    request: MediaRequestIdentity,
1993    conference_id: ConferenceId,
1994    codec: Codec,
1995    address_type: IpAddressType,
1996    state: MediaChannelState,
1997    deadline: Option<Instant>,
1998}
1999
2000#[derive(Debug)]
2001struct ExpiredVideoTransmit {
2002    call_id: CallId,
2003    codec: Codec,
2004    passthrough_party_id: PassthroughPartyId,
2005    stop: ServerMessage,
2006}
2007
2008#[derive(Clone, Debug)]
2009struct CallMedia {
2010    generation: u64,
2011    codec: Codec,
2012    packet_ms: u32,
2013    max_frames_per_packet: u32,
2014    receive: MediaLeg,
2015    transmit: MediaLeg,
2016    transmit_confirmation: TransmitConfirmation,
2017    /// Exact StartMediaTransmission endpoint paired with an outstanding
2018    /// OpenReceiveChannel in one outbound NAT compatibility transaction.
2019    /// A successful matching receive acknowledgement settles both halves.
2020    coupled_transmit_endpoint: Option<MediaEndpoint>,
2021    requested: bool,
2022}
2023
2024impl CallMedia {
2025    fn new(codec: Codec) -> Self {
2026        Self {
2027            generation: 0,
2028            codec,
2029            packet_ms: DEFAULT_AUDIO_PACKET_MS,
2030            max_frames_per_packet: DEFAULT_AUDIO_MAX_FRAMES_PER_PACKET,
2031            receive: MediaLeg::default(),
2032            transmit: MediaLeg::default(),
2033            transmit_confirmation: TransmitConfirmation::Inactive,
2034            coupled_transmit_endpoint: None,
2035            requested: false,
2036        }
2037    }
2038}
2039
2040#[derive(Clone, Debug, Default)]
2041struct MediaLeg {
2042    request: Option<MediaRequestIdentity>,
2043    telephone_event_payload: u8,
2044    peer: Option<MediaEndpoint>,
2045    state: MediaChannelState,
2046    deadline: Option<Instant>,
2047}
2048
2049#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2050enum SessionTransferRole {
2051    Source { consultation_call_id: CallId },
2052    Consultation { source_call_id: CallId },
2053}
2054
2055#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
2056enum MediaChannelState {
2057    #[default]
2058    Closed,
2059    Opening,
2060    Open,
2061}
2062
2063#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
2064enum TransmitConfirmation {
2065    #[default]
2066    Inactive,
2067    Awaiting {
2068        deadline: Instant,
2069    },
2070    NotReported,
2071    Settled(TransmitOpenOutcome),
2072}
2073
2074impl TransmitConfirmation {
2075    fn acknowledgement_is_reportable(self, status: MediaStatus) -> Option<bool> {
2076        match self {
2077            Self::Awaiting { .. } => Some(true),
2078            Self::NotReported => Some(status != MediaStatus::Ok),
2079            Self::Inactive | Self::Settled(_) => None,
2080        }
2081    }
2082}
2083
2084impl MediaChannelState {
2085    const fn is_open(self) -> bool {
2086        matches!(self, Self::Open)
2087    }
2088}
2089
2090fn validate_server_config(config: &ServerConfig) -> Result<(), ServerError> {
2091    if digit_character(config.dial_terminator).is_none() {
2092        return Err(ServerError::InvalidConfig(
2093            "dial terminator must be one DTMF character".into(),
2094        ));
2095    }
2096    if !(-840..=840).contains(&config.timezone_offset_minutes) {
2097        return Err(ServerError::InvalidConfig(
2098            "timezone offset must be between -840 and 840 minutes".into(),
2099        ));
2100    }
2101    if config.keepalive_seconds < 5 || config.secondary_keepalive_seconds < 5 {
2102        return Err(ServerError::InvalidConfig(
2103            "primary and secondary keepalive intervals must be at least 5 seconds".into(),
2104        ));
2105    }
2106    if config.advertised_address.is_unspecified()
2107        || config.advertised_address.is_multicast()
2108        || config
2109            .advertised_ipv6_address
2110            .is_some_and(|address| address.is_unspecified() || address.is_multicast())
2111    {
2112        return Err(ServerError::InvalidConfig(
2113            "advertised fallback addresses must be unicast".into(),
2114        ));
2115    }
2116    if !(MIN_REGISTRATION_BACKOFF..=MAX_REGISTRATION_BACKOFF)
2117        .contains(&config.registration_tokens.backoff)
2118    {
2119        return Err(ServerError::InvalidConfig(
2120            "registration-token backoff must be between 30 and 86400 seconds".into(),
2121        ));
2122    }
2123    if config.registration_tokens.server_priority == 0 {
2124        return Err(ServerError::InvalidConfig(
2125            "server priority must be nonzero".into(),
2126        ));
2127    }
2128    if config.signaling_servers.len() > crate::message::MAX_SIGNALING_SERVERS {
2129        return Err(ServerError::InvalidConfig(format!(
2130            "at most {} signaling servers may be advertised",
2131            crate::message::MAX_SIGNALING_SERVERS
2132        )));
2133    }
2134    let mut priorities = HashSet::new();
2135    for server in &config.signaling_servers {
2136        if server.priority == 0 || !priorities.insert(server.priority) {
2137            return Err(ServerError::InvalidConfig(
2138                "signaling server priorities must be nonzero and unique".into(),
2139            ));
2140        }
2141        if server.name.is_empty()
2142            || server.name.len() >= 48
2143            || server.name.chars().any(char::is_control)
2144            || server.address.is_unspecified()
2145            || server.address.is_multicast()
2146            || server.clear_port.is_none() && server.secure_port.is_none()
2147        {
2148            return Err(ServerError::InvalidConfig(
2149                "each signaling server requires a name, unicast address, and at least one port"
2150                    .into(),
2151            ));
2152        }
2153    }
2154    if !config.signaling_servers.is_empty()
2155        && !priorities.contains(&config.registration_tokens.server_priority)
2156    {
2157        return Err(ServerError::InvalidConfig(
2158            "the local server priority must occur in the advertised server list".into(),
2159        ));
2160    }
2161    config
2162        .signaling_qos
2163        .validate()
2164        .map_err(|error| ServerError::InvalidConfig(error.to_string()))
2165}
2166
2167impl Server {
2168    /// Bind the configured plain TCP endpoint and construct a server.
2169    ///
2170    /// The returned tuple contains the inert server, its cloneable command
2171    /// handle, and the sole event receiver. Call [`Self::local_addr`] after
2172    /// construction when `config.bind` used port zero, then spawn or await
2173    /// [`Self::run`]. This constructor classifies every accepted connection as
2174    /// [`StationTransport::Clear`]. Use [`Self::with_ingress`] when transport
2175    /// negotiation or multiple listeners are owned elsewhere.
2176    pub async fn bind(
2177        config: ServerConfig,
2178        definitions: impl IntoIterator<Item = DeviceDefinition>,
2179    ) -> Result<(Self, ServerHandle, mpsc::Receiver<Event>), ServerError> {
2180        validate_server_config(&config)?;
2181        let listener = TcpListener::bind(config.bind)
2182            .await
2183            .map_err(ServerError::Bind)?;
2184        if let Ok(local) = listener.local_addr() {
2185            match SignalingSocket::capture(&listener, local) {
2186                Ok(socket) => report_socket_qos(None, local, socket.apply(config.signaling_qos)),
2187                Err(error) => {
2188                    warn!(%local, %error, "unable to retain signaling listener QoS control")
2189                }
2190            }
2191        }
2192        let (server, handle, events, _) = Self::build(config, definitions, Some(listener))?;
2193        Ok((server, handle, events))
2194    }
2195
2196    /// Construct a server whose ready station streams are supplied externally.
2197    ///
2198    /// The additional [`ServerIngress`] value is cloned by clear and secure
2199    /// listener tasks. Each task completes its transport setup, preserves the
2200    /// accepted peer and local socket addresses, and submits the stream with an
2201    /// accurate [`StationTransport`] classification. The returned server has no
2202    /// bound listener, so [`Self::local_addr`] is unavailable.
2203    pub fn with_ingress(
2204        config: ServerConfig,
2205        definitions: impl IntoIterator<Item = DeviceDefinition>,
2206    ) -> Result<(Self, ServerHandle, mpsc::Receiver<Event>, ServerIngress), ServerError> {
2207        Self::build(config, definitions, None)
2208    }
2209
2210    fn build(
2211        config: ServerConfig,
2212        definitions: impl IntoIterator<Item = DeviceDefinition>,
2213        listener: Option<TcpListener>,
2214    ) -> Result<(Self, ServerHandle, mpsc::Receiver<Event>, ServerIngress), ServerError> {
2215        validate_server_config(&config)?;
2216        let mut by_id = HashMap::new();
2217        for definition in definitions {
2218            definition.validate()?;
2219            by_id.insert(definition.id.clone(), definition);
2220        }
2221        let (event_tx, event_rx) = mpsc::channel(EVENT_CAPACITY);
2222        let (command_tx, command_rx) = mpsc::channel(COMMAND_CAPACITY);
2223        let (ingress, accepted_rx) =
2224            ServerIngress::channel(SESSION_ACCEPT_CAPACITY, config.signaling_qos);
2225        let next_call_id = Arc::new(AtomicU64::new(1));
2226        let latest_media_statistics = Arc::new(RwLock::new(HashMap::new()));
2227        let call_answer_order = Arc::new(RwLock::new(config.call_answer_order));
2228        let anonymous_hotline = Arc::new(RwLock::new(config.anonymous_hotline.clone()));
2229        let handle = ServerHandle {
2230            command_tx,
2231            next_call_id: Arc::clone(&next_call_id),
2232            latest_media_statistics: Arc::clone(&latest_media_statistics),
2233            call_answer_order: Arc::clone(&call_answer_order),
2234        };
2235        Ok((
2236            Self {
2237                listener,
2238                accepted_rx,
2239                config: Arc::new(config),
2240                anonymous_hotline,
2241                definitions: Arc::new(RwLock::new(by_id)),
2242                sessions: Arc::new(Mutex::new(HashMap::new())),
2243                event_tx,
2244                command_rx,
2245                next_generation: Arc::new(AtomicU64::new(1)),
2246                next_statistics_generation: Arc::new(AtomicU64::new(1)),
2247                next_call_id,
2248                latest_media_statistics,
2249                call_answer_order,
2250            },
2251            handle,
2252            event_rx,
2253            ingress,
2254        ))
2255    }
2256
2257    /// Return the concrete address owned by [`Self::bind`].
2258    ///
2259    /// This exposes an operating-system-assigned port when the requested bind
2260    /// address used port zero. Servers created by [`Self::with_ingress`] return
2261    /// [`ServerError::InvalidConfig`] because listener addresses belong to the
2262    /// external transport owner.
2263    pub fn local_addr(&self) -> Result<SocketAddr, ServerError> {
2264        self.listener
2265            .as_ref()
2266            .ok_or_else(|| ServerError::InvalidConfig("server has no bound listener".into()))?
2267            .local_addr()
2268            .map_err(ServerError::Io)
2269    }
2270
2271    /// Drive admission, command dispatch, reconfiguration, and shutdown.
2272    ///
2273    /// This consuming future must be polled exactly once. It accepts plain
2274    /// sockets owned by [`Self::bind`] and streams submitted through
2275    /// [`ServerIngress`], starts an independent session task for each, and
2276    /// serializes server-wide commands. It returns normally after an explicit
2277    /// shutdown request or after every [`ServerHandle`] is dropped; before
2278    /// returning it asks each registered session to disconnect. Listener or
2279    /// server-level I/O failures are returned as [`ServerError`], while an
2280    /// individual session failure is emitted as [`Event::SessionError`].
2281    pub async fn run(mut self) -> Result<(), ServerError> {
2282        if let Some(listener) = &self.listener {
2283            info!(bind = %listener.local_addr()?, "SCCP server listening");
2284        }
2285        loop {
2286            tokio::select! {
2287                accepted = accept_clear(self.listener.as_ref(), self.config.signaling_qos) => {
2288                    self.start_session(accepted?);
2289                }
2290                accepted = self.accepted_rx.recv(), if !self.accepted_rx.is_closed() => {
2291                    if let Some(accepted) = accepted {
2292                        self.start_session(accepted);
2293                    }
2294                }
2295                command = self.command_rx.recv() => {
2296                    match command {
2297                        Some(ServerCommand::Public(command)) => {
2298                            if let Err(error) = self.dispatch_public(*command).await {
2299                                warn!(%error, "discarding SCCP command for a retired session");
2300                            }
2301                        }
2302                        Some(ServerCommand::Confirmed { command, written, expires_at }) => {
2303                            self.dispatch_confirmed(command, written, expires_at).await;
2304                        }
2305                        Some(ServerCommand::OfferIncoming { device_id, expected_generation, line_instance, call_id, info, presentation, ringer, mut delivery }) => {
2306                            let session = self.sessions.lock().await.get(&device_id).cloned();
2307                            let Some(session) = session else {
2308                                if let Some(delivery) = delivery.take() {
2309                                    let _ = delivery.send(IncomingOfferDelivery::SessionMissing);
2310                                }
2311                                warn!(%device_id, "discarding incoming offer for a missing session");
2312                                continue;
2313                            };
2314                            if let Some(expected) = expected_generation
2315                                && session.generation != expected
2316                            {
2317                                if let Some(delivery) = delivery.take() {
2318                                    let _ = delivery.send(IncomingOfferDelivery::SessionStale {
2319                                        actual_generation: session.generation,
2320                                    });
2321                                }
2322                                warn!(%device_id, ?expected, actual = ?session.generation, "discarding incoming offer for a stale session generation");
2323                                continue;
2324                            }
2325                            if let Err(error) = session.tx.send(SessionCommand::OfferIncoming {
2326                                line_instance,
2327                                call_id,
2328                                info: Box::new(info),
2329                                presentation,
2330                                ringer,
2331                                delivery,
2332                            }).await {
2333                                if let SessionCommand::OfferIncoming {
2334                                    delivery: Some(delivery),
2335                                    ..
2336                                } = error.0
2337                                {
2338                                    let _ = delivery.send(IncomingOfferDelivery::SessionMissing);
2339                                }
2340                                warn!(%device_id, "discarding incoming offer for a retired session");
2341                            }
2342                        }
2343                        Some(ServerCommand::Reconfigure { definitions, affected, applied }) => {
2344                            let result = self
2345                                .apply_station_policy(
2346                                    definitions,
2347                                    affected,
2348                                    AnonymousHotlineUpdate::Preserve,
2349                                )
2350                                .await;
2351                            let _ = applied.send(result);
2352                        }
2353                        Some(ServerCommand::ReconfigureStationPolicy {
2354                            definitions,
2355                            affected,
2356                            anonymous_hotline,
2357                            applied,
2358                        }) => {
2359                            let result = self
2360                                .apply_station_policy(
2361                                    definitions,
2362                                    affected,
2363                                    AnonymousHotlineUpdate::Replace(anonymous_hotline),
2364                                )
2365                                .await;
2366                            let _ = applied.send(result);
2367                        }
2368                        Some(ServerCommand::ReconfigureAnonymousHotline { definition, applied }) => {
2369                            let sessions = self.sessions.lock().await;
2370                            let changed = {
2371                                let mut current = self
2372                                    .anonymous_hotline
2373                                    .write()
2374                                    .expect("SCCP anonymous-hotline lock poisoned");
2375                                if *current == definition {
2376                                    false
2377                                } else {
2378                                    *current = definition;
2379                                    true
2380                                }
2381                            };
2382                            let affected = if changed {
2383                                sessions
2384                                    .values()
2385                                    .filter(|session| session.anonymous_hotline)
2386                                    .cloned()
2387                                    .collect::<Vec<_>>()
2388                            } else {
2389                                Vec::new()
2390                            };
2391                            drop(sessions);
2392                            let count = affected.len();
2393                            for session in affected {
2394                                let _ = session.tx.send(SessionCommand::Disconnect).await;
2395                            }
2396                            let _ = applied.send(count);
2397                        }
2398                        Some(ServerCommand::Shutdown) | None => {
2399                            let sessions: Vec<_> = self.sessions.lock().await.values().cloned().collect();
2400                            for session in sessions { let _ = session.tx.send(SessionCommand::Disconnect).await; }
2401                            return Ok(());
2402                        }
2403                    }
2404                }
2405            }
2406        }
2407    }
2408
2409    fn start_session(&self, accepted: AcceptedStation) {
2410        let AcceptedStation {
2411            stream,
2412            peer,
2413            local,
2414            transport,
2415            socket_qos,
2416        } = accepted;
2417        let context = SessionContext {
2418            peer,
2419            local,
2420            transport,
2421            socket_qos,
2422            config: Arc::clone(&self.config),
2423            definitions: Arc::clone(&self.definitions),
2424            anonymous_hotline: Arc::clone(&self.anonymous_hotline),
2425            sessions: Arc::clone(&self.sessions),
2426            event_tx: self.event_tx.clone(),
2427            next_generation: Arc::clone(&self.next_generation),
2428            next_statistics_generation: Arc::clone(&self.next_statistics_generation),
2429            next_call_id: Arc::clone(&self.next_call_id),
2430            latest_media_statistics: Arc::clone(&self.latest_media_statistics),
2431            call_answer_order: Arc::clone(&self.call_answer_order),
2432        };
2433        let error_tx = self.event_tx.clone();
2434        tokio::spawn(async move {
2435            match run_session(stream, context).await {
2436                Ok(()) => debug!(%peer, "SCCP session ended cleanly"),
2437                Err(error) => {
2438                    warn!(%peer, %error, "SCCP session ended with an error");
2439                    let _ = error_tx
2440                        .send(Event::SessionError {
2441                            peer,
2442                            error: error.to_string(),
2443                        })
2444                        .await;
2445                }
2446            }
2447        });
2448    }
2449
2450    async fn dispatch_public(&self, command: Command) -> Result<(), ServerError> {
2451        let device_id = command.device_id.clone();
2452        self.dispatch(&device_id, SessionCommand::Public(Box::new(command)))
2453            .await
2454    }
2455
2456    async fn dispatch_confirmed(
2457        &self,
2458        command: Box<Command>,
2459        written: CommandWriteConfirmation,
2460        expires_at: Instant,
2461    ) {
2462        let device_id = command.device_id.clone();
2463        if confirmed_command_expired(&written, expires_at) {
2464            reject_expired_confirmed_command(written);
2465            return;
2466        }
2467        let tx = self
2468            .sessions
2469            .lock()
2470            .await
2471            .get(&device_id)
2472            .map(|session| session.tx.clone());
2473        let Some(tx) = tx else {
2474            let _ = written.send(Err(ServerError::DeviceNotConnected(device_id).to_string()));
2475            return;
2476        };
2477        if let Err(error) = tx
2478            .send(SessionCommand::Confirmed {
2479                command,
2480                written,
2481                expires_at,
2482            })
2483            .await
2484        {
2485            let SessionCommand::Confirmed { written, .. } = error.0 else {
2486                unreachable!("confirmed dispatch returned a different command variant")
2487            };
2488            let _ = written.send(Err(ServerError::DeviceNotConnected(device_id).to_string()));
2489        }
2490    }
2491
2492    async fn dispatch(
2493        &self,
2494        device_id: &DeviceId,
2495        command: SessionCommand,
2496    ) -> Result<(), ServerError> {
2497        let tx = self
2498            .sessions
2499            .lock()
2500            .await
2501            .get(device_id)
2502            .map(|s| s.tx.clone())
2503            .ok_or_else(|| ServerError::DeviceNotConnected(device_id.clone()))?;
2504        tx.send(command)
2505            .await
2506            .map_err(|_| ServerError::DeviceNotConnected(device_id.clone()))
2507    }
2508
2509    async fn apply_station_policy(
2510        &self,
2511        definitions: HashMap<DeviceId, DeviceDefinition>,
2512        affected: HashSet<DeviceId>,
2513        anonymous_hotline: AnonymousHotlineUpdate,
2514    ) -> ReconfigureResult {
2515        // Registration takes the session and definition locks in the same
2516        // order, so it sees either the complete current policy or the complete
2517        // candidate policy.
2518        let sessions = self.sessions.lock().await;
2519        let result = {
2520            let mut current = self
2521                .definitions
2522                .write()
2523                .expect("SCCP definitions lock poisoned");
2524            let result = reconfigure_result(&current, &definitions, &affected);
2525            *current = definitions;
2526            result
2527        };
2528        let anonymous_changed = match anonymous_hotline {
2529            AnonymousHotlineUpdate::Preserve => false,
2530            AnonymousHotlineUpdate::Replace(next) => {
2531                let mut current = self
2532                    .anonymous_hotline
2533                    .write()
2534                    .expect("SCCP anonymous-hotline lock poisoned");
2535                if *current == next {
2536                    false
2537                } else {
2538                    *current = next;
2539                    true
2540                }
2541            }
2542        };
2543        let affected_devices = result
2544            .disconnected_devices()
2545            .chain(affected.iter())
2546            .cloned()
2547            .collect::<HashSet<_>>();
2548        let affected_sessions = sessions
2549            .iter()
2550            .filter(|(device, session)| {
2551                affected_devices.contains(*device)
2552                    || (anonymous_changed && session.anonymous_hotline)
2553            })
2554            .map(|(_, session)| session.clone())
2555            .collect::<Vec<_>>();
2556        drop(sessions);
2557        for session in affected_sessions {
2558            let _ = session.tx.send(SessionCommand::Disconnect).await;
2559        }
2560        result
2561    }
2562}
2563
2564fn confirmed_command_expired(written: &CommandWriteConfirmation, expires_at: Instant) -> bool {
2565    written.is_closed() || Instant::now() >= expires_at
2566}
2567
2568fn reject_expired_confirmed_command(written: CommandWriteConfirmation) {
2569    let _ = written.send(Err(ServerError::CommandAcknowledgementTimeout.to_string()));
2570}
2571
2572fn prepare_session_command(
2573    command: SessionCommand,
2574) -> Option<(SessionCommand, Option<CommandWriteConfirmation>)> {
2575    match command {
2576        SessionCommand::Confirmed {
2577            command,
2578            written,
2579            expires_at,
2580        } => {
2581            if confirmed_command_expired(&written, expires_at) {
2582                reject_expired_confirmed_command(written);
2583                None
2584            } else {
2585                Some((SessionCommand::Public(command), Some(written)))
2586            }
2587        }
2588        command => Some((command, None)),
2589    }
2590}
2591
2592fn reconfigure_result(
2593    current: &HashMap<DeviceId, DeviceDefinition>,
2594    next: &HashMap<DeviceId, DeviceDefinition>,
2595    affected: &HashSet<DeviceId>,
2596) -> ReconfigureResult {
2597    let mut result = ReconfigureResult::default();
2598    for (device, definition) in next {
2599        match current.get(device) {
2600            None => result.added.push(device.clone()),
2601            Some(previous) if previous != definition => result.changed.push(device.clone()),
2602            Some(_) => {}
2603        }
2604    }
2605    let explicitly_changed: Vec<_> = affected
2606        .iter()
2607        .filter(|device| {
2608            current.contains_key(*device)
2609                && next.contains_key(*device)
2610                && !result.changed.contains(*device)
2611        })
2612        .cloned()
2613        .collect();
2614    result.changed.extend(explicitly_changed);
2615    result.removed.extend(
2616        current
2617            .keys()
2618            .filter(|device| !next.contains_key(*device))
2619            .cloned(),
2620    );
2621    result.added.sort();
2622    result.changed.sort();
2623    result.removed.sort();
2624    result
2625}
2626
2627fn command_call_id(command: &Command) -> Option<CallId> {
2628    match &command.action {
2629        CommandAction::BeginCall { call_id, .. }
2630        | CommandAction::SetCallInfo { call_id, .. }
2631        | CommandAction::CommitOutboundCall { call_id, .. }
2632        | CommandAction::PresentOutboundProceeding { call_id, .. }
2633        | CommandAction::PresentOutboundRinging { call_id, .. }
2634        | CommandAction::SetCallState { call_id, .. }
2635        | CommandAction::SetCallSelected { call_id, .. }
2636        | CommandAction::DisplayPrompt { call_id, .. }
2637        | CommandAction::ClearPrompt { call_id, .. }
2638        | CommandAction::SetRecordingStatus { call_id, .. }
2639        | CommandAction::ShowConferenceParticipantActions { call_id, .. }
2640        | CommandAction::StartTone { call_id, .. }
2641        | CommandAction::StartRinging { call_id, .. }
2642        | CommandAction::StopRinging { call_id, .. }
2643        | CommandAction::OpenReceiveChannel { call_id, .. }
2644        | CommandAction::OpenMultimediaReceiveChannel { call_id, .. }
2645        | CommandAction::CloseMultimediaReceiveChannel { call_id, .. }
2646        | CommandAction::StartMultimediaTransmission { call_id, .. }
2647        | CommandAction::StopMultimediaTransmission { call_id, .. }
2648        | CommandAction::SetMultimediaTransmitBitRate { call_id, .. }
2649        | CommandAction::NotifyMultimediaTransmitBitRate { call_id, .. }
2650        | CommandAction::ControlMultimediaTransmission { call_id, .. }
2651        | CommandAction::OpenOutboundMedia { call_id, .. }
2652        | CommandAction::CloseReceiveChannel { call_id, .. }
2653        | CommandAction::StartMedia { call_id, .. }
2654        | CommandAction::StartMulticastReception { call_id, .. }
2655        | CommandAction::StopMulticastReception { call_id, .. }
2656        | CommandAction::StartMulticastTransmission { call_id, .. }
2657        | CommandAction::StopMulticastTransmission { call_id, .. }
2658        | CommandAction::StopMedia { call_id, .. }
2659        | CommandAction::CloseCall { call_id, .. } => Some(*call_id),
2660        CommandAction::BeginTransfer { source_call_id, .. } => Some(*source_call_id),
2661        CommandAction::SetMwi { .. }
2662        | CommandAction::SetStatusMessage { .. }
2663        | CommandAction::SetMicrophoneMode { .. }
2664        | CommandAction::ResetDevice { .. }
2665        | CommandAction::SetForwardStatus { .. }
2666        | CommandAction::SetFeatureStatus { .. }
2667        | CommandAction::SetDoNotDisturbStatus { .. }
2668        | CommandAction::SetMobilityAppearance { .. }
2669        | CommandAction::SetBlfStatus { .. }
2670        | CommandAction::ShowParkingMenu { .. }
2671        | CommandAction::ShowConferenceList { .. }
2672        | CommandAction::ShowTextService { .. }
2673        | CommandAction::ShowInputService { .. }
2674        | CommandAction::ExecutePhoneActions { .. }
2675        | CommandAction::ShowImageService { .. }
2676        | CommandAction::ShowStatusService { .. }
2677        | CommandAction::SetBackgroundImage { .. }
2678        | CommandAction::PreviewBackgroundImage { .. }
2679        | CommandAction::SetRingtone { .. }
2680        | CommandAction::StartAnnouncement { .. }
2681        | CommandAction::StopAnnouncement { .. }
2682        | CommandAction::AnnouncementFinish { .. }
2683        | CommandAction::DisconnectDevice { .. } => None,
2684    }
2685}
2686
2687async fn accept_clear(
2688    listener: Option<&TcpListener>,
2689    signaling_qos: SignalingQos,
2690) -> Result<AcceptedStation, ServerError> {
2691    let Some(listener) = listener else {
2692        return std::future::pending().await;
2693    };
2694    let (stream, peer) = listener.accept().await?;
2695    stream.set_nodelay(true)?;
2696    let local = stream.local_addr()?;
2697    let socket_qos = match SignalingSocket::capture(&stream, local) {
2698        Ok(socket) => {
2699            report_socket_qos(None, peer, socket.apply(signaling_qos));
2700            Some(Box::new(socket) as Box<dyn StationSocketQos>)
2701        }
2702        Err(error) => {
2703            warn!(%peer, %error, "unable to retain signaling socket QoS control");
2704            None
2705        }
2706    };
2707    Ok(AcceptedStation {
2708        stream: Box::new(stream),
2709        peer,
2710        local,
2711        transport: StationTransport::Clear,
2712        socket_qos,
2713    })
2714}
2715
2716fn report_socket_qos(device_id: Option<&DeviceId>, endpoint: SocketAddr, report: SocketQosReport) {
2717    for failure in report.failures() {
2718        match device_id {
2719            Some(device_id) => {
2720                warn!(%device_id, %endpoint, %failure, "signaling socket marking unavailable")
2721            }
2722            None => warn!(%endpoint, %failure, "signaling socket marking unavailable"),
2723        }
2724    }
2725}
2726
2727fn allocate_session_generation(
2728    next_generation: &AtomicU64,
2729) -> Result<SessionGeneration, ServerError> {
2730    let generation = next_generation
2731        .try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
2732            SessionGeneration::new(current).and_then(|_| current.checked_add(1))
2733        })
2734        .map_err(|_| ServerError::SessionGenerationExhausted)?;
2735    SessionGeneration::new(generation).ok_or(ServerError::SessionGenerationExhausted)
2736}
2737
2738const fn transport_allowed(
2739    requirement: StationTransportRequirement,
2740    transport: StationTransport,
2741) -> bool {
2742    matches!(
2743        (requirement, transport),
2744        (StationTransportRequirement::Either, _)
2745            | (StationTransportRequirement::Clear, StationTransport::Clear)
2746            | (
2747                StationTransportRequirement::Secure,
2748                StationTransport::Secure
2749            )
2750    )
2751}
2752
2753#[derive(Debug)]
2754struct SessionContext {
2755    peer: SocketAddr,
2756    local: SocketAddr,
2757    transport: StationTransport,
2758    socket_qos: Option<Box<dyn StationSocketQos>>,
2759    config: Arc<ServerConfig>,
2760    definitions: Arc<RwLock<HashMap<DeviceId, DeviceDefinition>>>,
2761    anonymous_hotline: Arc<RwLock<Option<AnonymousHotlineDefinition>>>,
2762    sessions: Sessions,
2763    event_tx: mpsc::Sender<Event>,
2764    next_generation: Arc<AtomicU64>,
2765    next_statistics_generation: Arc<AtomicU64>,
2766    next_call_id: Arc<AtomicU64>,
2767    latest_media_statistics: Arc<RwLock<HashMap<DeviceId, MediaStatisticsSnapshot>>>,
2768    call_answer_order: Arc<RwLock<CallSelectionOrder>>,
2769}
2770
2771#[derive(Debug)]
2772struct SessionState {
2773    device: DeviceDefinition,
2774    registration: DeviceRegistration,
2775    features: PhoneFeatures,
2776    generation: SessionGeneration,
2777    runtime: SessionRuntimeState,
2778}
2779
2780/// Context-free mutable bookkeeping for one already-valid station session.
2781///
2782/// Unlike `SessionState`, this value has a real neutral default: it contains
2783/// no device identity, peer address, negotiated protocol, or generation.
2784#[derive(Debug)]
2785struct SessionRuntimeState {
2786    calls_by_id: HashMap<CallId, SessionCall>,
2787    calls_by_wire: HashMap<u32, CallId>,
2788    media_capabilities: StationMediaCapabilities,
2789    next_media_token: Option<MediaRequestToken>,
2790    next_multicast_generation: u64,
2791    multicast: HashMap<MulticastKey, MulticastSession>,
2792    pending_connection_statistics: HashMap<u32, PendingConnectionStatistics>,
2793    statistics_references: HashSet<u32>,
2794    cancelled_calls: HashSet<CallId>,
2795    last_number_by_line: HashMap<u32, String>,
2796    forwarding_by_line: HashMap<u32, SessionForwarding>,
2797    feature_states: HashMap<u32, SessionFeatureState>,
2798    mwi_by_line: HashMap<u32, bool>,
2799    mobility_appearances: HashMap<u32, LineAppearance>,
2800    active_key_mode: KeyMode,
2801    active_call_id: Option<CallId>,
2802    ringer_owner: Option<CallId>,
2803    pending_parking_menu: Option<PendingParkingMenu>,
2804    active_blf_alerts: BTreeMap<u32, HandsetStatusMessage>,
2805    visible_blf_alert: Option<HandsetStatusMessage>,
2806    persistent_status_message: bool,
2807    headset_enabled: bool,
2808    media_path_states:
2809        HashMap<crate::message::values::MediaPathId, crate::message::values::MediaPathEvent>,
2810    pending_media_path_release: Option<PendingMediaPathRelease>,
2811}
2812
2813impl Default for SessionRuntimeState {
2814    fn default() -> Self {
2815        Self {
2816            calls_by_id: HashMap::new(),
2817            calls_by_wire: HashMap::new(),
2818            media_capabilities: StationMediaCapabilities::default(),
2819            next_media_token: MediaRequestToken::new(1),
2820            next_multicast_generation: 0,
2821            multicast: HashMap::new(),
2822            pending_connection_statistics: HashMap::new(),
2823            statistics_references: HashSet::new(),
2824            cancelled_calls: HashSet::new(),
2825            last_number_by_line: HashMap::new(),
2826            forwarding_by_line: HashMap::new(),
2827            feature_states: HashMap::new(),
2828            mwi_by_line: HashMap::new(),
2829            mobility_appearances: HashMap::new(),
2830            active_key_mode: KeyMode::OnHook,
2831            active_call_id: None,
2832            ringer_owner: None,
2833            pending_parking_menu: None,
2834            active_blf_alerts: BTreeMap::new(),
2835            visible_blf_alert: None,
2836            persistent_status_message: false,
2837            headset_enabled: false,
2838            media_path_states: HashMap::new(),
2839            pending_media_path_release: None,
2840        }
2841    }
2842}
2843
2844impl SessionState {
2845    fn new(
2846        device: DeviceDefinition,
2847        registration: DeviceRegistration,
2848        features: PhoneFeatures,
2849        generation: SessionGeneration,
2850    ) -> Self {
2851        debug_assert_eq!(device.id, registration.id);
2852        Self {
2853            device,
2854            registration,
2855            features,
2856            generation,
2857            runtime: SessionRuntimeState::default(),
2858        }
2859    }
2860}
2861
2862impl std::ops::Deref for SessionState {
2863    type Target = SessionRuntimeState;
2864
2865    fn deref(&self) -> &Self::Target {
2866        &self.runtime
2867    }
2868}
2869
2870impl std::ops::DerefMut for SessionState {
2871    fn deref_mut(&mut self) -> &mut Self::Target {
2872        &mut self.runtime
2873    }
2874}
2875
2876#[derive(Clone, Copy, Debug)]
2877struct PendingMediaPathRelease {
2878    call_id: CallId,
2879    path: crate::message::values::MediaPathId,
2880    deadline: Instant,
2881}
2882
2883#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
2884struct MulticastKey {
2885    conference_id: ConferenceId,
2886    call_id: CallId,
2887}
2888
2889#[derive(Clone, Debug)]
2890struct MulticastSession {
2891    wire_call_reference: u32,
2892    receive: Option<MulticastReceive>,
2893    transmit: Option<MulticastTransmit>,
2894}
2895
2896#[derive(Clone, Debug)]
2897struct MulticastReceive {
2898    request: MediaRequestIdentity,
2899    route: MulticastMediaRoute,
2900    state: MulticastReceiveState,
2901}
2902
2903#[derive(Clone, Debug)]
2904enum MulticastReceiveState {
2905    AwaitingAcknowledgement { deadline: Instant },
2906    Open,
2907}
2908
2909#[derive(Clone, Debug)]
2910struct MulticastTransmit {
2911    request: MediaRequestIdentity,
2912    route: MulticastMediaRoute,
2913}
2914
2915impl SessionState {
2916    fn station_context(&self) -> StationSessionContext {
2917        StationSessionContext::new(self.registration.protocol, self.features)
2918    }
2919}
2920
2921#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2922struct SessionFeatureState {
2923    button_type: ButtonType,
2924    state: u32,
2925}
2926
2927#[derive(Clone, Debug)]
2928struct PendingConnectionStatistics {
2929    session_generation: SessionGeneration,
2930    request_generation: u64,
2931    call_id: CallId,
2932    line_instance: u32,
2933    codec: Codec,
2934    packet_ms: u32,
2935    max_frames_per_packet: u32,
2936    receive_peer: Option<MediaEndpoint>,
2937    transmit_peer: Option<MediaEndpoint>,
2938    directory_number: String,
2939    processing: StatisticsProcessing,
2940    expires_at: Instant,
2941}
2942
2943#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2944struct PendingParkingMenu {
2945    instance: u32,
2946    transaction_id: u32,
2947}
2948
2949#[derive(Clone, Debug, Default)]
2950struct SessionForwarding {
2951    all: Option<String>,
2952    busy: Option<String>,
2953    no_answer: Option<String>,
2954}
2955
2956async fn run_session(
2957    mut stream: Box<dyn StationIo>,
2958    context: SessionContext,
2959) -> Result<(), ServerError> {
2960    let (session_tx, mut session_rx) = mpsc::channel(SESSION_COMMAND_CAPACITY);
2961    let mut decoder = FrameDecoder::new();
2962    let mut read_buffer = [0_u8; 4096];
2963    let mut state: Option<SessionState> = None;
2964    let mut last_keepalive = Instant::now();
2965    let mut session_deadlines = tokio::time::interval(Duration::from_millis(100));
2966    session_deadlines.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2967    let keepalive_seconds = if context.config.registration_tokens.server_priority == 1 {
2968        context.config.keepalive_seconds
2969    } else {
2970        context.config.secondary_keepalive_seconds
2971    };
2972    let keepalive_timeout = Duration::from_secs(u64::from(keepalive_seconds) * 3);
2973
2974    loop {
2975        tokio::select! {
2976            read = stream.read(&mut read_buffer) => {
2977                let count = read?;
2978                if count == 0 { break; }
2979                let frames = match decoder.push(&read_buffer[..count]) {
2980                    Ok(frames) => frames,
2981                    Err(error) if state.is_none() => {
2982                        debug!(
2983                            peer = %context.peer,
2984                            %error,
2985                            "discarding malformed pre-registration SCCP stream"
2986                        );
2987                        break;
2988                    }
2989                    Err(error) => return Err(error.into()),
2990                };
2991                for frame in frames {
2992                    let decode_protocol = state
2993                        .as_ref()
2994                        .map_or(ProtocolVersion::V3, |state| state.registration.protocol);
2995                    let message_id = frame.message_id;
2996                    let message = match ClientMessage::decode_with_version(frame, decode_protocol) {
2997                        Ok(message) => message,
2998                        Err(error) if message_id != crate::message::wire_id::REGISTER => {
2999                            let device_id = state.as_ref().map(|state| state.device.id.clone());
3000                            warn!(peer = %context.peer, message_id = format_args!("0x{message_id:04x}"), %error, "ignoring malformed SCCP application message");
3001                            let _ = context.event_tx.send(Event::ProtocolWarning {
3002                                peer: context.peer,
3003                                device_id,
3004                                message_id,
3005                                error: error.to_string(),
3006                            }).await;
3007                            continue;
3008                        }
3009                        Err(error) => return Err(error.into()),
3010                    };
3011                    if let ClientMessage::Register(registration) = &message {
3012                        if state.is_some() {
3013                            return Err(ServerError::Protocol(CodecError::InvalidDefinition("duplicate REGISTER on one TCP session".into())));
3014                        }
3015                        match handle_registration(&mut stream, registration, &context, &session_tx).await? {
3016                            Some(registered) => {
3017                                state = Some(registered);
3018                                last_keepalive = Instant::now();
3019                            }
3020                            None => return Ok(()),
3021                        }
3022                    } else if let Some(state) = state.as_mut() {
3023                        if matches!(message, ClientMessage::KeepAlive) { last_keepalive = Instant::now(); }
3024                        handle_client_message(&mut stream, state, message, &context).await?;
3025                    } else {
3026                        handle_pre_registration_message(&mut stream, message, &context).await?;
3027                    }
3028                }
3029            }
3030            command = session_rx.recv() => {
3031                let Some(command) = command else { break };
3032                let Some(state) = state.as_mut() else { continue };
3033                if handle_session_command_result(&mut stream, state, command, &context).await? {
3034                    break;
3035                }
3036            }
3037            _ = session_deadlines.tick(), if state.is_some() => {
3038                if let Some(state) = state.as_mut() {
3039                    handle_session_deadlines(&mut stream, state, &context, Instant::now()).await?;
3040                }
3041            }
3042            _ = tokio::time::sleep_until(last_keepalive + keepalive_timeout), if state.is_some() => {
3043                warn!(peer = %context.peer, "SCCP keepalive timeout");
3044                break;
3045            }
3046        }
3047    }
3048
3049    if let Some(mut state) = state {
3050        drain_session_media(&mut stream, &mut state).await;
3051        let mut sessions = context.sessions.lock().await;
3052        let was_current = sessions
3053            .get(&state.device.id)
3054            .is_some_and(|entry| entry.generation == state.generation);
3055        if was_current {
3056            sessions.remove(&state.device.id);
3057        }
3058        drop(sessions);
3059        if was_current {
3060            let _ = context
3061                .event_tx
3062                .send(Event::device(
3063                    state.device.id,
3064                    state.generation,
3065                    DeviceEventKind::Disconnected {},
3066                ))
3067                .await;
3068        }
3069    }
3070    Ok(())
3071}
3072
3073async fn handle_registration(
3074    stream: &mut dyn StationIo,
3075    registration: &crate::message::RegistrationMessage,
3076    context: &SessionContext,
3077    session_tx: &mpsc::Sender<SessionCommand>,
3078) -> Result<Option<SessionState>, ServerError> {
3079    let mut sessions = context.sessions.lock().await;
3080    let configured = context
3081        .definitions
3082        .read()
3083        .expect("SCCP definitions lock poisoned")
3084        .get(&registration.device_id)
3085        .cloned();
3086    let anonymous_hotline = configured.is_none();
3087    let definition = configured.or_else(|| {
3088        context
3089            .anonymous_hotline
3090            .read()
3091            .expect("SCCP anonymous-hotline lock poisoned")
3092            .as_ref()
3093            .map(|hotline| hotline.device_definition(registration.device_id.clone()))
3094    });
3095    let Some(definition) = definition else {
3096        drop(sessions);
3097        send_message(
3098            stream,
3099            &ServerMessage::RegisterReject {
3100                reason: "Device not configured".into(),
3101            },
3102            ProtocolVersion::V17,
3103        )
3104        .await?;
3105        return Ok(None);
3106    };
3107    if !transport_allowed(definition.transport, context.transport) {
3108        drop(sessions);
3109        send_message(
3110            stream,
3111            &ServerMessage::RegisterReject {
3112                reason: "Device transport not permitted".into(),
3113            },
3114            ProtocolVersion::V17,
3115        )
3116        .await?;
3117        return Ok(None);
3118    }
3119    let protocol = registration
3120        .advertised_protocol
3121        .map(ProtocolVersion::negotiate)
3122        .transpose()?
3123        .unwrap_or(ProtocolVersion::V3);
3124    if canonical_ip_address(context.peer.ip()).is_ipv6() && protocol < ProtocolVersion::V17 {
3125        drop(sessions);
3126        send_message(
3127            stream,
3128            &ServerMessage::RegisterReject {
3129                reason: "IPv6 requires protocol v17".into(),
3130            },
3131            protocol,
3132        )
3133        .await?;
3134        return Ok(None);
3135    }
3136    let features = registration.features;
3137    let generation = allocate_session_generation(&context.next_generation)?;
3138    if let Some(socket_qos) = &context.socket_qos {
3139        let signaling_qos = definition
3140            .signaling_qos
3141            .unwrap_or(context.config.signaling_qos);
3142        report_socket_qos(
3143            Some(&registration.device_id),
3144            context.peer,
3145            socket_qos.apply(signaling_qos),
3146        );
3147    }
3148    let device_registration = DeviceRegistration {
3149        id: registration.device_id.clone(),
3150        peer: context.peer,
3151        transport: context.transport,
3152        reported_address: registration.reported_address,
3153        reported_ipv6_address: registration.reported_ipv6_address,
3154        device_type: registration.device_type,
3155        protocol,
3156        firmware: registration.firmware.clone(),
3157    };
3158    let previous = sessions.insert(
3159        registration.device_id.clone(),
3160        SessionSender {
3161            generation,
3162            anonymous_hotline,
3163            tx: session_tx.clone(),
3164        },
3165    );
3166    drop(sessions);
3167    if let Some(previous) = previous {
3168        let _ = previous.tx.send(SessionCommand::Disconnect).await;
3169    }
3170    send_message(
3171        stream,
3172        &ServerMessage::RegisterAck {
3173            keepalive_seconds: context.config.keepalive_seconds,
3174            secondary_keepalive_seconds: context.config.secondary_keepalive_seconds,
3175            protocol,
3176            features: PhoneFeatures::empty(),
3177            date_template: context.config.date_template.clone(),
3178        },
3179        protocol,
3180    )
3181    .await?;
3182    send_message(stream, &ServerMessage::CapabilitiesRequest, protocol).await?;
3183    context
3184        .event_tx
3185        .send(Event::device(
3186            registration.device_id.clone(),
3187            generation,
3188            DeviceEventKind::Registered(device_registration.clone()),
3189        ))
3190        .await
3191        .map_err(|_| ServerError::Stopped)?;
3192    info!(device_id = %registration.device_id, %protocol, peer = %context.peer, "SCCP device registered");
3193    Ok(Some(SessionState::new(
3194        definition,
3195        device_registration,
3196        features,
3197        generation,
3198    )))
3199}
3200
3201async fn handle_session_command_result(
3202    stream: &mut dyn StationIo,
3203    state: &mut SessionState,
3204    command: SessionCommand,
3205    context: &SessionContext,
3206) -> Result<bool, ServerError> {
3207    let Some((mut command, written)) = prepare_session_command(command) else {
3208        return Ok(false);
3209    };
3210    let offer_call_id = match &command {
3211        SessionCommand::OfferIncoming { call_id, .. } => Some(*call_id),
3212        _ => None,
3213    };
3214    let offer_delivery = match &mut command {
3215        SessionCommand::OfferIncoming { delivery, .. } => delivery.take(),
3216        _ => None,
3217    };
3218    if offer_call_id.is_some_and(|call_id| state.cancelled_calls.remove(&call_id)) {
3219        if let Some(delivery) = offer_delivery {
3220            let _ = delivery.send(IncomingOfferDelivery::CancelledBeforePresentation);
3221        }
3222        debug!(device_id = %state.device.id, ?offer_call_id, "discarding incoming call cancelled before it was offered");
3223        return Ok(false);
3224    }
3225    match handle_session_command(stream, state, command, context).await {
3226        Ok(disconnect) => {
3227            if let Some(delivery) = offer_delivery {
3228                let _ = delivery.send(IncomingOfferDelivery::Presented);
3229            }
3230            if let Some(written) = written {
3231                let _ = written.send(Ok(()));
3232            }
3233            Ok(disconnect)
3234        }
3235        Err(error) => {
3236            if let Some(delivery) = offer_delivery {
3237                let _ = delivery.send(IncomingOfferDelivery::WriteFailed);
3238            }
3239            if let Some(written) = written {
3240                let _ = written.send(Err(error.to_string()));
3241            }
3242            if error.is_nonfatal_command_rejection() {
3243                warn!(
3244                    device_id = %state.device.id,
3245                    %error,
3246                    "rejected invalid SCCP station command"
3247                );
3248                Ok(false)
3249            } else {
3250                Err(error)
3251            }
3252        }
3253    }
3254}
3255
3256async fn handle_session_deadlines(
3257    stream: &mut dyn StationIo,
3258    state: &mut SessionState,
3259    context: &SessionContext,
3260    now: Instant,
3261) -> Result<(), ServerError> {
3262    for expired in expire_handset_acknowledgements(&mut state.calls_by_id, now) {
3263        let event = match expired {
3264            ExpiredHandsetAcknowledgement::Receive { call_id } => {
3265                DeviceEventKind::HandsetAcknowledgementTimedOut {
3266                    call_id,
3267                    acknowledgement: HandsetAcknowledgement::OpenReceiveChannel,
3268                }
3269            }
3270            ExpiredHandsetAcknowledgement::Transmit { call_id, endpoint } => {
3271                DeviceEventKind::TransmitChannelOpen {
3272                    call_id,
3273                    outcome: TransmitOpenOutcome::NotReported,
3274                    endpoint,
3275                }
3276            }
3277        };
3278        context
3279            .event_tx
3280            .send(Event::device(
3281                state.device.id.clone(),
3282                state.generation,
3283                event,
3284            ))
3285            .await
3286            .map_err(|_| ServerError::Stopped)?;
3287    }
3288    for (key, stop) in expire_multicast_reception_acknowledgements(state, now) {
3289        send_message(stream, &stop, state.registration.protocol).await?;
3290        context
3291            .event_tx
3292            .send(Event::device(
3293                state.device.id.clone(),
3294                state.generation,
3295                DeviceEventKind::MulticastReceptionTimedOut {
3296                    conference_id: key.conference_id,
3297                    call_id: key.call_id,
3298                },
3299            ))
3300            .await
3301            .map_err(|_| ServerError::Stopped)?;
3302    }
3303    for expired in expire_multimedia_receive_acknowledgements(state, now) {
3304        send_message(stream, &expired.close, state.registration.protocol).await?;
3305        context
3306            .event_tx
3307            .send(Event::device(
3308                state.device.id.clone(),
3309                state.generation,
3310                DeviceEventKind::MultimediaReceiveChannelTimedOut {
3311                    call_id: expired.call_id,
3312                    codec: expired.codec,
3313                    passthrough_party_id: expired.passthrough_party_id,
3314                },
3315            ))
3316            .await
3317            .map_err(|_| ServerError::Stopped)?;
3318    }
3319    for expired in expire_multimedia_transmit_acknowledgements(state, now) {
3320        send_message(stream, &expired.stop, state.registration.protocol).await?;
3321        context
3322            .event_tx
3323            .send(Event::device(
3324                state.device.id.clone(),
3325                state.generation,
3326                DeviceEventKind::MultimediaTransmitTimedOut {
3327                    call_id: expired.call_id,
3328                    codec: expired.codec,
3329                    passthrough_party_id: expired.passthrough_party_id,
3330                },
3331            ))
3332            .await
3333            .map_err(|_| ServerError::Stopped)?;
3334    }
3335    if let Some(pending) = state
3336        .pending_media_path_release
3337        .filter(|pending| pending.deadline <= now)
3338    {
3339        state.pending_media_path_release = None;
3340        let still_released = state.media_path_states.get(&pending.path)
3341            == Some(&crate::message::values::MediaPathEvent::Off)
3342            && !has_active_media_path(state)
3343            && active_media_path_call(state) == Some(pending.call_id);
3344        if still_released && let Some(call) = state.calls_by_id.get(&pending.call_id).cloned() {
3345            debug!(
3346                device_id = %state.device.id,
3347                call_id = ?call.call_id,
3348                path = ?pending.path,
3349                "completing unpaired media-path release as OnHook"
3350            );
3351            let line_instance = call.line_instance;
3352            complete_on_hook(stream, state, context, call, line_instance).await?;
3353        }
3354    }
3355    prune_connection_statistics(&mut state.pending_connection_statistics, now);
3356    Ok(())
3357}
3358
3359fn expire_handset_acknowledgements(
3360    calls_by_id: &mut HashMap<CallId, SessionCall>,
3361    now: Instant,
3362) -> Vec<ExpiredHandsetAcknowledgement> {
3363    let mut calls = calls_by_id.keys().copied().collect::<Vec<_>>();
3364    calls.sort_unstable_by_key(|call_id| call_id.0);
3365    let mut expired = Vec::new();
3366    for call_id in calls {
3367        let call = calls_by_id
3368            .get_mut(&call_id)
3369            .expect("call identifier came from session state");
3370        if call.media.receive.state == MediaChannelState::Opening
3371            && call
3372                .media
3373                .receive
3374                .deadline
3375                .is_some_and(|deadline| deadline <= now)
3376        {
3377            call.media.receive.state = MediaChannelState::Closed;
3378            call.media.receive.deadline = None;
3379            call.media.receive.peer = None;
3380            if call.media.coupled_transmit_endpoint.take().is_some() {
3381                call.media.transmit.state = MediaChannelState::Closed;
3382                call.media.transmit.deadline = None;
3383                call.media.transmit.peer = None;
3384                call.media.transmit_confirmation = TransmitConfirmation::Inactive;
3385            }
3386            expired.push(ExpiredHandsetAcknowledgement::Receive { call_id });
3387        }
3388        if matches!(
3389            call.media.transmit_confirmation,
3390            TransmitConfirmation::Awaiting { deadline } if deadline <= now
3391        ) {
3392            call.media.transmit_confirmation = TransmitConfirmation::NotReported;
3393            if let Some(endpoint) = call.media.transmit.peer {
3394                expired.push(ExpiredHandsetAcknowledgement::Transmit { call_id, endpoint });
3395            }
3396        }
3397    }
3398    expired
3399}
3400
3401#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3402enum ExpiredHandsetAcknowledgement {
3403    Receive {
3404        call_id: CallId,
3405    },
3406    Transmit {
3407        call_id: CallId,
3408        endpoint: MediaEndpoint,
3409    },
3410}
3411
3412async fn handle_pre_registration_message(
3413    stream: &mut dyn StationIo,
3414    message: ClientMessage,
3415    context: &SessionContext,
3416) -> Result<(), ServerError> {
3417    match message {
3418        ClientMessage::KeepAlive => {
3419            send_message(stream, &ServerMessage::KeepAliveAck, ProtocolVersion::V3).await?;
3420        }
3421        ClientMessage::RegisterToken(token) => {
3422            let definition = context
3423                .definitions
3424                .read()
3425                .expect("SCCP definitions lock poisoned")
3426                .get(&token.device_id)
3427                .cloned();
3428            let configured = definition.is_some()
3429                || context
3430                    .anonymous_hotline
3431                    .read()
3432                    .expect("SCCP anonymous-hotline lock poisoned")
3433                    .is_some();
3434            let transport_permitted = definition.as_ref().is_none_or(|definition| {
3435                transport_allowed(definition.transport, context.transport)
3436            });
3437            let already_registered = context.sessions.lock().await.contains_key(&token.device_id);
3438            let accept = configured
3439                && transport_permitted
3440                && !already_registered
3441                && context.config.registration_tokens.accepts(&token.device_id);
3442            let response = if accept {
3443                ServerMessage::RegisterTokenAck
3444            } else {
3445                ServerMessage::RegisterTokenReject {
3446                    backoff_seconds: u32::try_from(
3447                        context.config.registration_tokens.backoff.as_secs(),
3448                    )
3449                    .unwrap_or(u32::MAX),
3450                }
3451            };
3452            send_message(stream, &response, ProtocolVersion::V17).await?;
3453        }
3454        ClientMessage::Alarm {
3455            severity,
3456            text,
3457            parameters,
3458        } => {
3459            debug!(peer = %context.peer, ?severity, %text, ?parameters, "pre-registration SCCP alarm");
3460        }
3461        ClientMessage::XmlAlarm(message) => match parse_phone_alarm(message.xml_bytes()) {
3462            Ok(telemetry) => {
3463                debug!(
3464                    peer = %context.peer,
3465                    payload_len = message.xml_bytes().len(),
3466                    summary = ?telemetry.summary(),
3467                    opaque = telemetry.is_opaque(),
3468                    "pre-registration SCCP XML alarm"
3469                );
3470            }
3471            Err(error) => {
3472                warn!(
3473                    peer = %context.peer,
3474                    payload_len = message.xml_bytes().len(),
3475                    %error,
3476                    "rejected pre-registration SCCP XML alarm"
3477                );
3478            }
3479        },
3480        ClientMessage::LocationInfo { xml } => match parse_phone_location(xml.as_bytes()) {
3481            Ok(telemetry) => {
3482                debug!(
3483                    peer = %context.peer,
3484                    payload_len = xml.len(),
3485                    summary = ?telemetry.summary(),
3486                    opaque = telemetry.is_opaque(),
3487                    "pre-registration SCCP location information"
3488                );
3489            }
3490            Err(error) => {
3491                warn!(
3492                    peer = %context.peer,
3493                    payload_len = xml.len(),
3494                    %error,
3495                    "rejected pre-registration SCCP location information"
3496                );
3497            }
3498        },
3499        message @ (ClientMessage::MediaPortList(_) | ClientMessage::SpcpRegisterToken(_)) => {
3500            debug!(peer = %context.peer, message = ?message, "pre-registration deferred SCCP message");
3501        }
3502        ClientMessage::KnownOpaque(message) => {
3503            debug!(peer = %context.peer, message = ?message, "pre-registration deferred SCCP message");
3504        }
3505        ClientMessage::Unknown(message) => {
3506            warn!(peer = %context.peer, message = ?message, "pre-registration unknown SCCP message");
3507        }
3508        ClientMessage::Register(_)
3509        | ClientMessage::IpPort { .. }
3510        | ClientMessage::KeypadButton { .. }
3511        | ClientMessage::EnblocCall { .. }
3512        | ClientMessage::Stimulus { .. }
3513        | ClientMessage::OffHook { .. }
3514        | ClientMessage::OnHook { .. }
3515        | ClientMessage::OffHookWithCallingParty { .. }
3516        | ClientMessage::LineStatRequest { .. }
3517        | ClientMessage::ConfigStatRequest
3518        | ClientMessage::TimeDateRequest
3519        | ClientMessage::ButtonTemplateRequest
3520        | ClientMessage::VersionRequest
3521        | ClientMessage::CapabilitiesResponse(_)
3522        | ClientMessage::CapabilitiesUpdate(_)
3523        | ClientMessage::OpenMultimediaReceiveChannelAck(_)
3524        | ClientMessage::ServerRequest
3525        | ClientMessage::MulticastMediaReceptionAck { .. }
3526        | ClientMessage::OpenReceiveChannelAck { .. }
3527        | ClientMessage::SoftKeySetRequest
3528        | ClientMessage::SoftKeyTemplateRequest
3529        | ClientMessage::SoftKeyEvent { .. }
3530        | ClientMessage::Unregister { .. }
3531        | ClientMessage::HookFlash { .. }
3532        | ClientMessage::ForwardStatusRequest { .. }
3533        | ClientMessage::SpeedDialStatusRequest { .. }
3534        | ClientMessage::ConnectionStatisticsResponse(_)
3535        | ClientMessage::HeadsetStatus { .. }
3536        | ClientMessage::MediaResourceNotification(_)
3537        | ClientMessage::MediaPathEvent { .. }
3538        | ClientMessage::MediaPathCapability { .. }
3539        | ClientMessage::MediaTransmissionFailure { .. }
3540        | ClientMessage::RegisterAvailableLines { .. }
3541        | ClientMessage::ServiceUrlStatusRequest { .. }
3542        | ClientMessage::FeatureStatusRequest { .. }
3543        | ClientMessage::StartMediaTransmissionAck(_)
3544        | ClientMessage::StartMultimediaTransmissionAck(_)
3545        | ClientMessage::ExtensionDeviceCapabilities(_)
3546        | ClientMessage::DeviceToUserData(_)
3547        | ClientMessage::DeviceToUserDataResponse(_)
3548        | ClientMessage::DeviceToUserDataV1(_)
3549        | ClientMessage::DeviceToUserDataResponseV1(_)
3550        | ClientMessage::PortResponse(_)
3551        | ClientMessage::SubscriptionStatusRequest(_)
3552        | ClientMessage::SubscribeDtmfPayloadResponse(_)
3553        | ClientMessage::UnsubscribeDtmfPayloadResponse(_)
3554        | ClientMessage::CallCountRequest(_)
3555        | ClientMessage::CreateConferenceResponse(_)
3556        | ClientMessage::DeleteConferenceResponse { .. }
3557        | ClientMessage::ModifyConferenceResponse(_)
3558        | ClientMessage::AuditConferenceResponse(_)
3559        | ClientMessage::AddParticipantResponse(_)
3560        | ClientMessage::AuditParticipantResponse(_) => {
3561            warn!(peer = %context.peer, message = ?message, "ignoring SCCP message before registration");
3562        }
3563    }
3564    Ok(())
3565}
3566
3567async fn handle_client_message(
3568    stream: &mut dyn StationIo,
3569    state: &mut SessionState,
3570    message: ClientMessage,
3571    context: &SessionContext,
3572) -> Result<(), ServerError> {
3573    let protocol = state.registration.protocol;
3574    match message {
3575        ClientMessage::KeepAlive => {
3576            send_message(stream, &ServerMessage::KeepAliveAck, protocol).await?
3577        }
3578        ClientMessage::CapabilitiesResponse(capabilities) => {
3579            let capabilities = StationMediaCapabilities::from(capabilities);
3580            state.media_capabilities.clone_from(&capabilities);
3581            context
3582                .event_tx
3583                .send(Event::device(
3584                    state.device.id.clone(),
3585                    state.generation,
3586                    DeviceEventKind::Capabilities { capabilities },
3587                ))
3588                .await
3589                .map_err(|_| ServerError::Stopped)?;
3590        }
3591        ClientMessage::CapabilitiesUpdate(update) => {
3592            let capabilities = update.into_media_capabilities();
3593            state.media_capabilities.clone_from(&capabilities);
3594            context
3595                .event_tx
3596                .send(Event::device(
3597                    state.device.id.clone(),
3598                    state.generation,
3599                    DeviceEventKind::Capabilities { capabilities },
3600                ))
3601                .await
3602                .map_err(|_| ServerError::Stopped)?;
3603        }
3604        ClientMessage::ConfigStatRequest => {
3605            send_station_ui_message(
3606                stream,
3607                state,
3608                &ServerMessage::ConfigStatus(crate::message::ConfigurationStatus {
3609                    device_name: state.device.id.as_str().to_owned(),
3610                    station_user_id: 0,
3611                    station_instance: 1,
3612                    user_name: state.device.description.clone(),
3613                    server_name: context.config.server_name.clone(),
3614                    line_count: state.device.line_count() as u32,
3615                    speed_dial_count: 0,
3616                }),
3617            )
3618            .await?;
3619        }
3620        ClientMessage::LineStatRequest { line_instance } => {
3621            if let Some(message) = line_status(&state.device, line_instance) {
3622                send_station_ui_message(stream, state, &message).await?;
3623            }
3624        }
3625        ClientMessage::ButtonTemplateRequest => {
3626            send_button_template(stream, &state.device, protocol).await?;
3627        }
3628        ClientMessage::VersionRequest => {
3629            send_message(
3630                stream,
3631                &ServerMessage::Version {
3632                    firmware: context.config.firmware_version.clone(),
3633                },
3634                protocol,
3635            )
3636            .await?;
3637        }
3638        ClientMessage::ServerRequest => {
3639            send_message(
3640                stream,
3641                &ServerMessage::ServerResponse {
3642                    servers: server_response_endpoints(context, protocol)?,
3643                },
3644                protocol,
3645            )
3646            .await?;
3647        }
3648        ClientMessage::TimeDateRequest => {
3649            send_message(
3650                stream,
3651                &time_date_message(context.config.timezone_offset_minutes),
3652                protocol,
3653            )
3654            .await?
3655        }
3656        ClientMessage::SoftKeyTemplateRequest => {
3657            send_message(
3658                stream,
3659                &ServerMessage::SoftKeyTemplate {
3660                    actions: state.device.soft_keys.template_actions(),
3661                },
3662                protocol,
3663            )
3664            .await?
3665        }
3666        ClientMessage::SoftKeySetRequest => {
3667            send_message(
3668                stream,
3669                &ServerMessage::SoftKeySet {
3670                    profile: state.device.soft_keys.clone(),
3671                },
3672                protocol,
3673            )
3674            .await?
3675        }
3676        ClientMessage::ForwardStatusRequest { line_instance } => {
3677            let forwarding = state
3678                .forwarding_by_line
3679                .get(&line_instance)
3680                .cloned()
3681                .unwrap_or_default();
3682            send_message(
3683                stream,
3684                &ServerMessage::ForwardStatus {
3685                    line_instance,
3686                    forward_all: forwarding.all,
3687                    forward_busy: forwarding.busy,
3688                    forward_no_answer: forwarding.no_answer,
3689                },
3690                protocol,
3691            )
3692            .await?;
3693        }
3694        ClientMessage::SpeedDialStatusRequest {
3695            speed_dial_instance,
3696        } => {
3697            send_station_ui_message(
3698                stream,
3699                state,
3700                &speed_dial_status(&state.device, speed_dial_instance),
3701            )
3702            .await?;
3703        }
3704        ClientMessage::FeatureStatusRequest {
3705            index,
3706            capabilities,
3707        } => {
3708            if let Some(mut message) = feature_status(&state.device, index, capabilities) {
3709                apply_cached_feature_projection(&state.feature_states, index, &mut message);
3710                send_station_ui_message(stream, state, &message).await?;
3711            }
3712        }
3713        ClientMessage::ServiceUrlStatusRequest { index } => {
3714            if let Some(message) = service_url_status(&state.device, index) {
3715                send_station_ui_message(stream, state, &message).await?;
3716            }
3717        }
3718        ClientMessage::SubscriptionStatusRequest(request) => {
3719            send_message(
3720                stream,
3721                &ServerMessage::SubscriptionStatus {
3722                    transaction_id: request.transaction_id,
3723                    feature_id: request.feature_id,
3724                    timer_seconds: 0,
3725                    cause: SubscriptionCause::RouteFailure,
3726                },
3727                protocol,
3728            )
3729            .await?;
3730        }
3731        ClientMessage::RegisterAvailableLines { .. } => {
3732            debug!(device_id = %state.device.id, "phone finished registering available lines");
3733        }
3734        ClientMessage::OffHook {
3735            line_instance,
3736            call_reference,
3737        } => {
3738            if let Some(active_call) = find_call(state, call_reference)
3739                && !matches!(
3740                    active_call.state,
3741                    CallState::RingIn | CallState::CallWaiting | CallState::OnHook
3742                )
3743            {
3744                debug!(
3745                    device_id = %state.device.id,
3746                    call_id = ?active_call.call_id,
3747                    call_state = ?active_call.state,
3748                    line_instance,
3749                    call_reference,
3750                    "ignoring duplicate OffHook while a call is already active"
3751                );
3752                return Ok(());
3753            }
3754            let line = normalize_line(state, line_instance);
3755            let answer = find_answer_call(
3756                state,
3757                call_reference,
3758                line_instance,
3759                *context
3760                    .call_answer_order
3761                    .read()
3762                    .expect("SCCP call-answer-order lock poisoned"),
3763            )
3764            .cloned();
3765            let answering = answer.is_some();
3766            let call = answer.unwrap_or_else(|| {
3767                ensure_phone_call(state, call_reference, line, &context.next_call_id)
3768            });
3769            if let Some(stored) = state.calls_by_id.get_mut(&call.call_id) {
3770                stored.state = CallState::OffHook;
3771            }
3772            state.active_call_id = Some(call.call_id);
3773            if answering {
3774                begin_answer_ui(stream, &call, protocol).await?;
3775            } else {
3776                state.active_key_mode = KeyMode::OffHook;
3777                begin_phone_call_ui(stream, &call, &state.device, state.station_context()).await?;
3778            }
3779            context
3780                .event_tx
3781                .send(Event::device(
3782                    state.device.id.clone(),
3783                    state.generation,
3784                    DeviceEventKind::OffHook {
3785                        call_id: call.call_id,
3786                        line_instance: LineInstance::new(line),
3787                    },
3788                ))
3789                .await
3790                .map_err(|_| ServerError::Stopped)?;
3791        }
3792        ClientMessage::OnHook {
3793            line_instance,
3794            call_reference,
3795        } => {
3796            state.pending_media_path_release = None;
3797            if let Some(call) = find_call(state, call_reference).cloned() {
3798                let line = if line_instance == 0 {
3799                    call.line_instance
3800                } else {
3801                    line_instance
3802                };
3803                complete_on_hook(stream, state, context, call, line).await?;
3804            }
3805        }
3806        ClientMessage::HookFlash {
3807            line_instance,
3808            call_reference,
3809        } => {
3810            let line_instance = normalize_line(state, line_instance);
3811            let call_id = find_call(state, call_reference).map(|call| call.call_id);
3812            context
3813                .event_tx
3814                .send(Event::device(
3815                    state.device.id.clone(),
3816                    state.generation,
3817                    DeviceEventKind::HookFlash {
3818                        call_id,
3819                        line_instance: LineInstance::new(line_instance),
3820                    },
3821                ))
3822                .await
3823                .map_err(|_| ServerError::Stopped)?;
3824        }
3825        ClientMessage::KeypadButton {
3826            button,
3827            call_reference,
3828            ..
3829        } => {
3830            if let Some(call) = find_call(state, call_reference) {
3831                if matches!(button, Digit::Unknown(_)) {
3832                    return Ok(());
3833                }
3834                let call = call.clone();
3835                if matches!(
3836                    call.state,
3837                    CallState::Connected
3838                        | CallState::Hold
3839                        | CallState::HoldYellow
3840                        | CallState::HoldRed
3841                ) && call.media.transmit.state.is_open()
3842                    && call.media.transmit.telephone_event_payload != 0
3843                {
3844                    // The handset sends connected-call digits in RTP when a
3845                    // telephone-event payload was negotiated. Forwarding the
3846                    // signaling copy would produce duplicate DTMF in the PBX.
3847                    return Ok(());
3848                }
3849                let collecting = matches!(call.state, CallState::OffHook | CallState::Transfer);
3850                if collecting && state.active_key_mode != KeyMode::DigitsFollowing {
3851                    state.active_key_mode = KeyMode::DigitsFollowing;
3852                    send_message(
3853                        stream,
3854                        &ServerMessage::StopTone {
3855                            line_instance: call.line_instance,
3856                            call_reference: call.wire_reference,
3857                        },
3858                        protocol,
3859                    )
3860                    .await?;
3861                    send_message(
3862                        stream,
3863                        &ServerMessage::SelectSoftKeys {
3864                            line_instance: call.line_instance,
3865                            call_reference: call.wire_reference,
3866                            set: KeyMode::DigitsFollowing,
3867                            valid_mask: state.device.soft_keys.valid_mask(KeyMode::DigitsFollowing),
3868                        },
3869                        protocol,
3870                    )
3871                    .await?;
3872                }
3873                if collecting && let Some(character) = digit_character(button) {
3874                    let number = if let Some(stored) = state.calls_by_id.get_mut(&call.call_id) {
3875                        stored.dialed_number.push(character);
3876                        stored.dialed_number.clone()
3877                    } else {
3878                        String::new()
3879                    };
3880                    if button == context.config.dial_terminator {
3881                        remember_last_number(state, call.line_instance, &number, &context.config);
3882                    }
3883                }
3884                context
3885                    .event_tx
3886                    .send(Event::device(
3887                        state.device.id.clone(),
3888                        state.generation,
3889                        DeviceEventKind::Digit {
3890                            call_id: call.call_id,
3891                            digit: button,
3892                        },
3893                    ))
3894                    .await
3895                    .map_err(|_| ServerError::Stopped)?;
3896            }
3897        }
3898        ClientMessage::EnblocCall {
3899            called_party,
3900            line_instance,
3901            ..
3902        } => {
3903            let line = normalize_line(state, line_instance);
3904            let existing = state
3905                .calls_by_id
3906                .values()
3907                .find(|call| call.line_instance == line && call.state != CallState::OnHook)
3908                .cloned();
3909            let created = existing.is_none();
3910            let call = existing
3911                .unwrap_or_else(|| ensure_phone_call(state, 0, line, &context.next_call_id));
3912            if created {
3913                state.active_key_mode = KeyMode::OffHook;
3914                begin_phone_call_ui(stream, &call, &state.device, state.station_context()).await?;
3915                context
3916                    .event_tx
3917                    .send(Event::device(
3918                        state.device.id.clone(),
3919                        state.generation,
3920                        DeviceEventKind::OffHook {
3921                            call_id: call.call_id,
3922                            line_instance: LineInstance::new(line),
3923                        },
3924                    ))
3925                    .await
3926                    .map_err(|_| ServerError::Stopped)?;
3927            }
3928            if let Some(stored) = state.calls_by_id.get_mut(&call.call_id) {
3929                stored.dialed_number.clone_from(&called_party);
3930            }
3931            remember_last_number(state, call.line_instance, &called_party, &context.config);
3932            context
3933                .event_tx
3934                .send(Event::device(
3935                    state.device.id.clone(),
3936                    state.generation,
3937                    DeviceEventKind::EnblocCall {
3938                        call_id: call.call_id,
3939                        line_instance: LineInstance::new(line),
3940                        number: called_party,
3941                    },
3942                ))
3943                .await
3944                .map_err(|_| ServerError::Stopped)?;
3945        }
3946        ClientMessage::SoftKeyEvent {
3947            event,
3948            line_instance,
3949            call_reference,
3950        } => {
3951            let received_soft_key = SoftKey::from(event);
3952            if !state
3953                .device
3954                .soft_keys
3955                .allows(state.active_key_mode, received_soft_key)
3956            {
3957                debug!(
3958                    device_id = %state.device.id,
3959                    mode = state.active_key_mode.wire_value(),
3960                    event,
3961                    "ignoring unavailable soft-key event"
3962                );
3963                return Ok(());
3964            }
3965            let line = normalize_line(state, line_instance);
3966            let mut soft_key = received_soft_key;
3967            let ringing_call = find_answer_call(
3968                state,
3969                call_reference,
3970                line_instance,
3971                *context
3972                    .call_answer_order
3973                    .read()
3974                    .expect("SCCP call-answer-order lock poisoned"),
3975            );
3976            let mut call_id = if matches!(soft_key, SoftKey::Answer | SoftKey::NewCall)
3977                && let Some(call) = ringing_call
3978            {
3979                soft_key = SoftKey::Answer;
3980                Some(call.call_id)
3981            } else {
3982                find_call(state, call_reference).map(|call| call.call_id)
3983            };
3984            if soft_key == SoftKey::MeetMe
3985                && call_id.is_some_and(|call_id| {
3986                    state
3987                        .calls_by_id
3988                        .get(&call_id)
3989                        .is_some_and(|call| call.state != CallState::OffHook)
3990                })
3991            {
3992                call_id = None;
3993            }
3994            if matches!(
3995                soft_key,
3996                SoftKey::NewCall | SoftKey::Pickup | SoftKey::GroupPickup | SoftKey::MeetMe
3997            ) && call_id.is_some_and(|call_id| {
3998                state
3999                    .calls_by_id
4000                    .get(&call_id)
4001                    .is_some_and(|call| call.state == CallState::OnHook)
4002            }) {
4003                call_id = None;
4004            }
4005            if soft_key == SoftKey::Redial {
4006                begin_redial(stream, state, context, line, call_id).await?;
4007                return Ok(());
4008            }
4009            if call_id.is_none()
4010                && matches!(
4011                    soft_key,
4012                    SoftKey::NewCall | SoftKey::Pickup | SoftKey::GroupPickup | SoftKey::MeetMe
4013                )
4014            {
4015                let call = if soft_key == SoftKey::MeetMe {
4016                    reserve_phone_call(state, line, &context.next_call_id)
4017                } else {
4018                    ensure_phone_call(state, 0, line, &context.next_call_id)
4019                };
4020                state.active_call_id = Some(call.call_id);
4021                state.active_key_mode = KeyMode::OffHook;
4022                begin_phone_call_ui(stream, &call, &state.device, state.station_context()).await?;
4023                context
4024                    .event_tx
4025                    .send(Event::device(
4026                        state.device.id.clone(),
4027                        state.generation,
4028                        DeviceEventKind::OffHook {
4029                            call_id: call.call_id,
4030                            line_instance: LineInstance::new(line),
4031                        },
4032                    ))
4033                    .await
4034                    .map_err(|_| ServerError::Stopped)?;
4035                call_id = Some(call.call_id);
4036            }
4037            if soft_key == SoftKey::Backspace
4038                && let Some(call_id) = call_id
4039                && let Some(call) = state.calls_by_id.get_mut(&call_id)
4040            {
4041                call.dialed_number.pop();
4042                let call = call.clone();
4043                send_message(
4044                    stream,
4045                    &ServerMessage::BackspaceResponse {
4046                        line_instance: call.line_instance,
4047                        call_reference: call.wire_reference,
4048                    },
4049                    protocol,
4050                )
4051                .await?;
4052            }
4053            if soft_key == SoftKey::Dial
4054                && let Some(call) = call_id.and_then(|call_id| state.calls_by_id.get(&call_id))
4055            {
4056                let line_instance = call.line_instance;
4057                let number = call.dialed_number.clone();
4058                remember_last_number(state, line_instance, &number, &context.config);
4059            }
4060            if soft_key == SoftKey::Answer
4061                && let Some(call) = call_id.and_then(|call_id| state.calls_by_id.get_mut(&call_id))
4062            {
4063                call.state = CallState::OffHook;
4064                let call = call.clone();
4065                state.active_call_id = Some(call.call_id);
4066                begin_answer_ui(stream, &call, protocol).await?;
4067            }
4068            context
4069                .event_tx
4070                .send(Event::device(
4071                    state.device.id.clone(),
4072                    state.generation,
4073                    DeviceEventKind::SoftKey {
4074                        call_id,
4075                        line_instance: LineInstance::new(line),
4076                        soft_key,
4077                    },
4078                ))
4079                .await
4080                .map_err(|_| ServerError::Stopped)?;
4081        }
4082        ClientMessage::Stimulus {
4083            stimulus,
4084            instance,
4085            call_reference,
4086            ..
4087        } => {
4088            let mut call_id = find_call(state, call_reference).map(|call| call.call_id);
4089            if stimulus == Stimulus::MeetMeConference
4090                && call_id.is_some_and(|call_id| {
4091                    state
4092                        .calls_by_id
4093                        .get(&call_id)
4094                        .is_some_and(|call| call.state != CallState::OffHook)
4095                })
4096            {
4097                call_id = None;
4098            }
4099            if matches!(
4100                stimulus,
4101                Stimulus::Line
4102                    | Stimulus::NewCall
4103                    | Stimulus::CallPickup
4104                    | Stimulus::GroupCallPickup
4105            ) && call_id.is_some_and(|call_id| {
4106                state
4107                    .calls_by_id
4108                    .get(&call_id)
4109                    .is_some_and(|call| call.state == CallState::OnHook)
4110            }) {
4111                call_id = None;
4112            }
4113            if stimulus == Stimulus::Line {
4114                let line = normalize_line(state, instance);
4115                if call_id.is_none() {
4116                    let call = ensure_phone_call(state, 0, line, &context.next_call_id);
4117                    state.active_key_mode = KeyMode::OffHook;
4118                    begin_phone_call_ui(stream, &call, &state.device, state.station_context())
4119                        .await?;
4120                    context
4121                        .event_tx
4122                        .send(Event::device(
4123                            state.device.id.clone(),
4124                            state.generation,
4125                            DeviceEventKind::OffHook {
4126                                call_id: call.call_id,
4127                                line_instance: LineInstance::new(line),
4128                            },
4129                        ))
4130                        .await
4131                        .map_err(|_| ServerError::Stopped)?;
4132                } else {
4133                    context
4134                        .event_tx
4135                        .send(Event::device(
4136                            state.device.id.clone(),
4137                            state.generation,
4138                            DeviceEventKind::LineButton {
4139                                line_instance: LineInstance::new(line),
4140                                call_id,
4141                            },
4142                        ))
4143                        .await
4144                        .map_err(|_| ServerError::Stopped)?;
4145                }
4146            } else if matches!(stimulus, Stimulus::SpeedDial | Stimulus::BlfSpeedDial) {
4147                let number = state.device.buttons.iter().find_map(|button| match button {
4148                    ButtonDefinition::SpeedDial(speed_dial) if speed_dial.instance == instance => {
4149                        Some(speed_dial.number.clone())
4150                    }
4151                    ButtonDefinition::BlfSpeedDial(speed_dial)
4152                        if speed_dial.instance == instance =>
4153                    {
4154                        Some(speed_dial.number.clone())
4155                    }
4156                    _ => None,
4157                });
4158                let Some(number) = number else {
4159                    debug!(
4160                        device_id = %state.device.id,
4161                        instance,
4162                        "ignoring unconfigured speed-dial button stimulus"
4163                    );
4164                    return Ok(());
4165                };
4166
4167                // Reuse an existing outbound digit-collection call. Creating
4168                // a second call beside any other live call is permitted only
4169                // when the station advertised feature bit 30.
4170                let collecting_call = call_id
4171                    .and_then(|call_id| state.calls_by_id.get(&call_id))
4172                    .filter(|call| matches!(call.state, CallState::OffHook | CallState::Transfer))
4173                    .cloned();
4174                let has_live_call = state
4175                    .calls_by_id
4176                    .values()
4177                    .any(|call| call.state != CallState::OnHook);
4178                if collecting_call.is_none()
4179                    && has_live_call
4180                    && !state
4181                        .features
4182                        .contains(PhoneFeatures::MULTIPLE_ACTIVE_CALLS)
4183                {
4184                    debug!(
4185                        device_id = %state.device.id,
4186                        instance,
4187                        "ignoring speed-dial button beside an active call without multiple-active-call support"
4188                    );
4189                    return Ok(());
4190                }
4191                let (call, new_call) = collecting_call.map_or_else(
4192                    || {
4193                        let line = call_id
4194                            .and_then(|call_id| state.calls_by_id.get(&call_id))
4195                            .map_or_else(|| normalize_line(state, 0), |call| call.line_instance);
4196                        (reserve_phone_call(state, line, &context.next_call_id), true)
4197                    },
4198                    |call| (call, false),
4199                );
4200
4201                if new_call {
4202                    state.active_call_id = Some(call.call_id);
4203                    state.active_key_mode = KeyMode::OffHook;
4204                    begin_phone_call_ui(stream, &call, &state.device, state.station_context())
4205                        .await?;
4206                    context
4207                        .event_tx
4208                        .send(Event::device(
4209                            state.device.id.clone(),
4210                            state.generation,
4211                            DeviceEventKind::OffHook {
4212                                call_id: call.call_id,
4213                                line_instance: LineInstance::new(call.line_instance),
4214                            },
4215                        ))
4216                        .await
4217                        .map_err(|_| ServerError::Stopped)?;
4218                }
4219                if let Some(stored) = state.calls_by_id.get_mut(&call.call_id) {
4220                    stored.dialed_number.clone_from(&number);
4221                }
4222                let await_further_digits = state.device.ui.speed_dial_await_further_digits;
4223                if await_further_digits {
4224                    state.active_key_mode = KeyMode::DigitsFollowing;
4225                    for message in [
4226                        ServerMessage::StopTone {
4227                            line_instance: call.line_instance,
4228                            call_reference: call.wire_reference,
4229                        },
4230                        ServerMessage::DialedNumber {
4231                            number: number.clone(),
4232                            line_instance: call.line_instance,
4233                            call_reference: call.wire_reference,
4234                        },
4235                        ServerMessage::SelectSoftKeys {
4236                            line_instance: call.line_instance,
4237                            call_reference: call.wire_reference,
4238                            set: KeyMode::DigitsFollowing,
4239                            valid_mask: state.device.soft_keys.valid_mask(KeyMode::DigitsFollowing),
4240                        },
4241                    ] {
4242                        send_station_ui_message(stream, state, &message).await?;
4243                    }
4244                }
4245                context
4246                    .event_tx
4247                    .send(Event::device(
4248                        state.device.id.clone(),
4249                        state.generation,
4250                        DeviceEventKind::SpeedDial {
4251                            call_id: call.call_id,
4252                            line_instance: LineInstance::new(call.line_instance),
4253                            number,
4254                            await_further_digits,
4255                        },
4256                    ))
4257                    .await
4258                    .map_err(|_| ServerError::Stopped)?;
4259            } else if stimulus == Stimulus::ParkingLot {
4260                let configured = state.device.buttons.iter().any(|button| {
4261                    matches!(
4262                        button,
4263                        ButtonDefinition::Feature(feature)
4264                            if feature.instance == instance
4265                                && feature.feature == ButtonType::ParkingLot
4266                    )
4267                });
4268                if !configured {
4269                    debug!(
4270                        device_id = %state.device.id,
4271                        instance,
4272                        "ignoring unconfigured parking-lot button stimulus"
4273                    );
4274                    return Ok(());
4275                }
4276                let line_instance = call_id
4277                    .and_then(|call_id| state.calls_by_id.get(&call_id))
4278                    .map_or_else(|| normalize_line(state, 0), |call| call.line_instance);
4279                context
4280                    .event_tx
4281                    .send(Event::device(
4282                        state.device.id.clone(),
4283                        state.generation,
4284                        DeviceEventKind::ParkingLotButton {
4285                            instance: LineInstance::new(instance),
4286                            call_id,
4287                            line_instance: LineInstance::new(line_instance),
4288                        },
4289                    ))
4290                    .await
4291                    .map_err(|_| ServerError::Stopped)?;
4292            } else if stimulus == Stimulus::Privacy {
4293                let configured = state.device.buttons.iter().any(|button| {
4294                    matches!(
4295                        button,
4296                        ButtonDefinition::Feature(feature)
4297                            if feature.instance == instance
4298                                && feature.feature == ButtonType::Feature
4299                    )
4300                });
4301                if !configured {
4302                    debug!(
4303                        device_id = %state.device.id,
4304                        instance,
4305                        "ignoring unconfigured generic feature-button stimulus"
4306                    );
4307                    return Ok(());
4308                }
4309                context
4310                    .event_tx
4311                    .send(Event::device(
4312                        state.device.id.clone(),
4313                        state.generation,
4314                        DeviceEventKind::FeatureButton {
4315                            instance: LineInstance::new(instance),
4316                        },
4317                    ))
4318                    .await
4319                    .map_err(|_| ServerError::Stopped)?;
4320            } else if stimulus == Stimulus::DoNotDisturb {
4321                let configured = state.device.buttons.iter().any(|button| {
4322                    matches!(
4323                        button,
4324                        ButtonDefinition::Feature(feature)
4325                            if feature.instance == instance
4326                                && feature.feature == ButtonType::DoNotDisturb
4327                    )
4328                });
4329                if !configured {
4330                    debug!(
4331                        device_id = %state.device.id,
4332                        instance,
4333                        "ignoring unconfigured do-not-disturb button stimulus"
4334                    );
4335                    return Ok(());
4336                }
4337                context
4338                    .event_tx
4339                    .send(Event::device(
4340                        state.device.id.clone(),
4341                        state.generation,
4342                        DeviceEventKind::DoNotDisturbButton {
4343                            instance: LineInstance::new(instance),
4344                        },
4345                    ))
4346                    .await
4347                    .map_err(|_| ServerError::Stopped)?;
4348            } else if stimulus == Stimulus::Mobility {
4349                let configured = state.device.buttons.iter().any(|button| {
4350                    matches!(
4351                        button,
4352                        ButtonDefinition::Feature(feature)
4353                            if feature.instance == instance
4354                                && feature.feature == ButtonType::Mobility
4355                    )
4356                });
4357                if !configured {
4358                    debug!(
4359                        device_id = %state.device.id,
4360                        instance,
4361                        "ignoring unconfigured mobility button stimulus"
4362                    );
4363                    return Ok(());
4364                }
4365                context
4366                    .event_tx
4367                    .send(Event::device(
4368                        state.device.id.clone(),
4369                        state.generation,
4370                        DeviceEventKind::MobilityButton {
4371                            instance: LineInstance::new(instance),
4372                        },
4373                    ))
4374                    .await
4375                    .map_err(|_| ServerError::Stopped)?;
4376            } else if matches!(stimulus, Stimulus::Voicemail | Stimulus::Messages) {
4377                // Cisco stations report the dedicated Messages key as either
4378                // the legacy Voicemail stimulus or the newer Messages
4379                // stimulus. Unlike a programmable feature key, that physical
4380                // key is not present in the server-provided button template,
4381                // so it must not require a matching ButtonDefinition.
4382                let line = call_id
4383                    .and_then(|call_id| state.calls_by_id.get(&call_id))
4384                    .map_or_else(
4385                        || normalize_line(state, instance),
4386                        |call| call.line_instance,
4387                    );
4388                let call = call_id
4389                    .and_then(|call_id| state.calls_by_id.get(&call_id).cloned())
4390                    .unwrap_or_else(|| ensure_phone_call(state, 0, line, &context.next_call_id));
4391                if call_id.is_none() {
4392                    state.active_call_id = Some(call.call_id);
4393                    state.active_key_mode = KeyMode::OffHook;
4394                    begin_phone_call_ui(stream, &call, &state.device, state.station_context())
4395                        .await?;
4396                    context
4397                        .event_tx
4398                        .send(Event::device(
4399                            state.device.id.clone(),
4400                            state.generation,
4401                            DeviceEventKind::OffHook {
4402                                call_id: call.call_id,
4403                                line_instance: LineInstance::new(line),
4404                            },
4405                        ))
4406                        .await
4407                        .map_err(|_| ServerError::Stopped)?;
4408                }
4409                context
4410                    .event_tx
4411                    .send(Event::device(
4412                        state.device.id.clone(),
4413                        state.generation,
4414                        DeviceEventKind::VoicemailButton {
4415                            call_id: call.call_id,
4416                            line_instance: LineInstance::new(line),
4417                        },
4418                    ))
4419                    .await
4420                    .map_err(|_| ServerError::Stopped)?;
4421            } else {
4422                let line = normalize_line(state, instance);
4423                let Some(soft_key) = stimulus_soft_key(stimulus) else {
4424                    debug!(
4425                        device_id = %state.device.id,
4426                        stimulus = stimulus.wire_value(),
4427                        "ignoring stimulus without a soft-key action mapping"
4428                    );
4429                    return Ok(());
4430                };
4431                if !state
4432                    .device
4433                    .soft_keys
4434                    .allows(state.active_key_mode, soft_key)
4435                {
4436                    debug!(
4437                        device_id = %state.device.id,
4438                        mode = state.active_key_mode.wire_value(),
4439                        stimulus = stimulus.wire_value(),
4440                        "ignoring unavailable soft-key stimulus"
4441                    );
4442                    return Ok(());
4443                }
4444                if soft_key == SoftKey::Redial {
4445                    begin_redial(stream, state, context, line, call_id).await?;
4446                    return Ok(());
4447                }
4448                if matches!(
4449                    soft_key,
4450                    SoftKey::NewCall | SoftKey::Pickup | SoftKey::GroupPickup | SoftKey::MeetMe
4451                ) && call_id.is_none()
4452                {
4453                    let call = if soft_key == SoftKey::MeetMe {
4454                        reserve_phone_call(state, line, &context.next_call_id)
4455                    } else {
4456                        ensure_phone_call(state, 0, line, &context.next_call_id)
4457                    };
4458                    state.active_call_id = Some(call.call_id);
4459                    state.active_key_mode = KeyMode::OffHook;
4460                    begin_phone_call_ui(stream, &call, &state.device, state.station_context())
4461                        .await?;
4462                    context
4463                        .event_tx
4464                        .send(Event::device(
4465                            state.device.id.clone(),
4466                            state.generation,
4467                            DeviceEventKind::OffHook {
4468                                call_id: call.call_id,
4469                                line_instance: LineInstance::new(line),
4470                            },
4471                        ))
4472                        .await
4473                        .map_err(|_| ServerError::Stopped)?;
4474                    call_id = Some(call.call_id);
4475                }
4476                context
4477                    .event_tx
4478                    .send(Event::device(
4479                        state.device.id.clone(),
4480                        state.generation,
4481                        DeviceEventKind::SoftKey {
4482                            call_id,
4483                            line_instance: LineInstance::new(line),
4484                            soft_key,
4485                        },
4486                    ))
4487                    .await
4488                    .map_err(|_| ServerError::Stopped)?;
4489            }
4490        }
4491        ClientMessage::MulticastMediaReceptionAck {
4492            status,
4493            passthrough_party_id,
4494            call_reference,
4495        } => {
4496            let Some(key) =
4497                find_multicast_receive_key(state, call_reference.get(), passthrough_party_id.get())
4498            else {
4499                debug!(
4500                    device_id = %state.device.id,
4501                    "ignored stale or mismatched multicast reception acknowledgement"
4502                );
4503                return Ok(());
4504            };
4505            if status == MediaStatus::Ok {
4506                let route = {
4507                    let receive = state
4508                        .multicast
4509                        .get_mut(&key)
4510                        .and_then(|session| session.receive.as_mut())
4511                        .expect("multicast key came from current receive state");
4512                    receive.state = MulticastReceiveState::Open;
4513                    receive.route
4514                };
4515                context
4516                    .event_tx
4517                    .send(Event::device(
4518                        state.device.id.clone(),
4519                        state.generation,
4520                        DeviceEventKind::MulticastReceptionStarted {
4521                            conference_id: key.conference_id,
4522                            call_id: key.call_id,
4523                            route,
4524                        },
4525                    ))
4526                    .await
4527                    .map_err(|_| ServerError::Stopped)?;
4528            } else {
4529                if let Some(stop) = take_multicast_stop(state, key, true) {
4530                    send_message(stream, &stop, protocol).await?;
4531                }
4532                context
4533                    .event_tx
4534                    .send(Event::device(
4535                        state.device.id.clone(),
4536                        state.generation,
4537                        DeviceEventKind::MulticastReceptionFailed {
4538                            conference_id: key.conference_id,
4539                            call_id: key.call_id,
4540                            status,
4541                        },
4542                    ))
4543                    .await
4544                    .map_err(|_| ServerError::Stopped)?;
4545            }
4546        }
4547        ClientMessage::OpenReceiveChannelAck {
4548            status,
4549            address,
4550            port,
4551            call_reference,
4552            passthrough_party_id,
4553        } => {
4554            if let Some(call_id) =
4555                find_receive_media_call_id(state, call_reference, passthrough_party_id)
4556            {
4557                let call = state
4558                    .calls_by_id
4559                    .get(&call_id)
4560                    .expect("media call identifier came from session state")
4561                    .clone();
4562                if call.media.receive.state != MediaChannelState::Opening {
4563                    debug!(
4564                        device_id = %state.device.id,
4565                        call_id = ?call.call_id,
4566                        state = ?call.media.receive.state,
4567                        "ignored stale receive-channel acknowledgement"
4568                    );
4569                    return Ok(());
4570                }
4571                let endpoint = MediaEndpoint {
4572                    address,
4573                    rtp_port: port,
4574                    rtcp_port: port.saturating_add(1),
4575                    codec: call.media.codec,
4576                    packet_ms: call.media.packet_ms,
4577                    max_frames_per_packet: call.media.max_frames_per_packet,
4578                    telephone_event_payload: call.media.receive.telephone_event_payload,
4579                };
4580                let stored = state
4581                    .calls_by_id
4582                    .get_mut(&call_id)
4583                    .expect("media call identifier came from session state");
4584                let implied_transmit = if status == MediaStatus::Ok {
4585                    stored.media.receive.state = MediaChannelState::Open;
4586                    stored.media.receive.peer = Some(endpoint);
4587                    if let Some(endpoint) = stored.media.coupled_transmit_endpoint.take() {
4588                        stored.media.transmit.state = MediaChannelState::Open;
4589                        stored.media.transmit.peer = Some(endpoint);
4590                        stored.media.transmit.deadline = None;
4591                        stored.media.transmit_confirmation =
4592                            TransmitConfirmation::Settled(TransmitOpenOutcome::Implied);
4593                        Some(endpoint)
4594                    } else {
4595                        None
4596                    }
4597                } else {
4598                    stored.media.receive.state = MediaChannelState::Closed;
4599                    stored.media.receive.peer = None;
4600                    if stored.media.coupled_transmit_endpoint.take().is_some() {
4601                        stored.media.transmit.state = MediaChannelState::Closed;
4602                        stored.media.transmit.peer = None;
4603                        stored.media.transmit.deadline = None;
4604                        stored.media.transmit_confirmation = TransmitConfirmation::Inactive;
4605                    }
4606                    None
4607                };
4608                stored.media.receive.deadline = None;
4609                context
4610                    .event_tx
4611                    .send(Event::device(
4612                        state.device.id.clone(),
4613                        state.generation,
4614                        DeviceEventKind::ReceiveChannelOpened {
4615                            call_id: call.call_id,
4616                            status,
4617                            endpoint,
4618                        },
4619                    ))
4620                    .await
4621                    .map_err(|_| ServerError::Stopped)?;
4622                if let Some(endpoint) = implied_transmit {
4623                    context
4624                        .event_tx
4625                        .send(Event::device(
4626                            state.device.id.clone(),
4627                            state.generation,
4628                            DeviceEventKind::TransmitChannelOpen {
4629                                call_id: call.call_id,
4630                                outcome: TransmitOpenOutcome::Implied,
4631                                endpoint,
4632                            },
4633                        ))
4634                        .await
4635                        .map_err(|_| ServerError::Stopped)?;
4636                }
4637            }
4638        }
4639        ClientMessage::StartMediaTransmissionAck(ack) => {
4640            if let Some(call_id) = find_transmit_media_call_id(
4641                state,
4642                ack.conference_id,
4643                ack.call_reference,
4644                ack.passthrough_party_id,
4645            ) {
4646                let call = state
4647                    .calls_by_id
4648                    .get(&call_id)
4649                    .expect("media call identifier came from session state")
4650                    .clone();
4651                let Some(report_outcome) = call
4652                    .media
4653                    .transmit_confirmation
4654                    .acknowledgement_is_reportable(ack.status)
4655                else {
4656                    debug!(
4657                        device_id = %state.device.id,
4658                        call_id = ?call.call_id,
4659                        confirmation = ?call.media.transmit_confirmation,
4660                        "ignored stale transmit-channel acknowledgement"
4661                    );
4662                    return Ok(());
4663                };
4664                if call.media.transmit.state == MediaChannelState::Closed {
4665                    debug!(
4666                        device_id = %state.device.id,
4667                        call_id = ?call.call_id,
4668                        confirmation = ?call.media.transmit_confirmation,
4669                        "ignored stale transmit-channel acknowledgement"
4670                    );
4671                    return Ok(());
4672                }
4673                let endpoint = MediaEndpoint {
4674                    address: ack.address,
4675                    rtp_port: ack.port,
4676                    rtcp_port: ack.port.saturating_add(1),
4677                    codec: call.media.codec,
4678                    packet_ms: call.media.packet_ms,
4679                    max_frames_per_packet: call.media.max_frames_per_packet,
4680                    telephone_event_payload: call.media.transmit.telephone_event_payload,
4681                };
4682                let stored = state
4683                    .calls_by_id
4684                    .get_mut(&call_id)
4685                    .expect("media call identifier came from session state");
4686                let coupled = stored.media.coupled_transmit_endpoint.take().is_some();
4687                let outcome = match ack.status {
4688                    MediaStatus::Ok => {
4689                        stored.media.transmit.state = MediaChannelState::Open;
4690                        stored.media.transmit.peer = Some(endpoint);
4691                        TransmitOpenOutcome::Acknowledged
4692                    }
4693                    status => {
4694                        stored.media.transmit.state = MediaChannelState::Closed;
4695                        stored.media.transmit.peer = None;
4696                        if coupled {
4697                            stored.media.receive.state = MediaChannelState::Closed;
4698                            stored.media.receive.deadline = None;
4699                            stored.media.receive.peer = None;
4700                        }
4701                        TransmitOpenOutcome::Rejected(status)
4702                    }
4703                };
4704                stored.media.transmit.deadline = None;
4705                stored.media.transmit_confirmation = TransmitConfirmation::Settled(outcome);
4706                if report_outcome {
4707                    context
4708                        .event_tx
4709                        .send(Event::device(
4710                            state.device.id.clone(),
4711                            state.generation,
4712                            DeviceEventKind::TransmitChannelOpen {
4713                                call_id: call.call_id,
4714                                outcome,
4715                                endpoint,
4716                            },
4717                        ))
4718                        .await
4719                        .map_err(|_| ServerError::Stopped)?;
4720                }
4721            }
4722        }
4723        ClientMessage::Alarm {
4724            severity,
4725            text,
4726            parameters,
4727        } => {
4728            context
4729                .event_tx
4730                .send(Event::device(
4731                    state.device.id.clone(),
4732                    state.generation,
4733                    DeviceEventKind::Alarm {
4734                        severity,
4735                        text,
4736                        parameters,
4737                    },
4738                ))
4739                .await
4740                .map_err(|_| ServerError::Stopped)?;
4741        }
4742        ClientMessage::XmlAlarm(message) => match parse_phone_alarm(message.xml_bytes()) {
4743            Ok(telemetry) => {
4744                context
4745                    .event_tx
4746                    .send(Event::device(
4747                        state.device.id.clone(),
4748                        state.generation,
4749                        DeviceEventKind::XmlAlarm { telemetry },
4750                    ))
4751                    .await
4752                    .map_err(|_| ServerError::Stopped)?;
4753            }
4754            Err(error) => {
4755                warn!(
4756                    device_id = %state.device.id,
4757                    payload_len = message.xml_bytes().len(),
4758                    %error,
4759                    "rejected SCCP XML alarm"
4760                );
4761            }
4762        },
4763        ClientMessage::LocationInfo { xml } => match parse_phone_location(xml.as_bytes()) {
4764            Ok(telemetry) => {
4765                context
4766                    .event_tx
4767                    .send(Event::device(
4768                        state.device.id.clone(),
4769                        state.generation,
4770                        DeviceEventKind::LocationInformation { telemetry },
4771                    ))
4772                    .await
4773                    .map_err(|_| ServerError::Stopped)?;
4774            }
4775            Err(error) => {
4776                warn!(
4777                    device_id = %state.device.id,
4778                    payload_len = xml.len(),
4779                    %error,
4780                    "rejected SCCP location information"
4781                );
4782            }
4783        },
4784        ClientMessage::Unregister { .. } => {
4785            send_message(stream, &ServerMessage::UnregisterAck, protocol).await?;
4786        }
4787        ClientMessage::CallCountRequest(_) => {
4788            let response = call_count_response(&state.device)?;
4789            send_message(stream, &response, protocol).await?;
4790        }
4791        ClientMessage::ConnectionStatisticsResponse(statistics) => {
4792            collect_connection_statistics(state, statistics, context).await?;
4793        }
4794        ClientMessage::MediaTransmissionFailure {
4795            conference_id,
4796            passthrough_party_id,
4797            address,
4798            port,
4799            call_reference,
4800            status,
4801        } => {
4802            if let Some(key) = find_multicast_transmit_key(
4803                state,
4804                conference_id,
4805                call_reference,
4806                passthrough_party_id,
4807                address,
4808                port,
4809            ) {
4810                if let Some(stop) = take_multicast_stop(state, key, false) {
4811                    send_message(stream, &stop, protocol).await?;
4812                }
4813                context
4814                    .event_tx
4815                    .send(Event::device(
4816                        state.device.id.clone(),
4817                        state.generation,
4818                        DeviceEventKind::MulticastTransmissionFailed {
4819                            conference_id: key.conference_id,
4820                            call_id: key.call_id,
4821                            status,
4822                            address,
4823                            port,
4824                        },
4825                    ))
4826                    .await
4827                    .map_err(|_| ServerError::Stopped)?;
4828                return Ok(());
4829            }
4830            let Some(call_id) = find_transmit_media_call_id(
4831                state,
4832                conference_id,
4833                call_reference,
4834                passthrough_party_id,
4835            ) else {
4836                return Ok(());
4837            };
4838            let call = state
4839                .calls_by_id
4840                .get(&call_id)
4841                .expect("media call identifier came from session state")
4842                .clone();
4843            let Some(endpoint) = call.media.transmit.peer else {
4844                return Ok(());
4845            };
4846            if call.media.transmit.state != MediaChannelState::Open
4847                || (conference_id != 0 && conference_id != call.wire_reference)
4848                || endpoint.address != address
4849                || endpoint.rtp_port != port
4850            {
4851                debug!(
4852                    device_id = %state.device.id,
4853                    call_id = ?call.call_id,
4854                    "ignored stale or mismatched media-transmission failure"
4855                );
4856                return Ok(());
4857            }
4858            let stored = state
4859                .calls_by_id
4860                .get_mut(&call_id)
4861                .expect("media call identifier came from session state");
4862            stored.media.transmit.state = MediaChannelState::Closed;
4863            stored.media.transmit.peer = None;
4864            stored.media.transmit_confirmation = TransmitConfirmation::Inactive;
4865            context
4866                .event_tx
4867                .send(Event::device(
4868                    state.device.id.clone(),
4869                    state.generation,
4870                    DeviceEventKind::MediaTransmissionFailed {
4871                        call_id,
4872                        status,
4873                        endpoint,
4874                    },
4875                ))
4876                .await
4877                .map_err(|_| ServerError::Stopped)?;
4878        }
4879        ClientMessage::HeadsetStatus { enabled } => {
4880            if state.headset_enabled != enabled {
4881                state.headset_enabled = enabled;
4882                context
4883                    .event_tx
4884                    .send(Event::device(
4885                        state.device.id.clone(),
4886                        state.generation,
4887                        DeviceEventKind::HeadsetStatusChanged { enabled },
4888                    ))
4889                    .await
4890                    .map_err(|_| ServerError::Stopped)?;
4891            }
4892        }
4893        ClientMessage::MediaPathEvent {
4894            path,
4895            event: media_path_event,
4896        } => {
4897            if state.media_path_states.get(&path) != Some(&media_path_event) {
4898                state.media_path_states.insert(path, media_path_event);
4899                if media_path_event == crate::message::values::MediaPathEvent::On {
4900                    state.pending_media_path_release = None;
4901                } else if media_path_event == crate::message::values::MediaPathEvent::Off
4902                    && is_local_audio_path(path)
4903                    && !has_active_media_path(state)
4904                    && let Some(call_id) = active_media_path_call(state)
4905                {
4906                    state.pending_media_path_release = Some(PendingMediaPathRelease {
4907                        call_id,
4908                        path,
4909                        deadline: Instant::now() + MEDIA_PATH_RELEASE_GRACE,
4910                    });
4911                }
4912                context
4913                    .event_tx
4914                    .send(Event::device(
4915                        state.device.id.clone(),
4916                        state.generation,
4917                        DeviceEventKind::MediaPathChanged {
4918                            path,
4919                            event: media_path_event,
4920                        },
4921                    ))
4922                    .await
4923                    .map_err(|_| ServerError::Stopped)?;
4924            }
4925        }
4926        ClientMessage::MediaPathCapability { .. } => {}
4927        message @ (ClientMessage::IpPort { .. }
4928        | ClientMessage::OffHookWithCallingParty { .. }
4929        | ClientMessage::MediaResourceNotification(_)
4930        | ClientMessage::SubscribeDtmfPayloadResponse(_)
4931        | ClientMessage::UnsubscribeDtmfPayloadResponse(_)
4932        | ClientMessage::PortResponse(_)) => {
4933            debug!(device_id = %state.device.id, message = ?message, "consumed SCCP telemetry");
4934        }
4935        ClientMessage::DeviceToUserData(message) => {
4936            handle_phone_service_message(
4937                state,
4938                context,
4939                crate::message::wire_id::DEVICE_TO_USER_DATA,
4940                PhoneServiceMessageKind::Data,
4941                PhoneServiceRouting {
4942                    application_id: ApplicationId::new(message.application_id),
4943                    line_instance: LineInstance::new(message.line_instance),
4944                    call_reference: CallReference::new(message.call_reference),
4945                    transaction_id: TransactionId::new(message.transaction_id),
4946                },
4947                None,
4948                &message.data,
4949            )
4950            .await?;
4951        }
4952        ClientMessage::DeviceToUserDataResponse(message) => {
4953            handle_phone_service_message(
4954                state,
4955                context,
4956                crate::message::wire_id::DEVICE_TO_USER_DATA_RESPONSE,
4957                PhoneServiceMessageKind::Response,
4958                PhoneServiceRouting {
4959                    application_id: ApplicationId::new(message.application_id),
4960                    line_instance: LineInstance::new(message.line_instance),
4961                    call_reference: CallReference::new(message.call_reference),
4962                    transaction_id: TransactionId::new(message.transaction_id),
4963                },
4964                None,
4965                &message.data,
4966            )
4967            .await?;
4968        }
4969        ClientMessage::DeviceToUserDataV1(message) => {
4970            handle_phone_service_message(
4971                state,
4972                context,
4973                crate::message::wire_id::DEVICE_TO_USER_DATA_V1,
4974                PhoneServiceMessageKind::Data,
4975                PhoneServiceRouting {
4976                    application_id: ApplicationId::new(message.application_id),
4977                    line_instance: LineInstance::new(message.line_instance),
4978                    call_reference: CallReference::new(message.call_reference),
4979                    transaction_id: TransactionId::new(message.transaction_id),
4980                },
4981                Some(PhoneServiceExtendedRouting {
4982                    sequence_flag: message.sequence_flag,
4983                    display_priority: message.display_priority,
4984                    conference_id: message.conference_id,
4985                    application_instance_id: message.application_instance_id,
4986                    routing: message.routing,
4987                }),
4988                &message.data,
4989            )
4990            .await?;
4991        }
4992        ClientMessage::DeviceToUserDataResponseV1(message) => {
4993            handle_phone_service_message(
4994                state,
4995                context,
4996                crate::message::wire_id::DEVICE_TO_USER_DATA_RESPONSE_V1,
4997                PhoneServiceMessageKind::Response,
4998                PhoneServiceRouting {
4999                    application_id: ApplicationId::new(message.application_id),
5000                    line_instance: LineInstance::new(message.line_instance),
5001                    call_reference: CallReference::new(message.call_reference),
5002                    transaction_id: TransactionId::new(message.transaction_id),
5003                },
5004                Some(PhoneServiceExtendedRouting {
5005                    sequence_flag: message.sequence_flag,
5006                    display_priority: message.display_priority,
5007                    conference_id: message.conference_id,
5008                    application_instance_id: message.application_instance_id,
5009                    routing: message.routing,
5010                }),
5011                &message.data,
5012            )
5013            .await?;
5014        }
5015        ClientMessage::OpenMultimediaReceiveChannelAck(ack) => {
5016            let Some(call_id) = state.calls_by_wire.get(&ack.call_reference.get()).copied() else {
5017                debug!(device_id = %state.device.id, "ignored video receive acknowledgement for an unknown call");
5018                return Ok(());
5019            };
5020            let Some((request, codec, requested_address_type)) =
5021                state.calls_by_id.get(&call_id).and_then(|call| {
5022                    call.video_receive.leg.as_ref().and_then(|leg| {
5023                        (leg.state == MediaChannelState::Opening
5024                            && leg.request.token().get() == ack.passthrough_party_id.get())
5025                        .then_some((leg.request, leg.codec, leg.requested_address_type))
5026                    })
5027                })
5028            else {
5029                debug!(device_id = %state.device.id, ?call_id, "ignored stale video receive acknowledgement");
5030                return Ok(());
5031            };
5032
5033            let event = if ack.status == MediaStatus::Ok {
5034                if !endpoint_is_usable(ack.endpoint)
5035                    || !address_matches_type(ack.endpoint.address, requested_address_type)
5036                {
5037                    debug!(device_id = %state.device.id, ?call_id, "ignored unusable video receive endpoint");
5038                    return Ok(());
5039                }
5040                let leg = state
5041                    .calls_by_id
5042                    .get_mut(&call_id)
5043                    .and_then(|call| call.video_receive.leg.as_mut())
5044                    .expect("correlated video receive leg remains present");
5045                debug_assert_eq!(leg.request, request);
5046                leg.state = MediaChannelState::Open;
5047                leg.deadline = None;
5048                DeviceEventKind::MultimediaReceiveChannelOpened {
5049                    call_id,
5050                    codec,
5051                    endpoint: ack.endpoint,
5052                    passthrough_party_id: ack.passthrough_party_id,
5053                }
5054            } else {
5055                let close = take_multimedia_receive_close(state, call_id)
5056                    .expect("correlated video receive leg remains present");
5057                send_message(stream, &close, protocol).await?;
5058                DeviceEventKind::MultimediaReceiveChannelFailed {
5059                    call_id,
5060                    codec,
5061                    status: ack.status,
5062                    endpoint: ack.endpoint,
5063                    passthrough_party_id: ack.passthrough_party_id,
5064                }
5065            };
5066            context
5067                .event_tx
5068                .send(Event::device(
5069                    state.device.id.clone(),
5070                    state.generation,
5071                    event,
5072                ))
5073                .await
5074                .map_err(|_| ServerError::Stopped)?;
5075        }
5076        ClientMessage::StartMultimediaTransmissionAck(ack) => {
5077            let Some(call_id) = state.calls_by_wire.get(&ack.call_reference.get()).copied() else {
5078                debug!(device_id = %state.device.id, "ignored video transmit acknowledgement for an unknown call");
5079                return Ok(());
5080            };
5081            let Some((request, codec, address_type)) =
5082                state.calls_by_id.get(&call_id).and_then(|call| {
5083                    call.video_transmit.leg.as_ref().and_then(|leg| {
5084                        (leg.state == MediaChannelState::Opening
5085                            && leg.request.token().get() == ack.passthrough_party_id.get()
5086                            && leg.conference_id == ack.conference_id)
5087                            .then_some((leg.request, leg.codec, leg.address_type))
5088                    })
5089                })
5090            else {
5091                debug!(device_id = %state.device.id, ?call_id, "ignored stale video transmit acknowledgement");
5092                return Ok(());
5093            };
5094
5095            let event = if ack.status == MediaStatus::Ok {
5096                if !endpoint_is_usable(ack.endpoint)
5097                    || !address_matches_type(ack.endpoint.address, address_type)
5098                {
5099                    debug!(device_id = %state.device.id, ?call_id, "ignored unusable video transmit endpoint");
5100                    return Ok(());
5101                }
5102                let leg = state
5103                    .calls_by_id
5104                    .get_mut(&call_id)
5105                    .and_then(|call| call.video_transmit.leg.as_mut())
5106                    .expect("correlated video transmit leg remains present");
5107                debug_assert_eq!(leg.request, request);
5108                leg.state = MediaChannelState::Open;
5109                leg.deadline = None;
5110                DeviceEventKind::MultimediaTransmitStarted {
5111                    call_id,
5112                    codec,
5113                    endpoint: ack.endpoint,
5114                    passthrough_party_id: ack.passthrough_party_id,
5115                }
5116            } else {
5117                let stop = take_multimedia_transmit_stop(state, call_id)
5118                    .expect("correlated video transmit leg remains present");
5119                send_message(stream, &stop, protocol).await?;
5120                DeviceEventKind::MultimediaTransmitFailed {
5121                    call_id,
5122                    codec,
5123                    status: ack.status,
5124                    endpoint: ack.endpoint,
5125                    passthrough_party_id: ack.passthrough_party_id,
5126                }
5127            };
5128            context
5129                .event_tx
5130                .send(Event::device(
5131                    state.device.id.clone(),
5132                    state.generation,
5133                    event,
5134                ))
5135                .await
5136                .map_err(|_| ServerError::Stopped)?;
5137        }
5138        message @ (ClientMessage::MediaPortList(_)
5139        | ClientMessage::SpcpRegisterToken(_)
5140        | ClientMessage::ExtensionDeviceCapabilities(_)
5141        | ClientMessage::CreateConferenceResponse(_)
5142        | ClientMessage::DeleteConferenceResponse { .. }
5143        | ClientMessage::ModifyConferenceResponse(_)
5144        | ClientMessage::AuditConferenceResponse(_)
5145        | ClientMessage::AddParticipantResponse(_)
5146        | ClientMessage::AuditParticipantResponse(_)) => {
5147            debug!(device_id = %state.device.id, message = ?message, "deferred SCCP application message");
5148            context
5149                .event_tx
5150                .send(Event::device(
5151                    state.device.id.clone(),
5152                    state.generation,
5153                    DeviceEventKind::UnhandledMessage { message },
5154                ))
5155                .await
5156                .map_err(|_| ServerError::Stopped)?;
5157        }
5158        ClientMessage::KnownOpaque(message) => {
5159            let message = ClientMessage::KnownOpaque(message);
5160            debug!(device_id = %state.device.id, message = ?message, "unhandled SCCP message");
5161            context
5162                .event_tx
5163                .send(Event::device(
5164                    state.device.id.clone(),
5165                    state.generation,
5166                    DeviceEventKind::UnhandledMessage { message },
5167                ))
5168                .await
5169                .map_err(|_| ServerError::Stopped)?;
5170        }
5171        ClientMessage::Unknown(message) => {
5172            let message = ClientMessage::Unknown(message);
5173            warn!(device_id = %state.device.id, message = ?message, "unknown SCCP message");
5174            context
5175                .event_tx
5176                .send(Event::device(
5177                    state.device.id.clone(),
5178                    state.generation,
5179                    DeviceEventKind::UnhandledMessage { message },
5180                ))
5181                .await
5182                .map_err(|_| ServerError::Stopped)?;
5183        }
5184        ClientMessage::Register(_) | ClientMessage::RegisterToken(_) => {
5185            warn!(device_id = %state.device.id, "ignoring registration message on registered session");
5186        }
5187    }
5188    Ok(())
5189}
5190
5191const fn is_local_audio_path(path: crate::message::values::MediaPathId) -> bool {
5192    matches!(
5193        path,
5194        crate::message::values::MediaPathId::Headset
5195            | crate::message::values::MediaPathId::Handset
5196            | crate::message::values::MediaPathId::Speaker
5197    )
5198}
5199
5200fn has_active_media_path(state: &SessionState) -> bool {
5201    state.media_path_states.iter().any(|(path, event)| {
5202        is_local_audio_path(*path) && *event == crate::message::values::MediaPathEvent::On
5203    })
5204}
5205
5206fn active_media_path_call(state: &SessionState) -> Option<CallId> {
5207    state.active_call_id.filter(|call_id| {
5208        state.calls_by_id.get(call_id).is_some_and(|call| {
5209            !matches!(
5210                call.state,
5211                CallState::OnHook
5212                    | CallState::RingIn
5213                    | CallState::CallWaiting
5214                    | CallState::Hold
5215                    | CallState::HoldYellow
5216                    | CallState::HoldRed
5217            )
5218        })
5219    })
5220}
5221
5222async fn complete_on_hook(
5223    stream: &mut dyn StationIo,
5224    state: &mut SessionState,
5225    context: &SessionContext,
5226    call: SessionCall,
5227    line_instance: u32,
5228) -> Result<(), ServerError> {
5229    state.pending_media_path_release = None;
5230    let order = *context
5231        .call_answer_order
5232        .read()
5233        .expect("SCCP call-answer-order lock poisoned");
5234    let successor = incoming_successor(state, call.call_id, order);
5235    let successor_has_ringer = successor.is_some_and(|(call_id, _)| {
5236        state
5237            .calls_by_id
5238            .get(&call_id)
5239            .and_then(|call| incoming_ringer(call.ringer, CallState::RingIn))
5240            .is_some_and(ringer_is_audible)
5241    });
5242    let stop_ringer =
5243        !successor_has_ringer && state.ringer_owner.is_none_or(|owner| owner == call.call_id);
5244    context
5245        .event_tx
5246        .send(Event::device(
5247            state.device.id.clone(),
5248            state.generation,
5249            DeviceEventKind::OnHook {
5250                call_id: call.call_id,
5251                line_instance: LineInstance::new(line_instance),
5252            },
5253        ))
5254        .await
5255        .map_err(|_| ServerError::Stopped)?;
5256    state.active_key_mode = KeyMode::OnHook;
5257    stop_call_multicast(stream, state, call.call_id, state.registration.protocol).await?;
5258    close_call_media_messages(stream, &call, state.registration.protocol).await?;
5259    close_call_messages(
5260        stream,
5261        &call,
5262        &state.device.soft_keys,
5263        state.registration.protocol,
5264        context.config.timezone_offset_minutes,
5265        stop_ringer,
5266    )
5267    .await?;
5268    request_connection_statistics(stream, state, &call, context).await?;
5269    if let Some(stored) = state.calls_by_id.get_mut(&call.call_id) {
5270        stored.state = CallState::OnHook;
5271        stored.media.receive.state = MediaChannelState::Closed;
5272        stored.media.receive.deadline = None;
5273        stored.media.transmit.state = MediaChannelState::Closed;
5274        stored.media.transmit.deadline = None;
5275        stored.media.transmit_confirmation = TransmitConfirmation::Inactive;
5276        stored.media.coupled_transmit_endpoint = None;
5277        stored.video_receive.leg = None;
5278        stored.video_transmit.leg = None;
5279    }
5280    if state.active_call_id == Some(call.call_id) {
5281        state.active_call_id = None;
5282    }
5283    if state.ringer_owner == Some(call.call_id) {
5284        state.ringer_owner = None;
5285    }
5286    if let Some((call_id, promote)) = successor {
5287        present_incoming_successor(stream, state, call_id, promote).await?;
5288    }
5289    Ok(())
5290}
5291
5292fn button_template(device: &DeviceDefinition) -> Vec<ButtonTemplateEntry> {
5293    let mut buttons = Vec::with_capacity(56);
5294    let mut addon_buttons_remaining = None;
5295    for button in &device.buttons {
5296        if let ButtonDefinition::AddonModule(addon) = button {
5297            buttons.extend(std::iter::repeat_n(
5298                ButtonTemplateEntry {
5299                    instance: 0,
5300                    button_type: ButtonType::Unused,
5301                },
5302                addon_buttons_remaining.take().unwrap_or_default(),
5303            ));
5304            addon_buttons_remaining = addon.button_capacity();
5305            continue;
5306        }
5307        buttons.push(match button {
5308            ButtonDefinition::Line(appearance) => ButtonTemplateEntry {
5309                instance: appearance.instance,
5310                button_type: ButtonType::Line,
5311            },
5312            ButtonDefinition::SpeedDial(speed_dial) => ButtonTemplateEntry {
5313                instance: speed_dial.instance,
5314                button_type: ButtonType::SpeedDial,
5315            },
5316            ButtonDefinition::BlfSpeedDial(speed_dial) => ButtonTemplateEntry {
5317                instance: speed_dial.instance,
5318                button_type: ButtonType::BlfSpeedDial,
5319            },
5320            ButtonDefinition::Feature(feature) => ButtonTemplateEntry {
5321                instance: feature.instance,
5322                button_type: ButtonType::from(feature.feature.wire_value()),
5323            },
5324            ButtonDefinition::Service(service) => ButtonTemplateEntry {
5325                instance: service.instance,
5326                button_type: ButtonType::ServiceUrl,
5327            },
5328            ButtonDefinition::Unused => ButtonTemplateEntry {
5329                instance: 0,
5330                button_type: ButtonType::Unused,
5331            },
5332            ButtonDefinition::AddonModule(_) => unreachable!("addon marker handled above"),
5333        });
5334        if let Some(remaining) = &mut addon_buttons_remaining {
5335            *remaining = remaining.saturating_sub(1);
5336        }
5337    }
5338    buttons.extend(std::iter::repeat_n(
5339        ButtonTemplateEntry {
5340            instance: 0,
5341            button_type: ButtonType::Unused,
5342        },
5343        addon_buttons_remaining.unwrap_or_default(),
5344    ));
5345    buttons
5346}
5347
5348async fn send_button_template(
5349    stream: &mut dyn StationIo,
5350    device: &DeviceDefinition,
5351    session: impl Into<StationSessionContext>,
5352) -> Result<(), ServerError> {
5353    let session = session.into();
5354    for message in button_template_messages(device)? {
5355        send_message(stream, &message, session).await?;
5356    }
5357    Ok(())
5358}
5359
5360fn button_template_messages(device: &DeviceDefinition) -> Result<Vec<ServerMessage>, CodecError> {
5361    let buttons = button_template(device);
5362    let total = u32::try_from(buttons.len()).map_err(|_| {
5363        CodecError::InvalidDefinition(format!(
5364            "device {} button template is too large for SCCP",
5365            device.id
5366        ))
5367    })?;
5368    if buttons.is_empty() {
5369        return Ok(vec![ServerMessage::ButtonTemplate {
5370            offset: 0,
5371            total: 0,
5372            buttons: Vec::new(),
5373        }]);
5374    }
5375    Ok(buttons
5376        .chunks(BUTTON_TEMPLATE_ENTRIES_PER_CHUNK)
5377        .enumerate()
5378        .map(|(chunk_index, chunk)| ServerMessage::ButtonTemplate {
5379            offset: u32::try_from(chunk_index * BUTTON_TEMPLATE_ENTRIES_PER_CHUNK)
5380                .expect("validated button template offset"),
5381            total,
5382            buttons: chunk.to_vec(),
5383        })
5384        .collect())
5385}
5386
5387fn line_status(device: &DeviceDefinition, instance: u32) -> Option<ServerMessage> {
5388    device
5389        .line(instance)
5390        .map(|appearance| ServerMessage::LineStatus {
5391            instance: appearance.instance,
5392            number: appearance.line.number.clone(),
5393            display_name: appearance.display_label().to_owned(),
5394        })
5395}
5396
5397fn call_count_response(device: &DeviceDefinition) -> Result<ServerMessage, CodecError> {
5398    let lines = device.lines().collect::<Vec<_>>();
5399    let total_configured_lines = u32::try_from(lines.len()).map_err(|_| {
5400        CodecError::InvalidDefinition(format!(
5401            "device {} has too many lines for a call-count response",
5402            device.id
5403        ))
5404    })?;
5405    let starting_line_instance = lines.first().map_or(0, |line| line.instance);
5406    let line_data = lines
5407        .into_iter()
5408        .take(CALL_COUNT_RESPONSE_MAX_LINE_ENTRIES)
5409        .map(|_| CallCountLineData {
5410            max_calls: DEFAULT_MAX_CALLS_PER_LINE,
5411            busy_trigger: DEFAULT_BUSY_TRIGGER_PER_LINE,
5412        })
5413        .collect();
5414
5415    Ok(ServerMessage::CallCountResponse(CallCountResponse {
5416        total_configured_lines,
5417        starting_line_instance,
5418        line_data,
5419    }))
5420}
5421
5422fn mobility_device_candidate(
5423    current: &DeviceDefinition,
5424    current_appearances: &HashMap<u32, LineAppearance>,
5425    next_appearances: &HashMap<u32, LineAppearance>,
5426) -> Result<DeviceDefinition, CodecError> {
5427    let mut candidate = current.clone();
5428    candidate.buttons.retain(|button| {
5429        !matches!(
5430            button,
5431            ButtonDefinition::Line(line)
5432                if current_appearances.values().any(|appearance| appearance == line)
5433        )
5434    });
5435    let mut index = 0;
5436    while index < candidate.buttons.len() {
5437        let mobility_instance = match &candidate.buttons[index] {
5438            ButtonDefinition::Feature(feature) if feature.feature == ButtonType::Mobility => {
5439                Some(feature.instance)
5440            }
5441            _ => None,
5442        };
5443        if let Some(appearance) = mobility_instance
5444            .and_then(|instance| next_appearances.get(&instance))
5445            .cloned()
5446        {
5447            candidate
5448                .buttons
5449                .insert(index + 1, ButtonDefinition::Line(appearance));
5450            index += 2;
5451        } else {
5452            index += 1;
5453        }
5454    }
5455    candidate.validate()?;
5456    Ok(candidate)
5457}
5458
5459fn speed_dial_status(device: &DeviceDefinition, instance: u32) -> ServerMessage {
5460    let speed_dial = device.buttons.iter().find_map(|button| match button {
5461        ButtonDefinition::SpeedDial(speed_dial) if speed_dial.instance == instance => {
5462            Some((&speed_dial.number, &speed_dial.display_name))
5463        }
5464        _ => None,
5465    });
5466    ServerMessage::SpeedDialStatus {
5467        instance,
5468        number: speed_dial.map_or_else(String::new, |(number, _)| number.clone()),
5469        display_name: speed_dial.map_or_else(String::new, |(_, display_name)| display_name.clone()),
5470    }
5471}
5472
5473fn feature_status(
5474    device: &DeviceDefinition,
5475    instance: u32,
5476    _capabilities: u32,
5477) -> Option<ServerMessage> {
5478    if let Some(speed_dial) = device.blf_button(instance) {
5479        return Some(ServerMessage::FeatureStatus {
5480            instance,
5481            button_type: ButtonType::BlfSpeedDial,
5482            label: speed_dial.display_name.clone(),
5483            state: BusyLampFieldState::UnknownState.wire_value(),
5484        });
5485    }
5486    device
5487        .feature_button(instance)
5488        .map(|feature| ServerMessage::FeatureStatus {
5489            instance,
5490            button_type: ButtonType::from(feature.feature.wire_value()),
5491            label: feature.label.clone(),
5492            state: 0,
5493        })
5494}
5495
5496fn feature_state_messages(
5497    device: &DeviceDefinition,
5498    instance: u32,
5499    enabled: bool,
5500) -> Option<[ServerMessage; 2]> {
5501    let feature = device.feature_button(instance)?;
5502    Some([
5503        ServerMessage::FeatureStatus {
5504            instance,
5505            button_type: ButtonType::from(feature.feature.wire_value()),
5506            label: feature.label.clone(),
5507            state: u32::from(enabled),
5508        },
5509        ServerMessage::SetLamp {
5510            stimulus: feature.feature,
5511            instance,
5512            mode: if enabled { LampMode::On } else { LampMode::Off },
5513        },
5514    ])
5515}
5516
5517fn cache_feature_projection(
5518    cache: &mut HashMap<u32, SessionFeatureState>,
5519    instance: u32,
5520    message: &ServerMessage,
5521) {
5522    let ServerMessage::FeatureStatus {
5523        button_type, state, ..
5524    } = message
5525    else {
5526        debug_assert!(false, "feature projection cache requires FeatureStatus");
5527        return;
5528    };
5529    cache.insert(
5530        instance,
5531        SessionFeatureState {
5532            button_type: *button_type,
5533            state: *state,
5534        },
5535    );
5536}
5537
5538fn apply_cached_feature_projection(
5539    cache: &HashMap<u32, SessionFeatureState>,
5540    instance: u32,
5541    message: &mut ServerMessage,
5542) {
5543    let Some(cached) = cache.get(&instance) else {
5544        return;
5545    };
5546    if let ServerMessage::FeatureStatus {
5547        button_type, state, ..
5548    } = message
5549    {
5550        *button_type = cached.button_type;
5551        *state = cached.state;
5552    }
5553}
5554
5555/// Pack the three DND multiblink selectors used by v16+ stations.
5556///
5557/// The low byte selects the primary icon state, the middle byte the lamp
5558/// cadence, and the high byte the alternate state. Cisco's canonical words
5559/// are intentionally preserved rather than recomputed from boolean state.
5560const fn multiblink_dnd_state(mode: DoNotDisturbMode) -> u32 {
5561    const OFF: u32 = 0x01_00_00;
5562    const REJECT: u32 = 0x02_02_02;
5563    const SILENT: u32 = 0x03_03_02;
5564
5565    match mode {
5566        DoNotDisturbMode::Off => OFF,
5567        DoNotDisturbMode::Reject => REJECT,
5568        DoNotDisturbMode::Silent => SILENT,
5569    }
5570}
5571
5572fn do_not_disturb_state_messages(
5573    device: &DeviceDefinition,
5574    instance: u32,
5575    mode: DoNotDisturbMode,
5576    button_mode: DoNotDisturbButtonMode,
5577    protocol: ProtocolVersion,
5578) -> Option<[ServerMessage; 2]> {
5579    let feature = device
5580        .feature_button(instance)
5581        .filter(|feature| feature.feature == ButtonType::DoNotDisturb)?;
5582    let exact_enabled = match button_mode {
5583        DoNotDisturbButtonMode::Cycle => mode != DoNotDisturbMode::Off,
5584        DoNotDisturbButtonMode::Silent => mode == DoNotDisturbMode::Silent,
5585        DoNotDisturbButtonMode::Reject => mode == DoNotDisturbMode::Reject,
5586    };
5587    let multi_state =
5588        button_mode == DoNotDisturbButtonMode::Cycle && protocol > ProtocolVersion::V15;
5589    let (button_type, state) = if multi_state {
5590        (ButtonType::MultiblinkFeature, multiblink_dnd_state(mode))
5591    } else {
5592        (ButtonType::DoNotDisturb, u32::from(exact_enabled))
5593    };
5594    let lamp = match (exact_enabled, mode) {
5595        (false, _) | (_, DoNotDisturbMode::Off) => LampMode::Off,
5596        (true, DoNotDisturbMode::Silent) => LampMode::Blink,
5597        (true, DoNotDisturbMode::Reject) => LampMode::On,
5598    };
5599    Some([
5600        ServerMessage::FeatureStatus {
5601            instance,
5602            button_type,
5603            label: feature.label.clone(),
5604            state,
5605        },
5606        ServerMessage::SetLamp {
5607            stimulus: feature.feature,
5608            instance,
5609            mode: lamp,
5610        },
5611    ])
5612}
5613
5614fn blf_status_message(
5615    device: &DeviceDefinition,
5616    instance: u32,
5617    state: BlfState,
5618) -> Option<ServerMessage> {
5619    let definition = device.blf_button(instance)?;
5620    let icon = match state {
5621        BlfState::Idle => BusyLampFieldState::Idle,
5622        BlfState::Ringing => BusyLampFieldState::Alerting,
5623        BlfState::Busy | BlfState::Held => BusyLampFieldState::InUse,
5624        BlfState::DoNotDisturb => BusyLampFieldState::DoNotDisturb,
5625        BlfState::Unavailable | BlfState::Unknown => BusyLampFieldState::UnknownState,
5626    };
5627    Some(ServerMessage::FeatureStatus {
5628        instance,
5629        button_type: ButtonType::BlfSpeedDial,
5630        label: definition.display_name.clone(),
5631        state: icon.wire_value(),
5632    })
5633}
5634
5635fn hinted_ringing_notification(
5636    device: &DeviceDefinition,
5637    label: &str,
5638    caller: Option<&BlfCallerInfo>,
5639    state: BlfState,
5640) -> Option<HandsetStatusMessage> {
5641    if !device.ui.hinted_ringing_notification || state != BlfState::Ringing {
5642        return None;
5643    }
5644    let caller = caller.map(BlfCallerInfo::display).unwrap_or_default();
5645    let text = if caller.is_empty() {
5646        format!("{label} is ringing")
5647    } else {
5648        format!("{label} is ringing: {caller}")
5649    };
5650    Some(HandsetStatusMessage::Display {
5651        text: truncate_utf8(&text, 79),
5652        timeout_seconds: 5,
5653        priority: None,
5654    })
5655}
5656
5657fn reconcile_blf_alert(
5658    instance: u32,
5659    notification: Option<HandsetStatusMessage>,
5660    active: &mut BTreeMap<u32, HandsetStatusMessage>,
5661    visible: &mut Option<HandsetStatusMessage>,
5662) -> Option<HandsetStatusMessage> {
5663    match notification {
5664        Some(notification) => {
5665            active.insert(instance, notification);
5666        }
5667        None => {
5668            active.remove(&instance);
5669        }
5670    }
5671    let next = active.first_key_value().map(|(_, message)| message.clone());
5672    if *visible == next {
5673        return None;
5674    }
5675    *visible = next.clone();
5676    Some(next.unwrap_or(HandsetStatusMessage::Clear { priority: None }))
5677}
5678
5679fn truncate_utf8(value: &str, maximum_bytes: usize) -> String {
5680    if value.len() <= maximum_bytes {
5681        return value.to_owned();
5682    }
5683    let end = value
5684        .char_indices()
5685        .map(|(index, _)| index)
5686        .take_while(|index| *index <= maximum_bytes)
5687        .last()
5688        .unwrap_or(0);
5689    value[..end].to_owned()
5690}
5691
5692fn service_url_status(device: &DeviceDefinition, index: u32) -> Option<ServerMessage> {
5693    device.buttons.iter().find_map(|button| match button {
5694        ButtonDefinition::Service(service) if service.instance == index => {
5695            Some(ServerMessage::ServiceUrlStatus {
5696                index,
5697                url: service.url.clone(),
5698                label: service.label.clone(),
5699                extension_text: String::new(),
5700            })
5701        }
5702        _ => None,
5703    })
5704}
5705
5706const fn key_mode_for_call_state(state: CallState) -> KeyMode {
5707    match state {
5708        CallState::Connected => KeyMode::Connected,
5709        CallState::Hold | CallState::HoldYellow | CallState::HoldRed => KeyMode::OnHold,
5710        CallState::RingIn | CallState::CallWaiting => KeyMode::RingIn,
5711        CallState::OffHook
5712        | CallState::Busy
5713        | CallState::Congestion
5714        | CallState::InvalidNumber
5715        | CallState::IntercomOneWay => KeyMode::OffHook,
5716        CallState::Transfer => KeyMode::ConnectedTransfer,
5717        CallState::RingOut | CallState::Proceed => KeyMode::RingOut,
5718        CallState::RemoteMultiline => KeyMode::OnHookStealable,
5719        CallState::OnHook | CallState::Park | CallState::Unknown(_) => KeyMode::OnHook,
5720    }
5721}
5722
5723fn transfer_key_mode(call: &SessionCall, state: CallState) -> KeyMode {
5724    if matches!(
5725        call.transfer_role,
5726        Some(SessionTransferRole::Consultation { .. })
5727    ) && matches!(state, CallState::RingOut | CallState::Connected)
5728    {
5729        KeyMode::ConnectedTransfer
5730    } else {
5731        key_mode_for_call_state(state)
5732    }
5733}
5734
5735fn stimulus_soft_key(stimulus: Stimulus) -> Option<SoftKey> {
5736    Some(match stimulus {
5737        Stimulus::LastNumberRedial => SoftKey::Redial,
5738        Stimulus::Hold => SoftKey::Hold,
5739        Stimulus::Transfer => SoftKey::Transfer,
5740        Stimulus::ForwardAll => SoftKey::ForwardAll,
5741        Stimulus::ForwardBusy => SoftKey::ForwardBusy,
5742        Stimulus::ForwardNoAnswer => SoftKey::ForwardNoAnswer,
5743        Stimulus::Conference => SoftKey::Conference,
5744        Stimulus::MeetMeConference => SoftKey::MeetMe,
5745        Stimulus::CallPark => SoftKey::Park,
5746        Stimulus::CallPickup => SoftKey::Pickup,
5747        Stimulus::GroupCallPickup => SoftKey::GroupPickup,
5748        Stimulus::DoNotDisturb => SoftKey::DoNotDisturb,
5749        Stimulus::ConferenceList => SoftKey::ConferenceList,
5750        Stimulus::NewCall => SoftKey::NewCall,
5751        Stimulus::EndCall => SoftKey::EndCall,
5752        _ => return None,
5753    })
5754}
5755
5756fn parking_menu_xml(
5757    instance: u32,
5758    transaction_id: u32,
5759    lot: &str,
5760    calls: &[ParkingMenuEntry],
5761) -> Result<String, ServerError> {
5762    if calls.len() > PARKING_MENU_MAX_ITEMS {
5763        return Err(PhoneXmlError::LimitExceeded {
5764            kind: "parking menu",
5765            actual: calls.len(),
5766            maximum: PARKING_MENU_MAX_ITEMS,
5767        }
5768        .into());
5769    }
5770    let items = calls
5771        .iter()
5772        .map(|call| {
5773            let party = if !call.caller_name.trim().is_empty() {
5774                call.caller_name.trim()
5775            } else if !call.caller_number.trim().is_empty() {
5776                call.caller_number.trim()
5777            } else {
5778                "Unknown caller"
5779            };
5780            let connected = if !call.connected_name.trim().is_empty() {
5781                format!(" to {}", call.connected_name.trim())
5782            } else if !call.connected_number.trim().is_empty() {
5783                format!(" to {}", call.connected_number.trim())
5784            } else {
5785                String::new()
5786            };
5787            CiscoIpPhoneMenuItem {
5788                name: Some(format!("{}: {}{}", call.slot, party, connected)),
5789                url: Some(format!(
5790                    "UserCallData:{}:{instance}:0:{transaction_id}:retrieve/{}/{}",
5791                    PARKING_APPLICATION_ID,
5792                    utf8_percent_encode(lot, NON_ALPHANUMERIC),
5793                    call.slot,
5794                )),
5795            }
5796        })
5797        .collect();
5798    CiscoIpPhoneMenu::new(
5799        format!("Parked calls - {lot}"),
5800        if calls.is_empty() {
5801            "No parked calls"
5802        } else {
5803            "Select a call"
5804        },
5805        items,
5806    )?
5807    .to_xml_with_limit(2_000)
5808    .map_err(ServerError::from)
5809}
5810
5811fn text_service_messages(
5812    line_instance: LineInstance,
5813    call_reference: CallReference,
5814    transaction_id: TransactionId,
5815    priority: PhoneServicePriority,
5816    document: &CiscoIpPhoneText,
5817    protocol: ProtocolVersion,
5818) -> Result<Vec<ServerMessage>, ServerError> {
5819    if protocol <= ProtocolVersion::V17
5820        && document
5821            .text
5822            .as_deref()
5823            .is_some_and(|text| text.chars().count() > PHONE_TEXT_LEGACY_MAX_CHARS)
5824    {
5825        return Err(PhoneXmlError::InvalidField {
5826            field: "legacy phone text body",
5827            expected: "at most 1024 characters",
5828        }
5829        .into());
5830    }
5831    let maximum_bytes = if protocol <= ProtocolVersion::V17 {
5832        2_000
5833    } else {
5834        crate::phone::xml::PHONE_TEXT_MAX_BYTES
5835    };
5836    let xml = document.to_xml_with_limit(maximum_bytes)?.into_bytes();
5837    Ok(phone_service_document_messages(
5838        line_instance,
5839        call_reference,
5840        ApplicationId::new(PHONE_TEXT_APPLICATION_ID),
5841        transaction_id,
5842        priority,
5843        &xml,
5844    ))
5845}
5846
5847fn input_service_messages(
5848    line_instance: LineInstance,
5849    call_reference: CallReference,
5850    application_id: ApplicationId,
5851    transaction_id: TransactionId,
5852    priority: PhoneServicePriority,
5853    document: &CiscoIpPhoneInput,
5854    protocol: ProtocolVersion,
5855) -> Result<Vec<ServerMessage>, ServerError> {
5856    let maximum_bytes = if protocol <= ProtocolVersion::V17 {
5857        2_000
5858    } else {
5859        PHONE_INPUT_MAX_BYTES
5860    };
5861    let xml = document.to_xml_with_limit(maximum_bytes)?.into_bytes();
5862    Ok(phone_service_document_messages(
5863        line_instance,
5864        call_reference,
5865        application_id,
5866        transaction_id,
5867        priority,
5868        &xml,
5869    ))
5870}
5871
5872fn execute_phone_action_messages(
5873    line_instance: LineInstance,
5874    call_reference: CallReference,
5875    application_id: ApplicationId,
5876    transaction_id: TransactionId,
5877    priority: PhoneServicePriority,
5878    document: &CiscoIpPhoneExecute,
5879    protocol: ProtocolVersion,
5880) -> Result<Vec<ServerMessage>, ServerError> {
5881    let maximum_bytes = if protocol <= ProtocolVersion::V17 {
5882        2_000
5883    } else {
5884        PHONE_EXECUTE_MAX_BYTES
5885    };
5886    let xml = document.to_xml_with_limit(maximum_bytes)?.into_bytes();
5887    Ok(phone_service_document_messages(
5888        line_instance,
5889        call_reference,
5890        application_id,
5891        transaction_id,
5892        priority,
5893        &xml,
5894    ))
5895}
5896
5897fn image_service_messages(
5898    line_instance: LineInstance,
5899    call_reference: CallReference,
5900    application_id: ApplicationId,
5901    transaction_id: TransactionId,
5902    priority: PhoneServicePriority,
5903    document: &PhoneImageDocument,
5904    protocol: ProtocolVersion,
5905) -> Result<Vec<ServerMessage>, ServerError> {
5906    let maximum_bytes = if protocol <= ProtocolVersion::V17 {
5907        2_000
5908    } else {
5909        PHONE_IMAGE_MAX_BYTES
5910    };
5911    let xml = document.to_xml_with_limit(maximum_bytes)?.into_bytes();
5912    Ok(phone_service_document_messages(
5913        line_instance,
5914        call_reference,
5915        application_id,
5916        transaction_id,
5917        priority,
5918        &xml,
5919    ))
5920}
5921
5922fn status_service_messages(
5923    line_instance: LineInstance,
5924    call_reference: CallReference,
5925    application_id: ApplicationId,
5926    transaction_id: TransactionId,
5927    priority: PhoneServicePriority,
5928    document: &PhoneStatusDocument,
5929    protocol: ProtocolVersion,
5930) -> Result<Vec<ServerMessage>, ServerError> {
5931    let maximum_bytes = if protocol <= ProtocolVersion::V17 {
5932        2_000
5933    } else {
5934        PHONE_STATUS_MAX_BYTES
5935    };
5936    let xml = document.to_xml_with_limit(maximum_bytes)?.into_bytes();
5937    Ok(phone_service_document_messages(
5938        line_instance,
5939        call_reference,
5940        application_id,
5941        transaction_id,
5942        priority,
5943        &xml,
5944    ))
5945}
5946
5947fn background_control_message(
5948    transaction_id: TransactionId,
5949    document: &PhoneBackgroundControlDocument,
5950) -> Result<ServerMessage, ServerError> {
5951    let xml = document.to_xml()?.into_bytes();
5952    let [message] = phone_service_document_messages(
5953        LineInstance::new(0),
5954        CallReference::new(0),
5955        ApplicationId::new(PHONE_BACKGROUND_APPLICATION_ID),
5956        transaction_id,
5957        PhoneServicePriority::LOW,
5958        &xml,
5959    )
5960    .try_into()
5961    .map_err(|_| PhoneXmlError::InvalidField {
5962        field: "background control document",
5963        expected: "a single application-data frame",
5964    })?;
5965    Ok(message)
5966}
5967
5968fn ringtone_control_message(
5969    transaction_id: TransactionId,
5970    document: &CiscoIpPhoneSetRingTone,
5971) -> Result<ServerMessage, ServerError> {
5972    let xml = document.to_xml()?.into_bytes();
5973    let [message] = phone_service_document_messages(
5974        LineInstance::new(0),
5975        CallReference::new(0),
5976        ApplicationId::new(PHONE_RINGTONE_APPLICATION_ID),
5977        transaction_id,
5978        PhoneServicePriority::LOW,
5979        &xml,
5980    )
5981    .try_into()
5982    .map_err(|_| PhoneXmlError::InvalidField {
5983        field: "ringtone control document",
5984        expected: "a single application-data frame",
5985    })?;
5986    Ok(message)
5987}
5988
5989#[cfg(test)]
5990fn start_announcement_message(
5991    conference_id: ConferenceId,
5992    announcements: Vec<AnnouncementEntry>,
5993    end_of_ack: bool,
5994    participant_ids: Vec<ParticipantId>,
5995    hearing_participant_mask: u32,
5996    play_mode: u32,
5997) -> ServerMessage {
5998    ServerMessage::StartAnnouncement {
5999        announcements,
6000        end_of_ack: u32::from(end_of_ack),
6001        conference_id: conference_id.get(),
6002        matrix_conference_party_ids: participant_ids
6003            .into_iter()
6004            .map(ParticipantId::get)
6005            .collect(),
6006        hearing_conference_party_mask: hearing_participant_mask,
6007        play_mode,
6008    }
6009}
6010
6011fn phone_service_document_messages(
6012    line_instance: LineInstance,
6013    call_reference: CallReference,
6014    application_id: ApplicationId,
6015    transaction_id: TransactionId,
6016    priority: PhoneServicePriority,
6017    xml: &[u8],
6018) -> Vec<ServerMessage> {
6019    let chunks = xml.chunks(2_000);
6020    let chunk_count = chunks.len();
6021    chunks
6022        .enumerate()
6023        .map(|(index, data)| {
6024            let sequence_flag = if chunk_count == 1 || index + 1 == chunk_count {
6025                2
6026            } else if index == 0 {
6027                0
6028            } else {
6029                1
6030            };
6031            ServerMessage::UserToDeviceDataV1(UserDataV1Message {
6032                application_id: application_id.get(),
6033                line_instance: line_instance.get(),
6034                call_reference: call_reference.get(),
6035                transaction_id: transaction_id.get(),
6036                sequence_flag,
6037                display_priority: priority.wire(),
6038                conference_id: call_reference.get(),
6039                application_instance_id: application_id.get(),
6040                routing: 1,
6041                data: data.to_vec(),
6042            })
6043        })
6044        .collect()
6045}
6046
6047async fn handle_phone_service_message(
6048    state: &mut SessionState,
6049    context: &SessionContext,
6050    message_id: u32,
6051    kind: PhoneServiceMessageKind,
6052    routing: PhoneServiceRouting,
6053    extended: Option<PhoneServiceExtendedRouting>,
6054    data: &[u8],
6055) -> Result<(), ServerError> {
6056    let payload = match parse_phone_service_payload(data, kind) {
6057        Ok(payload) => payload,
6058        Err(error) => {
6059            warn!(
6060                device_id = %state.device.id,
6061                message_id = format_args!("0x{message_id:04x}"),
6062                %error,
6063                "ignoring malformed phone-service response"
6064            );
6065            context
6066                .event_tx
6067                .send(Event::ProtocolWarning {
6068                    peer: context.peer,
6069                    device_id: Some(state.device.id.clone()),
6070                    message_id,
6071                    error: error.to_string(),
6072                })
6073                .await
6074                .map_err(|_| ServerError::Stopped)?;
6075            return Ok(());
6076        }
6077    };
6078    let response = PhoneServiceEvent {
6079        kind,
6080        routing,
6081        extended,
6082        payload,
6083    };
6084
6085    if let Some((lot, slot)) = parking_menu_selection(state.pending_parking_menu, &response) {
6086        state.pending_parking_menu = None;
6087        context
6088            .event_tx
6089            .send(Event::device(
6090                state.device.id.clone(),
6091                state.generation,
6092                DeviceEventKind::ParkingMenuSelection { lot, slot },
6093            ))
6094            .await
6095            .map_err(|_| ServerError::Stopped)?;
6096    }
6097    if response.kind == PhoneServiceMessageKind::Data
6098        && response.routing.application_id.get() == ConferenceListAction::APPLICATION_ID
6099        && let PhoneServicePayload::Submission(submission) = &response.payload
6100        && let Some(action) = ConferenceListAction::from_route(&submission.route)
6101    {
6102        context
6103            .event_tx
6104            .send(Event::device(
6105                state.device.id.clone(),
6106                state.generation,
6107                DeviceEventKind::ConferenceListAction { action },
6108            ))
6109            .await
6110            .map_err(|_| ServerError::Stopped)?;
6111    }
6112    context
6113        .event_tx
6114        .send(Event::device(
6115            state.device.id.clone(),
6116            state.generation,
6117            DeviceEventKind::PhoneServiceResponse { response },
6118        ))
6119        .await
6120        .map_err(|_| ServerError::Stopped)
6121}
6122
6123fn parking_menu_selection(
6124    pending: Option<PendingParkingMenu>,
6125    response: &PhoneServiceEvent,
6126) -> Option<(String, u32)> {
6127    let pending = pending?;
6128    if response.kind != PhoneServiceMessageKind::Data
6129        || response.routing.application_id.get() != PARKING_APPLICATION_ID
6130        || response.routing.line_instance.get() != pending.instance
6131        || response.routing.call_reference.get() != 0
6132        || response.routing.transaction_id.get() != pending.transaction_id
6133        || response
6134            .extended
6135            .is_some_and(|extended| extended.application_instance_id != pending.instance)
6136    {
6137        return None;
6138    }
6139    let PhoneServicePayload::Submission(submission) = &response.payload else {
6140        return None;
6141    };
6142    let [action, lot, slot] = submission.route.as_slice() else {
6143        return None;
6144    };
6145    if action != "retrieve" || lot.is_empty() || !submission.values.is_empty() {
6146        return None;
6147    }
6148    let slot = slot.parse().ok()?;
6149    (slot != 0).then(|| (lot.clone(), slot))
6150}
6151
6152fn digit_character(digit: Digit) -> Option<char> {
6153    match digit {
6154        Digit::Number(number @ 0..=9) => Some(char::from(b'0' + number)),
6155        Digit::Star => Some('*'),
6156        Digit::Pound => Some('#'),
6157        Digit::A => Some('A'),
6158        Digit::B => Some('B'),
6159        Digit::C => Some('C'),
6160        Digit::D => Some('D'),
6161        Digit::Number(_) | Digit::Unknown(_) => None,
6162    }
6163}
6164
6165fn normalized_last_number(number: &str, config: &ServerConfig) -> Option<String> {
6166    let number = number.trim();
6167    let number = if config.record_dial_terminator {
6168        number
6169    } else {
6170        digit_character(config.dial_terminator)
6171            .map_or(number, |terminator| number.trim_end_matches(terminator))
6172    };
6173    (!number.is_empty()).then(|| number.to_owned())
6174}
6175
6176fn remember_last_number(
6177    state: &mut SessionState,
6178    line_instance: u32,
6179    number: &str,
6180    config: &ServerConfig,
6181) {
6182    if let Some(number) = normalized_last_number(number, config) {
6183        state.last_number_by_line.insert(line_instance, number);
6184    }
6185}
6186
6187async fn begin_redial(
6188    stream: &mut dyn StationIo,
6189    state: &mut SessionState,
6190    context: &SessionContext,
6191    line_instance: u32,
6192    existing_call_id: Option<CallId>,
6193) -> Result<(), ServerError> {
6194    if state.device.ui.placed_calls_redial_menu
6195        && placed_calls_menu_supported(state.registration.protocol)
6196    {
6197        let document = CiscoIpPhoneExecute::new(vec![CiscoIpPhoneExecuteItem::new(
6198            "Application:PlacedCalls",
6199        )?])?;
6200        for message in execute_phone_action_messages(
6201            LineInstance::new(line_instance),
6202            CallReference::new(0),
6203            ApplicationId::new(0),
6204            TransactionId::new(0),
6205            PhoneServicePriority::NORMAL,
6206            &document,
6207            state.registration.protocol,
6208        )? {
6209            send_message(stream, &message, state.registration.protocol).await?;
6210        }
6211        return Ok(());
6212    }
6213
6214    let Some(number) = state.last_number_by_line.get(&line_instance).cloned() else {
6215        return Ok(());
6216    };
6217    let existing = existing_call_id.and_then(|call_id| {
6218        state
6219            .calls_by_id
6220            .get(&call_id)
6221            .filter(|call| call.line_instance == line_instance && call.state != CallState::OnHook)
6222            .cloned()
6223    });
6224    let (call, created) = existing.map_or_else(
6225        || {
6226            (
6227                ensure_phone_call(state, 0, line_instance, &context.next_call_id),
6228                true,
6229            )
6230        },
6231        |call| (call, false),
6232    );
6233
6234    if created {
6235        state.active_key_mode = KeyMode::OffHook;
6236        begin_phone_call_ui(stream, &call, &state.device, state.station_context()).await?;
6237        context
6238            .event_tx
6239            .send(Event::device(
6240                state.device.id.clone(),
6241                state.generation,
6242                DeviceEventKind::OffHook {
6243                    call_id: call.call_id,
6244                    line_instance: LineInstance::new(line_instance),
6245                },
6246            ))
6247            .await
6248            .map_err(|_| ServerError::Stopped)?;
6249    }
6250    if let Some(stored) = state.calls_by_id.get_mut(&call.call_id) {
6251        stored.dialed_number.clone_from(&number);
6252    }
6253    send_message(
6254        stream,
6255        &ServerMessage::DialedNumber {
6256            number: number.clone(),
6257            line_instance,
6258            call_reference: call.wire_reference,
6259        },
6260        state.registration.protocol,
6261    )
6262    .await?;
6263    context
6264        .event_tx
6265        .send(Event::device(
6266            state.device.id.clone(),
6267            state.generation,
6268            DeviceEventKind::EnblocCall {
6269                call_id: call.call_id,
6270                line_instance: LineInstance::new(line_instance),
6271                number,
6272            },
6273        ))
6274        .await
6275        .map_err(|_| ServerError::Stopped)?;
6276    Ok(())
6277}
6278
6279fn placed_calls_menu_supported(protocol: ProtocolVersion) -> bool {
6280    protocol >= ProtocolVersion::V8
6281}
6282
6283async fn handle_session_command(
6284    stream: &mut dyn StationIo,
6285    state: &mut SessionState,
6286    command: SessionCommand,
6287    context: &SessionContext,
6288) -> Result<bool, ServerError> {
6289    let config = &context.config;
6290    let protocol = state.registration.protocol;
6291    match command {
6292        SessionCommand::Confirmed { .. } => {
6293            unreachable!("confirmed commands are unwrapped by the session loop")
6294        }
6295        SessionCommand::Disconnect => {
6296            drain_session_media(stream, state).await;
6297            return Ok(true);
6298        }
6299        SessionCommand::OfferIncoming {
6300            line_instance,
6301            call_id,
6302            info,
6303            presentation,
6304            ringer,
6305            delivery: _,
6306        } => {
6307            let line_instance = normalize_line(state, line_instance.get());
6308            let statistics_directory_number = statistics_directory_for_call_info(&info).to_owned();
6309            let caller = match (
6310                info.calling_name.trim().is_empty(),
6311                info.calling_number.trim().is_empty(),
6312            ) {
6313                (false, false) => format!("{} ({})", info.calling_name, info.calling_number),
6314                (false, true) => info.calling_name.clone(),
6315                (true, false) => info.calling_number.clone(),
6316                (true, true) => "Unknown number".to_owned(),
6317            };
6318            let incoming_state = presentation.call_state();
6319            let call = insert_call(state, call_id, line_instance, Codec::Pcmu, incoming_state);
6320            if incoming_state == CallState::RingIn && state.active_call_id.is_none() {
6321                state.active_call_id = Some(call.call_id);
6322            }
6323            if let Some(stored) = state.calls_by_id.get_mut(&call.call_id) {
6324                stored.statistics_directory_number = statistics_directory_number;
6325                stored.ringer = ringer;
6326            }
6327            send_message(
6328                stream,
6329                &ServerMessage::ClearPrompt {
6330                    line_instance,
6331                    call_reference: call.wire_reference,
6332                },
6333                protocol,
6334            )
6335            .await?;
6336            send_message(
6337                stream,
6338                &ServerMessage::CallState {
6339                    state: incoming_state,
6340                    line_instance,
6341                    call_reference: call.wire_reference,
6342                },
6343                protocol,
6344            )
6345            .await?;
6346            send_station_ui_message(
6347                stream,
6348                state,
6349                &ServerMessage::CallInfo {
6350                    info: *info,
6351                    line_instance,
6352                    call_reference: call.wire_reference,
6353                },
6354            )
6355            .await?;
6356            send_message(
6357                stream,
6358                &ServerMessage::SetLamp {
6359                    stimulus: ButtonType::Line,
6360                    instance: line_instance,
6361                    mode: LampMode::Blink,
6362                },
6363                protocol,
6364            )
6365            .await?;
6366            if let Some(ringer) = incoming_ringer(ringer, incoming_state) {
6367                let audible = ringer_is_audible(ringer);
6368                if audible || state.ringer_owner.is_none() {
6369                    send_message(
6370                        stream,
6371                        &ServerMessage::SetRinger {
6372                            mode: ringer.mode,
6373                            duration: ringer.duration,
6374                            line_instance,
6375                            call_reference: call.wire_reference,
6376                        },
6377                        protocol,
6378                    )
6379                    .await?;
6380                }
6381                if audible {
6382                    state.ringer_owner = Some(call.call_id);
6383                }
6384            }
6385            state.active_key_mode = KeyMode::RingIn;
6386            send_message(
6387                stream,
6388                &ServerMessage::SelectSoftKeys {
6389                    line_instance,
6390                    call_reference: call.wire_reference,
6391                    set: KeyMode::RingIn,
6392                    valid_mask: state.device.soft_keys.valid_mask(KeyMode::RingIn),
6393                },
6394                protocol,
6395            )
6396            .await?;
6397            send_station_ui_message(
6398                stream,
6399                state,
6400                &ServerMessage::DisplayPrompt {
6401                    timeout_seconds: 0,
6402                    text: format!("From {caller}"),
6403                    line_instance,
6404                    call_reference: call.wire_reference,
6405                },
6406            )
6407            .await?;
6408        }
6409        SessionCommand::Public(command) => {
6410            let command = *command;
6411            if let Some(call_id) = command_call_id(&command)
6412                && !matches!(
6413                    &command.action,
6414                    CommandAction::BeginCall { .. } | CommandAction::CloseCall { .. }
6415                )
6416                && !state.calls_by_id.contains_key(&call_id)
6417            {
6418                debug!(device_id = %state.device.id, ?call_id, command = ?command, "ignoring stale SCCP call command");
6419                return Ok(false);
6420            }
6421            let action = command.action;
6422            match action {
6423                CommandAction::DisconnectDevice { .. } => {
6424                    drain_session_media(stream, state).await;
6425                    return Ok(true);
6426                }
6427                CommandAction::BeginCall {
6428                    line_instance,
6429                    call_id,
6430                    codec,
6431                } => {
6432                    if state.calls_by_id.contains_key(&call_id) {
6433                        return Ok(false);
6434                    }
6435                    let line_instance = normalize_line(state, line_instance.get());
6436                    let call =
6437                        insert_call(state, call_id, line_instance, codec, CallState::OffHook);
6438                    state.active_call_id = Some(call.call_id);
6439                    state.active_key_mode = KeyMode::OffHook;
6440                    begin_phone_call_ui(stream, &call, &state.device, state.station_context())
6441                        .await?;
6442                }
6443                CommandAction::BeginTransfer {
6444                    source_call_id,
6445                    consultation_line_instance,
6446                    consultation_call_id,
6447                    codec,
6448                } => {
6449                    let consultation_line_instance = consultation_line_instance.get();
6450                    if state.calls_by_id.contains_key(&consultation_call_id) {
6451                        return Ok(false);
6452                    }
6453                    let source = require_call_mut(state, source_call_id)?;
6454                    if !matches!(
6455                        source.state,
6456                        CallState::Hold | CallState::HoldYellow | CallState::HoldRed
6457                    ) {
6458                        return Err(ServerError::InvalidCallTransaction {
6459                            call_id: source_call_id,
6460                            operation: "begin transfer",
6461                            state: source.state,
6462                        });
6463                    }
6464                    source.state = CallState::Transfer;
6465                    source.transfer_role = Some(SessionTransferRole::Source {
6466                        consultation_call_id,
6467                    });
6468                    let source = source.clone();
6469                    send_message(
6470                        stream,
6471                        &ServerMessage::CallState {
6472                            state: CallState::Transfer,
6473                            line_instance: source.line_instance,
6474                            call_reference: source.wire_reference,
6475                        },
6476                        protocol,
6477                    )
6478                    .await?;
6479                    send_station_ui_message(
6480                        stream,
6481                        state,
6482                        &ServerMessage::DisplayPrompt {
6483                            timeout_seconds: 0,
6484                            text: "Call Transfer".into(),
6485                            line_instance: source.line_instance,
6486                            call_reference: source.wire_reference,
6487                        },
6488                    )
6489                    .await?;
6490
6491                    let line_instance = normalize_line(state, consultation_line_instance);
6492                    let mut consultation = insert_call(
6493                        state,
6494                        consultation_call_id,
6495                        line_instance,
6496                        codec,
6497                        CallState::OffHook,
6498                    );
6499                    consultation.transfer_role =
6500                        Some(SessionTransferRole::Consultation { source_call_id });
6501                    state
6502                        .calls_by_id
6503                        .insert(consultation_call_id, consultation.clone());
6504                    state.active_call_id = Some(consultation.call_id);
6505                    state.active_key_mode = KeyMode::OffHookFeature;
6506                    begin_phone_call_ui_with_key_mode(
6507                        stream,
6508                        &consultation,
6509                        &state.device,
6510                        KeyMode::OffHookFeature,
6511                        state.station_context(),
6512                    )
6513                    .await?;
6514                    send_message(
6515                        stream,
6516                        &ServerMessage::SetLamp {
6517                            stimulus: ButtonType::Transfer,
6518                            instance: source.line_instance,
6519                            mode: LampMode::Flash,
6520                        },
6521                        protocol,
6522                    )
6523                    .await?;
6524                }
6525                CommandAction::SetCallSelected {
6526                    call_id, selected, ..
6527                } => {
6528                    let call = require_call(state, call_id)?.clone();
6529                    send_message(
6530                        stream,
6531                        &ServerMessage::CallSelectStatus {
6532                            status: u32::from(selected),
6533                            call_reference: call.wire_reference,
6534                            line_instance: call.line_instance,
6535                        },
6536                        protocol,
6537                    )
6538                    .await?;
6539                }
6540                CommandAction::SetMwi {
6541                    line_instance,
6542                    enabled,
6543                    ..
6544                } => {
6545                    let line_instance = line_instance.get();
6546                    state.mwi_by_line.insert(line_instance, enabled);
6547                    send_mwi_lamp(stream, state, line_instance, enabled, protocol).await?;
6548                }
6549                CommandAction::SetForwardStatus {
6550                    line_instance,
6551                    forward_all,
6552                    forward_busy,
6553                    forward_no_answer,
6554                    ..
6555                } => {
6556                    let line_instance = line_instance.get();
6557                    state.forwarding_by_line.insert(
6558                        line_instance,
6559                        SessionForwarding {
6560                            all: forward_all.clone(),
6561                            busy: forward_busy.clone(),
6562                            no_answer: forward_no_answer.clone(),
6563                        },
6564                    );
6565                    send_message(
6566                        stream,
6567                        &ServerMessage::ForwardStatus {
6568                            line_instance,
6569                            forward_all,
6570                            forward_busy,
6571                            forward_no_answer,
6572                        },
6573                        protocol,
6574                    )
6575                    .await?;
6576                }
6577                CommandAction::SetFeatureStatus {
6578                    instance, enabled, ..
6579                } => {
6580                    let instance = instance.get();
6581                    if let Some(messages) = feature_state_messages(&state.device, instance, enabled)
6582                    {
6583                        cache_feature_projection(&mut state.feature_states, instance, &messages[0]);
6584                        for message in messages {
6585                            send_station_ui_message(stream, state, &message).await?;
6586                        }
6587                    }
6588                }
6589                CommandAction::SetDoNotDisturbStatus {
6590                    instance,
6591                    mode,
6592                    button_mode,
6593                    ..
6594                } => {
6595                    let instance = instance.get();
6596                    if let Some(messages) = do_not_disturb_state_messages(
6597                        &state.device,
6598                        instance,
6599                        mode,
6600                        button_mode,
6601                        protocol,
6602                    ) {
6603                        cache_feature_projection(&mut state.feature_states, instance, &messages[0]);
6604                        for message in messages {
6605                            send_station_ui_message(stream, state, &message).await?;
6606                        }
6607                    }
6608                }
6609                CommandAction::SetMobilityAppearance {
6610                    mobility_instance,
6611                    appearance,
6612                    ..
6613                } => {
6614                    let mobility_instance = mobility_instance.get();
6615                    let configured = state.device.buttons.iter().any(|button| {
6616                        matches!(
6617                            button,
6618                            ButtonDefinition::Feature(feature)
6619                                if feature.instance == mobility_instance
6620                                    && feature.feature == ButtonType::Mobility
6621                        )
6622                    });
6623                    if !configured {
6624                        return Err(CodecError::InvalidDefinition(format!(
6625                            "device {} has no mobility button instance {mobility_instance}",
6626                            state.device.id
6627                        ))
6628                        .into());
6629                    }
6630                    let previous = state.mobility_appearances.get(&mobility_instance).cloned();
6631                    let mut next_appearances = state.mobility_appearances.clone();
6632                    match &appearance {
6633                        Some(appearance) => {
6634                            next_appearances.insert(mobility_instance, appearance.clone());
6635                        }
6636                        None => {
6637                            next_appearances.remove(&mobility_instance);
6638                        }
6639                    }
6640                    let candidate = mobility_device_candidate(
6641                        &state.device,
6642                        &state.mobility_appearances,
6643                        &next_appearances,
6644                    )?;
6645
6646                    send_button_template(stream, &candidate, protocol).await?;
6647                    if let Some(appearance) = &appearance {
6648                        if let Some(message) = line_status(&candidate, appearance.instance) {
6649                            send_station_ui_message(stream, state, &message).await?;
6650                        }
6651                    } else if let Some(previous) = &previous {
6652                        send_station_ui_message(
6653                            stream,
6654                            state,
6655                            &ServerMessage::LineStatus {
6656                                instance: previous.instance,
6657                                number: String::new(),
6658                                display_name: String::new(),
6659                            },
6660                        )
6661                        .await?;
6662                    }
6663                    state.device = candidate;
6664                    state.mobility_appearances = next_appearances;
6665                }
6666                CommandAction::SetBlfStatus {
6667                    instance,
6668                    state: blf_state,
6669                    caller,
6670                    ..
6671                } => {
6672                    let instance = instance.get();
6673                    let Some(message) = blf_status_message(&state.device, instance, blf_state)
6674                    else {
6675                        return Err(ServerError::UnknownBlfButton {
6676                            device: state.device.id.clone(),
6677                            instance,
6678                        });
6679                    };
6680                    let ServerMessage::FeatureStatus { ref label, .. } = message else {
6681                        unreachable!("BLF status is a feature-state message")
6682                    };
6683                    cache_feature_projection(&mut state.feature_states, instance, &message);
6684                    send_station_ui_message(stream, state, &message).await?;
6685                    let notification = hinted_ringing_notification(
6686                        &state.device,
6687                        label,
6688                        caller.as_ref(),
6689                        blf_state,
6690                    );
6691                    if let Some(notification) = reconcile_blf_alert(
6692                        instance,
6693                        notification,
6694                        &mut state.runtime.active_blf_alerts,
6695                        &mut state.runtime.visible_blf_alert,
6696                    ) {
6697                        for message in status_message_frames(
6698                            notification,
6699                            state.registration.device_type,
6700                            &mut state.persistent_status_message,
6701                        ) {
6702                            send_station_ui_message(stream, state, &message).await?;
6703                        }
6704                    }
6705                }
6706                CommandAction::ShowParkingMenu {
6707                    instance,
6708                    transaction_id,
6709                    lot,
6710                    calls,
6711                    ..
6712                } => {
6713                    let instance = instance.get();
6714                    let transaction_id = transaction_id.get();
6715                    send_message(
6716                        stream,
6717                        &ServerMessage::UserToDeviceDataV1(UserDataV1Message {
6718                            application_id: PARKING_APPLICATION_ID,
6719                            line_instance: instance,
6720                            call_reference: 0,
6721                            transaction_id,
6722                            sequence_flag: 0,
6723                            display_priority: 2,
6724                            conference_id: 0,
6725                            application_instance_id: instance,
6726                            routing: 0,
6727                            data: parking_menu_xml(instance, transaction_id, &lot, &calls)?
6728                                .into_bytes(),
6729                        }),
6730                        protocol,
6731                    )
6732                    .await?;
6733                    state.pending_parking_menu = Some(PendingParkingMenu {
6734                        instance,
6735                        transaction_id,
6736                    });
6737                }
6738                CommandAction::ShowConferenceList {
6739                    call_id,
6740                    conference_id,
6741                    participants,
6742                    ..
6743                } => {
6744                    let call = require_call(state, call_id)?.clone();
6745                    let family = if protocol >= ProtocolVersion::V8 {
6746                        ConferenceMenuFamily::IconMenu
6747                    } else {
6748                        ConferenceMenuFamily::Menu
6749                    };
6750                    let data = ConferenceListDocument::new(conference_id, &participants, family)?
6751                        .to_xml()?
6752                        .into_bytes();
6753                    send_message(
6754                        stream,
6755                        &ServerMessage::UserToDeviceDataV1(UserDataV1Message {
6756                            application_id: ConferenceListAction::APPLICATION_ID,
6757                            line_instance: call.line_instance,
6758                            call_reference: call.wire_reference,
6759                            transaction_id: conference_id.get(),
6760                            sequence_flag: 0,
6761                            display_priority: 2,
6762                            conference_id: conference_id.get(),
6763                            application_instance_id: call.line_instance,
6764                            routing: 0,
6765                            data,
6766                        }),
6767                        protocol,
6768                    )
6769                    .await?;
6770                }
6771                CommandAction::ShowConferenceParticipantActions {
6772                    call_id,
6773                    conference_id,
6774                    participant,
6775                    removable,
6776                    demotable,
6777                    ..
6778                } => {
6779                    let call = require_call(state, call_id)?.clone();
6780                    let family = if protocol >= ProtocolVersion::V8 {
6781                        ConferenceMenuFamily::IconMenu
6782                    } else {
6783                        ConferenceMenuFamily::Menu
6784                    };
6785                    let data = ConferenceParticipantActionsDocument::new(
6786                        conference_id,
6787                        &participant,
6788                        removable,
6789                        demotable,
6790                        family,
6791                    )?
6792                    .to_xml()?
6793                    .into_bytes();
6794                    send_message(
6795                        stream,
6796                        &ServerMessage::UserToDeviceDataV1(UserDataV1Message {
6797                            application_id: ConferenceListAction::APPLICATION_ID,
6798                            line_instance: call.line_instance,
6799                            call_reference: call.wire_reference,
6800                            transaction_id: conference_id.get(),
6801                            sequence_flag: 0,
6802                            display_priority: 2,
6803                            conference_id: conference_id.get(),
6804                            application_instance_id: call.line_instance,
6805                            routing: 0,
6806                            data,
6807                        }),
6808                        protocol,
6809                    )
6810                    .await?;
6811                }
6812                CommandAction::ShowTextService {
6813                    line_instance,
6814                    call_reference,
6815                    transaction_id,
6816                    priority,
6817                    document,
6818                    ..
6819                } => {
6820                    for message in text_service_messages(
6821                        line_instance,
6822                        call_reference,
6823                        transaction_id,
6824                        priority,
6825                        &document,
6826                        protocol,
6827                    )? {
6828                        send_message(stream, &message, protocol).await?;
6829                    }
6830                }
6831                CommandAction::ShowInputService {
6832                    line_instance,
6833                    call_reference,
6834                    application_id,
6835                    transaction_id,
6836                    priority,
6837                    document,
6838                    ..
6839                } => {
6840                    for message in input_service_messages(
6841                        line_instance,
6842                        call_reference,
6843                        application_id,
6844                        transaction_id,
6845                        priority,
6846                        &document,
6847                        protocol,
6848                    )? {
6849                        send_message(stream, &message, protocol).await?;
6850                    }
6851                }
6852                CommandAction::ExecutePhoneActions {
6853                    line_instance,
6854                    call_reference,
6855                    application_id,
6856                    transaction_id,
6857                    priority,
6858                    document,
6859                    ..
6860                } => {
6861                    for message in execute_phone_action_messages(
6862                        line_instance,
6863                        call_reference,
6864                        application_id,
6865                        transaction_id,
6866                        priority,
6867                        &document,
6868                        protocol,
6869                    )? {
6870                        send_message(stream, &message, protocol).await?;
6871                    }
6872                }
6873                CommandAction::ShowImageService {
6874                    line_instance,
6875                    call_reference,
6876                    application_id,
6877                    transaction_id,
6878                    priority,
6879                    document,
6880                    ..
6881                } => {
6882                    for message in image_service_messages(
6883                        line_instance,
6884                        call_reference,
6885                        application_id,
6886                        transaction_id,
6887                        priority,
6888                        &document,
6889                        protocol,
6890                    )? {
6891                        send_message(stream, &message, protocol).await?;
6892                    }
6893                }
6894                CommandAction::ShowStatusService {
6895                    line_instance,
6896                    call_reference,
6897                    application_id,
6898                    transaction_id,
6899                    priority,
6900                    document,
6901                    ..
6902                } => {
6903                    for message in status_service_messages(
6904                        line_instance,
6905                        call_reference,
6906                        application_id,
6907                        transaction_id,
6908                        priority,
6909                        &document,
6910                        protocol,
6911                    )? {
6912                        send_message(stream, &message, protocol).await?;
6913                    }
6914                }
6915                CommandAction::SetBackgroundImage {
6916                    transaction_id,
6917                    document,
6918                    ..
6919                } => {
6920                    let message = background_control_message(
6921                        transaction_id,
6922                        &PhoneBackgroundControlDocument::Set(document),
6923                    )?;
6924                    send_message(stream, &message, protocol).await?;
6925                }
6926                CommandAction::PreviewBackgroundImage {
6927                    transaction_id,
6928                    document,
6929                    ..
6930                } => {
6931                    let message = background_control_message(
6932                        transaction_id,
6933                        &PhoneBackgroundControlDocument::Preview(document),
6934                    )?;
6935                    send_message(stream, &message, protocol).await?;
6936                }
6937                CommandAction::SetRingtone {
6938                    transaction_id,
6939                    document,
6940                    ..
6941                } => {
6942                    let message = ringtone_control_message(transaction_id, &document)?;
6943                    send_message(stream, &message, protocol).await?;
6944                }
6945                CommandAction::StartTone { call_id, tone, .. } => {
6946                    let call = require_call(state, call_id)?.clone();
6947                    let message = if tone == Tone::Silence {
6948                        ServerMessage::StopTone {
6949                            line_instance: call.line_instance,
6950                            call_reference: call.wire_reference,
6951                        }
6952                    } else {
6953                        ServerMessage::StartTone {
6954                            tone,
6955                            direction: ToneDirection::User,
6956                            line_instance: call.line_instance,
6957                            call_reference: call.wire_reference,
6958                        }
6959                    };
6960                    send_message(stream, &message, protocol).await?;
6961                }
6962                CommandAction::StartAnnouncement {
6963                    conference_id,
6964                    announcements,
6965                    end_of_ack,
6966                    participant_ids,
6967                    hearing_participant_mask,
6968                    play_mode,
6969                    ..
6970                } => {
6971                    let _ = (
6972                        conference_id,
6973                        announcements,
6974                        end_of_ack,
6975                        participant_ids,
6976                        hearing_participant_mask,
6977                        play_mode,
6978                    );
6979                    return Err(ServerError::InvalidStationCommand {
6980                        message: "StartAnnouncement",
6981                    });
6982                }
6983                CommandAction::StopAnnouncement { conference_id, .. } => {
6984                    let _ = conference_id;
6985                    return Err(ServerError::InvalidStationCommand {
6986                        message: "StopAnnouncement",
6987                    });
6988                }
6989                CommandAction::AnnouncementFinish {
6990                    conference_id,
6991                    play_status,
6992                    ..
6993                } => {
6994                    let _ = (conference_id, play_status);
6995                    return Err(ServerError::InvalidStationCommand {
6996                        message: "AnnouncementFinish",
6997                    });
6998                }
6999                CommandAction::SetCallInfo { call_id, info, .. } => {
7000                    let statistics_directory_number =
7001                        statistics_directory_for_call_info(&info).to_owned();
7002                    if let Some(stored) = state.calls_by_id.get_mut(&call_id) {
7003                        stored.statistics_directory_number = statistics_directory_number;
7004                    }
7005                    let call = require_call(state, call_id)?.clone();
7006                    send_station_ui_message(
7007                        stream,
7008                        state,
7009                        &ServerMessage::CallInfo {
7010                            info,
7011                            line_instance: call.line_instance,
7012                            call_reference: call.wire_reference,
7013                        },
7014                    )
7015                    .await?;
7016                }
7017                CommandAction::CommitOutboundCall { call_id, info, .. } => {
7018                    let statistics_directory_number =
7019                        statistics_directory_for_call_info(&info).to_owned();
7020                    let call = require_call_mut(state, call_id)?;
7021                    call.state = CallState::Proceed;
7022                    call.history_disposition =
7023                        updated_history_disposition(call.history_disposition, CallState::Proceed);
7024                    call.statistics_directory_number = statistics_directory_number;
7025                    let call = call.clone();
7026                    let number = digit_character(config.dial_terminator)
7027                        .and_then(|terminator| call.dialed_number.strip_suffix(terminator))
7028                        .unwrap_or(&call.dialed_number)
7029                        .to_owned();
7030                    remember_last_number(state, call.line_instance, &number, config);
7031                    state.active_call_id = Some(call.call_id);
7032                    refresh_mwi_lamps(stream, state, protocol).await?;
7033                    for message in [
7034                        ServerMessage::StopTone {
7035                            line_instance: call.line_instance,
7036                            call_reference: call.wire_reference,
7037                        },
7038                        ServerMessage::SetLamp {
7039                            stimulus: ButtonType::Line,
7040                            instance: call.line_instance,
7041                            mode: LampMode::Blink,
7042                        },
7043                        ServerMessage::CallInfo {
7044                            info,
7045                            line_instance: call.line_instance,
7046                            call_reference: call.wire_reference,
7047                        },
7048                        ServerMessage::DialedNumber {
7049                            number,
7050                            line_instance: call.line_instance,
7051                            call_reference: call.wire_reference,
7052                        },
7053                        ServerMessage::CallState {
7054                            state: CallState::Proceed,
7055                            line_instance: call.line_instance,
7056                            call_reference: call.wire_reference,
7057                        },
7058                    ] {
7059                        send_station_ui_message(stream, state, &message).await?;
7060                    }
7061                }
7062                CommandAction::PresentOutboundProceeding { call_id, info, .. } => {
7063                    let statistics_directory_number =
7064                        statistics_directory_for_call_info(&info).to_owned();
7065                    let call = require_call_mut(state, call_id)?;
7066                    call.state = CallState::Proceed;
7067                    call.history_disposition =
7068                        updated_history_disposition(call.history_disposition, CallState::Proceed);
7069                    call.statistics_directory_number = statistics_directory_number;
7070                    let call = call.clone();
7071                    state.active_call_id = Some(call.call_id);
7072                    refresh_mwi_lamps(stream, state, protocol).await?;
7073                    for message in [
7074                        ServerMessage::StopTone {
7075                            line_instance: call.line_instance,
7076                            call_reference: call.wire_reference,
7077                        },
7078                        ServerMessage::CallState {
7079                            state: CallState::Proceed,
7080                            line_instance: call.line_instance,
7081                            call_reference: call.wire_reference,
7082                        },
7083                        ServerMessage::CallInfo {
7084                            info,
7085                            line_instance: call.line_instance,
7086                            call_reference: call.wire_reference,
7087                        },
7088                        ServerMessage::DisplayPrompt {
7089                            timeout_seconds: 0,
7090                            text: "Call Proceed".into(),
7091                            line_instance: call.line_instance,
7092                            call_reference: call.wire_reference,
7093                        },
7094                    ] {
7095                        send_station_ui_message(stream, state, &message).await?;
7096                    }
7097                }
7098                CommandAction::PresentOutboundRinging { call_id, info, .. } => {
7099                    let statistics_directory_number =
7100                        statistics_directory_for_call_info(&info).to_owned();
7101                    let call = require_call_mut(state, call_id)?;
7102                    call.state = CallState::Proceed;
7103                    call.history_disposition =
7104                        updated_history_disposition(call.history_disposition, CallState::Proceed);
7105                    call.statistics_directory_number = statistics_directory_number;
7106                    let call = call.clone();
7107                    state.active_call_id = Some(call.call_id);
7108                    let key_mode = transfer_key_mode(&call, CallState::RingOut);
7109                    state.active_key_mode = key_mode;
7110                    refresh_mwi_lamps(stream, state, protocol).await?;
7111                    for message in [
7112                        ServerMessage::CallState {
7113                            state: CallState::Proceed,
7114                            line_instance: call.line_instance,
7115                            call_reference: call.wire_reference,
7116                        },
7117                        ServerMessage::DisplayPrompt {
7118                            timeout_seconds: 0,
7119                            text: "Ring out".into(),
7120                            line_instance: call.line_instance,
7121                            call_reference: call.wire_reference,
7122                        },
7123                        ServerMessage::StopTone {
7124                            line_instance: call.line_instance,
7125                            call_reference: call.wire_reference,
7126                        },
7127                        ServerMessage::StartTone {
7128                            tone: Tone::Alerting,
7129                            direction: ToneDirection::User,
7130                            line_instance: call.line_instance,
7131                            call_reference: call.wire_reference,
7132                        },
7133                        ServerMessage::SelectSoftKeys {
7134                            line_instance: call.line_instance,
7135                            call_reference: call.wire_reference,
7136                            set: key_mode,
7137                            valid_mask: state.device.soft_keys.valid_mask(key_mode),
7138                        },
7139                        ServerMessage::CallInfo {
7140                            info,
7141                            line_instance: call.line_instance,
7142                            call_reference: call.wire_reference,
7143                        },
7144                    ] {
7145                        send_station_ui_message(stream, state, &message).await?;
7146                    }
7147                }
7148                CommandAction::SetCallState {
7149                    call_id,
7150                    state: call_state,
7151                    ..
7152                } => {
7153                    let transfer_source_to_clear =
7154                        state
7155                            .calls_by_id
7156                            .get(&call_id)
7157                            .and_then(|call| match call.transfer_role {
7158                                Some(SessionTransferRole::Source {
7159                                    consultation_call_id,
7160                                }) if call_state != CallState::Transfer => {
7161                                    Some((consultation_call_id, call.line_instance))
7162                                }
7163                                _ => None,
7164                            });
7165                    let call = require_call_mut(state, call_id)?;
7166                    call.state = call_state;
7167                    call.history_disposition =
7168                        updated_history_disposition(call.history_disposition, call_state);
7169                    let call = call.clone();
7170                    if matches!(
7171                        call_state,
7172                        CallState::Proceed | CallState::RingOut | CallState::Connected
7173                    ) {
7174                        remember_last_number(
7175                            state,
7176                            call.line_instance,
7177                            &call.dialed_number,
7178                            config,
7179                        );
7180                    }
7181                    prepare_call_state_ui(stream, &call, call_state, protocol).await?;
7182                    send_message(
7183                        stream,
7184                        &ServerMessage::CallState {
7185                            state: call_state,
7186                            line_instance: call.line_instance,
7187                            call_reference: call.wire_reference,
7188                        },
7189                        protocol,
7190                    )
7191                    .await?;
7192                    finish_call_state_ui(stream, &call, call_state, state.station_context())
7193                        .await?;
7194                    let set = transfer_key_mode(&call, call_state);
7195                    state.active_key_mode = set;
7196                    match call_state {
7197                        CallState::Connected
7198                        | CallState::OffHook
7199                        | CallState::Transfer
7200                        | CallState::RingOut
7201                        | CallState::Proceed
7202                        | CallState::IntercomOneWay => {
7203                            state.active_call_id = Some(call.call_id);
7204                        }
7205                        CallState::OnHook
7206                        | CallState::Hold
7207                        | CallState::HoldYellow
7208                        | CallState::HoldRed
7209                            if state.active_call_id == Some(call.call_id) =>
7210                        {
7211                            state.active_call_id = None;
7212                        }
7213                        _ => {}
7214                    }
7215                    refresh_mwi_lamps(stream, state, protocol).await?;
7216                    send_message(
7217                        stream,
7218                        &ServerMessage::SelectSoftKeys {
7219                            line_instance: call.line_instance,
7220                            call_reference: call.wire_reference,
7221                            set,
7222                            valid_mask: state.device.soft_keys.valid_mask(set),
7223                        },
7224                        protocol,
7225                    )
7226                    .await?;
7227                    if let Some((consultation_call_id, line_instance)) = transfer_source_to_clear {
7228                        if let Some(source) = state.calls_by_id.get_mut(&call_id) {
7229                            source.transfer_role = None;
7230                        }
7231                        if let Some(consultation) = state.calls_by_id.get_mut(&consultation_call_id)
7232                        {
7233                            consultation.transfer_role = None;
7234                        }
7235                        send_message(
7236                            stream,
7237                            &ServerMessage::SetLamp {
7238                                stimulus: ButtonType::Transfer,
7239                                instance: line_instance,
7240                                mode: LampMode::Off,
7241                            },
7242                            protocol,
7243                        )
7244                        .await?;
7245                    }
7246                }
7247                CommandAction::DisplayPrompt {
7248                    call_id,
7249                    timeout_seconds,
7250                    text,
7251                    ..
7252                } => {
7253                    let call = require_call(state, call_id)?.clone();
7254                    send_station_ui_message(
7255                        stream,
7256                        state,
7257                        &ServerMessage::DisplayPrompt {
7258                            timeout_seconds,
7259                            text,
7260                            line_instance: call.line_instance,
7261                            call_reference: call.wire_reference,
7262                        },
7263                    )
7264                    .await?;
7265                }
7266                CommandAction::ClearPrompt { call_id, .. } => {
7267                    let call = require_call(state, call_id)?.clone();
7268                    send_message(
7269                        stream,
7270                        &ServerMessage::ClearPrompt {
7271                            line_instance: call.line_instance,
7272                            call_reference: call.wire_reference,
7273                        },
7274                        protocol,
7275                    )
7276                    .await?;
7277                }
7278                CommandAction::SetStatusMessage { message, beep, .. } => {
7279                    let frames = status_message_frames(
7280                        message,
7281                        state.registration.device_type,
7282                        &mut state.persistent_status_message,
7283                    );
7284                    for frame in frames {
7285                        send_station_ui_message(stream, state, &frame).await?;
7286                    }
7287                    if beep {
7288                        send_message(
7289                            stream,
7290                            &ServerMessage::StartTone {
7291                                tone: Tone::ZipZip,
7292                                direction: ToneDirection::User,
7293                                line_instance: 0,
7294                                call_reference: 0,
7295                            },
7296                            protocol,
7297                        )
7298                        .await?;
7299                    }
7300                }
7301                CommandAction::SetMicrophoneMode { enabled, .. } => {
7302                    send_message(
7303                        stream,
7304                        &ServerMessage::SetMicrophoneMode(if enabled {
7305                            MicrophoneMode::On
7306                        } else {
7307                            MicrophoneMode::Off
7308                        }),
7309                        protocol,
7310                    )
7311                    .await?;
7312                }
7313                CommandAction::SetRecordingStatus {
7314                    call_id, active, ..
7315                } => {
7316                    let call = require_call(state, call_id)?.clone();
7317                    send_message(
7318                        stream,
7319                        &ServerMessage::RecordingStatus {
7320                            call_reference: call.wire_reference,
7321                            active,
7322                        },
7323                        protocol,
7324                    )
7325                    .await?;
7326                }
7327                CommandAction::ResetDevice { reset_type, .. } => {
7328                    send_message(stream, &ServerMessage::Reset(reset_type), protocol).await?;
7329                }
7330                ringing @ (CommandAction::StartRinging { call_id }
7331                | CommandAction::StopRinging { call_id }) => {
7332                    let enabled = matches!(ringing, CommandAction::StartRinging { .. });
7333                    let call = require_call(state, call_id)?.clone();
7334                    if let Some(stored) = state.calls_by_id.get_mut(&call_id) {
7335                        stored.ringer = enabled.then_some(IncomingRing::default());
7336                    }
7337                    if !enabled && state.ringer_owner != Some(call_id) {
7338                        return Ok(false);
7339                    }
7340                    send_message(
7341                        stream,
7342                        &ServerMessage::SetRinger {
7343                            mode: if enabled {
7344                                RingerMode::Inside
7345                            } else {
7346                                RingerMode::Off
7347                            },
7348                            duration: RingDuration::Normal,
7349                            line_instance: call.line_instance,
7350                            call_reference: call.wire_reference,
7351                        },
7352                        protocol,
7353                    )
7354                    .await?;
7355                    state.ringer_owner = enabled.then_some(call_id);
7356                    if !enabled {
7357                        let order = *context
7358                            .call_answer_order
7359                            .read()
7360                            .expect("SCCP call-answer-order lock poisoned");
7361                        if let Some((call_id, promote)) = incoming_successor(state, call_id, order)
7362                        {
7363                            present_incoming_successor(stream, state, call_id, promote).await?;
7364                        }
7365                    }
7366                }
7367                CommandAction::OpenReceiveChannel {
7368                    call_id,
7369                    source,
7370                    codec,
7371                    packet_ms,
7372                    max_frames_per_packet,
7373                    dtmf_mode,
7374                    audio_processing,
7375                    ..
7376                } => {
7377                    let telephone_event_payload = dtmf_mode.telephone_event_payload(state.features);
7378                    let request = allocate_media_request_identity(state, call_id)?;
7379                    let call = require_call_mut(state, call_id)?;
7380                    call.media.requested = true;
7381                    call.media.codec = codec;
7382                    call.media.packet_ms = packet_ms;
7383                    call.media.max_frames_per_packet = max_frames_per_packet;
7384                    call.media.receive.telephone_event_payload = telephone_event_payload;
7385                    call.media.receive.peer = None;
7386                    call.media.receive.state = MediaChannelState::Opening;
7387                    call.media.receive.deadline =
7388                        Some(Instant::now() + HANDSET_ACKNOWLEDGEMENT_TIMEOUT);
7389                    call.media.receive.request = Some(request);
7390                    if call.media.transmit.state == MediaChannelState::Closed {
7391                        call.media.transmit.request = None;
7392                    }
7393                    call.media.coupled_transmit_endpoint = None;
7394                    let call = call.clone();
7395                    send_message(
7396                        stream,
7397                        &ServerMessage::OpenReceiveChannel {
7398                            call_reference: call.wire_reference,
7399                            passthrough_party_id: request.token().get(),
7400                            packet_ms,
7401                            codec,
7402                            echo_cancellation: audio_processing.echo_cancellation,
7403                            telephone_event_payload,
7404                            source_address: source
7405                                .map(|endpoint| endpoint.address)
7406                                .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
7407                            source_port: source.map_or(0, |endpoint| endpoint.rtp_port),
7408                            encryption: None,
7409                            wire: None,
7410                        },
7411                        protocol,
7412                    )
7413                    .await?;
7414                }
7415                CommandAction::OpenMultimediaReceiveChannel {
7416                    call_id,
7417                    descriptor,
7418                } => {
7419                    let call_state = require_call(state, call_id)?.state;
7420                    if call_state != CallState::Connected {
7421                        return Err(ServerError::InvalidCallTransaction {
7422                            call_id,
7423                            operation: "open video receive media",
7424                            state: call_state,
7425                        });
7426                    }
7427                    validate_multimedia_receive(state, &descriptor)?;
7428                    let request = allocate_video_receive_identity(state, call_id)?;
7429                    let replacement_close = take_multimedia_receive_close(state, call_id);
7430                    let call = require_call_mut(state, call_id)?;
7431                    let line_instance = call.line_instance;
7432                    let call_reference = CallReference::new(call.wire_reference);
7433                    call.video_receive.leg = Some(VideoReceiveLeg {
7434                        request,
7435                        conference_id: descriptor.conference_id,
7436                        codec: descriptor.payload.codec(),
7437                        requested_address_type: descriptor.requested_address_type,
7438                        state: MediaChannelState::Opening,
7439                        deadline: Some(Instant::now() + HANDSET_ACKNOWLEDGEMENT_TIMEOUT),
7440                    });
7441
7442                    if let Some(close) = replacement_close {
7443                        send_message(stream, &close, protocol).await?;
7444                    }
7445                    send_message(
7446                        stream,
7447                        &ServerMessage::OpenMultimediaChannel(OpenMultimediaChannel {
7448                            conference_id: descriptor.conference_id,
7449                            passthrough_party_id: request.token().get().into(),
7450                            line_instance,
7451                            call_reference,
7452                            payload: descriptor.payload,
7453                            conference_creator: descriptor.conference_creator,
7454                            encryption: descriptor.encryption,
7455                            stream_passthrough_id: descriptor.stream_passthrough_id,
7456                            associated_stream_id: descriptor.associated_stream_id,
7457                            source: descriptor.source,
7458                            requested_address_type: descriptor.requested_address_type,
7459                        }),
7460                        protocol,
7461                    )
7462                    .await?;
7463                }
7464                CommandAction::CloseMultimediaReceiveChannel { call_id } => {
7465                    if let Some(close) = take_multimedia_receive_close(state, call_id) {
7466                        send_message(stream, &close, protocol).await?;
7467                    }
7468                }
7469                CommandAction::StartMultimediaTransmission {
7470                    call_id,
7471                    descriptor,
7472                } => {
7473                    let call_state = require_call(state, call_id)?.state;
7474                    if call_state != CallState::Connected {
7475                        return Err(ServerError::InvalidCallTransaction {
7476                            call_id,
7477                            operation: "start video transmit media",
7478                            state: call_state,
7479                        });
7480                    }
7481                    validate_multimedia_transmit(state, &descriptor)?;
7482                    let request = allocate_video_transmit_identity(state, call_id)?;
7483                    let replacement_stop = take_multimedia_transmit_stop(state, call_id);
7484                    let call_reference = {
7485                        let call = require_call_mut(state, call_id)?;
7486                        let call_reference = CallReference::new(call.wire_reference);
7487                        call.video_transmit.leg = Some(VideoTransmitLeg {
7488                            request,
7489                            conference_id: descriptor.conference_id,
7490                            codec: descriptor.payload.codec(),
7491                            address_type: address_type(descriptor.endpoint.address),
7492                            state: MediaChannelState::Opening,
7493                            deadline: Some(Instant::now() + HANDSET_ACKNOWLEDGEMENT_TIMEOUT),
7494                        });
7495                        call_reference
7496                    };
7497
7498                    if let Some(stop) = replacement_stop {
7499                        send_message(stream, &stop, protocol).await?;
7500                    }
7501                    send_message(
7502                        stream,
7503                        &ServerMessage::StartMultimediaTransmission(MultimediaTransmissionStart {
7504                            conference_id: descriptor.conference_id,
7505                            passthrough_party_id: request.token().get().into(),
7506                            endpoint: descriptor.endpoint,
7507                            call_reference,
7508                            payload: descriptor.payload,
7509                            traffic_class: descriptor.traffic_class,
7510                            encryption: descriptor.encryption,
7511                            stream_passthrough_id: descriptor.stream_passthrough_id,
7512                            associated_stream_id: descriptor.associated_stream_id,
7513                        }),
7514                        protocol,
7515                    )
7516                    .await?;
7517                }
7518                CommandAction::StopMultimediaTransmission { call_id } => {
7519                    if let Some(stop) = take_multimedia_transmit_stop(state, call_id) {
7520                        send_message(stream, &stop, protocol).await?;
7521                    }
7522                }
7523                flow_action @ (CommandAction::SetMultimediaTransmitBitRate {
7524                    call_id,
7525                    passthrough_party_id,
7526                    maximum_bit_rate,
7527                }
7528                | CommandAction::NotifyMultimediaTransmitBitRate {
7529                    call_id,
7530                    passthrough_party_id,
7531                    maximum_bit_rate,
7532                }) => {
7533                    if maximum_bit_rate == 0 {
7534                        return Err(ServerError::InvalidMultimediaTransmitControl(
7535                            "maximum bit rate must be nonzero",
7536                        ));
7537                    }
7538                    let (conference_id, call_reference) =
7539                        multimedia_transmit_control_identity(state, call_id, passthrough_party_id)?;
7540                    let flow = VideoFlowControl {
7541                        conference_id,
7542                        passthrough_party_id,
7543                        call_reference,
7544                        maximum_bit_rate,
7545                    };
7546                    let message = if matches!(
7547                        flow_action,
7548                        CommandAction::SetMultimediaTransmitBitRate { .. }
7549                    ) {
7550                        ServerMessage::FlowControlCommand(flow)
7551                    } else {
7552                        ServerMessage::FlowControlNotify(flow)
7553                    };
7554                    send_message(stream, &message, protocol).await?;
7555                }
7556                CommandAction::ControlMultimediaTransmission {
7557                    call_id,
7558                    passthrough_party_id,
7559                    control,
7560                } => {
7561                    let (conference_id, call_reference) =
7562                        multimedia_transmit_control_identity(state, call_id, passthrough_party_id)?;
7563                    let (command, data) = encode_multimedia_transmit_control(control)?;
7564                    send_message(
7565                        stream,
7566                        &ServerMessage::MiscellaneousCommand(MiscellaneousCommand {
7567                            conference_id,
7568                            passthrough_party_id,
7569                            call_reference,
7570                            command,
7571                            data,
7572                        }),
7573                        protocol,
7574                    )
7575                    .await?;
7576                }
7577                CommandAction::OpenOutboundMedia {
7578                    call_id,
7579                    source,
7580                    mut endpoint,
7581                    codec,
7582                    packet_ms,
7583                    max_frames_per_packet,
7584                    dtmf_mode,
7585                    audio_processing,
7586                    traffic_class,
7587                } => {
7588                    let call_state = require_call(state, call_id)?.state;
7589                    if !matches!(call_state, CallState::Proceed | CallState::RingOut) {
7590                        return Err(ServerError::InvalidCallTransaction {
7591                            call_id,
7592                            operation: "open coupled outbound media",
7593                            state: call_state,
7594                        });
7595                    }
7596                    let telephone_event_payload = dtmf_mode.telephone_event_payload(state.features);
7597                    let source_address = source
7598                        .map(|source| source.address)
7599                        .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
7600                    let source_port = source.map_or(0, |source| source.rtp_port);
7601                    let request = allocate_media_request_identity(state, call_id)?;
7602                    let call = require_call_mut(state, call_id)?;
7603                    call.media.requested = true;
7604                    call.media.codec = codec;
7605                    call.media.packet_ms = packet_ms;
7606                    call.media.max_frames_per_packet = max_frames_per_packet;
7607                    call.media.receive.telephone_event_payload = telephone_event_payload;
7608                    call.media.receive.peer = None;
7609                    call.media.receive.state = MediaChannelState::Opening;
7610                    call.media.receive.deadline =
7611                        Some(Instant::now() + HANDSET_ACKNOWLEDGEMENT_TIMEOUT);
7612                    call.media.receive.request = Some(request);
7613                    endpoint.telephone_event_payload = telephone_event_payload;
7614                    call.media.transmit.telephone_event_payload = telephone_event_payload;
7615                    call.media.transmit.peer = Some(endpoint);
7616                    call.media.transmit.state = MediaChannelState::Open;
7617                    call.media.transmit.deadline = None;
7618                    call.media.transmit.request = Some(request);
7619                    call.media.transmit_confirmation = TransmitConfirmation::Awaiting {
7620                        deadline: Instant::now() + HANDSET_ACKNOWLEDGEMENT_TIMEOUT,
7621                    };
7622                    call.media.coupled_transmit_endpoint = Some(endpoint);
7623                    let call = call.clone();
7624                    send_message(
7625                        stream,
7626                        &ServerMessage::OpenReceiveChannel {
7627                            call_reference: call.wire_reference,
7628                            passthrough_party_id: request.token().get(),
7629                            packet_ms,
7630                            codec,
7631                            echo_cancellation: audio_processing.echo_cancellation,
7632                            telephone_event_payload,
7633                            source_address,
7634                            source_port,
7635                            encryption: None,
7636                            wire: None,
7637                        },
7638                        protocol,
7639                    )
7640                    .await?;
7641                    send_message(
7642                        stream,
7643                        &ServerMessage::StartMediaTransmission {
7644                            call_reference: call.wire_reference,
7645                            passthrough_party_id: request.token().get(),
7646                            endpoint,
7647                            silence_suppression: audio_processing.silence_suppression,
7648                            traffic_class,
7649                            encryption: None,
7650                            wire: None,
7651                        },
7652                        protocol,
7653                    )
7654                    .await?;
7655                }
7656                CommandAction::CloseReceiveChannel { call_id, .. } => {
7657                    let call = require_call_mut(state, call_id)?;
7658                    call.media.coupled_transmit_endpoint = None;
7659                    if call.media.receive.state != MediaChannelState::Closed {
7660                        call.media.receive.state = MediaChannelState::Closed;
7661                        call.media.receive.deadline = None;
7662                        let call = call.clone();
7663                        send_message(
7664                            stream,
7665                            &ServerMessage::CloseReceiveChannel(AudioStreamControl {
7666                                conference_id: ConferenceId::new(call.wire_reference),
7667                                call_reference: CallReference::new(call.wire_reference),
7668                                passthrough_party_id: media_request_party_id(
7669                                    call.media.receive.request,
7670                                    call.wire_reference,
7671                                )
7672                                .into(),
7673                                port_handling_flag: 0,
7674                            }),
7675                            protocol,
7676                        )
7677                        .await?;
7678                    }
7679                }
7680                CommandAction::StartMedia {
7681                    call_id,
7682                    mut endpoint,
7683                    dtmf_mode,
7684                    audio_processing,
7685                    traffic_class,
7686                } => {
7687                    let telephone_event_payload = dtmf_mode.telephone_event_payload(state.features);
7688                    let request = {
7689                        let call = require_call(state, call_id)?;
7690                        if call.media.transmit.request.is_none() {
7691                            call.media.receive.request
7692                        } else {
7693                            None
7694                        }
7695                    };
7696                    let request = match request {
7697                        Some(request) => request,
7698                        None => allocate_media_request_identity(state, call_id)?,
7699                    };
7700                    let call = require_call_mut(state, call_id)?;
7701                    call.media.requested = true;
7702                    call.media.transmit.telephone_event_payload = telephone_event_payload;
7703                    endpoint.telephone_event_payload = telephone_event_payload;
7704                    call.media.transmit.peer = Some(endpoint);
7705                    call.media.transmit.state = MediaChannelState::Open;
7706                    call.media.transmit.deadline = None;
7707                    call.media.transmit.request = Some(request);
7708                    call.media.transmit_confirmation = TransmitConfirmation::Awaiting {
7709                        deadline: Instant::now() + HANDSET_ACKNOWLEDGEMENT_TIMEOUT,
7710                    };
7711                    call.media.coupled_transmit_endpoint = None;
7712                    let call = call.clone();
7713                    send_message(
7714                        stream,
7715                        &ServerMessage::StartMediaTransmission {
7716                            call_reference: call.wire_reference,
7717                            passthrough_party_id: request.token().get(),
7718                            endpoint,
7719                            silence_suppression: audio_processing.silence_suppression,
7720                            traffic_class,
7721                            encryption: None,
7722                            wire: None,
7723                        },
7724                        protocol,
7725                    )
7726                    .await?;
7727                }
7728                CommandAction::StartMulticastReception {
7729                    conference_id,
7730                    call_id,
7731                    route,
7732                    echo_cancellation,
7733                    g723_bitrate,
7734                } => {
7735                    validate_multicast_route(state, route, None)?;
7736                    let wire_call_reference = require_call(state, call_id)?.wire_reference;
7737                    let request = allocate_multicast_request_identity(state)?;
7738                    let key = MulticastKey {
7739                        conference_id,
7740                        call_id,
7741                    };
7742                    if let Some(stop) = take_multicast_stop(state, key, true) {
7743                        send_message(stream, &stop, protocol).await?;
7744                    }
7745                    send_message(
7746                        stream,
7747                        &ServerMessage::StartMulticastMediaReception(MulticastMediaReception {
7748                            conference_id,
7749                            passthrough_party_id: request.token().get().into(),
7750                            call_reference: CallReference::new(wire_call_reference),
7751                            address: route.address,
7752                            port: route.port,
7753                            packet_millis: route.packet_millis,
7754                            codec: route.codec,
7755                            echo_cancellation,
7756                            g723_bitrate,
7757                        }),
7758                        protocol,
7759                    )
7760                    .await?;
7761                    state
7762                        .multicast
7763                        .entry(key)
7764                        .or_insert_with(|| MulticastSession {
7765                            wire_call_reference,
7766                            receive: None,
7767                            transmit: None,
7768                        })
7769                        .receive = Some(MulticastReceive {
7770                        request,
7771                        route,
7772                        state: MulticastReceiveState::AwaitingAcknowledgement {
7773                            deadline: Instant::now() + HANDSET_ACKNOWLEDGEMENT_TIMEOUT,
7774                        },
7775                    });
7776                }
7777                CommandAction::StopMulticastReception {
7778                    conference_id,
7779                    call_id,
7780                } => {
7781                    let key = MulticastKey {
7782                        conference_id,
7783                        call_id,
7784                    };
7785                    if let Some(stop) = take_multicast_stop(state, key, true) {
7786                        send_message(stream, &stop, protocol).await?;
7787                    }
7788                }
7789                CommandAction::StartMulticastTransmission {
7790                    conference_id,
7791                    call_id,
7792                    route,
7793                    precedence,
7794                    silence_suppression,
7795                    max_frames_per_packet,
7796                    g723_bitrate,
7797                } => {
7798                    validate_multicast_route(state, route, Some(max_frames_per_packet))?;
7799                    let wire_call_reference = require_call(state, call_id)?.wire_reference;
7800                    let request = allocate_multicast_request_identity(state)?;
7801                    let key = MulticastKey {
7802                        conference_id,
7803                        call_id,
7804                    };
7805                    if let Some(stop) = take_multicast_stop(state, key, false) {
7806                        send_message(stream, &stop, protocol).await?;
7807                    }
7808                    send_message(
7809                        stream,
7810                        &ServerMessage::StartMulticastMediaTransmission(
7811                            MulticastMediaTransmission {
7812                                conference_id,
7813                                passthrough_party_id: request.token().get().into(),
7814                                call_reference: CallReference::new(wire_call_reference),
7815                                address: route.address,
7816                                port: route.port,
7817                                packet_millis: route.packet_millis,
7818                                codec: route.codec,
7819                                precedence,
7820                                silence_suppression: silence_suppression.wire_value(),
7821                                max_frames_per_packet,
7822                                g723_bitrate,
7823                            },
7824                        ),
7825                        protocol,
7826                    )
7827                    .await?;
7828                    state
7829                        .multicast
7830                        .entry(key)
7831                        .or_insert_with(|| MulticastSession {
7832                            wire_call_reference,
7833                            receive: None,
7834                            transmit: None,
7835                        })
7836                        .transmit = Some(MulticastTransmit { request, route });
7837                    context
7838                        .event_tx
7839                        .send(Event::device(
7840                            state.device.id.clone(),
7841                            state.generation,
7842                            DeviceEventKind::MulticastTransmissionStarted {
7843                                conference_id,
7844                                call_id,
7845                                route,
7846                            },
7847                        ))
7848                        .await
7849                        .map_err(|_| ServerError::Stopped)?;
7850                }
7851                CommandAction::StopMulticastTransmission {
7852                    conference_id,
7853                    call_id,
7854                } => {
7855                    let key = MulticastKey {
7856                        conference_id,
7857                        call_id,
7858                    };
7859                    if let Some(stop) = take_multicast_stop(state, key, false) {
7860                        send_message(stream, &stop, protocol).await?;
7861                    }
7862                }
7863                CommandAction::StopMedia { call_id, .. } => {
7864                    if let Some(call) = state
7865                        .calls_by_id
7866                        .get_mut(&call_id)
7867                        .filter(|call| call.media.transmit.state != MediaChannelState::Closed)
7868                    {
7869                        call.media.transmit.state = MediaChannelState::Closed;
7870                        call.media.transmit.deadline = None;
7871                        call.media.transmit_confirmation = TransmitConfirmation::Inactive;
7872                        call.media.coupled_transmit_endpoint = None;
7873                        let call = call.clone();
7874                        send_message(
7875                            stream,
7876                            &ServerMessage::StopMediaTransmission(AudioStreamControl {
7877                                conference_id: ConferenceId::new(call.wire_reference),
7878                                call_reference: CallReference::new(call.wire_reference),
7879                                passthrough_party_id: media_request_party_id(
7880                                    call.media.transmit.request,
7881                                    call.wire_reference,
7882                                )
7883                                .into(),
7884                                port_handling_flag: 0,
7885                            }),
7886                            protocol,
7887                        )
7888                        .await?;
7889                    }
7890                }
7891                CommandAction::CloseCall { call_id, .. } => {
7892                    if let Some(call) = state.calls_by_id.get(&call_id).cloned() {
7893                        let order = *context
7894                            .call_answer_order
7895                            .read()
7896                            .expect("SCCP call-answer-order lock poisoned");
7897                        let successor = incoming_successor(state, call_id, order);
7898                        let successor_has_ringer = successor.is_some_and(|(call_id, _)| {
7899                            state
7900                                .calls_by_id
7901                                .get(&call_id)
7902                                .and_then(|call| incoming_ringer(call.ringer, CallState::RingIn))
7903                                .is_some_and(ringer_is_audible)
7904                        });
7905                        let stop_ringer = !successor_has_ringer
7906                            && state.ringer_owner.is_none_or(|owner| owner == call_id);
7907                        state.active_key_mode = KeyMode::OnHook;
7908                        stop_call_multicast(stream, state, call_id, protocol).await?;
7909                        if call.state != CallState::OnHook {
7910                            close_call_media_messages(stream, &call, protocol).await?;
7911                            close_call_messages(
7912                                stream,
7913                                &call,
7914                                &state.device.soft_keys,
7915                                protocol,
7916                                context.config.timezone_offset_minutes,
7917                                stop_ringer,
7918                            )
7919                            .await?;
7920                            request_connection_statistics(stream, state, &call, context).await?;
7921                        }
7922                        remove_call(state, call_id);
7923                        if state.ringer_owner == Some(call_id) {
7924                            state.ringer_owner = None;
7925                        }
7926                        if let Some((call_id, promote)) = successor {
7927                            present_incoming_successor(stream, state, call_id, promote).await?;
7928                        }
7929                        refresh_mwi_lamps(stream, state, protocol).await?;
7930                    } else {
7931                        state.cancelled_calls.insert(call_id);
7932                    }
7933                }
7934            }
7935        }
7936    }
7937    Ok(false)
7938}
7939
7940async fn send_mwi_lamp(
7941    stream: &mut dyn StationIo,
7942    state: &SessionState,
7943    line_instance: u32,
7944    enabled: bool,
7945    protocol: ProtocolVersion,
7946) -> Result<(), ServerError> {
7947    let mode = projected_mwi_lamp(state.device.ui, state.active_call_id.is_some(), enabled);
7948    send_message(
7949        stream,
7950        &ServerMessage::SetLamp {
7951            stimulus: ButtonType::Voicemail,
7952            instance: line_instance,
7953            mode,
7954        },
7955        protocol,
7956    )
7957    .await
7958}
7959
7960fn projected_mwi_lamp(ui: crate::types::StationUiPolicy, on_call: bool, enabled: bool) -> LampMode {
7961    if enabled && (ui.mwi_on_call || !on_call) {
7962        ui.mwi_lamp_mode
7963    } else {
7964        LampMode::Off
7965    }
7966}
7967
7968fn updated_history_disposition(
7969    current: CallHistoryDisposition,
7970    state: CallState,
7971) -> CallHistoryDisposition {
7972    if current != CallHistoryDisposition::Missed {
7973        return current;
7974    }
7975    match state {
7976        CallState::Connected => CallHistoryDisposition::Received,
7977        CallState::RemoteMultiline => CallHistoryDisposition::Ignore,
7978        _ => current,
7979    }
7980}
7981
7982async fn refresh_mwi_lamps(
7983    stream: &mut dyn StationIo,
7984    state: &SessionState,
7985    protocol: ProtocolVersion,
7986) -> Result<(), ServerError> {
7987    for (&line_instance, &enabled) in &state.mwi_by_line {
7988        send_mwi_lamp(stream, state, line_instance, enabled, protocol).await?;
7989    }
7990    Ok(())
7991}
7992
7993fn incoming_ringer(
7994    ringer: Option<IncomingRing>,
7995    incoming_state: CallState,
7996) -> Option<IncomingRing> {
7997    ringer.map(|mut ringer| {
7998        if incoming_state == CallState::CallWaiting {
7999            ringer.duration = RingDuration::Single;
8000            if ringer.mode != RingerMode::Urgent {
8001                ringer.mode = RingerMode::Silent;
8002            }
8003        }
8004        ringer
8005    })
8006}
8007
8008const fn ringer_is_audible(ringer: IncomingRing) -> bool {
8009    !matches!(ringer.mode, RingerMode::Off | RingerMode::Silent)
8010}
8011
8012fn incoming_successor(
8013    state: &SessionState,
8014    removed_call_id: CallId,
8015    order: CallSelectionOrder,
8016) -> Option<(CallId, bool)> {
8017    let select = |call_state| {
8018        let candidates = state
8019            .calls_by_id
8020            .values()
8021            .filter(|call| call.call_id != removed_call_id && call.state == call_state);
8022        match order {
8023            CallSelectionOrder::OldestFirst => candidates.min_by_key(|call| call.call_id.0),
8024            CallSelectionOrder::LastFirst => candidates.max_by_key(|call| call.call_id.0),
8025        }
8026    };
8027    if let Some(call) = select(CallState::RingIn) {
8028        return Some((call.call_id, false));
8029    }
8030    let has_active_call = state.calls_by_id.values().any(|call| {
8031        call.call_id != removed_call_id
8032            && matches!(
8033                call.state,
8034                CallState::Connected | CallState::Hold | CallState::HoldYellow | CallState::HoldRed
8035            )
8036    });
8037    (!has_active_call)
8038        .then(|| select(CallState::CallWaiting))
8039        .flatten()
8040        .map(|call| (call.call_id, true))
8041}
8042
8043async fn present_incoming_successor(
8044    stream: &mut dyn StationIo,
8045    state: &mut SessionState,
8046    call_id: CallId,
8047    promote: bool,
8048) -> Result<(), ServerError> {
8049    if promote && let Some(call) = state.calls_by_id.get_mut(&call_id) {
8050        call.state = CallState::RingIn;
8051    }
8052    let call = state
8053        .calls_by_id
8054        .get(&call_id)
8055        .expect("incoming successor came from session state")
8056        .clone();
8057    state.active_call_id = Some(call_id);
8058    state.active_key_mode = KeyMode::RingIn;
8059    if promote {
8060        send_message(
8061            stream,
8062            &ServerMessage::CallState {
8063                state: CallState::RingIn,
8064                line_instance: call.line_instance,
8065                call_reference: call.wire_reference,
8066            },
8067            state.registration.protocol,
8068        )
8069        .await?;
8070    }
8071    send_message(
8072        stream,
8073        &ServerMessage::SetLamp {
8074            stimulus: ButtonType::Line,
8075            instance: call.line_instance,
8076            mode: LampMode::Blink,
8077        },
8078        state.registration.protocol,
8079    )
8080    .await?;
8081    if let Some(ringer) = incoming_ringer(call.ringer, CallState::RingIn) {
8082        let audible = ringer_is_audible(ringer);
8083        if audible || state.ringer_owner.is_none() {
8084            send_message(
8085                stream,
8086                &ServerMessage::SetRinger {
8087                    mode: ringer.mode,
8088                    duration: ringer.duration,
8089                    line_instance: call.line_instance,
8090                    call_reference: call.wire_reference,
8091                },
8092                state.registration.protocol,
8093            )
8094            .await?;
8095        }
8096        if audible {
8097            state.ringer_owner = Some(call_id);
8098        }
8099    }
8100    send_message(
8101        stream,
8102        &ServerMessage::SelectSoftKeys {
8103            line_instance: call.line_instance,
8104            call_reference: call.wire_reference,
8105            set: KeyMode::RingIn,
8106            valid_mask: state.device.soft_keys.valid_mask(KeyMode::RingIn),
8107        },
8108        state.registration.protocol,
8109    )
8110    .await?;
8111    Ok(())
8112}
8113
8114async fn request_connection_statistics(
8115    stream: &mut dyn StationIo,
8116    state: &mut SessionState,
8117    call: &SessionCall,
8118    context: &SessionContext,
8119) -> Result<(), ServerError> {
8120    prune_connection_statistics(&mut state.pending_connection_statistics, Instant::now());
8121    if !call.media.requested
8122        || state.pending_connection_statistics.len() >= MAX_PENDING_CONNECTION_STATISTICS
8123        || state.statistics_references.len() >= MAX_STATISTICS_REFERENCES_PER_SESSION
8124    {
8125        return Ok(());
8126    }
8127    let directory_number = if call.statistics_directory_number.is_empty() {
8128        call.dialed_number.trim()
8129    } else {
8130        call.statistics_directory_number.trim()
8131    };
8132    let maximum = if state.registration.protocol >= ProtocolVersion::V19 {
8133        24
8134    } else {
8135        23
8136    };
8137    if directory_number.is_empty()
8138        || directory_number.len() > maximum
8139        || directory_number.contains(['\0', '\r', '\n'])
8140    {
8141        warn!(
8142            device_id = %state.device.id,
8143            ?call.call_id,
8144            byte_count = directory_number.len(),
8145            "skipping connection-statistics request with unusable directory number"
8146        );
8147        return Ok(());
8148    }
8149    if !state.statistics_references.insert(call.wire_reference) {
8150        warn!(
8151            device_id = %state.device.id,
8152            ?call.call_id,
8153            call_reference = call.wire_reference,
8154            "skipping connection-statistics request for a reused call reference"
8155        );
8156        return Ok(());
8157    }
8158    let request_generation = context
8159        .next_statistics_generation
8160        .fetch_add(1, Ordering::Relaxed);
8161    let processing = StatisticsProcessing::Clear;
8162    let session_generation = state.generation;
8163    state.pending_connection_statistics.insert(
8164        call.wire_reference,
8165        PendingConnectionStatistics {
8166            session_generation,
8167            request_generation,
8168            call_id: call.call_id,
8169            line_instance: call.line_instance,
8170            codec: call.media.codec,
8171            packet_ms: call.media.packet_ms,
8172            max_frames_per_packet: call.media.max_frames_per_packet,
8173            receive_peer: call.media.receive.peer,
8174            transmit_peer: call.media.transmit.peer,
8175            directory_number: directory_number.to_owned(),
8176            processing,
8177            expires_at: Instant::now() + CONNECTION_STATISTICS_TIMEOUT,
8178        },
8179    );
8180    send_message(
8181        stream,
8182        &ServerMessage::ConnectionStatisticsRequest {
8183            directory_number: directory_number.to_owned(),
8184            call_reference: call.wire_reference,
8185            processing,
8186        },
8187        state.registration.protocol,
8188    )
8189    .await
8190}
8191
8192fn statistics_directory_for_call_info(info: &CallInfo) -> &str {
8193    match info.direction {
8194        crate::types::CallDirection::Inbound => &info.calling_number,
8195        crate::types::CallDirection::Outbound => &info.called_number,
8196    }
8197}
8198
8199fn prune_connection_statistics(
8200    pending_statistics: &mut HashMap<u32, PendingConnectionStatistics>,
8201    now: Instant,
8202) {
8203    pending_statistics.retain(|_, pending| pending.expires_at > now);
8204}
8205
8206async fn collect_connection_statistics(
8207    state: &mut SessionState,
8208    statistics: ConnectionStatistics,
8209    context: &SessionContext,
8210) -> Result<(), ServerError> {
8211    prune_connection_statistics(&mut state.pending_connection_statistics, Instant::now());
8212    let Some(pending) = state
8213        .pending_connection_statistics
8214        .get(&statistics.call_reference)
8215        .cloned()
8216    else {
8217        warn!(
8218            device_id = %state.device.id,
8219            call_reference = statistics.call_reference,
8220            "ignoring unsolicited or expired connection-statistics response"
8221        );
8222        return Ok(());
8223    };
8224    let current_session = context
8225        .sessions
8226        .lock()
8227        .await
8228        .get(&state.device.id)
8229        .is_some_and(|session| session.generation == pending.session_generation);
8230    if !current_session
8231        || pending.session_generation != state.generation
8232        || statistics.processing != pending.processing
8233        || statistics.directory_number != pending.directory_number
8234    {
8235        warn!(
8236            device_id = %state.device.id,
8237            call_reference = statistics.call_reference,
8238            processing = ?statistics.processing,
8239            "ignoring mismatched connection-statistics response"
8240        );
8241        return Ok(());
8242    }
8243    state
8244        .pending_connection_statistics
8245        .remove(&statistics.call_reference);
8246    let snapshot = MediaStatisticsSnapshot {
8247        request_generation: pending.request_generation,
8248        call_id: pending.call_id,
8249        line_instance: LineInstance::new(pending.line_instance),
8250        codec: pending.codec,
8251        packet_ms: pending.packet_ms,
8252        max_frames_per_packet: pending.max_frames_per_packet,
8253        receive_peer: pending.receive_peer,
8254        transmit_peer: pending.transmit_peer,
8255        packets_sent: statistics.packets_sent,
8256        octets_sent: statistics.octets_sent,
8257        packets_received: statistics.packets_received,
8258        octets_received: statistics.octets_received,
8259        packets_lost: statistics.packets_lost,
8260        jitter_millis: statistics.jitter_millis,
8261        latency_millis: statistics.latency_millis,
8262        quality_byte_count: statistics.quality.as_bytes().len(),
8263    };
8264    {
8265        let mut latest = context
8266            .latest_media_statistics
8267            .write()
8268            .expect("SCCP media-statistics lock poisoned");
8269        let replace = latest
8270            .get(&state.device.id)
8271            .is_none_or(|existing| existing.request_generation < snapshot.request_generation);
8272        if !replace {
8273            return Ok(());
8274        }
8275        latest.insert(state.device.id.clone(), snapshot.clone());
8276    }
8277    context
8278        .event_tx
8279        .send(Event::device(
8280            state.device.id.clone(),
8281            state.generation,
8282            DeviceEventKind::ConnectionStatisticsCollected { snapshot },
8283        ))
8284        .await
8285        .map_err(|_| ServerError::Stopped)
8286}
8287
8288fn status_message_frames(
8289    message: HandsetStatusMessage,
8290    device_type: DeviceType,
8291    persistent: &mut bool,
8292) -> Vec<ServerMessage> {
8293    let prompt_for_timed_message = matches!(
8294        device_type,
8295        DeviceType::Cisco6901
8296            | DeviceType::Cisco6921
8297            | DeviceType::Cisco6941
8298            | DeviceType::Cisco6945
8299            | DeviceType::Cisco6961
8300    );
8301    match message {
8302        HandsetStatusMessage::Display {
8303            text,
8304            timeout_seconds,
8305            priority: Some(priority),
8306        } => vec![ServerMessage::DisplayPriorityNotify {
8307            timeout_seconds: u32::from(timeout_seconds),
8308            priority,
8309            text,
8310        }],
8311        HandsetStatusMessage::Clear {
8312            priority: Some(priority),
8313        } => vec![ServerMessage::ClearPriorityNotify { priority }],
8314        HandsetStatusMessage::Display {
8315            text,
8316            timeout_seconds,
8317            priority: None,
8318        } if timeout_seconds == 0 || prompt_for_timed_message => {
8319            if timeout_seconds == 0 {
8320                *persistent = true;
8321            }
8322            vec![ServerMessage::DisplayPrompt {
8323                timeout_seconds: u32::from(timeout_seconds),
8324                text,
8325                line_instance: 0,
8326                call_reference: 0,
8327            }]
8328        }
8329        HandsetStatusMessage::Display {
8330            text,
8331            timeout_seconds,
8332            priority: None,
8333        } => vec![ServerMessage::DisplayPriorityNotify {
8334            timeout_seconds: u32::from(timeout_seconds),
8335            priority: NotificationPriority::Timed,
8336            text,
8337        }],
8338        HandsetStatusMessage::Clear { priority: None } => {
8339            let clear_prompt = std::mem::take(persistent) || prompt_for_timed_message;
8340            let mut frames = Vec::with_capacity(2);
8341            if clear_prompt {
8342                frames.push(ServerMessage::ClearPrompt {
8343                    line_instance: 0,
8344                    call_reference: 0,
8345                });
8346            }
8347            if !prompt_for_timed_message {
8348                frames.push(ServerMessage::ClearPriorityNotify {
8349                    priority: NotificationPriority::Timed,
8350                });
8351            }
8352            frames
8353        }
8354    }
8355}
8356
8357async fn send_message(
8358    stream: &mut dyn StationIo,
8359    message: &ServerMessage,
8360    session: impl Into<StationSessionContext>,
8361) -> Result<(), ServerError> {
8362    stream
8363        .write_all(&message.encode_for_session(session.into())?)
8364        .await?;
8365    Ok(())
8366}
8367
8368async fn send_station_ui_message(
8369    stream: &mut dyn StationIo,
8370    state: &SessionState,
8371    message: &ServerMessage,
8372) -> Result<(), ServerError> {
8373    let session = state.station_context();
8374    let bytes = if state.features.contains(PhoneFeatures::UTF8) {
8375        message.encode_for_session(session)?
8376    } else {
8377        message.encode_for_legacy_session(session, state.device.ui.legacy_code_page)?
8378    };
8379    stream.write_all(&bytes).await?;
8380    Ok(())
8381}
8382
8383async fn begin_phone_call_ui(
8384    stream: &mut dyn StationIo,
8385    call: &SessionCall,
8386    device: &DeviceDefinition,
8387    session: StationSessionContext,
8388) -> Result<(), ServerError> {
8389    begin_phone_call_ui_with_key_mode(stream, call, device, KeyMode::OffHook, session).await
8390}
8391
8392async fn begin_phone_call_ui_with_key_mode(
8393    stream: &mut dyn StationIo,
8394    call: &SessionCall,
8395    device: &DeviceDefinition,
8396    key_mode: KeyMode,
8397    session: StationSessionContext,
8398) -> Result<(), ServerError> {
8399    let initial_tone = device
8400        .line(call.line_instance)
8401        .map_or(Tone::InsideDial, |line| line.initial_tone);
8402    send_message(
8403        stream,
8404        &ServerMessage::SetSpeakerMode(SpeakerMode::On),
8405        session,
8406    )
8407    .await?;
8408    send_message(
8409        stream,
8410        &ServerMessage::SetLamp {
8411            stimulus: ButtonType::Line,
8412            instance: call.line_instance,
8413            mode: LampMode::On,
8414        },
8415        session,
8416    )
8417    .await?;
8418    send_message(
8419        stream,
8420        &ServerMessage::CallState {
8421            state: CallState::OffHook,
8422            line_instance: call.line_instance,
8423            call_reference: call.wire_reference,
8424        },
8425        session,
8426    )
8427    .await?;
8428    send_message(
8429        stream,
8430        &ServerMessage::ActivateCallPlane {
8431            line_instance: call.line_instance,
8432        },
8433        session,
8434    )
8435    .await?;
8436    send_message(
8437        stream,
8438        &ServerMessage::DisplayPrompt {
8439            timeout_seconds: 0,
8440            text: "Enter number".into(),
8441            line_instance: call.line_instance,
8442            call_reference: call.wire_reference,
8443        },
8444        session,
8445    )
8446    .await?;
8447    send_message(
8448        stream,
8449        &ServerMessage::StartTone {
8450            tone: initial_tone,
8451            direction: ToneDirection::User,
8452            line_instance: call.line_instance,
8453            call_reference: call.wire_reference,
8454        },
8455        session,
8456    )
8457    .await?;
8458    send_message(
8459        stream,
8460        &ServerMessage::SelectSoftKeys {
8461            line_instance: call.line_instance,
8462            call_reference: call.wire_reference,
8463            set: key_mode,
8464            valid_mask: device.soft_keys.valid_mask(key_mode),
8465        },
8466        session,
8467    )
8468    .await
8469}
8470
8471async fn begin_answer_ui(
8472    stream: &mut dyn StationIo,
8473    call: &SessionCall,
8474    protocol: ProtocolVersion,
8475) -> Result<(), ServerError> {
8476    send_message(
8477        stream,
8478        &ServerMessage::SetRinger {
8479            mode: RingerMode::Off,
8480            duration: RingDuration::Normal,
8481            line_instance: call.line_instance,
8482            call_reference: call.wire_reference,
8483        },
8484        protocol,
8485    )
8486    .await?;
8487    send_message(
8488        stream,
8489        &ServerMessage::CallState {
8490            state: CallState::OffHook,
8491            line_instance: call.line_instance,
8492            call_reference: call.wire_reference,
8493        },
8494        protocol,
8495    )
8496    .await?;
8497    send_message(
8498        stream,
8499        &ServerMessage::ActivateCallPlane {
8500            line_instance: call.line_instance,
8501        },
8502        protocol,
8503    )
8504    .await?;
8505    send_message(
8506        stream,
8507        &ServerMessage::StopTone {
8508            line_instance: call.line_instance,
8509            call_reference: call.wire_reference,
8510        },
8511        protocol,
8512    )
8513    .await?;
8514    send_message(
8515        stream,
8516        &ServerMessage::SetLamp {
8517            stimulus: ButtonType::Line,
8518            instance: call.line_instance,
8519            mode: LampMode::On,
8520        },
8521        protocol,
8522    )
8523    .await?;
8524    Ok(())
8525}
8526
8527async fn prepare_call_state_ui(
8528    stream: &mut dyn StationIo,
8529    call: &SessionCall,
8530    state: CallState,
8531    protocol: ProtocolVersion,
8532) -> Result<(), ServerError> {
8533    match state {
8534        CallState::Connected => {
8535            send_message(
8536                stream,
8537                &ServerMessage::SetRinger {
8538                    mode: RingerMode::Off,
8539                    duration: RingDuration::Normal,
8540                    line_instance: call.line_instance,
8541                    call_reference: call.wire_reference,
8542                },
8543                protocol,
8544            )
8545            .await?;
8546            send_message(
8547                stream,
8548                &ServerMessage::SetSpeakerMode(SpeakerMode::On),
8549                protocol,
8550            )
8551            .await?;
8552            send_message(
8553                stream,
8554                &ServerMessage::StopTone {
8555                    line_instance: call.line_instance,
8556                    call_reference: call.wire_reference,
8557                },
8558                protocol,
8559            )
8560            .await?;
8561            send_message(
8562                stream,
8563                &ServerMessage::SetLamp {
8564                    stimulus: ButtonType::Line,
8565                    instance: call.line_instance,
8566                    mode: LampMode::On,
8567                },
8568                protocol,
8569            )
8570            .await?;
8571        }
8572        CallState::RemoteMultiline => {
8573            send_message(
8574                stream,
8575                &ServerMessage::SetRinger {
8576                    mode: RingerMode::Off,
8577                    duration: RingDuration::Normal,
8578                    line_instance: call.line_instance,
8579                    call_reference: call.wire_reference,
8580                },
8581                protocol,
8582            )
8583            .await?;
8584            send_message(
8585                stream,
8586                &ServerMessage::SetSpeakerMode(SpeakerMode::Off),
8587                protocol,
8588            )
8589            .await?;
8590            send_message(
8591                stream,
8592                &ServerMessage::SetLamp {
8593                    stimulus: ButtonType::Line,
8594                    instance: call.line_instance,
8595                    mode: LampMode::On,
8596                },
8597                protocol,
8598            )
8599            .await?;
8600        }
8601        CallState::OnHook => {
8602            send_message(
8603                stream,
8604                &ServerMessage::SetRinger {
8605                    mode: RingerMode::Off,
8606                    duration: RingDuration::Normal,
8607                    line_instance: call.line_instance,
8608                    call_reference: call.wire_reference,
8609                },
8610                protocol,
8611            )
8612            .await?;
8613        }
8614        CallState::Hold | CallState::HoldYellow | CallState::HoldRed => {
8615            send_message(
8616                stream,
8617                &ServerMessage::SetLamp {
8618                    stimulus: ButtonType::Line,
8619                    instance: call.line_instance,
8620                    mode: LampMode::Wink,
8621                },
8622                protocol,
8623            )
8624            .await?;
8625        }
8626        CallState::RingOut | CallState::Proceed => {
8627            send_message(
8628                stream,
8629                &ServerMessage::SetLamp {
8630                    stimulus: ButtonType::Line,
8631                    instance: call.line_instance,
8632                    mode: LampMode::Blink,
8633                },
8634                protocol,
8635            )
8636            .await?;
8637        }
8638        _ => {}
8639    }
8640    Ok(())
8641}
8642
8643async fn finish_call_state_ui(
8644    stream: &mut dyn StationIo,
8645    call: &SessionCall,
8646    state: CallState,
8647    session: StationSessionContext,
8648) -> Result<(), ServerError> {
8649    let prompt = match state {
8650        CallState::Connected => Some("Connected"),
8651        CallState::Hold | CallState::HoldYellow | CallState::HoldRed => Some("Hold"),
8652        CallState::RingOut => Some("Ring out"),
8653        CallState::Proceed => Some("Call proceeding"),
8654        CallState::Busy => Some("Busy"),
8655        CallState::Congestion => Some("Network congestion"),
8656        CallState::InvalidNumber => Some("Unknown number"),
8657        _ => None,
8658    };
8659    if state == CallState::Connected {
8660        send_message(
8661            stream,
8662            &ServerMessage::ActivateCallPlane {
8663                line_instance: call.line_instance,
8664            },
8665            session,
8666        )
8667        .await?;
8668    } else if matches!(
8669        state,
8670        CallState::Hold | CallState::HoldYellow | CallState::HoldRed
8671    ) {
8672        send_message(
8673            stream,
8674            &ServerMessage::SetSpeakerMode(SpeakerMode::Off),
8675            session,
8676        )
8677        .await?;
8678    }
8679    if let Some(text) = prompt {
8680        send_message(
8681            stream,
8682            &ServerMessage::DisplayPrompt {
8683                timeout_seconds: 0,
8684                text: text.into(),
8685                line_instance: call.line_instance,
8686                call_reference: call.wire_reference,
8687            },
8688            session,
8689        )
8690        .await?;
8691    }
8692    Ok(())
8693}
8694
8695fn normalize_line(state: &SessionState, requested: u32) -> u32 {
8696    if requested != 0 && state.device.line(requested).is_some() {
8697        requested
8698    } else {
8699        state.device.first_line().map_or(1, |line| line.instance)
8700    }
8701}
8702
8703fn ensure_phone_call(
8704    state: &mut SessionState,
8705    wire_reference: u32,
8706    line_instance: u32,
8707    next: &AtomicU64,
8708) -> SessionCall {
8709    let reusable = if wire_reference == 0 {
8710        state
8711            .calls_by_id
8712            .values()
8713            .filter(|call| call.state != CallState::OnHook)
8714            .max_by_key(|call| call.call_id.0)
8715    } else {
8716        find_call(state, wire_reference).filter(|call| call.state != CallState::OnHook)
8717    };
8718    if let Some(call) = reusable {
8719        return call.clone();
8720    }
8721    let mut call = reserve_phone_call(state, line_instance, next);
8722    if wire_reference != 0
8723        && wire_reference != call.wire_reference
8724        && !state.statistics_references.contains(&wire_reference)
8725    {
8726        state.calls_by_wire.remove(&call.wire_reference);
8727        call.wire_reference = wire_reference;
8728        state.calls_by_wire.insert(wire_reference, call.call_id);
8729        state.calls_by_id.insert(call.call_id, call.clone());
8730    }
8731    call
8732}
8733
8734fn reserve_phone_call(
8735    state: &mut SessionState,
8736    line_instance: u32,
8737    next: &AtomicU64,
8738) -> SessionCall {
8739    let call_id = CallId(next.fetch_add(1, Ordering::Relaxed));
8740    insert_call(
8741        state,
8742        call_id,
8743        line_instance,
8744        Codec::Pcmu,
8745        CallState::OffHook,
8746    )
8747}
8748
8749fn insert_call(
8750    state: &mut SessionState,
8751    call_id: CallId,
8752    line_instance: u32,
8753    codec: Codec,
8754    call_state: CallState,
8755) -> SessionCall {
8756    let mut wire_reference = (call_id.0 as u32).max(1);
8757    while state.calls_by_wire.contains_key(&wire_reference)
8758        || state.statistics_references.contains(&wire_reference)
8759    {
8760        wire_reference = wire_reference.wrapping_add(1).max(1);
8761    }
8762    let call = SessionCall {
8763        call_id,
8764        wire_reference,
8765        line_instance,
8766        media: CallMedia::new(codec),
8767        video_receive: VideoReceive::default(),
8768        video_transmit: VideoTransmit::default(),
8769        state: call_state,
8770        ringer: None,
8771        history_disposition: if matches!(call_state, CallState::RingIn | CallState::CallWaiting) {
8772            CallHistoryDisposition::Missed
8773        } else {
8774            CallHistoryDisposition::Placed
8775        },
8776        dialed_number: String::new(),
8777        statistics_directory_number: String::new(),
8778        transfer_role: None,
8779    };
8780    state.calls_by_wire.insert(wire_reference, call_id);
8781    state.calls_by_id.insert(call_id, call.clone());
8782    call
8783}
8784
8785fn find_call(state: &SessionState, wire_reference: u32) -> Option<&SessionCall> {
8786    if wire_reference != 0 {
8787        state
8788            .calls_by_wire
8789            .get(&wire_reference)
8790            .and_then(|id| state.calls_by_id.get(id))
8791    } else {
8792        state
8793            .active_call_id
8794            .and_then(|call_id| state.calls_by_id.get(&call_id))
8795            .or_else(|| {
8796                (state.calls_by_id.len() == 1)
8797                    .then(|| state.calls_by_id.values().next())
8798                    .flatten()
8799            })
8800    }
8801}
8802
8803fn find_answer_call(
8804    state: &SessionState,
8805    wire_reference: u32,
8806    line_instance: u32,
8807    order: CallSelectionOrder,
8808) -> Option<&SessionCall> {
8809    let matches_line = |call: &&SessionCall| {
8810        matches!(call.state, CallState::RingIn | CallState::CallWaiting)
8811            && (line_instance == 0 || call.line_instance == line_instance)
8812    };
8813    if wire_reference != 0 {
8814        return state
8815            .calls_by_wire
8816            .get(&wire_reference)
8817            .and_then(|call_id| state.calls_by_id.get(call_id))
8818            .filter(matches_line);
8819    }
8820    if let Some(active) = state
8821        .active_call_id
8822        .and_then(|call_id| state.calls_by_id.get(&call_id))
8823        .filter(matches_line)
8824    {
8825        return Some(active);
8826    }
8827    let candidates = state.calls_by_id.values().filter(matches_line);
8828    match order {
8829        CallSelectionOrder::OldestFirst => candidates.min_by_key(|call| call.call_id.0),
8830        CallSelectionOrder::LastFirst => candidates.max_by_key(|call| call.call_id.0),
8831    }
8832}
8833
8834fn find_receive_media_call_id(
8835    state: &SessionState,
8836    wire_reference: u32,
8837    passthrough_party_id: u32,
8838) -> Option<CallId> {
8839    find_media_call_id(state, wire_reference, passthrough_party_id, |call| {
8840        call.media.receive.request
8841    })
8842}
8843
8844fn find_multicast_receive_key(
8845    state: &SessionState,
8846    wire_reference: u32,
8847    passthrough_party_id: u32,
8848) -> Option<MulticastKey> {
8849    state.multicast.iter().find_map(|(key, session)| {
8850        session.receive.as_ref().and_then(|receive| {
8851            (matches!(
8852                receive.state,
8853                MulticastReceiveState::AwaitingAcknowledgement { .. }
8854            ) && session.wire_call_reference == wire_reference
8855                && receive.request.token().get() == passthrough_party_id)
8856                .then_some(*key)
8857        })
8858    })
8859}
8860
8861fn find_multicast_transmit_key(
8862    state: &SessionState,
8863    conference_id: u32,
8864    wire_reference: u32,
8865    passthrough_party_id: u32,
8866    address: IpAddr,
8867    port: u16,
8868) -> Option<MulticastKey> {
8869    state.multicast.iter().find_map(|(key, session)| {
8870        session.transmit.as_ref().and_then(|transmit| {
8871            (key.conference_id.get() == conference_id
8872                && session.wire_call_reference == wire_reference
8873                && transmit.request.token().get() == passthrough_party_id
8874                && canonical_ip_address(transmit.route.address) == canonical_ip_address(address)
8875                && transmit.route.port == port)
8876                .then_some(*key)
8877        })
8878    })
8879}
8880
8881fn find_transmit_media_call_id(
8882    state: &SessionState,
8883    conference_id: u32,
8884    wire_reference: u32,
8885    passthrough_party_id: u32,
8886) -> Option<CallId> {
8887    find_media_call_id(state, wire_reference, passthrough_party_id, |call| {
8888        call.media.transmit.request
8889    })
8890    .filter(|call_id| {
8891        state
8892            .calls_by_id
8893            .get(call_id)
8894            .is_some_and(|call| conference_id == 0 || conference_id == call.wire_reference)
8895    })
8896}
8897
8898fn find_media_call_id(
8899    state: &SessionState,
8900    wire_reference: u32,
8901    passthrough_party_id: u32,
8902    request: impl Fn(&SessionCall) -> Option<MediaRequestIdentity>,
8903) -> Option<CallId> {
8904    state
8905        .calls_by_id
8906        .values()
8907        .find(|call| {
8908            request(call).is_some_and(|identity| {
8909                identity.accepts_ack(passthrough_party_id, wire_reference, call.wire_reference)
8910            })
8911        })
8912        .map(|call| call.call_id)
8913}
8914
8915fn require_call(state: &SessionState, call_id: CallId) -> Result<&SessionCall, ServerError> {
8916    state
8917        .calls_by_id
8918        .get(&call_id)
8919        .ok_or(ServerError::UnknownCall(call_id))
8920}
8921
8922fn require_call_mut(
8923    state: &mut SessionState,
8924    call_id: CallId,
8925) -> Result<&mut SessionCall, ServerError> {
8926    state
8927        .calls_by_id
8928        .get_mut(&call_id)
8929        .ok_or(ServerError::UnknownCall(call_id))
8930}
8931
8932fn address_matches_type(address: IpAddr, requested: IpAddressType) -> bool {
8933    match requested {
8934        IpAddressType::Ipv4 => address.is_ipv4(),
8935        IpAddressType::Ipv6 => address.is_ipv6(),
8936        IpAddressType::Ipv4AndIpv6 => true,
8937        IpAddressType::Invalid | IpAddressType::Unknown(_) => false,
8938    }
8939}
8940
8941fn address_type(address: IpAddr) -> IpAddressType {
8942    if address.is_ipv4() {
8943        IpAddressType::Ipv4
8944    } else {
8945        IpAddressType::Ipv6
8946    }
8947}
8948
8949fn endpoint_is_usable(endpoint: MediaEndpointAddress) -> bool {
8950    endpoint.port != 0 && !endpoint.address.is_unspecified() && !endpoint.address.is_multicast()
8951}
8952
8953fn capability_supports_address(
8954    advertised: Option<IpAddressType>,
8955    requested: IpAddressType,
8956) -> bool {
8957    match advertised {
8958        None => requested == IpAddressType::Ipv4,
8959        Some(IpAddressType::Ipv4AndIpv6) => true,
8960        Some(address_type) => address_type == requested,
8961    }
8962}
8963
8964fn validate_multimedia_receive_descriptor(
8965    descriptor: &MultimediaReceiveDescriptor,
8966) -> Result<(), ServerError> {
8967    if !descriptor
8968        .payload
8969        .is_direction(MultimediaPayloadDirection::Receive)
8970    {
8971        return Err(ServerError::InvalidMultimediaReceive(
8972            "payload was not decoded from a receive message",
8973        ));
8974    }
8975    if descriptor.payload.codec().kind() != CodecKind::Video {
8976        return Err(ServerError::InvalidMultimediaReceive("codec is not video"));
8977    }
8978    if !address_matches_type(descriptor.source.address, descriptor.requested_address_type) {
8979        return Err(ServerError::InvalidMultimediaReceive(
8980            "source address does not match the requested address type",
8981        ));
8982    }
8983    if descriptor.source.address.is_multicast() {
8984        return Err(ServerError::InvalidMultimediaReceive(
8985            "source address must not be multicast",
8986        ));
8987    }
8988    Ok(())
8989}
8990
8991fn validate_multimedia_receive(
8992    state: &SessionState,
8993    descriptor: &MultimediaReceiveDescriptor,
8994) -> Result<(), ServerError> {
8995    validate_multimedia_receive_descriptor(descriptor)?;
8996
8997    if !descriptor.payload.is_valid_for(
8998        MultimediaPayloadDirection::Receive,
8999        state.registration.protocol,
9000    ) {
9001        return Err(ServerError::InvalidMultimediaReceive(
9002            "payload protocol does not match the live session",
9003        ));
9004    }
9005
9006    match state.registration.protocol {
9007        protocol if protocol < ProtocolVersion::V12 => {
9008            if descriptor.source
9009                != (MediaEndpointAddress {
9010                    address: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
9011                    port: 0,
9012                })
9013                || descriptor.requested_address_type != IpAddressType::Ipv4
9014            {
9015                return Err(ServerError::InvalidMultimediaReceive(
9016                    "this protocol version cannot carry a source endpoint",
9017                ));
9018            }
9019        }
9020        protocol
9021            if protocol < ProtocolVersion::V17
9022                && (!descriptor.source.address.is_ipv4()
9023                    || descriptor.requested_address_type != IpAddressType::Ipv4) =>
9024        {
9025            return Err(ServerError::InvalidMultimediaReceive(
9026                "this protocol version carries only IPv4 video endpoints",
9027            ));
9028        }
9029        _ => {}
9030    }
9031
9032    let supported = state.media_capabilities.video().iter().any(|capability| {
9033        let encryption_supported = descriptor.encryption.is_none()
9034            || capability.encryption_capability == Some(EncryptionCapability::Capable);
9035        capability.codec == descriptor.payload.codec()
9036            && capability.direction.contains(ReceiveTransmit::RECEIVE)
9037            && capability_supports_address(
9038                capability.address_type,
9039                descriptor.requested_address_type,
9040            )
9041            && encryption_supported
9042    });
9043    supported
9044        .then_some(())
9045        .ok_or(ServerError::UnsupportedMultimediaReceive)
9046}
9047
9048fn validate_multimedia_transmit_descriptor(
9049    descriptor: &MultimediaTransmitDescriptor,
9050) -> Result<(), ServerError> {
9051    if !descriptor
9052        .payload
9053        .is_direction(MultimediaPayloadDirection::Transmit)
9054    {
9055        return Err(ServerError::InvalidMultimediaTransmit(
9056            "payload was not decoded from a transmit message",
9057        ));
9058    }
9059    if descriptor.payload.codec().kind() != CodecKind::Video {
9060        return Err(ServerError::InvalidMultimediaTransmit("codec is not video"));
9061    }
9062    if !endpoint_is_usable(descriptor.endpoint) {
9063        return Err(ServerError::InvalidMultimediaTransmit(
9064            "destination endpoint must be unicast and nonzero",
9065        ));
9066    }
9067    Ok(())
9068}
9069
9070fn validate_multimedia_transmit(
9071    state: &SessionState,
9072    descriptor: &MultimediaTransmitDescriptor,
9073) -> Result<(), ServerError> {
9074    validate_multimedia_transmit_descriptor(descriptor)?;
9075    if !descriptor.payload.is_valid_for(
9076        MultimediaPayloadDirection::Transmit,
9077        state.registration.protocol,
9078    ) {
9079        return Err(ServerError::InvalidMultimediaTransmit(
9080            "payload protocol does not match the live session",
9081        ));
9082    }
9083    if state.registration.protocol < ProtocolVersion::V17 && descriptor.endpoint.address.is_ipv6() {
9084        return Err(ServerError::InvalidMultimediaTransmit(
9085            "this protocol version carries only IPv4 video endpoints",
9086        ));
9087    }
9088    let requested_address = address_type(descriptor.endpoint.address);
9089    let supported = state.media_capabilities.video().iter().any(|capability| {
9090        let encryption_supported = descriptor.encryption.is_none()
9091            || capability.encryption_capability == Some(EncryptionCapability::Capable);
9092        capability.codec == descriptor.payload.codec()
9093            && capability.direction.contains(ReceiveTransmit::TRANSMIT)
9094            && capability_supports_address(capability.address_type, requested_address)
9095            && encryption_supported
9096    });
9097    supported
9098        .then_some(())
9099        .ok_or(ServerError::UnsupportedMultimediaTransmit)
9100}
9101
9102fn allocate_video_receive_identity(
9103    state: &mut SessionState,
9104    call_id: CallId,
9105) -> Result<MediaRequestIdentity, ServerError> {
9106    let generation = require_call(state, call_id)?
9107        .video_receive
9108        .generation
9109        .checked_add(1)
9110        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9111    let token = state
9112        .next_media_token
9113        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9114    let request = MediaRequestIdentity::new(generation, token)
9115        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9116    state.next_media_token = token.checked_next();
9117    require_call_mut(state, call_id)?.video_receive.generation = generation;
9118    Ok(request)
9119}
9120
9121fn multimedia_receive_close_message(call: &SessionCall, leg: &VideoReceiveLeg) -> ServerMessage {
9122    ServerMessage::CloseMultimediaReceiveChannel(MultimediaStreamControl {
9123        conference_id: leg.conference_id,
9124        passthrough_party_id: leg.request.token().get().into(),
9125        call_reference: CallReference::new(call.wire_reference),
9126        port_handling_flag: 0,
9127    })
9128}
9129
9130fn take_multimedia_receive_close(
9131    state: &mut SessionState,
9132    call_id: CallId,
9133) -> Option<ServerMessage> {
9134    let call = state.calls_by_id.get_mut(&call_id)?;
9135    let leg = call.video_receive.leg.take()?;
9136    Some(multimedia_receive_close_message(call, &leg))
9137}
9138
9139fn take_all_multimedia_receive_closes(state: &mut SessionState) -> Vec<ServerMessage> {
9140    let mut call_ids = state.calls_by_id.keys().copied().collect::<Vec<_>>();
9141    call_ids.sort_unstable_by_key(|call_id| call_id.get());
9142    call_ids
9143        .into_iter()
9144        .filter_map(|call_id| take_multimedia_receive_close(state, call_id))
9145        .collect()
9146}
9147
9148fn expire_multimedia_receive_acknowledgements(
9149    state: &mut SessionState,
9150    now: Instant,
9151) -> Vec<ExpiredVideoReceive> {
9152    let mut call_ids = state
9153        .calls_by_id
9154        .iter()
9155        .filter_map(|(&call_id, call)| {
9156            call.video_receive.leg.as_ref().and_then(|leg| {
9157                (leg.state == MediaChannelState::Opening
9158                    && leg.deadline.is_some_and(|deadline| deadline <= now))
9159                .then_some(call_id)
9160            })
9161        })
9162        .collect::<Vec<_>>();
9163    call_ids.sort_unstable_by_key(|call_id| call_id.get());
9164    call_ids
9165        .into_iter()
9166        .filter_map(|call_id| {
9167            let leg = state
9168                .calls_by_id
9169                .get(&call_id)?
9170                .video_receive
9171                .leg
9172                .as_ref()?;
9173            let codec = leg.codec;
9174            let passthrough_party_id = leg.request.token().get().into();
9175            take_multimedia_receive_close(state, call_id).map(|close| ExpiredVideoReceive {
9176                call_id,
9177                codec,
9178                passthrough_party_id,
9179                close,
9180            })
9181        })
9182        .collect()
9183}
9184
9185fn allocate_video_transmit_identity(
9186    state: &mut SessionState,
9187    call_id: CallId,
9188) -> Result<MediaRequestIdentity, ServerError> {
9189    let generation = require_call(state, call_id)?
9190        .video_transmit
9191        .generation
9192        .checked_add(1)
9193        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9194    let token = state
9195        .next_media_token
9196        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9197    let request = MediaRequestIdentity::new(generation, token)
9198        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9199    state.next_media_token = token.checked_next();
9200    require_call_mut(state, call_id)?.video_transmit.generation = generation;
9201    Ok(request)
9202}
9203
9204fn multimedia_transmit_control_identity(
9205    state: &SessionState,
9206    call_id: CallId,
9207    passthrough_party_id: PassthroughPartyId,
9208) -> Result<(ConferenceId, CallReference), ServerError> {
9209    let call = require_call(state, call_id)?;
9210    if call.state != CallState::Connected {
9211        return Err(ServerError::InvalidCallTransaction {
9212            call_id,
9213            operation: "control video transmit media",
9214            state: call.state,
9215        });
9216    }
9217    let leg = call
9218        .video_transmit
9219        .leg
9220        .as_ref()
9221        .filter(|leg| {
9222            leg.state == MediaChannelState::Open
9223                && leg.request.token().get() == passthrough_party_id.get()
9224        })
9225        .ok_or(ServerError::StaleMultimediaTransmitControl {
9226            call_id,
9227            passthrough_party_id,
9228        })?;
9229    Ok((leg.conference_id, CallReference::new(call.wire_reference)))
9230}
9231
9232fn encode_multimedia_transmit_control(
9233    control: MultimediaTransmitControl,
9234) -> Result<(MiscCommandType, BoundedBytes<36>), ServerError> {
9235    let (command, words) = match control {
9236        MultimediaTransmitControl::FreezePicture => {
9237            (MiscCommandType::VideoFreezePicture, Vec::new())
9238        }
9239        MultimediaTransmitControl::FastPictureUpdate {
9240            first_gob,
9241            gob_count,
9242        } => (
9243            MiscCommandType::VideoFastUpdatePicture,
9244            vec![first_gob, gob_count],
9245        ),
9246        MultimediaTransmitControl::FastGobUpdate {
9247            first_gob,
9248            gob_count,
9249        } => (
9250            MiscCommandType::VideoFastUpdateGob,
9251            vec![first_gob, gob_count],
9252        ),
9253        MultimediaTransmitControl::FastMacroblockUpdate {
9254            first_gob,
9255            first_macroblock,
9256            macroblock_count,
9257        } => (
9258            MiscCommandType::VideoFastUpdateMacroblock,
9259            vec![first_gob, first_macroblock, macroblock_count],
9260        ),
9261        MultimediaTransmitControl::LostPicture {
9262            picture_number,
9263            long_term_picture_index,
9264        } => (
9265            MiscCommandType::LostPicture,
9266            vec![picture_number, long_term_picture_index],
9267        ),
9268        MultimediaTransmitControl::LostPartialPicture {
9269            picture_number,
9270            long_term_picture_index,
9271            first_macroblock,
9272            macroblock_count,
9273        } => (
9274            MiscCommandType::LostPartialPicture,
9275            vec![
9276                picture_number,
9277                long_term_picture_index,
9278                first_macroblock,
9279                macroblock_count,
9280            ],
9281        ),
9282        MultimediaTransmitControl::RecoveryReferencePicture { pictures } => {
9283            let words =
9284                std::iter::once(pictures.as_slice().len() as u32)
9285                    .chain(pictures.as_slice().iter().flat_map(|picture| {
9286                        [picture.picture_number, picture.long_term_picture_index]
9287                    }))
9288                    .collect();
9289            (MiscCommandType::RecoveryReferencePicture, words)
9290        }
9291        MultimediaTransmitControl::TemporalSpatialTradeoff { value } => {
9292            (MiscCommandType::TemporalSpatialTradeoff, vec![value])
9293        }
9294    };
9295    let data = words
9296        .into_iter()
9297        .flat_map(u32::to_le_bytes)
9298        .collect::<Vec<_>>();
9299    let data = BoundedBytes::new(data.into_boxed_slice()).map_err(|_| {
9300        ServerError::InvalidMultimediaTransmitControl("parameter area exceeds 36 bytes")
9301    })?;
9302    Ok((command, data))
9303}
9304
9305fn multimedia_transmit_stop_message(call: &SessionCall, leg: &VideoTransmitLeg) -> ServerMessage {
9306    ServerMessage::StopMultimediaTransmission(MultimediaStreamControl {
9307        conference_id: leg.conference_id,
9308        passthrough_party_id: leg.request.token().get().into(),
9309        call_reference: CallReference::new(call.wire_reference),
9310        port_handling_flag: 0,
9311    })
9312}
9313
9314fn take_multimedia_transmit_stop(
9315    state: &mut SessionState,
9316    call_id: CallId,
9317) -> Option<ServerMessage> {
9318    let call = state.calls_by_id.get_mut(&call_id)?;
9319    let leg = call.video_transmit.leg.take()?;
9320    Some(multimedia_transmit_stop_message(call, &leg))
9321}
9322
9323fn take_all_multimedia_transmit_stops(state: &mut SessionState) -> Vec<ServerMessage> {
9324    let mut call_ids = state.calls_by_id.keys().copied().collect::<Vec<_>>();
9325    call_ids.sort_unstable_by_key(|call_id| call_id.get());
9326    call_ids
9327        .into_iter()
9328        .filter_map(|call_id| take_multimedia_transmit_stop(state, call_id))
9329        .collect()
9330}
9331
9332fn expire_multimedia_transmit_acknowledgements(
9333    state: &mut SessionState,
9334    now: Instant,
9335) -> Vec<ExpiredVideoTransmit> {
9336    let mut call_ids = state
9337        .calls_by_id
9338        .iter()
9339        .filter_map(|(&call_id, call)| {
9340            call.video_transmit.leg.as_ref().and_then(|leg| {
9341                (leg.state == MediaChannelState::Opening
9342                    && leg.deadline.is_some_and(|deadline| deadline <= now))
9343                .then_some(call_id)
9344            })
9345        })
9346        .collect::<Vec<_>>();
9347    call_ids.sort_unstable_by_key(|call_id| call_id.get());
9348    call_ids
9349        .into_iter()
9350        .filter_map(|call_id| {
9351            let leg = state
9352                .calls_by_id
9353                .get(&call_id)?
9354                .video_transmit
9355                .leg
9356                .as_ref()?;
9357            let codec = leg.codec;
9358            let passthrough_party_id = leg.request.token().get().into();
9359            take_multimedia_transmit_stop(state, call_id).map(|stop| ExpiredVideoTransmit {
9360                call_id,
9361                codec,
9362                passthrough_party_id,
9363                stop,
9364            })
9365        })
9366        .collect()
9367}
9368
9369fn validate_multicast_route(
9370    state: &SessionState,
9371    route: MulticastMediaRoute,
9372    max_frames_per_packet: Option<u32>,
9373) -> Result<(), ServerError> {
9374    if !route.address.is_multicast() {
9375        return Err(ServerError::InvalidMulticastMedia(
9376            "address must be multicast",
9377        ));
9378    }
9379    if route.address.is_ipv6() && state.registration.protocol < ProtocolVersion::V17 {
9380        return Err(ServerError::InvalidMulticastMedia(
9381            "IPv6 requires protocol v17 or later",
9382        ));
9383    }
9384    if route.port == 0 {
9385        return Err(ServerError::InvalidMulticastMedia("port must be nonzero"));
9386    }
9387    if route.packet_millis == 0 {
9388        return Err(ServerError::InvalidMulticastMedia(
9389            "packet duration must be nonzero",
9390        ));
9391    }
9392    if route.codec.kind() != CodecKind::Audio {
9393        return Err(ServerError::UnsupportedMulticastCodec);
9394    }
9395    let capability = state
9396        .media_capabilities
9397        .audio()
9398        .iter()
9399        .find(|capability| capability.codec == route.codec)
9400        .filter(|capability| capability.max_frames_per_packet != 0)
9401        .ok_or(ServerError::UnsupportedMulticastCodec)?;
9402    if let Some(requested) = max_frames_per_packet
9403        && (requested == 0 || requested > capability.max_frames_per_packet)
9404    {
9405        return Err(ServerError::InvalidMulticastMedia(
9406            "packet framing exceeds the advertised capability",
9407        ));
9408    }
9409    Ok(())
9410}
9411
9412fn allocate_multicast_request_identity(
9413    state: &mut SessionState,
9414) -> Result<MediaRequestIdentity, ServerError> {
9415    let generation = state
9416        .next_multicast_generation
9417        .checked_add(1)
9418        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9419    let token = state
9420        .next_media_token
9421        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9422    let identity = MediaRequestIdentity::new(generation, token)
9423        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9424    state.next_multicast_generation = generation;
9425    state.next_media_token = token.checked_next();
9426    Ok(identity)
9427}
9428
9429fn multicast_stop_message(
9430    key: MulticastKey,
9431    wire_call_reference: u32,
9432    request: MediaRequestIdentity,
9433    receive: bool,
9434) -> ServerMessage {
9435    if receive {
9436        ServerMessage::StopMulticastMediaReception {
9437            conference_id: key.conference_id,
9438            passthrough_party_id: request.token().get().into(),
9439            call_reference: CallReference::new(wire_call_reference),
9440        }
9441    } else {
9442        ServerMessage::StopMulticastMediaTransmission {
9443            conference_id: key.conference_id,
9444            passthrough_party_id: request.token().get().into(),
9445            call_reference: CallReference::new(wire_call_reference),
9446        }
9447    }
9448}
9449
9450fn take_multicast_stop(
9451    state: &mut SessionState,
9452    key: MulticastKey,
9453    receive: bool,
9454) -> Option<ServerMessage> {
9455    let session = state.multicast.get_mut(&key)?;
9456    let request = if receive {
9457        session.receive.take().map(|leg| leg.request)
9458    } else {
9459        session.transmit.take().map(|leg| leg.request)
9460    }?;
9461    let message = multicast_stop_message(key, session.wire_call_reference, request, receive);
9462    if session.receive.is_none() && session.transmit.is_none() {
9463        state.multicast.remove(&key);
9464    }
9465    Some(message)
9466}
9467
9468fn expire_multicast_reception_acknowledgements(
9469    state: &mut SessionState,
9470    now: Instant,
9471) -> Vec<(MulticastKey, ServerMessage)> {
9472    let mut expired = state
9473        .multicast
9474        .iter()
9475        .filter_map(|(key, session)| {
9476            session.receive.as_ref().and_then(|receive| {
9477                matches!(
9478                    receive.state,
9479                    MulticastReceiveState::AwaitingAcknowledgement { deadline }
9480                        if deadline <= now
9481                )
9482                .then_some(*key)
9483            })
9484        })
9485        .collect::<Vec<_>>();
9486    expired.sort_unstable_by_key(|key| (key.conference_id.get(), key.call_id.get()));
9487    expired
9488        .into_iter()
9489        .filter_map(|key| take_multicast_stop(state, key, true).map(|stop| (key, stop)))
9490        .collect()
9491}
9492
9493fn take_multicast_stops_for_call(state: &mut SessionState, call_id: CallId) -> Vec<ServerMessage> {
9494    let mut keys = state
9495        .multicast
9496        .keys()
9497        .copied()
9498        .filter(|key| key.call_id == call_id)
9499        .collect::<Vec<_>>();
9500    keys.sort_unstable_by_key(|key| key.conference_id.get());
9501    keys.into_iter()
9502        .flat_map(|key| {
9503            [
9504                take_multicast_stop(state, key, true),
9505                take_multicast_stop(state, key, false),
9506            ]
9507            .into_iter()
9508            .flatten()
9509        })
9510        .collect()
9511}
9512
9513fn take_all_multicast_stops(state: &mut SessionState) -> Vec<ServerMessage> {
9514    let mut sessions = std::mem::take(&mut state.multicast)
9515        .into_iter()
9516        .collect::<Vec<_>>();
9517    sessions.sort_unstable_by_key(|(key, _)| (key.conference_id.get(), key.call_id.get()));
9518    sessions
9519        .into_iter()
9520        .flat_map(|(key, session)| {
9521            [
9522                session.receive.map(|leg| {
9523                    multicast_stop_message(key, session.wire_call_reference, leg.request, true)
9524                }),
9525                session.transmit.map(|leg| {
9526                    multicast_stop_message(key, session.wire_call_reference, leg.request, false)
9527                }),
9528            ]
9529            .into_iter()
9530            .flatten()
9531        })
9532        .collect()
9533}
9534
9535async fn drain_session_media(stream: &mut dyn StationIo, state: &mut SessionState) {
9536    let protocol = state.registration.protocol;
9537    let messages = take_all_multimedia_receive_closes(state)
9538        .into_iter()
9539        .chain(take_all_multimedia_transmit_stops(state))
9540        .chain(take_all_multicast_stops(state));
9541    for message in messages {
9542        if send_message(stream, &message, protocol).await.is_err() {
9543            break;
9544        }
9545    }
9546}
9547
9548async fn stop_call_multicast(
9549    stream: &mut dyn StationIo,
9550    state: &mut SessionState,
9551    call_id: CallId,
9552    protocol: ProtocolVersion,
9553) -> Result<(), ServerError> {
9554    for message in take_multicast_stops_for_call(state, call_id) {
9555        send_message(stream, &message, protocol).await?;
9556    }
9557    Ok(())
9558}
9559
9560fn allocate_media_request_identity(
9561    state: &mut SessionState,
9562    call_id: CallId,
9563) -> Result<MediaRequestIdentity, ServerError> {
9564    let generation = require_call(state, call_id)?
9565        .media
9566        .generation
9567        .checked_add(1)
9568        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9569    let token = state
9570        .next_media_token
9571        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9572    let identity = MediaRequestIdentity::new(generation, token)
9573        .ok_or(ServerError::MediaRequestIdentityExhausted)?;
9574    state.next_media_token = token.checked_next();
9575    require_call_mut(state, call_id)?.media.generation = generation;
9576    Ok(identity)
9577}
9578
9579fn media_request_party_id(
9580    request: Option<MediaRequestIdentity>,
9581    stable_call_reference: u32,
9582) -> u32 {
9583    request.map_or(stable_call_reference, |identity| identity.token().get())
9584}
9585
9586fn remove_call(state: &mut SessionState, call_id: CallId) {
9587    if let Some(call) = state.calls_by_id.remove(&call_id) {
9588        state.calls_by_wire.remove(&call.wire_reference);
9589        if state.active_call_id == Some(call_id) {
9590            state.active_call_id = None;
9591        }
9592        if state.ringer_owner == Some(call_id) {
9593            state.ringer_owner = None;
9594        }
9595    }
9596}
9597
9598fn canonical_ip_address(address: IpAddr) -> IpAddr {
9599    match address {
9600        IpAddr::V6(address) => address
9601            .to_ipv4_mapped()
9602            .map_or(IpAddr::V6(address), IpAddr::V4),
9603        address => address,
9604    }
9605}
9606
9607fn server_response_address(
9608    local: IpAddr,
9609    configured_ipv4_fallback: Ipv4Addr,
9610    configured_ipv6_fallback: Option<Ipv6Addr>,
9611) -> IpAddr {
9612    match canonical_ip_address(local) {
9613        IpAddr::V4(address) if address.is_unspecified() => IpAddr::V4(configured_ipv4_fallback),
9614        IpAddr::V6(address) if address.is_unspecified() => {
9615            configured_ipv6_fallback.map_or(IpAddr::V4(configured_ipv4_fallback), IpAddr::V6)
9616        }
9617        local => local,
9618    }
9619}
9620
9621fn server_response_endpoints(
9622    context: &SessionContext,
9623    protocol: ProtocolVersion,
9624) -> Result<Vec<SignalingServerEndpoint>, ServerError> {
9625    let local_endpoint = || {
9626        let address = server_response_address(
9627            context.local.ip(),
9628            context.config.advertised_address,
9629            context.config.advertised_ipv6_address,
9630        );
9631        let address = if protocol < ProtocolVersion::V17 && address.is_ipv6() {
9632            IpAddr::V4(context.config.advertised_address)
9633        } else {
9634            address
9635        };
9636        if address.is_unspecified() {
9637            return Err(ServerError::InvalidConfig(
9638                "server-list fallback address is unspecified".into(),
9639            ));
9640        }
9641        Ok(SignalingServerEndpoint {
9642            name: context.config.server_name.clone(),
9643            address,
9644            port: NonZeroU16::new(context.local.port()).ok_or_else(|| {
9645                ServerError::InvalidConfig("accepted local endpoint has port zero".into())
9646            })?,
9647        })
9648    };
9649    if context.config.signaling_servers.is_empty() {
9650        return local_endpoint().map(|endpoint| vec![endpoint]);
9651    }
9652
9653    let mut routes = context.config.signaling_servers.iter().collect::<Vec<_>>();
9654    routes.sort_unstable_by_key(|route| route.priority);
9655    let endpoints = routes
9656        .into_iter()
9657        .filter(|route| protocol >= ProtocolVersion::V17 || route.address.is_ipv4())
9658        .filter_map(|route| route.endpoint(context.transport))
9659        .collect::<Vec<_>>();
9660    if endpoints.is_empty() {
9661        local_endpoint().map(|endpoint| vec![endpoint])
9662    } else {
9663        Ok(endpoints)
9664    }
9665}
9666
9667async fn close_call_media_messages(
9668    stream: &mut dyn StationIo,
9669    call: &SessionCall,
9670    protocol: ProtocolVersion,
9671) -> Result<(), ServerError> {
9672    if let Some(leg) = &call.video_receive.leg {
9673        send_message(
9674            stream,
9675            &multimedia_receive_close_message(call, leg),
9676            protocol,
9677        )
9678        .await?;
9679    }
9680    if let Some(leg) = &call.video_transmit.leg {
9681        send_message(
9682            stream,
9683            &multimedia_transmit_stop_message(call, leg),
9684            protocol,
9685        )
9686        .await?;
9687    }
9688    if call.media.receive.state != MediaChannelState::Closed {
9689        send_message(
9690            stream,
9691            &ServerMessage::CloseReceiveChannel(AudioStreamControl {
9692                conference_id: ConferenceId::new(call.wire_reference),
9693                call_reference: CallReference::new(call.wire_reference),
9694                passthrough_party_id: media_request_party_id(
9695                    call.media.receive.request,
9696                    call.wire_reference,
9697                )
9698                .into(),
9699                port_handling_flag: 0,
9700            }),
9701            protocol,
9702        )
9703        .await?;
9704    }
9705    if call.media.transmit.state != MediaChannelState::Closed {
9706        send_message(
9707            stream,
9708            &ServerMessage::StopMediaTransmission(AudioStreamControl {
9709                conference_id: ConferenceId::new(call.wire_reference),
9710                call_reference: CallReference::new(call.wire_reference),
9711                passthrough_party_id: media_request_party_id(
9712                    call.media.transmit.request,
9713                    call.wire_reference,
9714                )
9715                .into(),
9716                port_handling_flag: 0,
9717            }),
9718            protocol,
9719        )
9720        .await?;
9721    }
9722    Ok(())
9723}
9724
9725async fn close_call_messages(
9726    stream: &mut dyn StationIo,
9727    call: &SessionCall,
9728    soft_keys: &SoftKeyProfile,
9729    protocol: ProtocolVersion,
9730    timezone_offset_minutes: i16,
9731    stop_ringer: bool,
9732) -> Result<(), ServerError> {
9733    send_message(
9734        stream,
9735        &ServerMessage::StopTone {
9736            line_instance: call.line_instance,
9737            call_reference: call.wire_reference,
9738        },
9739        protocol,
9740    )
9741    .await?;
9742    send_message(
9743        stream,
9744        &ServerMessage::SetLamp {
9745            stimulus: ButtonType::Line,
9746            instance: call.line_instance,
9747            mode: LampMode::Off,
9748        },
9749        protocol,
9750    )
9751    .await?;
9752    send_message(
9753        stream,
9754        &ServerMessage::ClearPrompt {
9755            line_instance: call.line_instance,
9756            call_reference: call.wire_reference,
9757        },
9758        protocol,
9759    )
9760    .await?;
9761    send_message(
9762        stream,
9763        &ServerMessage::CallState {
9764            state: CallState::OnHook,
9765            line_instance: call.line_instance,
9766            call_reference: call.wire_reference,
9767        },
9768        protocol,
9769    )
9770    .await?;
9771    send_message(
9772        stream,
9773        &ServerMessage::SelectSoftKeys {
9774            line_instance: 0,
9775            call_reference: 0,
9776            set: KeyMode::OnHook,
9777            valid_mask: soft_keys.valid_mask(KeyMode::OnHook),
9778        },
9779        protocol,
9780    )
9781    .await?;
9782    send_message(
9783        stream,
9784        &time_date_message(timezone_offset_minutes),
9785        protocol,
9786    )
9787    .await?;
9788    send_message(
9789        stream,
9790        &ServerMessage::SetSpeakerMode(SpeakerMode::Off),
9791        protocol,
9792    )
9793    .await?;
9794    if stop_ringer {
9795        send_message(
9796            stream,
9797            &ServerMessage::SetRinger {
9798                mode: RingerMode::Off,
9799                duration: RingDuration::Normal,
9800                line_instance: call.line_instance,
9801                call_reference: call.wire_reference,
9802            },
9803            protocol,
9804        )
9805        .await?;
9806    }
9807    Ok(())
9808}
9809
9810fn time_date_message(timezone_offset_minutes: i16) -> ServerMessage {
9811    time_date_message_at(SystemTime::now(), timezone_offset_minutes)
9812}
9813
9814fn time_date_message_at(now: SystemTime, timezone_offset_minutes: i16) -> ServerMessage {
9815    let unix = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
9816    let local = (unix as i128 + i128::from(timezone_offset_minutes) * 60)
9817        .clamp(0, i128::from(u32::MAX)) as u64;
9818    let days = (local / 86_400) as i64;
9819    let seconds = local % 86_400;
9820    let (year, month, day) = civil_from_days(days);
9821    ServerMessage::TimeDate {
9822        year: year as u32,
9823        month,
9824        weekday: ((days + 4).rem_euclid(7) + 1) as u32,
9825        day,
9826        hour: (seconds / 3600) as u32,
9827        minute: ((seconds % 3600) / 60) as u32,
9828        second: (seconds % 60) as u32,
9829        milliseconds: 0,
9830        unix_seconds: local as u32,
9831    }
9832}
9833
9834// Howard Hinnant's civil-from-days algorithm, with days based at Unix epoch.
9835fn civil_from_days(days_since_epoch: i64) -> (i64, u32, u32) {
9836    let z = days_since_epoch + 719_468;
9837    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
9838    let doe = z - era * 146_097;
9839    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
9840    let mut year = yoe + era * 400;
9841    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
9842    let mp = (5 * doy + 2) / 153;
9843    let day = doy - (153 * mp + 2) / 5 + 1;
9844    let month = mp + if mp < 10 { 3 } else { -9 };
9845    year += i64::from(month <= 2);
9846    (year, month as u32, day as u32)
9847}
9848
9849#[cfg(test)]
9850#[path = "server/tests/mod.rs"]
9851mod tests;