mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-10 14:53:30 -07:00
uz801: Add initial (experimental) support
This commit is contained in:
@@ -6,6 +6,7 @@ pub mod tmobile;
|
||||
pub mod tplink;
|
||||
pub mod tplink_framebuffer;
|
||||
pub mod tplink_onebit;
|
||||
pub mod uz801;
|
||||
pub mod wingtech;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/// Display module for Uz801, light LEDs on the front of the device.
|
||||
/// DisplayState::Recording => Signal LED is solid blue (wifi LED).
|
||||
/// DisplayState::Paused => Green LED is solid.
|
||||
/// DisplayState::WarningDetected => Signal LED is solid red.
|
||||
use log::{error, info};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
|
||||
macro_rules! led {
|
||||
($l:expr) => {{ format!("/sys/class/leds/{}/brightness", $l) }};
|
||||
}
|
||||
|
||||
async fn led_on(path: String) {
|
||||
tokio::fs::write(&path, "1").await.ok();
|
||||
}
|
||||
|
||||
async fn led_off(path: String) {
|
||||
tokio::fs::write(&path, "0").await.ok();
|
||||
}
|
||||
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
mut ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
mut ui_update_rx: mpsc::Receiver<DisplayState>,
|
||||
) {
|
||||
let mut invisible: bool = false;
|
||||
if config.ui_level == 0 {
|
||||
info!("Invisible mode, not spawning UI.");
|
||||
invisible = true;
|
||||
}
|
||||
task_tracker.spawn(async move {
|
||||
let mut state = DisplayState::Recording;
|
||||
let mut last_state = DisplayState::Paused;
|
||||
|
||||
loop {
|
||||
match ui_shutdown_rx.try_recv() {
|
||||
Ok(_) => {
|
||||
info!("received UI shutdown");
|
||||
break;
|
||||
}
|
||||
Err(oneshot::error::TryRecvError::Empty) => {}
|
||||
Err(e) => panic!("error receiving shutdown message: {e}"),
|
||||
}
|
||||
match ui_update_rx.try_recv() {
|
||||
Ok(new_state) => state = new_state,
|
||||
Err(mpsc::error::TryRecvError::Empty) => {}
|
||||
Err(e) => error!("error receiving ui update message: {e}"),
|
||||
};
|
||||
if invisible || state == last_state {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
match state {
|
||||
DisplayState::Paused => {
|
||||
led_off(led!("red")).await;
|
||||
led_off(led!("green")).await;
|
||||
led_on(led!("wifi")).await;
|
||||
}
|
||||
DisplayState::Recording => {
|
||||
led_off(led!("red")).await;
|
||||
led_off(led!("wifi")).await;
|
||||
led_on(led!("green")).await;
|
||||
}
|
||||
DisplayState::WarningDetected => {
|
||||
led_off(led!("green")).await;
|
||||
led_off(led!("wifi")).await;
|
||||
led_on(led!("red")).await;
|
||||
}
|
||||
}
|
||||
last_state = state;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -243,6 +243,7 @@ async fn run_with_config(
|
||||
Device::Tmobile => display::tmobile::update_ui,
|
||||
Device::Wingtech => display::wingtech::update_ui,
|
||||
Device::Pinephone => display::headless::update_ui,
|
||||
Device::Uz801 => display::uz801::update_ui
|
||||
};
|
||||
update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
|
||||
|
||||
|
||||
+89
-30
@@ -7,7 +7,7 @@ use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use log::error;
|
||||
use rayhunter::util::RuntimeMetadata;
|
||||
use rayhunter::{Device, util::RuntimeMetadata};
|
||||
use serde::Serialize;
|
||||
use tokio::process::Command;
|
||||
|
||||
@@ -19,10 +19,10 @@ pub struct SystemStats {
|
||||
}
|
||||
|
||||
impl SystemStats {
|
||||
pub async fn new(qmdl_path: &str) -> Result<Self, String> {
|
||||
pub async fn new(qmdl_path: &str, device: &Device) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
disk_stats: DiskStats::new(qmdl_path).await?,
|
||||
memory_stats: MemoryStats::new().await?,
|
||||
disk_stats: DiskStats::new(qmdl_path, device).await?,
|
||||
memory_stats: MemoryStats::new(device).await?,
|
||||
runtime_metadata: RuntimeMetadata::new(),
|
||||
})
|
||||
}
|
||||
@@ -40,21 +40,46 @@ pub struct DiskStats {
|
||||
|
||||
impl DiskStats {
|
||||
// runs "df -h <qmdl_path>" to get storage statistics for the partition containing
|
||||
// the QMDL file
|
||||
pub async fn new(qmdl_path: &str) -> Result<Self, String> {
|
||||
// the QMDL file. The Uz801 device doesn't support the -h flag, so we skip it for that device.
|
||||
pub async fn new(qmdl_path: &str, device: &Device) -> Result<Self, String> {
|
||||
let mut df_cmd = Command::new("df");
|
||||
df_cmd.arg("-h");
|
||||
// Only add -h flag for devices other than Uz801, as Uz801's df doesn't support it
|
||||
if !matches!(device, Device::Uz801) {
|
||||
df_cmd.arg("-h");
|
||||
}
|
||||
df_cmd.arg(qmdl_path);
|
||||
let stdout = get_cmd_output(df_cmd).await?;
|
||||
let mut parts = stdout.split_whitespace().skip(7).to_owned();
|
||||
Ok(Self {
|
||||
partition: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
total_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
used_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
available_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
used_percent: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
mounted_on: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
})
|
||||
|
||||
if matches!(device, Device::Uz801) {
|
||||
// Handle Uz801 format:
|
||||
// Filesystem Size Used Free Blksize
|
||||
// /data/rayhunter/ 774.9M 68.0M 706.9M 4096
|
||||
let lines: Vec<&str> = stdout.lines().collect();
|
||||
if lines.len() < 2 {
|
||||
return Err("error parsing df output: insufficient lines".to_string());
|
||||
}
|
||||
let data_line = lines[1];
|
||||
let mut parts = data_line.split_whitespace();
|
||||
Ok(Self {
|
||||
partition: parts.next().ok_or("error parsing df output: missing filesystem")?.to_string(),
|
||||
total_size: parts.next().ok_or("error parsing df output: missing size")?.to_string(),
|
||||
used_size: parts.next().ok_or("error parsing df output: missing used")?.to_string(),
|
||||
available_size: parts.next().ok_or("error parsing df output: missing free")?.to_string(),
|
||||
used_percent: "N/A".to_string(), // Uz801 df doesn't provide percentage
|
||||
mounted_on: qmdl_path.to_string(), // Use the path we queried
|
||||
})
|
||||
} else {
|
||||
// Handle standard df -h format
|
||||
let mut parts = stdout.split_whitespace().skip(7);
|
||||
Ok(Self {
|
||||
partition: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
total_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
used_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
available_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
used_percent: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
mounted_on: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,19 +108,53 @@ async fn get_cmd_output(mut cmd: Command) -> Result<String, String> {
|
||||
}
|
||||
|
||||
impl MemoryStats {
|
||||
// runs "free -k" and parses the output to retrieve memory stats
|
||||
pub async fn new() -> Result<Self, String> {
|
||||
let mut free_cmd = Command::new("free");
|
||||
free_cmd.arg("-k");
|
||||
let stdout = get_cmd_output(free_cmd).await?;
|
||||
let mut numbers = stdout
|
||||
.split_whitespace()
|
||||
.flat_map(|part| part.parse::<usize>());
|
||||
Ok(Self {
|
||||
total: humanize_kb(numbers.next().ok_or("error parsing free output")?),
|
||||
used: humanize_kb(numbers.next().ok_or("error parsing free output")?),
|
||||
free: humanize_kb(numbers.next().ok_or("error parsing free output")?),
|
||||
})
|
||||
// runs "free -k" and parses the output to retrieve memory stats for most devices,
|
||||
// or reads /proc/meminfo for Uz801 which doesn't have the free command
|
||||
pub async fn new(device: &Device) -> Result<Self, String> {
|
||||
if matches!(device, Device::Uz801) {
|
||||
// Read /proc/meminfo for Uz801
|
||||
let meminfo_content = tokio::fs::read_to_string("/proc/meminfo")
|
||||
.await
|
||||
.map_err(|e| format!("error reading /proc/meminfo: {}", e))?;
|
||||
|
||||
let mut mem_total_kb = None;
|
||||
let mut mem_free_kb = None;
|
||||
|
||||
for line in meminfo_content.lines() {
|
||||
if let Some(value_str) = line.strip_prefix("MemTotal:") {
|
||||
if let Some(kb_str) = value_str.trim().strip_suffix(" kB") {
|
||||
mem_total_kb = kb_str.trim().parse::<usize>().ok();
|
||||
}
|
||||
} else if let Some(value_str) = line.strip_prefix("MemFree:") {
|
||||
if let Some(kb_str) = value_str.trim().strip_suffix(" kB") {
|
||||
mem_free_kb = kb_str.trim().parse::<usize>().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total = mem_total_kb.ok_or("error parsing MemTotal from /proc/meminfo")?;
|
||||
let free = mem_free_kb.ok_or("error parsing MemFree from /proc/meminfo")?;
|
||||
let used = total - free;
|
||||
|
||||
Ok(Self {
|
||||
total: humanize_kb(total),
|
||||
used: humanize_kb(used),
|
||||
free: humanize_kb(free),
|
||||
})
|
||||
} else {
|
||||
// Use free command for other devices
|
||||
let mut free_cmd = Command::new("free");
|
||||
free_cmd.arg("-k");
|
||||
let stdout = get_cmd_output(free_cmd).await?;
|
||||
let mut numbers = stdout
|
||||
.split_whitespace()
|
||||
.flat_map(|part| part.parse::<usize>());
|
||||
Ok(Self {
|
||||
total: humanize_kb(numbers.next().ok_or("error parsing free output")?),
|
||||
used: humanize_kb(numbers.next().ok_or("error parsing free output")?),
|
||||
free: humanize_kb(numbers.next().ok_or("error parsing free output")?),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +170,7 @@ pub async fn get_system_stats(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<Json<SystemStats>, (StatusCode, String)> {
|
||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||
match SystemStats::new(qmdl_store.path.to_str().unwrap()).await {
|
||||
match SystemStats::new(qmdl_store.path.to_str().unwrap(), &state.config.device).await {
|
||||
Ok(stats) => Ok(Json(stats)),
|
||||
Err(err) => {
|
||||
error!("error getting system stats: {err}");
|
||||
|
||||
@@ -6,6 +6,7 @@ mod orbic;
|
||||
mod pinephone;
|
||||
mod tmobile;
|
||||
mod tplink;
|
||||
mod uz801;
|
||||
mod util;
|
||||
mod wingtech;
|
||||
|
||||
@@ -27,6 +28,8 @@ enum Command {
|
||||
Orbic(InstallOrbic),
|
||||
/// Install rayhunter on the TMobile TMOHS1.
|
||||
Tmobile(TmobileArgs),
|
||||
/// Install rayhunter on the Uz801.
|
||||
Uz801(Uz801Args),
|
||||
/// Install rayhunter on a PinePhone's Quectel modem.
|
||||
Pinephone(InstallPinephone),
|
||||
/// Install rayhunter on the TP-Link M7350.
|
||||
@@ -82,6 +85,8 @@ enum UtilSubCommand {
|
||||
TmobileStartAdb(TmobileArgs),
|
||||
/// Root the Tmobile and launch telnetd.
|
||||
TmobileStartTelnet(TmobileArgs),
|
||||
/// Root the Uz801 and launch adb.
|
||||
Uz801StartAdb(Uz801Args),
|
||||
/// Root the tplink and launch telnetd.
|
||||
TplinkStartTelnet(TplinkStartTelnet),
|
||||
/// Root the Wingtech and launch telnetd.
|
||||
@@ -115,6 +120,17 @@ struct TmobileArgs {
|
||||
admin_password: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Uz801Args {
|
||||
/// IP address for Uz801 admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Web portal admin password.
|
||||
#[arg(long)]
|
||||
admin_password: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct TplinkStartTelnet {
|
||||
/// IP address for TP-Link admin interface, if custom.
|
||||
@@ -168,6 +184,7 @@ async fn run() -> Result<(), Error> {
|
||||
|
||||
match command {
|
||||
Command::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||
Command::Uz801(args) => uz801::install(args).await.context("Failed to install rayhunter on the Uz801. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||
Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||
Command::Pinephone(_) => pinephone::install().await
|
||||
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
|
||||
@@ -195,6 +212,7 @@ async fn run() -> Result<(), Error> {
|
||||
UtilSubCommand::Shell => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
|
||||
UtilSubCommand::TmobileStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Tmobile TMOHS1")?,
|
||||
UtilSubCommand::TmobileStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Tmobile TMOHS1")?,
|
||||
UtilSubCommand::Uz801StartAdb(args) => uz801::activate_usb_debug(&args.admin_ip).await.context("\nFailed to activate USB debug on the Uz801")?,
|
||||
UtilSubCommand::TplinkStartTelnet(options) => {
|
||||
tplink::start_telnet(&options.admin_ip).await?;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/// Installer for the Uz801 hotspot.
|
||||
///
|
||||
/// Installation process:
|
||||
/// 1. Use curl to activate USB debugging backdoor
|
||||
/// 2. Wait for device reboot and ADB availability
|
||||
/// 3. Use ADB to install rayhunter files
|
||||
/// 4. Modify startup script to launch rayhunter on boot
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::time::sleep;
|
||||
use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError};
|
||||
use std::io::ErrorKind;
|
||||
|
||||
use crate::Uz801Args as Args;
|
||||
use crate::util::echo;
|
||||
|
||||
pub async fn install(
|
||||
Args {
|
||||
admin_ip,
|
||||
admin_password: _, // Not used for Uz801
|
||||
}: Args,
|
||||
) -> Result<()> {
|
||||
run_install(admin_ip).await
|
||||
}
|
||||
|
||||
async fn run_install(admin_ip: String) -> Result<()> {
|
||||
echo!("Activating USB debugging backdoor... ");
|
||||
activate_usb_debug(&admin_ip).await?;
|
||||
println!("ok");
|
||||
|
||||
echo!("Waiting for device reboot and ADB connection... ");
|
||||
let mut adb_device = wait_for_adb().await?;
|
||||
println!("ok");
|
||||
|
||||
echo!("Installing rayhunter files... ");
|
||||
install_rayhunter_files(&mut adb_device).await?;
|
||||
println!("ok");
|
||||
|
||||
echo!("Modifying startup script... ");
|
||||
modify_startup_script(&mut adb_device).await?;
|
||||
println!("ok");
|
||||
|
||||
echo!("Starting rayhunter daemon... ");
|
||||
start_rayhunter(&mut adb_device).await?;
|
||||
println!("ok");
|
||||
|
||||
echo!("Testing rayhunter... ");
|
||||
test_rayhunter(&admin_ip).await?;
|
||||
println!("ok");
|
||||
println!("rayhunter is running at http://{admin_ip}:8080");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> {
|
||||
let url = format!("http://{}/usbdebug.html", admin_ip);
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.get(&url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("Failed to activate USB debug: HTTP {}", response.status());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_adb() -> Result<ADBUSBDevice> {
|
||||
const MAX_ATTEMPTS: u32 = 30; // 30 seconds
|
||||
let mut attempts = 0;
|
||||
|
||||
// Wait a bit for the reboot to start
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
|
||||
loop {
|
||||
if attempts >= MAX_ATTEMPTS {
|
||||
anyhow::bail!("Timeout waiting for ADB connection after USB debug activation");
|
||||
}
|
||||
|
||||
match ADBUSBDevice::new(0x05c6, 0x9025) { // Common Qualcomm ADB VID/PID
|
||||
Ok(mut device) => {
|
||||
// Test ADB connection
|
||||
if test_adb_connection(&mut device).await.is_ok() {
|
||||
return Ok(device);
|
||||
}
|
||||
}
|
||||
Err(RustADBError::DeviceNotFound(_)) => {
|
||||
// Device not ready yet, continue waiting
|
||||
}
|
||||
Err(e) => {
|
||||
anyhow::bail!("ADB connection error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
attempts += 1;
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_adb_connection(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
let mut buf = Vec::<u8>::new();
|
||||
adb_device.shell_command(&["echo", "test"], &mut buf)?;
|
||||
let output = String::from_utf8_lossy(&buf);
|
||||
if output.contains("test") {
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!("ADB connection test failed")
|
||||
}
|
||||
}
|
||||
|
||||
async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
// Create rayhunter directory
|
||||
let mut buf = Vec::<u8>::new();
|
||||
adb_device.shell_command(&["mkdir", "-p", "/data/rayhunter"], &mut buf)?;
|
||||
|
||||
// Install rayhunter daemon binary
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
let mut daemon_data = rayhunter_daemon_bin.as_slice();
|
||||
adb_device.push(&mut daemon_data, "/data/rayhunter/rayhunter-daemon")?;
|
||||
|
||||
// Install config file
|
||||
let config_content = crate::CONFIG_TOML
|
||||
.replace("#device = \"orbic\"", "device = \"uz801\"");
|
||||
let mut config_data = config_content.as_bytes();
|
||||
adb_device.push(&mut config_data, "/data/rayhunter/config.toml")?;
|
||||
|
||||
// Make daemon executable
|
||||
let mut buf = Vec::<u8>::new();
|
||||
adb_device.shell_command(&["chmod", "755", "/data/rayhunter/rayhunter-daemon"], &mut buf)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn modify_startup_script(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
// Pull the existing startup script
|
||||
let mut script_content = Vec::<u8>::new();
|
||||
adb_device.pull("/system/bin/initmifiservice.sh", &mut script_content)?;
|
||||
|
||||
// Convert to string and add our line
|
||||
let mut script_str = String::from_utf8_lossy(&script_content).into_owned();
|
||||
|
||||
// Add rayhunter startup line if not already present
|
||||
let rayhunter_line = "/data/rayhunter/rayhunter-daemon /data/rayhunter/config.toml &\n";
|
||||
if !script_str.contains("/data/rayhunter/rayhunter-daemon") {
|
||||
script_str.push_str(rayhunter_line);
|
||||
}
|
||||
|
||||
// Push the modified script back
|
||||
let mut modified_script = script_str.as_bytes();
|
||||
adb_device.push(&mut modified_script, "/system/bin/initmifiservice.sh")?;
|
||||
|
||||
// Make sure it's executable
|
||||
let mut buf = Vec::<u8>::new();
|
||||
adb_device.shell_command(&["chmod", "755", "/system/bin/initmifiservice.sh"], &mut buf)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
let mut buf = Vec::<u8>::new();
|
||||
adb_device.shell_command(&[
|
||||
"/data/rayhunter/rayhunter-daemon",
|
||||
"/data/rayhunter/config.toml",
|
||||
"&"
|
||||
], &mut buf)?;
|
||||
|
||||
// Give it a moment to start
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_rayhunter(admin_ip: &str) -> Result<()> {
|
||||
const MAX_FAILURES: u32 = 10;
|
||||
let mut failures = 0;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
while failures < MAX_FAILURES {
|
||||
let url = format!("http://{}:8080/index.html", admin_ip);
|
||||
|
||||
if let Ok(response) = client.get(&url).send().await {
|
||||
if response.status().is_success() {
|
||||
if let Ok(body) = response.text().await {
|
||||
if body.contains("html") {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
failures += 1;
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
|
||||
anyhow::bail!("timeout reached! failed to reach rayhunter, something went wrong :(")
|
||||
}
|
||||
@@ -25,4 +25,5 @@ pub enum Device {
|
||||
Tmobile,
|
||||
Wingtech,
|
||||
Pinephone,
|
||||
Uz801
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user