Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ The device overview page displays a real-time snapshot of the device's health an
- **OS Variant & Version**: omnect Secure OS build name and version string
- **Boot Time**: Timestamp of the last system boot
- **Wait-Online Timeout**: Configured network wait timeout (seconds)
- **Service Versions**: omnect-device-service, Azure SDK, and WiFi commissioning service versions (the WiFi entry also shows the minimum required version when the installed version is too old)
- **Service Versions**: omnect-device-service, Azure SDK, and WiFi commissioning service versions (when the installed WiFi service is too old, the entry shows the required version next to it)

---

Expand Down
1 change: 1 addition & 0 deletions project-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ omnect-ui/
│ │ │ ├── keycloak_client.rs
│ │ │ ├── omnect_device_service_client.rs
│ │ │ ├── wifi_commissioning_client.rs
│ │ │ ├── version.rs # Service version requirement check
│ │ │ └── services/ # Business logic services
│ │ │ ├── mod.rs
│ │ │ ├── certificate.rs
Expand Down
28 changes: 19 additions & 9 deletions src/app/src/types/factory_reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@ use std::fmt;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub enum FactoryResetStatus {
/// No result received yet.
#[default]
Unknown,
ModeSupported,
ModeUnsupported,
BackupRestoreError,
ConfigurationError,
Success,
Invalid,
Error,
ConfigError,
/// Reset succeeded, but a partition needed a second format attempt.
Warning,
/// A status code this version does not know.
Unrecognized,
}

impl fmt::Display for FactoryResetStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unknown => write!(f, "unknown"),
Self::ModeSupported => write!(f, "modeSupported"),
Self::ModeUnsupported => write!(f, "modeUnsupported"),
Self::BackupRestoreError => write!(f, "backupRestoreError"),
Self::ConfigurationError => write!(f, "configurationError"),
Self::Success => write!(f, "success"),
Self::Invalid => write!(f, "invalid"),
Self::Error => write!(f, "error"),
Self::ConfigError => write!(f, "configError"),
Self::Warning => write!(f, "warning"),
Self::Unrecognized => write!(f, "unrecognized"),
}
}
}
Expand All @@ -31,8 +38,11 @@ impl fmt::Display for FactoryResetStatus {
pub struct FactoryResetResult {
pub status: FactoryResetStatus,
pub context: Option<String>,
pub error: String,
pub error: Option<String>,
pub paths: Vec<String>,
/// `true` once the reset started wiping data. On a failure this separates a
/// safe abort from one that left the device half wiped.
pub data_wiped: bool,
}

/// Factory reset state from WebSocket
Expand Down
34 changes: 22 additions & 12 deletions src/app/src/types/ods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,34 +170,43 @@ impl From<OdsNetworkStatus> for NetworkStatus {
}
}

/// Factory reset result status — ODS sends numeric values (`serde_repr`)
/// Factory reset result status — ODS sends numeric values (`serde_repr`).
#[derive(Debug, Clone, Deserialize_repr, PartialEq, Eq)]
#[repr(u8)]
#[repr(u32)]
pub enum OdsFactoryResetResultStatus {
ModeSupported = 0,
ModeUnsupported = 1,
BackupRestoreError = 2,
ConfigurationError = 3,
Success = 0,
Invalid = 1,
Error = 2,
ConfigError = 3,
Warning = 4,
/// A future status code must parse instead of failing the whole update.
#[serde(other)]
Unrecognized = u32::MAX,
}

impl From<OdsFactoryResetResultStatus> for FactoryResetStatus {
fn from(ods: OdsFactoryResetResultStatus) -> Self {
match ods {
OdsFactoryResetResultStatus::ModeSupported => Self::ModeSupported,
OdsFactoryResetResultStatus::ModeUnsupported => Self::ModeUnsupported,
OdsFactoryResetResultStatus::BackupRestoreError => Self::BackupRestoreError,
OdsFactoryResetResultStatus::ConfigurationError => Self::ConfigurationError,
OdsFactoryResetResultStatus::Success => Self::Success,
OdsFactoryResetResultStatus::Invalid => Self::Invalid,
OdsFactoryResetResultStatus::Error => Self::Error,
OdsFactoryResetResultStatus::ConfigError => Self::ConfigError,
OdsFactoryResetResultStatus::Warning => Self::Warning,
OdsFactoryResetResultStatus::Unrecognized => Self::Unrecognized,
}
}
}

/// Factory reset result
/// Factory reset result.
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct OdsFactoryResetResult {
pub status: OdsFactoryResetResultStatus,
pub context: Option<String>,
pub error: String,
pub error: Option<String>,
#[serde(default)]
pub paths: Vec<String>,
#[serde(default)]
pub data_wiped: bool,
}

impl From<OdsFactoryResetResult> for FactoryResetResult {
Expand All @@ -207,6 +216,7 @@ impl From<OdsFactoryResetResult> for FactoryResetResult {
context: ods.context,
error: ods.error,
paths: ods.paths,
data_wiped: ods.data_wiped,
}
}
}
Expand Down
14 changes: 9 additions & 5 deletions src/app/src/types/wifi.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::update::VersionInfo;
use serde::{Deserialize, Serialize};

/// `WiFi` service availability info returned by the backend
Expand All @@ -10,8 +11,8 @@ pub enum WifiAvailability {
},
Unavailable {
socket_present: bool,
version: Option<String>,
min_required_version: String,
/// `None` when the probe gave no usable version.
version_info: Option<VersionInfo>,
},
}

Expand Down Expand Up @@ -70,8 +71,7 @@ pub enum WifiState {
Unknown,
Unavailable {
socket_present: bool,
version: Option<String>,
min_required_version: String,
version_info: Option<VersionInfo>,
},
Ready {
interface_name: String,
Expand Down Expand Up @@ -146,7 +146,11 @@ pub struct WifiForgetResponse {
pub status: String,
}

/// Capabilities of the running wifi-commissioning-service instance
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct WifiVersionResponse {
pub struct WifiServiceInfoResponse {
/// Live BLE transport state: `true` only once BLE actually started.
pub ble_enabled: bool,
pub interface_name: String,
pub version: String,
}
5 changes: 3 additions & 2 deletions src/app/src/update/device/network/verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,10 +479,11 @@ mod tests {
factory_reset: Some(FactoryReset {
keys: vec!["key1".to_string()],
result: Some(FactoryResetResult {
status: FactoryResetStatus::ModeSupported,
status: FactoryResetStatus::Success,
context: None,
error: String::new(),
error: None,
paths: vec![],
data_wiped: true,
}),
}),
..Default::default()
Expand Down
3 changes: 1 addition & 2 deletions src/app/src/update/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ mod tests {
Event::Wifi(WifiEvent::CheckAvailabilityResponse(Ok(
WifiAvailability::Unavailable {
socket_present: false,
version: None,
min_required_version: "0.1.0".into(),
version_info: None,
},
))),
&mut model,
Expand Down
93 changes: 83 additions & 10 deletions src/app/src/update/websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,20 +199,93 @@ mod tests {
assert_eq!(model.factory_reset, Some(expected_status));
}

#[test]
fn parses_integer_status_from_ods() {
fn parse_result(json: &str) -> crate::types::FactoryResetResult {
let mut model = Model::default();
let _ = handle(WebSocketEvent::FactoryResetUpdated(json.into()), &mut model);
model
.factory_reset
.expect("factory_reset should be set")
.result
.expect("result should be set")
}

#[test]
fn parses_successful_result() {
let result = parse_result(
r#"{"keys":["network"],"result":{"status":0,"error":null,"context":null,"paths":["network"],"data_wiped":true}}"#,
);

// ODS sends status as integer (serde_repr): 0=ModeSupported, 1=ModeUnsupported, etc.
let json = r#"{"keys":["network"],"result":{"status":0,"error":"0","paths":["/etc/systemd/network/"]}}"#;
assert_eq!(result.status, FactoryResetStatus::Success);
assert_eq!(result.error, None);
assert_eq!(result.context, None);
assert_eq!(result.paths, vec!["network"]);
assert!(result.data_wiped);
}

let _ = handle(WebSocketEvent::FactoryResetUpdated(json.into()), &mut model);
#[test]
fn parses_error_result_with_message() {
let result = parse_result(
r#"{"keys":["network"],"result":{"status":2,"error":"format failed","context":"factory partition","paths":[],"data_wiped":true}}"#,
);

assert_eq!(result.status, FactoryResetStatus::Error);
assert_eq!(result.error.as_deref(), Some("format failed"));
assert_eq!(result.context.as_deref(), Some("factory partition"));
assert!(result.data_wiped);
}

let factory_reset = model.factory_reset.expect("factory_reset should be set");
let result = factory_reset.result.expect("result should be set");
assert_eq!(result.status, FactoryResetStatus::ModeSupported);
assert_eq!(result.error, "0");
assert_eq!(result.paths, vec!["/etc/systemd/network/"]);
#[test]
fn parses_warning_result() {
let result = parse_result(
r#"{"keys":["network"],"result":{"status":4,"error":null,"context":"second format attempt","paths":[],"data_wiped":true}}"#,
);

assert_eq!(result.status, FactoryResetStatus::Warning);
}

#[test]
fn abort_before_wipe_reports_data_not_wiped() {
let result = parse_result(
r#"{"keys":["network"],"result":{"status":1,"error":"factory reset mode 4 is not supported","context":null,"paths":[],"data_wiped":false}}"#,
);

assert_eq!(result.status, FactoryResetStatus::Invalid);
assert!(!result.data_wiped);
}

#[test]
fn config_error_maps_to_config_error() {
let result = parse_result(
r#"{"keys":["network"],"result":{"status":3,"error":"bad config","context":null,"paths":[],"data_wiped":false}}"#,
);

assert_eq!(result.status, FactoryResetStatus::ConfigError);
}

#[test]
fn unknown_status_codes_parse_as_unrecognized() {
for code in [5, u32::MAX] {
let result = parse_result(&format!(
r#"{{"keys":["network"],"result":{{"status":{code},"error":null,"context":null,"paths":[],"data_wiped":false}}}}"#
));

assert_eq!(
result.status,
FactoryResetStatus::Unrecognized,
"code {code}"
);
}
}

#[test]
fn missing_optional_keys_fall_back_to_defaults() {
let result = parse_result(r#"{"keys":["network"],"result":{"status":0}}"#);

assert_eq!(result.status, FactoryResetStatus::Success);
assert_eq!(result.error, None);
assert_eq!(result.context, None);
assert_eq!(result.paths, Vec::<String>::new());
assert!(!result.data_wiped);
}
}

Expand Down
Loading
Loading