Skip to main content

bacnet_encoding/constructed/event_parameter/
mod.rs

1//! `BACnetEventParameter` full ASN.1 framing.
2//!
3//! The Event_Parameters CHOICE (ASHRAE 135-2020 Clause 21) picks one event
4//! algorithm by context tag, the number of the event type it reports, and
5//! carries that algorithm's settings as context-tagged members. Every
6//! algorithm the 2020 production assigns has a [`BACnetEventParameter`]
7//! variant, from change-of-bitstring `[0]` to change-of-timer `[22]`, `none`
8//! `[20]` (a primitive NULL) included; see the variants for their members.
9//! The decoders for the variants added in #2090 are in `members`.
10//!
11//! A list member is its tag's opening and closing pair around application
12//! tagged items, at most 10,000 of them, and a reference member the same
13//! pair around the reference's own members. An `extended` parameter list is
14//! kept as its encoded items, each checked to be an application value or a
15//! `[0]`-framed reference.
16//!
17//! Tags 6, 7, 12 and 19 carry no alternative (Clause 21 omits 6 and 19,
18//! deprecates 7 and reserves 12), so decoding one is an error. An
19//! alternative under a tag the 2020 production doesn't assign (23 and up) is
20//! preserved byte-for-byte as [`BACnetEventParameter::Opaque`].
21
22#[cfg(not(feature = "std"))]
23use alloc::vec::Vec;
24
25use bacnet_types::constructed::{
26    BACnetDeviceObjectPropertyReference, BACnetEventParameter, ChangeOfValueCriteria,
27};
28use bacnet_types::error::{DecodeError, DecodingKind, EncodeError, Error};
29use bytes::BytesMut;
30
31use crate::primitives;
32use crate::tags;
33
34use super::tagged::{
35    contents, decode_app_bit_string, decode_ctx_bit_string, decode_ctx_constructed,
36    decode_ctx_real, decode_ctx_unsigned, expect_closing, expect_opening, next_is_closing,
37};
38use super::{
39    decode_dopr_body, decode_property_state, encode_dopr_body, encode_property_state,
40    validate_extended_parameters, validate_tlv_sequence, MAX_FRAMED_ITEMS,
41};
42
43/// Context tags the Clause 21 production omits, deprecates, or reserves.
44/// Decoding one means the peer is speaking something other than the 135-2020
45/// production, so these are hard errors rather than Opaque shadows.
46const RESERVED_EVENT_TAGS: [u8; 4] = [6, 7, 12, 19];
47
48/// The highest tag the 2020 production assigns or reserves: every tag up to
49/// it has a variant or is rejected, so none of them is [`BACnetEventParameter::Opaque`].
50const LAST_ASSIGNED_TAG: u8 = 22;
51
52/// Encode a [`BACnetEventParameter`] as its full CHOICE framing.
53///
54/// Validation includes the complete framing depth. Errors leave `buf`
55/// unchanged.
56pub fn encode_event_parameter(
57    buf: &mut BytesMut,
58    value: &BACnetEventParameter,
59) -> Result<(), Error> {
60    let mut encoded = BytesMut::new();
61    encode_event_parameter_into(&mut encoded, value)?;
62    validate_tlv_sequence(&encoded, "BACnetEventParameter")
63        .map_err(Error::into_encode_refusal)?;
64    buf.extend_from_slice(&encoded);
65    Ok(())
66}
67
68fn encode_event_parameter_into(
69    buf: &mut BytesMut,
70    value: &BACnetEventParameter,
71) -> Result<(), Error> {
72    match value {
73        BACnetEventParameter::ChangeOfBitstring {
74            time_delay,
75            bitmask,
76            list_of_values,
77        } => {
78            tags::encode_opening_tag(buf, 0);
79            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
80            primitives::encode_ctx_bit_string(buf, 1, bitmask.0, &bitmask.1);
81            tags::encode_opening_tag(buf, 2);
82            for (unused_bits, data) in list_of_values {
83                primitives::encode_app_bit_string(buf, *unused_bits, data);
84            }
85            tags::encode_closing_tag(buf, 2);
86            tags::encode_closing_tag(buf, 0);
87        }
88        BACnetEventParameter::ChangeOfState {
89            time_delay,
90            list_of_values,
91        } => {
92            let mut encoded_states = BytesMut::new();
93            for state in list_of_values {
94                encode_property_state(&mut encoded_states, state)?;
95            }
96            tags::encode_opening_tag(buf, 1);
97            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
98            tags::encode_opening_tag(buf, 1);
99            buf.extend_from_slice(&encoded_states);
100            tags::encode_closing_tag(buf, 1);
101            tags::encode_closing_tag(buf, 1);
102        }
103        BACnetEventParameter::ChangeOfValue {
104            time_delay,
105            criteria,
106        } => {
107            tags::encode_opening_tag(buf, 2);
108            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
109            // cov-criteria [1] CHOICE — a tagged CHOICE is always explicitly
110            // tagged: opening/closing [1] around the alternative's own tag.
111            tags::encode_opening_tag(buf, 1);
112            match criteria {
113                ChangeOfValueCriteria::Bitmask { unused_bits, data } => {
114                    primitives::encode_ctx_bit_string(buf, 0, *unused_bits, data);
115                }
116                ChangeOfValueCriteria::ReferencedPropertyIncrement(increment) => {
117                    primitives::encode_ctx_real(buf, 1, *increment);
118                }
119            }
120            tags::encode_closing_tag(buf, 1);
121            tags::encode_closing_tag(buf, 2);
122        }
123        BACnetEventParameter::FloatingLimit {
124            time_delay,
125            setpoint_reference,
126            low_diff_limit,
127            high_diff_limit,
128            deadband,
129        } => {
130            tags::encode_opening_tag(buf, 4);
131            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
132            // setpoint-reference [1] BACnetDeviceObjectPropertyReference (SEQUENCE)
133            tags::encode_opening_tag(buf, 1);
134            encode_dopr_body(buf, setpoint_reference);
135            tags::encode_closing_tag(buf, 1);
136            primitives::encode_ctx_real(buf, 2, *low_diff_limit);
137            primitives::encode_ctx_real(buf, 3, *high_diff_limit);
138            primitives::encode_ctx_real(buf, 4, *deadband);
139            tags::encode_closing_tag(buf, 4);
140        }
141        BACnetEventParameter::OutOfRange {
142            time_delay,
143            low_limit,
144            high_limit,
145            deadband,
146        } => {
147            tags::encode_opening_tag(buf, 5);
148            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
149            primitives::encode_ctx_real(buf, 1, *low_limit);
150            primitives::encode_ctx_real(buf, 2, *high_limit);
151            primitives::encode_ctx_real(buf, 3, *deadband);
152            tags::encode_closing_tag(buf, 5);
153        }
154        BACnetEventParameter::Extended {
155            vendor_id,
156            extended_event_type,
157            parameters,
158        } => {
159            validate_extended_parameters(parameters, "extended")
160                .map_err(Error::into_encode_refusal)?;
161            tags::encode_opening_tag(buf, 9);
162            primitives::encode_ctx_unsigned(buf, 0, *vendor_id as u64);
163            primitives::encode_ctx_unsigned(buf, 1, *extended_event_type as u64);
164            // parameters [2] SEQUENCE OF CHOICE — held as raw pre-encoded items.
165            tags::encode_opening_tag(buf, 2);
166            buf.extend_from_slice(parameters);
167            tags::encode_closing_tag(buf, 2);
168            tags::encode_closing_tag(buf, 9);
169        }
170        BACnetEventParameter::None => {
171            // Clause 21 defines none [20] as context NULL, not an empty SEQUENCE.
172            tags::encode_tag(buf, 20, tags::TagClass::Context, 0);
173        }
174        BACnetEventParameter::Opaque { tag, data } if *tag == u8::MAX => {
175            // Legacy pre-framing EventEnrollment values used an Octet String.
176            primitives::encode_app_octet_string(buf, data);
177        }
178        BACnetEventParameter::Opaque { tag, .. } if *tag <= LAST_ASSIGNED_TAG => {
179            return Err(
180                EncodeError::new("tag [{}] cannot use an opaque constructed body")
181                    .within("BACnetEventParameter")
182                    .with_value((*tag).into())
183                    .into(),
184            );
185        }
186        BACnetEventParameter::Opaque { tag, data } => {
187            validate_tlv_sequence(data, "opaque event parameters")
188                .map_err(Error::into_encode_refusal)?;
189            // Preserved alternative: its captured bytes are the complete
190            // SEQUENCE body, re-emitted under its own opening/closing pair.
191            tags::encode_opening_tag(buf, *tag);
192            buf.extend_from_slice(data);
193            tags::encode_closing_tag(buf, *tag);
194        }
195        BACnetEventParameter::CommandFailure {
196            time_delay,
197            feedback_property_reference,
198        } => {
199            tags::encode_opening_tag(buf, 3);
200            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
201            encode_reference(buf, 1, feedback_property_reference);
202            tags::encode_closing_tag(buf, 3);
203        }
204        BACnetEventParameter::ChangeOfLifeSafety {
205            time_delay,
206            list_of_life_safety_alarm_values,
207            list_of_alarm_values,
208            mode_property_reference,
209        } => {
210            tags::encode_opening_tag(buf, 8);
211            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
212            encode_enumerations(buf, 1, list_of_life_safety_alarm_values.iter().map(|s| s.to_raw()));
213            encode_enumerations(buf, 2, list_of_alarm_values.iter().map(|s| s.to_raw()));
214            encode_reference(buf, 3, mode_property_reference);
215            tags::encode_closing_tag(buf, 8);
216        }
217        BACnetEventParameter::BufferReady {
218            notification_threshold,
219            previous_notification_count,
220        } => {
221            tags::encode_opening_tag(buf, 10);
222            primitives::encode_ctx_unsigned(buf, 0, u64::from(*notification_threshold));
223            primitives::encode_ctx_unsigned(buf, 1, u64::from(*previous_notification_count));
224            tags::encode_closing_tag(buf, 10);
225        }
226        BACnetEventParameter::UnsignedRange {
227            time_delay,
228            low_limit,
229            high_limit,
230        } => {
231            tags::encode_opening_tag(buf, 11);
232            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
233            primitives::encode_ctx_unsigned(buf, 1, *low_limit);
234            primitives::encode_ctx_unsigned(buf, 2, *high_limit);
235            tags::encode_closing_tag(buf, 11);
236        }
237        BACnetEventParameter::AccessEvent {
238            list_of_access_events,
239            access_event_time_reference,
240        } => {
241            tags::encode_opening_tag(buf, 13);
242            encode_enumerations(buf, 0, list_of_access_events.iter().map(|e| e.to_raw()));
243            encode_reference(buf, 1, access_event_time_reference);
244            tags::encode_closing_tag(buf, 13);
245        }
246        BACnetEventParameter::DoubleOutOfRange {
247            time_delay,
248            low_limit,
249            high_limit,
250            deadband,
251        } => {
252            tags::encode_opening_tag(buf, 14);
253            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
254            primitives::encode_ctx_double(buf, 1, *low_limit);
255            primitives::encode_ctx_double(buf, 2, *high_limit);
256            primitives::encode_ctx_double(buf, 3, *deadband);
257            tags::encode_closing_tag(buf, 14);
258        }
259        BACnetEventParameter::SignedOutOfRange {
260            time_delay,
261            low_limit,
262            high_limit,
263            deadband,
264        } => {
265            tags::encode_opening_tag(buf, 15);
266            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
267            primitives::encode_ctx_signed(buf, 1, *low_limit);
268            primitives::encode_ctx_signed(buf, 2, *high_limit);
269            primitives::encode_ctx_unsigned(buf, 3, *deadband);
270            tags::encode_closing_tag(buf, 15);
271        }
272        BACnetEventParameter::UnsignedOutOfRange {
273            time_delay,
274            low_limit,
275            high_limit,
276            deadband,
277        } => {
278            tags::encode_opening_tag(buf, 16);
279            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
280            primitives::encode_ctx_unsigned(buf, 1, *low_limit);
281            primitives::encode_ctx_unsigned(buf, 2, *high_limit);
282            primitives::encode_ctx_unsigned(buf, 3, *deadband);
283            tags::encode_closing_tag(buf, 16);
284        }
285        BACnetEventParameter::ChangeOfCharacterstring {
286            time_delay,
287            list_of_alarm_values,
288        } => {
289            let mut strings = BytesMut::new();
290            for value in list_of_alarm_values {
291                primitives::encode_app_character_string(&mut strings, value)?;
292            }
293            tags::encode_opening_tag(buf, 17);
294            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
295            tags::encode_opening_tag(buf, 1);
296            buf.extend_from_slice(&strings);
297            tags::encode_closing_tag(buf, 1);
298            tags::encode_closing_tag(buf, 17);
299        }
300        BACnetEventParameter::ChangeOfStatusFlags {
301            time_delay,
302            selected_flags,
303        } => {
304            tags::encode_opening_tag(buf, 18);
305            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
306            primitives::encode_ctx_bit_string(buf, 1, 4, &[selected_flags.bits() << 4]);
307            tags::encode_closing_tag(buf, 18);
308        }
309        BACnetEventParameter::ChangeOfDiscreteValue { time_delay } => {
310            tags::encode_opening_tag(buf, 21);
311            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
312            tags::encode_closing_tag(buf, 21);
313        }
314        BACnetEventParameter::ChangeOfTimer {
315            time_delay,
316            alarm_values,
317            update_time_reference,
318        } => {
319            tags::encode_opening_tag(buf, 22);
320            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
321            encode_enumerations(buf, 1, alarm_values.iter().map(|s| s.to_raw()));
322            encode_reference(buf, 2, update_time_reference);
323            tags::encode_closing_tag(buf, 22);
324        }
325    }
326    Ok(())
327}
328
329/// A reference member: its opening tag, the reference's members, its
330/// closing tag.
331fn encode_reference(buf: &mut BytesMut, tag: u8, reference: &BACnetDeviceObjectPropertyReference) {
332    tags::encode_opening_tag(buf, tag);
333    encode_dopr_body(buf, reference);
334    tags::encode_closing_tag(buf, tag);
335}
336
337/// A list of enumerations member: its opening tag, each value application
338/// tagged, its closing tag.
339fn encode_enumerations(buf: &mut BytesMut, tag: u8, values: impl Iterator<Item = u32>) {
340    tags::encode_opening_tag(buf, tag);
341    for value in values {
342        primitives::encode_app_enumerated(buf, value);
343    }
344    tags::encode_closing_tag(buf, tag);
345}
346
347/// Decode one framed [`BACnetEventParameter`] CHOICE alternative at `offset`.
348///
349/// Returns the value and the offset past its encoding. An alternative under a
350/// tag the 2020 production doesn't assign is preserved as
351/// [`BACnetEventParameter::Opaque`]; the omitted/deprecated/reserved tags 6,
352/// 7, 12, 19 are rejected.
353pub fn decode_event_parameter(
354    data: &[u8],
355    offset: usize,
356) -> Result<(BACnetEventParameter, usize), Error> {
357    use BACnetEventParameter as EP;
358
359    // Pre-framing EventEnrollment writes use [255] as an internal wrapper.
360    // Keep this shipped compatibility form local so the generic tag parser
361    // can reject ASHRAE-reserved tag number 255 everywhere else.
362    if data.get(offset..offset.saturating_add(2)) == Some(&[0xFE, 0xFF]) {
363        let body_start = offset + 2;
364        // The wrapper needs its closing pair after the opening one.
365        if data.len() < body_start + 2 {
366            return Err(Error::buffer_too_short(body_start + 2, data.len()));
367        }
368        let body_end = data.len() - 2;
369        if data.get(body_end..) != Some(&[0xFF, 0xFF]) {
370            return Err(Error::decoding(
371                offset,
372                "BACnetEventParameter: missing legacy closing tag",
373            ));
374        }
375        return Ok((
376            EP::Opaque {
377                tag: u8::MAX,
378                data: data[body_start..body_end].to_vec(),
379            },
380            data.len(),
381        ));
382    }
383
384    let (tag, pos) = tags::decode_tag(data, offset)?;
385    let what = "BACnetEventParameter";
386
387    if tag.class == tags::TagClass::Application && tag.number == tags::app_tag::OCTET_STRING {
388        let (octets, end) = contents(data, pos, tag.length)?;
389        return Ok((
390            EP::Opaque {
391                tag: u8::MAX,
392                data: octets.to_vec(),
393            },
394            end,
395        ));
396    }
397
398    if tag.class != tags::TagClass::Context {
399        return Err(Error::decoding(
400            offset,
401            "BACnetEventParameter: expected a context tag",
402        ));
403    }
404
405    if RESERVED_EVENT_TAGS.contains(&tag.number) {
406        return Err(DecodeError::new(
407            DecodingKind::InvalidEncoding,
408            offset,
409            "BACnetEventParameter: context tag [{}] is omitted/deprecated/reserved in Clause 21",
410        )
411        .with_value(tag.number.into())
412        .into());
413    }
414
415    // `none [20] NULL` is the only primitive alternative in this CHOICE.
416    if !tag.is_opening && !tag.is_closing {
417        if tag.number == 20 && tag.length == 0 {
418            return Ok((EP::None, pos));
419        }
420        return Err(Error::decoding(
421            offset,
422            "BACnetEventParameter: invalid primitive alternative",
423        ));
424    }
425
426    if !tag.is_opening {
427        return Err(Error::decoding(
428            offset,
429            "BACnetEventParameter: unexpected closing tag",
430        ));
431    }
432    if tag.number == 20 {
433        return Err(Error::decoding(
434            offset,
435            "BACnetEventParameter: none [20] must use primitive NULL form",
436        ));
437    }
438
439    let value = match tag.number {
440        0 => {
441            let what = "BACnetEventParameter change-of-bitstring";
442            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
443            let (bitmask, pos) = decode_ctx_bit_string(data, pos, 1, what)?;
444            let mut pos = expect_opening(data, pos, 2, what)?;
445            let mut list_of_values = Vec::new();
446            while !next_is_closing(data, pos, 2)? {
447                if list_of_values.len() >= MAX_FRAMED_ITEMS {
448                    return Err(Error::overflow(
449                        pos,
450                        "change-of-bitstring: list-of-bitstring-values exceeds limit",
451                    ));
452                }
453                let (bits, p) = decode_app_bit_string(data, pos, what)?;
454                list_of_values.push(bits);
455                pos = p;
456            }
457            pos = expect_closing(data, pos, 2, what)?;
458            pos = expect_closing(data, pos, 0, what)?;
459            (
460                EP::ChangeOfBitstring {
461                    time_delay,
462                    bitmask,
463                    list_of_values,
464                },
465                pos,
466            )
467        }
468        1 => {
469            let what = "BACnetEventParameter change-of-state";
470            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
471            let mut pos = expect_opening(data, pos, 1, what)?;
472            let mut list_of_values = Vec::new();
473            while !next_is_closing(data, pos, 1)? {
474                if list_of_values.len() >= MAX_FRAMED_ITEMS {
475                    return Err(Error::overflow(
476                        pos,
477                        "change-of-state: list-of-values exceeds limit",
478                    ));
479                }
480                let (state, p) = decode_property_state(data, pos)?;
481                list_of_values.push(state);
482                pos = p;
483            }
484            pos = expect_closing(data, pos, 1, what)?;
485            pos = expect_closing(data, pos, 1, what)?;
486            (
487                EP::ChangeOfState {
488                    time_delay,
489                    list_of_values,
490                },
491                pos,
492            )
493        }
494        2 => {
495            let what = "BACnetEventParameter change-of-value";
496            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
497            let mut pos = expect_opening(data, pos, 1, what)?;
498            let (inner, _) = tags::decode_tag(data, pos)?;
499            let criteria = if inner.is_context(0) {
500                let (bits, p) = decode_ctx_bit_string(data, pos, 0, what)?;
501                pos = p;
502                ChangeOfValueCriteria::Bitmask {
503                    unused_bits: bits.0,
504                    data: bits.1,
505                }
506            } else if inner.is_context(1) {
507                let (increment, p) = decode_ctx_real(data, pos, 1, what)?;
508                pos = p;
509                ChangeOfValueCriteria::ReferencedPropertyIncrement(increment)
510            } else {
511                return Err(DecodeError::new(
512                    DecodingKind::InvalidEncoding,
513                    pos,
514                    "change-of-value: unexpected cov-criteria tag [{}]",
515                )
516                .with_value(inner.number.into())
517                .into());
518            };
519            pos = expect_closing(data, pos, 1, what)?;
520            pos = expect_closing(data, pos, 2, what)?;
521            (
522                EP::ChangeOfValue {
523                    time_delay,
524                    criteria,
525                },
526                pos,
527            )
528        }
529        4 => {
530            let what = "BACnetEventParameter floating-limit";
531            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
532            let pos = expect_opening(data, pos, 1, what)?;
533            let (setpoint_reference, pos) = decode_dopr_body(data, pos, what)?;
534            let pos = expect_closing(data, pos, 1, what)?;
535            let (low_diff_limit, pos) = decode_ctx_real(data, pos, 2, what)?;
536            let (high_diff_limit, pos) = decode_ctx_real(data, pos, 3, what)?;
537            let (deadband, pos) = decode_ctx_real(data, pos, 4, what)?;
538            let pos = expect_closing(data, pos, 4, what)?;
539            (
540                EP::FloatingLimit {
541                    time_delay,
542                    setpoint_reference,
543                    low_diff_limit,
544                    high_diff_limit,
545                    deadband,
546                },
547                pos,
548            )
549        }
550        5 => {
551            let what = "BACnetEventParameter out-of-range";
552            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
553            let (low_limit, pos) = decode_ctx_real(data, pos, 1, what)?;
554            let (high_limit, pos) = decode_ctx_real(data, pos, 2, what)?;
555            let (deadband, pos) = decode_ctx_real(data, pos, 3, what)?;
556            let pos = expect_closing(data, pos, 5, what)?;
557            (
558                EP::OutOfRange {
559                    time_delay,
560                    low_limit,
561                    high_limit,
562                    deadband,
563                },
564                pos,
565            )
566        }
567        9 => {
568            let what = "BACnetEventParameter extended";
569            let (vendor_id, pos) = decode_ctx_unsigned::<u16>(data, pos, 0, what)?;
570            let (extended_event_type, pos) = decode_ctx_unsigned::<u32>(data, pos, 1, what)?;
571            let (raw_params, pos) = decode_ctx_constructed(data, pos, 2, what)?;
572            validate_extended_parameters(raw_params, "extended")?;
573            let pos = expect_closing(data, pos, 9, what)?;
574            (
575                EP::Extended {
576                    vendor_id,
577                    extended_event_type,
578                    parameters: raw_params.to_vec(),
579                },
580                pos,
581            )
582        }
583        n @ (3 | 8 | 10 | 11 | 13..=18 | 21 | 22) => members::decode(data, pos, n)?,
584        n => {
585            // A tag the 2020 production doesn't assign: preserve the SEQUENCE
586            // body verbatim after validating its nested tag structure.
587            let (body, p) = tags::extract_context_value(data, pos, n)?;
588            validate_tlv_sequence(body, "opaque event parameters")?;
589            (
590                EP::Opaque {
591                    tag: n,
592                    data: body.to_vec(),
593                },
594                p,
595            )
596        }
597    };
598    let (value, end) = value;
599    validate_tlv_sequence(&data[offset..end], what)?;
600    Ok((value, end))
601}
602
603mod members;