Skip to main content

epics_base_rs/
error.rs

1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum CaError {
5    #[error("I/O error: {0}")]
6    Io(#[from] std::io::Error),
7
8    #[error("timeout waiting for response")]
9    Timeout,
10
11    #[error("channel not found: {0}")]
12    ChannelNotFound(String),
13
14    #[error("protocol error: {0}")]
15    Protocol(String),
16
17    #[error("unsupported DBR type: {0}")]
18    UnsupportedType(u16),
19
20    #[error("write failed: ECA status {0:#06x}")]
21    WriteFailed(u32),
22
23    #[error("field not found: {0}")]
24    FieldNotFound(String),
25
26    #[error("field is read-only: {0}")]
27    ReadOnlyField(String),
28
29    #[error("type mismatch for field {0}")]
30    TypeMismatch(String),
31
32    #[error("invalid value: {0}")]
33    InvalidValue(String),
34
35    /// C `S_db_badField` ("Illegal RECORD FIELD") — a record's `special()`
36    /// refused the value that `dbPut` had already stored, e.g.
37    /// `calcRecord.c:146-151` returning it for an uncompilable `CALC`. The
38    /// value stays written, the field's monitor is not posted, the record is
39    /// not processed, and the status propagates to the client (rsrv
40    /// `write_action` → `ECA_PUTFAIL`).
41    #[error("illegal record field: {0}")]
42    BadField(String),
43
44    /// C `S_db_badChoice` ("Illegal choice") — a `DBR_STRING` write to a
45    /// `DBF_MENU` field named neither an exact choice label nor an in-range
46    /// index (`dbConvert.c::putStringMenu:1216-1229`). C's converter returns
47    /// this from inside `dbPut`, *before* the value is stored
48    /// (`dbAccess.c:1362`), so the field keeps its old value, no monitor is
49    /// posted and the record is not processed.
50    #[error("illegal menu choice: {0}")]
51    BadChoice(String),
52
53    #[error("put disabled (DISP=1) for field {0}")]
54    PutDisabled(String),
55
56    #[error("link error: {0}")]
57    LinkError(String),
58
59    #[error("DB parse error at line {line}, column {column}: {message}")]
60    DbParseError {
61        line: usize,
62        column: usize,
63        message: String,
64    },
65
66    #[error("calc error: {0}")]
67    CalcError(String),
68
69    #[error("channel disconnected")]
70    Disconnected,
71
72    #[error("client shut down")]
73    Shutdown,
74
75    /// CA WRITE_NOTIFY arrived while a previous async put on the same
76    /// record is still in flight. Mirrors C EPICS `S_db_Blocked`; the
77    /// CA server replies `ECA_PUTCBINPROG`.
78    #[error("put callback in progress for record {0}")]
79    PutCallbackInProgress(String),
80
81    /// Server-emitted ECA status carried out-of-band on an otherwise
82    /// data-shaped frame — used by libca `cac::eventAddRespAction`
83    /// (`cac.cpp:973-977`) when a monitor frame's `m_cid` is non-
84    /// NORMAL (e.g. `ECA_NORDACCESS` from `no_read_access_event`
85    /// after an ACF reload). Routed to the per-subscription
86    /// callback as `Err(CaError::ServerError(eca_status))` so the
87    /// subscriber surfaces the status instead of seeing the bogus
88    /// zeroed payload that travels with the frame.
89    #[error("server reported ECA status {0:#06x}")]
90    ServerError(u32),
91
92    /// Request cannot be framed for the peer: it needs the extended
93    /// (24-byte) CA header, and the peer's protocol version predates
94    /// CA_V49, or the element count exceeds what the peer can carry.
95    /// libca raises this locally — `comQueSend::insertRequestHeader`
96    /// throws `cacChannel::outOfBounds()` (`comQueSend.cpp:299,313`)
97    /// and `ca_array_get`/`ca_array_put` return `ECA_TOLARGE` — so no
98    /// byte reaches the wire.
99    #[error("request too large for the peer's CA protocol version (ECA_TOLARGE)")]
100    TooLarge,
101
102    /// Element count out of bounds for the request C would build. libca's
103    /// put path throws `cacChannel::outOfBounds()` for an array that cannot
104    /// fit the peer's message-body limit (`comQueSend.cpp:361`) or that
105    /// needs an extended header the peer cannot parse
106    /// (`comQueSend.cpp:313`); `oldChannelNotify.cpp:309,378,453` map that
107    /// to `ECA_BADCOUNT`. Raised locally — no byte reaches the wire.
108    #[error("element count out of bounds for this CA circuit (ECA_BADCOUNT)")]
109    BadCount,
110}
111
112// ECA status constants (originally from protocol.rs, now in epics-ca-rs)
113const ECA_TIMEOUT: u32 = 80; // defmsg(CA_K_WARNING, 10)
114const ECA_NOWTACCESS: u32 = 376; // defmsg(CA_K_WARNING, 47)
115const ECA_PUTFAIL: u32 = 160; // defmsg(CA_K_WARNING, 20)
116const ECA_BADTYPE: u32 = 114; // defmsg(CA_K_ERROR, 14)
117const ECA_DISCONN: u32 = 192; // defmsg(CA_K_WARNING, 24)
118const ECA_PUTCBINPROG: u32 = 362; // defmsg(CA_K_ERROR, 45) = (45 << 3) | 2
119const ECA_TOLARGE: u32 = 72; // defmsg(CA_K_WARNING, 9)
120const ECA_BADCOUNT: u32 = 176; // defmsg(CA_K_WARNING, 22)
121
122impl CaError {
123    pub fn to_eca_status(&self) -> u32 {
124        match self {
125            CaError::Timeout => ECA_TIMEOUT,
126            CaError::ReadOnlyField(_) => ECA_NOWTACCESS,
127            CaError::PutDisabled(_) => ECA_PUTFAIL,
128            CaError::TypeMismatch(_) => ECA_BADTYPE,
129            CaError::UnsupportedType(_) => ECA_BADTYPE,
130            CaError::InvalidValue(_) => ECA_BADTYPE,
131            CaError::FieldNotFound(_) => ECA_PUTFAIL,
132            // C rsrv answers any non-zero `db_put_field` status — including
133            // `S_db_badField` — with ECA_PUTFAIL (`camessage.c::write_action`).
134            CaError::BadField(_) => ECA_PUTFAIL,
135            // Likewise `S_db_badChoice` from the string→menu converter: the
136            // put-notify path maps any non-`notifyOK` status to ECA_PUTFAIL
137            // (`db_access.c::db_put_process:1041`, `camessage.c:1386`).
138            CaError::BadChoice(_) => ECA_PUTFAIL,
139            // Disconnection / shutdown are surfaced as ECA_DISCONN so a
140            // downstream client (e.g. caput on a CA gateway whose
141            // upstream just dropped) sees the actionable
142            // "CA channel disconnected" message rather than the
143            // catch-all "Put fail".
144            CaError::Disconnected | CaError::Shutdown => ECA_DISCONN,
145            // I/O errors usually mean the upstream connection is
146            // wedged; mapping to ECA_DISCONN matches operator
147            // expectations from C ca-gateway.
148            CaError::Io(_) => ECA_DISCONN,
149            CaError::WriteFailed(code) => *code,
150            CaError::PutCallbackInProgress(_) => ECA_PUTCBINPROG,
151            CaError::ServerError(code) => *code,
152            CaError::TooLarge => ECA_TOLARGE,
153            CaError::BadCount => ECA_BADCOUNT,
154            _ => ECA_PUTFAIL,
155        }
156    }
157}
158
159pub type CaResult<T> = Result<T, CaError>;