bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! Passing a Channel object's Present_Value on to its members (Clause 12.53,
//! #1151, #1264).
//!
//! Each member's Execution_Delay is measured from the moment the
//! distribution starts, so the delays overlap rather than add up
//! (Clause 12.53.12), and each member is written as soon as its own time is
//! reached, whatever the members before it are doing (#1343). Members in this
//! device are written one at a time, in order of delay and list order among
//! equal delays, so their writes keep that order. A member in another device
//! is its own future, so a write there that waits for its answer holds back
//! no member here or in another device. Both kinds are polled together, so a
//! member's future is never left unpolled while another member's write waits
//! for the database. Requests in other devices wait their turn in the
//! server's queues (`remote_slots`): one at a time per device and a bounded
//! number across the server, so a member due while its device is answering
//! another request is written once that one ends.
//!
//! A device that answers none of a request's attempts, or none of the Who-Is
//! sent to find it when it had no binding (#1322), is taken to be offline for
//! the rest of the distribution: its members whose requests come up after
//! that, their turn in the queue included, fail at once as communication
//! failures, with nothing sent. A distribution so waits out one request's
//! retries per silent device.
//!
//! For a member in this device the runner looks up the datatype of the
//! property's current value, coerces the channel value to it (Table 12-63)
//! and writes the result through the host's local write path at the priority
//! the Present_Value write carried, or none if it carried none. A member
//! naming another Device goes out as a confirmed WriteProperty through
//! [`RunHost::write_remote`] (Clause 12.53.11 leaves the method open). Its
//! datatype is learned first (#1342): a ReadProperty of the member's property
//! in that device, sent when the distribution starts so it overlaps the
//! member's delay, and the value is coerced to the datatype of what it
//! returns, as for a member here. A primitive datatype learned is kept on the
//! Channel for later distributions until the member is replaced, or until a
//! write made with it is refused as a configuration fault (an invalid
//! datatype, say), so a property whose datatype can change is read again. No
//! read is sent when the coercion can't depend on the answer: for a NULL, a
//! constructed value (a lighting command, an xy colour or a colour command),
//! or a member whose property fixes its datatype (a Lighting_Command, a
//! Color_Command, or a Color object's xy colour, #1474). A read that gets
//! no answer finds the device silent, since every device executes
//! ReadProperty: the member fails as a communication failure with no write
//! sent. A read that is refused, or returns NULL, a constructed value or an
//! answer that doesn't fit, leaves the value as written, and the device
//! itself refuses a datatype its property doesn't take; nothing is kept, so
//! the next distribution reads again.
//!
//! A coercion failure means that member isn't written, and counts as a
//! configuration failure: the member's datatype doesn't fit the value. A
//! refused or unanswered write counts as a failure too (sorted in `target`),
//! with one exception: a NULL refused as an invalid datatype, by an Error or
//! a Reject, so one channel can relinquish its commandable members while its
//! other members ignore the NULL (Clause 12.53.7). One failure doesn't stop
//! the rest (Clause 12.53.5.8); once every member has been tried, the
//! Channel's Write_Status becomes SUCCESSFUL or FAILED and its Reliability
//! takes the kind of the first failure to finish (Clause 12.53.9).

use std::pin::pin;
use std::sync::Mutex;
use std::time::Duration;

use bacnet_objects::channel::{
    ChannelDistribution, ChannelMember, MemberDatatype, coerce_channel_value,
};
use bacnet_objects::command::{CommandRun, WriteFailure};
use bacnet_objects::database::ObjectDatabase;
use bacnet_types::constructed::BACnetActionCommand;
use bacnet_types::enums::PropertyIdentifier;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use futures_util::StreamExt;
use futures_util::stream::{self, FuturesUnordered};
use tokio::time::Instant;
use tracing::debug;

use super::remote_slots::RemoteSlot;
use super::target::{self, Failed};
use super::{Owner, RunHost};
use crate::server::RemoteRequestError;

/// What a distribution's members share while they are written.
struct Shared {
    start: Instant,
    /// Devices that answered none of a request's attempts, or its Who-Is, in
    /// this distribution. Never held across an await.
    silent: Mutex<Vec<ObjectIdentifier>>,
}

impl Shared {
    fn is_silent(&self, device: ObjectIdentifier) -> bool {
        self.silent
            .lock()
            .is_ok_and(|silent| silent.contains(&device))
    }

    fn found_silent(&self, device: ObjectIdentifier) {
        if let Ok(mut silent) = self.silent.lock() {
            silent.push(device);
        }
    }

    fn due(&self, member: &ChannelMember) -> Instant {
        self.start + Duration::from_millis(member.delay_ms.into())
    }
}

/// Write each member once its delay is up: `Ok` if all succeeded, otherwise
/// the first failure to finish, or `None` once the run is stale.
pub(super) async fn distribute<H: RunHost>(
    host: &H,
    run: &CommandRun,
    distribution: &ChannelDistribution,
    owner: &mut Owner<'_, H>,
) -> Option<Result<(), WriteFailure>> {
    let shared = Shared {
        start: Instant::now(),
        silent: Mutex::new(Vec::new()),
    };
    let (mut local, remote): (Vec<&ChannelMember>, Vec<(&ChannelMember, ObjectIdentifier)>) = {
        let db = host.database().read().await;
        if !current(&db, run) {
            return None;
        }
        let mut local = Vec::new();
        let mut remote = Vec::new();
        for member in &distribution.members {
            // A member naming this Device is local, as one naming none is.
            match member.reference.device_identifier {
                Some(device) if !db.local_device().is_local(Some(device)) => {
                    remote.push((member, device));
                }
                _ => local.push(member),
            }
        }
        (local, remote)
    };
    local.sort_by_key(|member| member.delay_ms);
    let shared = &shared;
    // The members here, one at a time, polled beside the members in other
    // devices on every wake. A future left unpolled while another is awaited
    // can still be granted a database guard it is queued for, and then holds
    // it until it is polled again; were the members here awaited on their
    // own, the one being written could wait on that guard for good, and with
    // it every other user of the database.
    let local = stream::unfold(local.into_iter(), |mut members| async move {
        let member = members.next()?;
        tokio::time::sleep_until(shared.due(member)).await;
        let written = write_member(host, run, distribution, member, None, shared).await;
        Some((written, members))
    });
    // Fused: the loop polls it again after it has ended.
    let mut local = pin!(local.fuse());
    let mut remote: FuturesUnordered<_> = remote
        .into_iter()
        .map(|(member, device)| write_remote(host, run, distribution, member, device, shared))
        .collect();
    let mut outcome = Ok(());
    let mut made = 0;
    loop {
        let finished = tokio::select! {
            biased;
            Some(finished) = remote.next() => finished,
            Some(finished) = local.next() => finished,
            else => break,
        };
        // The first failure to finish stands.
        outcome = outcome.and(finished?);
        made += 1;
        owner.progress(made, outcome);
    }
    Some(outcome)
}

/// Whether `run` is still its object's current run.
fn current(db: &ObjectDatabase, run: &CommandRun) -> bool {
    db.get(&run.source)
        .and_then(|object| object.command_generation_internal())
        == Some(run.generation)
}

/// Learn the datatype of `member`, in `device`, then write it once its delay
/// is up.
async fn write_remote<H: RunHost>(
    host: &H,
    run: &CommandRun,
    distribution: &ChannelDistribution,
    member: &ChannelMember,
    device: ObjectIdentifier,
    shared: &Shared,
) -> Option<Result<(), WriteFailure>> {
    let datatype = match member.learned {
        Some(learned) => learned,
        None => learn(host, run, distribution, member, device, shared).await,
    };
    tokio::time::sleep_until(shared.due(member)).await;
    write_member(
        host,
        run,
        distribution,
        member,
        Some((device, datatype)),
        shared,
    )
    .await
}

/// Room for one request in `device`, from the host's queues; `None` from a
/// host with no network, whose request fails unsent anyway.
async fn slot<H: RunHost>(host: &H, device: ObjectIdentifier) -> Option<RemoteSlot<'_>> {
    match host.remote_slots() {
        Some(slots) => Some(slots.acquire(device).await),
        None => None,
    }
}

/// The datatype `member`'s property in `device` holds, read there when the
/// coercion depends on it and kept on the Channel when it's a primitive one.
/// Anything short of that is [`MemberDatatype::Unknown`], or the constructed
/// datatype the property's identifier fixes (Lighting_Command,
/// Color_Command, a Color object's xy colours), and the value goes as
/// written; a read that gets no answer marks the device silent, so the
/// member's write is never sent.
async fn learn<H: RunHost>(
    host: &H,
    run: &CommandRun,
    distribution: &ChannelDistribution,
    member: &ChannelMember,
    device: ObjectIdentifier,
    shared: &Shared,
) -> MemberDatatype {
    let reference = &member.reference;
    let property = PropertyIdentifier::from_raw(reference.property_identifier);
    let object_type = reference.object_identifier.object_type();
    let unread = MemberDatatype::of(object_type, property, None);
    // NULL passes to every primitive datatype, a constructed value goes only
    // to a property of its own datatype, and such a property takes nothing
    // else, so none of them needs a read.
    let decides = !matches!(
        distribution.value,
        PropertyValue::Null | PropertyValue::ApplicationData(_)
    ) && unread == MemberDatatype::Unknown;
    if !decides {
        return unread;
    }
    let read = {
        let _slot = slot(host, device).await;
        // The run may have gone stale, or the device silent, while this read
        // waited its turn: then nothing is read, and the member's write finds
        // the run stale or the device silent.
        let current = current(&*host.database().read().await, run);
        if !current || shared.is_silent(device) {
            return unread;
        }
        host.read_remote(device, reference).await
    };
    let datatype = match read {
        Ok(value) => MemberDatatype::of(object_type, property, Some(&value)),
        Err(error @ (RemoteRequestError::Unanswered | RemoteRequestError::Undiscovered)) => {
            debug!(
                channel = %run.source,
                %device,
                target = %reference.object_identifier,
                ?property,
                %error,
                "Channel member's device answered no datatype read; it counts as silent"
            );
            shared.found_silent(device);
            return unread;
        }
        Err(error) => {
            debug!(
                channel = %run.source,
                %device,
                target = %reference.object_identifier,
                ?property,
                %error,
                "Channel member's datatype not learned; its value goes as written"
            );
            return unread;
        }
    };
    if datatype != MemberDatatype::Unknown {
        remember(host, run, member, Some(datatype)).await;
    }
    datatype
}

/// Keep `datatype` on the Channel for `member`, or forget what it had with
/// `None`.
async fn remember<H: RunHost>(
    host: &H,
    run: &CommandRun,
    member: &ChannelMember,
    datatype: Option<MemberDatatype>,
) {
    let mut db = host.database().write().await;
    if let Some(channel) = db.get_mut(&run.source) {
        channel.remember_member_datatype_internal(member.slot, &member.reference, datatype);
    }
}

/// Write one member: `Ok` if that counts as a success, otherwise how it
/// failed. `None` once the run is stale. `remote` names the member's device
/// and datatype when it is in another device; a member here takes the
/// datatype of the value its property holds now.
async fn write_member<H: RunHost>(
    host: &H,
    run: &CommandRun,
    distribution: &ChannelDistribution,
    member: &ChannelMember,
    remote: Option<(ObjectIdentifier, MemberDatatype)>,
    shared: &Shared,
) -> Option<Result<(), WriteFailure>> {
    let reference = &member.reference;
    let property = PropertyIdentifier::from_raw(reference.property_identifier);
    let (device, datatype) = {
        let db = host.database().read().await;
        if !current(&db, run) {
            return None;
        }
        match remote {
            Some((device, datatype)) => (Some(device), datatype),
            None => {
                let value = db.get(&reference.object_identifier).and_then(|object| {
                    object
                        .read_property(property, reference.property_array_index)
                        .ok()
                });
                let object_type = reference.object_identifier.object_type();
                (
                    None,
                    MemberDatatype::of(object_type, property, value.as_ref()),
                )
            }
        }
    };
    // A member in another device waits its turn there first, and the device
    // may go silent meanwhile.
    let _slot = match device {
        Some(device) => slot(host, device).await,
        None => None,
    };
    if let Some(device) = device.filter(|device| shared.is_silent(*device)) {
        debug!(
            channel = %run.source,
            %device,
            target = %reference.object_identifier,
            ?property,
            "Channel member not sent: its device answered nothing earlier in this distribution"
        );
        return Some(Err(WriteFailure::Communication));
    }
    let Ok(value) = coerce_channel_value(&distribution.value, datatype) else {
        debug!(
            channel = %run.source,
            device = ?device,
            target = %reference.object_identifier,
            ?property,
            ?datatype,
            "Channel value can't be coerced to the member's datatype"
        );
        return Some(Err(WriteFailure::Configuration));
    };
    let command = BACnetActionCommand {
        device_identifier: device,
        object_identifier: reference.object_identifier,
        property_identifier: property,
        property_array_index: reference.property_array_index,
        property_value: value,
        priority: distribution.priority,
        post_delay: None,
        quit_on_failure: false,
        write_successful: false,
    };
    Some(match target::write(host, run, device, &command).await {
        Ok(()) => Ok(()),
        Err(Failed {
            answer: Some(answer),
            ..
        }) if command.property_value == PropertyValue::Null
            && target::refuses_datatype(&answer) =>
        {
            Ok(())
        }
        Err(failed) => {
            if let Some(device) = device.filter(|_| failed.unanswered) {
                shared.found_silent(device);
            }
            // The device refused the coerced value as a configuration fault:
            // the datatype kept for it may be wrong now, so the next
            // distribution reads it again.
            let refused = failed.answer.is_some() && failed.failure == WriteFailure::Configuration;
            let kept = remote.is_some_and(|(_, datatype)| {
                !matches!(
                    datatype,
                    MemberDatatype::Unknown
                        | MemberDatatype::LightingCommand
                        | MemberDatatype::ColorCommand
                        | MemberDatatype::XyColor
                )
            });
            if refused && kept {
                remember(host, run, member, None).await;
            }
            Err(failed.failure)
        }
    })
}