From a0f07b5303dcf63e04e5730662299bf4f2f46bc2 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 | 420 +++++++++++++----- 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, 829 insertions(+), 114 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..35984175 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 { @@ -109,14 +112,30 @@ impl Flow { self.job.attempt_count += 1; // Get McAppSKey + McNwkSKey. - let mc_app_s_key = multicastsetup::v1::get_mc_app_s_key( - self.fuota_deployment.multicast_key, - self.fuota_deployment.multicast_addr, - )?; - let mc_nwk_s_key = multicastsetup::v1::get_mc_net_s_key( - self.fuota_deployment.multicast_key, - self.fuota_deployment.multicast_addr, - )?; + let (mc_app_s_key, mc_nwk_s_key) = match self.device_profile.app_layer_params.ts005_version + { + Some(Ts005Version::V100) => ( + multicastsetup::v1::get_mc_app_s_key( + self.fuota_deployment.multicast_key, + self.fuota_deployment.multicast_addr, + )?, + multicastsetup::v1::get_mc_net_s_key( + self.fuota_deployment.multicast_key, + self.fuota_deployment.multicast_addr, + )?, + ), + Some(Ts005Version::V200) => ( + multicastsetup::v2::get_mc_app_s_key( + self.fuota_deployment.multicast_key, + self.fuota_deployment.multicast_addr, + )?, + multicastsetup::v2::get_mc_net_s_key( + self.fuota_deployment.multicast_key, + self.fuota_deployment.multicast_addr, + )?, + ), + None => return Err(anyhow!("Device-profile does not support TS005")), + }; let _ = multicast::create(multicast::MulticastGroup { id: self.fuota_deployment.id, @@ -201,38 +220,85 @@ impl Flow { for fuota_dev in &fuota_devices { let dev_keys = device_keys::get(&fuota_dev.dev_eui).await?; - let mc_root_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 => { - multicastsetup::v1::get_mc_root_key_for_gen_app_key(dev_keys.gen_app_key)? + + let pl = match self.device_profile.app_layer_params.ts005_version { + Some(Ts005Version::V100) => { + let mc_root_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 => { + multicastsetup::v1::get_mc_root_key_for_gen_app_key( + dev_keys.gen_app_key, + )? + } + MacVersion::LORAWAN_1_1_0 | MacVersion::Latest => { + multicastsetup::v1::get_mc_root_key_for_app_key(dev_keys.app_key)? + } + }; + let mc_ke_key = multicastsetup::v1::get_mc_ke_key(mc_root_key)?; + let mc_key_encrypted = multicastsetup::v1::encrypt_mc_key( + mc_ke_key, + self.fuota_deployment.multicast_key, + ); + + 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, + }, + ) + .to_vec()? } - MacVersion::LORAWAN_1_1_0 | MacVersion::Latest => { - multicastsetup::v1::get_mc_root_key_for_app_key(dev_keys.app_key)? + Some(Ts005Version::V200) => { + let mc_root_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 => { + multicastsetup::v2::get_mc_root_key_for_gen_app_key( + dev_keys.gen_app_key, + )? + } + MacVersion::LORAWAN_1_1_0 | MacVersion::Latest => { + multicastsetup::v2::get_mc_root_key_for_app_key(dev_keys.app_key)? + } + }; + let mc_ke_key = multicastsetup::v2::get_mc_ke_key(mc_root_key)?; + let mc_key_encrypted = multicastsetup::v2::encrypt_mc_key( + mc_ke_key, + self.fuota_deployment.multicast_key, + ); + + 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")), }; - let mc_ke_key = multicastsetup::v1::get_mc_ke_key(mc_root_key)?; - 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, - }, - mc_addr: self.fuota_deployment.multicast_addr, - mc_key_encrypted, - min_mc_f_count: 0, - max_mc_f_count: u32::MAX, - }, - ); - - 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 +351,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?; @@ -362,51 +475,98 @@ impl Flow { .num_seconds() % (1 << 32); - let pl = match self.fuota_deployment.multicast_group_type.as_ref() { - "B" => multicastsetup::v1::Payload::McClassBSessionReq( - multicastsetup::v1::McClassBSessionReqPayload { - mc_group_id_header: - multicastsetup::v1::McClassBSessionReqPayloadMcGroupIdHeader { - mc_group_id: 0, + let pl = match self.device_profile.app_layer_params.ts005_version { + Some(Ts005Version::V100) => { + match self.fuota_deployment.multicast_group_type.as_ref() { + "B" => multicastsetup::v1::Payload::McClassBSessionReq( + multicastsetup::v1::McClassBSessionReqPayload { + mc_group_id_header: + multicastsetup::v1::McClassBSessionReqPayloadMcGroupIdHeader { + mc_group_id: 0, + }, + session_time: (session_start - (session_start % 128)) as u32, + time_out_periodicity: + multicastsetup::v1::McClassBSessionReqPayloadTimeOutPeriodicity { + time_out: self.fuota_deployment.multicast_timeout as u8, + periodicity: self.fuota_deployment.multicast_class_b_ping_slot_nb_k + as u8, + }, + dl_frequ: self.fuota_deployment.multicast_frequency as u32, + dr: self.fuota_deployment.multicast_dr as u8, }, - session_time: (session_start - (session_start % 128)) as u32, - time_out_periodicity: - multicastsetup::v1::McClassBSessionReqPayloadTimeOutPeriodicity { - time_out: self.fuota_deployment.multicast_timeout as u8, - periodicity: self.fuota_deployment.multicast_class_b_ping_slot_nb_k - as u8, + ).to_vec()?, + "C" => multicastsetup::v1::Payload::McClassCSessionReq( + multicastsetup::v1::McClassCSessionReqPayload { + mc_group_id_header: + multicastsetup::v1::McClassCSessionReqPayloadMcGroupIdHeader { + mc_group_id: 0, + }, + session_time: session_start as u32, + session_time_out: + multicastsetup::v1::McClassCSessionReqPayloadSessionTimeOut { + time_out: self.fuota_deployment.multicast_timeout as u8, + }, + dl_frequ: self.fuota_deployment.multicast_frequency as u32, + dr: self.fuota_deployment.multicast_dr as u8, }, - dl_frequ: self.fuota_deployment.multicast_frequency as u32, - dr: self.fuota_deployment.multicast_dr as u8, - }, - ), - "C" => multicastsetup::v1::Payload::McClassCSessionReq( - multicastsetup::v1::McClassCSessionReqPayload { - mc_group_id_header: - multicastsetup::v1::McClassCSessionReqPayloadMcGroupIdHeader { - mc_group_id: 0, - }, - session_time: session_start as u32, - session_time_out: - multicastsetup::v1::McClassCSessionReqPayloadSessionTimeOut { - time_out: self.fuota_deployment.multicast_timeout as u8, - }, - dl_frequ: self.fuota_deployment.multicast_frequency as u32, - dr: self.fuota_deployment.multicast_dr as u8, - }, - ), - _ => { - return Err(anyhow!( - "Unsupported group-type: {}", - self.fuota_deployment.multicast_group_type - )) + ).to_vec()?, + _ => { + return Err(anyhow!( + "Unsupported group-type: {}", + self.fuota_deployment.multicast_group_type + )) + } + } } + Some(Ts005Version::V200) => { + match self.fuota_deployment.multicast_group_type.as_ref() { + "B" => multicastsetup::v2::Payload::McClassBSessionReq( + multicastsetup::v2::McClassBSessionReqPayload { + mc_group_id_header: + multicastsetup::v2::McClassBSessionReqPayloadMcGroupIdHeader { + mc_group_id: 0, + }, + session_time: (session_start - (session_start % 128)) as u32, + time_out_periodicity: + multicastsetup::v2::McClassBSessionReqPayloadTimeOutPeriodicity { + time_out: self.fuota_deployment.multicast_timeout as u8, + periodicity: self.fuota_deployment.multicast_class_b_ping_slot_nb_k + as u8, + }, + dl_frequ: self.fuota_deployment.multicast_frequency as u32, + dr: self.fuota_deployment.multicast_dr as u8, + }, + ).to_vec()?, + "C" => multicastsetup::v2::Payload::McClassCSessionReq( + multicastsetup::v2::McClassCSessionReqPayload { + mc_group_id_header: + multicastsetup::v2::McClassCSessionReqPayloadMcGroupIdHeader { + mc_group_id: 0, + }, + session_time: session_start as u32, + session_time_out: + multicastsetup::v2::McClassCSessionReqPayloadSessionTimeOut { + time_out: self.fuota_deployment.multicast_timeout as u8, + }, + dl_frequ: self.fuota_deployment.multicast_frequency as u32, + dr: self.fuota_deployment.multicast_dr as u8, + }, + ).to_vec()?, + _ => { + return Err(anyhow!( + "Unsupported group-type: {}", + self.fuota_deployment.multicast_group_type + )) + } + } + } + None => return Err(anyhow!("Device-profile does not support TS005")), }; 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?; @@ -459,22 +619,55 @@ impl Flow { let mut payload = self.fuota_deployment.payload.clone(); payload.extend_from_slice(&vec![0; padding]); - let encoded_fragments = fragmentation::v1::encode(&payload, fragment_size, redundancy)?; - - for (i, frag) in encoded_fragments.iter().enumerate() { - let pl = - fragmentation::v1::Payload::DataFragment(fragmentation::v1::DataFragmentPayload { - index_and_n: fragmentation::v1::DataFragmentPayloadIndexAndN { - frag_index: 0, - n: (i + 1) as u16, - }, - data: frag.clone(), - }); + let payloads = match self.device_profile.app_layer_params.ts004_version { + Some(Ts004Version::V100) => { + let mut payloads = Vec::new(); + let encoded_fragments = + fragmentation::v1::encode(&payload, fragment_size, redundancy)?; + for (i, frag) in encoded_fragments.iter().enumerate() { + payloads.push( + fragmentation::v1::Payload::DataFragment( + fragmentation::v1::DataFragmentPayload { + index_and_n: fragmentation::v1::DataFragmentPayloadIndexAndN { + frag_index: 0, + n: (i + 1) as u16, + }, + data: frag.clone(), + }, + ) + .to_vec()?, + ); + } + payloads + } + Some(Ts004Version::V200) => { + let mut payloads = Vec::new(); + let encoded_fragments = + fragmentation::v2::encode(&payload, fragment_size, redundancy)?; + for (i, frag) in encoded_fragments.iter().enumerate() { + payloads.push( + fragmentation::v2::Payload::DataFragment( + fragmentation::v2::DataFragmentPayload { + index_and_n: fragmentation::v2::DataFragmentPayloadIndexAndN { + frag_index: 0, + n: (i + 1) as u16, + }, + data: frag.clone(), + }, + ) + .to_vec()?, + ); + } + payloads + } + None => return Err(anyhow!("Device-profile does not support TS004")), + }; + for pl in payloads { let _ = downlink::multicast::enqueue(multicast::MulticastGroupQueueItem { multicast_group_id: self.fuota_deployment.id, f_port: self.device_profile.app_layer_params.ts004_f_port as i16, - data: pl.to_vec()?, + data: pl, ..Default::default() }) .await?; @@ -524,17 +717,28 @@ impl Flow { } for fuota_dev in &fuota_devices { - let pl = fragmentation::v1::Payload::FragSessionStatusReq( - fragmentation::v1::FragSessionStatusReqPayload { - participants: true, - frag_index: 0, - }, - ); + let pl = match self.device_profile.app_layer_params.ts004_version { + Some(Ts004Version::V100) => fragmentation::v1::Payload::FragSessionStatusReq( + fragmentation::v1::FragSessionStatusReqPayload { + participants: true, + frag_index: 0, + }, + ) + .to_vec()?, + Some(Ts004Version::V200) => fragmentation::v2::Payload::FragSessionStatusReq( + fragmentation::v2::FragSessionStatusReqPayload { + participants: true, + frag_index: 0, + }, + ) + .to_vec()?, + None => return Err(anyhow!("Device-profile does not support TS004")), + }; 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) {