use std::ffi::{CStr, CString}; use libc::{c_char, c_int, c_uint}; use std::os::unix::io::AsRawFd; use std::process::exit; use libc::FILE; include!(concat!(env!("OUT_DIR"), "/bindings.rs")); pub struct Radio { ptr: *const radio_device_t } static mut RADIO_TABLE: [(&'static str, &'static radio_device_t); 16] = unsafe { [ ("DR780", &radio_md380), // TYT MD-380, Retevis RT3, RT8 ("MD390", &radio_md390), // TYT MD-390 ("MD-UV380", &radio_uv380), // TYT MD-UV380 ("MD-UV390", &radio_uv390), // TYT MD-UV390, Retevis RT3S ("2017", &radio_md2017), // TYT MD-2017, Retevis RT82 ("MD9600", &radio_md9600), // TYT MD-9600 ("BF-5R", &radio_rd5r), // Baofeng RD-5R, TD-5R ("1801", &radio_dm1801), // Baofeng DM-1801 ("DM-1701", &radio_rt84), // Baofeng DM-1701, Retevis RT84 ("MD-760P", &radio_gd77), // Radioddity GD-77, version 3.1.1 and later ("D868UVE", &radio_d868uv), // Anytone AT-D868UV ("D878UV", &radio_d878uv), // Anytone AT-D878UV ("D6X2UV", &radio_dmr6x2), // BTECH DMR-6x2 ("ZD3688", &radio_d900), // Zastone D900 ("TP660", &radio_dp880), // Zastone DP880 ("ZN><:", &radio_rt27d), // Radtel RT-27D ] }; // Radio memory contents, up to 2 Mbytes #[no_mangle] pub static mut radio_mem: [u8; 1024*1024*2] = [0; 1024*1024*2]; extern { fn dfu_init(vid: c_uint, pid: c_uint) -> *const c_char; fn dfu_reboot(); fn dfu_close(); fn hid_init(vid: c_int, pid: c_int) -> c_int; fn hid_identify() -> *const c_char; fn hid_close(); } static mut active_device: *const radio_device_t = std::ptr::null(); // Device-dependent interface unsafe fn get_active_device() -> *const radio_device_t { return active_device; } unsafe fn set_active_device(d: *const radio_device_t) { active_device = d; } /// Connect to the radio via the serial port. /// and identify the type of device. pub fn connect() -> Radio { let ident_str = (|| { let mut ident: *const c_char; // Try TYT MD family. ident = unsafe { dfu_init(0x0483, 0xdf11) }; if ident.is_null() { // Try RD-5R, DM-1801 and GD-77. if unsafe { hid_init(0x15a2, 0x0073) } >= 0 { ident = unsafe { hid_identify() }; } } if ident.is_null() { // Try AT-D868UV. let trace_flag = false; //TODO fix if let Some(device_path) = crate::serial::serial_init(0x28e9, 0x018a, trace_flag) { if let Some(identifier) = crate::serial::identify(&device_path) { unsafe { crate::serial::device_path = Some(device_path); } return identifier; } } } if ident.is_null() { eprintln!("No radio detected."); eprintln!("Check your USB cable!"); exit(-1); } unsafe { CStr::from_ptr(ident).to_str().unwrap().to_string() } })(); unsafe { let mut device: *const radio_device_t = std::ptr::null(); for (table_ident, device_ptr) in RADIO_TABLE.iter() { if ident_str == *table_ident { device = *device_ptr as *const radio_device_t; break; } } if device.is_null() { eprintln!("Unrecognized radio '{}'.", ident_str); exit(-1); } let name_ptr = (*device).name; let name = CStr::from_ptr(name_ptr).to_str().unwrap(); println!("Connect to {}", name); set_active_device(device); return Radio { ptr: device as *mut radio_device_t }; } } /// Close the serial port. pub fn disconnect() { eprintln!("Close device."); unsafe { // Restore the normal radio mode. dfu_reboot(); dfu_close(); hid_close(); crate::serial::close(); } } /// Read firmware image from the device pub fn download(radio: &Radio, trace: bool) { let device = radio.ptr as *mut radio_device_t; if !trace { eprint!("Read device: "); } unsafe { let download_fn = (*device).download.unwrap(); download_fn(device); } if !trace { eprintln!(" done."); } } /// Write firmware image to the device. pub fn upload(radio: &Radio, cont_flag: c_int, trace: bool) { let device = radio.ptr as *mut radio_device_t; unsafe { let is_compatible_fn = (*device).is_compatible.unwrap(); if is_compatible_fn(device) == 0 { eprintln!("Incompatible image - cannot upload."); exit(-1); } if !trace { eprint!("Write device: "); } let upload_fn = (*device).upload.unwrap(); upload_fn(device, cont_flag); if !trace { eprintln!(" done."); } } } /// List all supported radios. pub fn list() { println!("Supported radios:"); unsafe { for (_, device) in RADIO_TABLE.iter() { let name_ptr = (*device).name; let name = CStr::from_ptr(name_ptr).to_str().unwrap().to_string(); println!(" {}", name); } } } /// Check that the configuration is correct. pub fn verify_config(radio: &Radio) { let device = radio.ptr as *mut radio_device_t; unsafe { let verify_fn = (*device).verify_config.unwrap(); if verify_fn(device) == 0 { // Message should be already printed. exit(-1); } } } /// Read firmware image from the binary file. pub fn read_image(filename: &str) -> Radio { use std::io::{Seek, SeekFrom, Read}; fn read_header(file: &mut std::fs::File, filename: &str) -> Vec { let mut header_buf: [u8; 7] = [0; 7]; match file.read(&mut header_buf) { Ok(7) => (), Ok(_) => { eprintln!("{}: Cannot read header.", filename); exit(-1); }, Err(e) => { eprintln!("{}: Cannot read header. Error: {}", filename, e); exit(-1); } }; file.seek(SeekFrom::Start(0)).unwrap(); header_buf.to_vec() } eprintln!("Read codeplug from file '{}'.", filename); let mut img_file = std::fs::File::open(filename).unwrap(); let metadata = std::fs::metadata(filename).unwrap(); let file_size = metadata.len(); let device = unsafe {match file_size { 851968 | 852533 => { &radio_uv380 } 262144 | 262709 => { &radio_md380 } 1606528 => { let header = read_header(&mut img_file, filename); if header.as_slice().starts_with(b"D868UVE") { &radio_d868uv } else if header.as_slice().starts_with(b"D878UV") { &radio_d878uv } else if header.as_slice().starts_with(b"D6X2UV") { &radio_dmr6x2 } else { match std::str::from_utf8(&header) { Ok(s) => eprintln!("Unrecognized header: {}", s), Err(_) => eprintln!("Unrecognized header: {:?}", header), }; exit(-1) } } 131072 => { let header = read_header(&mut img_file, filename); if header.as_slice().starts_with(b"BF-5R") { &radio_rd5r } else if header.as_slice().starts_with(b"MD-760P") { &radio_gd77 } else if header.as_slice().starts_with(b"1801") { &radio_dm1801 } else if header.as_slice().starts_with(b"MD-760") { eprintln!("Old Radioddity GD-77 v2.6 image not supported!"); exit(-1) } else { match std::str::from_utf8(&header) { Ok(s) => eprintln!("Unrecognized header: {}", s), Err(_) => eprintln!("Unrecognized header: {:?}", header), }; exit(-1) } } size => { eprintln!("{}: Unrecognized file size {} bytes.", filename, size); exit(-1) } } }; let fd = img_file.as_raw_fd(); let mode = CString::new("rb").unwrap(); unsafe { let device = device as *const radio_device_t; let device = device as *mut radio_device_t; let file = libc::fdopen(fd, mode.as_ptr()); let read_image_fn = (*device).read_image.unwrap(); read_image_fn(device, file); libc::fclose(file); set_active_device(device); Radio { ptr: device } } } /// Save firmware image to the binary file. pub fn save_image(radio: &Radio, filename: &str) { eprintln!("Write codeplug to file '{}'.", filename); let device = radio.ptr as *mut radio_device_t; let file = std::fs::File::create(filename).unwrap(); let fd = file.as_raw_fd(); let mode = CString::new("wb").unwrap(); unsafe { let file = libc::fdopen(fd, mode.as_ptr()); let save_image_fn = (*device).save_image.unwrap(); save_image_fn(device, file); libc::fclose(file); } } /// Read the configuration from a text file, and modify the firmware. pub fn parse_config(radio: &Radio, filename: &str) { use std::io::{BufRead, BufReader}; let device = radio.ptr as *mut radio_device_t; let parse_header_fn = unsafe { (*device).parse_header.unwrap() }; let parse_parameter_fn = unsafe { (*device).parse_parameter.unwrap() }; let parse_row_fn = unsafe { (*device).parse_row.unwrap() }; let update_timestamp_fn = unsafe { (*device).update_timestamp.unwrap() }; eprintln!("Read configuration from file '{}'.", filename); let file = std::fs::File::open(filename).unwrap(); let file = BufReader::new(file); let mut table_id: c_int = 0; let mut table_dirty: c_int = 0; for line in file.lines() { let line = line.unwrap(); // Strip text after comment marker '#' let trimmed_line = line .split('#') .next() .unwrap() .trim_end(); // Skip comments and blank lines if trimmed_line.is_empty() { continue; } // Table row if trimmed_line.chars().nth(0) == Some(' ') { if table_id == 0 { eprintln!("Invalid line: '{}'", line); exit(-1); } let trimmed_more = CString::new(trimmed_line.trim_start()).unwrap(); let ptr = trimmed_more.as_ptr() as *mut c_char; let output = unsafe { parse_row_fn(device, table_id, !table_dirty, ptr) }; if output == 0 { eprintln!("Invalid line: '{}'", line); exit(-1); } table_dirty = 1; } else { // Table finished table_id = 0; // Find the value if let Some(colon_byte_idx) = line.find(':') { let (param_str, value_str) = line.split_at(colon_byte_idx); let stripped_value_str = value_str.strip_prefix(':').unwrap(); let param_c_str = CString::new(param_str.trim_start()).unwrap(); let param_ptr = param_c_str.as_ptr() as *mut c_char; let value_c_str = CString::new(stripped_value_str.trim_start()).unwrap(); let value_ptr = value_c_str.as_ptr() as *mut c_char; unsafe { parse_parameter_fn(device, param_ptr, value_ptr); } } else { // Table header let c_line = CString::new(trimmed_line).unwrap(); let ptr = c_line.as_ptr() as *mut c_char; table_id = unsafe { parse_header_fn(device, ptr) }; if table_id == 0 { eprintln!("Invalid line: '{}'", line); exit(-1); } table_dirty = 0; } } } unsafe { update_timestamp_fn(device); } } /// Print full information about the device configuration. /// If `filename` is `None`, write to stdout. pub fn print_config(radio: &Radio, filename: Option<&str>) { use std::io::Write; fn make_config_string() -> String { use chrono::Datelike; let date = chrono::offset::Local::today(); let y = date.year(); let m = date.month(); let d = date.day(); format!("#\n# Configuration generated {}/{}/{} by dmrconfig, version {}\n#\n", y, m, d, crate::version()) } let mut file; let (fd, verbose) = match filename { None => { unsafe { let verbose = libc::isatty(libc::STDOUT_FILENO) != 1; if verbose { print!("{}", make_config_string()); } (libc::STDOUT_FILENO, verbose) } }, Some(filename) => { file = std::fs::File::create(filename).unwrap(); file.write_all(make_config_string().as_bytes()).unwrap(); (file.as_raw_fd(), true) } }; let mode = CString::new("w").unwrap(); unsafe { let device = radio.ptr as *mut radio_device_t; let print_config_fn = (*device).print_config.unwrap(); let file = libc::fdopen(fd, mode.as_ptr()); print_config_fn(device, file, if verbose { 1 } else { 0 } ); libc::fclose(file); } } /// Print generic information about the device. pub fn print_version(radio: &Radio) { let device = radio.ptr as *mut radio_device_t; let mode = CString::new("w").unwrap(); unsafe { let print_version_fn = (*device).print_version.unwrap(); print_version_fn(device, libc::fdopen(libc::STDOUT_FILENO, mode.as_ptr())); } } /// Update contacts database on the device. pub fn write_csv(radio: &Radio, filename: &str) { let device = radio.ptr as *mut radio_device_t; unsafe { let write_csv_fn = match (*device).write_csv { Some(func) => func, None => { let name_ptr = (*device).name; let name = CStr::from_ptr(name_ptr).to_str().unwrap(); eprintln!("{} does not support CSV database.", name); exit(-1); } }; let file = std::fs::File::open(filename).unwrap(); eprintln!("Read file '{}.", filename); let fd = file.as_raw_fd(); let mode = CString::new("r").unwrap(); let file = libc::fdopen(fd, mode.as_ptr()); write_csv_fn(device, file); libc::fclose(file); } } /// Check for compatible radio model. #[no_mangle] pub extern "C" fn radio_is_compatible(name: *const c_char) -> c_int { unsafe { let name = CStr::from_ptr(name).to_str().unwrap(); let dev = get_active_device(); for (_, device) in RADIO_TABLE.iter() { // Radio is compatible when it has the same parse routine. let same_parse = (*dev).parse_parameter.unwrap() == (*device).parse_parameter.unwrap(); let name_ptr = (*device).name; let device_name = CStr::from_ptr(name_ptr).to_str().unwrap(); let same_name = name.eq_ignore_ascii_case(device_name); if same_parse && same_name { return 1; } } } return 0; }