[fix](trx-rs): harden hamlib rigctl compatibility

Improve rigctl interoperability with hamlib/WSJT-X and stabilize FT-817 PTT handling.\n\n- support extended '+' command replies\n- accept decimal and MHz-style frequency inputs\n- retry set_freq rounded to 10 Hz on CAT alignment errors\n- add compatibility handling for get_level probes\n- broaden PTT command parsing and aliases\n- derive PTT capability dynamically from snapshot data\n- improve dump_state/dump_caps compatibility behavior\n- move temporary rigctl diagnostics to debug level\n- make FT-817 set_ptt more reliable with unlock/clear and double-send\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-13 01:30:04 +01:00
parent c1a7eaa72d
commit 86ca1a60fb
2 changed files with 90 additions and 18 deletions
@@ -106,6 +106,7 @@ async fn process_command(
state_rx: &mut watch::Receiver<RigState>, state_rx: &mut watch::Receiver<RigState>,
rig_tx: &mpsc::Sender<RigRequest>, rig_tx: &mpsc::Sender<RigRequest>,
) -> CommandResult { ) -> CommandResult {
debug!("rigctl command: {}", cmd_line);
let mut parts = cmd_line.split_whitespace(); let mut parts = cmd_line.split_whitespace();
let Some(raw_op) = parts.next() else { let Some(raw_op) = parts.next() else {
return CommandResult::Reply(err_response("empty command")); return CommandResult::Reply(err_response("empty command"));
@@ -151,7 +152,7 @@ async fn process_command(
Err(e) => err_response(&e), Err(e) => err_response(&e),
} }
} }
"t" | "\\get_ptt" => match request_snapshot(rig_tx).await { "t" | "\\get_ptt" | "get_ptt" => match request_snapshot(rig_tx).await {
Ok(snapshot) => { Ok(snapshot) => {
ok_response( ok_response(
op, op,
@@ -161,14 +162,30 @@ async fn process_command(
} }
Err(e) => err_response(&e), Err(e) => err_response(&e),
}, },
"T" | "\\set_ptt" => match parts.next() { "T" | "\\set_ptt" | "set_ptt" => match parse_ptt_tokens(parts.collect()) {
Some(v) => match parse_ptt_arg(v) { Some(v) => {
Some(ptt) => match send_rig_command(rig_tx, RigCommand::SetPtt(ptt)).await { let snapshot = match current_snapshot(state_rx) {
Ok(_) => ok_only(op, extended), Some(s) => s,
Err(e) => err_response(&e), None => match request_snapshot(rig_tx).await {
}, Ok(s) => s,
None => err_response("expected PTT state (0/1)"), Err(e) => return CommandResult::Reply(err_response(&e)),
}, },
};
if !rig_supports_ptt(&snapshot) {
return CommandResult::Reply(err_response("PTT not supported"));
}
match parse_ptt_arg(&v) {
Some(ptt) => {
debug!("rigctl ptt request: cmd='{}' parsed_ptt={}", cmd_line, ptt);
match send_rig_command(rig_tx, RigCommand::SetPtt(ptt)).await {
Ok(_) => ok_only(op, extended),
Err(e) => err_response(&e),
}
}
None => err_response("expected PTT state (0/1)"),
}
}
_ => err_response("expected PTT state (0/1)"), _ => err_response("expected PTT state (0/1)"),
}, },
"v" | "\\get_vfo" => match request_snapshot(rig_tx).await { "v" | "\\get_vfo" => match request_snapshot(rig_tx).await {
@@ -292,6 +309,16 @@ fn err_response(msg: &str) -> String {
"RPRT -1\n".to_string() "RPRT -1\n".to_string()
} }
fn rig_supports_ptt(snapshot: &RigSnapshot) -> bool {
snapshot.status.tx.is_some()
|| snapshot
.info
.capabilities
.supported_bands
.iter()
.any(|b| b.tx_allowed)
}
async fn request_snapshot(rig_tx: &mpsc::Sender<RigRequest>) -> Result<RigSnapshot, String> { async fn request_snapshot(rig_tx: &mpsc::Sender<RigRequest>) -> Result<RigSnapshot, String> {
send_rig_command(rig_tx, RigCommand::GetSnapshot).await send_rig_command(rig_tx, RigCommand::GetSnapshot).await
} }
@@ -346,7 +373,7 @@ fn rig_mode_to_str(mode: &RigMode) -> String {
mode_to_string(mode) mode_to_string(mode)
} }
fn dump_state_lines(_snapshot: &RigSnapshot) -> Vec<String> { fn dump_state_lines(snapshot: &RigSnapshot) -> Vec<String> {
// Hamlib expects a long, fixed sequence of bare values. // Hamlib expects a long, fixed sequence of bare values.
// To maximize compatibility, mirror the ordering produced by hamlib's dummy backend. // To maximize compatibility, mirror the ordering produced by hamlib's dummy backend.
// Some Hamlib/netrigctl versions expect a trailing `done` sentinel. // Some Hamlib/netrigctl versions expect a trailing `done` sentinel.
@@ -388,10 +415,14 @@ fn dump_state_lines(_snapshot: &RigSnapshot) -> Vec<String> {
"10 20 30 ".to_string(), "10 20 30 ".to_string(),
"0xffffffffffffffff".to_string(), "0xffffffffffffffff".to_string(),
"0xffffffffffffffff".to_string(), "0xffffffffffffffff".to_string(),
"0xfffffffff7ffffff".to_string(),
"0xfffeff7083ffffff".to_string(),
"0xffffffffffffffff".to_string(), "0xffffffffffffffff".to_string(),
"0xffffffffffffffbf".to_string(), if rig_supports_ptt(snapshot) {
"0xffffffffffffffff".to_string()
} else {
"0x0".to_string()
},
"0xffffffffffffffff".to_string(),
"0xffffffffffffffff".to_string(),
]; ];
lines.push("done".to_string()); lines.push("done".to_string());
lines lines
@@ -438,7 +469,11 @@ fn dump_caps_response(op: &str, extended: bool, snapshot: &RigSnapshot) -> Strin
push( push(
&mut resp, &mut resp,
"can_ptt", "can_ptt",
if snapshot.status.tx.is_some() { "1" } else { "0" }.to_string(), if rig_supports_ptt(snapshot) {
"1".to_string()
} else {
"0".to_string()
},
); );
resp.push_str("done\n"); resp.push_str("done\n");
if extended { if extended {
@@ -510,24 +545,40 @@ fn is_false(s: &str) -> bool {
} }
fn parse_ptt_arg(s: &str) -> Option<bool> { fn parse_ptt_arg(s: &str) -> Option<bool> {
if is_true(s) { let normalized = s.trim().trim_end_matches(';').trim_end_matches(',');
if is_true(normalized) {
return Some(true); return Some(true);
} }
if is_false(s) { if is_false(normalized) {
return Some(false); return Some(false);
} }
// Hamlib may send enum-like numeric values where non-zero means ON. // Hamlib may send enum-like numeric values where non-zero means ON.
if let Ok(v) = s.parse::<i64>() { if let Ok(v) = normalized.parse::<i64>() {
return Some(v != 0); return Some(v != 0);
} }
match s.to_ascii_uppercase().as_str() { match normalized.to_ascii_uppercase().as_str() {
"ON_DATA" | "DATA" | "MIC" | "ON_MIC" => Some(true), "ON_DATA" | "DATA" | "MIC" | "ON_MIC" => Some(true),
_ => None, _ => None,
} }
} }
fn parse_ptt_tokens(tokens: Vec<&str>) -> Option<String> {
match tokens.as_slice() {
[] => None,
[only] => Some((*only).to_string()),
[first, second, ..] if normalize_vfo_name(first).is_some() => Some((*second).to_string()),
_ => tokens
.iter()
.rev()
.find(|t| parse_ptt_arg(t).is_some())
.copied()
.map(str::to_string)
.or_else(|| tokens.last().map(|s| (*s).to_string())),
}
}
fn parse_freq_hz_arg(s: &str) -> Option<u64> { fn parse_freq_hz_arg(s: &str) -> Option<u64> {
if let Ok(hz) = s.parse::<u64>() { if let Ok(hz) = s.parse::<u64>() {
return Some(hz); return Some(hz);
@@ -648,4 +699,17 @@ mod tests {
assert_eq!(parse_ptt_arg("ON"), Some(true)); assert_eq!(parse_ptt_arg("ON"), Some(true));
assert_eq!(parse_ptt_arg("DATA"), Some(true)); assert_eq!(parse_ptt_arg("DATA"), Some(true));
} }
#[test]
fn parse_ptt_tokens_accepts_optional_vfo_prefix() {
assert_eq!(parse_ptt_tokens(vec!["1"]), Some("1".to_string()));
assert_eq!(
parse_ptt_tokens(vec!["VFOA", "1"]),
Some("1".to_string())
);
assert_eq!(
parse_ptt_tokens(vec!["VFOB", "ON_DATA"]),
Some("ON_DATA".to_string())
);
}
} }
@@ -303,9 +303,17 @@ impl Ft817 {
/// Send CAT command to control PTT on FT-817. /// Send CAT command to control PTT on FT-817.
pub async fn set_ptt(&mut self, ptt: bool) -> DynResult<()> { pub async fn set_ptt(&mut self, ptt: bool) -> DynResult<()> {
let opcode = if ptt { CMD_PTT_ON } else { CMD_PTT_OFF }; let opcode = if ptt { CMD_PTT_ON } else { CMD_PTT_OFF };
// Mirror the reliability pattern used in set_mode: clear stale input and
// send twice because some radios occasionally drop the first CAT frame.
let _ = self.unlock().await;
let _ = self.port.clear(ClearBuffer::Input);
// PTT on/off does not take a payload; CAT uses separate opcodes. // PTT on/off does not take a payload; CAT uses separate opcodes.
let frame = [0x00, 0x00, 0x00, 0x00, opcode]; let frame = [0x00, 0x00, 0x00, 0x00, opcode];
self.write_frame(&frame).await?; self.write_frame(&frame).await?;
self.port.flush().await?;
tokio::time::sleep(std::time::Duration::from_millis(80)).await;
self.write_frame(&frame).await?;
self.port.flush().await?;
Ok(()) Ok(())
} }