Skip to main content

sccp_protocol/message/
values.rs

1//! Typed numeric values used by SCCP messages.
2//!
3//! Most Skinny numeric fields are extensible firmware contracts.  Data-bearing
4//! `Unknown` variants keep them type-safe without making newer phones fail to
5//! decode. Convert from the wire with `From<u32>`, inspect known values through
6//! `ALL_KNOWN`, and use `wire_value` (or `Into<u32>`) when encoding.
7
8use std::fmt;
9
10use bitflags::bitflags;
11
12use super::wire::CodecError;
13
14macro_rules! wire_enum {
15    ($(#[$meta:meta])* pub enum $name:ident { $($variant:ident = $value:expr),+ $(,)? }) => {
16        $(#[$meta])*
17        #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18        pub enum $name {
19            $($variant,)+
20            Unknown(u32),
21        }
22
23        impl $name {
24            pub const ALL_KNOWN: &'static [Self] = &[$(Self::$variant,)+];
25
26            pub const fn wire_value(self) -> u32 {
27                match self {
28                    $(Self::$variant => $value,)+
29                    Self::Unknown(value) => value,
30                }
31            }
32
33            pub const fn is_known(self) -> bool {
34                !matches!(self, Self::Unknown(_))
35            }
36        }
37
38        impl From<u32> for $name {
39            fn from(value: u32) -> Self {
40                match value {
41                    $($value => Self::$variant,)+
42                    value => Self::Unknown(value),
43                }
44            }
45        }
46
47        impl From<$name> for u32 {
48            fn from(value: $name) -> Self {
49                value.wire_value()
50            }
51        }
52    };
53}
54
55/// A negotiated SCCP protocol version in the supported 3..=22 range.
56#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
57pub struct ProtocolVersion(u8);
58
59impl ProtocolVersion {
60    pub const MIN: Self = Self(3);
61    pub const MAX: Self = Self(22);
62    pub const V3: Self = Self(3);
63    pub const V5: Self = Self(5);
64    pub const V7: Self = Self(7);
65    pub const V8: Self = Self(8);
66    pub const V9: Self = Self(9);
67    pub const V10: Self = Self(10);
68    pub const V11: Self = Self(11);
69    pub const V12: Self = Self(12);
70    pub const V13: Self = Self(13);
71    pub const V14: Self = Self(14);
72    pub const V15: Self = Self(15);
73    pub const V16: Self = Self(16);
74    pub const V17: Self = Self(17);
75    pub const V18: Self = Self(18);
76    pub const V19: Self = Self(19);
77    pub const V20: Self = Self(20);
78    pub const V21: Self = Self(21);
79    pub const V22: Self = Self(22);
80
81    /// Validates and constructs an exact supported protocol version.
82    pub fn new(value: u32) -> Result<Self, CodecError> {
83        let value = u8::try_from(value).map_err(|_| CodecError::UnsupportedProtocol(value))?;
84        if !(Self::MIN.0..=Self::MAX.0).contains(&value) {
85            return Err(CodecError::UnsupportedProtocol(u32::from(value)));
86        }
87        Ok(Self(value))
88    }
89
90    /// Negotiate the highest version supported by both peers.
91    pub fn negotiate(advertised: u32) -> Result<Self, CodecError> {
92        if advertised < u32::from(Self::MIN.0) {
93            return Err(CodecError::UnsupportedProtocol(advertised));
94        }
95        Self::new(advertised.min(u32::from(Self::MAX.0)))
96    }
97
98    pub const fn wire(self) -> u32 {
99        self.0 as u32
100    }
101
102    /// Returns the group of version-dependent wire layouts selected by this version.
103    pub const fn layout(self) -> LayoutProfile {
104        match self.0 {
105            3..=4 => LayoutProfile::V3,
106            5..=7 => LayoutProfile::V5,
107            8..=10 => LayoutProfile::V8,
108            11..=14 => LayoutProfile::V11,
109            15 => LayoutProfile::V15,
110            16 => LayoutProfile::V16,
111            17 => LayoutProfile::V17,
112            18 => LayoutProfile::V18,
113            19..=21 => LayoutProfile::V19,
114            _ => LayoutProfile::V22,
115        }
116    }
117
118    /// General station UI messages use dynamic text layouts from version 9.
119    pub const fn uses_dynamic_general_ui(self) -> bool {
120        self.0 >= Self::V9.0
121    }
122
123    /// Returns the number of strings in this version's dynamic call-info body.
124    pub const fn dynamic_call_info_layout(self) -> DynamicCallInfoLayout {
125        match self.0 {
126            ..=15 => DynamicCallInfoLayout::Fields12,
127            16..=18 => DynamicCallInfoLayout::Fields13,
128            _ => DynamicCallInfoLayout::Fields15,
129        }
130    }
131
132    /// Reports whether dynamic speed-dial status is selected by protocol version.
133    pub const fn uses_dynamic_speed_dial_status(self) -> bool {
134        self.0 >= Self::V15.0
135    }
136}
137
138/// Version-selected shape of a dynamic call-information payload.
139#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
140pub enum DynamicCallInfoLayout {
141    Fields12,
142    Fields13,
143    Fields15,
144}
145
146impl DynamicCallInfoLayout {
147    pub const fn string_count(self) -> usize {
148        match self {
149            Self::Fields12 => 12,
150            Self::Fields13 => 13,
151            Self::Fields15 => 15,
152        }
153    }
154}
155
156impl fmt::Debug for ProtocolVersion {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        write!(f, "V{}", self.0)
159    }
160}
161
162impl fmt::Display for ProtocolVersion {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "v{}", self.0)
165    }
166}
167
168impl TryFrom<u32> for ProtocolVersion {
169    type Error = CodecError;
170
171    fn try_from(value: u32) -> Result<Self, Self::Error> {
172        Self::new(value)
173    }
174}
175
176impl From<ProtocolVersion> for u32 {
177    fn from(value: ProtocolVersion) -> Self {
178        value.wire()
179    }
180}
181
182/// Wire-layout transitions used between SCCP versions 3 and 22.
183#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
184pub enum LayoutProfile {
185    /// Layouts used by versions 3 and 4.
186    V3,
187    /// Layouts used by versions 5 through 7.
188    V5,
189    /// Layouts used by versions 8 through 10.
190    V8,
191    /// Layouts used by versions 11 through 14.
192    V11,
193    /// Layouts specific to version 15.
194    V15,
195    /// Layouts specific to version 16.
196    V16,
197    /// Layouts specific to version 17.
198    V17,
199    /// Layouts specific to version 18.
200    V18,
201    /// Layouts used by versions 19 through 21.
202    V19,
203    /// Layouts used by version 22.
204    V22,
205}
206
207wire_enum! {
208    /// Station device model identifier.
209    pub enum DeviceType {
210        Undefined = 0,
211        Phone30SpPlus = 1,
212        Phone12SpPlus = 2,
213        Phone12Sp = 3,
214        Phone12 = 4,
215        Phone30Vip = 5,
216        Cisco7910 = 6,
217        Cisco7960 = 7,
218        Cisco7940 = 8,
219        Cisco7935 = 9,
220        Vgc = 10,
221        Ata186 = 12,
222        Ata188 = 13,
223        Virtual30SpPlus = 20,
224        PhoneApplication = 21,
225        AnalogAccess = 30,
226        DigitalAccessPri = 40,
227        DigitalAccessT1 = 41,
228        DigitalAccessTitan2 = 42,
229        AnalogAccessElvis = 43,
230        DigitalAccessLennon = 47,
231        ConferenceBridge = 50,
232        ConferenceBridgeYoko = 51,
233        ConferenceBridgeDixieland = 52,
234        ConferenceBridgeSummit = 53,
235        H225 = 60,
236        H323Phone = 61,
237        H323Trunk = 62,
238        MusicOnHold = 70,
239        Pilot = 71,
240        TapiPort = 72,
241        TapiRoutePoint = 73,
242        VoiceInbox = 80,
243        VoiceInboxAdmin = 81,
244        LineAnnunciator = 82,
245        SoftwareMtpDixieland = 83,
246        CiscoMediaServer = 84,
247        ConferenceBridgeFlint = 85,
248        RouteList = 90,
249        LoadSimulator = 100,
250        MediaTerminationPoint = 110,
251        MediaTerminationPointYoko = 111,
252        MediaTerminationPointDixieland = 112,
253        MediaTerminationPointSummit = 113,
254        Cisco7941 = 115,
255        Cisco7971 = 119,
256        MgcpStation = 120,
257        MgcpTrunk = 121,
258        RasProxy = 122,
259        CiscoAddon7914 = 124,
260        Trunk = 125,
261        Annunciator = 126,
262        MonitorBridge = 127,
263        Recorder = 128,
264        MonitorBridgeYoko = 129,
265        SipTrunk = 131,
266        CiscoAddon7915_12 = 227,
267        CiscoAddon7915_24 = 228,
268        CiscoAddon7916_12 = 229,
269        CiscoAddon7916_24 = 230,
270        NokiaESeries = 275,
271        Cisco7985 = 302,
272        Cisco7911 = 307,
273        Cisco7961Ge = 308,
274        Cisco7941Ge = 309,
275        Cisco7931 = 348,
276        Cisco7921 = 365,
277        Cisco7906 = 369,
278        NokiaIcc = 376,
279        Cisco7962 = 404,
280        Cisco7937 = 431,
281        Cisco7942 = 434,
282        Cisco7945 = 435,
283        Cisco7965 = 436,
284        Cisco7975 = 437,
285        Cisco7925 = 484,
286        Cisco6921 = 495,
287        Cisco6941 = 496,
288        Cisco6961 = 497,
289        Cisco6901 = 547,
290        Cisco6911 = 548,
291        Cisco6945 = 564,
292        Cisco7926 = 577,
293        Cisco8945 = 585,
294        Cisco8941 = 586,
295        CiscoIpCommunicator = 30016,
296        Cisco7905 = 20000,
297        Cisco7920 = 30002,
298        Cisco7970 = 30006,
299        Cisco7912 = 30007,
300        Cisco7902 = 30008,
301        Cisco7961 = 30018,
302        Cisco7936 = 30019,
303        AnalogGateway = 30027,
304        BriGateway = 30028,
305        Spa521s = 80000,
306        Spa524sg = 80001,
307        Spa502g = 80003,
308        Spa504g = 80004,
309        Spa525g = 80005,
310        Spa508g = 80006,
311        Spa509g = 80007,
312        Spa525g2 = 80009,
313        Spa303g = 80011,
314        Spa512g = 80012,
315        Spa514g = 80013,
316        AddonSpa500s = 99991,
317        AddonSpa500ds = 99992,
318        AddonSpa932ds = 99993,
319        NotDefined = 99999
320    }
321}
322
323/// Broad media class for a codec capability.
324#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
325pub enum CodecKind {
326    Audio,
327    Video,
328    Text,
329    Data,
330    TelephoneEvent,
331    Unknown,
332}
333
334wire_enum! {
335    /// Skinny payload capability / codec identifier.
336    pub enum Codec {
337        None = 0x0000,
338        NonStandard = 0x0001,
339        Pcma = 0x0002,
340        G711Alaw56k = 0x0003,
341        Pcmu = 0x0004,
342        G711Ulaw56k = 0x0005,
343        G72264k = 0x0006,
344        G72256k = 0x0007,
345        G72248k = 0x0008,
346        G7231 = 0x0009,
347        G728 = 0x000a,
348        G729 = 0x000b,
349        G729A = 0x000c,
350        Is11172 = 0x000d,
351        Is13818 = 0x000e,
352        G729B = 0x000f,
353        G729Ab = 0x0010,
354        GsmFullRate = 0x0012,
355        GsmHalfRate = 0x0013,
356        GsmEnhancedFullRate = 0x0014,
357        Wideband256k = 0x0019,
358        Data64k = 0x0020,
359        Data56k = 0x0021,
360        G7221_32k = 0x0028,
361        G7221_24k = 0x0029,
362        Aac = 0x002a,
363        Mp4aLatm128 = 0x002b,
364        Mp4aLatm64 = 0x002c,
365        Mp4aLatm56 = 0x002d,
366        Mp4aLatm48 = 0x002e,
367        Mp4aLatm32 = 0x002f,
368        Mp4aLatm24 = 0x0030,
369        Mp4aLatm = 0x0031,
370        Gsm = 0x0050,
371        ActiveVoice = 0x0051,
372        G726_32k = 0x0052,
373        G726_24k = 0x0053,
374        G726_16k = 0x0054,
375        G729AnnexB = 0x0055,
376        Ilbc = 0x0056,
377        Isac = 0x0059,
378        Opus = 0x005a,
379        Amr = 0x0061,
380        AmrWb = 0x0062,
381        H261 = 0x0064,
382        H263 = 0x0065,
383        H263Plus = 0x0066,
384        H264 = 0x0067,
385        H264Svc = 0x0068,
386        T120 = 0x0069,
387        H224 = 0x006a,
388        T38Fax = 0x006b,
389        Tote = 0x006c,
390        H265 = 0x006d,
391        H264Uc = 0x006e,
392        Xv150ModemRelay711u = 0x006f,
393        NseVbd711u = 0x0070,
394        Xv150ModemRelay729a = 0x0071,
395        NseVbd729a = 0x0072,
396        H264Fec = 0x0073,
397        ClearChannel = 0x0078,
398        UniversalTranscoder = 0x00de,
399        DtmfOutOfBandRfc2833 = 0x0101,
400        DtmfPassthrough = 0x0102,
401        DtmfDynamic = 0x0103,
402        DtmfOutOfBand = 0x0104,
403        DtmfInBandRfc2833 = 0x0105,
404        CfbTones = 0x0106,
405        DtmfNoAudio = 0x012b,
406        V150ModemRelay = 0x012c,
407        V150Sprt = 0x012d,
408        V150Sse = 0x012e
409    }
410}
411
412impl Codec {
413    /// Backward-compatible name for the SCCP numeric value.
414    pub const fn skinny(self) -> u32 {
415        self.wire_value()
416    }
417
418    /// Classifies this capability into its broad media family.
419    pub const fn kind(self) -> CodecKind {
420        match self {
421            Self::H261
422            | Self::H263
423            | Self::H263Plus
424            | Self::H264
425            | Self::H264Svc
426            | Self::H265
427            | Self::H264Uc
428            | Self::H264Fec => CodecKind::Video,
429            Self::T120 | Self::H224 => CodecKind::Text,
430            Self::Data64k
431            | Self::Data56k
432            | Self::T38Fax
433            | Self::Tote
434            | Self::Xv150ModemRelay711u
435            | Self::NseVbd711u
436            | Self::Xv150ModemRelay729a
437            | Self::NseVbd729a
438            | Self::ClearChannel
439            | Self::UniversalTranscoder
440            | Self::V150ModemRelay
441            | Self::V150Sprt
442            | Self::V150Sse => CodecKind::Data,
443            Self::DtmfOutOfBandRfc2833
444            | Self::DtmfPassthrough
445            | Self::DtmfDynamic
446            | Self::DtmfOutOfBand
447            | Self::DtmfInBandRfc2833
448            | Self::DtmfNoAudio
449            | Self::CfbTones => CodecKind::TelephoneEvent,
450            Self::None | Self::NonStandard | Self::Unknown(_) => CodecKind::Unknown,
451            _ => CodecKind::Audio,
452        }
453    }
454
455    /// Returns the nominal clock rate in hertz for supported audio codecs.
456    ///
457    /// Non-audio, unrecognized, and codecs without a defined mapping return
458    /// `None`.
459    pub const fn sample_rate(self) -> Option<u32> {
460        match self {
461            Self::G72264k
462            | Self::G72256k
463            | Self::G72248k
464            | Self::G7221_32k
465            | Self::G7221_24k
466            | Self::Wideband256k
467            | Self::AmrWb => Some(16_000),
468            Self::Opus | Self::Isac => Some(48_000),
469            codec if matches!(codec.kind(), CodecKind::Audio) => Some(8_000),
470            _ => None,
471        }
472    }
473
474    /// Returns the codec's default RTP payload type when statically assigned.
475    ///
476    /// Dynamically assigned codecs return `None` and require negotiated
477    /// payload metadata.
478    pub const fn rtp_payload_type(self) -> Option<u8> {
479        match self {
480            Self::Pcmu | Self::G711Ulaw56k => Some(0),
481            Self::Gsm => Some(3),
482            Self::G7231 => Some(4),
483            Self::Pcma | Self::G711Alaw56k => Some(8),
484            Self::G72264k | Self::G72256k | Self::G72248k => Some(9),
485            Self::G729 | Self::G729A | Self::G729B | Self::G729Ab | Self::G729AnnexB => Some(18),
486            Self::Wideband256k => Some(25),
487            Self::Ilbc => Some(97),
488            Self::G7221_32k => Some(102),
489            Self::Opus => Some(107),
490            Self::G726_32k => Some(112),
491            _ => None,
492        }
493    }
494}
495
496wire_enum! {
497    /// Station call-state indication shown by the call plane.
498    pub enum CallState {
499        OffHook = 1,
500        OnHook = 2,
501        RingOut = 3,
502        RingIn = 4,
503        Connected = 5,
504        Busy = 6,
505        Congestion = 7,
506        Hold = 8,
507        CallWaiting = 9,
508        Transfer = 10,
509        Park = 11,
510        Proceed = 12,
511        RemoteMultiline = 13,
512        InvalidNumber = 14,
513        HoldYellow = 15,
514        IntercomOneWay = 16,
515        HoldRed = 17
516    }
517}
518
519wire_enum! {
520    /// Direction and origin classification attached to call information.
521    pub enum CallType {
522        Inbound = 1,
523        Outbound = 2,
524        Forward = 3
525    }
526}
527
528wire_enum! {
529    /// Operational severity attached to a station alarm report.
530    pub enum AlarmSeverity {
531        Critical = 0,
532        Warning = 1,
533        Informational = 2,
534        ProtocolUnknown = 4,
535        Major = 7,
536        Minor = 8,
537        Marginal = 10,
538        TraceInfo = 20
539    }
540}
541
542wire_enum! {
543    /// Result status returned by media-channel operations.
544    pub enum MediaStatus {
545        Ok = 0,
546        UnspecifiedError = 1,
547        OutOfChannels = 2,
548        CodecTooComplex = 3,
549        InvalidPartyId = 4,
550        InvalidCallReference = 5,
551        InvalidCodec = 6,
552        InvalidPacketSize = 7,
553        OutOfSockets = 8,
554        EncoderOrDecoderFailed = 9,
555        InvalidDynamicPayload = 10,
556        RequestedAddressTypeUnavailable = 11,
557        DeviceOnHook = 12
558    }
559}
560
561wire_enum! {
562    /// Physical or logical button stimulus reported by a station.
563    pub enum Stimulus {
564        Unused = 0x00,
565        LastNumberRedial = 0x01,
566        SpeedDial = 0x02,
567        Hold = 0x03,
568        Transfer = 0x04,
569        ForwardAll = 0x05,
570        ForwardBusy = 0x06,
571        ForwardNoAnswer = 0x07,
572        Display = 0x08,
573        Line = 0x09,
574        T120Chat = 0x0a,
575        T120Whiteboard = 0x0b,
576        T120ApplicationSharing = 0x0c,
577        T120FileTransfer = 0x0d,
578        Video = 0x0e,
579        Voicemail = 0x0f,
580        AnswerRelease = 0x10,
581        AutoAnswer = 0x11,
582        Select = 0x12,
583        Privacy = 0x13,
584        ServiceUrl = 0x14,
585        BlfSpeedDial = 0x15,
586        DirectedPark = 0x16,
587        Intercom = 0x17,
588        MaliciousCall = 0x1b,
589        GenericAppB1 = 0x21,
590        GenericAppB2 = 0x22,
591        GenericAppB3 = 0x23,
592        GenericAppB4 = 0x24,
593        GenericAppB5 = 0x25,
594        MultiblinkFeature = 0x26,
595        MeetMeConference = 0x7b,
596        Conference = 0x7d,
597        CallPark = 0x7e,
598        CallPickup = 0x7f,
599        GroupCallPickup = 0x80,
600        Mobility = 0x81,
601        DoNotDisturb = 0x82,
602        ConferenceList = 0x83,
603        RemoveLastParticipant = 0x84,
604        QualityReportTool = 0x85,
605        Callback = 0x86,
606        OtherPickup = 0x87,
607        VideoMode = 0x88,
608        NewCall = 0x89,
609        EndCall = 0x8a,
610        HuntGroupLogin = 0x8b,
611        Queuing = 0x8f,
612        ParkingLot = 0xc0,
613        Messages = 0xc2,
614        Directory = 0xc3,
615        Application = 0xc5,
616        Headset = 0xc6,
617        Keypad = 0xf0,
618        AcousticEchoCancellation = 0xfd,
619        Undefined = 0xff
620    }
621}
622
623wire_enum! {
624    /// Soft-key events are one-based positions in the advertised template.
625    pub enum SoftKey {
626        Redial = 1,
627        NewCall = 2,
628        Hold = 3,
629        Transfer = 4,
630        ForwardAll = 5,
631        ForwardBusy = 6,
632        ForwardNoAnswer = 7,
633        Backspace = 8,
634        EndCall = 9,
635        Resume = 10,
636        Answer = 11,
637        Info = 12,
638        Conference = 13,
639        Park = 14,
640        Join = 15,
641        MeetMe = 16,
642        Pickup = 17,
643        GroupPickup = 18,
644        Monitor = 19,
645        Callback = 20,
646        Barge = 21,
647        DoNotDisturb = 22,
648        ConferenceList = 23,
649        Select = 24,
650        Private = 25,
651        TransferToVoicemail = 26,
652        DirectTransfer = 27,
653        ImmediateDivert = 28,
654        VideoMode = 29,
655        Intercept = 30,
656        Empty = 31,
657        Dial = 32
658    }
659}
660
661wire_enum! {
662    /// Call-state context used to select an advertised soft-key set.
663    pub enum KeyMode {
664        OnHook = 0,
665        Connected = 1,
666        OnHold = 2,
667        RingIn = 3,
668        OffHook = 4,
669        ConnectedTransfer = 5,
670        DigitsFollowing = 6,
671        ConnectedConference = 7,
672        RingOut = 8,
673        OffHookFeature = 9,
674        InUseHint = 10,
675        OnHookStealable = 11,
676        HoldConference = 12,
677        Empty = 13
678    }
679}
680
681wire_enum! {
682    /// Audible ringer pattern selected for the station.
683    pub enum RingerMode {
684        Off = 1,
685        Inside = 2,
686        Outside = 3,
687        Feature = 4,
688        Silent = 5,
689        Urgent = 6,
690        Bellcore1 = 7,
691        Bellcore2 = 8,
692        Bellcore3 = 9,
693        Bellcore4 = 10,
694        Bellcore5 = 11
695    }
696}
697
698wire_enum! {
699    /// Whether a ringer command applies once or continuously.
700    pub enum RingDuration {
701        Normal = 1,
702        Single = 2
703    }
704}
705
706wire_enum! {
707    /// Visual state selected for a station lamp.
708    pub enum LampMode {
709        Off = 1,
710        On = 2,
711        Wink = 3,
712        Flash = 4,
713        Blink = 5,
714        Hold = 6,
715        Ring = 7,
716        Custom1 = 8,
717        Custom2 = 9
718    }
719}
720
721wire_enum! {
722    /// Tone identifier used by tone and announcement commands.
723    pub enum Tone {
724        Silence = 0x00,
725        Dtmf1 = 0x01,
726        Dtmf2 = 0x02,
727        Dtmf3 = 0x03,
728        Dtmf4 = 0x04,
729        Dtmf5 = 0x05,
730        Dtmf6 = 0x06,
731        Dtmf7 = 0x07,
732        Dtmf8 = 0x08,
733        Dtmf9 = 0x09,
734        Dtmf0 = 0x0a,
735        DtmfStar = 0x0e,
736        DtmfPound = 0x0f,
737        DtmfA = 0x10,
738        DtmfB = 0x11,
739        DtmfC = 0x12,
740        DtmfD = 0x13,
741        InsideDial = 0x21,
742        OutsideDial = 0x22,
743        LineBusy = 0x23,
744        Alerting = 0x24,
745        Reorder = 0x25,
746        RecorderWarning = 0x26,
747        RecorderDetected = 0x27,
748        Reverting = 0x28,
749        ReceiverOffHook = 0x29,
750        PartialDial = 0x2a,
751        NoSuchNumber = 0x2b,
752        BusyVerification = 0x2c,
753        CallWaiting = 0x2d,
754        Confirmation = 0x2e,
755        CampOn = 0x2f,
756        RecallDial = 0x30,
757        ZipZip = 0x31,
758        Zip = 0x32,
759        BeepBonk = 0x33,
760        Music = 0x34,
761        Hold = 0x35,
762        Test = 0x36,
763        MonitorWarning = 0x37,
764        AddCallWaiting = 0x40,
765        PriorityCallWaiting = 0x41,
766        BargeIn = 0x43,
767        DistinctAlert = 0x44,
768        PriorityAlert = 0x45,
769        ReminderRing = 0x46,
770        PrecedenceRingback = 0x47,
771        Preemption = 0x48,
772        NoTone = 0x7f,
773        MeetMeGreeting = 0x80,
774        MeetMeNumberInvalid = 0x81,
775        MeetMeNumberFailed = 0x82,
776        MeetMeEnterPin = 0x83,
777        MeetMeInvalidPin = 0x84,
778        MeetMeFailedPin = 0x85,
779        MeetMeCfbFailed = 0x86,
780        MeetMeEnterAccessCode = 0x87,
781        MeetMeAccessCodeInvalid = 0x88,
782        MeetMeAccessCodeFailed = 0x89
783    }
784}
785
786wire_enum! {
787    /// Media direction in which a station should play a tone.
788    pub enum ToneDirection {
789        User = 0,
790        Network = 1,
791        Both = 2
792    }
793}
794
795wire_enum! {
796    /// Station audio-path component named by a media-path event.
797    pub enum MediaPathId {
798        None = 0,
799        Headset = 1,
800        Handset = 2,
801        Speaker = 3
802    }
803}
804
805wire_enum! {
806    /// Availability transition reported for a station media path.
807    pub enum MediaPathEvent {
808        None = 0,
809        On = 1,
810        Off = 2
811    }
812}
813
814wire_enum! {
815    /// Capability state reported for a station media path.
816    pub enum MediaPathCapability {
817        None = 0,
818        Enable = 1,
819        Disable = 2,
820        Monitor = 3
821    }
822}
823
824wire_enum! {
825    /// Media class used when allocating or closing ports.
826    pub enum MediaType {
827        Invalid = 0,
828        Audio = 1,
829        MainVideo = 2,
830        Fecc = 3,
831        PresentationVideo = 4,
832        Bfcp = 5,
833        IxChannel = 6,
834        T38 = 7
835    }
836}
837
838wire_enum! {
839    /// Transport family requested for a media endpoint.
840    pub enum MediaTransport {
841        Rtp = 1,
842        Udp = 2,
843        Tcp = 3
844    }
845}
846
847wire_enum! {
848    /// RSVP reservation direction carried by SCCP QoS service messages.
849    pub enum QosDirection {
850        Send = 1,
851        Receive = 2,
852        SendReceive = 3
853    }
854}
855
856wire_enum! {
857    /// RSVP reservation style used by QoS path setup.
858    pub enum QosReservationStyle {
859        FixedFilter = 1,
860        SharedExplicit = 2,
861        WildcardFilter = 3
862    }
863}
864
865wire_enum! {
866    /// QoS service failure reported independently of RSVP protocol errors.
867    pub enum QosErrorCode {
868        ReservationTimeout = 0,
869        PathFailed = 1,
870        ReservationFailed = 2,
871        ListenFailed = 3,
872        ResourceUnavailable = 4,
873        ListenTimeout = 5,
874        ReservationRetriesFailed = 6,
875        PathRetriesFailed = 7,
876        ReservationPreempted = 8,
877        PathPreempted = 9,
878        ReservationModifyFailed = 10,
879        PathModifyFailed = 11,
880        ReservationTornDown = 12
881    }
882}
883
884wire_enum! {
885    /// RSVP protocol error returned by a failed QoS reservation.
886    pub enum RsvpErrorCode {
887        Confirm = 0,
888        Admission = 1,
889        Administrative = 2,
890        NoPathInformation = 3,
891        NoSenderInformation = 4,
892        ConflictingStyle = 5,
893        UnknownStyle = 6,
894        ConflictingDestinationPorts = 7,
895        ConflictingSourcePorts = 8,
896        ServicePreempted = 12,
897        UnknownObjectClass = 13,
898        UnknownClassType = 14,
899        Api = 20,
900        Traffic = 21,
901        TrafficSystem = 22,
902        System = 23,
903        RoutingProblem = 24
904    }
905}
906
907wire_enum! {
908    /// Requested acknowledgement behavior at the end of an announcement.
909    pub enum EndOfAnnouncementAck {
910        NotRequired = 0,
911        Required = 1
912    }
913}
914
915wire_enum! {
916    /// Ordering policy for playing an announcement sequence.
917    pub enum AnnouncementPlayMode {
918        XmlConfigured = 0,
919        OneShot = 1,
920        Continuous = 2
921    }
922}
923
924wire_enum! {
925    /// Completion status returned when announcement playback finishes.
926    pub enum AnnouncementPlayStatus {
927        Ok = 0,
928        Error = 1
929    }
930}
931
932wire_enum! {
933    /// Result returned for a message-waiting notification.
934    pub enum MessageWaitingResult {
935        Ok = 0,
936        GeneralError = 1,
937        RequestRejected = 2,
938        VoicemailCountOutOfBounds = 3,
939        FaxCountOutOfBounds = 4,
940        InvalidPriorityVoicemailCount = 5,
941        InvalidPriorityFaxCount = 6
942    }
943}
944
945wire_enum! {
946    /// Whether a connection-statistics response clears the station counters.
947    pub enum StatisticsProcessing {
948        Clear = 0,
949        DoNotClear = 1
950    }
951}
952
953wire_enum! {
954    /// Network-address family selected by a versioned media layout.
955    pub enum IpAddressType {
956        Ipv4 = 0,
957        Ipv6 = 1,
958        Ipv4AndIpv6 = 2,
959        Invalid = 3
960    }
961}
962
963wire_enum! {
964    /// Restart scope requested from a station.
965    pub enum ResetType {
966        Reset = 1,
967        Restart = 2,
968        ApplyConfiguration = 3
969    }
970}
971
972wire_enum! {
973    /// Policy used to transport connected-call DTMF digits.
974    pub enum DtmfMode {
975        Auto = 0,
976        Rfc2833 = 1,
977        Skinny = 2
978    }
979}
980
981wire_enum! {
982    /// Call-forwarding condition represented by a forwarding entry.
983    pub enum CallForwardKind {
984        None = 0,
985        All = 1,
986        Busy = 2,
987        NoAnswer = 3
988    }
989}
990
991wire_enum! {
992    /// Precedence assigned to a call or media request.
993    pub enum CallPriority {
994        Highest = 0,
995        High = 1,
996        Medium = 2,
997        Low = 3,
998        Normal = 4
999    }
1000}
1001
1002wire_enum! {
1003    /// Ordered status-line notification slots. Larger values take precedence.
1004    pub enum NotificationPriority {
1005        Idle = 0,
1006        Voicemail = 1,
1007        Monitor = 2,
1008        Privacy = 3,
1009        DoNotDisturb = 4,
1010        CallForward = 5,
1011        Timed = 6
1012    }
1013}
1014
1015wire_enum! {
1016    /// Visibility policy for call-information presentation.
1017    pub enum CallInfoVisibility {
1018        Default = 0,
1019        Collapsed = 1,
1020        Hidden = 2
1021    }
1022}
1023
1024wire_enum! {
1025    /// Security indication presented for a call.
1026    pub enum CallSecurityState {
1027        UnknownState = 0,
1028        NotAuthenticated = 1,
1029        Authenticated = 2
1030    }
1031}
1032
1033wire_enum! {
1034    /// Busy-lamp-field availability reported by a subscription notification.
1035    pub enum BusyLampFieldState {
1036        UnknownState = 0,
1037        Idle = 1,
1038        InUse = 2,
1039        DoNotDisturb = 3,
1040        Alerting = 4
1041    }
1042}
1043
1044wire_enum! {
1045    /// Result of a phone-book/BLF subscription request.
1046    pub enum SubscriptionCause {
1047        Ok = 0,
1048        RouteFailure = 1,
1049        AuthenticationFailure = 2,
1050        Timeout = 3,
1051        TrunkTerminated = 4,
1052        TrunkForbidden = 5,
1053        Throttled = 6
1054    }
1055}
1056
1057wire_enum! {
1058    /// Picture-size profile used by a video capability.
1059    pub enum VideoFormat {
1060        Undefined = 0,
1061        Sqcif = 1,
1062        Qcif = 2,
1063        Cif = 3,
1064        Cif4 = 4,
1065        Cif16 = 5,
1066        Custom = 6,
1067        ProtocolUnknown = 232
1068    }
1069}
1070
1071wire_enum! {
1072    /// Codec-specific operation carried by a miscellaneous multimedia command.
1073    pub enum MiscCommandType {
1074        VideoFreezePicture = 0,
1075        VideoFastUpdatePicture = 1,
1076        VideoFastUpdateGob = 2,
1077        VideoFastUpdateMacroblock = 3,
1078        LostPicture = 4,
1079        LostPartialPicture = 5,
1080        RecoveryReferencePicture = 6,
1081        TemporalSpatialTradeoff = 7
1082    }
1083}
1084
1085wire_enum! {
1086    /// Station echo-cancellation policy for an audio channel.
1087    pub enum EchoCancellation {
1088        Off = 0,
1089        On = 1
1090    }
1091}
1092
1093wire_enum! {
1094    /// Station-side voice-activity detection/silence suppression policy.
1095    pub enum SilenceSuppression {
1096        Off = 0,
1097        On = 1
1098    }
1099}
1100
1101wire_enum! {
1102    /// Bit-rate selector occupying the codec qualifier word for G.723.
1103    pub enum G723BitRate {
1104        Rate5_3 = 1,
1105        Rate6_3 = 2
1106    }
1107}
1108
1109wire_enum! {
1110    /// Station result attached to an unregister acknowledgement.
1111    pub enum UnregisterStatus {
1112        Ok = 0,
1113        Error = 1,
1114        ActiveCall = 2
1115    }
1116}
1117
1118wire_enum! {
1119    /// Button definitions use the stimulus values plus provisioning-only
1120    /// placeholder values in the 0xf1..=0xf5 range.
1121    pub enum ButtonType {
1122        Unused = 0x00,
1123        LastNumberRedial = 0x01,
1124        SpeedDial = 0x02,
1125        Hold = 0x03,
1126        Transfer = 0x04,
1127        ForwardAll = 0x05,
1128        ForwardBusy = 0x06,
1129        ForwardNoAnswer = 0x07,
1130        Display = 0x08,
1131        Line = 0x09,
1132        T120Chat = 0x0a,
1133        T120Whiteboard = 0x0b,
1134        T120ApplicationSharing = 0x0c,
1135        T120FileTransfer = 0x0d,
1136        Video = 0x0e,
1137        Voicemail = 0x0f,
1138        AnswerRelease = 0x10,
1139        AutoAnswer = 0x11,
1140        Select = 0x12,
1141        Feature = 0x13,
1142        ServiceUrl = 0x14,
1143        BlfSpeedDial = 0x15,
1144        DirectedPark = 0x16,
1145        Intercom = 0x17,
1146        MaliciousCall = 0x1b,
1147        GenericAppB1 = 0x21,
1148        GenericAppB2 = 0x22,
1149        GenericAppB3 = 0x23,
1150        GenericAppB4 = 0x24,
1151        GenericAppB5 = 0x25,
1152        MultiblinkFeature = 0x26,
1153        MeetMeConference = 0x7b,
1154        Conference = 0x7d,
1155        CallPark = 0x7e,
1156        CallPickup = 0x7f,
1157        GroupCallPickup = 0x80,
1158        Mobility = 0x81,
1159        DoNotDisturb = 0x82,
1160        ConferenceList = 0x83,
1161        RemoveLastParticipant = 0x84,
1162        QualityReportTool = 0x85,
1163        Callback = 0x86,
1164        OtherPickup = 0x87,
1165        VideoMode = 0x88,
1166        NewCall = 0x89,
1167        EndCall = 0x8a,
1168        HuntGroupLogin = 0x8b,
1169        Queuing = 0x8f,
1170        ParkingLot = 0xc0,
1171        Messages = 0xc2,
1172        Directory = 0xc3,
1173        Application = 0xc5,
1174        Headset = 0xc6,
1175        Keypad = 0xf0,
1176        PlaceholderMulti = 0xf1,
1177        PlaceholderLine = 0xf2,
1178        PlaceholderSpeedDial = 0xf3,
1179        PlaceholderHint = 0xf4,
1180        PlaceholderAbbreviatedDial = 0xf5,
1181        AcousticEchoCancellation = 0xfd,
1182        Undefined = 0xff
1183    }
1184}
1185
1186wire_enum! {
1187    /// SRTP encryption algorithm selected for a media channel.
1188    pub enum EncryptionMethod {
1189        None = 0,
1190        Aes128HmacSha1_32 = 1,
1191        Aes128HmacSha1_80 = 2,
1192        F8_128HmacSha1_32 = 3,
1193        F8_128HmacSha1_80 = 4,
1194        AeadAes128Gcm = 5,
1195        AeadAes256Gcm = 6
1196    }
1197}
1198
1199wire_enum! {
1200    /// SRTP algorithm support advertised by a media capability.
1201    pub enum EncryptionCapability {
1202        NotCapable = 0,
1203        Capable = 1
1204    }
1205}
1206
1207wire_enum! {
1208    /// History bucket assigned to a completed call.
1209    pub enum CallHistoryDisposition {
1210        Ignore = 0,
1211        Placed = 1,
1212        Received = 2,
1213        Missed = 3,
1214        ProtocolUnknown = 0xffff_fffe
1215    }
1216}
1217
1218wire_enum! {
1219    /// Station speaker state selected by call control.
1220    pub enum SpeakerMode {
1221        On = 1,
1222        Off = 2
1223    }
1224}
1225
1226wire_enum! {
1227    /// Station microphone state selected by call control.
1228    pub enum MicrophoneMode {
1229        On = 1,
1230        Off = 2
1231    }
1232}
1233
1234wire_enum! {
1235    /// Media resource allocated for a station-managed conference.
1236    pub enum ConferenceResourceType {
1237        Conference = 0,
1238        InteractiveVoiceResponse = 1
1239    }
1240}
1241
1242wire_enum! {
1243    /// Outcome returned by conference creation.
1244    pub enum CreateConferenceResult {
1245        Ok = 0,
1246        ResourceNotAvailable = 1,
1247        ConferenceAlreadyExists = 2,
1248        SystemError = 3
1249    }
1250}
1251
1252wire_enum! {
1253    /// Outcome returned by conference deletion.
1254    pub enum DeleteConferenceResult {
1255        Ok = 0,
1256        ConferenceDoesNotExist = 1,
1257        SystemError = 2
1258    }
1259}
1260
1261wire_enum! {
1262    /// Outcome returned by conference modification.
1263    pub enum ModifyConferenceResult {
1264        Ok = 0,
1265        ResourceNotAvailable = 1,
1266        ConferenceDoesNotExist = 2,
1267        InvalidParameter = 3,
1268        MoreActiveCallsThanReserved = 4,
1269        InvalidResourceType = 5,
1270        SystemError = 6
1271    }
1272}
1273
1274wire_enum! {
1275    /// Outcome returned when attaching a conference participant.
1276    pub enum AddParticipantResult {
1277        Ok = 0,
1278        ResourceNotAvailable = 1,
1279        ConferenceDoesNotExist = 2,
1280        DuplicateCallReference = 3,
1281        SystemError = 4
1282    }
1283}
1284
1285wire_enum! {
1286    /// Outcome returned by a conference-participant audit.
1287    pub enum AuditParticipantResult {
1288        Ok = 0,
1289        ConferenceDoesNotExist = 1
1290    }
1291}
1292
1293bitflags! {
1294    /// Identity fields a station must suppress for a conference participant.
1295    #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
1296    pub struct PartyInformationRestrictions: u32 {
1297        const CALLING_NAME = 1 << 0;
1298        const CALLING_NUMBER = 1 << 1;
1299        const CALLED_NAME = 1 << 2;
1300        const CALLED_NUMBER = 1 << 3;
1301        const ORIGINAL_CALLED_NAME = 1 << 4;
1302        const ORIGINAL_CALLED_NUMBER = 1 << 5;
1303        const LAST_REDIRECT_NAME = 1 << 6;
1304        const LAST_REDIRECT_NUMBER = 1 << 7;
1305    }
1306}
1307
1308/// Negotiated inputs that select station-facing message layouts.
1309#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1310pub struct StationSessionContext {
1311    /// Negotiated frame and payload version.
1312    pub protocol: ProtocolVersion,
1313    /// Station feature bits that can select layouts independently of version.
1314    pub features: PhoneFeatures,
1315}
1316
1317impl StationSessionContext {
1318    /// Creates the layout-selection context for a registered station session.
1319    pub const fn new(protocol: ProtocolVersion, features: PhoneFeatures) -> Self {
1320        Self { protocol, features }
1321    }
1322
1323    /// Reports whether general UI responses use their dynamic string layouts.
1324    pub const fn uses_dynamic_general_ui(self) -> bool {
1325        self.protocol.uses_dynamic_general_ui()
1326            || self.features.contains(PhoneFeatures::DYNAMIC_MESSAGES)
1327    }
1328
1329    /// Reports whether feature status uses its dynamic response identifier.
1330    pub const fn uses_dynamic_feature_status(self) -> bool {
1331        self.features.contains(PhoneFeatures::DYNAMIC_MESSAGES)
1332    }
1333
1334    /// Returns the call-info string layout selected by the negotiated version.
1335    pub const fn dynamic_call_info_layout(self) -> DynamicCallInfoLayout {
1336        self.protocol.dynamic_call_info_layout()
1337    }
1338
1339    /// Returns the dynamic service-URL string count selected by the session.
1340    pub const fn dynamic_service_url_string_count(self) -> usize {
1341        if self.protocol.wire() >= ProtocolVersion::V19.wire() {
1342            3
1343        } else {
1344            2
1345        }
1346    }
1347}
1348
1349impl From<ProtocolVersion> for StationSessionContext {
1350    fn from(protocol: ProtocolVersion) -> Self {
1351        Self::new(protocol, PhoneFeatures::empty())
1352    }
1353}
1354
1355/// Dynamic RTP payload type used for telephone-event DTMF when a station is
1356/// configured to send digits through the media stream.
1357pub const RFC2833_TELEPHONE_EVENT_PAYLOAD: u8 = 101;
1358
1359impl DtmfMode {
1360    /// Resolves the automatic policy against the feature bits advertised by
1361    /// the registered station. Explicit policies are always preserved.
1362    pub const fn resolve(self, features: PhoneFeatures) -> Self {
1363        match self {
1364            Self::Auto if features.contains(PhoneFeatures::RFC2833) => Self::Rfc2833,
1365            Self::Auto => Self::Skinny,
1366            explicit => explicit,
1367        }
1368    }
1369
1370    /// Returns the wire payload type for the resolved policy. A zero payload
1371    /// tells the station to report connected-call digits as signaling events.
1372    pub const fn telephone_event_payload(self, features: PhoneFeatures) -> u8 {
1373        match self.resolve(features) {
1374            Self::Rfc2833 => RFC2833_TELEPHONE_EVENT_PAYLOAD,
1375            Self::Skinny | Self::Unknown(_) => 0,
1376            Self::Auto => unreachable!(),
1377        }
1378    }
1379}
1380
1381bitflags! {
1382    /// Feature flags advertised in the three-byte station protocol field.
1383    #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
1384    pub struct PhoneFeatures: u32 {
1385        // The low byte contains the protocol version. Feature bits occupy the
1386        // following three bytes.
1387        const PORT_REQUEST = 1 << 17;
1388        const UTF8 = 1 << 20;
1389        const DYNAMIC_MESSAGES = 1 << 24;
1390        const RFC2833 = 1 << 26;
1391        const INTERNAL_CM_MEDIA = 1 << 28;
1392        const ABBREVIATED_DIAL = 1 << 31;
1393    }
1394}
1395
1396bitflags! {
1397    /// Permitted media directions in a capability entry.
1398    #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
1399    pub struct ReceiveTransmit: u32 {
1400        const RECEIVE = 1;
1401        const TRANSMIT = 2;
1402    }
1403}
1404
1405#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1406/// One keypad digit, including the extended A-D symbols.
1407pub enum Digit {
1408    /// A numeric digit; valid decoded values are zero through nine.
1409    Number(u8),
1410    Star,
1411    Pound,
1412    A,
1413    B,
1414    C,
1415    D,
1416    /// An unrecognized keypad word retained from the wire.
1417    Unknown(u32),
1418}
1419
1420impl Digit {
1421    /// Converts a keypad wire word to a typed digit without discarding unknowns.
1422    pub const fn from_keypad(value: u32) -> Self {
1423        match value {
1424            0..=9 => Self::Number(value as u8),
1425            10 => Self::Star,
1426            11 => Self::Pound,
1427            12 => Self::A,
1428            13 => Self::B,
1429            14 => Self::C,
1430            15 => Self::D,
1431            value => Self::Unknown(value),
1432        }
1433    }
1434
1435    /// Returns the numeric keypad word used on the wire.
1436    pub const fn keypad_value(self) -> u32 {
1437        match self {
1438            Self::Number(number) => number as u32,
1439            Self::Star => 10,
1440            Self::Pound => 11,
1441            Self::A => 12,
1442            Self::B => 13,
1443            Self::C => 14,
1444            Self::D => 15,
1445            Self::Unknown(value) => value,
1446        }
1447    }
1448
1449    /// Returns the printable digit, or `?` for an invalid/unknown value.
1450    pub fn as_char(self) -> char {
1451        match self {
1452            Self::Number(n) if n <= 9 => char::from(b'0' + n),
1453            Self::Number(_) => '?',
1454            Self::Star => '*',
1455            Self::Pound => '#',
1456            Self::A => 'A',
1457            Self::B => 'B',
1458            Self::C => 'C',
1459            Self::D => 'D',
1460            Self::Unknown(_) => '?',
1461        }
1462    }
1463}
1464
1465impl From<u32> for Digit {
1466    fn from(value: u32) -> Self {
1467        Self::from_keypad(value)
1468    }
1469}
1470
1471impl From<Digit> for u32 {
1472    fn from(value: Digit) -> Self {
1473        value.keypad_value()
1474    }
1475}
1476
1477#[cfg(test)]
1478mod tests {
1479    use super::*;
1480
1481    #[test]
1482    fn protocol_versions_select_layout_profiles() {
1483        assert_eq!(ProtocolVersion::new(3).unwrap().layout(), LayoutProfile::V3);
1484        assert_eq!(
1485            ProtocolVersion::new(14).unwrap().layout(),
1486            LayoutProfile::V11
1487        );
1488        assert_eq!(ProtocolVersion::new(13).unwrap(), ProtocolVersion::V13);
1489        assert_eq!(ProtocolVersion::new(14).unwrap(), ProtocolVersion::V14);
1490        assert_eq!(
1491            ProtocolVersion::new(18).unwrap().layout(),
1492            LayoutProfile::V18
1493        );
1494        assert_eq!(
1495            ProtocolVersion::new(21).unwrap().layout(),
1496            LayoutProfile::V19
1497        );
1498        assert_eq!(
1499            ProtocolVersion::new(22).unwrap().layout(),
1500            LayoutProfile::V22
1501        );
1502        assert_eq!(
1503            ProtocolVersion::negotiate(99).unwrap(),
1504            ProtocolVersion::V22
1505        );
1506        assert!(ProtocolVersion::new(2).is_err());
1507    }
1508
1509    #[test]
1510    fn dynamic_station_layout_boundaries_follow_session_negotiation() {
1511        assert!(!ProtocolVersion::V8.uses_dynamic_general_ui());
1512        assert!(ProtocolVersion::V9.uses_dynamic_general_ui());
1513        assert!(
1514            !ProtocolVersion::new(14)
1515                .unwrap()
1516                .uses_dynamic_speed_dial_status()
1517        );
1518        assert!(ProtocolVersion::V15.uses_dynamic_speed_dial_status());
1519
1520        assert_eq!(
1521            ProtocolVersion::V15.dynamic_call_info_layout(),
1522            DynamicCallInfoLayout::Fields12
1523        );
1524        assert_eq!(
1525            ProtocolVersion::V16.dynamic_call_info_layout(),
1526            DynamicCallInfoLayout::Fields13
1527        );
1528        assert_eq!(
1529            ProtocolVersion::V18.dynamic_call_info_layout(),
1530            DynamicCallInfoLayout::Fields13
1531        );
1532        assert_eq!(
1533            ProtocolVersion::V19.dynamic_call_info_layout(),
1534            DynamicCallInfoLayout::Fields15
1535        );
1536
1537        let baseline = StationSessionContext::from(ProtocolVersion::V8);
1538        assert!(!baseline.uses_dynamic_general_ui());
1539        assert!(!baseline.uses_dynamic_feature_status());
1540        let negotiated =
1541            StationSessionContext::new(ProtocolVersion::V8, PhoneFeatures::DYNAMIC_MESSAGES);
1542        assert!(negotiated.uses_dynamic_general_ui());
1543        assert!(negotiated.uses_dynamic_feature_status());
1544    }
1545
1546    #[test]
1547    fn extensible_values_preserve_unknown_numbers() {
1548        let codec = Codec::from(0xfeed);
1549        assert_eq!(codec, Codec::Unknown(0xfeed));
1550        assert_eq!(codec.wire_value(), 0xfeed);
1551        let state = CallState::from(1000);
1552        assert_eq!(state.wire_value(), 1000);
1553    }
1554
1555    #[test]
1556    fn soft_key_events_are_template_positions() {
1557        assert_eq!(SoftKey::from(1), SoftKey::Redial);
1558        assert_eq!(SoftKey::from(13), SoftKey::Conference);
1559        assert_eq!(SoftKey::from(32), SoftKey::Dial);
1560        assert_eq!(SoftKey::from(201), SoftKey::Unknown(201));
1561    }
1562
1563    #[test]
1564    fn automatic_dtmf_uses_only_an_advertised_rfc2833_capability() {
1565        assert_eq!(
1566            DtmfMode::Auto.resolve(PhoneFeatures::RFC2833),
1567            DtmfMode::Rfc2833
1568        );
1569        assert_eq!(
1570            DtmfMode::Auto.telephone_event_payload(PhoneFeatures::RFC2833),
1571            RFC2833_TELEPHONE_EVENT_PAYLOAD
1572        );
1573        assert_eq!(
1574            DtmfMode::Auto.resolve(PhoneFeatures::empty()),
1575            DtmfMode::Skinny
1576        );
1577        assert_eq!(
1578            DtmfMode::Auto.telephone_event_payload(PhoneFeatures::empty()),
1579            0
1580        );
1581        assert_eq!(
1582            DtmfMode::Skinny.telephone_event_payload(PhoneFeatures::RFC2833),
1583            0
1584        );
1585        assert_eq!(
1586            DtmfMode::Rfc2833.telephone_event_payload(PhoneFeatures::empty()),
1587            RFC2833_TELEPHONE_EVENT_PAYLOAD
1588        );
1589    }
1590
1591    #[test]
1592    fn phone_feature_bits_match_the_three_register_feature_bytes() {
1593        // CP-7961G firmware SCCP41.9-4-2SR3-1S advertises protocol/features
1594        // 16 00 72 85. The final feature byte carries dynamic messages,
1595        // RFC2833, and abbreviated dial.
1596        let advertised = PhoneFeatures::from_bits_retain(0x8572_0000);
1597        assert!(advertised.contains(PhoneFeatures::PORT_REQUEST));
1598        assert!(advertised.contains(PhoneFeatures::UTF8));
1599        assert!(advertised.contains(PhoneFeatures::DYNAMIC_MESSAGES));
1600        assert!(advertised.contains(PhoneFeatures::RFC2833));
1601        assert!(advertised.contains(PhoneFeatures::ABBREVIATED_DIAL));
1602        assert!(!advertised.contains(PhoneFeatures::INTERNAL_CM_MEDIA));
1603    }
1604
1605    #[test]
1606    fn every_named_wire_enum_value_is_unique_and_round_trips() {
1607        macro_rules! assert_wire_enum {
1608            ($type:ty) => {{
1609                let mut values = std::collections::HashSet::new();
1610                for value in <$type>::ALL_KNOWN {
1611                    assert!(
1612                        values.insert(value.wire_value()),
1613                        "duplicate {} value {value:?}",
1614                        stringify!($type)
1615                    );
1616                    assert_eq!(<$type>::from(value.wire_value()), *value);
1617                    assert!(value.is_known());
1618                }
1619            }};
1620        }
1621
1622        assert_wire_enum!(DeviceType);
1623        assert_wire_enum!(Codec);
1624        assert_wire_enum!(CallState);
1625        assert_wire_enum!(CallType);
1626        assert_wire_enum!(AlarmSeverity);
1627        assert_wire_enum!(MediaStatus);
1628        assert_wire_enum!(Stimulus);
1629        assert_wire_enum!(SoftKey);
1630        assert_wire_enum!(KeyMode);
1631        assert_wire_enum!(RingerMode);
1632        assert_wire_enum!(RingDuration);
1633        assert_wire_enum!(LampMode);
1634        assert_wire_enum!(Tone);
1635        assert_wire_enum!(ToneDirection);
1636        assert_wire_enum!(MediaPathId);
1637        assert_wire_enum!(MediaPathEvent);
1638        assert_wire_enum!(MediaPathCapability);
1639        assert_wire_enum!(MediaType);
1640        assert_wire_enum!(MediaTransport);
1641        assert_wire_enum!(QosDirection);
1642        assert_wire_enum!(QosReservationStyle);
1643        assert_wire_enum!(QosErrorCode);
1644        assert_wire_enum!(RsvpErrorCode);
1645        assert_wire_enum!(EndOfAnnouncementAck);
1646        assert_wire_enum!(AnnouncementPlayMode);
1647        assert_wire_enum!(AnnouncementPlayStatus);
1648        assert_wire_enum!(MessageWaitingResult);
1649        assert_wire_enum!(IpAddressType);
1650        assert_wire_enum!(ResetType);
1651        assert_wire_enum!(DtmfMode);
1652        assert_wire_enum!(CallForwardKind);
1653        assert_wire_enum!(CallPriority);
1654        assert_wire_enum!(NotificationPriority);
1655        assert_wire_enum!(CallInfoVisibility);
1656        assert_wire_enum!(CallSecurityState);
1657        assert_wire_enum!(BusyLampFieldState);
1658        assert_wire_enum!(SubscriptionCause);
1659        assert_wire_enum!(VideoFormat);
1660        assert_wire_enum!(MiscCommandType);
1661        assert_wire_enum!(EchoCancellation);
1662        assert_wire_enum!(SilenceSuppression);
1663        assert_wire_enum!(G723BitRate);
1664        assert_wire_enum!(UnregisterStatus);
1665        assert_wire_enum!(ButtonType);
1666        assert_wire_enum!(EncryptionMethod);
1667        assert_wire_enum!(EncryptionCapability);
1668        assert_wire_enum!(CallHistoryDisposition);
1669        assert_wire_enum!(SpeakerMode);
1670        assert_wire_enum!(MicrophoneMode);
1671        assert_wire_enum!(ConferenceResourceType);
1672        assert_wire_enum!(CreateConferenceResult);
1673        assert_wire_enum!(DeleteConferenceResult);
1674        assert_wire_enum!(ModifyConferenceResult);
1675        assert_wire_enum!(AddParticipantResult);
1676        assert_wire_enum!(AuditParticipantResult);
1677    }
1678
1679    #[test]
1680    fn codec_metadata_covers_static_and_cisco_dynamic_payloads() {
1681        assert_eq!(Codec::Pcmu.rtp_payload_type(), Some(0));
1682        assert_eq!(Codec::G711Ulaw56k.rtp_payload_type(), Some(0));
1683        assert_eq!(Codec::Pcma.rtp_payload_type(), Some(8));
1684        assert_eq!(Codec::G72248k.rtp_payload_type(), Some(9));
1685        assert_eq!(Codec::Wideband256k.rtp_payload_type(), Some(25));
1686        assert_eq!(Codec::Ilbc.rtp_payload_type(), Some(97));
1687        assert_eq!(Codec::G7221_32k.rtp_payload_type(), Some(102));
1688        assert_eq!(Codec::Opus.rtp_payload_type(), Some(107));
1689        assert_eq!(Codec::G726_32k.rtp_payload_type(), Some(112));
1690        assert_eq!(Codec::Wideband256k.sample_rate(), Some(16_000));
1691        assert_eq!(Codec::H264.kind(), CodecKind::Video);
1692        assert_eq!(Codec::ClearChannel.kind(), CodecKind::Data);
1693    }
1694}