Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9451b98
feat(backend, prtotocol): update protocol
JavierRibaldelRio Oct 9, 2026
dba85ba
feat(backend, protocol): main structure
JavierRibaldelRio Oct 9, 2026
bb6f5a5
Feat(backend, protocol): Implemented Order Encoding.
Oct 10, 2026
9a097fd
Feat(backend, protocol): Changed byte encoding in Order.rs to Little-…
Oct 10, 2026
553e3dd
feat(backend, protocol): function that returns the number of bytes th…
JavierRibaldelRio Oct 11, 2026
0edcd3a
fix(backend, adj): add methods to ADJID to separate get messurement …
JavierRibaldelRio Oct 11, 2026
d5e7e4b
Feat(backend, protocol): Removed todos and left as comments. Implemen…
Oct 12, 2026
144b31b
Merge branch 'backend/protocol' of https://github.com/Hyperloop-UPV/s…
Oct 12, 2026
e8180bd
Feat(backend, protocol): used new function to retrieve the width of a…
Oct 12, 2026
9670fae
Feat(backend, protocol): Removed todos and left as comments. Implemen…
Oct 12, 2026
abf0e44
Feat(backend, protocol): used new function to retrieve the width of a…
Oct 12, 2026
c3b2939
Feat(backend, protocol): Refactored to merge data.rs and order.rs to …
Oct 12, 2026
d9d2b4d
Feat(backend, protocol): Created tests for data_order.rs.
Oct 12, 2026
71258e5
Feat(backend, protocol): Created ReadMe for the protocol directory.
Oct 12, 2026
60dfd9c
Feat(backend, protocol): Refactored to merge data.rs and order.rs to …
Oct 12, 2026
e658715
Feat(backend, protocol): Created tests for data_order.rs.
Oct 12, 2026
8218e74
Feat(backend, protocol): Created ReadMe for the protocol directory.
Oct 12, 2026
b615ec8
fix(backend, model): use phantom type to diferenciate betwen orders a…
JavierRibaldelRio Oct 12, 2026
fc7216b
fix(backend, protocol): changes due to unification of data_order
JavierRibaldelRio Oct 12, 2026
45642f8
fix(backend, protocol): implement partial eq manually for decode err…
JavierRibaldelRio Oct 12, 2026
7f63644
docs(backend, protocol): update docs
JavierRibaldelRio Oct 12, 2026
5a99aa4
fix(backend, protocol): format
JavierRibaldelRio Oct 12, 2026
3e13e09
fix(backend, protocol): remove duplicated data_order exportation
JavierRibaldelRio Oct 12, 2026
618e696
fix(backend, model): include kind
JavierRibaldelRio Oct 12, 2026
d47baea
feat(backend, protocol): implement boolean
JavierRibaldelRio Oct 12, 2026
87c0159
fix(backend, protocol): solve issues and add comments
JavierRibaldelRio Oct 12, 2026
cbcbfd9
Merge branch 'backend/protocol' into backend/protocol-Javier-Ribal
JavierRibaldelRio Oct 12, 2026
1a2127f
feat(backend, protocol): protection
JavierRibaldelRio Oct 12, 2026
7e632fb
feat(backend, protection): optional id
JavierRibaldelRio Oct 12, 2026
5719787
fix(backend, adj-model): read messages id as a proper package
JavierRibaldelRio Oct 12, 2026
e3aa05e
fix(backend, adj, model): define messages packets in the same way as …
JavierRibaldelRio Oct 12, 2026
cfa789c
feat(backend, protocol): allow index to index packets
JavierRibaldelRio Oct 12, 2026
cc65fbe
feat(backend, protocol): messages decode & encode
JavierRibaldelRio Oct 12, 2026
2a11b67
fix(backend, protocol): remove duplicated function
JavierRibaldelRio Oct 12, 2026
80d34b3
feat(backend, protocol): tcp decode
JavierRibaldelRio Oct 12, 2026
8d7b6b5
refactor(backend): change PacketKind for Pakcet .type
JavierRibaldelRio Oct 13, 2026
c88661d
Merge pull request #659 from Hyperloop-UPV/backend/protocol-Javier-Ribal
JavierRibaldelRio Oct 14, 2026
3c7e77e
Merge branch 'new-backend' into backend/protocol
JavierRibaldelRio Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions backend/crates/core/src/adj/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,57 @@ Adj
│ └── boards: HashMap<BoardId, Board>
│ ├── measurements: HashMap<String, Measurement>
│ └── packets: HashMap<AdjId, PacketDef>
│ ├── explicit (Data, Order, Message — declared in packets.json)
│ ├── explicit (Data, Order, Message — declared in packets.json;
│ │ a "message"-type packet's own `name` must be one of
│ │ MessageLevel::ALL's four severities, resolved via
│ │ MessageLevel::from_name — UnknownMessageSeverity if not)
│ └── synthetic (Protection — one per declared protection,
│ derived from each measurement's own `protections`;
│ never declared directly in packets.json)
└── info (AdjInfo)
├── ports: HashMap<String, Port>
├── addresses: HashMap<String, IpAddr>
├── message_ids: HashMap<String, AdjId>
├── message_ids: HashMap<String, AdjId> -- name -> id, straight from the ADJ
├── message_levels: HashMap<AdjId, MessageLevel> -- id -> severity, the reverse
└── protection_types: HashMap<String, ProtectionTypeInfo>
```

`message_ids` is the ADJ's own direction: a name (e.g. `"fault"`) to its
id. Not every entry is one of `model::MessageLevel`'s four severities —
other reserved names (e.g. `add_state_order`) share this same map.

`message_levels` is the direction decoding actually needs: a wire id
(all a decoder ever has) straight to its severity, with no name in
between. It's the reverse of `message_ids`, restricted to whichever of
`MessageLevel::ALL`'s four names `message_ids` declares — built once in
`adj::raw::types`'s `TryFrom<RawGeneralInfo> for AdjInfo`, right after
`message_ids` itself, so it can never drift out of sync with it:

```rust
let message_levels: HashMap<AdjId, MessageLevel> = MessageLevel::ALL
.into_iter()
.filter_map(|level| message_ids.get(level.name()).map(|&id| (id, level)))
.collect();
```

Which ADJ name corresponds to which `MessageLevel` is **not** decided
here — `level.name()`/`MessageLevel::from_name` (see `model/README.md`
§4.4) is the one place that association is spelled out, so `adj`
doesn't hardcode `"fault"`/`"warning"`/`"info"`/`"panic"` itself.

**One layer up, `protocol::Index` is what actually turns
`message_levels` into something a decoder can look up a packet by.**
`Index::new(pod_data, message_levels)` takes `message_levels` as
`Option<&HashMap<AdjId, MessageLevel>>` — `None` for callers with no
`AdjInfo` at all (most tests in this crate, which only build a bare
`PodData`) — and, for each entry, synthesizes a
`PacketDef { packet_type: PacketType::Message(level), board: BoardId(0), .. }`
into the very same `AdjId -> PacketDef` table every other packet lives
in. `BoardId(0)` (reserved, not validated — see `AdjId`/`BoardId`'s own
docs) because a severity isn't owned by any one board — any board's
free-text log frame can reuse the same `"fault"` id. See
`protocol/README.md`'s `index.rs` section for the full picture.

---

## Why It Is Structured This Way
Expand All @@ -49,4 +89,4 @@ The `Adj` object enforces a clear architectural boundary between **vehicle topol
The raw deserialization types and the loader logic live isolated inside `raw`[cite: 2]. This enables non-station tools (such as bus sniffers, test runners, or simulators) to parse and ingest ADJ directories without dragging along application-level UI or station state logic.

### 3. Unified Packet Map (`packets`)
Protections defined under measurements are materialized during loading as first-class `PacketKind::Protection(Measurement)` packets alongside the explicit data, order and message packets declared in `packets.json`. Downstream networking layers can route and handle every wire event uniformly via `Board::packets` without needing specialized traversal logic for nested measurement alarms. `packets.json` itself never declares a `"type": "protection"` entry directly — that shape only exists synthesized, one per protection a measurement declares, never as a free-standing declaration with its own arbitrary variable list.
Protections defined under measurements are materialized during loading as first-class `PacketType::Protection(Measurement)` packets alongside the explicit data, order and message packets declared in `packets.json`. Downstream networking layers can route and handle every wire event uniformly via `Board::packets` without needing specialized traversal logic for nested measurement alarms. `packets.json` itself never declares a `"type": "protection"` entry directly — that shape only exists synthesized, one per protection a measurement declares, never as a free-standing declaration with its own arbitrary variable list.
10 changes: 10 additions & 0 deletions backend/crates/core/src/adj/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ pub enum LoadError {
type_name: String,
},

/// A `"type": "message"` packet's `name` isn't one of
/// [`crate::model::MessageLevel`]'s four severities.
#[error("board {board:?}, packet {name:?}: not a known message severity")]
UnknownMessageSeverity {
/// The board declaring this message packet.
board: String,
/// The packet's name — the text that failed to match a severity.
name: String,
},

/// A packet or order references a measurement alias that doesn't exist
/// among that board's own measurements.
#[error("board {board:?}, packet {name:?}: unknown measurement alias {alias:?}")]
Expand Down
20 changes: 19 additions & 1 deletion backend/crates/core/src/adj/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! addresses, message-id conventions, and how to display a triggered
//! protection.

use crate::model::{AdjId, Port};
use crate::model::{AdjId, MessageLevel, Port};
use std::collections::HashMap;
use std::net::IpAddr;

Expand Down Expand Up @@ -34,7 +34,25 @@ pub struct AdjInfo {
/// Known message ids, by name (e.g. `"fault"` -> `AdjId(2)`) — these
/// live in the ADJ's own special reserved id range (`[0, 511]`, see
/// [`AdjId`]'s own docs), not a separate numbering of their own.
/// This is the ADJ's own direction: a severity's *name* to its id.
/// Not every entry here is one of [`MessageLevel`]'s four severities
/// — other reserved names (e.g. `add_state_order`) share this same
/// map — see [`MessageLevel::name`] for which ones are.
pub message_ids: HashMap<String, AdjId>,
/// The reverse of [`AdjInfo::message_ids`], and only for the four
/// entries that actually are a [`MessageLevel`]: a wire id straight
/// to its severity.
///
/// Decoding a free-text log frame only ever has the raw id off the
/// wire, not its name — without this, every decoder would have to
/// search `message_ids` by value (or re-derive this same reverse
/// map itself) just to answer "which severity is id 2?". Built once
/// here, when the ADJ is loaded (see `adj::raw::types`'s
/// `TryFrom<RawGeneralInfo> for AdjInfo`),
/// from `message_ids` and [`MessageLevel::ALL`]/[`MessageLevel::name`]
/// — never constructed by hand, and never out of sync with
/// `message_ids` since it's derived from it.
pub message_levels: HashMap<AdjId, MessageLevel>,
/// Declared protection types, by name (e.g. `"Range"`).
pub protection_types: HashMap<String, ProtectionTypeInfo>,
}
74 changes: 57 additions & 17 deletions backend/crates/core/src/adj/raw/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use super::types::{RawBoard, RawGeneralInfo, RawMeasurement, RawPacket};
use crate::adj::error::LoadError;
use crate::adj::{Adj, AdjInfo};
use crate::model::{
AdjId, Board, BoardId, MacAddress, Measurement, PacketDef, PacketKind, PodData,
AdjId, Board, BoardId, MacAddress, Measurement, MessageLevel, PacketDef, PacketType, PodData,
};
use serde::de::DeserializeOwned;
use std::collections::HashMap;
Expand Down Expand Up @@ -71,15 +71,18 @@ pub(crate) fn load_board(root: &Path, name: &str, rel_path: &str) -> Result<Boar
})?;

for (idx, _protection) in measurement.protections.iter().enumerate() {
// protection number into bits 13-15, and the measurement ID into bits 0-12.
let pos = idx as u16 + 1; //Change to u16 to match ADJ ID size. We start at one because 0 is the measurement id not a protection by ADJ definition.
let protection_packet_id = (pos << 13) | measurement.id.0; // Push Protection number 13 bits into specified region of the ID. Perform OR with the ID.
// The compound id's high 3 bits/low 13 bits split — see
// `AdjId::protection_position`/`AdjId::measurement_id` for
// the inverse of this composition. `pos` starts at 1, not
// 0: that's reserved for a normal, non-protection id.
let pos = idx as u16 + 1;
let protection_packet_id = (pos << 13) | measurement.id.0;

let packet = PacketDef {
id: AdjId(protection_packet_id),
board: board_id,
name: format!("{}_{}_protection_{}", name, measurement.alias, idx + 1),
kind: PacketKind::Protection(measurement.clone()),
packet_type: PacketType::Protection(measurement.clone()),
};
packets.insert(packet.id, packet);
}
Expand Down Expand Up @@ -127,20 +130,29 @@ fn build_packet_def(
raw: RawPacket,
measurements: &HashMap<String, Measurement>,
) -> Result<PacketDef, LoadError> {
let kind = match raw.kind.as_str() {
"data" => PacketKind::Data(resolve_measurements(
let packet_type = match raw.packet_type.as_str() {
"data" => PacketType::Data(resolve_measurements(
board,
&raw.name,
&raw.variables,
measurements,
)?),
"order" => PacketKind::Order(resolve_measurements(
"order" => PacketType::Order(resolve_measurements(
board,
&raw.name,
&raw.variables,
measurements,
)?),
"message" => PacketKind::Message,
// The packet's own `name` must be one of `MessageLevel`'s four
// severities (`"fault"`/`"warning"`/`"info"`/`"panic"`) — that's
// the only way a `"message"`-type packet in `packets.json` says
// which severity it is; nothing else in its JSON shape does.
"message" => PacketType::Message(MessageLevel::from_name(&raw.name).ok_or_else(|| {
LoadError::UnknownMessageSeverity {
board: board.to_string(),
name: raw.name.clone(),
}
})?),
other => {
return Err(LoadError::UnknownPacketType {
board: board.to_string(),
Expand All @@ -154,7 +166,7 @@ fn build_packet_def(
id: AdjId(raw.id),
board: board_id,
name: raw.name,
kind,
packet_type,
})
}

Expand Down Expand Up @@ -205,7 +217,7 @@ fn parse_mac(raw: &str) -> Option<MacAddress> {
mod tests {
use super::*;
use crate::adj::ProtectionTypeInfo;
use crate::model::{PacketKind, Port, ProtectionKind, Severity};
use crate::model::{PacketType, Port, ProtectionKind, Severity};
use std::path::Path;
use std::time::Duration;

Expand Down Expand Up @@ -237,7 +249,7 @@ mod tests {
fn build_packet_def_rejects_unknown_packet_type() {
let raw = RawPacket {
id: 1,
kind: "status".to_string(),
packet_type: "status".to_string(),
name: "Status".to_string(),
variables: vec![],
};
Expand All @@ -251,7 +263,7 @@ mod tests {
fn build_packet_def_rejects_unknown_alias() {
let raw = RawPacket {
id: 1,
kind: "data".to_string(),
packet_type: "data".to_string(),
name: "Status".to_string(),
variables: vec!["missing".to_string()],
};
Expand All @@ -261,6 +273,34 @@ mod tests {
));
}

#[test]
fn build_packet_def_resolves_a_message_packets_severity_from_its_name() {
let raw = RawPacket {
id: 2,
packet_type: "message".to_string(),
name: "fault".to_string(),
variables: vec![],
};
let Ok(packet) = build_packet_def("VCU", BoardId(3), raw, &HashMap::new()) else {
unreachable!("\"fault\" is a known message severity");
};
assert_eq!(packet.packet_type, PacketType::Message(MessageLevel::Fault));
}

#[test]
fn build_packet_def_rejects_a_message_whose_name_isnt_a_severity() {
let raw = RawPacket {
id: 2,
packet_type: "message".to_string(),
name: "not_a_severity".to_string(),
variables: vec![],
};
assert!(matches!(
build_packet_def("VCU", BoardId(3), raw, &HashMap::new()),
Err(LoadError::UnknownMessageSeverity { .. })
));
}

#[test]
fn loads_the_sample_adj_fixture() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/adj/test/adj");
Expand All @@ -286,14 +326,14 @@ mod tests {
let Some(order) = vcu.packets.get(&AdjId(600)) else {
unreachable!("VCU should have order 600");
};
let PacketKind::Order(_) = &order.kind else {
let PacketType::Order(_) = &order.packet_type else {
unreachable!("order 600 should be an Order");
};
assert_eq!(order.board, BoardId(3));
let Some(data) = vcu.packets.get(&AdjId(625)) else {
unreachable!("VCU should have data packet 625");
};
let PacketKind::Data(_) = &data.kind else {
let PacketType::Data(_) = &data.packet_type else {
unreachable!("data packet 625 should be Data");
};

Expand All @@ -303,7 +343,7 @@ mod tests {
let Some(protected) = pcu
.packets
.values()
.flat_map(|p| p.kind.measurements())
.flat_map(|p| p.packet_type.measurements())
.find(|m| !m.protections.is_empty())
else {
unreachable!("PCU should have a measurement with a protection");
Expand Down Expand Up @@ -335,7 +375,7 @@ mod tests {
let Some(protection_packet) = pcu.packets.get(&AdjId((1u16 << 13) | protected.id.0)) else {
unreachable!("PCU should have a synthetic protection packet");
};
let PacketKind::Protection(measurement) = &protection_packet.kind else {
let PacketType::Protection(measurement) = &protection_packet.packet_type else {
unreachable!("protection_packet should be a Protection");
};
assert_eq!(measurement, protected);
Expand Down
44 changes: 34 additions & 10 deletions backend/crates/core/src/adj/raw/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
use crate::adj::error::{LoadError, MeasurementError, ProtectionError};
use crate::adj::info::{AdjInfo, ProtectionTypeInfo};
use crate::model::{
AdjId, Measurement, MeasurementKind, NumericKind, Port, Protection, ProtectionKind, Severity,
AdjId, Measurement, MeasurementKind, MessageLevel, NumericKind, Port, Protection,
ProtectionKind, Severity,
};
use serde::Deserialize;
use std::collections::HashMap;
Expand Down Expand Up @@ -78,7 +79,7 @@ fn default_fault() -> bool {
pub(crate) struct RawPacket {
pub(crate) id: u16,
#[serde(rename = "type")]
pub(crate) kind: String,
pub(crate) packet_type: String,
pub(crate) name: String,
#[serde(default)]
pub(crate) variables: Vec<String>,
Expand All @@ -100,18 +101,30 @@ impl TryFrom<RawGeneralInfo> for AdjInfo {
addresses.insert(name, addr);
}

let message_ids: HashMap<String, AdjId> = raw
.message_ids
.into_iter()
.map(|(name, id)| (name, AdjId(id)))
.collect();

// The reverse of `message_ids`, restricted to the four names
// that are actually a `MessageLevel` — see `AdjInfo::message_levels`'s
// own doc comment for why this is worth precomputing here rather
// than leaving every caller to search `message_ids` by value.
let message_levels: HashMap<AdjId, MessageLevel> = MessageLevel::ALL
.into_iter()
.filter_map(|level| message_ids.get(level.name()).map(|&id| (id, level)))
.collect();

Ok(AdjInfo {
ports: raw
.ports
.into_iter()
.map(|(name, port)| (name, Port(port)))
.collect(),
addresses,
message_ids: raw
.message_ids
.into_iter()
.map(|(name, id)| (name, AdjId(id)))
.collect(),
message_ids,
message_levels,
protection_types: raw
.protection_types
.into_iter()
Expand Down Expand Up @@ -293,7 +306,7 @@ fn parse_duration(text: &str) -> Result<Duration, ProtectionError> {
mod tests {
use super::*;
use crate::adj::raw::loader::load_board;
use crate::model::PacketKind;
use crate::model::PacketType;
use std::net::IpAddr;

fn raw_protection(
Expand Down Expand Up @@ -488,7 +501,12 @@ mod tests {
ports: HashMap::from([("TCP_SERVER".to_string(), 50500u16)]),
addresses: HashMap::from([("backend".to_string(), "192.168.0.9".to_string())]),
protection_types,
message_ids: HashMap::from([("fault".to_string(), 2u16)]),
message_ids: HashMap::from([
("fault".to_string(), 2u16),
// Not a `MessageLevel` — shares `message_ids` with the
// four severities, but shouldn't appear in `message_levels`.
("add_state_order".to_string(), 5u16),
]),
};

let Ok(info) = AdjInfo::try_from(raw) else {
Expand All @@ -498,7 +516,13 @@ mod tests {
assert_eq!(info.ports["TCP_SERVER"], Port(50500));
assert_eq!(info.addresses["backend"], IpAddr::from([192, 168, 0, 9]));
assert_eq!(info.message_ids["fault"], AdjId(2));
assert_eq!(info.message_ids["add_state_order"], AdjId(5));
assert!(info.protection_types["Range"].is_range);

// `message_levels` is the reverse of `message_ids`, but only for
// the one entry that's actually a `MessageLevel`.
assert_eq!(info.message_levels.len(), 1);
assert_eq!(info.message_levels[&AdjId(2)], MessageLevel::Fault);
}

#[test]
Expand Down Expand Up @@ -599,7 +623,7 @@ mod tests {
let protection_count = board
.packets
.values()
.filter(|p| matches!(p.kind, PacketKind::Protection(_)))
.filter(|p| matches!(p.packet_type, PacketType::Protection(_)))
.count();
assert_eq!(
protection_count, 2,
Expand Down
Loading
Loading