From f3f3eab440c1cecc38dd9f5093df5ad5cc03b41a Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Thu, 13 Mar 2025 15:06:23 +0000 Subject: [PATCH] Initial FUOTA v2 implementation. This implements selecting the v2.0.0 app-layer package in the device-profile and handling these payloads in the FUOTA flow. --- api/proto/api/device_profile.proto | 9 + .../proto/chirpstack/api/device_profile.proto | 9 + chirpstack/src/api/helpers.rs | 6 + chirpstack/src/applayer/clocksync.rs | 217 ++++++++++++++++++ chirpstack/src/applayer/fragmentation.rs | 83 +++++++ chirpstack/src/applayer/fuota/flow.rs | 128 ++++++++--- chirpstack/src/applayer/multicastsetup.rs | 124 ++++++++++ .../src/storage/fields/device_profile.rs | 3 + lrwn/src/applayer/fragmentation/v2.rs | 51 ++++ lrwn/src/applayer/multicastsetup/v1.rs | 9 +- lrwn/src/applayer/multicastsetup/v2.rs | 9 +- .../device-profiles/DeviceProfileForm.tsx | 3 + 12 files changed, 615 insertions(+), 36 deletions(-) diff --git a/api/proto/api/device_profile.proto b/api/proto/api/device_profile.proto index bc1e7b74..fd555240 100644 --- a/api/proto/api/device_profile.proto +++ b/api/proto/api/device_profile.proto @@ -104,6 +104,9 @@ enum Ts003Version { // v1.0.0. TS003_V100 = 1; + + // v2.0.0 + TS003_v200 = 2; } enum Ts004Version { @@ -112,6 +115,9 @@ enum Ts004Version { // v1.0.0. TS004_V100 = 1; + + // v2.0.0 + TS004_V200 = 2; } enum Ts005Version { @@ -120,6 +126,9 @@ enum Ts005Version { // v1.0.0. TS005_V100 = 1; + + // v2.0.0 + TS005_V200 = 2; } // DeviceProfileService is the service providing API methods for managing diff --git a/api/rust/proto/chirpstack/api/device_profile.proto b/api/rust/proto/chirpstack/api/device_profile.proto index bc1e7b74..fd555240 100644 --- a/api/rust/proto/chirpstack/api/device_profile.proto +++ b/api/rust/proto/chirpstack/api/device_profile.proto @@ -104,6 +104,9 @@ enum Ts003Version { // v1.0.0. TS003_V100 = 1; + + // v2.0.0 + TS003_v200 = 2; } enum Ts004Version { @@ -112,6 +115,9 @@ enum Ts004Version { // v1.0.0. TS004_V100 = 1; + + // v2.0.0 + TS004_V200 = 2; } enum Ts005Version { @@ -120,6 +126,9 @@ enum Ts005Version { // v1.0.0. TS005_V100 = 1; + + // v2.0.0 + TS005_V200 = 2; } // DeviceProfileService is the service providing API methods for managing diff --git a/chirpstack/src/api/helpers.rs b/chirpstack/src/api/helpers.rs index 90d35274..96243c01 100644 --- a/chirpstack/src/api/helpers.rs +++ b/chirpstack/src/api/helpers.rs @@ -292,6 +292,7 @@ impl ToProto for Option match self { None => api::Ts003Version::Ts003NotImplemented, Some(fields::device_profile::Ts003Version::V100) => api::Ts003Version::Ts003V100, + Some(fields::device_profile::Ts003Version::V200) => api::Ts003Version::Ts003V200, } } } @@ -301,6 +302,7 @@ impl FromProto> for api::Ts003Versi match self { api::Ts003Version::Ts003NotImplemented => None, api::Ts003Version::Ts003V100 => Some(fields::device_profile::Ts003Version::V100), + api::Ts003Version::Ts003V200 => Some(fields::device_profile::Ts003Version::V200), } } } @@ -310,6 +312,7 @@ impl ToProto for Option match self { None => api::Ts004Version::Ts004NotImplemented, Some(fields::device_profile::Ts004Version::V100) => api::Ts004Version::Ts004V100, + Some(fields::device_profile::Ts004Version::V200) => api::Ts004Version::Ts004V200, } } } @@ -319,6 +322,7 @@ impl FromProto> for api::Ts004Versi match self { api::Ts004Version::Ts004NotImplemented => None, api::Ts004Version::Ts004V100 => Some(fields::device_profile::Ts004Version::V100), + api::Ts004Version::Ts004V200 => Some(fields::device_profile::Ts004Version::V200), } } } @@ -328,6 +332,7 @@ impl ToProto for Option match self { None => api::Ts005Version::Ts005NotImplemented, Some(fields::device_profile::Ts005Version::V100) => api::Ts005Version::Ts005V100, + Some(fields::device_profile::Ts005Version::V200) => api::Ts005Version::Ts005V200, } } } @@ -337,6 +342,7 @@ impl FromProto> for api::Ts005Versi match self { api::Ts005Version::Ts005NotImplemented => None, api::Ts005Version::Ts005V100 => Some(fields::device_profile::Ts005Version::V100), + api::Ts005Version::Ts005V200 => Some(fields::device_profile::Ts005Version::V200), } } } diff --git a/chirpstack/src/applayer/clocksync.rs b/chirpstack/src/applayer/clocksync.rs index cc400a80..94717293 100644 --- a/chirpstack/src/applayer/clocksync.rs +++ b/chirpstack/src/applayer/clocksync.rs @@ -21,6 +21,7 @@ pub async fn handle_uplink( match version { Ts003Version::V100 => handle_uplink_v100(dev, dp, rx_info, data).await, + Ts003Version::V200 => handle_uplink_v200(dev, dp, rx_info, data).await, } } @@ -42,6 +43,24 @@ async fn handle_uplink_v100( Ok(()) } +async fn handle_uplink_v200( + dev: &device::Device, + dp: &device_profile::DeviceProfile, + rx_info: &[gw::UplinkRxInfo], + data: &[u8], +) -> Result<()> { + let pl = clocksync::v2::Payload::from_slice(true, data)?; + + match pl { + clocksync::v2::Payload::AppTimeReq(pl) => { + handle_v2_app_time_req(dev, dp, rx_info, pl).await? + } + _ => {} + } + + Ok(()) +} + async fn handle_v1_app_time_req( dev: &device::Device, dp: &device_profile::DeviceProfile, @@ -91,6 +110,55 @@ async fn handle_v1_app_time_req( Ok(()) } +async fn handle_v2_app_time_req( + dev: &device::Device, + dp: &device_profile::DeviceProfile, + rx_info: &[gw::UplinkRxInfo], + pl: clocksync::v2::AppTimeReqPayload, +) -> Result<()> { + info!("Handling AppTimeReq"); + + let now_time_since_gps = if let Some(t) = helpers::get_time_since_gps_epoch(rx_info) { + chrono::Duration::from_std(t)? + } else { + helpers::get_rx_timestamp_chrono(rx_info).to_gps_time() + }; + let dev_time_since_gps = chrono::Duration::seconds(pl.device_time.into()); + + let time_diff = (now_time_since_gps - dev_time_since_gps).num_seconds(); + let time_correction: i32 = if time_diff < 0 { + time_diff.try_into().unwrap_or(i32::MIN) + } else { + time_diff.try_into().unwrap_or(i32::MAX) + }; + + if time_diff == 0 && !pl.param.ans_required { + return Ok(()); + } + + info!( + time_correcrtion = time_correction, + "Responding with AppTimeAns" + ); + + let ans = clocksync::v2::Payload::AppTimeAns(clocksync::v2::AppTimeAnsPayload { + time_correction, + param: clocksync::v2::AppTimeAnsPayloadParam { + token_ans: pl.param.token_req, + }, + }); + + device_queue::enqueue_item(device_queue::DeviceQueueItem { + dev_eui: dev.dev_eui, + f_port: dp.app_layer_params.ts003_f_port.into(), + data: ans.to_vec()?, + ..Default::default() + }) + .await?; + + Ok(()) +} + #[cfg(test)] mod test { use super::*; @@ -248,4 +316,153 @@ mod test { } } } + + #[tokio::test] + async fn test_handle_v2_app_time_req() { + struct Test { + name: String, + rx_info: gw::UplinkRxInfo, + req: clocksync::v2::AppTimeReqPayload, + expected: Option, + } + + let tests = vec![ + Test { + name: "device synced".into(), + rx_info: gw::UplinkRxInfo { + time_since_gps_epoch: Some(Duration::from_secs(1234).try_into().unwrap()), + ..Default::default() + }, + req: clocksync::v2::AppTimeReqPayload { + device_time: 1234, + param: clocksync::v2::AppTimeReqPayloadParam { + token_req: 8, + ans_required: false, + }, + }, + expected: None, + }, + Test { + name: "device synced - ans required".into(), + rx_info: gw::UplinkRxInfo { + time_since_gps_epoch: Some(Duration::from_secs(1234).try_into().unwrap()), + ..Default::default() + }, + req: clocksync::v2::AppTimeReqPayload { + device_time: 1234, + param: clocksync::v2::AppTimeReqPayloadParam { + token_req: 8, + ans_required: true, + }, + }, + expected: Some(clocksync::v2::AppTimeAnsPayload { + time_correction: 0, + param: clocksync::v2::AppTimeAnsPayloadParam { token_ans: 8 }, + }), + }, + Test { + name: "device not synced (positive correction)".into(), + rx_info: gw::UplinkRxInfo { + time_since_gps_epoch: Some(Duration::from_secs(1234).try_into().unwrap()), + ..Default::default() + }, + req: clocksync::v2::AppTimeReqPayload { + device_time: 1200, + param: clocksync::v2::AppTimeReqPayloadParam { + token_req: 8, + ans_required: false, + }, + }, + expected: Some(clocksync::v2::AppTimeAnsPayload { + time_correction: 34, + param: clocksync::v2::AppTimeAnsPayloadParam { token_ans: 8 }, + }), + }, + Test { + name: "device not synced (negative correction)".into(), + rx_info: gw::UplinkRxInfo { + time_since_gps_epoch: Some(Duration::from_secs(1200).try_into().unwrap()), + ..Default::default() + }, + req: clocksync::v2::AppTimeReqPayload { + device_time: 1234, + param: clocksync::v2::AppTimeReqPayloadParam { + token_req: 8, + ans_required: false, + }, + }, + expected: Some(clocksync::v2::AppTimeAnsPayload { + time_correction: -34, + param: clocksync::v2::AppTimeAnsPayloadParam { token_ans: 8 }, + }), + }, + ]; + + let _guard = test::prepare().await; + let t = tenant::create(tenant::Tenant { + name: "test-tenant".into(), + ..Default::default() + }) + .await + .unwrap(); + + let app = application::create(application::Application { + name: "test-app".into(), + tenant_id: t.id, + ..Default::default() + }) + .await + .unwrap(); + + let dp = device_profile::create(device_profile::DeviceProfile { + name: "test-dp".into(), + tenant_id: t.id, + app_layer_params: fields::AppLayerParams { + ts003_version: Some(Ts003Version::V200), + ..Default::default() + }, + ..Default::default() + }) + .await + .unwrap(); + + let d = device::create(device::Device { + name: "test-dev".into(), + dev_eui: EUI64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), + application_id: app.id, + device_profile_id: dp.id, + ..Default::default() + }) + .await + .unwrap(); + + for tst in &tests { + println!("> {}", tst.name); + device_queue::flush_for_dev_eui(&d.dev_eui).await.unwrap(); + let pl = clocksync::v2::Payload::AppTimeReq(tst.req.clone()); + + handle_uplink( + &d, + &dp, + &[tst.rx_info.clone()], + dp.app_layer_params.ts003_f_port, + &pl.to_vec().unwrap(), + ) + .await; + + let queue_items = device_queue::get_for_dev_eui(&d.dev_eui).await.unwrap(); + if let Some(expected_pl) = &tst.expected { + assert_eq!(1, queue_items.len()); + let qi = queue_items.first().unwrap(); + assert_eq!(dp.app_layer_params.ts003_f_port as i16, qi.f_port); + + let qi_pl = clocksync::v2::Payload::from_slice(false, &qi.data).unwrap(); + let expected_pl = clocksync::v2::Payload::AppTimeAns(expected_pl.clone()); + + assert_eq!(expected_pl, qi_pl); + } else { + assert!(queue_items.is_empty()); + } + } + } } diff --git a/chirpstack/src/applayer/fragmentation.rs b/chirpstack/src/applayer/fragmentation.rs index 92af2263..2ced414b 100644 --- a/chirpstack/src/applayer/fragmentation.rs +++ b/chirpstack/src/applayer/fragmentation.rs @@ -18,6 +18,7 @@ pub async fn handle_uplink( match version { Ts004Version::V100 => handle_uplink_v100(dev, data).await, + Ts004Version::V200 => handle_uplink_v200(dev, data).await, } } @@ -37,6 +38,22 @@ async fn handle_uplink_v100(dev: &device::Device, data: &[u8]) -> Result<()> { Ok(()) } +async fn handle_uplink_v200(dev: &device::Device, data: &[u8]) -> Result<()> { + let pl = fragmentation::v2::Payload::from_slice(true, data)?; + + match pl { + fragmentation::v2::Payload::FragSessionSetupAns(pl) => { + handle_v2_frag_session_setup_ans(dev, pl).await? + } + fragmentation::v2::Payload::FragSessionStatusAns(pl) => { + handle_v2_frag_session_status_ans(dev, pl).await? + } + _ => {} + } + + Ok(()) +} + async fn handle_v1_frag_session_setup_ans( dev: &device::Device, pl: fragmentation::v1::FragSessionSetupAnsPayload, @@ -68,6 +85,39 @@ async fn handle_v1_frag_session_setup_ans( Ok(()) } +async fn handle_v2_frag_session_setup_ans( + dev: &device::Device, + pl: fragmentation::v2::FragSessionSetupAnsPayload, +) -> Result<()> { + info!("Handling FragSessionSetupAns"); + + let mut fuota_dev = fuota::get_latest_device_by_dev_eui(dev.dev_eui).await?; + + if pl.frag_algo_unsupported + | pl.not_enough_memory + | pl.frag_index_unsupported + | pl.wrong_descriptor + | pl.session_cnt_replay + { + warn!( + frag_index = pl.frag_index, + frag_algo_unsupported = pl.frag_algo_unsupported, + not_enough_memory = pl.not_enough_memory, + frag_index_unsupported = pl.frag_index_unsupported, + wrong_descriptor = pl.wrong_descriptor, + session_cnt_replay = pl.session_cnt_replay, + "FragSessionAns contains errors" + ); + fuota_dev.error_msg = format!("Error: FragSessionAns response frag_algo_unsupported={}, not_enough_memory={}, frag_index_unsupported={}, wrong_descriptor={}, session_cnt_replay={}", pl.frag_algo_unsupported, pl.not_enough_memory, pl.frag_index_unsupported, pl.wrong_descriptor, pl.session_cnt_replay); + } else { + fuota_dev.frag_session_setup_completed_at = Some(Utc::now()); + } + + let _ = fuota::update_device(fuota_dev).await?; + + Ok(()) +} + async fn handle_v1_frag_session_status_ans( dev: &device::Device, pl: fragmentation::v1::FragSessionStatusAnsPayload, @@ -94,3 +144,36 @@ async fn handle_v1_frag_session_status_ans( Ok(()) } + +async fn handle_v2_frag_session_status_ans( + dev: &device::Device, + pl: fragmentation::v2::FragSessionStatusAnsPayload, +) -> Result<()> { + info!("Handling FragSessionStatusAnsPayload"); + + let mut fuota_dev = fuota::get_latest_device_by_dev_eui(dev.dev_eui).await?; + + if pl.missing_frag != 0 + || pl.status.memory_error + || pl.status.mic_error + || pl.status.session_does_not_exist + { + warn!( + frag_index = pl.received_and_index.frag_index, + nb_frag_received = pl.received_and_index.nb_frag_received, + missing_frag = pl.missing_frag, + memory_error = pl.status.memory_error, + mic_error = pl.status.mic_error, + session_does_not_exist = pl.status.session_does_not_exist, + "FragSessionStatusAns contains errors" + ); + + fuota_dev.error_msg = format!("Error: FragSessionStatusAns response nb_frag_received={}, missing_frag={}, memory_error={}, mic_error={}, session_does_not_exist={}", pl.received_and_index.nb_frag_received, pl.missing_frag, pl.status.memory_error, pl.status.mic_error, pl.status.session_does_not_exist); + } else { + fuota_dev.frag_status_completed_at = Some(Utc::now()); + } + + let _ = fuota::update_device(fuota_dev).await?; + + Ok(()) +} diff --git a/chirpstack/src/applayer/fuota/flow.rs b/chirpstack/src/applayer/fuota/flow.rs index c4a57460..9e4a50ec 100644 --- a/chirpstack/src/applayer/fuota/flow.rs +++ b/chirpstack/src/applayer/fuota/flow.rs @@ -11,7 +11,10 @@ use lrwn::region::MacVersion; use crate::config; use crate::downlink; use crate::gpstime::ToGpsTime; -use crate::storage::fields::{FuotaJob, RequestFragmentationSessionStatus}; +use crate::storage::fields::{ + device_profile::Ts004Version, device_profile::Ts005Version, FuotaJob, + RequestFragmentationSessionStatus, +}; use crate::storage::{device, device_keys, device_profile, device_queue, fuota, multicast}; pub struct Flow { @@ -217,22 +220,40 @@ impl Flow { let mc_key_encrypted = multicastsetup::v1::encrypt_mc_key(mc_ke_key, self.fuota_deployment.multicast_key); - let pl = multicastsetup::v1::Payload::McGroupSetupReq( - multicastsetup::v1::McGroupSetupReqPayload { - mc_group_id_header: multicastsetup::v1::McGroupSetupReqPayloadMcGroupIdHeader { - mc_group_id: 0, + let pl = match self.device_profile.app_layer_params.ts005_version { + Some(Ts005Version::V100) => multicastsetup::v1::Payload::McGroupSetupReq( + multicastsetup::v1::McGroupSetupReqPayload { + mc_group_id_header: + multicastsetup::v1::McGroupSetupReqPayloadMcGroupIdHeader { + mc_group_id: 0, + }, + mc_addr: self.fuota_deployment.multicast_addr, + mc_key_encrypted, + min_mc_f_count: 0, + max_mc_f_count: u32::MAX, }, - mc_addr: self.fuota_deployment.multicast_addr, - mc_key_encrypted, - min_mc_f_count: 0, - max_mc_f_count: u32::MAX, - }, - ); + ) + .to_vec()?, + Some(Ts005Version::V200) => multicastsetup::v2::Payload::McGroupSetupReq( + multicastsetup::v2::McGroupSetupReqPayload { + mc_group_id_header: + multicastsetup::v2::McGroupSetupReqPayloadMcGroupIdHeader { + mc_group_id: 0, + }, + mc_addr: self.fuota_deployment.multicast_addr, + mc_key_encrypted, + min_mc_f_count: 0, + max_mc_f_count: u32::MAX, + }, + ) + .to_vec()?, + None => return Err(anyhow!("Device-profile does not support TS005")), + }; - device_queue::enqueue_item(device_queue::DeviceQueueItem { + let _ = device_queue::enqueue_item(device_queue::DeviceQueueItem { dev_eui: fuota_dev.dev_eui, f_port: self.device_profile.app_layer_params.ts005_f_port.into(), - data: pl.to_vec()?, + data: pl, ..Default::default() }) .await?; @@ -285,27 +306,74 @@ impl Flow { } for fuota_dev in &fuota_devices { - let pl = fragmentation::v1::Payload::FragSessionSetupReq( - fragmentation::v1::FragSessionSetupReqPayload { - frag_session: fragmentation::v1::FragSessionSetuReqPayloadFragSession { - mc_group_bit_mask: [true, false, false, false], - frag_index: 0, + let pl = match self.device_profile.app_layer_params.ts004_version { + Some(Ts004Version::V100) => fragmentation::v1::Payload::FragSessionSetupReq( + fragmentation::v1::FragSessionSetupReqPayload { + frag_session: fragmentation::v1::FragSessionSetuReqPayloadFragSession { + mc_group_bit_mask: [true, false, false, false], + frag_index: 0, + }, + nb_frag: fragments as u16, + frag_size: fragment_size as u8, + padding: padding as u8, + control: fragmentation::v1::FragSessionSetuReqPayloadControl { + block_ack_delay: 0, + fragmentation_matrix: 0, + }, + descriptor: [0, 0, 0, 0], }, - nb_frag: fragments as u16, - frag_size: fragment_size as u8, - padding: padding as u8, - control: fragmentation::v1::FragSessionSetuReqPayloadControl { - block_ack_delay: 0, - fragmentation_matrix: 0, - }, - descriptor: [0, 0, 0, 0], - }, - ); + ) + .to_vec()?, + Some(Ts004Version::V200) => { + let dev_keys = device_keys::get(&fuota_dev.dev_eui).await?; + let data_block_int_key = match self.device_profile.mac_version { + MacVersion::LORAWAN_1_0_0 + | MacVersion::LORAWAN_1_0_1 + | MacVersion::LORAWAN_1_0_2 + | MacVersion::LORAWAN_1_0_3 + | MacVersion::LORAWAN_1_0_4 => { + fragmentation::v2::get_data_block_int_key(dev_keys.gen_app_key)? + } + MacVersion::LORAWAN_1_1_0 | MacVersion::Latest => { + fragmentation::v2::get_data_block_int_key(dev_keys.app_key)? + } + }; + let mic = fragmentation::v2::calculate_mic( + data_block_int_key, + 0, + 0, + [0, 0, 0, 0], + &self.fuota_deployment.payload, + )?; - device_queue::enqueue_item(device_queue::DeviceQueueItem { + fragmentation::v2::Payload::FragSessionSetupReq( + fragmentation::v2::FragSessionSetupReqPayload { + frag_session: fragmentation::v2::FragSessionSetuReqPayloadFragSession { + mc_group_bit_mask: [true, false, false, false], + frag_index: 0, + }, + nb_frag: fragments as u16, + frag_size: fragment_size as u8, + padding: padding as u8, + control: fragmentation::v2::FragSessionSetuReqPayloadControl { + block_ack_delay: 0, + frag_algo: 0, + ack_reception: false, + }, + descriptor: [0, 0, 0, 0], + mic, + session_cnt: 0, + }, + ) + .to_vec()? + } + None => return Err(anyhow!("Device-profile does not support TS004")), + }; + + let _ = device_queue::enqueue_item(device_queue::DeviceQueueItem { dev_eui: fuota_dev.dev_eui, f_port: self.device_profile.app_layer_params.ts004_f_port.into(), - data: pl.to_vec()?, + data: pl, ..Default::default() }) .await?; diff --git a/chirpstack/src/applayer/multicastsetup.rs b/chirpstack/src/applayer/multicastsetup.rs index c070273e..d407a5cc 100644 --- a/chirpstack/src/applayer/multicastsetup.rs +++ b/chirpstack/src/applayer/multicastsetup.rs @@ -18,6 +18,7 @@ pub async fn handle_uplink( match version { Ts005Version::V100 => handle_uplink_v100(dev, data).await, + Ts005Version::V200 => handle_uplink_v200(dev, data).await, } } @@ -40,6 +41,25 @@ async fn handle_uplink_v100(dev: &device::Device, data: &[u8]) -> Result<()> { Ok(()) } +async fn handle_uplink_v200(dev: &device::Device, data: &[u8]) -> Result<()> { + let pl = multicastsetup::v2::Payload::from_slice(true, data)?; + + match pl { + multicastsetup::v2::Payload::McGroupSetupAns(pl) => { + handle_v2_mc_group_setup_ans(dev, pl).await? + } + multicastsetup::v2::Payload::McClassBSessionAns(pl) => { + handle_v2_mc_class_b_session_ans(dev, pl).await? + } + multicastsetup::v2::Payload::McClassCSessionAns(pl) => { + handle_v2_mc_class_c_session_ans(dev, pl).await? + } + _ => {} + } + + Ok(()) +} + async fn handle_v1_mc_group_setup_ans( dev: &device::Device, pl: multicastsetup::v1::McGroupSetupAnsPayload, @@ -64,6 +84,30 @@ async fn handle_v1_mc_group_setup_ans( Ok(()) } +async fn handle_v2_mc_group_setup_ans( + dev: &device::Device, + pl: multicastsetup::v2::McGroupSetupAnsPayload, +) -> Result<()> { + info!("Handling McGroupSetupAns"); + + let mut fuota_dev = fuota::get_latest_device_by_dev_eui(dev.dev_eui).await?; + + if pl.mc_group_id_header.id_error { + warn!( + mc_group_id = pl.mc_group_id_header.mc_group_id, + id_error = true, + "McGroupSetupAns contains errors" + ); + fuota_dev.error_msg = "Error: McGroupSetupAns response id_error=true".into(); + } else { + fuota_dev.mc_group_setup_completed_at = Some(Utc::now()); + } + + let _ = fuota::update_device(fuota_dev).await?; + + Ok(()) +} + async fn handle_v1_mc_class_b_session_ans( dev: &device::Device, pl: multicastsetup::v1::McClassBSessionAnsPayload, @@ -101,6 +145,46 @@ async fn handle_v1_mc_class_b_session_ans( Ok(()) } +async fn handle_v2_mc_class_b_session_ans( + dev: &device::Device, + pl: multicastsetup::v2::McClassBSessionAnsPayload, +) -> Result<()> { + info!("Handling McClassBSessionAns"); + + let mut fuota_dev = fuota::get_latest_device_by_dev_eui(dev.dev_eui).await?; + + if pl.status_and_mc_group_id.dr_error + | pl.status_and_mc_group_id.freq_error + | pl.status_and_mc_group_id.mc_group_undefined + | pl.status_and_mc_group_id.start_missed + { + warn!( + dr_error = pl.status_and_mc_group_id.dr_error, + freq_error = pl.status_and_mc_group_id.freq_error, + mc_group_undefined = pl.status_and_mc_group_id.mc_group_undefined, + start_missed = pl.status_and_mc_group_id.start_missed, + "McClassBSessionAns contains errors" + ); + + fuota_dev.error_msg= format!("Error: McClassBSessionAns response dr_error: {}, freq_error: {}, mc_group_undefined: {}, start_missed: {}", + pl.status_and_mc_group_id.dr_error, + pl.status_and_mc_group_id.freq_error, + pl.status_and_mc_group_id.mc_group_undefined, + pl.status_and_mc_group_id.start_missed, + ); + } else { + info!( + time_to_start = pl.time_to_start.unwrap_or_default(), + "McClassBSessionAns OK" + ); + fuota_dev.mc_session_completed_at = Some(Utc::now()); + } + + let _ = fuota::update_device(fuota_dev).await?; + + Ok(()) +} + async fn handle_v1_mc_class_c_session_ans( dev: &device::Device, pl: multicastsetup::v1::McClassCSessionAnsPayload, @@ -137,3 +221,43 @@ async fn handle_v1_mc_class_c_session_ans( Ok(()) } + +async fn handle_v2_mc_class_c_session_ans( + dev: &device::Device, + pl: multicastsetup::v2::McClassCSessionAnsPayload, +) -> Result<()> { + info!("Handling McClassCSessionAns"); + + let mut fuota_dev = fuota::get_latest_device_by_dev_eui(dev.dev_eui).await?; + + if pl.status_and_mc_group_id.dr_error + | pl.status_and_mc_group_id.freq_error + | pl.status_and_mc_group_id.mc_group_undefined + | pl.status_and_mc_group_id.start_missed + { + warn!( + dr_error = pl.status_and_mc_group_id.dr_error, + freq_error = pl.status_and_mc_group_id.freq_error, + mc_group_undefined = pl.status_and_mc_group_id.mc_group_undefined, + start_missed = pl.status_and_mc_group_id.start_missed, + "McClassCSessionAns contains errors" + ); + + fuota_dev.error_msg = format!("Error: McClassCSessionAns response dr_error: {}, freq_error: {}, mc_group_undefined: {}, start_missed: {}", + pl.status_and_mc_group_id.dr_error, + pl.status_and_mc_group_id.freq_error, + pl.status_and_mc_group_id.mc_group_undefined, + pl.status_and_mc_group_id.start_missed, + ); + } else { + info!( + time_to_start = pl.time_to_start.unwrap_or_default(), + "McClassCSessionAns OK" + ); + fuota_dev.mc_session_completed_at = Some(Utc::now()); + } + + let _ = fuota::update_device(fuota_dev).await?; + + Ok(()) +} diff --git a/chirpstack/src/storage/fields/device_profile.rs b/chirpstack/src/storage/fields/device_profile.rs index d91e03cf..b38263b0 100644 --- a/chirpstack/src/storage/fields/device_profile.rs +++ b/chirpstack/src/storage/fields/device_profile.rs @@ -314,16 +314,19 @@ impl serialize::ToSql for AppLayerParams { pub enum Ts003Version { #[default] V100, + V200, } #[derive(Default, Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub enum Ts004Version { #[default] V100, + V200, } #[derive(Default, Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub enum Ts005Version { #[default] V100, + V200, } diff --git a/lrwn/src/applayer/fragmentation/v2.rs b/lrwn/src/applayer/fragmentation/v2.rs index 400cdbea..989f6f5a 100644 --- a/lrwn/src/applayer/fragmentation/v2.rs +++ b/lrwn/src/applayer/fragmentation/v2.rs @@ -1,6 +1,15 @@ +#[cfg(feature = "crypto")] +use aes::{ + cipher::generic_array::GenericArray, + cipher::{BlockEncrypt, KeyInit}, + Aes128, Block, +}; use anyhow::Result; +#[cfg(feature = "crypto")] +use cmac::{Cmac, Mac}; use crate::applayer::PayloadCodec; +use crate::AES128Key; pub enum Cid { PackageVersionReq, @@ -662,6 +671,48 @@ fn matrix_line(n: usize, m: usize) -> Vec { line } +pub fn get_data_block_int_key(app_key: AES128Key) -> Result { + let mut b: [u8; 16] = [0x30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + let key_bytes = app_key.to_bytes(); + let key = GenericArray::from_slice(&key_bytes); + let cipher = Aes128::new(key); + + let block = Block::from_mut_slice(&mut b); + cipher.encrypt_block(block); + + Ok(AES128Key::from_slice(block)?) +} + +pub fn calculate_mic( + data_block_int_key: AES128Key, + session_cnt: u16, + frag_index: u8, + descriptor: [u8; 4], + data: &[u8], +) -> Result<[u8; 4]> { + let mut b0: [u8; 16] = [0; 16]; + b0[0] = 0x49; + b0[1..3].clone_from_slice(&session_cnt.to_le_bytes()); + b0[3] = frag_index; + b0[4..8].clone_from_slice(&descriptor); + b0[12..16].clone_from_slice(&(data.len() as u32).to_le_bytes()); + + let mut mac = as Mac>::new_from_slice(&data_block_int_key.to_bytes()).unwrap(); + mac.update(&b0); + mac.update(data); + + let cmac = mac.finalize().into_bytes(); + if cmac.len() < 4 { + return Err(anyhow!("cmac is less than 4 bytes")); + } + + let mut mic: [u8; 4] = [0; 4]; + mic.clone_from_slice(&cmac[0..4]); + + Ok(mic) +} + #[cfg(test)] mod test { use super::*; diff --git a/lrwn/src/applayer/multicastsetup/v1.rs b/lrwn/src/applayer/multicastsetup/v1.rs index dddf816f..816d3d69 100644 --- a/lrwn/src/applayer/multicastsetup/v1.rs +++ b/lrwn/src/applayer/multicastsetup/v1.rs @@ -1,6 +1,9 @@ -use aes::cipher::BlockDecrypt; -use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}; -use aes::{Aes128, Block}; +#[cfg(feature = "crypto")] +use aes::{ + cipher::BlockDecrypt, + cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}, + Aes128, Block, +}; use anyhow::Result; use crate::applayer::PayloadCodec; diff --git a/lrwn/src/applayer/multicastsetup/v2.rs b/lrwn/src/applayer/multicastsetup/v2.rs index e2deb18b..0df56fa7 100644 --- a/lrwn/src/applayer/multicastsetup/v2.rs +++ b/lrwn/src/applayer/multicastsetup/v2.rs @@ -1,6 +1,9 @@ -use aes::cipher::BlockDecrypt; -use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}; -use aes::{Aes128, Block}; +#[cfg(feature = "crypto")] +use aes::{ + cipher::BlockDecrypt, + cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}, + Aes128, Block, +}; use anyhow::Result; use crate::applayer::PayloadCodec; diff --git a/ui/src/views/device-profiles/DeviceProfileForm.tsx b/ui/src/views/device-profiles/DeviceProfileForm.tsx index 59c589ae..75d85f70 100644 --- a/ui/src/views/device-profiles/DeviceProfileForm.tsx +++ b/ui/src/views/device-profiles/DeviceProfileForm.tsx @@ -1077,6 +1077,7 @@ function DeviceProfileForm(props: IProps) { @@ -1096,6 +1097,7 @@ function DeviceProfileForm(props: IProps) { @@ -1115,6 +1117,7 @@ function DeviceProfileForm(props: IProps) {