Merge "Update source for Rust 1.78.0" into main
diff --git a/Android.bp b/Android.bp
index 9a95b6f..907f567 100644
--- a/Android.bp
+++ b/Android.bp
@@ -73,11 +73,13 @@
         "libpica",
         "libprotobuf",
         "libprotobuf_json_mapping",
+        "librand",
         "libregex",
         "libserde",
         "libserde_json",
         "libthiserror",
         "libtokio",
+        "libtokio_stream",
         "libtungstenite",
         "liblazy_static",
         "liblog_rust",
@@ -377,7 +379,7 @@
         "libnetsim_common",
         "libnetsim_proto",
         "libprotobuf",
-        "libtracing"
+        "libtracing",
     ],
 }
 
diff --git a/proto/Android.bp b/proto/Android.bp
index 294118a..e23ea9c 100644
--- a/proto/Android.bp
+++ b/proto/Android.bp
@@ -113,6 +113,7 @@
         "netsim/model.proto",
         "netsim/packet_streamer.proto",
         "netsim/startup.proto",
+        "netsim/stats.proto",
         ":rootcanal-protos",
     ],
 }
diff --git a/proto/netsim/stats.proto b/proto/netsim/stats.proto
index 2d372cc..fd8b049 100644
--- a/proto/netsim/stats.proto
+++ b/proto/netsim/stats.proto
@@ -23,6 +23,7 @@
     PARSE_ERROR = 1;
     UNSUPPORTED = 2;
     OTHERS = 3;
+    DELAYED = 4;
   }
   optional Reason reason = 1;
   optional string description = 2;
@@ -56,6 +57,20 @@
   repeated InvalidPacket invalid_packets = 8;
 }
 
+// Frontend statistics for a netsim session representing count of API calls
+message NetsimFrontendStats {
+  optional uint32 get_version = 1;
+  optional uint32 create_device = 2;
+  optional uint32 delete_chip = 3;
+  optional uint32 patch_device = 4;
+  optional uint32 reset = 5;
+  optional uint32 list_device = 6;
+  optional uint32 subscribe_device = 7;
+  optional uint32 patch_capture = 8;
+  optional uint32 list_capture = 9;
+  optional uint32 get_capture = 10;
+}
+
 // Statistics for a netsim session.
 message NetsimStats {
   // The length of the session in seconds
@@ -68,4 +83,6 @@
   repeated NetsimRadioStats radio_stats = 4;
   // The version of netsim daemon
   optional string version = 5;
+  // Frontend API statistics
+  optional NetsimFrontendStats frontend_stats = 6;
 }
\ No newline at end of file
diff --git a/rust/cli/Cargo.toml b/rust/cli/Cargo.toml
index 82b7916..1fa1a3a 100644
--- a/rust/cli/Cargo.toml
+++ b/rust/cli/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "netsim-cli"
-version = "0.3.7"
+version = "0.3.8"
 edition = "2021"
 build = "build.rs"
 
diff --git a/rust/common/Cargo.toml b/rust/common/Cargo.toml
index f3438bf..b118761 100644
--- a/rust/common/Cargo.toml
+++ b/rust/common/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "netsim-common"
-version = "0.3.7"
+version = "0.3.8"
 edition = "2021"
 
 [lib]
diff --git a/rust/daemon/Cargo.toml b/rust/daemon/Cargo.toml
index 1fcd9a8..1aefc7c 100644
--- a/rust/daemon/Cargo.toml
+++ b/rust/daemon/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "netsim-daemon"
-version = "0.3.7"
+version = "0.3.8"
 edition = "2021"
 build = "build.rs"
 
@@ -21,6 +21,8 @@
 protobuf = "3.2.0"
 protobuf-json-mapping = "3.2.0"
 regex = "1.6.0"
+tokio = { verison = "1.32.0", features = ["fs", "io-util", "macros", "net", "rt"] }
+tokio-stream = { version = "0.1.14", features = ["sync"] }
 thiserror = { version = ">=1.0.40"}
 tungstenite = { version = ">=0.19.0", default-features = false }
 lazy_static = "1.4.0"
diff --git a/rust/daemon/src/echip/bluetooth.rs b/rust/daemon/src/echip/bluetooth.rs
index 71d4611..65a9a43 100644
--- a/rust/daemon/src/echip/bluetooth.rs
+++ b/rust/daemon/src/echip/bluetooth.rs
@@ -199,6 +199,35 @@
     ffi_bluetooth::bluetooth_stop();
 }
 
+/// Report Invalid Packet
+pub fn report_invalid_packet(
+    rootcanal_id: RootcanalIdentifier,
+    reason: InvalidPacketReason,
+    description: String,
+    packet: Vec<u8>,
+) {
+    // TODO(b/330726276): spawn task on tokio once context is provided from rust_main
+    let _ = std::thread::Builder::new().name("report_invalid_packet".to_string()).spawn(move || {
+        match BLUETOOTH_INVALID_PACKETS.lock().unwrap().get_mut(&rootcanal_id) {
+            Some(v) => {
+                // Remove the earliest reported packet if length greater than 5
+                if v.len() >= 5 {
+                    v.remove(0);
+                }
+                // append error packet
+                let mut invalid_packet = InvalidPacket::new();
+                invalid_packet.set_reason(reason);
+                invalid_packet.set_description(description.clone());
+                invalid_packet.set_packet(packet.clone());
+                v.push(invalid_packet);
+                // Log the report
+                info!("Invalid Packet for rootcanal_id: {rootcanal_id}, reason: {reason:?}, description: {description:?}, packet: {packet:?}");
+            }
+            None => error!("Bluetooth EmulatedChip not created for rootcanal_id: {rootcanal_id}"),
+        }
+    });
+}
+
 /// (Called by C++) Report Invalid Packet
 pub fn report_invalid_packet_cxx(
     rootcanal_id: RootcanalIdentifier,
@@ -206,23 +235,10 @@
     description: &CxxString,
     packet: &CxxVector<u8>,
 ) {
-    match BLUETOOTH_INVALID_PACKETS.lock().unwrap().get_mut(&rootcanal_id) {
-        Some(v) => {
-            // Remove the earliest reported packet if length greater than 5
-            if v.len() >= 5 {
-                v.remove(0);
-            }
-            // append error packet
-            let mut invalid_packet = InvalidPacket::new();
-            invalid_packet.set_reason(
-                InvalidPacketReason::from_i32(reason).unwrap_or(InvalidPacketReason::UNKNOWN),
-            );
-            invalid_packet.set_description(description.to_string());
-            invalid_packet.set_packet(packet.as_slice().to_vec());
-            v.push(invalid_packet);
-            // Log the report
-            info!("Reported Invalid Packet for Bluetooth EmulatedChip with rootcanal_id: {rootcanal_id}, reason:{reason}, description: {description:?}, packet: {packet:?}");
-        }
-        None => error!("Bluetooth EmulatedChip not created for rootcanal_id: {rootcanal_id}"),
-    }
+    report_invalid_packet(
+        rootcanal_id,
+        InvalidPacketReason::from_i32(reason).unwrap_or(InvalidPacketReason::UNKNOWN),
+        description.to_string(),
+        packet.as_slice().to_vec(),
+    );
 }
diff --git a/rust/daemon/src/echip/mod.rs b/rust/daemon/src/echip/mod.rs
index 222bc63..4e8f955 100644
--- a/rust/daemon/src/echip/mod.rs
+++ b/rust/daemon/src/echip/mod.rs
@@ -27,4 +27,4 @@
 pub use crate::echip::emulated_chip::EmulatedChip;
 pub use crate::echip::emulated_chip::SharedEmulatedChip;
 pub use crate::echip::emulated_chip::{get, new, remove};
-pub use crate::echip::packet::{handle_request, handle_request_cxx, handle_response};
+pub use crate::echip::packet::{handle_request, handle_request_cxx, handle_response_cxx};
diff --git a/rust/daemon/src/echip/packet.rs b/rust/daemon/src/echip/packet.rs
index 16db730..7954c85 100644
--- a/rust/daemon/src/echip/packet.rs
+++ b/rust/daemon/src/echip/packet.rs
@@ -101,7 +101,7 @@
 }
 
 /// Handle requests from gRPC transport in C++.
-pub fn handle_response(chip_id: ChipIdentifier, packet: &cxx::CxxVector<u8>, packet_type: u8) {
+pub fn handle_response_cxx(chip_id: ChipIdentifier, packet: &cxx::CxxVector<u8>, packet_type: u8) {
     // TODO(b/314840701):
     // 1. Per EChip Struct should contain private field of channel & facade_id
     // 2. Lookup from ECHIPS with given chip_id
@@ -123,34 +123,12 @@
 }
 
 // Handle response from rust libraries
-#[cfg(feature = "cuttlefish")]
-pub fn handle_response_rust(chip_id: ChipIdentifier, packet: Bytes) {
+pub fn handle_response(chip_id: ChipIdentifier, packet: &Bytes) {
     let packet_type = PacketType::HCI_PACKET_UNSPECIFIED.value() as u8;
-    captures_handler::handle_packet_response(chip_id, &packet, packet_type.into());
+    captures_handler::handle_packet_response(chip_id, packet, packet_type.into());
 
     let result = if let Some(transport) = MANAGER.transports.read().unwrap().get(&chip_id) {
-        transport.send(ResponsePacket { packet, packet_type })
-    } else {
-        warn!("handle_response: chip {chip_id} not found");
-        Ok(())
-    };
-    // transports lock is now released
-    if let Err(e) = result {
-        warn!("handle_response: error {:?}", e);
-        unregister_transport(chip_id);
-    }
-}
-
-// Send HwsimCmd packets to guest OS.
-pub fn hwsim_cmd_response(chip_id: u32, packet: &[u8]) {
-    let packet_type = PacketType::HCI_PACKET_UNSPECIFIED;
-    captures_handler::handle_packet_response(chip_id, packet, packet_type as u32);
-
-    let result = if let Some(transport) = MANAGER.transports.read().unwrap().get(&chip_id) {
-        transport.send(ResponsePacket {
-            packet: Bytes::from(packet.to_vec()),
-            packet_type: packet_type as u8,
-        })
+        transport.send(ResponsePacket { packet: packet.clone(), packet_type })
     } else {
         warn!("handle_response: chip {chip_id} not found");
         Ok(())
diff --git a/rust/daemon/src/echip/uwb.rs b/rust/daemon/src/echip/uwb.rs
index f7ce279..75d49c7 100644
--- a/rust/daemon/src/echip/uwb.rs
+++ b/rust/daemon/src/echip/uwb.rs
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 use bytes::Bytes;
-use futures::{channel::mpsc::UnboundedSender, sink::SinkExt};
+use futures::{channel::mpsc::UnboundedSender, sink::SinkExt, StreamExt};
 use lazy_static::lazy_static;
 use pica::{Handle, Pica};
 
@@ -22,7 +22,7 @@
 use netsim_proto::stats::{netsim_radio_stats, NetsimRadioStats as ProtoRadioStats};
 
 use crate::devices::chip::ChipIdentifier;
-use crate::echip::packet::handle_response_rust;
+use crate::echip::packet::handle_response;
 use crate::uwb::ranging_estimator::{SharedState, UwbRangingEstimator};
 
 use std::sync::{Arc, Mutex};
@@ -53,7 +53,7 @@
     uci_stream_writer: UnboundedSender<Vec<u8>>,
     state: bool,
     tx_count: i32,
-    rx_count: i32, // TODO(b/330788870): Increment rx_count after handle_response_rust
+    rx_count: i32, // TODO(b/330788870): Increment rx_count after handle_response
 }
 
 impl EmulatedChip for Uwb {
@@ -133,13 +133,13 @@
         rx_count: 0,
     };
 
-    // Thread for obtaining packet from pica and invoking handle_response_rust
-    let _ =
-        thread::Builder::new().name(format!("uwb_packet_response_{chip_id}")).spawn(move || {
-            for packet in futures::executor::block_on_stream(uci_sink_receiver) {
-                handle_response_rust(chip_id, packet.into());
-            }
-        });
+    // Spawn a future for obtaining packet from pica and invoking handle_response_rust
+    PICA_RUNTIME.spawn(async move {
+        let mut uci_sink_receiver = uci_sink_receiver;
+        while let Some(packet) = uci_sink_receiver.next().await {
+            handle_response(chip_id, &Bytes::from(packet));
+        }
+    });
     Arc::new(Box::new(echip))
 }
 
diff --git a/rust/daemon/src/echip/wifi.rs b/rust/daemon/src/echip/wifi.rs
index 7448c75..6df26c1 100644
--- a/rust/daemon/src/echip/wifi.rs
+++ b/rust/daemon/src/echip/wifi.rs
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 use crate::devices::chip::ChipIdentifier;
-use crate::echip::{packet::hwsim_cmd_response, EmulatedChip, SharedEmulatedChip};
+use crate::echip::{packet::handle_response, EmulatedChip, SharedEmulatedChip};
 use crate::ffi::ffi_wifi;
 use crate::wifi::medium::Medium;
 use bytes::Bytes;
@@ -23,6 +23,7 @@
 use netsim_proto::model::Chip as ProtoChip;
 use netsim_proto::stats::{netsim_radio_stats, NetsimRadioStats as ProtoRadioStats};
 use protobuf::{Message, MessageField};
+use std::sync::atomic::Ordering;
 use std::sync::Arc;
 use std::thread;
 use std::time::{Duration, Instant};
@@ -48,7 +49,7 @@
         let (request_sender, rx) = std::sync::mpsc::channel::<(u32, Bytes)>();
 
         thread::spawn(move || {
-            const POLL_INTERVAL: Duration = Duration::from_micros(10);
+            const POLL_INTERVAL: Duration = Duration::from_millis(1);
             let mut next_instant = Instant::now() + POLL_INTERVAL;
 
             loop {
@@ -65,6 +66,7 @@
                             || !WIFI_MANAGER.medium.process(chip_id, &packet)
                         {
                             ffi_wifi::handle_wifi_request(chip_id, &packet.to_vec());
+                            ffi_wifi::libslirp_main_loop_wait();
                         }
                     }
                     _ => {
@@ -76,14 +78,11 @@
         });
 
         let (response_sender, rx) = std::sync::mpsc::channel::<Bytes>();
-        thread::spawn(move || {
-            loop {
-                // rx.recv() should either returns packets or timeout after 1 second.
-                let packet = rx.recv().unwrap();
-                WIFI_MANAGER.medium.process_response(&packet);
-            }
+        thread::spawn(move || loop {
+            let packet = rx.recv().unwrap();
+            WIFI_MANAGER.medium.process_response(&packet);
         });
-        WifiManager { medium: Medium::new(hwsim_cmd_response), request_sender, response_sender }
+        WifiManager { medium: Medium::new(handle_response), request_sender, response_sender }
     }
 }
 
@@ -104,9 +103,9 @@
     fn get(&self) -> ProtoChip {
         let mut chip_proto = ProtoChip::new();
         if let Some(client) = WIFI_MANAGER.medium.get(self.chip_id) {
-            chip_proto.mut_wifi().state = Some(client.enabled);
-            chip_proto.mut_wifi().tx_count = client.tx_count as i32;
-            chip_proto.mut_wifi().rx_count = client.rx_count as i32;
+            chip_proto.mut_wifi().state = Some(client.enabled.load(Ordering::Relaxed));
+            chip_proto.mut_wifi().tx_count = client.tx_count.load(Ordering::Relaxed) as i32;
+            chip_proto.mut_wifi().rx_count = client.rx_count.load(Ordering::Relaxed) as i32;
         }
         chip_proto
     }
diff --git a/rust/daemon/src/ffi.rs b/rust/daemon/src/ffi.rs
index 24dcc5c..c9edd2c 100644
--- a/rust/daemon/src/ffi.rs
+++ b/rust/daemon/src/ffi.rs
@@ -28,7 +28,7 @@
     add_chip_cxx, get_distance_cxx, handle_device_cxx, remove_chip_cxx, AddChipResultCxx,
 };
 use crate::echip::wifi::handle_wifi_response;
-use crate::echip::{bluetooth::report_invalid_packet_cxx, handle_request_cxx, handle_response};
+use crate::echip::{bluetooth::report_invalid_packet_cxx, handle_request_cxx, handle_response_cxx};
 use crate::ranging::*;
 use crate::transport::grpc::{register_grpc_transport, unregister_grpc_transport};
 use crate::version::*;
@@ -41,7 +41,7 @@
         fn handle_request_cxx(chip_id: u32, packet: &CxxVector<u8>, packet_type: u8);
 
         #[cxx_name = HandleResponse]
-        fn handle_response(chip_id: u32, packet: &CxxVector<u8>, packet_type: u8);
+        fn handle_response_cxx(chip_id: u32, packet: &CxxVector<u8>, packet_type: u8);
     }
 }
 
diff --git a/rust/daemon/src/uwb/mod.rs b/rust/daemon/src/uwb/mod.rs
index 7c1a6b2..5787f6e 100644
--- a/rust/daemon/src/uwb/mod.rs
+++ b/rust/daemon/src/uwb/mod.rs
@@ -12,4 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// TODO(b/325298096): Use RangingDataSet in ranging_estimator
+#[allow(dead_code)]
+mod ranging_data;
 pub(crate) mod ranging_estimator;
diff --git a/rust/daemon/src/uwb/ranging_data.rs b/rust/daemon/src/uwb/ranging_data.rs
new file mode 100644
index 0000000..c442d45
--- /dev/null
+++ b/rust/daemon/src/uwb/ranging_data.rs
@@ -0,0 +1,117 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use rand::{prelude::SliceRandom, rngs::ThreadRng, thread_rng};
+
+use std::collections::BTreeMap;
+
+type TrueDistance = f64; // meters
+type EstimatedDistance = f64; // meters
+
+/// The data is organized as a `BTreeMap` for efficient lookup and interpolation.
+struct RangingDataSet {
+    /// Stores ranging data in the form of (true distance, [estimated distances]) pairs.
+    /// The map keys are true distances in u16 centimeters.
+    data: BTreeMap<u16, Vec<EstimatedDistance>>,
+}
+
+impl RangingDataSet {
+    /// Creates a new `RangingDataSet` instance by loading ranging data.
+    /// Data is in a format where each entry is a tuple of
+    /// (true distance, estimated distance), typically representing samples from a
+    /// ranging sensor.
+    pub fn new(ranging_data: Option<Vec<(TrueDistance, EstimatedDistance)>>) -> Self {
+        // Use sample_ranging_data.csv if ranging_data is not provided.
+        let sample_ranging_data: Vec<(TrueDistance, EstimatedDistance)> =
+            ranging_data.unwrap_or(include!("sample_ranging_data.csv"));
+
+        // Process the sample_raning_data into BTreeMap
+        let mut data: BTreeMap<u16, Vec<EstimatedDistance>> = BTreeMap::new();
+        for (true_distance, estimated_distance) in sample_ranging_data {
+            // Convert true_distance into u16 centimeters
+            data.entry((true_distance * 100.0).round() as u16)
+                .or_default()
+                .push(estimated_distance);
+        }
+        RangingDataSet { data }
+    }
+
+    /// Samples an estimated distance for the given true distance.
+    ///
+    /// # Arguments
+    ///
+    /// * `distance` - The true distance for which an estimated distance is required.
+    /// * `option_rng` - An optional random number generator
+    ///     (if not provided, a default one will be used)
+    ///
+    /// # Returns
+    ///
+    /// An estimated distance sampled from the dataset.
+    /// If the exact true distance is found in the dataset,
+    ///     a random estimated distance is chosen from its associated values.
+    /// If the true distance falls between known values,
+    ///     linear interpolation is used to estimate a distance.
+    /// If the true distance is outside the range of known values,
+    ///     the distance itself is returned as the estimated distance.
+    pub fn sample(
+        &self,
+        distance: TrueDistance,
+        option_rng: Option<ThreadRng>,
+    ) -> EstimatedDistance {
+        // Generate a new ThreadRng if not provided
+        let mut rng = option_rng.unwrap_or(thread_rng());
+        // Convert TrueDistance into u16 centimeters.
+        let distance_u16 = (distance * 100.0).round() as u16;
+
+        // Random sampling if distance is an existing data key
+        if let Some(vec_estimated_distance) = self.data.get(&distance_u16) {
+            return *vec_estimated_distance.choose(&mut rng).unwrap();
+        }
+
+        // Linear Interpolation if provided TrueDistance lies in between data keys
+        let lower = self.data.range(..=&distance_u16).next_back();
+        let upper = self.data.range(&distance_u16..).next();
+        match (lower, upper) {
+            (Some((lower_key, lower_vals)), Some((upper_key, upper_vals))) => {
+                let x1 = *lower_key as f64 / 100.0;
+                let y1 = *lower_vals.choose(&mut rng).unwrap();
+                let x2 = *upper_key as f64 / 100.0;
+                let y2 = *upper_vals.choose(&mut rng).unwrap();
+                y1 + (distance - x1) * (y2 - y1) / (x2 - x1)
+            }
+            _ => distance,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn sample_ranging_data_set() -> RangingDataSet {
+        let ranging_data = vec![(0.0, 0.2), (1.0, 0.9), (2.0, 1.9), (2.0, 2.1)];
+        RangingDataSet::new(Some(ranging_data))
+    }
+
+    #[test]
+    fn test_sample_ranging_data_set() {
+        let ranging_data_set = sample_ranging_data_set();
+        // Linear Interpolation
+        assert_eq!(ranging_data_set.sample(0.5, None), 0.55);
+        // Exact distance found in dataset
+        assert!([1.9, 2.1].contains(&ranging_data_set.sample(2.0, None)));
+        // Out of Range
+        assert_eq!(ranging_data_set.sample(3.0, None), 3.0);
+    }
+}
diff --git a/rust/daemon/src/uwb/sample_ranging_data.csv b/rust/daemon/src/uwb/sample_ranging_data.csv
new file mode 100644
index 0000000..c9a9fd9
--- /dev/null
+++ b/rust/daemon/src/uwb/sample_ranging_data.csv
@@ -0,0 +1,83 @@
+vec![

+    (0.5, 0.224999994),

+    (0.5, 0.1679999977),

+    (0.5, 0.2150000036),

+    (0.5, 0.201000005),

+    (0.5, 0.1959999949),

+    (1.0, 0.7450000048),

+    (1.0, 0.7450000048),

+    (1.0, 0.7829999924),

+    (1.0, 0.7450000048),

+    (1.5, 1.271000028),

+    (1.5, 1.340999961),

+    (1.5, 1.261000037),

+    (1.5, 1.274999976),

+    (1.5, 1.274999976),

+    (1.5, 1.312999964),

+    (1.5, 1.332000017),

+    (2.0, 1.866000056),

+    (2.0, 1.889999986),

+    (2.0, 1.866000056),

+    (2.0, 1.899000049),

+    (2.0, 1.894000053),

+    (2.0, 1.889999986),

+    (2.5, 2.302000046),

+    (2.5, 2.292999983),

+    (2.5, 2.279000044),

+    (2.5, 2.292999983),

+    (3.0, 2.832000017),

+    (3.0, 2.822999954),

+    (3.0, 2.809000015),

+    (3.0, 2.869999886),

+    (3.0, 2.828000069),

+    (3.0, 2.818000078),

+    (3.5, 3.40899992),

+    (3.5, 3.41899991),

+    (3.5, 3.40899992),

+    (3.5, 3.40899992),

+    (3.5, 3.404999971),

+    (3.5, 3.41899991),

+    (3.5, 3.41899991),

+    (3.5, 3.41899991),

+    (3.5, 3.40899992),

+    (4.0, 4.024000168),

+    (4.0, 3.990999937),

+    (4.0, 3.986000061),

+    (4.0, 4.0),

+    (4.0, 3.982000113),

+    (4.0, 3.996000051),

+    (4.0, 4.013999939),

+    (4.0, 3.996000051),

+    (4.0, 3.967999935),

+    (4.0, 3.990999937),

+    (4.5, 4.446000099),

+    (4.5, 4.451000214),

+    (4.5, 4.460000038),

+    (4.5, 4.418000221),

+    (4.5, 4.446000099),

+    (4.5, 4.436999798),

+    (4.5, 4.422999859),

+    (4.5, 4.451000214),

+    (4.5, 4.436999798),

+    (5.0, 4.934000015),

+    (5.0, 4.938000202),

+    (5.0, 4.938000202),

+    (5.0, 4.914999962),

+    (5.0, 4.94299984),

+    (5.0, 4.914999962),

+    (5.5, 5.426000118),

+    (5.5, 5.43599987),

+    (6.0, 6.392000198),

+    (6.0, 6.396999836),

+    (6.0, 6.388000011),

+    (6.0, 6.368999958),

+    (6.5, 6.899000168),

+    (6.5, 6.918000221),

+    (6.5, 6.889999866),

+    (6.5, 6.913000107),

+    (6.5, 6.903999805),

+    (6.5, 6.899000168),

+    (6.5, 6.885000229),

+    (6.5, 6.899000168),

+    (6.5, 6.913000107)

+]

diff --git a/rust/daemon/src/version.rs b/rust/daemon/src/version.rs
index af5816b..b8d679a 100644
--- a/rust/daemon/src/version.rs
+++ b/rust/daemon/src/version.rs
@@ -14,7 +14,7 @@
 
 /// Version library.
 
-pub const VERSION: &str = "0.3.7";
+pub const VERSION: &str = "0.3.8";
 
 pub fn get_version() -> String {
     VERSION.to_owned()
diff --git a/rust/daemon/src/wifi/medium.rs b/rust/daemon/src/wifi/medium.rs
index aa8f4fe..256fc7c 100644
--- a/rust/daemon/src/wifi/medium.rs
+++ b/rust/daemon/src/wifi/medium.rs
@@ -17,11 +17,12 @@
 use crate::wifi::frame::Frame;
 use crate::wifi::hwsim_attr_set::HwsimAttrSet;
 use anyhow::{anyhow, Context};
+use bytes::Bytes;
 use log::{debug, info, warn};
 use pdl_runtime::Packet;
 use std::collections::{HashMap, HashSet};
-use std::sync::{Arc, Mutex};
-
+use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
+use std::sync::{Arc, RwLock};
 const NLMSG_MIN_TYPE: u16 = 0x10;
 // Default values for mac80211_hwsim.
 const RX_RATE: u32 = 0;
@@ -59,29 +60,33 @@
 
 #[derive(Clone)]
 pub struct Client {
-    pub enabled: bool,
-    pub tx_count: u32,
-    pub rx_count: u32,
+    pub enabled: Arc<AtomicBool>,
+    pub tx_count: Arc<AtomicU32>,
+    pub rx_count: Arc<AtomicU32>,
 }
 
 impl Client {
     fn new() -> Self {
-        Self { enabled: true, tx_count: 0, rx_count: 0 }
+        Self {
+            enabled: Arc::new(AtomicBool::new(true)),
+            tx_count: Arc::new(AtomicU32::new(0)),
+            rx_count: Arc::new(AtomicU32::new(0)),
+        }
     }
 }
 
 pub struct Medium {
     callback: HwsimCmdCallback,
     // Ieee80211 source address
-    stations: Mutex<HashMap<MacAddress, Arc<Station>>>,
-    clients: Mutex<HashMap<u32, Client>>,
+    stations: RwLock<HashMap<MacAddress, Arc<Station>>>,
+    clients: RwLock<HashMap<u32, Client>>,
     // BSSID. MAC address of the access point in WiFi Service.
     hostapd_bssid: MacAddress,
     // Simulate the re-transmission of frames sent to hostapd
     ap_simulation: bool,
 }
 
-type HwsimCmdCallback = fn(u32, &[u8]);
+type HwsimCmdCallback = fn(u32, &Bytes);
 impl Medium {
     pub fn new(callback: HwsimCmdCallback) -> Medium {
         // Defined in external/qemu/android-qemu2-glue/emulation/WifiService.cpp
@@ -89,51 +94,51 @@
         let bssid_bytes: [u8; 6] = [0x00, 0x13, 0x10, 0x85, 0xfe, 0x01];
         Self {
             callback,
-            stations: Mutex::new(HashMap::new()),
-            clients: Mutex::new(HashMap::new()),
+            stations: RwLock::new(HashMap::new()),
+            clients: RwLock::new(HashMap::new()),
             hostapd_bssid: MacAddress::from(&bssid_bytes),
             ap_simulation: true,
         }
     }
 
     pub fn add(&self, client_id: u32) {
-        let _ = self.clients.lock().unwrap().entry(client_id).or_insert_with(|| {
+        let _ = self.clients.write().unwrap().entry(client_id).or_insert_with(|| {
             info!("Insert client {}", client_id);
             Client::new()
         });
     }
 
     pub fn remove(&self, client_id: u32) {
-        self.stations.lock().unwrap().retain(|_, s| s.client_id != client_id);
-        self.clients.lock().unwrap().remove(&client_id);
+        self.stations.write().unwrap().retain(|_, s| s.client_id != client_id);
+        self.clients.write().unwrap().remove(&client_id);
     }
 
     pub fn reset(&self, client_id: u32) {
-        if let Some(client) = self.clients.lock().unwrap().get_mut(&client_id) {
-            client.enabled = true;
-            client.tx_count = 0;
-            client.rx_count = 0;
+        if let Some(client) = self.clients.read().unwrap().get(&client_id) {
+            client.enabled.store(true, Ordering::Relaxed);
+            client.tx_count.store(0, Ordering::Relaxed);
+            client.rx_count.store(0, Ordering::Relaxed);
         }
     }
 
     pub fn get(&self, client_id: u32) -> Option<Client> {
-        self.clients.lock().unwrap().get(&client_id).map(|c| c.to_owned())
+        self.clients.read().unwrap().get(&client_id).map(|c| c.to_owned())
     }
 
     fn contains_client(&self, client_id: u32) -> bool {
-        self.clients.lock().unwrap().contains_key(&client_id)
+        self.clients.read().unwrap().contains_key(&client_id)
     }
 
     fn stations(&self) -> impl Iterator<Item = Arc<Station>> {
-        self.stations.lock().unwrap().clone().into_values()
+        self.stations.read().unwrap().clone().into_values()
     }
 
     fn contains_station(&self, addr: &MacAddress) -> bool {
-        self.stations.lock().unwrap().contains_key(addr)
+        self.stations.read().unwrap().contains_key(addr)
     }
 
     fn get_station(&self, addr: &MacAddress) -> anyhow::Result<Arc<Station>> {
-        self.stations.lock().unwrap().get(addr).context("get station").cloned()
+        self.stations.read().unwrap().get(addr).context("get station").cloned()
     }
 
     /// Process commands from the kernel's mac80211_hwsim subsystem.
@@ -148,7 +153,7 @@
     ///
     /// * 802.11 multicast frames are re-broadcast to connected stations.
     ///
-    pub fn process(&self, client_id: u32, packet: &[u8]) -> bool {
+    pub fn process(&self, client_id: u32, packet: &Bytes) -> bool {
         self.process_internal(client_id, packet).unwrap_or_else(move |e| {
             // TODO: add this error to the netsim_session_stats
             warn!("error processing wifi {e}");
@@ -156,7 +161,7 @@
         })
     }
 
-    fn process_internal(&self, client_id: u32, packet: &[u8]) -> anyhow::Result<bool> {
+    fn process_internal(&self, client_id: u32, packet: &Bytes) -> anyhow::Result<bool> {
         let hwsim_msg = HwsimMsg::parse(packet)?;
 
         // The virtio handler only accepts HWSIM_CMD_FRAME, HWSIM_CMD_TX_INFO_FRAME and HWSIM_CMD_REPORT_PMSR
@@ -186,7 +191,7 @@
                 // new networks. This block associates the new mac with the station.
                 let source = self
                     .stations
-                    .lock()
+                    .write()
                     .unwrap()
                     .entry(src_addr)
                     .or_insert_with(|| {
@@ -213,17 +218,17 @@
 
     /// Handle Wi-Fi MwsimMsg from libslirp and hostapd.
     /// Send it to clients.
-    pub fn process_response(&self, packet: &[u8]) {
+    pub fn process_response(&self, packet: &Bytes) {
         if let Err(e) = self.send_response(packet) {
             warn!("{}", e);
         }
     }
 
     /// Determine the client id based on Ieee80211 destination and send to client.
-    fn send_response(&self, packet: &[u8]) -> anyhow::Result<()> {
+    fn send_response(&self, packet: &Bytes) -> anyhow::Result<()> {
         // When Wi-Fi P2P is disabled, send all packets from WifiService to all clients.
         if crate::config::get_disable_wifi_p2p() {
-            for client_id in self.clients.lock().unwrap().keys() {
+            for client_id in self.clients.read().unwrap().keys() {
                 (self.callback)(*client_id, packet);
             }
             return Ok(());
@@ -240,7 +245,7 @@
         Ok(())
     }
 
-    fn send_frame_response(&self, packet: &[u8], hwsim_msg: &HwsimMsg) -> anyhow::Result<()> {
+    fn send_frame_response(&self, packet: &Bytes, hwsim_msg: &HwsimMsg) -> anyhow::Result<()> {
         let frame = Frame::parse(hwsim_msg)?;
         let dest_addr = frame.ieee80211.get_destination();
         if let Ok(destination) = self.get_station(&dest_addr) {
@@ -258,7 +263,7 @@
     /// Send frame from DS to STA.
     fn send_from_ds_frame(
         &self,
-        packet: &[u8],
+        packet: &Bytes,
         frame: &Frame,
         destination: &Station,
     ) -> anyhow::Result<()> {
@@ -269,13 +274,13 @@
             let hwsim_msg = self
                 .create_hwsim_msg(frame, &destination.hwsim_addr)
                 .context("Create HwsimMsg from WifiService")?;
-            (self.callback)(destination.client_id, &hwsim_msg.to_vec());
+            (self.callback)(destination.client_id, &hwsim_msg.to_vec().into());
         }
         self.incr_rx(destination.client_id)?;
         Ok(())
     }
 
-    fn send_tx_info_response(&self, packet: &[u8], hwsim_msg: &HwsimMsg) -> anyhow::Result<()> {
+    fn send_tx_info_response(&self, packet: &Bytes, hwsim_msg: &HwsimMsg) -> anyhow::Result<()> {
         let attrs = HwsimAttrSet::parse(hwsim_msg.get_attributes()).context("HwsimAttrSet")?;
         let hwsim_addr = attrs.transmitter.context("missing transmitter")?;
         let client_ids = self
@@ -295,36 +300,49 @@
     }
 
     pub fn set_enabled(&self, client_id: u32, enabled: bool) {
-        if let Some(client) = self.clients.lock().unwrap().get_mut(&client_id) {
-            client.enabled = enabled;
+        if let Some(client) = self.clients.read().unwrap().get(&client_id) {
+            client.enabled.store(enabled, Ordering::Relaxed);
         }
     }
 
     fn enabled(&self, client_id: u32) -> anyhow::Result<bool> {
         Ok(self
             .clients
-            .lock()
+            .read()
             .unwrap()
             .get(&client_id)
             .context(format!("client {client_id} is missing"))?
-            .enabled)
+            .enabled
+            .load(Ordering::Relaxed))
     }
 
     /// Create tx info frame to station to ack HwsimMsg.
     fn send_tx_info_frame(&self, frame: &Frame) -> anyhow::Result<()> {
         let client_id = self.get_station(&frame.ieee80211.get_source())?.client_id;
         let hwsim_msg_tx_info = build_tx_info(&frame.hwsim_msg).unwrap().to_vec();
-        (self.callback)(client_id, &hwsim_msg_tx_info);
+        (self.callback)(client_id, &hwsim_msg_tx_info.into());
         Ok(())
     }
 
     fn incr_tx(&self, client_id: u32) -> anyhow::Result<()> {
-        self.clients.lock().unwrap().get_mut(&client_id).context("incr_tx")?.tx_count += 1;
+        self.clients
+            .read()
+            .unwrap()
+            .get(&client_id)
+            .context("incr_tx")?
+            .tx_count
+            .fetch_add(1, Ordering::Relaxed);
         Ok(())
     }
 
     fn incr_rx(&self, client_id: u32) -> anyhow::Result<()> {
-        self.clients.lock().unwrap().get_mut(&client_id).context("incr_rx")?.rx_count += 1;
+        self.clients
+            .read()
+            .unwrap()
+            .get(&client_id)
+            .context("incr_rx")?
+            .rx_count
+            .fetch_add(1, Ordering::Relaxed);
         Ok(())
     }
 
@@ -342,7 +360,7 @@
             if let Some(packet) = self.create_hwsim_msg(frame, &destination.hwsim_addr) {
                 self.incr_tx(source.client_id)?;
                 self.incr_rx(destination.client_id)?;
-                (self.callback)(destination.client_id, &packet.clone().to_vec());
+                (self.callback)(destination.client_id, &packet.into());
                 log_hwsim_msg(frame, source.client_id, destination.client_id);
             }
         }
@@ -368,20 +386,20 @@
             let destination = self.get_station(&dest_addr)?;
             self.send_from_sta_frame(&frame, source, &destination)?;
             Ok(true)
-        } else if dest_addr.is_broadcast() {
-            debug!("Frame broadcast {}", frame.ieee80211);
-            self.broadcast_from_sta_frame(&frame, source)?;
-            // Stations send Probe Request management frame to the broadcast address to scan network actively.
-            // Pass to WiFiService so hostapd will send Probe Response for AndroidWiFi.
-            // TODO: Only pass necessary packets to hostapd.
-            Ok(false)
         } else if dest_addr.is_multicast() {
             debug!("Frame multicast {}", frame.ieee80211);
             self.send_tx_info_frame(&frame)?;
             self.broadcast_from_sta_frame(&frame, source)?;
-            Ok(true)
+            // Forward multicast packets to WifiService:
+            // 1. Stations send probe Request management frame scan network actively,
+            //    so hostapd will send Probe Response for AndroidWiFi.
+            // 2. DNS packets.
+            // TODO: Only pass necessary packets to hostapd and libslirp. (e.g.: Don't forward mDNS packet.)
+            self.incr_tx(source.client_id)?;
+            Ok(false)
         } else {
             // pass to libslirp
+            self.incr_tx(source.client_id)?;
             Ok(false)
         }
     }
@@ -529,7 +547,7 @@
         let callback: HwsimCmdCallback = |_, _| {};
         let medium = Medium {
             callback,
-            stations: Mutex::new(HashMap::from([
+            stations: RwLock::new(HashMap::from([
                 (addr, Arc::new(Station { client_id: test_client_id, addr, hwsim_addr })),
                 (
                     other_addr,
@@ -540,7 +558,7 @@
                     }),
                 ),
             ])),
-            clients: Mutex::new(HashMap::from([
+            clients: RwLock::new(HashMap::from([
                 (test_client_id, Client::new()),
                 (other_client_id, Client::new()),
             ])),
@@ -604,7 +622,7 @@
         assert_eq!(hwsim_msg_tx_info.get_hwsim_hdr().hwsim_cmd, HwsimCmd::TxInfoFrame);
     }
 
-    fn build_tx_info_and_compare(frame_bytes: &[u8], tx_info_expected_bytes: &[u8]) {
+    fn build_tx_info_and_compare(frame_bytes: &Bytes, tx_info_expected_bytes: &Bytes) {
         let frame = HwsimMsg::parse(frame_bytes).unwrap();
         let tx_info = build_tx_info(&frame).unwrap();
 
@@ -627,17 +645,17 @@
 
     #[test]
     fn test_build_tx_info_and_compare() {
-        let frame_bytes: Vec<u8> = include!("test_packets/hwsim_cmd_frame_request.csv");
-        let tx_info_expected_bytes: Vec<u8> =
-            include!("test_packets/hwsim_cmd_tx_info_response.csv");
+        let frame_bytes = Bytes::from(include!("test_packets/hwsim_cmd_frame_request.csv"));
+        let tx_info_expected_bytes =
+            Bytes::from(include!("test_packets/hwsim_cmd_tx_info_response.csv"));
         build_tx_info_and_compare(&frame_bytes, &tx_info_expected_bytes);
     }
 
     #[test]
     fn test_build_tx_info_and_compare_mdns() {
-        let frame_bytes: Vec<u8> = include!("test_packets/hwsim_cmd_frame_request_mdns.csv");
-        let tx_info_expected_bytes: Vec<u8> =
-            include!("test_packets/hwsim_cmd_tx_info_response_mdns.csv");
+        let frame_bytes = Bytes::from(include!("test_packets/hwsim_cmd_frame_request_mdns.csv"));
+        let tx_info_expected_bytes =
+            Bytes::from(include!("test_packets/hwsim_cmd_tx_info_response_mdns.csv"));
         build_tx_info_and_compare(&frame_bytes, &tx_info_expected_bytes);
     }
 }
diff --git a/rust/proto/src/stats.rs b/rust/proto/src/stats.rs
index a3613ca..90a3c93 100644
--- a/rust/proto/src/stats.rs
+++ b/rust/proto/src/stats.rs
@@ -290,6 +290,8 @@
         UNSUPPORTED = 2,
         // @@protoc_insertion_point(enum_value:netsim.stats.InvalidPacket.Reason.OTHERS)
         OTHERS = 3,
+        // @@protoc_insertion_point(enum_value:netsim.stats.InvalidPacket.Reason.DELAYED)
+        DELAYED = 4,
     }
 
     impl ::protobuf::Enum for Reason {
@@ -305,6 +307,7 @@
                 1 => ::std::option::Option::Some(Reason::PARSE_ERROR),
                 2 => ::std::option::Option::Some(Reason::UNSUPPORTED),
                 3 => ::std::option::Option::Some(Reason::OTHERS),
+                4 => ::std::option::Option::Some(Reason::DELAYED),
                 _ => ::std::option::Option::None
             }
         }
@@ -314,6 +317,7 @@
             Reason::PARSE_ERROR,
             Reason::UNSUPPORTED,
             Reason::OTHERS,
+            Reason::DELAYED,
         ];
     }
 
@@ -805,6 +809,480 @@
 }
 
 #[derive(PartialEq,Clone,Default,Debug)]
+// @@protoc_insertion_point(message:netsim.stats.NetsimFrontendStats)
+pub struct NetsimFrontendStats {
+    // message fields
+    // @@protoc_insertion_point(field:netsim.stats.NetsimFrontendStats.get_version)
+    pub get_version: ::std::option::Option<u32>,
+    // @@protoc_insertion_point(field:netsim.stats.NetsimFrontendStats.create_device)
+    pub create_device: ::std::option::Option<u32>,
+    // @@protoc_insertion_point(field:netsim.stats.NetsimFrontendStats.delete_chip)
+    pub delete_chip: ::std::option::Option<u32>,
+    // @@protoc_insertion_point(field:netsim.stats.NetsimFrontendStats.patch_device)
+    pub patch_device: ::std::option::Option<u32>,
+    // @@protoc_insertion_point(field:netsim.stats.NetsimFrontendStats.reset)
+    pub reset: ::std::option::Option<u32>,
+    // @@protoc_insertion_point(field:netsim.stats.NetsimFrontendStats.list_device)
+    pub list_device: ::std::option::Option<u32>,
+    // @@protoc_insertion_point(field:netsim.stats.NetsimFrontendStats.subscribe_device)
+    pub subscribe_device: ::std::option::Option<u32>,
+    // @@protoc_insertion_point(field:netsim.stats.NetsimFrontendStats.patch_capture)
+    pub patch_capture: ::std::option::Option<u32>,
+    // @@protoc_insertion_point(field:netsim.stats.NetsimFrontendStats.list_capture)
+    pub list_capture: ::std::option::Option<u32>,
+    // @@protoc_insertion_point(field:netsim.stats.NetsimFrontendStats.get_capture)
+    pub get_capture: ::std::option::Option<u32>,
+    // special fields
+    // @@protoc_insertion_point(special_field:netsim.stats.NetsimFrontendStats.special_fields)
+    pub special_fields: ::protobuf::SpecialFields,
+}
+
+impl<'a> ::std::default::Default for &'a NetsimFrontendStats {
+    fn default() -> &'a NetsimFrontendStats {
+        <NetsimFrontendStats as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl NetsimFrontendStats {
+    pub fn new() -> NetsimFrontendStats {
+        ::std::default::Default::default()
+    }
+
+    // optional uint32 get_version = 1;
+
+    pub fn get_version(&self) -> u32 {
+        self.get_version.unwrap_or(0)
+    }
+
+    pub fn clear_get_version(&mut self) {
+        self.get_version = ::std::option::Option::None;
+    }
+
+    pub fn has_get_version(&self) -> bool {
+        self.get_version.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_get_version(&mut self, v: u32) {
+        self.get_version = ::std::option::Option::Some(v);
+    }
+
+    // optional uint32 create_device = 2;
+
+    pub fn create_device(&self) -> u32 {
+        self.create_device.unwrap_or(0)
+    }
+
+    pub fn clear_create_device(&mut self) {
+        self.create_device = ::std::option::Option::None;
+    }
+
+    pub fn has_create_device(&self) -> bool {
+        self.create_device.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_create_device(&mut self, v: u32) {
+        self.create_device = ::std::option::Option::Some(v);
+    }
+
+    // optional uint32 delete_chip = 3;
+
+    pub fn delete_chip(&self) -> u32 {
+        self.delete_chip.unwrap_or(0)
+    }
+
+    pub fn clear_delete_chip(&mut self) {
+        self.delete_chip = ::std::option::Option::None;
+    }
+
+    pub fn has_delete_chip(&self) -> bool {
+        self.delete_chip.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_delete_chip(&mut self, v: u32) {
+        self.delete_chip = ::std::option::Option::Some(v);
+    }
+
+    // optional uint32 patch_device = 4;
+
+    pub fn patch_device(&self) -> u32 {
+        self.patch_device.unwrap_or(0)
+    }
+
+    pub fn clear_patch_device(&mut self) {
+        self.patch_device = ::std::option::Option::None;
+    }
+
+    pub fn has_patch_device(&self) -> bool {
+        self.patch_device.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_patch_device(&mut self, v: u32) {
+        self.patch_device = ::std::option::Option::Some(v);
+    }
+
+    // optional uint32 reset = 5;
+
+    pub fn reset(&self) -> u32 {
+        self.reset.unwrap_or(0)
+    }
+
+    pub fn clear_reset(&mut self) {
+        self.reset = ::std::option::Option::None;
+    }
+
+    pub fn has_reset(&self) -> bool {
+        self.reset.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_reset(&mut self, v: u32) {
+        self.reset = ::std::option::Option::Some(v);
+    }
+
+    // optional uint32 list_device = 6;
+
+    pub fn list_device(&self) -> u32 {
+        self.list_device.unwrap_or(0)
+    }
+
+    pub fn clear_list_device(&mut self) {
+        self.list_device = ::std::option::Option::None;
+    }
+
+    pub fn has_list_device(&self) -> bool {
+        self.list_device.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_list_device(&mut self, v: u32) {
+        self.list_device = ::std::option::Option::Some(v);
+    }
+
+    // optional uint32 subscribe_device = 7;
+
+    pub fn subscribe_device(&self) -> u32 {
+        self.subscribe_device.unwrap_or(0)
+    }
+
+    pub fn clear_subscribe_device(&mut self) {
+        self.subscribe_device = ::std::option::Option::None;
+    }
+
+    pub fn has_subscribe_device(&self) -> bool {
+        self.subscribe_device.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_subscribe_device(&mut self, v: u32) {
+        self.subscribe_device = ::std::option::Option::Some(v);
+    }
+
+    // optional uint32 patch_capture = 8;
+
+    pub fn patch_capture(&self) -> u32 {
+        self.patch_capture.unwrap_or(0)
+    }
+
+    pub fn clear_patch_capture(&mut self) {
+        self.patch_capture = ::std::option::Option::None;
+    }
+
+    pub fn has_patch_capture(&self) -> bool {
+        self.patch_capture.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_patch_capture(&mut self, v: u32) {
+        self.patch_capture = ::std::option::Option::Some(v);
+    }
+
+    // optional uint32 list_capture = 9;
+
+    pub fn list_capture(&self) -> u32 {
+        self.list_capture.unwrap_or(0)
+    }
+
+    pub fn clear_list_capture(&mut self) {
+        self.list_capture = ::std::option::Option::None;
+    }
+
+    pub fn has_list_capture(&self) -> bool {
+        self.list_capture.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_list_capture(&mut self, v: u32) {
+        self.list_capture = ::std::option::Option::Some(v);
+    }
+
+    // optional uint32 get_capture = 10;
+
+    pub fn get_capture(&self) -> u32 {
+        self.get_capture.unwrap_or(0)
+    }
+
+    pub fn clear_get_capture(&mut self) {
+        self.get_capture = ::std::option::Option::None;
+    }
+
+    pub fn has_get_capture(&self) -> bool {
+        self.get_capture.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_get_capture(&mut self, v: u32) {
+        self.get_capture = ::std::option::Option::Some(v);
+    }
+
+    fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
+        let mut fields = ::std::vec::Vec::with_capacity(10);
+        let mut oneofs = ::std::vec::Vec::with_capacity(0);
+        fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+            "get_version",
+            |m: &NetsimFrontendStats| { &m.get_version },
+            |m: &mut NetsimFrontendStats| { &mut m.get_version },
+        ));
+        fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+            "create_device",
+            |m: &NetsimFrontendStats| { &m.create_device },
+            |m: &mut NetsimFrontendStats| { &mut m.create_device },
+        ));
+        fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+            "delete_chip",
+            |m: &NetsimFrontendStats| { &m.delete_chip },
+            |m: &mut NetsimFrontendStats| { &mut m.delete_chip },
+        ));
+        fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+            "patch_device",
+            |m: &NetsimFrontendStats| { &m.patch_device },
+            |m: &mut NetsimFrontendStats| { &mut m.patch_device },
+        ));
+        fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+            "reset",
+            |m: &NetsimFrontendStats| { &m.reset },
+            |m: &mut NetsimFrontendStats| { &mut m.reset },
+        ));
+        fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+            "list_device",
+            |m: &NetsimFrontendStats| { &m.list_device },
+            |m: &mut NetsimFrontendStats| { &mut m.list_device },
+        ));
+        fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+            "subscribe_device",
+            |m: &NetsimFrontendStats| { &m.subscribe_device },
+            |m: &mut NetsimFrontendStats| { &mut m.subscribe_device },
+        ));
+        fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+            "patch_capture",
+            |m: &NetsimFrontendStats| { &m.patch_capture },
+            |m: &mut NetsimFrontendStats| { &mut m.patch_capture },
+        ));
+        fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+            "list_capture",
+            |m: &NetsimFrontendStats| { &m.list_capture },
+            |m: &mut NetsimFrontendStats| { &mut m.list_capture },
+        ));
+        fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+            "get_capture",
+            |m: &NetsimFrontendStats| { &m.get_capture },
+            |m: &mut NetsimFrontendStats| { &mut m.get_capture },
+        ));
+        ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<NetsimFrontendStats>(
+            "NetsimFrontendStats",
+            fields,
+            oneofs,
+        )
+    }
+}
+
+impl ::protobuf::Message for NetsimFrontendStats {
+    const NAME: &'static str = "NetsimFrontendStats";
+
+    fn is_initialized(&self) -> bool {
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> {
+        while let Some(tag) = is.read_raw_tag_or_eof()? {
+            match tag {
+                8 => {
+                    self.get_version = ::std::option::Option::Some(is.read_uint32()?);
+                },
+                16 => {
+                    self.create_device = ::std::option::Option::Some(is.read_uint32()?);
+                },
+                24 => {
+                    self.delete_chip = ::std::option::Option::Some(is.read_uint32()?);
+                },
+                32 => {
+                    self.patch_device = ::std::option::Option::Some(is.read_uint32()?);
+                },
+                40 => {
+                    self.reset = ::std::option::Option::Some(is.read_uint32()?);
+                },
+                48 => {
+                    self.list_device = ::std::option::Option::Some(is.read_uint32()?);
+                },
+                56 => {
+                    self.subscribe_device = ::std::option::Option::Some(is.read_uint32()?);
+                },
+                64 => {
+                    self.patch_capture = ::std::option::Option::Some(is.read_uint32()?);
+                },
+                72 => {
+                    self.list_capture = ::std::option::Option::Some(is.read_uint32()?);
+                },
+                80 => {
+                    self.get_capture = ::std::option::Option::Some(is.read_uint32()?);
+                },
+                tag => {
+                    ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u64 {
+        let mut my_size = 0;
+        if let Some(v) = self.get_version {
+            my_size += ::protobuf::rt::uint32_size(1, v);
+        }
+        if let Some(v) = self.create_device {
+            my_size += ::protobuf::rt::uint32_size(2, v);
+        }
+        if let Some(v) = self.delete_chip {
+            my_size += ::protobuf::rt::uint32_size(3, v);
+        }
+        if let Some(v) = self.patch_device {
+            my_size += ::protobuf::rt::uint32_size(4, v);
+        }
+        if let Some(v) = self.reset {
+            my_size += ::protobuf::rt::uint32_size(5, v);
+        }
+        if let Some(v) = self.list_device {
+            my_size += ::protobuf::rt::uint32_size(6, v);
+        }
+        if let Some(v) = self.subscribe_device {
+            my_size += ::protobuf::rt::uint32_size(7, v);
+        }
+        if let Some(v) = self.patch_capture {
+            my_size += ::protobuf::rt::uint32_size(8, v);
+        }
+        if let Some(v) = self.list_capture {
+            my_size += ::protobuf::rt::uint32_size(9, v);
+        }
+        if let Some(v) = self.get_capture {
+            my_size += ::protobuf::rt::uint32_size(10, v);
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
+        self.special_fields.cached_size().set(my_size as u32);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> {
+        if let Some(v) = self.get_version {
+            os.write_uint32(1, v)?;
+        }
+        if let Some(v) = self.create_device {
+            os.write_uint32(2, v)?;
+        }
+        if let Some(v) = self.delete_chip {
+            os.write_uint32(3, v)?;
+        }
+        if let Some(v) = self.patch_device {
+            os.write_uint32(4, v)?;
+        }
+        if let Some(v) = self.reset {
+            os.write_uint32(5, v)?;
+        }
+        if let Some(v) = self.list_device {
+            os.write_uint32(6, v)?;
+        }
+        if let Some(v) = self.subscribe_device {
+            os.write_uint32(7, v)?;
+        }
+        if let Some(v) = self.patch_capture {
+            os.write_uint32(8, v)?;
+        }
+        if let Some(v) = self.list_capture {
+            os.write_uint32(9, v)?;
+        }
+        if let Some(v) = self.get_capture {
+            os.write_uint32(10, v)?;
+        }
+        os.write_unknown_fields(self.special_fields.unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn special_fields(&self) -> &::protobuf::SpecialFields {
+        &self.special_fields
+    }
+
+    fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields {
+        &mut self.special_fields
+    }
+
+    fn new() -> NetsimFrontendStats {
+        NetsimFrontendStats::new()
+    }
+
+    fn clear(&mut self) {
+        self.get_version = ::std::option::Option::None;
+        self.create_device = ::std::option::Option::None;
+        self.delete_chip = ::std::option::Option::None;
+        self.patch_device = ::std::option::Option::None;
+        self.reset = ::std::option::Option::None;
+        self.list_device = ::std::option::Option::None;
+        self.subscribe_device = ::std::option::Option::None;
+        self.patch_capture = ::std::option::Option::None;
+        self.list_capture = ::std::option::Option::None;
+        self.get_capture = ::std::option::Option::None;
+        self.special_fields.clear();
+    }
+
+    fn default_instance() -> &'static NetsimFrontendStats {
+        static instance: NetsimFrontendStats = NetsimFrontendStats {
+            get_version: ::std::option::Option::None,
+            create_device: ::std::option::Option::None,
+            delete_chip: ::std::option::Option::None,
+            patch_device: ::std::option::Option::None,
+            reset: ::std::option::Option::None,
+            list_device: ::std::option::Option::None,
+            subscribe_device: ::std::option::Option::None,
+            patch_capture: ::std::option::Option::None,
+            list_capture: ::std::option::Option::None,
+            get_capture: ::std::option::Option::None,
+            special_fields: ::protobuf::SpecialFields::new(),
+        };
+        &instance
+    }
+}
+
+impl ::protobuf::MessageFull for NetsimFrontendStats {
+    fn descriptor() -> ::protobuf::reflect::MessageDescriptor {
+        static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new();
+        descriptor.get(|| file_descriptor().message_by_package_relative_name("NetsimFrontendStats").unwrap()).clone()
+    }
+}
+
+impl ::std::fmt::Display for NetsimFrontendStats {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for NetsimFrontendStats {
+    type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage<Self>;
+}
+
+#[derive(PartialEq,Clone,Default,Debug)]
 // @@protoc_insertion_point(message:netsim.stats.NetsimStats)
 pub struct NetsimStats {
     // message fields
@@ -818,6 +1296,8 @@
     pub radio_stats: ::std::vec::Vec<NetsimRadioStats>,
     // @@protoc_insertion_point(field:netsim.stats.NetsimStats.version)
     pub version: ::std::option::Option<::std::string::String>,
+    // @@protoc_insertion_point(field:netsim.stats.NetsimStats.frontend_stats)
+    pub frontend_stats: ::protobuf::MessageField<NetsimFrontendStats>,
     // special fields
     // @@protoc_insertion_point(special_field:netsim.stats.NetsimStats.special_fields)
     pub special_fields: ::protobuf::SpecialFields,
@@ -928,7 +1408,7 @@
     }
 
     fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
-        let mut fields = ::std::vec::Vec::with_capacity(5);
+        let mut fields = ::std::vec::Vec::with_capacity(6);
         let mut oneofs = ::std::vec::Vec::with_capacity(0);
         fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
             "duration_secs",
@@ -955,6 +1435,11 @@
             |m: &NetsimStats| { &m.version },
             |m: &mut NetsimStats| { &mut m.version },
         ));
+        fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, NetsimFrontendStats>(
+            "frontend_stats",
+            |m: &NetsimStats| { &m.frontend_stats },
+            |m: &mut NetsimStats| { &mut m.frontend_stats },
+        ));
         ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<NetsimStats>(
             "NetsimStats",
             fields,
@@ -988,6 +1473,9 @@
                 42 => {
                     self.version = ::std::option::Option::Some(is.read_string()?);
                 },
+                50 => {
+                    ::protobuf::rt::read_singular_message_into_field(is, &mut self.frontend_stats)?;
+                },
                 tag => {
                     ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
                 },
@@ -1016,6 +1504,10 @@
         if let Some(v) = self.version.as_ref() {
             my_size += ::protobuf::rt::string_size(5, &v);
         }
+        if let Some(v) = self.frontend_stats.as_ref() {
+            let len = v.compute_size();
+            my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len;
+        }
         my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
         self.special_fields.cached_size().set(my_size as u32);
         my_size
@@ -1037,6 +1529,9 @@
         if let Some(v) = self.version.as_ref() {
             os.write_string(5, v)?;
         }
+        if let Some(v) = self.frontend_stats.as_ref() {
+            ::protobuf::rt::write_message_field_with_cached_size(6, v, os)?;
+        }
         os.write_unknown_fields(self.special_fields.unknown_fields())?;
         ::std::result::Result::Ok(())
     }
@@ -1059,6 +1554,7 @@
         self.peak_concurrent_devices = ::std::option::Option::None;
         self.radio_stats.clear();
         self.version = ::std::option::Option::None;
+        self.frontend_stats.clear();
         self.special_fields.clear();
     }
 
@@ -1069,6 +1565,7 @@
             peak_concurrent_devices: ::std::option::Option::None,
             radio_stats: ::std::vec::Vec::new(),
             version: ::std::option::Option::None,
+            frontend_stats: ::protobuf::MessageField::none(),
             special_fields: ::protobuf::SpecialFields::new(),
         };
         &instance
@@ -1093,28 +1590,39 @@
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x12netsim/stats.proto\x12\x0cnetsim.stats\"\xca\x01\n\rInvalidPacket\
+    \n\x12netsim/stats.proto\x12\x0cnetsim.stats\"\xd7\x01\n\rInvalidPacket\
     \x12:\n\x06reason\x18\x01\x20\x01(\x0e2\".netsim.stats.InvalidPacket.Rea\
     sonR\x06reason\x12\x20\n\x0bdescription\x18\x02\x20\x01(\tR\x0bdescripti\
-    on\x12\x16\n\x06packet\x18\x03\x20\x01(\x0cR\x06packet\"C\n\x06Reason\
+    on\x12\x16\n\x06packet\x18\x03\x20\x01(\x0cR\x06packet\"P\n\x06Reason\
     \x12\x0b\n\x07UNKNOWN\x10\0\x12\x0f\n\x0bPARSE_ERROR\x10\x01\x12\x0f\n\
-    \x0bUNSUPPORTED\x10\x02\x12\n\n\x06OTHERS\x10\x03\"\xb5\x03\n\x10NetsimR\
-    adioStats\x12\x1b\n\tdevice_id\x18\x01\x20\x01(\rR\x08deviceId\x127\n\
-    \x04kind\x18\x02\x20\x01(\x0e2#.netsim.stats.NetsimRadioStats.KindR\x04k\
-    ind\x12#\n\rduration_secs\x18\x03\x20\x01(\x04R\x0cdurationSecs\x12\x19\
-    \n\x08tx_count\x18\x04\x20\x01(\x05R\x07txCount\x12\x19\n\x08rx_count\
-    \x18\x05\x20\x01(\x05R\x07rxCount\x12\x19\n\x08tx_bytes\x18\x06\x20\x01(\
-    \x05R\x07txBytes\x12\x19\n\x08rx_bytes\x18\x07\x20\x01(\x05R\x07rxBytes\
-    \x12D\n\x0finvalid_packets\x18\x08\x20\x03(\x0b2\x1b.netsim.stats.Invali\
-    dPacketR\x0einvalidPackets\"t\n\x04Kind\x12\x0f\n\x0bUNSPECIFIED\x10\0\
-    \x12\x18\n\x14BLUETOOTH_LOW_ENERGY\x10\x01\x12\x15\n\x11BLUETOOTH_CLASSI\
-    C\x10\x02\x12\x0e\n\nBLE_BEACON\x10\x03\x12\x08\n\x04WIFI\x10\x04\x12\
-    \x07\n\x03UWB\x10\x05\x12\x07\n\x03NFC\x10\x06\"\xe8\x01\n\x0bNetsimStat\
-    s\x12#\n\rduration_secs\x18\x01\x20\x01(\x04R\x0cdurationSecs\x12!\n\x0c\
-    device_count\x18\x02\x20\x01(\x05R\x0bdeviceCount\x126\n\x17peak_concurr\
-    ent_devices\x18\x03\x20\x01(\x05R\x15peakConcurrentDevices\x12?\n\x0brad\
-    io_stats\x18\x04\x20\x03(\x0b2\x1e.netsim.stats.NetsimRadioStatsR\nradio\
-    Stats\x12\x18\n\x07version\x18\x05\x20\x01(\tR\x07version\
+    \x0bUNSUPPORTED\x10\x02\x12\n\n\x06OTHERS\x10\x03\x12\x0b\n\x07DELAYED\
+    \x10\x04\"\xb5\x03\n\x10NetsimRadioStats\x12\x1b\n\tdevice_id\x18\x01\
+    \x20\x01(\rR\x08deviceId\x127\n\x04kind\x18\x02\x20\x01(\x0e2#.netsim.st\
+    ats.NetsimRadioStats.KindR\x04kind\x12#\n\rduration_secs\x18\x03\x20\x01\
+    (\x04R\x0cdurationSecs\x12\x19\n\x08tx_count\x18\x04\x20\x01(\x05R\x07tx\
+    Count\x12\x19\n\x08rx_count\x18\x05\x20\x01(\x05R\x07rxCount\x12\x19\n\
+    \x08tx_bytes\x18\x06\x20\x01(\x05R\x07txBytes\x12\x19\n\x08rx_bytes\x18\
+    \x07\x20\x01(\x05R\x07rxBytes\x12D\n\x0finvalid_packets\x18\x08\x20\x03(\
+    \x0b2\x1b.netsim.stats.InvalidPacketR\x0einvalidPackets\"t\n\x04Kind\x12\
+    \x0f\n\x0bUNSPECIFIED\x10\0\x12\x18\n\x14BLUETOOTH_LOW_ENERGY\x10\x01\
+    \x12\x15\n\x11BLUETOOTH_CLASSIC\x10\x02\x12\x0e\n\nBLE_BEACON\x10\x03\
+    \x12\x08\n\x04WIFI\x10\x04\x12\x07\n\x03UWB\x10\x05\x12\x07\n\x03NFC\x10\
+    \x06\"\xea\x02\n\x13NetsimFrontendStats\x12\x1f\n\x0bget_version\x18\x01\
+    \x20\x01(\rR\ngetVersion\x12#\n\rcreate_device\x18\x02\x20\x01(\rR\x0ccr\
+    eateDevice\x12\x1f\n\x0bdelete_chip\x18\x03\x20\x01(\rR\ndeleteChip\x12!\
+    \n\x0cpatch_device\x18\x04\x20\x01(\rR\x0bpatchDevice\x12\x14\n\x05reset\
+    \x18\x05\x20\x01(\rR\x05reset\x12\x1f\n\x0blist_device\x18\x06\x20\x01(\
+    \rR\nlistDevice\x12)\n\x10subscribe_device\x18\x07\x20\x01(\rR\x0fsubscr\
+    ibeDevice\x12#\n\rpatch_capture\x18\x08\x20\x01(\rR\x0cpatchCapture\x12!\
+    \n\x0clist_capture\x18\t\x20\x01(\rR\x0blistCapture\x12\x1f\n\x0bget_cap\
+    ture\x18\n\x20\x01(\rR\ngetCapture\"\xb2\x02\n\x0bNetsimStats\x12#\n\rdu\
+    ration_secs\x18\x01\x20\x01(\x04R\x0cdurationSecs\x12!\n\x0cdevice_count\
+    \x18\x02\x20\x01(\x05R\x0bdeviceCount\x126\n\x17peak_concurrent_devices\
+    \x18\x03\x20\x01(\x05R\x15peakConcurrentDevices\x12?\n\x0bradio_stats\
+    \x18\x04\x20\x03(\x0b2\x1e.netsim.stats.NetsimRadioStatsR\nradioStats\
+    \x12\x18\n\x07version\x18\x05\x20\x01(\tR\x07version\x12H\n\x0efrontend_\
+    stats\x18\x06\x20\x01(\x0b2!.netsim.stats.NetsimFrontendStatsR\rfrontend\
+    Stats\
 ";
 
 /// `FileDescriptorProto` object which was a source for this generated file
@@ -1132,9 +1640,10 @@
     file_descriptor.get(|| {
         let generated_file_descriptor = generated_file_descriptor_lazy.get(|| {
             let mut deps = ::std::vec::Vec::with_capacity(0);
-            let mut messages = ::std::vec::Vec::with_capacity(3);
+            let mut messages = ::std::vec::Vec::with_capacity(4);
             messages.push(InvalidPacket::generated_message_descriptor_data());
             messages.push(NetsimRadioStats::generated_message_descriptor_data());
+            messages.push(NetsimFrontendStats::generated_message_descriptor_data());
             messages.push(NetsimStats::generated_message_descriptor_data());
             let mut enums = ::std::vec::Vec::with_capacity(2);
             enums.push(invalid_packet::Reason::generated_enum_descriptor_data());
diff --git a/scripts/build_tools.py b/scripts/build_tools.py
index ff7e478..3a36b29 100755
--- a/scripts/build_tools.py
+++ b/scripts/build_tools.py
@@ -106,6 +106,16 @@
           " emulator-linux_x64"
       ),
   )
+  parser.add_argument(
+      "--local_emulator_dir",
+      type=str,
+      default="",
+      help=(
+          "For providing an emulator build artifact in a directory."
+          " This will install the emulator from local_emulator_dir instead of"
+          " fetching the artifacts"
+      ),
+  )
 
   args = parser.parse_args()
 
diff --git a/scripts/tasks/install_emulator_task.py b/scripts/tasks/install_emulator_task.py
index 6df4904..e3a9c90 100644
--- a/scripts/tasks/install_emulator_task.py
+++ b/scripts/tasks/install_emulator_task.py
@@ -45,10 +45,12 @@
     self.out_dir = args.out_dir
     # Local fetching use only - default to emulator-linux_x64
     self.target = args.emulator_target
+    # Local Emulator directory
+    self.local_emulator_dir = args.local_emulator_dir
 
   def do_run(self):
     install_emulator_manager = InstallEmulatorManager(
-        self.buildbot, self.out_dir, self.target
+        self.buildbot, self.out_dir, self.target, self.local_emulator_dir
     )
     return install_emulator_manager.process()
 
@@ -67,7 +69,7 @@
       used for Android Build Bots.
   """
 
-  def __init__(self, buildbot, out_dir, target):
+  def __init__(self, buildbot, out_dir, target, local_emulator_dir):
     """Initializes the instances based on environment
 
     Args:
@@ -78,6 +80,7 @@
     self.buildbot = buildbot
     self.out_dir = out_dir
     self.target = target
+    self.local_emulator_dir = local_emulator_dir
 
   def __os_name_fetch(self):
     """Obtains the os substring of the emulator artifact"""
@@ -158,7 +161,7 @@
       os.remove(EMULATOR_ARTIFACT_PATH / file)
     return True
 
-  def __copy_artifacts(self):
+  def __copy_artifacts(self, emulator_filepath):
     """Copy artifacts into desired location
 
     In the local case, the emulator artifacts get copied into objs/
@@ -167,7 +170,6 @@
 
     Note that the downloaded netsim artifacts are removed before copying.
     """
-    emulator_filepath = EMULATOR_ARTIFACT_PATH / "emulator"
     # Remove all downloaded netsim artifacts
     files = glob.glob(str(emulator_filepath / "netsim*"))
     for fname in files:
@@ -213,30 +215,34 @@
     if not self.__prerequisites():
       return False
 
-    # Artifact fetching for local case
-    if not self.buildbot:
-      # Simulating the shell command
-      run(
-          [
-              "/google/data/ro/projects/android/fetch_artifact",
-              "--latest",
-              "--target",
-              self.target,
-              "--branch",
-              "aosp-emu-master-dev",
-              "sdk-repo-linux-emulator-*.zip",
-          ],
-          get_default_environment(AOSP_ROOT),
-          "install_emulator",
-          cwd=EMULATOR_ARTIFACT_PATH,
-      )
+    if self.local_emulator_dir:
+      # If local_emulator_dir is provided, copy the artifacts from this directory.
+      self.__copy_artifacts(Path(self.local_emulator_dir))
+    else:
+      # Artifact fetching for local case
+      if not self.buildbot:
+        # Simulating the shell command
+        run(
+            [
+                "/google/data/ro/projects/android/fetch_artifact",
+                "--latest",
+                "--target",
+                self.target,
+                "--branch",
+                "aosp-emu-master-dev",
+                "sdk-repo-linux-emulator-*.zip",
+            ],
+            get_default_environment(AOSP_ROOT),
+            "install_emulator",
+            cwd=EMULATOR_ARTIFACT_PATH,
+        )
 
-    # Unzipping emulator artifacts and remove zip files
-    if not self.__unzip_emulator_artifacts(os_name_artifact):
-      return False
+      # Unzipping emulator artifacts and remove zip files
+      if not self.__unzip_emulator_artifacts(os_name_artifact):
+        return False
 
-    # Copy artifacts after removing downloaded netsim artifacts
-    self.__copy_artifacts()
+      # Copy artifacts after removing downloaded netsim artifacts
+      self.__copy_artifacts(EMULATOR_ARTIFACT_PATH / "emulator")
 
     # Remove the EMULATOR_ARTIFACT_PATH in local case
     if not self.buildbot:
diff --git a/src/hci/hci_packet_transport.cc b/src/hci/hci_packet_transport.cc
index 81de642..277df91 100644
--- a/src/hci/hci_packet_transport.cc
+++ b/src/hci/hci_packet_transport.cc
@@ -21,6 +21,7 @@
 #include "model/hci/hci_transport.h"
 #include "netsim-daemon/src/ffi.rs.h"
 #include "netsim/hci_packet.pb.h"
+#include "netsim/stats.pb.h"
 #include "rust/cxx.h"
 #include "util/log.h"
 
@@ -92,9 +93,25 @@
   // HCIPacket_PacketType to rootcanal::PacketType is safe.
   rootcanal::PacketType rootcanal_packet_type =
       static_cast<rootcanal::PacketType>(packet_type);
-  mAsyncManager->Synchronize([this, rootcanal_packet_type, packet]() {
-    mPacketCallback(rootcanal_packet_type, packet);
-  });
+  auto beforeScheduleTime = std::chrono::steady_clock::now();
+  mAsyncManager->Synchronize(
+      [this, rootcanal_packet_type, packet, beforeScheduleTime]() {
+        auto elapsedTime =
+            std::chrono::duration_cast<std::chrono::milliseconds>(
+                std::chrono::steady_clock::now() - beforeScheduleTime)
+                .count();
+        // If the elapsed time of the packet delivery is greater than 5ms,
+        // report invalid packet with DELAYED reasoning.
+        if (elapsedTime > 5) {
+          netsim::hci::facade::ReportInvalidPacket(
+              this->rootcanalId.value(),
+              stats::InvalidPacket_Reason::InvalidPacket_Reason_DELAYED,
+              "Delayed packet with " + std::to_string(elapsedTime) +
+                  " milliseconds",
+              *packet);
+        }
+        mPacketCallback(rootcanal_packet_type, packet);
+      });
 }
 
 void HciPacketTransport::Add(